• 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 "tools/viewer/ParticlesSlide.h"
9 
10 #include "modules/particles/include/SkParticleDrawable.h"
11 #include "modules/particles/include/SkParticleEffect.h"
12 #include "modules/particles/include/SkParticleSerialization.h"
13 #include "modules/particles/include/SkReflected.h"
14 #include "src/core/SkOSFile.h"
15 #include "src/utils/SkOSPath.h"
16 #include "tools/Resources.h"
17 #include "tools/viewer/ImGuiLayer.h"
18 
19 #include "imgui.h"
20 
21 using namespace sk_app;
22 
23 namespace {
24 
25 static SkScalar kDragSize = 8.0f;
26 static SkTArray<SkPoint*> gDragPoints;
27 int gDragIndex = -1;
28 
29 }
30 
31 ///////////////////////////////////////////////////////////////////////////////
32 
InputTextCallback(ImGuiInputTextCallbackData * data)33 static int InputTextCallback(ImGuiInputTextCallbackData* data) {
34     if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
35         SkString* s = (SkString*)data->UserData;
36         SkASSERT(data->Buf == s->writable_str());
37         SkString tmp(data->Buf, data->BufTextLen);
38         s->swap(tmp);
39         data->Buf = s->writable_str();
40     }
41     return 0;
42 }
43 
count_lines(const SkString & s)44 static int count_lines(const SkString& s) {
45     int lines = 1;
46     for (size_t i = 0; i < s.size(); ++i) {
47         if (s[i] == '\n') {
48             ++lines;
49         }
50     }
51     return lines;
52 }
53 
54 class SkGuiVisitor : public SkFieldVisitor {
55 public:
SkGuiVisitor()56     SkGuiVisitor() {
57         fTreeStack.push_back(true);
58     }
59 
60 #define IF_OPEN(WIDGET) if (fTreeStack.back()) { WIDGET; }
61 
visit(const char * name,float & f)62     void visit(const char* name, float& f) override {
63         IF_OPEN(ImGui::DragFloat(item(name), &f))
64     }
visit(const char * name,int & i)65     void visit(const char* name, int& i) override {
66         IF_OPEN(ImGui::DragInt(item(name), &i))
67     }
visit(const char * name,bool & b)68     void visit(const char* name, bool& b) override {
69         IF_OPEN(ImGui::Checkbox(item(name), &b))
70     }
visit(const char * name,SkString & s)71     void visit(const char* name, SkString& s) override {
72         if (fTreeStack.back()) {
73             int lines = count_lines(s);
74             ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize;
75             if (lines > 1) {
76                 ImVec2 boxSize(-1.0f, ImGui::GetTextLineHeight() * (lines + 1));
77                 ImGui::InputTextMultiline(item(name), s.writable_str(), s.size() + 1, boxSize,
78                                           flags, InputTextCallback, &s);
79             } else {
80                 ImGui::InputText(item(name), s.writable_str(), s.size() + 1, flags,
81                                  InputTextCallback, &s);
82             }
83         }
84     }
visit(const char * name,int & i,const EnumStringMapping * map,int count)85     void visit(const char* name, int& i, const EnumStringMapping* map, int count) override {
86         if (fTreeStack.back()) {
87             const char* curStr = EnumToString(i, map, count);
88             if (ImGui::BeginCombo(item(name), curStr ? curStr : "Unknown")) {
89                 for (int j = 0; j < count; ++j) {
90                     if (ImGui::Selectable(map[j].fName, i == map[j].fValue)) {
91                         i = map[j].fValue;
92                     }
93                 }
94                 ImGui::EndCombo();
95             }
96         }
97     }
98 
visit(const char * name,SkPoint & p)99     void visit(const char* name, SkPoint& p) override {
100         if (fTreeStack.back()) {
101             ImGui::DragFloat2(item(name), &p.fX);
102             gDragPoints.push_back(&p);
103         }
104     }
visit(const char * name,SkColor4f & c)105     void visit(const char* name, SkColor4f& c) override {
106         IF_OPEN(ImGui::ColorEdit4(item(name), c.vec()))
107     }
108 
109 #undef IF_OPEN
110 
visit(sk_sp<SkReflected> & e,const SkReflected::Type * baseType)111     void visit(sk_sp<SkReflected>& e, const SkReflected::Type* baseType) override {
112         if (fTreeStack.back()) {
113             const SkReflected::Type* curType = e ? e->getType() : nullptr;
114             if (ImGui::BeginCombo("Type", curType ? curType->fName : "Null")) {
115                 auto visitType = [baseType, curType, &e](const SkReflected::Type* t) {
116                     if (t->fFactory && (t == baseType || t->isDerivedFrom(baseType)) &&
117                         ImGui::Selectable(t->fName, curType == t)) {
118                         e = t->fFactory();
119                     }
120                 };
121                 SkReflected::VisitTypes(visitType);
122                 ImGui::EndCombo();
123             }
124         }
125     }
126 
enterObject(const char * name)127     void enterObject(const char* name) override {
128         if (fTreeStack.back()) {
129             fTreeStack.push_back(ImGui::TreeNodeEx(item(name),
130                                                    ImGuiTreeNodeFlags_AllowItemOverlap));
131         } else {
132             fTreeStack.push_back(false);
133         }
134     }
exitObject()135     void exitObject() override {
136         if (fTreeStack.back()) {
137             ImGui::TreePop();
138         }
139         fTreeStack.pop_back();
140     }
141 
enterArray(const char * name,int oldCount)142     int enterArray(const char* name, int oldCount) override {
143         this->enterObject(item(name));
144         fArrayCounterStack.push_back(0);
145         fArrayEditStack.push_back();
146 
147         int count = oldCount;
148         if (fTreeStack.back()) {
149             ImGui::SameLine();
150             if (ImGui::Button("+")) {
151                 ++count;
152             }
153         }
154         return count;
155     }
exitArray()156     ArrayEdit exitArray() override {
157         fArrayCounterStack.pop_back();
158         auto edit = fArrayEditStack.back();
159         fArrayEditStack.pop_back();
160         this->exitObject();
161         return edit;
162     }
163 
164 private:
item(const char * name)165     const char* item(const char* name) {
166         if (name) {
167             return name;
168         }
169 
170         // We're in an array. Add extra controls and a dynamic label.
171         int index = fArrayCounterStack.back()++;
172         ArrayEdit& edit(fArrayEditStack.back());
173         fScratchLabel = SkStringPrintf("[%d]", index);
174 
175         ImGui::PushID(index);
176 
177         if (ImGui::Button("X")) {
178             edit.fVerb = ArrayEdit::Verb::kRemove;
179             edit.fIndex = index;
180         }
181         ImGui::SameLine();
182         if (ImGui::Button("^")) {
183             edit.fVerb = ArrayEdit::Verb::kMoveForward;
184             edit.fIndex = index;
185         }
186         ImGui::SameLine();
187         if (ImGui::Button("v")) {
188             edit.fVerb = ArrayEdit::Verb::kMoveForward;
189             edit.fIndex = index + 1;
190         }
191         ImGui::SameLine();
192 
193         ImGui::PopID();
194 
195         return fScratchLabel.c_str();
196     }
197 
198     SkSTArray<16, bool, true> fTreeStack;
199     SkSTArray<16, int, true>  fArrayCounterStack;
200     SkSTArray<16, ArrayEdit, true> fArrayEditStack;
201     SkString fScratchLabel;
202 };
203 
ParticlesSlide()204 ParticlesSlide::ParticlesSlide() {
205     // Register types for serialization
206     REGISTER_REFLECTED(SkReflected);
207     SkParticleBinding::RegisterBindingTypes();
208     SkParticleDrawable::RegisterDrawableTypes();
209     fName = "Particles";
210     fPlayPosition.set(200.0f, 200.0f);
211 }
212 
loadEffects(const char * dirname)213 void ParticlesSlide::loadEffects(const char* dirname) {
214     fLoaded.reset();
215     fRunning.reset();
216     SkOSFile::Iter iter(dirname, ".json");
217     for (SkString file; iter.next(&file); ) {
218         LoadedEffect effect;
219         effect.fName = SkOSPath::Join(dirname, file.c_str());
220         effect.fParams.reset(new SkParticleEffectParams());
221         if (auto fileData = SkData::MakeFromFileName(effect.fName.c_str())) {
222             skjson::DOM dom(static_cast<const char*>(fileData->data()), fileData->size());
223             SkFromJsonVisitor fromJson(dom.root());
224             effect.fParams->visitFields(&fromJson);
225             fLoaded.push_back(effect);
226         }
227     }
228 }
229 
load(SkScalar winWidth,SkScalar winHeight)230 void ParticlesSlide::load(SkScalar winWidth, SkScalar winHeight) {
231     this->loadEffects(GetResourcePath("particles").c_str());
232 }
233 
draw(SkCanvas * canvas)234 void ParticlesSlide::draw(SkCanvas* canvas) {
235     canvas->clear(0);
236 
237     gDragPoints.reset();
238     gDragPoints.push_back(&fPlayPosition);
239 
240     // Window to show all loaded effects, and allow playing them
241     if (ImGui::Begin("Library", nullptr, ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
242         static bool looped = true;
243         ImGui::Checkbox("Looped", &looped);
244 
245         static SkString dirname = GetResourcePath("particles");
246         ImGuiInputTextFlags textFlags = ImGuiInputTextFlags_CallbackResize;
247         ImGui::InputText("Directory", dirname.writable_str(), dirname.size() + 1, textFlags,
248                          InputTextCallback, &dirname);
249 
250         if (ImGui::Button("New")) {
251             LoadedEffect effect;
252             effect.fName = SkOSPath::Join(dirname.c_str(), "new.json");
253             effect.fParams.reset(new SkParticleEffectParams());
254             fLoaded.push_back(effect);
255         }
256         ImGui::SameLine();
257 
258         if (ImGui::Button("Load")) {
259             this->loadEffects(dirname.c_str());
260         }
261         ImGui::SameLine();
262 
263         if (ImGui::Button("Save")) {
264             for (const auto& effect : fLoaded) {
265                 SkFILEWStream fileStream(effect.fName.c_str());
266                 if (fileStream.isValid()) {
267                     SkJSONWriter writer(&fileStream, SkJSONWriter::Mode::kPretty);
268                     SkToJsonVisitor toJson(writer);
269                     writer.beginObject();
270                     effect.fParams->visitFields(&toJson);
271                     writer.endObject();
272                     writer.flush();
273                     fileStream.flush();
274                 } else {
275                     SkDebugf("Failed to open %s\n", effect.fName.c_str());
276                 }
277             }
278         }
279 
280         SkGuiVisitor gui;
281         for (int i = 0; i < fLoaded.count(); ++i) {
282             ImGui::PushID(i);
283             if (fAnimated && ImGui::Button("Play")) {
284                 sk_sp<SkParticleEffect> effect(new SkParticleEffect(fLoaded[i].fParams, fRandom));
285                 effect->start(fAnimationTime, looped);
286                 fRunning.push_back({ fPlayPosition, fLoaded[i].fName, effect });
287             }
288             ImGui::SameLine();
289 
290             ImGui::InputText("##Name", fLoaded[i].fName.writable_str(), fLoaded[i].fName.size() + 1,
291                              textFlags, InputTextCallback, &fLoaded[i].fName);
292 
293             if (ImGui::TreeNode("##Details")) {
294                 fLoaded[i].fParams->visitFields(&gui);
295                 ImGui::TreePop();
296             }
297             ImGui::PopID();
298         }
299     }
300     ImGui::End();
301 
302     // Another window to show all the running effects
303     if (ImGui::Begin("Running")) {
304         for (int i = 0; i < fRunning.count(); ++i) {
305             ImGui::PushID(i);
306             bool remove = ImGui::Button("X") || !fRunning[i].fEffect->isAlive();
307             ImGui::SameLine();
308             ImGui::Text("%4g, %4g %5d %s", fRunning[i].fPosition.fX, fRunning[i].fPosition.fY,
309                         fRunning[i].fEffect->getCount(), fRunning[i].fName.c_str());
310             if (remove) {
311                 fRunning.removeShuffle(i);
312             }
313             ImGui::PopID();
314         }
315     }
316     ImGui::End();
317 
318     SkPaint dragPaint;
319     dragPaint.setColor(SK_ColorLTGRAY);
320     dragPaint.setAntiAlias(true);
321     SkPaint dragHighlight;
322     dragHighlight.setStyle(SkPaint::kStroke_Style);
323     dragHighlight.setColor(SK_ColorGREEN);
324     dragHighlight.setStrokeWidth(2);
325     dragHighlight.setAntiAlias(true);
326     for (int i = 0; i < gDragPoints.count(); ++i) {
327         canvas->drawCircle(*gDragPoints[i], kDragSize, dragPaint);
328         if (gDragIndex == i) {
329             canvas->drawCircle(*gDragPoints[i], kDragSize, dragHighlight);
330         }
331     }
332     for (const auto& effect : fRunning) {
333         canvas->save();
334         canvas->translate(effect.fPosition.fX, effect.fPosition.fY);
335         effect.fEffect->draw(canvas);
336         canvas->restore();
337     }
338 }
339 
animate(double nanos)340 bool ParticlesSlide::animate(double nanos) {
341     fAnimated = true;
342     fAnimationTime = 1e-9 * nanos;
343     for (const auto& effect : fRunning) {
344         effect.fEffect->update(fAnimationTime);
345     }
346     return true;
347 }
348 
onMouse(SkScalar x,SkScalar y,InputState state,ModifierKey modifiers)349 bool ParticlesSlide::onMouse(SkScalar x, SkScalar y, InputState state, ModifierKey modifiers) {
350     if (gDragIndex == -1) {
351         if (state == InputState::kDown) {
352             float bestDistance = kDragSize;
353             SkPoint mousePt = { x, y };
354             for (int i = 0; i < gDragPoints.count(); ++i) {
355                 float distance = SkPoint::Distance(*gDragPoints[i], mousePt);
356                 if (distance < bestDistance) {
357                     gDragIndex = i;
358                     bestDistance = distance;
359                 }
360             }
361             return gDragIndex != -1;
362         }
363     } else {
364         // Currently dragging
365         SkASSERT(gDragIndex < gDragPoints.count());
366         gDragPoints[gDragIndex]->set(x, y);
367         if (state == InputState::kUp) {
368             gDragIndex = -1;
369         }
370         return true;
371     }
372     return false;
373 }
374