blob: 4bf2575b663e2fce70bfbb9dd44a4ef5edbbee52 [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
Mike Kleinc0bd9f92019-04-23 12:05:21 -05008#include "tools/viewer/ParticlesSlide.h"
Brian Osman7c979f52019-02-12 13:27:51 -05009
Mike Kleinc0bd9f92019-04-23 12:05:21 -050010#include "modules/particles/include/SkParticleAffector.h"
11#include "modules/particles/include/SkParticleDrawable.h"
12#include "modules/particles/include/SkParticleEffect.h"
13#include "modules/particles/include/SkParticleSerialization.h"
14#include "modules/particles/include/SkReflected.h"
15#include "src/core/SkOSFile.h"
16#include "src/utils/SkOSPath.h"
17#include "tools/Resources.h"
18#include "tools/timer/AnimTimer.h"
19#include "tools/viewer/ImGuiLayer.h"
Brian Osman7c979f52019-02-12 13:27:51 -050020
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 Osman32d24902019-05-08 10:41:42 -040046static int count_lines(const SkString& s) {
47 int lines = 1;
48 for (size_t i = 0; i < s.size(); ++i) {
49 if (s[i] == '\n') {
50 ++lines;
51 }
52 }
53 return lines;
54}
55
Brian Osman7c979f52019-02-12 13:27:51 -050056class SkGuiVisitor : public SkFieldVisitor {
57public:
58 SkGuiVisitor() {
59 fTreeStack.push_back(true);
60 }
61
62#define IF_OPEN(WIDGET) if (fTreeStack.back()) { WIDGET; }
63
Brian Osman2991cbe2019-02-19 10:45:56 -050064 void visit(const char* name, float& f) override {
65 IF_OPEN(ImGui::DragFloat(item(name), &f))
Brian Osman7c979f52019-02-12 13:27:51 -050066 }
Brian Osman2991cbe2019-02-19 10:45:56 -050067 void visit(const char* name, int& i) override {
Brian Osman5de7ea42019-02-14 13:23:51 -050068 IF_OPEN(ImGui::DragInt(item(name), &i))
Brian Osman7c979f52019-02-12 13:27:51 -050069 }
Brian Osman2991cbe2019-02-19 10:45:56 -050070 void visit(const char* name, bool& b) override {
Brian Osman5de7ea42019-02-14 13:23:51 -050071 IF_OPEN(ImGui::Checkbox(item(name), &b))
Brian Osman7c979f52019-02-12 13:27:51 -050072 }
Brian Osman2991cbe2019-02-19 10:45:56 -050073 void visit(const char* name, SkString& s) override {
Brian Osman7c979f52019-02-12 13:27:51 -050074 if (fTreeStack.back()) {
Brian Osman32d24902019-05-08 10:41:42 -040075 int lines = count_lines(s);
Brian Osman7c979f52019-02-12 13:27:51 -050076 ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize;
Brian Osman32d24902019-05-08 10:41:42 -040077 if (lines > 1) {
78 ImVec2 boxSize(-1.0f, ImGui::GetTextLineHeight() * (lines + 1));
79 ImGui::InputTextMultiline(item(name), s.writable_str(), s.size() + 1, boxSize,
80 flags, InputTextCallback, &s);
81 } else {
82 ImGui::InputText(item(name), s.writable_str(), s.size() + 1, flags,
83 InputTextCallback, &s);
84 }
Brian Osman7c979f52019-02-12 13:27:51 -050085 }
86 }
Brian Osmane5d532e2019-02-26 14:58:40 -050087 void visit(const char* name, int& i, const EnumStringMapping* map, int count) override {
88 if (fTreeStack.back()) {
89 const char* curStr = EnumToString(i, map, count);
90 if (ImGui::BeginCombo(item(name), curStr ? curStr : "Unknown")) {
91 for (int j = 0; j < count; ++j) {
92 if (ImGui::Selectable(map[j].fName, i == map[j].fValue)) {
93 i = map[j].fValue;
94 }
95 }
96 ImGui::EndCombo();
97 }
98 }
99 }
Brian Osman7c979f52019-02-12 13:27:51 -0500100
Brian Osman2991cbe2019-02-19 10:45:56 -0500101 void visit(const char* name, SkPoint& p) override {
Brian Osman7c979f52019-02-12 13:27:51 -0500102 if (fTreeStack.back()) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500103 ImGui::DragFloat2(item(name), &p.fX);
Brian Osman7c979f52019-02-12 13:27:51 -0500104 gDragPoints.push_back(&p);
105 }
106 }
Brian Osman2991cbe2019-02-19 10:45:56 -0500107 void visit(const char* name, SkColor4f& c) override {
Brian Osman5de7ea42019-02-14 13:23:51 -0500108 IF_OPEN(ImGui::ColorEdit4(item(name), c.vec()))
Brian Osman7c979f52019-02-12 13:27:51 -0500109 }
110
Brian Osman5de7ea42019-02-14 13:23:51 -0500111#undef IF_OPEN
112
Brian Osman7c979f52019-02-12 13:27:51 -0500113 void visit(sk_sp<SkReflected>& e, const SkReflected::Type* baseType) override {
114 if (fTreeStack.back()) {
115 const SkReflected::Type* curType = e ? e->getType() : nullptr;
116 if (ImGui::BeginCombo("Type", curType ? curType->fName : "Null")) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500117 auto visitType = [baseType, curType, &e](const SkReflected::Type* t) {
118 if (t->fFactory && (t == baseType || t->isDerivedFrom(baseType)) &&
119 ImGui::Selectable(t->fName, curType == t)) {
Brian Osman7c979f52019-02-12 13:27:51 -0500120 e = t->fFactory();
121 }
122 };
Brian Osmanb77d5022019-03-06 11:08:48 -0500123 SkReflected::VisitTypes(visitType);
Brian Osman7c979f52019-02-12 13:27:51 -0500124 ImGui::EndCombo();
125 }
126 }
127 }
128
129 void enterObject(const char* name) override {
130 if (fTreeStack.back()) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500131 fTreeStack.push_back(ImGui::TreeNodeEx(item(name),
132 ImGuiTreeNodeFlags_AllowItemOverlap));
Brian Osman7c979f52019-02-12 13:27:51 -0500133 } else {
134 fTreeStack.push_back(false);
135 }
136 }
137 void exitObject() override {
138 if (fTreeStack.back()) {
139 ImGui::TreePop();
140 }
141 fTreeStack.pop_back();
142 }
143
Brian Osman5de7ea42019-02-14 13:23:51 -0500144 int enterArray(const char* name, int oldCount) override {
145 this->enterObject(item(name));
146 fArrayCounterStack.push_back(0);
147 fArrayEditStack.push_back();
Brian Osman7c979f52019-02-12 13:27:51 -0500148
Brian Osman5de7ea42019-02-14 13:23:51 -0500149 int count = oldCount;
Brian Osman7c979f52019-02-12 13:27:51 -0500150 if (fTreeStack.back()) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500151 ImGui::SameLine();
Brian Osman7c979f52019-02-12 13:27:51 -0500152 if (ImGui::Button("+")) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500153 ++count;
Brian Osman7c979f52019-02-12 13:27:51 -0500154 }
155 }
Brian Osman5de7ea42019-02-14 13:23:51 -0500156 return count;
157 }
158 ArrayEdit exitArray() override {
159 fArrayCounterStack.pop_back();
160 auto edit = fArrayEditStack.back();
161 fArrayEditStack.pop_back();
Brian Osman7c979f52019-02-12 13:27:51 -0500162 this->exitObject();
Brian Osman5de7ea42019-02-14 13:23:51 -0500163 return edit;
Brian Osman7c979f52019-02-12 13:27:51 -0500164 }
165
166private:
Brian Osman5de7ea42019-02-14 13:23:51 -0500167 const char* item(const char* name) {
168 if (name) {
169 return name;
170 }
171
172 // We're in an array. Add extra controls and a dynamic label.
173 int index = fArrayCounterStack.back()++;
174 ArrayEdit& edit(fArrayEditStack.back());
175 fScratchLabel = SkStringPrintf("[%d]", index);
176
177 ImGui::PushID(index);
178
179 if (ImGui::Button("X")) {
180 edit.fVerb = ArrayEdit::Verb::kRemove;
181 edit.fIndex = index;
182 }
183 ImGui::SameLine();
184 if (ImGui::Button("^")) {
185 edit.fVerb = ArrayEdit::Verb::kMoveForward;
186 edit.fIndex = index;
187 }
188 ImGui::SameLine();
189 if (ImGui::Button("v")) {
190 edit.fVerb = ArrayEdit::Verb::kMoveForward;
191 edit.fIndex = index + 1;
192 }
193 ImGui::SameLine();
194
195 ImGui::PopID();
196
197 return fScratchLabel.c_str();
198 }
199
Brian Osman7c979f52019-02-12 13:27:51 -0500200 SkSTArray<16, bool, true> fTreeStack;
Brian Osman5de7ea42019-02-14 13:23:51 -0500201 SkSTArray<16, int, true> fArrayCounterStack;
202 SkSTArray<16, ArrayEdit, true> fArrayEditStack;
203 SkString fScratchLabel;
Brian Osman7c979f52019-02-12 13:27:51 -0500204};
205
Brian Osman7c979f52019-02-12 13:27:51 -0500206ParticlesSlide::ParticlesSlide() {
207 // Register types for serialization
208 REGISTER_REFLECTED(SkReflected);
209 SkParticleAffector::RegisterAffectorTypes();
Brian Osman543d2e22019-02-15 14:29:38 -0500210 SkParticleDrawable::RegisterDrawableTypes();
Brian Osman7c979f52019-02-12 13:27:51 -0500211 fName = "Particles";
Brian Osmanb77d5022019-03-06 11:08:48 -0500212 fPlayPosition.set(200.0f, 200.0f);
213}
214
215void ParticlesSlide::loadEffects(const char* dirname) {
216 fLoaded.reset();
217 fRunning.reset();
218 SkOSFile::Iter iter(dirname, ".json");
219 for (SkString file; iter.next(&file); ) {
220 LoadedEffect effect;
221 effect.fName = SkOSPath::Join(dirname, file.c_str());
222 effect.fParams.reset(new SkParticleEffectParams());
223 if (auto fileData = SkData::MakeFromFileName(effect.fName.c_str())) {
224 skjson::DOM dom(static_cast<const char*>(fileData->data()), fileData->size());
225 SkFromJsonVisitor fromJson(dom.root());
226 effect.fParams->visitFields(&fromJson);
227 fLoaded.push_back(effect);
228 }
229 }
Ben Wagnera57d8682019-02-14 13:22:43 -0500230}
231
232void ParticlesSlide::load(SkScalar winWidth, SkScalar winHeight) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500233 this->loadEffects(GetResourcePath("particles").c_str());
Brian Osman7c979f52019-02-12 13:27:51 -0500234}
235
236void ParticlesSlide::draw(SkCanvas* canvas) {
237 canvas->clear(0);
238
239 gDragPoints.reset();
Brian Osmanb77d5022019-03-06 11:08:48 -0500240 gDragPoints.push_back(&fPlayPosition);
241
242 // Window to show all loaded effects, and allow playing them
243 if (ImGui::Begin("Library", nullptr, ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
Brian Osman5c1f8eb2019-02-14 14:49:55 -0500244 static bool looped = true;
245 ImGui::Checkbox("Looped", &looped);
Brian Osmanb77d5022019-03-06 11:08:48 -0500246
247 static SkString dirname = GetResourcePath("particles");
248 ImGuiInputTextFlags textFlags = ImGuiInputTextFlags_CallbackResize;
249 ImGui::InputText("Directory", dirname.writable_str(), dirname.size() + 1, textFlags,
250 InputTextCallback, &dirname);
251
252 if (ImGui::Button("New")) {
253 LoadedEffect effect;
254 effect.fName = SkOSPath::Join(dirname.c_str(), "new.json");
255 effect.fParams.reset(new SkParticleEffectParams());
256 fLoaded.push_back(effect);
Brian Osman5c1f8eb2019-02-14 14:49:55 -0500257 }
Brian Osmanb77d5022019-03-06 11:08:48 -0500258 ImGui::SameLine();
259
Brian Osman7c979f52019-02-12 13:27:51 -0500260 if (ImGui::Button("Load")) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500261 this->loadEffects(dirname.c_str());
Brian Osman7c979f52019-02-12 13:27:51 -0500262 }
263 ImGui::SameLine();
264
265 if (ImGui::Button("Save")) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500266 for (const auto& effect : fLoaded) {
267 SkFILEWStream fileStream(effect.fName.c_str());
268 if (fileStream.isValid()) {
269 SkJSONWriter writer(&fileStream, SkJSONWriter::Mode::kPretty);
270 SkToJsonVisitor toJson(writer);
271 writer.beginObject();
272 effect.fParams->visitFields(&toJson);
273 writer.endObject();
274 writer.flush();
275 fileStream.flush();
276 } else {
277 SkDebugf("Failed to open %s\n", effect.fName.c_str());
278 }
Brian Osman7c979f52019-02-12 13:27:51 -0500279 }
280 }
281
282 SkGuiVisitor gui;
Brian Osmanb77d5022019-03-06 11:08:48 -0500283 for (int i = 0; i < fLoaded.count(); ++i) {
284 ImGui::PushID(i);
285 if (fTimer && ImGui::Button("Play")) {
286 sk_sp<SkParticleEffect> effect(new SkParticleEffect(fLoaded[i].fParams, fRandom));
287 effect->start(fTimer->secs(), looped);
288 fRunning.push_back({ fPlayPosition, fLoaded[i].fName, effect });
289 }
290 ImGui::SameLine();
291
292 ImGui::InputText("##Name", fLoaded[i].fName.writable_str(), fLoaded[i].fName.size() + 1,
293 textFlags, InputTextCallback, &fLoaded[i].fName);
294
295 if (ImGui::TreeNode("##Details")) {
296 fLoaded[i].fParams->visitFields(&gui);
297 ImGui::TreePop();
298 }
299 ImGui::PopID();
300 }
301 }
302 ImGui::End();
303
304 // Another window to show all the running effects
305 if (ImGui::Begin("Running")) {
306 for (int i = 0; i < fRunning.count(); ++i) {
307 ImGui::PushID(i);
308 bool remove = ImGui::Button("X") || !fRunning[i].fEffect->isAlive();
309 ImGui::SameLine();
310 ImGui::Text("%4g, %4g %5d %s", fRunning[i].fPosition.fX, fRunning[i].fPosition.fY,
311 fRunning[i].fEffect->getCount(), fRunning[i].fName.c_str());
312 if (remove) {
313 fRunning.removeShuffle(i);
314 }
315 ImGui::PopID();
316 }
Brian Osman7c979f52019-02-12 13:27:51 -0500317 }
318 ImGui::End();
319
320 SkPaint dragPaint;
321 dragPaint.setColor(SK_ColorLTGRAY);
322 dragPaint.setAntiAlias(true);
323 SkPaint dragHighlight;
324 dragHighlight.setStyle(SkPaint::kStroke_Style);
325 dragHighlight.setColor(SK_ColorGREEN);
326 dragHighlight.setStrokeWidth(2);
327 dragHighlight.setAntiAlias(true);
328 for (int i = 0; i < gDragPoints.count(); ++i) {
329 canvas->drawCircle(*gDragPoints[i], kDragSize, dragPaint);
330 if (gDragIndex == i) {
331 canvas->drawCircle(*gDragPoints[i], kDragSize, dragHighlight);
332 }
333 }
Brian Osmanb77d5022019-03-06 11:08:48 -0500334 for (const auto& effect : fRunning) {
335 canvas->save();
336 canvas->translate(effect.fPosition.fX, effect.fPosition.fY);
337 effect.fEffect->draw(canvas);
338 canvas->restore();
339 }
Brian Osman7c979f52019-02-12 13:27:51 -0500340}
341
Mike Kleincd5104e2019-03-20 11:55:08 -0500342bool ParticlesSlide::animate(const AnimTimer& timer) {
Brian Osman5c1f8eb2019-02-14 14:49:55 -0500343 fTimer = &timer;
Brian Osmanb77d5022019-03-06 11:08:48 -0500344 for (const auto& effect : fRunning) {
345 effect.fEffect->update(timer.secs());
346 }
Brian Osman7c979f52019-02-12 13:27:51 -0500347 return true;
348}
349
Hal Canary3a85ed12019-07-08 16:07:57 -0400350bool ParticlesSlide::onMouse(SkScalar x, SkScalar y, Window::InputState state, ModifierKey modifiers) {
Brian Osman7c979f52019-02-12 13:27:51 -0500351 if (gDragIndex == -1) {
352 if (state == Window::kDown_InputState) {
353 float bestDistance = kDragSize;
354 SkPoint mousePt = { x, y };
355 for (int i = 0; i < gDragPoints.count(); ++i) {
356 float distance = SkPoint::Distance(*gDragPoints[i], mousePt);
357 if (distance < bestDistance) {
358 gDragIndex = i;
359 bestDistance = distance;
360 }
361 }
362 return gDragIndex != -1;
363 }
364 } else {
365 // Currently dragging
366 SkASSERT(gDragIndex < gDragPoints.count());
367 gDragPoints[gDragIndex]->set(x, y);
368 if (state == Window::kUp_InputState) {
369 gDragIndex = -1;
370 }
371 return true;
372 }
373 return false;
374}