blob: 9943e9b84acc60d760a8731bd2ae8c0706cd5b69 [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"
Mike Kleinc0bd9f92019-04-23 12:05:21 -050018#include "tools/viewer/ImGuiLayer.h"
Brian Osman7c979f52019-02-12 13:27:51 -050019
20#include "imgui.h"
21
22using namespace sk_app;
23
24namespace {
25
26static SkScalar kDragSize = 8.0f;
27static SkTArray<SkPoint*> gDragPoints;
28int gDragIndex = -1;
29
30}
31
32///////////////////////////////////////////////////////////////////////////////
33
34static int InputTextCallback(ImGuiInputTextCallbackData* data) {
35 if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
36 SkString* s = (SkString*)data->UserData;
37 SkASSERT(data->Buf == s->writable_str());
38 SkString tmp(data->Buf, data->BufTextLen);
39 s->swap(tmp);
40 data->Buf = s->writable_str();
41 }
42 return 0;
43}
44
Brian Osman32d24902019-05-08 10:41:42 -040045static int count_lines(const SkString& s) {
46 int lines = 1;
47 for (size_t i = 0; i < s.size(); ++i) {
48 if (s[i] == '\n') {
49 ++lines;
50 }
51 }
52 return lines;
53}
54
Brian Osman7c979f52019-02-12 13:27:51 -050055class SkGuiVisitor : public SkFieldVisitor {
56public:
57 SkGuiVisitor() {
58 fTreeStack.push_back(true);
59 }
60
61#define IF_OPEN(WIDGET) if (fTreeStack.back()) { WIDGET; }
62
Brian Osman2991cbe2019-02-19 10:45:56 -050063 void visit(const char* name, float& f) override {
64 IF_OPEN(ImGui::DragFloat(item(name), &f))
Brian Osman7c979f52019-02-12 13:27:51 -050065 }
Brian Osman2991cbe2019-02-19 10:45:56 -050066 void visit(const char* name, int& i) override {
Brian Osman5de7ea42019-02-14 13:23:51 -050067 IF_OPEN(ImGui::DragInt(item(name), &i))
Brian Osman7c979f52019-02-12 13:27:51 -050068 }
Brian Osman2991cbe2019-02-19 10:45:56 -050069 void visit(const char* name, bool& b) override {
Brian Osman5de7ea42019-02-14 13:23:51 -050070 IF_OPEN(ImGui::Checkbox(item(name), &b))
Brian Osman7c979f52019-02-12 13:27:51 -050071 }
Brian Osman2991cbe2019-02-19 10:45:56 -050072 void visit(const char* name, SkString& s) override {
Brian Osman7c979f52019-02-12 13:27:51 -050073 if (fTreeStack.back()) {
Brian Osman32d24902019-05-08 10:41:42 -040074 int lines = count_lines(s);
Brian Osman7c979f52019-02-12 13:27:51 -050075 ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize;
Brian Osman32d24902019-05-08 10:41:42 -040076 if (lines > 1) {
77 ImVec2 boxSize(-1.0f, ImGui::GetTextLineHeight() * (lines + 1));
78 ImGui::InputTextMultiline(item(name), s.writable_str(), s.size() + 1, boxSize,
79 flags, InputTextCallback, &s);
80 } else {
81 ImGui::InputText(item(name), s.writable_str(), s.size() + 1, flags,
82 InputTextCallback, &s);
83 }
Brian Osman7c979f52019-02-12 13:27:51 -050084 }
85 }
Brian Osmane5d532e2019-02-26 14:58:40 -050086 void visit(const char* name, int& i, const EnumStringMapping* map, int count) override {
87 if (fTreeStack.back()) {
88 const char* curStr = EnumToString(i, map, count);
89 if (ImGui::BeginCombo(item(name), curStr ? curStr : "Unknown")) {
90 for (int j = 0; j < count; ++j) {
91 if (ImGui::Selectable(map[j].fName, i == map[j].fValue)) {
92 i = map[j].fValue;
93 }
94 }
95 ImGui::EndCombo();
96 }
97 }
98 }
Brian Osman7c979f52019-02-12 13:27:51 -050099
Brian Osman2991cbe2019-02-19 10:45:56 -0500100 void visit(const char* name, SkPoint& p) override {
Brian Osman7c979f52019-02-12 13:27:51 -0500101 if (fTreeStack.back()) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500102 ImGui::DragFloat2(item(name), &p.fX);
Brian Osman7c979f52019-02-12 13:27:51 -0500103 gDragPoints.push_back(&p);
104 }
105 }
Brian Osman2991cbe2019-02-19 10:45:56 -0500106 void visit(const char* name, SkColor4f& c) override {
Brian Osman5de7ea42019-02-14 13:23:51 -0500107 IF_OPEN(ImGui::ColorEdit4(item(name), c.vec()))
Brian Osman7c979f52019-02-12 13:27:51 -0500108 }
109
Brian Osman5de7ea42019-02-14 13:23:51 -0500110#undef IF_OPEN
111
Brian Osman7c979f52019-02-12 13:27:51 -0500112 void visit(sk_sp<SkReflected>& e, const SkReflected::Type* baseType) override {
113 if (fTreeStack.back()) {
114 const SkReflected::Type* curType = e ? e->getType() : nullptr;
115 if (ImGui::BeginCombo("Type", curType ? curType->fName : "Null")) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500116 auto visitType = [baseType, curType, &e](const SkReflected::Type* t) {
117 if (t->fFactory && (t == baseType || t->isDerivedFrom(baseType)) &&
118 ImGui::Selectable(t->fName, curType == t)) {
Brian Osman7c979f52019-02-12 13:27:51 -0500119 e = t->fFactory();
120 }
121 };
Brian Osmanb77d5022019-03-06 11:08:48 -0500122 SkReflected::VisitTypes(visitType);
Brian Osman7c979f52019-02-12 13:27:51 -0500123 ImGui::EndCombo();
124 }
125 }
126 }
127
128 void enterObject(const char* name) override {
129 if (fTreeStack.back()) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500130 fTreeStack.push_back(ImGui::TreeNodeEx(item(name),
131 ImGuiTreeNodeFlags_AllowItemOverlap));
Brian Osman7c979f52019-02-12 13:27:51 -0500132 } else {
133 fTreeStack.push_back(false);
134 }
135 }
136 void exitObject() override {
137 if (fTreeStack.back()) {
138 ImGui::TreePop();
139 }
140 fTreeStack.pop_back();
141 }
142
Brian Osman5de7ea42019-02-14 13:23:51 -0500143 int enterArray(const char* name, int oldCount) override {
144 this->enterObject(item(name));
145 fArrayCounterStack.push_back(0);
146 fArrayEditStack.push_back();
Brian Osman7c979f52019-02-12 13:27:51 -0500147
Brian Osman5de7ea42019-02-14 13:23:51 -0500148 int count = oldCount;
Brian Osman7c979f52019-02-12 13:27:51 -0500149 if (fTreeStack.back()) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500150 ImGui::SameLine();
Brian Osman7c979f52019-02-12 13:27:51 -0500151 if (ImGui::Button("+")) {
Brian Osman5de7ea42019-02-14 13:23:51 -0500152 ++count;
Brian Osman7c979f52019-02-12 13:27:51 -0500153 }
154 }
Brian Osman5de7ea42019-02-14 13:23:51 -0500155 return count;
156 }
157 ArrayEdit exitArray() override {
158 fArrayCounterStack.pop_back();
159 auto edit = fArrayEditStack.back();
160 fArrayEditStack.pop_back();
Brian Osman7c979f52019-02-12 13:27:51 -0500161 this->exitObject();
Brian Osman5de7ea42019-02-14 13:23:51 -0500162 return edit;
Brian Osman7c979f52019-02-12 13:27:51 -0500163 }
164
165private:
Brian Osman5de7ea42019-02-14 13:23:51 -0500166 const char* item(const char* name) {
167 if (name) {
168 return name;
169 }
170
171 // We're in an array. Add extra controls and a dynamic label.
172 int index = fArrayCounterStack.back()++;
173 ArrayEdit& edit(fArrayEditStack.back());
174 fScratchLabel = SkStringPrintf("[%d]", index);
175
176 ImGui::PushID(index);
177
178 if (ImGui::Button("X")) {
179 edit.fVerb = ArrayEdit::Verb::kRemove;
180 edit.fIndex = index;
181 }
182 ImGui::SameLine();
183 if (ImGui::Button("^")) {
184 edit.fVerb = ArrayEdit::Verb::kMoveForward;
185 edit.fIndex = index;
186 }
187 ImGui::SameLine();
188 if (ImGui::Button("v")) {
189 edit.fVerb = ArrayEdit::Verb::kMoveForward;
190 edit.fIndex = index + 1;
191 }
192 ImGui::SameLine();
193
194 ImGui::PopID();
195
196 return fScratchLabel.c_str();
197 }
198
Brian Osman7c979f52019-02-12 13:27:51 -0500199 SkSTArray<16, bool, true> fTreeStack;
Brian Osman5de7ea42019-02-14 13:23:51 -0500200 SkSTArray<16, int, true> fArrayCounterStack;
201 SkSTArray<16, ArrayEdit, true> fArrayEditStack;
202 SkString fScratchLabel;
Brian Osman7c979f52019-02-12 13:27:51 -0500203};
204
Brian Osman7c979f52019-02-12 13:27:51 -0500205ParticlesSlide::ParticlesSlide() {
206 // Register types for serialization
207 REGISTER_REFLECTED(SkReflected);
208 SkParticleAffector::RegisterAffectorTypes();
Brian Osman543d2e22019-02-15 14:29:38 -0500209 SkParticleDrawable::RegisterDrawableTypes();
Brian Osman7c979f52019-02-12 13:27:51 -0500210 fName = "Particles";
Brian Osmanb77d5022019-03-06 11:08:48 -0500211 fPlayPosition.set(200.0f, 200.0f);
212}
213
214void ParticlesSlide::loadEffects(const char* dirname) {
215 fLoaded.reset();
216 fRunning.reset();
217 SkOSFile::Iter iter(dirname, ".json");
218 for (SkString file; iter.next(&file); ) {
219 LoadedEffect effect;
220 effect.fName = SkOSPath::Join(dirname, file.c_str());
221 effect.fParams.reset(new SkParticleEffectParams());
222 if (auto fileData = SkData::MakeFromFileName(effect.fName.c_str())) {
223 skjson::DOM dom(static_cast<const char*>(fileData->data()), fileData->size());
224 SkFromJsonVisitor fromJson(dom.root());
225 effect.fParams->visitFields(&fromJson);
226 fLoaded.push_back(effect);
227 }
228 }
Ben Wagnera57d8682019-02-14 13:22:43 -0500229}
230
231void ParticlesSlide::load(SkScalar winWidth, SkScalar winHeight) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500232 this->loadEffects(GetResourcePath("particles").c_str());
Brian Osman7c979f52019-02-12 13:27:51 -0500233}
234
235void ParticlesSlide::draw(SkCanvas* canvas) {
236 canvas->clear(0);
237
238 gDragPoints.reset();
Brian Osmanb77d5022019-03-06 11:08:48 -0500239 gDragPoints.push_back(&fPlayPosition);
240
241 // Window to show all loaded effects, and allow playing them
242 if (ImGui::Begin("Library", nullptr, ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
Brian Osman5c1f8eb2019-02-14 14:49:55 -0500243 static bool looped = true;
244 ImGui::Checkbox("Looped", &looped);
Brian Osmanb77d5022019-03-06 11:08:48 -0500245
246 static SkString dirname = GetResourcePath("particles");
247 ImGuiInputTextFlags textFlags = ImGuiInputTextFlags_CallbackResize;
248 ImGui::InputText("Directory", dirname.writable_str(), dirname.size() + 1, textFlags,
249 InputTextCallback, &dirname);
250
251 if (ImGui::Button("New")) {
252 LoadedEffect effect;
253 effect.fName = SkOSPath::Join(dirname.c_str(), "new.json");
254 effect.fParams.reset(new SkParticleEffectParams());
255 fLoaded.push_back(effect);
Brian Osman5c1f8eb2019-02-14 14:49:55 -0500256 }
Brian Osmanb77d5022019-03-06 11:08:48 -0500257 ImGui::SameLine();
258
Brian Osman7c979f52019-02-12 13:27:51 -0500259 if (ImGui::Button("Load")) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500260 this->loadEffects(dirname.c_str());
Brian Osman7c979f52019-02-12 13:27:51 -0500261 }
262 ImGui::SameLine();
263
264 if (ImGui::Button("Save")) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500265 for (const auto& effect : fLoaded) {
266 SkFILEWStream fileStream(effect.fName.c_str());
267 if (fileStream.isValid()) {
268 SkJSONWriter writer(&fileStream, SkJSONWriter::Mode::kPretty);
269 SkToJsonVisitor toJson(writer);
270 writer.beginObject();
271 effect.fParams->visitFields(&toJson);
272 writer.endObject();
273 writer.flush();
274 fileStream.flush();
275 } else {
276 SkDebugf("Failed to open %s\n", effect.fName.c_str());
277 }
Brian Osman7c979f52019-02-12 13:27:51 -0500278 }
279 }
280
281 SkGuiVisitor gui;
Brian Osmanb77d5022019-03-06 11:08:48 -0500282 for (int i = 0; i < fLoaded.count(); ++i) {
283 ImGui::PushID(i);
Hal Canary41248072019-07-11 16:32:53 -0400284 if (fAnimated && ImGui::Button("Play")) {
Brian Osmanb77d5022019-03-06 11:08:48 -0500285 sk_sp<SkParticleEffect> effect(new SkParticleEffect(fLoaded[i].fParams, fRandom));
Hal Canary41248072019-07-11 16:32:53 -0400286 effect->start(fAnimationTime, looped);
Brian Osmanb77d5022019-03-06 11:08:48 -0500287 fRunning.push_back({ fPlayPosition, fLoaded[i].fName, effect });
288 }
289 ImGui::SameLine();
290
291 ImGui::InputText("##Name", fLoaded[i].fName.writable_str(), fLoaded[i].fName.size() + 1,
292 textFlags, InputTextCallback, &fLoaded[i].fName);
293
294 if (ImGui::TreeNode("##Details")) {
295 fLoaded[i].fParams->visitFields(&gui);
296 ImGui::TreePop();
297 }
298 ImGui::PopID();
299 }
300 }
301 ImGui::End();
302
303 // Another window to show all the running effects
304 if (ImGui::Begin("Running")) {
305 for (int i = 0; i < fRunning.count(); ++i) {
306 ImGui::PushID(i);
307 bool remove = ImGui::Button("X") || !fRunning[i].fEffect->isAlive();
308 ImGui::SameLine();
309 ImGui::Text("%4g, %4g %5d %s", fRunning[i].fPosition.fX, fRunning[i].fPosition.fY,
310 fRunning[i].fEffect->getCount(), fRunning[i].fName.c_str());
311 if (remove) {
312 fRunning.removeShuffle(i);
313 }
314 ImGui::PopID();
315 }
Brian Osman7c979f52019-02-12 13:27:51 -0500316 }
317 ImGui::End();
318
319 SkPaint dragPaint;
320 dragPaint.setColor(SK_ColorLTGRAY);
321 dragPaint.setAntiAlias(true);
322 SkPaint dragHighlight;
323 dragHighlight.setStyle(SkPaint::kStroke_Style);
324 dragHighlight.setColor(SK_ColorGREEN);
325 dragHighlight.setStrokeWidth(2);
326 dragHighlight.setAntiAlias(true);
327 for (int i = 0; i < gDragPoints.count(); ++i) {
328 canvas->drawCircle(*gDragPoints[i], kDragSize, dragPaint);
329 if (gDragIndex == i) {
330 canvas->drawCircle(*gDragPoints[i], kDragSize, dragHighlight);
331 }
332 }
Brian Osmanb77d5022019-03-06 11:08:48 -0500333 for (const auto& effect : fRunning) {
334 canvas->save();
335 canvas->translate(effect.fPosition.fX, effect.fPosition.fY);
336 effect.fEffect->draw(canvas);
337 canvas->restore();
338 }
Brian Osman7c979f52019-02-12 13:27:51 -0500339}
340
Hal Canary41248072019-07-11 16:32:53 -0400341bool ParticlesSlide::animate(double nanos) {
342 fAnimated = true;
343 fAnimationTime = 1e-9 * nanos;
Brian Osmanb77d5022019-03-06 11:08:48 -0500344 for (const auto& effect : fRunning) {
Hal Canary41248072019-07-11 16:32:53 -0400345 effect.fEffect->update(fAnimationTime);
Brian Osmanb77d5022019-03-06 11:08:48 -0500346 }
Brian Osman7c979f52019-02-12 13:27:51 -0500347 return true;
348}
349
Hal Canaryff2e8fe2019-07-16 09:58:43 -0400350bool ParticlesSlide::onMouse(SkScalar x, SkScalar y, InputState state, ModifierKey modifiers) {
Brian Osman7c979f52019-02-12 13:27:51 -0500351 if (gDragIndex == -1) {
Hal Canaryff2e8fe2019-07-16 09:58:43 -0400352 if (state == InputState::kDown) {
Brian Osman7c979f52019-02-12 13:27:51 -0500353 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);
Hal Canaryff2e8fe2019-07-16 09:58:43 -0400368 if (state == InputState::kUp) {
Brian Osman7c979f52019-02-12 13:27:51 -0500369 gDragIndex = -1;
370 }
371 return true;
372 }
373 return false;
374}