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 "ParticlesSlide.h" 9 10 #include "ImGuiLayer.h" 11 #include "Resources.h" 12 #include "SkAnimTimer.h" 13 #include "SkOSFile.h" 14 #include "SkOSPath.h" 15 #include "SkParticleAffector.h" 16 #include "SkParticleDrawable.h" 17 #include "SkParticleEffect.h" 18 #include "SkParticleSerialization.h" 19 #include "SkReflected.h" 20 21 #include "imgui.h" 22 23 using namespace sk_app; 24 25 namespace { 26 27 static SkScalar kDragSize = 8.0f; 28 static SkTArray<SkPoint*> gDragPoints; 29 int gDragIndex = -1; 30 31 } 32 33 /////////////////////////////////////////////////////////////////////////////// 34 35 static int InputTextCallback(ImGuiInputTextCallbackData* data) { 36 if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { 37 SkString* s = (SkString*)data->UserData; 38 SkASSERT(data->Buf == s->writable_str()); 39 SkString tmp(data->Buf, data->BufTextLen); 40 s->swap(tmp); 41 data->Buf = s->writable_str(); 42 } 43 return 0; 44 } 45 46 class SkGuiVisitor : public SkFieldVisitor { 47 public: 48 SkGuiVisitor() { 49 fTreeStack.push_back(true); 50 } 51 52 #define IF_OPEN(WIDGET) if (fTreeStack.back()) { WIDGET; } 53 54 void visit(const char* name, float& f) override { 55 IF_OPEN(ImGui::DragFloat(item(name), &f)) 56 } 57 void visit(const char* name, int& i) override { 58 IF_OPEN(ImGui::DragInt(item(name), &i)) 59 } 60 void visit(const char* name, bool& b) override { 61 IF_OPEN(ImGui::Checkbox(item(name), &b)) 62 } 63 void visit(const char* name, SkString& s) override { 64 if (fTreeStack.back()) { 65 ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize; 66 ImGui::InputText(item(name), s.writable_str(), s.size() + 1, flags, InputTextCallback, 67 &s); 68 } 69 } 70 void visit(const char* name, int& i, const EnumStringMapping* map, int count) override { 71 if (fTreeStack.back()) { 72 const char* curStr = EnumToString(i, map, count); 73 if (ImGui::BeginCombo(item(name), curStr ? curStr : "Unknown")) { 74 for (int j = 0; j < count; ++j) { 75 if (ImGui::Selectable(map[j].fName, i == map[j].fValue)) { 76 i = map[j].fValue; 77 } 78 } 79 ImGui::EndCombo(); 80 } 81 } 82 } 83 84 void visit(const char* name, SkPoint& p) override { 85 if (fTreeStack.back()) { 86 ImGui::DragFloat2(item(name), &p.fX); 87 gDragPoints.push_back(&p); 88 } 89 } 90 void visit(const char* name, SkColor4f& c) override { 91 IF_OPEN(ImGui::ColorEdit4(item(name), c.vec())) 92 } 93 94 #undef IF_OPEN 95 96 void visit(sk_sp<SkReflected>& e, const SkReflected::Type* baseType) override { 97 if (fTreeStack.back()) { 98 const SkReflected::Type* curType = e ? e->getType() : nullptr; 99 if (ImGui::BeginCombo("Type", curType ? curType->fName : "Null")) { 100 auto visitType = [baseType, curType, &e](const SkReflected::Type* t) { 101 if (t->fFactory && (t == baseType || t->isDerivedFrom(baseType)) && 102 ImGui::Selectable(t->fName, curType == t)) { 103 e = t->fFactory(); 104 } 105 }; 106 SkReflected::VisitTypes(visitType); 107 ImGui::EndCombo(); 108 } 109 } 110 } 111 112 void enterObject(const char* name) override { 113 if (fTreeStack.back()) { 114 fTreeStack.push_back(ImGui::TreeNodeEx(item(name), 115 ImGuiTreeNodeFlags_AllowItemOverlap)); 116 } else { 117 fTreeStack.push_back(false); 118 } 119 } 120 void exitObject() override { 121 if (fTreeStack.back()) { 122 ImGui::TreePop(); 123 } 124 fTreeStack.pop_back(); 125 } 126 127 int enterArray(const char* name, int oldCount) override { 128 this->enterObject(item(name)); 129 fArrayCounterStack.push_back(0); 130 fArrayEditStack.push_back(); 131 132 int count = oldCount; 133 if (fTreeStack.back()) { 134 ImGui::SameLine(); 135 if (ImGui::Button("+")) { 136 ++count; 137 } 138 } 139 return count; 140 } 141 ArrayEdit exitArray() override { 142 fArrayCounterStack.pop_back(); 143 auto edit = fArrayEditStack.back(); 144 fArrayEditStack.pop_back(); 145 this->exitObject(); 146 return edit; 147 } 148 149 private: 150 const char* item(const char* name) { 151 if (name) { 152 return name; 153 } 154 155 // We're in an array. Add extra controls and a dynamic label. 156 int index = fArrayCounterStack.back()++; 157 ArrayEdit& edit(fArrayEditStack.back()); 158 fScratchLabel = SkStringPrintf("[%d]", index); 159 160 ImGui::PushID(index); 161 162 if (ImGui::Button("X")) { 163 edit.fVerb = ArrayEdit::Verb::kRemove; 164 edit.fIndex = index; 165 } 166 ImGui::SameLine(); 167 if (ImGui::Button("^")) { 168 edit.fVerb = ArrayEdit::Verb::kMoveForward; 169 edit.fIndex = index; 170 } 171 ImGui::SameLine(); 172 if (ImGui::Button("v")) { 173 edit.fVerb = ArrayEdit::Verb::kMoveForward; 174 edit.fIndex = index + 1; 175 } 176 ImGui::SameLine(); 177 178 ImGui::PopID(); 179 180 return fScratchLabel.c_str(); 181 } 182 183 SkSTArray<16, bool, true> fTreeStack; 184 SkSTArray<16, int, true> fArrayCounterStack; 185 SkSTArray<16, ArrayEdit, true> fArrayEditStack; 186 SkString fScratchLabel; 187 }; 188 189 ParticlesSlide::ParticlesSlide() { 190 // Register types for serialization 191 REGISTER_REFLECTED(SkReflected); 192 SkParticleAffector::RegisterAffectorTypes(); 193 SkParticleDrawable::RegisterDrawableTypes(); 194 fName = "Particles"; 195 fPlayPosition.set(200.0f, 200.0f); 196 } 197 198 void ParticlesSlide::loadEffects(const char* dirname) { 199 fLoaded.reset(); 200 fRunning.reset(); 201 SkOSFile::Iter iter(dirname, ".json"); 202 for (SkString file; iter.next(&file); ) { 203 LoadedEffect effect; 204 effect.fName = SkOSPath::Join(dirname, file.c_str()); 205 effect.fParams.reset(new SkParticleEffectParams()); 206 if (auto fileData = SkData::MakeFromFileName(effect.fName.c_str())) { 207 skjson::DOM dom(static_cast<const char*>(fileData->data()), fileData->size()); 208 SkFromJsonVisitor fromJson(dom.root()); 209 effect.fParams->visitFields(&fromJson); 210 fLoaded.push_back(effect); 211 } 212 } 213 } 214 215 void ParticlesSlide::load(SkScalar winWidth, SkScalar winHeight) { 216 this->loadEffects(GetResourcePath("particles").c_str()); 217 } 218 219 void ParticlesSlide::draw(SkCanvas* canvas) { 220 canvas->clear(0); 221 222 gDragPoints.reset(); 223 gDragPoints.push_back(&fPlayPosition); 224 225 // Window to show all loaded effects, and allow playing them 226 if (ImGui::Begin("Library", nullptr, ImGuiWindowFlags_AlwaysVerticalScrollbar)) { 227 static bool looped = true; 228 ImGui::Checkbox("Looped", &looped); 229 230 static SkString dirname = GetResourcePath("particles"); 231 ImGuiInputTextFlags textFlags = ImGuiInputTextFlags_CallbackResize; 232 ImGui::InputText("Directory", dirname.writable_str(), dirname.size() + 1, textFlags, 233 InputTextCallback, &dirname); 234 235 if (ImGui::Button("New")) { 236 LoadedEffect effect; 237 effect.fName = SkOSPath::Join(dirname.c_str(), "new.json"); 238 effect.fParams.reset(new SkParticleEffectParams()); 239 fLoaded.push_back(effect); 240 } 241 ImGui::SameLine(); 242 243 if (ImGui::Button("Load")) { 244 this->loadEffects(dirname.c_str()); 245 } 246 ImGui::SameLine(); 247 248 if (ImGui::Button("Save")) { 249 for (const auto& effect : fLoaded) { 250 SkFILEWStream fileStream(effect.fName.c_str()); 251 if (fileStream.isValid()) { 252 SkJSONWriter writer(&fileStream, SkJSONWriter::Mode::kPretty); 253 SkToJsonVisitor toJson(writer); 254 writer.beginObject(); 255 effect.fParams->visitFields(&toJson); 256 writer.endObject(); 257 writer.flush(); 258 fileStream.flush(); 259 } else { 260 SkDebugf("Failed to open %s\n", effect.fName.c_str()); 261 } 262 } 263 } 264 265 SkGuiVisitor gui; 266 for (int i = 0; i < fLoaded.count(); ++i) { 267 ImGui::PushID(i); 268 if (fTimer && ImGui::Button("Play")) { 269 sk_sp<SkParticleEffect> effect(new SkParticleEffect(fLoaded[i].fParams, fRandom)); 270 effect->start(fTimer->secs(), looped); 271 fRunning.push_back({ fPlayPosition, fLoaded[i].fName, effect }); 272 } 273 ImGui::SameLine(); 274 275 ImGui::InputText("##Name", fLoaded[i].fName.writable_str(), fLoaded[i].fName.size() + 1, 276 textFlags, InputTextCallback, &fLoaded[i].fName); 277 278 if (ImGui::TreeNode("##Details")) { 279 fLoaded[i].fParams->visitFields(&gui); 280 ImGui::TreePop(); 281 } 282 ImGui::PopID(); 283 } 284 } 285 ImGui::End(); 286 287 // Another window to show all the running effects 288 if (ImGui::Begin("Running")) { 289 for (int i = 0; i < fRunning.count(); ++i) { 290 ImGui::PushID(i); 291 bool remove = ImGui::Button("X") || !fRunning[i].fEffect->isAlive(); 292 ImGui::SameLine(); 293 ImGui::Text("%4g, %4g %5d %s", fRunning[i].fPosition.fX, fRunning[i].fPosition.fY, 294 fRunning[i].fEffect->getCount(), fRunning[i].fName.c_str()); 295 if (remove) { 296 fRunning.removeShuffle(i); 297 } 298 ImGui::PopID(); 299 } 300 } 301 ImGui::End(); 302 303 SkPaint dragPaint; 304 dragPaint.setColor(SK_ColorLTGRAY); 305 dragPaint.setAntiAlias(true); 306 SkPaint dragHighlight; 307 dragHighlight.setStyle(SkPaint::kStroke_Style); 308 dragHighlight.setColor(SK_ColorGREEN); 309 dragHighlight.setStrokeWidth(2); 310 dragHighlight.setAntiAlias(true); 311 for (int i = 0; i < gDragPoints.count(); ++i) { 312 canvas->drawCircle(*gDragPoints[i], kDragSize, dragPaint); 313 if (gDragIndex == i) { 314 canvas->drawCircle(*gDragPoints[i], kDragSize, dragHighlight); 315 } 316 } 317 for (const auto& effect : fRunning) { 318 canvas->save(); 319 canvas->translate(effect.fPosition.fX, effect.fPosition.fY); 320 effect.fEffect->draw(canvas); 321 canvas->restore(); 322 } 323 } 324 325 bool ParticlesSlide::animate(const SkAnimTimer& timer) { 326 fTimer = &timer; 327 for (const auto& effect : fRunning) { 328 effect.fEffect->update(timer.secs()); 329 } 330 return true; 331 } 332 333 bool ParticlesSlide::onMouse(SkScalar x, SkScalar y, Window::InputState state, uint32_t modifiers) { 334 if (gDragIndex == -1) { 335 if (state == Window::kDown_InputState) { 336 float bestDistance = kDragSize; 337 SkPoint mousePt = { x, y }; 338 for (int i = 0; i < gDragPoints.count(); ++i) { 339 float distance = SkPoint::Distance(*gDragPoints[i], mousePt); 340 if (distance < bestDistance) { 341 gDragIndex = i; 342 bestDistance = distance; 343 } 344 } 345 return gDragIndex != -1; 346 } 347 } else { 348 // Currently dragging 349 SkASSERT(gDragIndex < gDragPoints.count()); 350 gDragPoints[gDragIndex]->set(x, y); 351 if (state == Window::kUp_InputState) { 352 gDragIndex = -1; 353 } 354 return true; 355 } 356 return false; 357 } 358