• 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