blob: cd4165607d767a46015d3ed12cf40fe246b04617 [file] [log] [blame]
Brian Osman7c979f52019-02-12 13:27:51 -05001/*
2* Copyright 2019 Google LLC
3*
4* Use of this source code is governed by a BSD-style license that can be
5* found in the LICENSE file.
6*/
7
8#include "ParticlesSlide.h"
9
Mike Kleincd5104e2019-03-20 11:55:08 -050010#include "AnimTimer.h"
Brian Osman112aa2d2019-02-15 10:45:56 -050011#include "ImGuiLayer.h"
Mike Reed58a75382019-03-02 17:57:20 -050012#include "Resources.h"
Brian Osmanb77d5022019-03-06 11:08:48 -050013#include "SkOSFile.h"
14#include "SkOSPath.h"
Brian Osman7c979f52019-02-12 13:27:51 -050015#include "SkParticleAffector.h"
Brian Osman543d2e22019-02-15 14:29:38 -050016#include "SkParticleDrawable.h"
Brian Osman7c979f52019-02-12 13:27:51 -050017#include "SkParticleEffect.h"
Brian Osman7c979f52019-02-12 13:27:51 -050018#include "SkParticleSerialization.h"
19#include "SkReflected.h"
20
21#include "imgui.h"
22
23using namespace sk_app;
24
25namespace {
26
27static SkScalar kDragSize = 8.0f;
28static SkTArray<SkPoint*> gDragPoints;
29int gDragIndex = -1;
30
31}
32
33///////////////////////////////////////////////////////////////////////////////
34
35static int InputTextCallback(ImGuiInputTextCallbackData* data) {
36 if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
37 SkString* s = (SkString*)data->UserData;
38 SkASSERT(data->Buf == s->writable_str());
39 SkString tmp(data->Buf, data->BufTextLen);
40 s->swap(tmp);
41 data->Buf = s->writable_str();
42 }
43 return 0;
44}
45
Brian Osman7c979f52019-02-12 13:27:51 -050046class SkGuiVisitor : public SkFieldVisitor {
47public:
48 SkGuiVisitor() {
49 fTreeStack.push_back(true);
50 }
51
52#define IF_OPEN(WIDGET) if (fTreeStack.back()) { WIDGET; }
53
Brian Osman2991cbe2019-02-19 10:45:56 -050054 void visit(const char* name, float& f) override {
55 IF_OPEN(ImGui::DragFloat(item(name), &f))
Brian Osman7c979f52019-02-12 13:27:51 -050056 }
Brian Osman2991cbe2019-02-19 10:45:56 -050057 void visit(const char* name, int& i) override {
Brian Osman5de7ea42019-02-14 13:23:51 -050058 IF_OPEN(ImGui::DragInt(item(name), &i))
Brian Osman7c979f52019-02-12 13:27:51 -050059 }
Brian Osman2991cbe2019-02-19 10:45:56 -050060 void visit(const char* name, bool& b) override {
Brian Osman5de7ea42019-02-14 13:23:51 -050061 IF_OPEN(ImGui::Checkbox(item(name), &b))
Brian Osman7c979f52019-02-12 13:27:51 -050062 }
Brian Osman2991cbe2019-02-19 10:45:56 -050063 void visit(const char* name, SkString& s) override {
Brian Osman7c979f52019-02-12 13:27:51 -050064 if (fTreeStack.back()) {
65 ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize;
Brian Osman5de7ea42019-02-14 13:23:51 -050066 ImGui::InputText(item(name), s.writable_str(), s.size() + 1, flags, InputTextCallback,
67 &s);
Brian Osman7c979f52019-02-12 13:27:51 -050068 }
69 }
Brian Osmane5d532e2019-02-26 14:58:40 -050070 void visit(const char* name, int& i, const EnumStringMapping* map, int count) override {
71 if (fTreeStack.back()) {
72 const char* curStr = EnumToString(i, map, count);
73 if (ImGui::BeginCombo(item(name), curStr ? curStr : "Unknown")) {
74 for (int j = 0; j < count; ++j) {
75 if (ImGui::Selectable(map[j].fName, i == map[j].fValue)) {
76 i = map[j].fValue;
77 }
78 }
79 ImGui::EndCombo();
80 }
81 }
82 }
Brian Osman7c979f52019-02-12 13:27:51 -050083
Brian Osman2991cbe2019-02-19 10:45:56 -050084 void visit(const char* name, SkPoint& p) override {
Brian Osman7c979f52019-02-12 13:27:51 -050085 if (fTreeStack.back()) {
Brian Osman5de7ea42019-02-14 13:23:51 -050086 ImGui::DragFloat2(item(name), &p.fX);
Brian Osman7c979f52019-02-12 13:27:51 -050087 gDragPoints.push_back(&p);
88 }
89 }
Brian Osman2991cbe2019-02-19 10:45:56 -050090 void visit(const char* name, SkColor4f& c) override {
Brian Osman5de7ea42019-02-14 13:23:51 -050091 IF_OPEN(ImGui::ColorEdit4(item(name), c.vec()))
Brian Osman7c979f52019-02-12 13:27:51 -050092 }
93
Brian Osman5de7ea42019-02-14 13:23:51 -050094#undef IF_OPEN
95
Brian Osman7c979f52019-02-12 13:27:51 -050096 void visit(sk_sp<SkReflected>& e, const SkReflected::Type* baseType) override {
97 if (fTreeStack.back()) {
98 const SkReflected::Type* curType = e ? e->getType() : nullptr;
99 if (ImGui::BeginCombo("Type", curType ? curType->fName : "Null")) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500100 auto visitType = [baseType, curType, &e](const SkReflected::Type* t) {
101 if (t->fFactory && (t == baseType || t->isDerivedFrom(baseType)) &&
102 ImGui::Selectable(t->fName, curType == t)) {
Brian Osman7c979f52019-02-12 13:27:51 -0500103 e = t->fFactory();
104 }
105 };
Brian Osmanb77d5022019-03-06 11:08:48 -0500106 SkReflected::VisitTypes(visitType);
Brian Osman7c979f52019-02-12 13:27:51 -0500107 ImGui::EndCombo();
108 }
109 }
110 }
111
112 void enterObject(const char* name) override {
113 if (fTreeStack.back()) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500114 fTreeStack.push_back(ImGui::TreeNodeEx(item(name),
115 ImGuiTreeNodeFlags_AllowItemOverlap));
Brian Osman7c979f52019-02-12 13:27:51 -0500116 } else {
117 fTreeStack.push_back(false);
118 }
119 }
120 void exitObject() override {
121 if (fTreeStack.back()) {
122 ImGui::TreePop();
123 }
124 fTreeStack.pop_back();
125 }
126
Brian Osman5de7ea42019-02-14 13:23:51 -0500127 int enterArray(const char* name, int oldCount) override {
128 this->enterObject(item(name));
129 fArrayCounterStack.push_back(0);
130 fArrayEditStack.push_back();
Brian Osman7c979f52019-02-12 13:27:51 -0500131
Brian Osman5de7ea42019-02-14 13:23:51 -0500132 int count = oldCount;
Brian Osman7c979f52019-02-12 13:27:51 -0500133 if (fTreeStack.back()) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500134 ImGui::SameLine();
Brian Osman7c979f52019-02-12 13:27:51 -0500135 if (ImGui::Button("+")) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500136 ++count;
Brian Osman7c979f52019-02-12 13:27:51 -0500137 }
138 }
Brian Osman5de7ea42019-02-14 13:23:51 -0500139 return count;
140 }
141 ArrayEdit exitArray() override {
142 fArrayCounterStack.pop_back();
143 auto edit = fArrayEditStack.back();
144 fArrayEditStack.pop_back();
Brian Osman7c979f52019-02-12 13:27:51 -0500145 this->exitObject();
Brian Osman5de7ea42019-02-14 13:23:51 -0500146 return edit;
Brian Osman7c979f52019-02-12 13:27:51 -0500147 }
148
149private:
Brian Osman5de7ea42019-02-14 13:23:51 -0500150 const char* item(const char* name) {
151 if (name) {
152 return name;
153 }
154
155 // We're in an array. Add extra controls and a dynamic label.
156 int index = fArrayCounterStack.back()++;
157 ArrayEdit& edit(fArrayEditStack.back());
158 fScratchLabel = SkStringPrintf("[%d]", index);
159
160 ImGui::PushID(index);
161
162 if (ImGui::Button("X")) {
163 edit.fVerb = ArrayEdit::Verb::kRemove;
164 edit.fIndex = index;
165 }
166 ImGui::SameLine();
167 if (ImGui::Button("^")) {
168 edit.fVerb = ArrayEdit::Verb::kMoveForward;
169 edit.fIndex = index;
170 }
171 ImGui::SameLine();
172 if (ImGui::Button("v")) {
173 edit.fVerb = ArrayEdit::Verb::kMoveForward;
174 edit.fIndex = index + 1;
175 }
176 ImGui::SameLine();
177
178 ImGui::PopID();
179
180 return fScratchLabel.c_str();
181 }
182
Brian Osman7c979f52019-02-12 13:27:51 -0500183 SkSTArray<16, bool, true> fTreeStack;
Brian Osman5de7ea42019-02-14 13:23:51 -0500184 SkSTArray<16, int, true> fArrayCounterStack;
185 SkSTArray<16, ArrayEdit, true> fArrayEditStack;
186 SkString fScratchLabel;
Brian Osman7c979f52019-02-12 13:27:51 -0500187};
188
Brian Osman7c979f52019-02-12 13:27:51 -0500189ParticlesSlide::ParticlesSlide() {
190 // Register types for serialization
191 REGISTER_REFLECTED(SkReflected);
192 SkParticleAffector::RegisterAffectorTypes();
Brian Osman543d2e22019-02-15 14:29:38 -0500193 SkParticleDrawable::RegisterDrawableTypes();
Brian Osman7c979f52019-02-12 13:27:51 -0500194 fName = "Particles";
Brian Osmanb77d5022019-03-06 11:08:48 -0500195 fPlayPosition.set(200.0f, 200.0f);
196}
197
198void ParticlesSlide::loadEffects(const char* dirname) {
199 fLoaded.reset();
200 fRunning.reset();
201 SkOSFile::Iter iter(dirname, ".json");
202 for (SkString file; iter.next(&file); ) {
203 LoadedEffect effect;
204 effect.fName = SkOSPath::Join(dirname, file.c_str());
205 effect.fParams.reset(new SkParticleEffectParams());
206 if (auto fileData = SkData::MakeFromFileName(effect.fName.c_str())) {
207 skjson::DOM dom(static_cast<const char*>(fileData->data()), fileData->size());
208 SkFromJsonVisitor fromJson(dom.root());
209 effect.fParams->visitFields(&fromJson);
210 fLoaded.push_back(effect);
211 }
212 }
Ben Wagnera57d8682019-02-14 13:22:43 -0500213}
214
215void ParticlesSlide::load(SkScalar winWidth, SkScalar winHeight) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500216 this->loadEffects(GetResourcePath("particles").c_str());
Brian Osman7c979f52019-02-12 13:27:51 -0500217}
218
219void ParticlesSlide::draw(SkCanvas* canvas) {
220 canvas->clear(0);
221
222 gDragPoints.reset();
Brian Osmanb77d5022019-03-06 11:08:48 -0500223 gDragPoints.push_back(&fPlayPosition);
224
225 // Window to show all loaded effects, and allow playing them
226 if (ImGui::Begin("Library", nullptr, ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
Brian Osman5c1f8eb2019-02-14 14:49:55 -0500227 static bool looped = true;
228 ImGui::Checkbox("Looped", &looped);
Brian Osmanb77d5022019-03-06 11:08:48 -0500229
230 static SkString dirname = GetResourcePath("particles");
231 ImGuiInputTextFlags textFlags = ImGuiInputTextFlags_CallbackResize;
232 ImGui::InputText("Directory", dirname.writable_str(), dirname.size() + 1, textFlags,
233 InputTextCallback, &dirname);
234
235 if (ImGui::Button("New")) {
236 LoadedEffect effect;
237 effect.fName = SkOSPath::Join(dirname.c_str(), "new.json");
238 effect.fParams.reset(new SkParticleEffectParams());
239 fLoaded.push_back(effect);
Brian Osman5c1f8eb2019-02-14 14:49:55 -0500240 }
Brian Osmanb77d5022019-03-06 11:08:48 -0500241 ImGui::SameLine();
242
Brian Osman7c979f52019-02-12 13:27:51 -0500243 if (ImGui::Button("Load")) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500244 this->loadEffects(dirname.c_str());
Brian Osman7c979f52019-02-12 13:27:51 -0500245 }
246 ImGui::SameLine();
247
248 if (ImGui::Button("Save")) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500249 for (const auto& effect : fLoaded) {
250 SkFILEWStream fileStream(effect.fName.c_str());
251 if (fileStream.isValid()) {
252 SkJSONWriter writer(&fileStream, SkJSONWriter::Mode::kPretty);
253 SkToJsonVisitor toJson(writer);
254 writer.beginObject();
255 effect.fParams->visitFields(&toJson);
256 writer.endObject();
257 writer.flush();
258 fileStream.flush();
259 } else {
260 SkDebugf("Failed to open %s\n", effect.fName.c_str());
261 }
Brian Osman7c979f52019-02-12 13:27:51 -0500262 }
263 }
264
265 SkGuiVisitor gui;
Brian Osmanb77d5022019-03-06 11:08:48 -0500266 for (int i = 0; i < fLoaded.count(); ++i) {
267 ImGui::PushID(i);
268 if (fTimer && ImGui::Button("Play")) {
269 sk_sp<SkParticleEffect> effect(new SkParticleEffect(fLoaded[i].fParams, fRandom));
270 effect->start(fTimer->secs(), looped);
271 fRunning.push_back({ fPlayPosition, fLoaded[i].fName, effect });
272 }
273 ImGui::SameLine();
274
275 ImGui::InputText("##Name", fLoaded[i].fName.writable_str(), fLoaded[i].fName.size() + 1,
276 textFlags, InputTextCallback, &fLoaded[i].fName);
277
278 if (ImGui::TreeNode("##Details")) {
279 fLoaded[i].fParams->visitFields(&gui);
280 ImGui::TreePop();
281 }
282 ImGui::PopID();
283 }
284 }
285 ImGui::End();
286
287 // Another window to show all the running effects
288 if (ImGui::Begin("Running")) {
289 for (int i = 0; i < fRunning.count(); ++i) {
290 ImGui::PushID(i);
291 bool remove = ImGui::Button("X") || !fRunning[i].fEffect->isAlive();
292 ImGui::SameLine();
293 ImGui::Text("%4g, %4g %5d %s", fRunning[i].fPosition.fX, fRunning[i].fPosition.fY,
294 fRunning[i].fEffect->getCount(), fRunning[i].fName.c_str());
295 if (remove) {
296 fRunning.removeShuffle(i);
297 }
298 ImGui::PopID();
299 }
Brian Osman7c979f52019-02-12 13:27:51 -0500300 }
301 ImGui::End();
302
303 SkPaint dragPaint;
304 dragPaint.setColor(SK_ColorLTGRAY);
305 dragPaint.setAntiAlias(true);
306 SkPaint dragHighlight;
307 dragHighlight.setStyle(SkPaint::kStroke_Style);
308 dragHighlight.setColor(SK_ColorGREEN);
309 dragHighlight.setStrokeWidth(2);
310 dragHighlight.setAntiAlias(true);
311 for (int i = 0; i < gDragPoints.count(); ++i) {
312 canvas->drawCircle(*gDragPoints[i], kDragSize, dragPaint);
313 if (gDragIndex == i) {
314 canvas->drawCircle(*gDragPoints[i], kDragSize, dragHighlight);
315 }
316 }
Brian Osmanb77d5022019-03-06 11:08:48 -0500317 for (const auto& effect : fRunning) {
318 canvas->save();
319 canvas->translate(effect.fPosition.fX, effect.fPosition.fY);
320 effect.fEffect->draw(canvas);
321 canvas->restore();
322 }
Brian Osman7c979f52019-02-12 13:27:51 -0500323}
324
Mike Kleincd5104e2019-03-20 11:55:08 -0500325bool ParticlesSlide::animate(const AnimTimer& timer) {
Brian Osman5c1f8eb2019-02-14 14:49:55 -0500326 fTimer = &timer;
Brian Osmanb77d5022019-03-06 11:08:48 -0500327 for (const auto& effect : fRunning) {
328 effect.fEffect->update(timer.secs());
329 }
Brian Osman7c979f52019-02-12 13:27:51 -0500330 return true;
331}
332
333bool ParticlesSlide::onMouse(SkScalar x, SkScalar y, Window::InputState state, uint32_t modifiers) {
334 if (gDragIndex == -1) {
335 if (state == Window::kDown_InputState) {
336 float bestDistance = kDragSize;
337 SkPoint mousePt = { x, y };
338 for (int i = 0; i < gDragPoints.count(); ++i) {
339 float distance = SkPoint::Distance(*gDragPoints[i], mousePt);
340 if (distance < bestDistance) {
341 gDragIndex = i;
342 bestDistance = distance;
343 }
344 }
345 return gDragIndex != -1;
346 }
347 } else {
348 // Currently dragging
349 SkASSERT(gDragIndex < gDragPoints.count());
350 gDragPoints[gDragIndex]->set(x, y);
351 if (state == Window::kUp_InputState) {
352 gDragIndex = -1;
353 }
354 return true;
355 }
356 return false;
357}