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