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