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