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