Home | History | Annotate | Download | only in viewer
      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