skia2/tools/viewer/ParticlesSlide.cpp
Brian Osman b77d502946 Particles: Cleanup and better UI
Remove SkAnimTimer from the module interface entirely.
Clean up some other SkParticleEffect methods. Simplify
VisitTypes to just visit all of them, it's easier for
the client to do any filtering.

In the slide, make the UI far nicer. Load all files in
a given directory, and allow editing (and saving) them
all at once, or adding a new entry. Support multiple
playing effects, with a draggable handle to set the
position.

Bug: skia:
Change-Id: I0bec4077f9135bc122569f1410bebc96d5439480
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/197243
Reviewed-by: Brian Osman <brianosman@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
2019-03-06 18:42:11 +00:00

358 lines
11 KiB
C++

/*
* Copyright 2019 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "ParticlesSlide.h"
#include "ImGuiLayer.h"
#include "Resources.h"
#include "SkAnimTimer.h"
#include "SkOSFile.h"
#include "SkOSPath.h"
#include "SkParticleAffector.h"
#include "SkParticleDrawable.h"
#include "SkParticleEffect.h"
#include "SkParticleSerialization.h"
#include "SkReflected.h"
#include "imgui.h"
using namespace sk_app;
namespace {
static SkScalar kDragSize = 8.0f;
static SkTArray<SkPoint*> gDragPoints;
int gDragIndex = -1;
}
///////////////////////////////////////////////////////////////////////////////
static int InputTextCallback(ImGuiInputTextCallbackData* data) {
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
SkString* s = (SkString*)data->UserData;
SkASSERT(data->Buf == s->writable_str());
SkString tmp(data->Buf, data->BufTextLen);
s->swap(tmp);
data->Buf = s->writable_str();
}
return 0;
}
class SkGuiVisitor : public SkFieldVisitor {
public:
SkGuiVisitor() {
fTreeStack.push_back(true);
}
#define IF_OPEN(WIDGET) if (fTreeStack.back()) { WIDGET; }
void visit(const char* name, float& f) override {
IF_OPEN(ImGui::DragFloat(item(name), &f))
}
void visit(const char* name, int& i) override {
IF_OPEN(ImGui::DragInt(item(name), &i))
}
void visit(const char* name, bool& b) override {
IF_OPEN(ImGui::Checkbox(item(name), &b))
}
void visit(const char* name, SkString& s) override {
if (fTreeStack.back()) {
ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize;
ImGui::InputText(item(name), s.writable_str(), s.size() + 1, flags, InputTextCallback,
&s);
}
}
void visit(const char* name, int& i, const EnumStringMapping* map, int count) override {
if (fTreeStack.back()) {
const char* curStr = EnumToString(i, map, count);
if (ImGui::BeginCombo(item(name), curStr ? curStr : "Unknown")) {
for (int j = 0; j < count; ++j) {
if (ImGui::Selectable(map[j].fName, i == map[j].fValue)) {
i = map[j].fValue;
}
}
ImGui::EndCombo();
}
}
}
void visit(const char* name, SkPoint& p) override {
if (fTreeStack.back()) {
ImGui::DragFloat2(item(name), &p.fX);
gDragPoints.push_back(&p);
}
}
void visit(const char* name, SkColor4f& c) override {
IF_OPEN(ImGui::ColorEdit4(item(name), c.vec()))
}
#undef IF_OPEN
void visit(sk_sp<SkReflected>& e, const SkReflected::Type* baseType) override {
if (fTreeStack.back()) {
const SkReflected::Type* curType = e ? e->getType() : nullptr;
if (ImGui::BeginCombo("Type", curType ? curType->fName : "Null")) {
auto visitType = [baseType, curType, &e](const SkReflected::Type* t) {
if (t->fFactory && (t == baseType || t->isDerivedFrom(baseType)) &&
ImGui::Selectable(t->fName, curType == t)) {
e = t->fFactory();
}
};
SkReflected::VisitTypes(visitType);
ImGui::EndCombo();
}
}
}
void enterObject(const char* name) override {
if (fTreeStack.back()) {
fTreeStack.push_back(ImGui::TreeNodeEx(item(name),
ImGuiTreeNodeFlags_AllowItemOverlap));
} else {
fTreeStack.push_back(false);
}
}
void exitObject() override {
if (fTreeStack.back()) {
ImGui::TreePop();
}
fTreeStack.pop_back();
}
int enterArray(const char* name, int oldCount) override {
this->enterObject(item(name));
fArrayCounterStack.push_back(0);
fArrayEditStack.push_back();
int count = oldCount;
if (fTreeStack.back()) {
ImGui::SameLine();
if (ImGui::Button("+")) {
++count;
}
}
return count;
}
ArrayEdit exitArray() override {
fArrayCounterStack.pop_back();
auto edit = fArrayEditStack.back();
fArrayEditStack.pop_back();
this->exitObject();
return edit;
}
private:
const char* item(const char* name) {
if (name) {
return name;
}
// We're in an array. Add extra controls and a dynamic label.
int index = fArrayCounterStack.back()++;
ArrayEdit& edit(fArrayEditStack.back());
fScratchLabel = SkStringPrintf("[%d]", index);
ImGui::PushID(index);
if (ImGui::Button("X")) {
edit.fVerb = ArrayEdit::Verb::kRemove;
edit.fIndex = index;
}
ImGui::SameLine();
if (ImGui::Button("^")) {
edit.fVerb = ArrayEdit::Verb::kMoveForward;
edit.fIndex = index;
}
ImGui::SameLine();
if (ImGui::Button("v")) {
edit.fVerb = ArrayEdit::Verb::kMoveForward;
edit.fIndex = index + 1;
}
ImGui::SameLine();
ImGui::PopID();
return fScratchLabel.c_str();
}
SkSTArray<16, bool, true> fTreeStack;
SkSTArray<16, int, true> fArrayCounterStack;
SkSTArray<16, ArrayEdit, true> fArrayEditStack;
SkString fScratchLabel;
};
ParticlesSlide::ParticlesSlide() {
// Register types for serialization
REGISTER_REFLECTED(SkReflected);
SkParticleAffector::RegisterAffectorTypes();
SkParticleDrawable::RegisterDrawableTypes();
fName = "Particles";
fPlayPosition.set(200.0f, 200.0f);
}
void ParticlesSlide::loadEffects(const char* dirname) {
fLoaded.reset();
fRunning.reset();
SkOSFile::Iter iter(dirname, ".json");
for (SkString file; iter.next(&file); ) {
LoadedEffect effect;
effect.fName = SkOSPath::Join(dirname, file.c_str());
effect.fParams.reset(new SkParticleEffectParams());
if (auto fileData = SkData::MakeFromFileName(effect.fName.c_str())) {
skjson::DOM dom(static_cast<const char*>(fileData->data()), fileData->size());
SkFromJsonVisitor fromJson(dom.root());
effect.fParams->visitFields(&fromJson);
fLoaded.push_back(effect);
}
}
}
void ParticlesSlide::load(SkScalar winWidth, SkScalar winHeight) {
this->loadEffects(GetResourcePath("particles").c_str());
}
void ParticlesSlide::draw(SkCanvas* canvas) {
canvas->clear(0);
gDragPoints.reset();
gDragPoints.push_back(&fPlayPosition);
// Window to show all loaded effects, and allow playing them
if (ImGui::Begin("Library", nullptr, ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
static bool looped = true;
ImGui::Checkbox("Looped", &looped);
static SkString dirname = GetResourcePath("particles");
ImGuiInputTextFlags textFlags = ImGuiInputTextFlags_CallbackResize;
ImGui::InputText("Directory", dirname.writable_str(), dirname.size() + 1, textFlags,
InputTextCallback, &dirname);
if (ImGui::Button("New")) {
LoadedEffect effect;
effect.fName = SkOSPath::Join(dirname.c_str(), "new.json");
effect.fParams.reset(new SkParticleEffectParams());
fLoaded.push_back(effect);
}
ImGui::SameLine();
if (ImGui::Button("Load")) {
this->loadEffects(dirname.c_str());
}
ImGui::SameLine();
if (ImGui::Button("Save")) {
for (const auto& effect : fLoaded) {
SkFILEWStream fileStream(effect.fName.c_str());
if (fileStream.isValid()) {
SkJSONWriter writer(&fileStream, SkJSONWriter::Mode::kPretty);
SkToJsonVisitor toJson(writer);
writer.beginObject();
effect.fParams->visitFields(&toJson);
writer.endObject();
writer.flush();
fileStream.flush();
} else {
SkDebugf("Failed to open %s\n", effect.fName.c_str());
}
}
}
SkGuiVisitor gui;
for (int i = 0; i < fLoaded.count(); ++i) {
ImGui::PushID(i);
if (fTimer && ImGui::Button("Play")) {
sk_sp<SkParticleEffect> effect(new SkParticleEffect(fLoaded[i].fParams, fRandom));
effect->start(fTimer->secs(), looped);
fRunning.push_back({ fPlayPosition, fLoaded[i].fName, effect });
}
ImGui::SameLine();
ImGui::InputText("##Name", fLoaded[i].fName.writable_str(), fLoaded[i].fName.size() + 1,
textFlags, InputTextCallback, &fLoaded[i].fName);
if (ImGui::TreeNode("##Details")) {
fLoaded[i].fParams->visitFields(&gui);
ImGui::TreePop();
}
ImGui::PopID();
}
}
ImGui::End();
// Another window to show all the running effects
if (ImGui::Begin("Running")) {
for (int i = 0; i < fRunning.count(); ++i) {
ImGui::PushID(i);
bool remove = ImGui::Button("X") || !fRunning[i].fEffect->isAlive();
ImGui::SameLine();
ImGui::Text("%4g, %4g %5d %s", fRunning[i].fPosition.fX, fRunning[i].fPosition.fY,
fRunning[i].fEffect->getCount(), fRunning[i].fName.c_str());
if (remove) {
fRunning.removeShuffle(i);
}
ImGui::PopID();
}
}
ImGui::End();
SkPaint dragPaint;
dragPaint.setColor(SK_ColorLTGRAY);
dragPaint.setAntiAlias(true);
SkPaint dragHighlight;
dragHighlight.setStyle(SkPaint::kStroke_Style);
dragHighlight.setColor(SK_ColorGREEN);
dragHighlight.setStrokeWidth(2);
dragHighlight.setAntiAlias(true);
for (int i = 0; i < gDragPoints.count(); ++i) {
canvas->drawCircle(*gDragPoints[i], kDragSize, dragPaint);
if (gDragIndex == i) {
canvas->drawCircle(*gDragPoints[i], kDragSize, dragHighlight);
}
}
for (const auto& effect : fRunning) {
canvas->save();
canvas->translate(effect.fPosition.fX, effect.fPosition.fY);
effect.fEffect->draw(canvas);
canvas->restore();
}
}
bool ParticlesSlide::animate(const SkAnimTimer& timer) {
fTimer = &timer;
for (const auto& effect : fRunning) {
effect.fEffect->update(timer.secs());
}
return true;
}
bool ParticlesSlide::onMouse(SkScalar x, SkScalar y, Window::InputState state, uint32_t modifiers) {
if (gDragIndex == -1) {
if (state == Window::kDown_InputState) {
float bestDistance = kDragSize;
SkPoint mousePt = { x, y };
for (int i = 0; i < gDragPoints.count(); ++i) {
float distance = SkPoint::Distance(*gDragPoints[i], mousePt);
if (distance < bestDistance) {
gDragIndex = i;
bestDistance = distance;
}
}
return gDragIndex != -1;
}
} else {
// Currently dragging
SkASSERT(gDragIndex < gDragPoints.count());
gDragPoints[gDragIndex]->set(x, y);
if (state == Window::kUp_InputState) {
gDragIndex = -1;
}
return true;
}
return false;
}