• 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 "include/core/SkCanvas.h"
11 #include "modules/particles/include/SkParticleEffect.h"
12 #include "modules/particles/include/SkParticleSerialization.h"
13 #include "modules/particles/include/SkReflected.h"
14 #include "modules/skresources/include/SkResources.h"
15 #include "src/core/SkOSFile.h"
16 #include "src/sksl/codegen/SkSLVMCodeGenerator.h"
17 #include "src/utils/SkOSPath.h"
18 #include "tools/Resources.h"
19 #include "tools/ToolUtils.h"
20 #include "tools/viewer/ImGuiLayer.h"
21 
22 #include "imgui.h"
23 
24 #include <string>
25 #include <unordered_map>
26 
27 using namespace sk_app;
28 
29 class TestingResourceProvider : public skresources::ResourceProvider {
30 public:
TestingResourceProvider()31     TestingResourceProvider() {}
32 
load(const char resource_path[],const char resource_name[]) const33     sk_sp<SkData> load(const char resource_path[], const char resource_name[]) const override {
34         auto it = fResources.find(resource_name);
35         if (it != fResources.end()) {
36             return it->second;
37         } else {
38             return GetResourceAsData(SkOSPath::Join(resource_path, resource_name).c_str());
39         }
40     }
41 
loadImageAsset(const char resource_path[],const char resource_name[],const char[]) const42     sk_sp<skresources::ImageAsset> loadImageAsset(const char resource_path[],
43                                                   const char resource_name[],
44                                                   const char /*resource_id*/[]) const override {
45         auto data = this->load(resource_path, resource_name);
46         return skresources::MultiFrameImageAsset::Make(data);
47     }
48 
addPath(const char resource_name[],const SkPath & path)49     void addPath(const char resource_name[], const SkPath& path) {
50         fResources[resource_name] = path.serialize();
51     }
52 
53 private:
54     std::unordered_map<std::string, sk_sp<SkData>> fResources;
55 };
56 
57 ///////////////////////////////////////////////////////////////////////////////
58 
InputTextCallback(ImGuiInputTextCallbackData * data)59 static int InputTextCallback(ImGuiInputTextCallbackData* data) {
60     if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
61         SkString* s = (SkString*)data->UserData;
62         SkASSERT(data->Buf == s->writable_str());
63         SkString tmp(data->Buf, data->BufTextLen);
64         s->swap(tmp);
65         data->Buf = s->writable_str();
66     }
67     return 0;
68 }
69 
count_lines(const SkString & s)70 static int count_lines(const SkString& s) {
71     int lines = 1;
72     for (size_t i = 0; i < s.size(); ++i) {
73         if (s[i] == '\n') {
74             ++lines;
75         }
76     }
77     return lines;
78 }
79 
80 class SkGuiVisitor : public SkFieldVisitor {
81 public:
SkGuiVisitor()82     SkGuiVisitor() {
83         fTreeStack.push_back(true);
84     }
85 
visit(const char * name,float & f)86     void visit(const char* name, float& f) override {
87         fDirty = (fTreeStack.back() && ImGui::DragFloat(item(name), &f)) || fDirty;
88     }
visit(const char * name,int & i)89     void visit(const char* name, int& i) override {
90         fDirty = (fTreeStack.back() && ImGui::DragInt(item(name), &i)) || fDirty;
91     }
visit(const char * name,bool & b)92     void visit(const char* name, bool& b) override {
93         fDirty = (fTreeStack.back() && ImGui::Checkbox(item(name), &b)) || fDirty;
94     }
95 
visit(const char * name,SkString & s)96     void visit(const char* name, SkString& s) override {
97         if (fTreeStack.back()) {
98             int lines = count_lines(s);
99             ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize;
100             if (lines > 1) {
101                 ImGui::LabelText("##Label", "%s", name);
102                 ImVec2 boxSize(-1.0f, ImGui::GetTextLineHeight() * (lines + 1));
103                 fDirty = ImGui::InputTextMultiline(item(name), s.writable_str(), s.size() + 1,
104                                                    boxSize, flags, InputTextCallback, &s)
105                       || fDirty;
106             } else {
107                 fDirty = ImGui::InputText(item(name), s.writable_str(), s.size() + 1, flags,
108                                           InputTextCallback, &s)
109                       || fDirty;
110             }
111         }
112     }
113 
visit(sk_sp<SkReflected> & e,const SkReflected::Type * baseType)114     void visit(sk_sp<SkReflected>& e, const SkReflected::Type* baseType) override {
115         if (fTreeStack.back()) {
116             const SkReflected::Type* curType = e ? e->getType() : nullptr;
117             if (ImGui::BeginCombo("Type", curType ? curType->fName : "Null")) {
118                 auto visitType = [baseType, curType, &e, this](const SkReflected::Type* t) {
119                     if (t->fFactory && (t == baseType || t->isDerivedFrom(baseType)) &&
120                         ImGui::Selectable(t->fName, curType == t)) {
121                         e = t->fFactory();
122                         fDirty = true;
123                     }
124                 };
125                 SkReflected::VisitTypes(visitType);
126                 ImGui::EndCombo();
127             }
128         }
129     }
130 
enterObject(const char * name)131     void enterObject(const char* name) override {
132         if (fTreeStack.back()) {
133             fTreeStack.push_back(ImGui::TreeNodeEx(item(name),
134                                                    ImGuiTreeNodeFlags_AllowItemOverlap));
135         } else {
136             fTreeStack.push_back(false);
137         }
138     }
exitObject()139     void exitObject() override {
140         if (fTreeStack.back()) {
141             ImGui::TreePop();
142         }
143         fTreeStack.pop_back();
144     }
145 
enterArray(const char * name,int oldCount)146     int enterArray(const char* name, int oldCount) override {
147         this->enterObject(item(name));
148         fArrayCounterStack.push_back(0);
149         fArrayEditStack.push_back();
150 
151         int count = oldCount;
152         if (fTreeStack.back()) {
153             ImGui::SameLine();
154             if (ImGui::Button("+")) {
155                 ++count;
156                 fDirty = true;
157             }
158         }
159         return count;
160     }
exitArray()161     ArrayEdit exitArray() override {
162         fArrayCounterStack.pop_back();
163         auto edit = fArrayEditStack.back();
164         fArrayEditStack.pop_back();
165         this->exitObject();
166         return edit;
167     }
168 
169     bool fDirty = false;
170 
171 private:
item(const char * name)172     const char* item(const char* name) {
173         if (name) {
174             return name;
175         }
176 
177         // We're in an array. Add extra controls and a dynamic label.
178         int index = fArrayCounterStack.back()++;
179         ArrayEdit& edit(fArrayEditStack.back());
180         fScratchLabel = SkStringPrintf("[%d]", index);
181 
182         ImGui::PushID(index);
183 
184         if (ImGui::Button("X")) {
185             edit.fVerb = ArrayEdit::Verb::kRemove;
186             edit.fIndex = index;
187             fDirty = true;
188         }
189         ImGui::SameLine();
190 
191         ImGui::PopID();
192 
193         return fScratchLabel.c_str();
194     }
195 
196     SkSTArray<16, bool, true> fTreeStack;
197     SkSTArray<16, int, true>  fArrayCounterStack;
198     SkSTArray<16, ArrayEdit, true> fArrayEditStack;
199     SkString fScratchLabel;
200 };
201 
ParticlesSlide()202 ParticlesSlide::ParticlesSlide() {
203     // Register types for serialization
204     SkParticleEffect::RegisterParticleTypes();
205     fName = "Particles";
206     auto provider = sk_make_sp<TestingResourceProvider>();
207     SkPath star = ToolUtils::make_star({ 0, 0, 100, 100 }, 5);
208     star.close();
209     provider->addPath("star", star);
210     fResourceProvider = provider;
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             effect.fParams->prepare(fResourceProvider.get());
226             fLoaded.push_back(effect);
227         }
228     }
229     std::sort(fLoaded.begin(), fLoaded.end(), [](const LoadedEffect& a, const LoadedEffect& b) {
230         return strcmp(a.fName.c_str(), b.fName.c_str()) < 0;
231     });
232 }
233 
load(SkScalar winWidth,SkScalar winHeight)234 void ParticlesSlide::load(SkScalar winWidth, SkScalar winHeight) {
235     this->loadEffects(GetResourcePath("particles").c_str());
236 }
237 
draw(SkCanvas * canvas)238 void ParticlesSlide::draw(SkCanvas* canvas) {
239     canvas->clear(SK_ColorGRAY);
240 
241     // Window to show all loaded effects, and allow playing them
242     if (ImGui::Begin("Library", nullptr, ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
243         static bool looped = true;
244         ImGui::Checkbox("Looped", &looped);
245 
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);
256         }
257         ImGui::SameLine();
258 
259         if (ImGui::Button("Load")) {
260             this->loadEffects(dirname.c_str());
261         }
262         ImGui::SameLine();
263 
264         if (ImGui::Button("Save")) {
265             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                 }
278             }
279         }
280 
281         SkGuiVisitor gui;
282         for (int i = 0; i < fLoaded.count(); ++i) {
283             ImGui::PushID(i);
284             if (fAnimated && ImGui::Button("Play")) {
285                 sk_sp<SkParticleEffect> effect(new SkParticleEffect(fLoaded[i].fParams));
286                 effect->start(fAnimationTime, looped, { 0, 0 }, { 0, -1 }, 1, { 0, 0 }, 0,
287                               { 1, 1, 1, 1 }, 0, fRandom.nextF());
288                 fRunning.push_back({ fLoaded[i].fName, effect, false });
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                 if (gui.fDirty) {
299                     fLoaded[i].fParams->prepare(fResourceProvider.get());
300                     gui.fDirty = false;
301                 }
302             }
303             ImGui::PopID();
304         }
305     }
306     ImGui::End();
307 
308     // Most effects are centered around the origin, so we shift the canvas...
309     constexpr SkVector kTranslation = { 250.0f, 250.0f };
310     const SkPoint mousePos = fMousePos - kTranslation;
311 
312     // Another window to show all the running effects
313     if (ImGui::Begin("Running")) {
314         for (int i = 0; i < fRunning.count(); ++i) {
315             SkParticleEffect* effect = fRunning[i].fEffect.get();
316             ImGui::PushID(effect);
317 
318             ImGui::Checkbox("##Track", &fRunning[i].fTrackMouse);
319             ImGui::SameLine();
320             bool remove = ImGui::Button("X") || !effect->isAlive();
321             ImGui::SameLine();
322             ImGui::Text("%5d %s", effect->getCount(), fRunning[i].fName.c_str());
323             if (fRunning[i].fTrackMouse) {
324                 effect->setPosition(mousePos);
325             }
326 
327             auto uniformsGui = [mousePos](const SkSL::UniformInfo* info, float* data) {
328                 if (!info || !data) {
329                     return;
330                 }
331                 for (size_t i = 0; i < info->fUniforms.size(); ++i) {
332                     const auto& uni = info->fUniforms[i];
333                     float* vals = data + uni.fSlot;
334 
335                     // Skip over builtin uniforms, to reduce clutter
336                     if (uni.fName == "dt" || uni.fName.starts_with("effect.")) {
337                         continue;
338                     }
339 
340                     // Special case for 'uniform float2 mouse_pos' - an example of likely app logic
341                     if (uni.fName == "mouse_pos" &&
342                         uni.fKind == SkSL::Type::NumberKind::kFloat &&
343                         uni.fRows == 2 && uni.fColumns == 1) {
344                         vals[0] = mousePos.fX;
345                         vals[1] = mousePos.fY;
346                         continue;
347                     }
348 
349                     if (uni.fKind == SkSL::Type::NumberKind::kBoolean) {
350                         for (int c = 0; c < uni.fColumns; ++c, vals += uni.fRows) {
351                             for (int r = 0; r < uni.fRows; ++r, ++vals) {
352                                 ImGui::PushID(c*uni.fRows + r);
353                                 if (r > 0) {
354                                     ImGui::SameLine();
355                                 }
356                                 ImGui::CheckboxFlags(r == uni.fRows - 1 ? uni.fName.c_str()
357                                                                         : "##Hidden",
358                                                      (unsigned int*)vals, ~0);
359                                 ImGui::PopID();
360                             }
361                         }
362                         continue;
363                     }
364 
365                     ImGuiDataType dataType = ImGuiDataType_COUNT;
366                     using NumberKind = SkSL::Type::NumberKind;
367                     switch (uni.fKind) {
368                         case NumberKind::kSigned:   dataType = ImGuiDataType_S32;   break;
369                         case NumberKind::kUnsigned: dataType = ImGuiDataType_U32;   break;
370                         case NumberKind::kFloat:    dataType = ImGuiDataType_Float; break;
371                         default:                                                    break;
372                     }
373                     SkASSERT(dataType != ImGuiDataType_COUNT);
374                     for (int c = 0; c < uni.fColumns; ++c, vals += uni.fRows) {
375                         ImGui::PushID(c);
376                         ImGui::DragScalarN(uni.fName.c_str(), dataType, vals, uni.fRows, 1.0f);
377                         ImGui::PopID();
378                     }
379                 }
380             };
381             uniformsGui(effect->uniformInfo(), effect->uniformData());
382             if (remove) {
383                 fRunning.removeShuffle(i);
384             }
385             ImGui::PopID();
386         }
387     }
388     ImGui::End();
389 
390     canvas->save();
391     canvas->translate(kTranslation.fX, kTranslation.fY);
392     for (const auto& effect : fRunning) {
393         effect.fEffect->draw(canvas);
394     }
395     canvas->restore();
396 }
397 
animate(double nanos)398 bool ParticlesSlide::animate(double nanos) {
399     fAnimated = true;
400     fAnimationTime = 1e-9 * nanos;
401     for (const auto& effect : fRunning) {
402         effect.fEffect->update(fAnimationTime);
403     }
404     return true;
405 }
406 
onMouse(SkScalar x,SkScalar y,skui::InputState state,skui::ModifierKey modifiers)407 bool ParticlesSlide::onMouse(SkScalar x, SkScalar y, skui::InputState state, skui::ModifierKey modifiers) {
408     fMousePos.set(x, y);
409     return false;
410 }
411