• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
10 #include "ImGuiLayer.h"
11 #include "Resources.h"
12 #include "SkAnimTimer.h"
13 #include "SkOSFile.h"
14 #include "SkOSPath.h"
15 #include "SkParticleAffector.h"
16 #include "SkParticleDrawable.h"
17 #include "SkParticleEffect.h"
18 #include "SkParticleSerialization.h"
19 #include "SkReflected.h"
20 
21 #include "imgui.h"
22 
23 using namespace sk_app;
24 
25 namespace {
26 
27 static SkScalar kDragSize = 8.0f;
28 static SkTArray<SkPoint*> gDragPoints;
29 int gDragIndex = -1;
30 
31 }
32 
33 ///////////////////////////////////////////////////////////////////////////////
34 
InputTextCallback(ImGuiInputTextCallbackData * data)35 static 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 
46 class SkGuiVisitor : public SkFieldVisitor {
47 public:
SkGuiVisitor()48     SkGuiVisitor() {
49         fTreeStack.push_back(true);
50     }
51 
52 #define IF_OPEN(WIDGET) if (fTreeStack.back()) { WIDGET; }
53 
visit(const char * name,float & f)54     void visit(const char* name, float& f) override {
55         IF_OPEN(ImGui::DragFloat(item(name), &f))
56     }
visit(const char * name,int & i)57     void visit(const char* name, int& i) override {
58         IF_OPEN(ImGui::DragInt(item(name), &i))
59     }
visit(const char * name,bool & b)60     void visit(const char* name, bool& b) override {
61         IF_OPEN(ImGui::Checkbox(item(name), &b))
62     }
visit(const char * name,SkString & s)63     void visit(const char* name, SkString& s) override {
64         if (fTreeStack.back()) {
65             ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize;
66             ImGui::InputText(item(name), s.writable_str(), s.size() + 1, flags, InputTextCallback,
67                              &s);
68         }
69     }
visit(const char * name,int & i,const EnumStringMapping * map,int count)70     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     }
83 
visit(const char * name,SkPoint & p)84     void visit(const char* name, SkPoint& p) override {
85         if (fTreeStack.back()) {
86             ImGui::DragFloat2(item(name), &p.fX);
87             gDragPoints.push_back(&p);
88         }
89     }
visit(const char * name,SkColor4f & c)90     void visit(const char* name, SkColor4f& c) override {
91         IF_OPEN(ImGui::ColorEdit4(item(name), c.vec()))
92     }
93 
94 #undef IF_OPEN
95 
visit(sk_sp<SkReflected> & e,const SkReflected::Type * baseType)96     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")) {
100                 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)) {
103                         e = t->fFactory();
104                     }
105                 };
106                 SkReflected::VisitTypes(visitType);
107                 ImGui::EndCombo();
108             }
109         }
110     }
111 
enterObject(const char * name)112     void enterObject(const char* name) override {
113         if (fTreeStack.back()) {
114             fTreeStack.push_back(ImGui::TreeNodeEx(item(name),
115                                                    ImGuiTreeNodeFlags_AllowItemOverlap));
116         } else {
117             fTreeStack.push_back(false);
118         }
119     }
exitObject()120     void exitObject() override {
121         if (fTreeStack.back()) {
122             ImGui::TreePop();
123         }
124         fTreeStack.pop_back();
125     }
126 
enterArray(const char * name,int oldCount)127     int enterArray(const char* name, int oldCount) override {
128         this->enterObject(item(name));
129         fArrayCounterStack.push_back(0);
130         fArrayEditStack.push_back();
131 
132         int count = oldCount;
133         if (fTreeStack.back()) {
134             ImGui::SameLine();
135             if (ImGui::Button("+")) {
136                 ++count;
137             }
138         }
139         return count;
140     }
exitArray()141     ArrayEdit exitArray() override {
142         fArrayCounterStack.pop_back();
143         auto edit = fArrayEditStack.back();
144         fArrayEditStack.pop_back();
145         this->exitObject();
146         return edit;
147     }
148 
149 private:
item(const char * name)150     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 
183     SkSTArray<16, bool, true> fTreeStack;
184     SkSTArray<16, int, true>  fArrayCounterStack;
185     SkSTArray<16, ArrayEdit, true> fArrayEditStack;
186     SkString fScratchLabel;
187 };
188 
ParticlesSlide()189 ParticlesSlide::ParticlesSlide() {
190     // Register types for serialization
191     REGISTER_REFLECTED(SkReflected);
192     SkParticleAffector::RegisterAffectorTypes();
193     SkParticleDrawable::RegisterDrawableTypes();
194     fName = "Particles";
195     fPlayPosition.set(200.0f, 200.0f);
196 }
197 
loadEffects(const char * dirname)198 void 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     }
213 }
214 
load(SkScalar winWidth,SkScalar winHeight)215 void ParticlesSlide::load(SkScalar winWidth, SkScalar winHeight) {
216     this->loadEffects(GetResourcePath("particles").c_str());
217 }
218 
draw(SkCanvas * canvas)219 void ParticlesSlide::draw(SkCanvas* canvas) {
220     canvas->clear(0);
221 
222     gDragPoints.reset();
223     gDragPoints.push_back(&fPlayPosition);
224 
225     // Window to show all loaded effects, and allow playing them
226     if (ImGui::Begin("Library", nullptr, ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
227         static bool looped = true;
228         ImGui::Checkbox("Looped", &looped);
229 
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);
240         }
241         ImGui::SameLine();
242 
243         if (ImGui::Button("Load")) {
244             this->loadEffects(dirname.c_str());
245         }
246         ImGui::SameLine();
247 
248         if (ImGui::Button("Save")) {
249             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                 }
262             }
263         }
264 
265         SkGuiVisitor gui;
266         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         }
300     }
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     }
317     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     }
323 }
324 
animate(const SkAnimTimer & timer)325 bool ParticlesSlide::animate(const SkAnimTimer& timer) {
326     fTimer = &timer;
327     for (const auto& effect : fRunning) {
328         effect.fEffect->update(timer.secs());
329     }
330     return true;
331 }
332 
onMouse(SkScalar x,SkScalar y,Window::InputState state,uint32_t modifiers)333 bool 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 }
358