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