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