blob: a948c7e0b77d6fac2c6bfa4016b203adfe47a309 [file] [log] [blame]
/*
* 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 "SkParticleAffector.h"
#include "SkParticleEffect.h"
#include "SkParticleEmitter.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;
}
static ImVec2 map_point(float x, float y, ImVec2 pos, ImVec2 size, float yMin, float yMax) {
// Turn y into 0 - 1 value
float yNorm = 1.0f - ((y - yMin) / (yMax - yMin));
return ImVec2(pos.x + size.x * x, pos.y + size.y * yNorm);
}
static void ImGui_DrawCurve(SkScalar* pts) {
ImDrawList* drawList = ImGui::GetWindowDrawList();
// Fit our image/canvas to the available width, and scale the height to maintain aspect ratio.
float canvasWidth = SkTMax(ImGui::GetContentRegionAvailWidth(), 50.0f);
ImVec2 size = ImVec2(canvasWidth, canvasWidth);
ImVec2 pos = ImGui::GetCursorScreenPos();
// Background rectangle
drawList->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y), IM_COL32(0, 0, 0, 128));
// Determine min/max extents
float yMin = pts[0], yMax = pts[0];
for (int i = 1; i < 4; ++i) {
yMin = SkTMin(yMin, pts[i]);
yMax = SkTMax(yMax, pts[i]);
}
// Grow the extents by 10%, at least 1.0f
float grow = SkTMax((yMax - yMin) * 0.1f, 1.0f);
yMin -= grow;
yMax += grow;
ImVec2 a = map_point(0.0f , pts[0], pos, size, yMin, yMax),
b = map_point(1 / 3.0f, pts[1], pos, size, yMin, yMax),
c = map_point(2 / 3.0f, pts[2], pos, size, yMin, yMax),
d = map_point(1.0f , pts[3], pos, size, yMin, yMax);
drawList->AddBezierCurve(a, b, c, d, IM_COL32(255, 255, 255, 255), 1.0f);
// Draw markers
drawList->AddCircle(a, 5.0f, 0xFFFFFFFF);
drawList->AddCircle(b, 5.0f, 0xFFFFFFFF);
drawList->AddCircle(c, 5.0f, 0xFFFFFFFF);
drawList->AddCircle(d, 5.0f, 0xFFFFFFFF);
ImGui::SetCursorScreenPos(ImVec2(pos.x, pos.y + size.y));
ImGui::Spacing();
}
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, SkField field) override {
if (fTreeStack.back()) {
if (field.fFlags & SkField::kAngle_Field) {
ImGui::SliderAngle(item(name), &f, 0.0f);
} else {
ImGui::DragFloat(item(name), &f);
}
}
}
void visit(const char* name, int& i, SkField) override {
IF_OPEN(ImGui::DragInt(item(name), &i))
}
void visit(const char* name, bool& b, SkField) override {
IF_OPEN(ImGui::Checkbox(item(name), &b))
}
void visit(const char* name, SkString& s, SkField) 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, SkPoint& p, SkField) override {
if (fTreeStack.back()) {
ImGui::DragFloat2(item(name), &p.fX);
gDragPoints.push_back(&p);
}
}
void visit(const char* name, SkColor4f& c, SkField) override {
IF_OPEN(ImGui::ColorEdit4(item(name), c.vec()))
}
#undef IF_OPEN
void visit(const char* name, SkCurve& c, SkField) override {
this->enterObject(item(name));
if (fTreeStack.back()) {
ImGui::Checkbox("Ranged", &c.fRanged);
ImGui::DragFloat4("Min", c.fMin);
ImGui_DrawCurve(c.fMin);
if (c.fRanged) {
ImGui::DragFloat4("Max", c.fMax);
ImGui_DrawCurve(c.fMax);
}
}
this->exitObject();
}
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 = [curType,&e](const SkReflected::Type* t) {
if (ImGui::Selectable(t->fName, curType == t)) {
e = t->fFactory();
}
};
SkReflected::VisitTypes(visitType, baseType);
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;
};
static sk_sp<SkParticleEffectParams> LoadEffectParams(const char* filename) {
sk_sp<SkParticleEffectParams> params(new SkParticleEffectParams());
if (auto fileData = SkData::MakeFromFileName(filename)) {
skjson::DOM dom(static_cast<const char*>(fileData->data()), fileData->size());
SkFromJsonVisitor fromJson(dom.root());
params->visitFields(&fromJson);
}
return params;
}
ParticlesSlide::ParticlesSlide() {
// Register types for serialization
REGISTER_REFLECTED(SkReflected);
SkParticleAffector::RegisterAffectorTypes();
SkParticleEmitter::RegisterEmitterTypes();
fName = "Particles";
}
void ParticlesSlide::load(SkScalar winWidth, SkScalar winHeight) {
fEffect.reset(new SkParticleEffect(LoadEffectParams("resources/particles/default.json")));
}
void ParticlesSlide::draw(SkCanvas* canvas) {
canvas->clear(0);
gDragPoints.reset();
if (ImGui::Begin("Particles")) {
static char filename[64] = "resources/particles/default.json";
ImGui::InputText("Filename", filename, sizeof(filename));
if (ImGui::Button("Load")) {
if (auto newParams = LoadEffectParams(filename)) {
fEffect.reset(new SkParticleEffect(std::move(newParams)));
}
}
ImGui::SameLine();
if (ImGui::Button("Save")) {
SkFILEWStream fileStream(filename);
if (fileStream.isValid()) {
SkJSONWriter writer(&fileStream, SkJSONWriter::Mode::kPretty);
SkToJsonVisitor toJson(writer);
writer.beginObject();
fEffect->getParams()->visitFields(&toJson);
writer.endObject();
writer.flush();
fileStream.flush();
} else {
SkDebugf("Failed to open file\n");
}
}
SkGuiVisitor gui;
fEffect->getParams()->visitFields(&gui);
}
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);
}
}
fEffect->draw(canvas);
}
bool ParticlesSlide::animate(const SkAnimTimer& timer) {
fEffect->update(fRandom, timer);
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;
}