Particle effect scripting update

This change adds another layer of complexity and control to
the particle system. There are now two code chunks: the old
code that's run per-particle, and new code that's run for
the effect itself. This allows for effect lifetime to be set
by the script (eg, randomly), as well as the emission rate.
Rate can vary over time (see pulse.json), and particles can
be emitted in bursts by setting the effect's burst field
(see fireworks.json).

Additionally, the effect has its own frame of reference and
color, which becomes the default state for newly emitted
particles. This allows synchronizing state across particles
in various interesting ways (see color in fireworks.json).

Change-Id: Iec2f7a3427ce1d6411ed7ef5b3023cbef2e8a134
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/240498
Reviewed-by: Brian Osman <brianosman@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/modules/canvaskit/canvaskit/extra.html b/modules/canvaskit/canvaskit/extra.html
index b2017c1..ece46cd 100644
--- a/modules/canvaskit/canvaskit/extra.html
+++ b/modules/canvaskit/canvaskit/extra.html
@@ -193,15 +193,20 @@
 
 const snowfall = {
    "MaxCount": 4096,
-   "Duration": 1,
-   "Rate": 30,
    "Drawable": {
       "Type": "SkCircleDrawable",
       "Radius": 1
    },
-   "Code": [
-      "// float rand; Every read returns a random float [0 .. 1)",
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.rate = 30;",
+      "}",
       "",
+      "void effectUpdate(inout Effect effect) {",
+      "}",
+      ""
+   ],
+   "Code": [
       "void spawn(inout Particle p) {",
       "  p.lifetime = 10;",
       "  p.vel.y = 10 + rand * 20;",
diff --git a/modules/particles/include/SkParticleEffect.h b/modules/particles/include/SkParticleEffect.h
index 2306257..691b15f 100644
--- a/modules/particles/include/SkParticleEffect.h
+++ b/modules/particles/include/SkParticleEffect.h
@@ -8,6 +8,8 @@
 #ifndef SkParticleEffect_DEFINED
 #define SkParticleEffect_DEFINED
 
+#include "include/core/SkColor.h"
+#include "include/core/SkPoint.h"
 #include "include/core/SkRefCnt.h"
 #include "include/core/SkString.h"
 #include "include/private/SkTArray.h"
@@ -31,21 +33,26 @@
 public:
     SkParticleEffectParams();
 
-    int   fMaxCount;        // Maximum number of particles per instance of the effect
-    float fEffectDuration;  // How long does the effect last after being played, in seconds?
-    float fRate;            // How many particles are emitted per second?
+    // Maximum number of particles per instance of the effect
+    int   fMaxCount;
 
     // What is drawn for each particle? (Image, shape, sprite sheet, etc.)
     // See SkParticleDrawable::Make*
     sk_sp<SkParticleDrawable> fDrawable;
 
-    // Particle behavior is driven by two SkSL functions defined in the fCode string.
-    // Both functions get a mutable Particle struct:
+    // Particle behavior is driven by two chunks of SkSL code. Effect functions are defined in
+    // fEffectCode, and get a mutable Effect struct:
     //
-    // struct Particle {
-    //   float  age;
-    //   float  lifetime;
-    //   float2 pos   = { 0, 0 };        // Local position, relative to the effect.
+    // struct Effect {
+    //   float age;
+    //   float lifetime;
+    //   int   loop;
+    //   float rate;
+    //   int   burst;                    // Set to trigger a burst of particles.
+    //
+    //   // Everything below this line controls the state of the effect, which is also the
+    //   // default values for new particles.
+    //   float2 pos   = { 0, 0 };        // Local position
     //   float2 dir   = { 0, -1 };       // Heading. Should be a normalized vector.
     //   float  scale = 1;               // Size, normalized relative to the drawable's native size
     //   float2 vel   = { 0, 0 };        // Linear velocity, in (units / second)
@@ -54,24 +61,48 @@
     //   float  frame = 0;               // Normalized sprite index for multi-frame drawables
     // };
     //
-    // In addition, both functions have access to a global variable named 'rand'. Every read of
-    // 'rand' returns a random floating point value in [0, 1). The random generator is stored
-    // per-particle, and the state is rewound after each update, so calls to 'rand' will return
-    // consistent values from one update to the next.
+    // Particle functions are defined in fParticleCode, and get a mutable Particle struct, as well
+    // as a uniform copy of the current Effect, named 'effect'.
     //
-    // Finally, there are two global uniform values available. The first is 'dt', a floating point
-    // number of seconds that have elapsed since the last update. The second is 'effectAge', which
-    // is the normalized age of the effect (not particle). For looping effects, this will wrap
-    // back to zero when the effect's age exceeds its duration.
+    // struct Particle {
+    //   float  age;
+    //   float  lifetime;
+    //   float2 pos;
+    //   float2 dir;
+    //   float  scale;
+    //   float2 vel;
+    //   float  spin;
+    //   float4 color;
+    //   float  frame;
+    // };
+    //
+    // All functions have access to a global variable named 'rand'. Every read of 'rand' returns a
+    // random floating point value in [0, 1). For particle functions, the state is rewound after
+    // each update, so calls to 'rand' will return consistent values from one update to the next.
+    //
+    // Finally, there is one global uniform values available, 'dt'. This is a floating point
+    // number of seconds that have elapsed since the last update.
+    //
+    // Effect code should define two functions:
+    //
+    // 'void effectSpawn(inout Effect e)' is called when an instance of the effect is first
+    // created, and again at every loop point (if the effect is played with the looping flag).
+    //
+    // 'void effectUpdate(inout Effect e)' is called once per update to adjust properties of the
+    // effect (ie emitter).
+    //
+    // Particle code should also define two functions:
     //
     // 'void spawn(inout Particle p)' is called once for each particle when it is first created,
     // to set initial values. At a minimum, this should set 'lifetime' to the number of seconds
-    // that the particle will exist. Other parameters have defaults shown above.
+    // that the particle will exist. Other parameters will will get default values from the effect.
     //
     // 'void update(inout Particle p)' is called for each particle on every call to the running
     // SkParticleEffect's update() method. It can animate any of the particle's values. Note that
     // the 'lifetime' field has a different meaning in 'update', and should not be used or changed.
-    SkString fCode;
+
+    SkString fEffectCode;
+    SkString fParticleCode;
 
     // External objects accessible by the effect's SkSL code. Each binding is a name and particular
     // kind of object. See SkParticleBinding::Make* for details.
@@ -83,8 +114,13 @@
     friend class SkParticleEffect;
 
     // Cached
-    std::unique_ptr<SkSL::ByteCode> fByteCode;
-    SkTArray<std::unique_ptr<SkParticleExternalValue>> fExternalValues;
+    struct Program {
+        std::unique_ptr<SkSL::ByteCode> fByteCode;
+        SkTArray<std::unique_ptr<SkParticleExternalValue>> fExternalValues;
+    };
+
+    Program fEffectProgram;
+    Program fParticleProgram;
 
     void rebuild();
 };
@@ -97,7 +133,7 @@
     void update(double now);
     void draw(SkCanvas* canvas);
 
-    bool isAlive() const { return fEffectAge >= 0 && fEffectAge <= 1; }
+    bool isAlive() const { return fState.fAge >= 0 && fState.fAge <= 1; }
     int getCount() const { return fCount; }
 
     static void RegisterParticleTypes();
@@ -110,12 +146,34 @@
     SkRandom fRandom;
 
     bool   fLooping;
-    float  fEffectAge;
-
     int    fCount;
     double fLastTime;
     float  fSpawnRemainder;
 
+    // Effect-associated values exposed to script. They are some mix of uniform and inout,
+    // depending on whether we're executing per-feffect or per-particle scripts.
+    struct EffectState {
+        float fDeltaTime;
+
+        // Above this line is always uniform. Below is uniform for particles, inout for effect.
+
+        float fAge;
+        float fLifetime;
+        int   fLoopCount;
+        float fRate;
+        int   fBurst;
+
+        // Properties that determine default values for new particles
+        SkPoint   fPosition;
+        SkVector  fHeading;
+        float     fScale;
+        SkVector  fVelocity;
+        float     fSpin;
+        SkColor4f fColor;
+        float     fFrame;
+    };
+    EffectState fState;
+
     SkParticles             fParticles;
     SkAutoTMalloc<SkRandom> fStableRandoms;
 
diff --git a/modules/particles/src/SkParticleEffect.cpp b/modules/particles/src/SkParticleEffect.cpp
index 965f16a..32c6174 100644
--- a/modules/particles/src/SkParticleEffect.cpp
+++ b/modules/particles/src/SkParticleEffect.cpp
@@ -28,11 +28,29 @@
     }
 };
 
-static const char* kCodeHeader =
+static const char* kCommonHeader =
 R"(
-layout(ctype=float) in uniform float dt;
-layout(ctype=float) in uniform float effectAge;
+struct Effect {
+  float  age;
+  float  lifetime;
+  int    loop;
+  float  rate;
+  int    burst;
 
+  float2 pos;
+  float2 dir;
+  float  scale;
+  float2 vel;
+  float  spin;
+  float4 color;
+  float  frame;
+};
+
+in uniform float dt;
+)";
+
+static const char* kParticleHeader =
+R"(
 struct Particle {
   float  age;
   float  lifetime;
@@ -44,12 +62,20 @@
   float4 color;
   float  frame;
 };
+
+in uniform Effect effect;
 )";
 
-static const char* kDefaultCode =
-R"(// float rand; Every read returns a random float [0 .. 1)
+static const char* kDefaultEffectCode =
+R"(void effectSpawn(inout Effect effect) {
+}
 
-void spawn(inout Particle p) {
+void effectUpdate(inout Effect effect) {
+}
+)";
+
+static const char* kDefaultParticleCode =
+R"(void spawn(inout Particle p) {
 }
 
 void update(inout Particle p) {
@@ -58,86 +84,114 @@
 
 SkParticleEffectParams::SkParticleEffectParams()
         : fMaxCount(128)
-        , fEffectDuration(1.0f)
-        , fRate(8.0f)
         , fDrawable(nullptr)
-        , fCode(kDefaultCode) {
+        , fEffectCode(kDefaultEffectCode)
+        , fParticleCode(kDefaultParticleCode) {
     this->rebuild();
 }
 
 void SkParticleEffectParams::visitFields(SkFieldVisitor* v) {
-    SkString oldCode = fCode;
+    SkString oldEffectCode = fEffectCode;
+    SkString oldParticleCode = fParticleCode;
 
     v->visit("MaxCount", fMaxCount);
-    v->visit("Duration", fEffectDuration);
-    v->visit("Rate", fRate);
 
     v->visit("Drawable", fDrawable);
 
-    v->visit("Code", fCode);
+    v->visit("EffectCode", fEffectCode);
+    v->visit("Code", fParticleCode);
 
     v->visit("Bindings", fBindings);
 
     // TODO: Or, if any change to binding metadata?
-    if (fCode != oldCode) {
+    if (fParticleCode != oldParticleCode || fEffectCode != oldEffectCode) {
         this->rebuild();
     }
 }
 
 void SkParticleEffectParams::rebuild() {
-    SkSL::Compiler compiler;
-    SkSL::Program::Settings settings;
+    auto buildProgram = [this](const SkSL::String& code, Program* p) {
+        SkSL::Compiler compiler;
+        SkSL::Program::Settings settings;
 
-    SkTArray<std::unique_ptr<SkParticleExternalValue>> externalValues;
+        SkTArray<std::unique_ptr<SkParticleExternalValue>> externalValues;
 
-    auto rand = skstd::make_unique<SkRandomExternalValue>("rand", compiler);
-    compiler.registerExternalValue(rand.get());
-    externalValues.push_back(std::move(rand));
+        auto rand = skstd::make_unique<SkRandomExternalValue>("rand", compiler);
+        compiler.registerExternalValue(rand.get());
+        externalValues.push_back(std::move(rand));
 
-    for (const auto& binding : fBindings) {
-        if (binding) {
-            auto value = binding->toValue(compiler);
-            compiler.registerExternalValue(value.get());
-            externalValues.push_back(std::move(value));
+        for (const auto& binding : fBindings) {
+            if (binding) {
+                auto value = binding->toValue(compiler);
+                compiler.registerExternalValue(value.get());
+                externalValues.push_back(std::move(value));
+            }
         }
-    }
 
-    SkSL::String code(kCodeHeader);
-    code.append(fCode.c_str());
+        auto program = compiler.convertProgram(SkSL::Program::kGeneric_Kind, code, settings);
+        if (!program) {
+            SkDebugf("%s\n", compiler.errorText().c_str());
+            return;
+        }
 
-    auto program = compiler.convertProgram(SkSL::Program::kGeneric_Kind, code, settings);
-    if (!program) {
-        SkDebugf("%s\n", compiler.errorText().c_str());
-        return;
-    }
+        auto byteCode = compiler.toByteCode(*program);
+        if (!byteCode) {
+            SkDebugf("%s\n", compiler.errorText().c_str());
+            return;
+        }
 
-    auto byteCode = compiler.toByteCode(*program);
-    if (!byteCode) {
-        SkDebugf("%s\n", compiler.errorText().c_str());
-        return;
-    }
+        p->fByteCode = std::move(byteCode);
+        p->fExternalValues.swap(externalValues);
+    };
 
-    fByteCode = std::move(byteCode);
-    fExternalValues.swap(externalValues);
+    SkSL::String effectCode(kCommonHeader);
+    effectCode.append(fEffectCode.c_str());
+
+    SkSL::String particleCode(kCommonHeader);
+    particleCode.append(kParticleHeader);
+    particleCode.append(fParticleCode.c_str());
+
+    buildProgram(effectCode, &fEffectProgram);
+    buildProgram(particleCode, &fParticleProgram);
 }
 
 SkParticleEffect::SkParticleEffect(sk_sp<SkParticleEffectParams> params, const SkRandom& random)
         : fParams(std::move(params))
         , fRandom(random)
         , fLooping(false)
-        , fEffectAge(-1.0)
         , fCount(0)
         , fLastTime(-1.0)
         , fSpawnRemainder(0.0f) {
+    fState.fAge = -1.0f;
     this->setCapacity(fParams->fMaxCount);
 }
 
 void SkParticleEffect::start(double now, bool looping) {
     fCount = 0;
     fLastTime = now;
-    fEffectAge = 0.0f;
     fSpawnRemainder = 0.0f;
     fLooping = looping;
+
+    fState.fDeltaTime = 0.0f;
+    fState.fAge = 0.0f;
+
+    // A default lifetime makes sense - many effects are simple loops that don't really care.
+    // Every effect should define its own rate of emission, or only use bursts, so leave that as
+    // zero initially.
+    fState.fLifetime = 1.0f;
+    fState.fLoopCount = 0;
+    fState.fRate = 0.0f;
+    fState.fBurst = 0;
+
+    fState.fPosition = { 0.0f, 0.0f };
+    fState.fHeading  = { 0.0f, -1.0f };
+    fState.fScale    = 1.0f;
+    fState.fVelocity = { 0.0f, 0.0f };
+    fState.fSpin     = 0.0f;
+    fState.fColor    = { 1.0f, 1.0f, 1.0f, 1.0f };
+    fState.fFrame    = 0.0f;
+
+    // Defer running effectSpawn until the first update (to reuse the code when looping)
 }
 
 void SkParticleEffect::update(double now) {
@@ -145,8 +199,10 @@
         return;
     }
 
-    float deltaTime = static_cast<float>(now - fLastTime);
-    if (deltaTime <= 0.0f) {
+    // TODO: Sub-frame spawning. Tricky with script driven position. Supply variable effect.age?
+    // Could be done if effect.age were an external value that offset by particle lane, perhaps.
+    fState.fDeltaTime = static_cast<float>(now - fLastTime);
+    if (fState.fDeltaTime <= 0.0f) {
         return;
     }
     fLastTime = now;
@@ -156,22 +212,39 @@
         this->setCapacity(fParams->fMaxCount);
     }
 
-    fEffectAge += deltaTime / fParams->fEffectDuration;
-    if (fEffectAge > 1) {
+    bool runEffectSpawn = (fState.fAge == 0.0f) && (fState.fLoopCount == 0);
+
+    fState.fAge += fState.fDeltaTime / fState.fLifetime;
+    if (fState.fAge > 1) {
         if (fLooping) {
-            fEffectAge = fmodf(fEffectAge, 1.0f);
+            fState.fLoopCount += sk_float_floor2int(fState.fAge);
+            fState.fAge = fmodf(fState.fAge, 1.0f);
+            runEffectSpawn = true;
         } else {
             // Effect is dead if we've reached the end (and are not looping)
             return;
         }
     }
 
-    float updateParams[2] = { deltaTime, fEffectAge };
+    // Run optional effectSpawn to set initial spawn rate and other emitter properties.
+    // This also runs on each loop point, for looped effects.
+    if (runEffectSpawn) {
+        if (const auto& byteCode = fParams->fEffectProgram.fByteCode) {
+            if (auto fun = byteCode->getFunction("effectSpawn")) {
+                for (const auto& value : fParams->fEffectProgram.fExternalValues) {
+                    value->setRandom(&fRandom);
+                }
+                SkAssertResult(byteCode->run(fun, &fState.fAge, nullptr, 1,
+                                             &fState.fDeltaTime, 1));
+            }
+        }
+    }
 
     // Advance age for existing particles, and remove any that have reached their end of life
+    // TODO: Add an (optional) death script for particles?
     for (int i = 0; i < fCount; ++i) {
         fParticles.fData[SkParticles::kAge][i] +=
-                fParticles.fData[SkParticles::kLifetime][i] * deltaTime;
+                fParticles.fData[SkParticles::kLifetime][i] * fState.fDeltaTime;
         if (fParticles.fData[SkParticles::kAge][i] > 1.0f) {
             // NOTE: This is fast, but doesn't preserve drawing order. Could be a problem...
             for (int j = 0; j < SkParticles::kNumChannels; ++j) {
@@ -183,28 +256,55 @@
         }
     }
 
-    auto runProgram = [](const SkParticleEffectParams* params, const char* entry,
-                         SkParticles& particles, float updateParams[], int start, int count) {
-        if (const auto& byteCode = params->fByteCode) {
+    // On first frame, we may have a pending burst from effectSpawn
+    int burstCount = fState.fBurst;
+    fState.fBurst = 0;
+
+    // Run optional effectUpdate to adjust spawn rate and other emitter properties
+    if (const auto& byteCode = fParams->fEffectProgram.fByteCode) {
+        if (auto fun = byteCode->getFunction("effectUpdate")) {
+            for (const auto& value : fParams->fEffectProgram.fExternalValues) {
+                value->setRandom(&fRandom);
+            }
+            SkAssertResult(byteCode->run(fun, &fState.fAge, nullptr, 1,
+                                         &fState.fDeltaTime, 1));
+            burstCount += fState.fBurst;
+        }
+    }
+
+    // Do integration of effect position and orientation
+    {
+        fState.fPosition += fState.fVelocity * fState.fDeltaTime;
+        float s = sk_float_sin(fState.fSpin * fState.fDeltaTime),
+              c = sk_float_cos(fState.fSpin * fState.fDeltaTime);
+        // Using setNormalize to prevent scale drift
+        fState.fHeading.setNormalize(fState.fHeading.fX * c - fState.fHeading.fY * s,
+                                     fState.fHeading.fX * s + fState.fHeading.fY * c);
+    }
+
+    auto runProgram = [this](const SkParticleEffectParams* params, const char* entry,
+                             SkParticles& particles, int start, int count) {
+        if (const auto& byteCode = params->fParticleProgram.fByteCode) {
             float* args[SkParticles::kNumChannels];
             for (int i = 0; i < SkParticles::kNumChannels; ++i) {
                 args[i] = particles.fData[i].get() + start;
             }
             SkRandom* randomBase = particles.fRandom.get() + start;
-            for (const auto& value : params->fExternalValues) {
+            for (const auto& value : params->fParticleProgram.fExternalValues) {
                 value->setRandom(randomBase);
             }
             SkAssertResult(byteCode->runStriped(byteCode->getFunction(entry),
                                                 args, SkParticles::kNumChannels, count,
-                                                updateParams, 2, nullptr, 0));
+                                                &fState.fDeltaTime, sizeof(EffectState) / 4,
+                                                nullptr, 0));
         }
     };
 
     // Spawn new particles
-    float desired = fParams->fRate * deltaTime + fSpawnRemainder;
+    float desired = fState.fRate * fState.fDeltaTime + fSpawnRemainder;
     int numToSpawn = sk_float_round2int(desired);
     fSpawnRemainder = desired - numToSpawn;
-    numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount);
+    numToSpawn = SkTPin(numToSpawn + burstCount, 0, fParams->fMaxCount - fCount);
     if (numToSpawn) {
         const int spawnBase = fCount;
 
@@ -213,25 +313,25 @@
             fRandom.nextU();
             fParticles.fData[SkParticles::kAge            ][fCount] = 0.0f;
             fParticles.fData[SkParticles::kLifetime       ][fCount] = 0.0f;
-            fParticles.fData[SkParticles::kPositionX      ][fCount] = 0.0f;
-            fParticles.fData[SkParticles::kPositionY      ][fCount] = 0.0f;
-            fParticles.fData[SkParticles::kHeadingX       ][fCount] = 0.0f;
-            fParticles.fData[SkParticles::kHeadingY       ][fCount] = -1.0f;
-            fParticles.fData[SkParticles::kScale          ][fCount] = 1.0f;
-            fParticles.fData[SkParticles::kVelocityX      ][fCount] = 0.0f;
-            fParticles.fData[SkParticles::kVelocityY      ][fCount] = 0.0f;
-            fParticles.fData[SkParticles::kVelocityAngular][fCount] = 0.0f;
-            fParticles.fData[SkParticles::kColorR         ][fCount] = 1.0f;
-            fParticles.fData[SkParticles::kColorG         ][fCount] = 1.0f;
-            fParticles.fData[SkParticles::kColorB         ][fCount] = 1.0f;
-            fParticles.fData[SkParticles::kColorA         ][fCount] = 1.0f;
-            fParticles.fData[SkParticles::kSpriteFrame    ][fCount] = 0.0f;
+            fParticles.fData[SkParticles::kPositionX      ][fCount] = fState.fPosition.fX;
+            fParticles.fData[SkParticles::kPositionY      ][fCount] = fState.fPosition.fY;
+            fParticles.fData[SkParticles::kHeadingX       ][fCount] = fState.fHeading.fX;
+            fParticles.fData[SkParticles::kHeadingY       ][fCount] = fState.fHeading.fY;
+            fParticles.fData[SkParticles::kScale          ][fCount] = fState.fScale;
+            fParticles.fData[SkParticles::kVelocityX      ][fCount] = fState.fVelocity.fX;
+            fParticles.fData[SkParticles::kVelocityY      ][fCount] = fState.fVelocity.fY;
+            fParticles.fData[SkParticles::kVelocityAngular][fCount] = fState.fSpin;
+            fParticles.fData[SkParticles::kColorR         ][fCount] = fState.fColor.fR;
+            fParticles.fData[SkParticles::kColorG         ][fCount] = fState.fColor.fG;
+            fParticles.fData[SkParticles::kColorB         ][fCount] = fState.fColor.fB;
+            fParticles.fData[SkParticles::kColorA         ][fCount] = fState.fColor.fA;
+            fParticles.fData[SkParticles::kSpriteFrame    ][fCount] = fState.fFrame;
             fParticles.fRandom[fCount] = fRandom;
             fCount++;
         }
 
         // Run the spawn script
-        runProgram(fParams.get(), "spawn", fParticles, updateParams, spawnBase, numToSpawn);
+        runProgram(fParams.get(), "spawn", fParticles, spawnBase, numToSpawn);
 
         // Now stash copies of the random generators and compute inverse particle lifetimes
         // (so that subsequent updates are faster)
@@ -248,17 +348,18 @@
     }
 
     // Run the update script
-    runProgram(fParams.get(), "update", fParticles, updateParams, 0, fCount);
+    runProgram(fParams.get(), "update", fParticles, 0, fCount);
 
     // Do fixed-function update work (integration of position and orientation)
     for (int i = 0; i < fCount; ++i) {
         fParticles.fData[SkParticles::kPositionX][i] +=
-                fParticles.fData[SkParticles::kVelocityX][i] * deltaTime;
+                fParticles.fData[SkParticles::kVelocityX][i] * fState.fDeltaTime;
         fParticles.fData[SkParticles::kPositionY][i] +=
-                fParticles.fData[SkParticles::kVelocityY][i] * deltaTime;
+                fParticles.fData[SkParticles::kVelocityY][i] * fState.fDeltaTime;
 
-        SkScalar s = SkScalarSin(fParticles.fData[SkParticles::kVelocityAngular][i] * deltaTime),
-                 c = SkScalarCos(fParticles.fData[SkParticles::kVelocityAngular][i] * deltaTime);
+        float spin = fParticles.fData[SkParticles::kVelocityAngular][i];
+        float s = sk_float_sin(spin * fState.fDeltaTime),
+              c = sk_float_cos(spin * fState.fDeltaTime);
         float oldHeadingX = fParticles.fData[SkParticles::kHeadingX][i],
               oldHeadingY = fParticles.fData[SkParticles::kHeadingY][i];
         fParticles.fData[SkParticles::kHeadingX][i] = oldHeadingX * c - oldHeadingY * s;
diff --git a/resources/particles/default.json b/resources/particles/default.json
index 450f946..e2fab7e 100644
--- a/resources/particles/default.json
+++ b/resources/particles/default.json
@@ -1,14 +1,19 @@
 {
    "MaxCount": 4096,
-   "Duration": 1,
-   "Rate": 1000,
    "Drawable": {
       "Type": "SkCircleDrawable",
       "Radius": 1
    },
-   "Code": [
-      "// float rand; Every read returns a random float [0 .. 1)",
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.rate = 1000;",
+      "}",
       "",
+      "void effectUpdate(inout Effect effect) {",
+      "}",
+      ""
+   ],
+   "Code": [
       "void spawn(inout Particle p) {",
       "  p.lifetime = mix(1, 3, rand);",
       "  float a = radians(mix(250, 290, rand));",
diff --git a/resources/particles/explosion.json b/resources/particles/explosion.json
index cfef277..6f581d2 100644
--- a/resources/particles/explosion.json
+++ b/resources/particles/explosion.json
@@ -1,16 +1,21 @@
 {
    "MaxCount": 32,
-   "Duration": 1,
-   "Rate": 8,
    "Drawable": {
       "Type": "SkImageDrawable",
       "Path": "resources/images/explosion_sprites.png",
       "Columns": 4,
       "Rows": 4
    },
-   "Code": [
-      "// float rand; Every read returns a random float [0 .. 1)",
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.rate = 8;",
+      "}",
       "",
+      "void effectUpdate(inout Effect effect) {",
+      "}",
+      ""
+   ],
+   "Code": [
       "float2 circle() {",
       "  float x;",
       "  float y;",
diff --git a/resources/particles/fireworks.json b/resources/particles/fireworks.json
new file mode 100644
index 0000000..1e08ef7
--- /dev/null
+++ b/resources/particles/fireworks.json
@@ -0,0 +1,34 @@
+{
+   "MaxCount": 1000,
+   "Drawable": {
+      "Type": "SkCircleDrawable",
+      "Radius": 3
+   },
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.burst = 50;",
+      "  effect.pos.x = mix(-100, 100, rand);",
+      "  effect.pos.y = mix(-100, 100, rand);",
+      "  effect.color.rgb = float3(rand, rand, rand);",
+      "}",
+      "",
+      "void effectUpdate(inout Effect effect) {",
+      "}",
+      ""
+   ],
+   "Code": [
+      "void spawn(inout Particle p) {",
+      "  p.lifetime = 2.5;",
+      "  float a = radians(rand * 360);",
+      "  float s = mix(90, 100, rand);",
+      "  p.vel.x = cos(a) * s;",
+      "  p.vel.y = sin(a) * s;",
+      "}",
+      "",
+      "void update(inout Particle p) {",
+      "  p.color.a = 1 - p.age;",
+      "}",
+      ""
+   ],
+   "Bindings": []
+}
\ No newline at end of file
diff --git a/resources/particles/interp.json b/resources/particles/interp.json
index e2d028e..5d2fbf9 100644
--- a/resources/particles/interp.json
+++ b/resources/particles/interp.json
@@ -1,14 +1,20 @@
 {
    "MaxCount": 6000,
-   "Duration": 5,
-   "Rate": 2000,
    "Drawable": {
       "Type": "SkCircleDrawable",
       "Radius": 2
    },
-   "Code": [
-      "// float rand; Every read returns a random float [0 .. 1)",
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.rate = 2000;",
+      "}",
       "",
+      "void effectUpdate(inout Effect effect) {",
+      "  effect.pos.y = sin(effect.age * 6.28) * 40;",
+      "}",
+      ""
+   ],
+   "Code": [
       "void spawn(inout Particle p) {",
       "  p.lifetime = 2 + (rand * 2);",
       "  p.vel.x = (30 * rand) + 50;",
diff --git a/resources/particles/penguin_cannon.json b/resources/particles/penguin_cannon.json
index ca64f2f..751752c 100644
--- a/resources/particles/penguin_cannon.json
+++ b/resources/particles/penguin_cannon.json
@@ -1,16 +1,21 @@
 {
    "MaxCount": 32,
-   "Duration": 1,
-   "Rate": 0.5,
    "Drawable": {
       "Type": "SkImageDrawable",
       "Path": "resources/images/baby_tux.png",
       "Columns": 1,
       "Rows": 1
    },
-   "Code": [
-      "// float rand; Every read returns a random float [0 .. 1)",
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.rate = 0.5;",
+      "}",
       "",
+      "void effectUpdate(inout Effect effect) {",
+      "}",
+      ""
+   ],
+   "Code": [
       "void spawn(inout Particle p) {",
       "  p.lifetime = 20;",
       "  float a = radians(10 + 60 * rand);",
diff --git a/resources/particles/pulse.json b/resources/particles/pulse.json
new file mode 100644
index 0000000..bcf5e96
--- /dev/null
+++ b/resources/particles/pulse.json
@@ -0,0 +1,31 @@
+{
+   "MaxCount": 4000,
+   "Drawable": {
+      "Type": "SkCircleDrawable",
+      "Radius": 3
+   },
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.lifetime = 1;",
+      "}",
+      "",
+      "void effectUpdate(inout Effect effect) {",
+      "  effect.rate = (sin(radians(effect.age * 720)) + 0.7) * 200;",
+      "}",
+      ""
+   ],
+   "Code": [
+      "void spawn(inout Particle p) {",
+      "  p.lifetime = 6;",
+      "  float a = radians(rand * 360);",
+      "  p.pos = float2(cos(a), sin(a)) * 40;",
+      "  p.vel = p.pos;",
+      "  p.scale = 0.5;",
+      "}",
+      "",
+      "void update(inout Particle p) {",
+      "}",
+      ""
+   ],
+   "Bindings": []
+}
\ No newline at end of file
diff --git a/resources/particles/snowfall.json b/resources/particles/snowfall.json
index d66f157..9f4e731 100644
--- a/resources/particles/snowfall.json
+++ b/resources/particles/snowfall.json
@@ -1,14 +1,19 @@
 {
    "MaxCount": 4096,
-   "Duration": 1,
-   "Rate": 30,
    "Drawable": {
       "Type": "SkCircleDrawable",
       "Radius": 1
    },
-   "Code": [
-      "// float rand; Every read returns a random float [0 .. 1)",
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.rate = 30;",
+      "}",
       "",
+      "void effectUpdate(inout Effect effect) {",
+      "}",
+      ""
+   ],
+   "Code": [
       "void spawn(inout Particle p) {",
       "  p.lifetime = 10;",
       "  p.vel.y = 10 + rand * 20;",
diff --git a/resources/particles/spiral.json b/resources/particles/spiral.json
index 539d1df..6076ca8 100644
--- a/resources/particles/spiral.json
+++ b/resources/particles/spiral.json
@@ -1,20 +1,24 @@
 {
    "MaxCount": 800,
-   "Duration": 4,
-   "Rate": 120,
    "Drawable": {
       "Type": "SkCircleDrawable",
       "Radius": 2
    },
-   "Code": [
-      "// float rand; Every read returns a random float [0 .. 1)",
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.lifetime = 4;",
+      "  effect.rate = 120;",
+      "  effect.spin = 6;",
+      "}",
       "",
+      "void effectUpdate(inout Effect effect) {",
+      "}",
+      ""
+   ],
+   "Code": [
       "void spawn(inout Particle p) {",
       "  p.lifetime = 2 + rand;",
-      "  float a = radians(effectAge * 1080);",
-      "  float s = 50 + rand * 10;",
-      "  p.vel.x = cos(a) * s;",
-      "  p.vel.y = sin(a) * s;",
+      "  p.vel = p.dir * mix(50, 60, rand);",
       "}",
       "",
       "void update(inout Particle p) {",
diff --git a/resources/particles/swirl.json b/resources/particles/swirl.json
index b51658a..9c1dea6 100644
--- a/resources/particles/swirl.json
+++ b/resources/particles/swirl.json
@@ -1,14 +1,19 @@
 {
    "MaxCount": 4096,
-   "Duration": 1,
-   "Rate": 400,
    "Drawable": {
       "Type": "SkCircleDrawable",
       "Radius": 2
    },
-   "Code": [
-      "// float rand; Every read returns a random float [0 .. 1)",
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.rate = 400;",
+      "}",
       "",
+      "void effectUpdate(inout Effect effect) {",
+      "}",
+      ""
+   ],
+   "Code": [
       "void spawn(inout Particle p) {",
       "  p.lifetime = 1 + 2 * rand;",
       "  p.pos.x = rand * 50;",
diff --git a/resources/particles/warp.json b/resources/particles/warp.json
index 98f1428..94007c7 100644
--- a/resources/particles/warp.json
+++ b/resources/particles/warp.json
@@ -1,15 +1,19 @@
 {
    "MaxCount": 4096,
-   "Duration": 1,
-   "Rate": 90,
    "Drawable": {
       "Type": "SkCircleDrawable",
       "Radius": 2
    },
+   "EffectCode": [
+      "void effectSpawn(inout Effect effect) {",
+      "  effect.rate = 90;",
+      "}",
+      "",
+      "void effectUpdate(inout Effect effect) {",
+      "}",
+      ""
+   ],
    "Code": [
-      "",
-      "// float rand; Every read returns a random float [0 .. 1)",
-      "",
       "float2 circle() {",
       "  float x;",
       "  float y;",
diff --git a/tools/viewer/ParticlesSlide.cpp b/tools/viewer/ParticlesSlide.cpp
index b8ddf78..3c2abbf 100644
--- a/tools/viewer/ParticlesSlide.cpp
+++ b/tools/viewer/ParticlesSlide.cpp
@@ -72,6 +72,7 @@
             int lines = count_lines(s);
             ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize;
             if (lines > 1) {
+                ImGui::LabelText("##Label", "%s", name);
                 ImVec2 boxSize(-1.0f, ImGui::GetTextLineHeight() * (lines + 1));
                 ImGui::InputTextMultiline(item(name), s.writable_str(), s.size() + 1, boxSize,
                                           flags, InputTextCallback, &s);
@@ -281,6 +282,7 @@
                 sk_sp<SkParticleEffect> effect(new SkParticleEffect(fLoaded[i].fParams, fRandom));
                 effect->start(fAnimationTime, looped);
                 fRunning.push_back({ fPlayPosition, fLoaded[i].fName, effect });
+                fRandom.nextU();
             }
             ImGui::SameLine();