Refactor and further generalization of particle model

- Collapsed the per-particle data into a single struct, and
  use that to communicate with drawables, too. Let the drawables
  manage allocation of xforms, colors, etc. Helpful for non-atlas
  drawables, and just to keep the effect code simpler.
- Having all of the params in a single struct allows us to move
  the remaining animated behaviors into affectors (color/frame).
- Added SkColorCurve, which works like SkCurve for SkColor4f.
  Use that to create a color affector (rather than simple
  start/end colors in the effect params).
- Also put the stable random in SkParticleState. This is going
  to be necessary if/when we change affectors to operate on all
  particles (rather than one at a time). Still need to move t
  value into the particle struct (or eval it from the lifetime
  params on demand).

Change-Id: Icf39116acbfd5d6e8eb91e9affbd8898d106211d
Reviewed-on: https://skia-review.googlesource.com/c/193473
Reviewed-by: Brian Osman <brianosman@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/modules/particles/include/SkCurve.h b/modules/particles/include/SkCurve.h
index 58c8e2f..b3c0ca6 100644
--- a/modules/particles/include/SkCurve.h
+++ b/modules/particles/include/SkCurve.h
@@ -8,6 +8,7 @@
 #ifndef SkCurve_DEFINED
 #define SkCurve_DEFINED
 
+#include "SkColor.h"
 #include "SkScalar.h"
 #include "SkTArray.h"
 
@@ -78,4 +79,46 @@
     SkTArray<SkCurveSegment, true> fSegments;
 };
 
+/**
+ * SkColorCurve is similar to SkCurve, but keyframes 4D values - specifically colors. Because
+ * negative colors rarely make sense, SkColorCurves do not support bidirectional segments, but
+ * support all other features (including cubic interpolation).
+ */
+
+struct SkColorCurveSegment {
+    SkColorCurveSegment() {
+        for (int i = 0; i < 4; ++i) {
+            fMin[i] = { 1.0f, 1.0f, 1.0f, 1.0f };
+            fMax[i] = { 1.0f, 1.0f, 1.0f, 1.0f };
+        }
+    }
+
+    SkColor4f eval(SkScalar x, SkRandom& random) const;
+    void visitFields(SkFieldVisitor* v);
+
+    void setConstant(SkColor4f c) {
+        fConstant = true;
+        fRanged = false;
+        fMin[0] = c;
+    }
+
+    SkColor4f fMin[4];
+    SkColor4f fMax[4];
+
+    bool fConstant = true;
+    bool fRanged = false;
+};
+
+struct SkColorCurve {
+    SkColorCurve(SkColor4f c = { 1.0f, 1.0f, 1.0f, 1.0f }) {
+        fSegments.push_back().setConstant(c);
+    }
+
+    SkColor4f eval(SkScalar x, SkRandom& random) const;
+    void visitFields(SkFieldVisitor* v);
+
+    SkTArray<SkScalar, true>            fXValues;
+    SkTArray<SkColorCurveSegment, true> fSegments;
+};
+
 #endif // SkCurve_DEFINED
diff --git a/modules/particles/include/SkParticleAffector.h b/modules/particles/include/SkParticleAffector.h
index 82570db..7de9fbe 100644
--- a/modules/particles/include/SkParticleAffector.h
+++ b/modules/particles/include/SkParticleAffector.h
@@ -12,15 +12,16 @@
 
 #include "SkPoint.h"
 
+struct SkColorCurve;
 struct SkCurve;
-struct SkParticlePoseAndVelocity;
+struct SkParticleState;
 struct SkParticleUpdateParams;
 
 class SkParticleAffector : public SkReflected {
 public:
     REFLECTED_ABSTRACT(SkParticleAffector, SkReflected)
 
-    virtual void apply(SkParticleUpdateParams& params, SkParticlePoseAndVelocity& pv) = 0;
+    virtual void apply(SkParticleUpdateParams& params, SkParticleState& ps) = 0;
 
     static void RegisterAffectorTypes();
 
@@ -31,7 +32,9 @@
                                                     SkScalar invSquare);
     static sk_sp<SkParticleAffector> MakeOrientAlongVelocity();
 
-    static sk_sp<SkParticleAffector> MakeSizeAffector(const SkCurve& curve);
+    static sk_sp<SkParticleAffector> MakeSize(const SkCurve& curve);
+    static sk_sp<SkParticleAffector> MakeFrame(const SkCurve& curve);
+    static sk_sp<SkParticleAffector> MakeColor(const SkColorCurve& curve);
 };
 
 #endif // SkParticleAffector_DEFINED
diff --git a/modules/particles/include/SkParticleData.h b/modules/particles/include/SkParticleData.h
index b99576b..54e0244 100644
--- a/modules/particles/include/SkParticleData.h
+++ b/modules/particles/include/SkParticleData.h
@@ -8,6 +8,7 @@
 #ifndef SkParticleData_DEFINED
 #define SkParticleData_DEFINED
 
+#include "SkColor.h"
 #include "SkPoint.h"
 #include "SkRandom.h"
 #include "SkRSXform.h"
@@ -35,14 +36,18 @@
     SkScalar fAngular;
 };
 
-struct SkParticlePoseAndVelocity {
+struct SkParticleState {
+    double             fTimeOfBirth;
+    double             fTimeOfDeath;
     SkParticlePose     fPose;
     SkParticleVelocity fVelocity;
+    SkColor4f          fColor;
+    SkScalar           fFrame;         // Parameter to drawable for animated sprites, etc.
+    SkRandom           fStableRandom;
 };
 
 struct SkParticleUpdateParams {
     SkRandom* fRandom;
-    SkRandom* fStableRandom;
     float fDeltaTime;
     float fParticleT;
 };
diff --git a/modules/particles/include/SkParticleDrawable.h b/modules/particles/include/SkParticleDrawable.h
index aedfdb1..a47b24d 100644
--- a/modules/particles/include/SkParticleDrawable.h
+++ b/modules/particles/include/SkParticleDrawable.h
@@ -10,20 +10,17 @@
 
 #include "SkReflected.h"
 
-#include "SkColor.h"
-
 class SkCanvas;
+struct SkParticleState;
 class SkPaint;
-struct SkRSXform;
 class SkString;
 
 class SkParticleDrawable : public SkReflected {
 public:
     REFLECTED_ABSTRACT(SkParticleDrawable, SkReflected)
 
-    virtual void draw(SkCanvas* canvas, const SkRSXform xform[], const float tex[],
-                      const SkColor colors[], int count, const SkPaint* paint) = 0;
-    virtual SkPoint center() const = 0;
+    virtual void draw(SkCanvas* canvas, const SkParticleState particles[], int count,
+                      const SkPaint* paint) = 0;
 
     static void RegisterDrawableTypes();
 
diff --git a/modules/particles/include/SkParticleEffect.h b/modules/particles/include/SkParticleEffect.h
index 7397976..c636409 100644
--- a/modules/particles/include/SkParticleEffect.h
+++ b/modules/particles/include/SkParticleEffect.h
@@ -22,7 +22,6 @@
 class SkParticleAffector;
 class SkParticleDrawable;
 class SkParticleEmitter;
-struct SkRSXform;
 
 class SkParticleEffectParams : public SkRefCnt {
 public:
@@ -30,8 +29,6 @@
     float     fEffectDuration = 1.0f;
     float     fRate = 8.0f;
     SkCurve   fLifetime = 1.0f;
-    SkColor4f fStartColor = { 1.0f, 1.0f, 1.0f, 1.0f };
-    SkColor4f fEndColor = { 1.0f, 1.0f, 1.0f, 1.0f };
 
     // Drawable (image, sprite sheet, etc.)
     sk_sp<SkParticleDrawable> fDrawable;
@@ -63,15 +60,6 @@
 private:
     void setCapacity(int capacity);
 
-    struct Particle {
-        double fTimeOfBirth;
-        double fTimeOfDeath;
-        SkRandom fStableRandom;
-
-        // Texture coord rects and colors are stored in parallel arrays for drawAtlas.
-        SkParticlePoseAndVelocity fPV;
-    };
-
     sk_sp<SkParticleEffectParams> fParams;
 
     SkRandom fRandom;
@@ -83,10 +71,8 @@
     double fLastTime;
     float  fSpawnRemainder;
 
-    SkAutoTMalloc<Particle>  fParticles;
-    SkAutoTMalloc<SkRSXform> fXforms;
-    SkAutoTMalloc<float>     fFrames;
-    SkAutoTMalloc<SkColor>   fColors;
+    SkAutoTMalloc<SkParticleState> fParticles;
+    SkAutoTMalloc<SkRandom>        fStableRandoms;
 
     // Cached
     int fCapacity;
diff --git a/modules/particles/src/SkCurve.cpp b/modules/particles/src/SkCurve.cpp
index ac3d983..aa03853 100644
--- a/modules/particles/src/SkCurve.cpp
+++ b/modules/particles/src/SkCurve.cpp
@@ -15,6 +15,15 @@
     return pts[0]*ix*ix*ix + pts[1]*3*ix*ix*x + pts[2]*3*ix*x*x + pts[3]*x*x*x;
 }
 
+static SkColor4f operator+(SkColor4f c1, SkColor4f c2) {
+    return { c1.fR + c2.fR, c1.fG + c2.fG, c1.fB + c2.fB, c1.fA + c2.fA };
+}
+
+static SkColor4f eval_cubic(const SkColor4f* pts, SkScalar x) {
+    SkScalar ix = (1 - x);
+    return pts[0]*(ix*ix*ix) + pts[1]*(3*ix*ix*x) + pts[2]*(3*ix*x*x) + pts[3]*(x*x*x);
+}
+
 SkScalar SkCurveSegment::eval(SkScalar x, SkRandom& random) const {
     SkScalar result = fConstant ? fMin[0] : eval_cubic(fMin, x);
     if (fRanged) {
@@ -93,3 +102,59 @@
         }
     }
 }
+
+SkColor4f SkColorCurveSegment::eval(SkScalar x, SkRandom& random) const {
+    SkColor4f result = fConstant ? fMin[0] : eval_cubic(fMin, x);
+    if (fRanged) {
+        result = result +
+                ((fConstant ? fMax[0] : eval_cubic(fMax, x)) + (result * -1)) * random.nextF();
+    }
+    return result;
+}
+
+void SkColorCurveSegment::visitFields(SkFieldVisitor* v) {
+    v->visit("Constant", fConstant);
+    v->visit("Ranged", fRanged);
+    v->visit("A0", fMin[0]);
+    v->visit("B0", fMin[1]);
+    v->visit("C0", fMin[2]);
+    v->visit("D0", fMin[3]);
+    v->visit("A1", fMax[0]);
+    v->visit("B1", fMax[1]);
+    v->visit("C1", fMax[2]);
+    v->visit("D1", fMax[3]);
+}
+
+SkColor4f SkColorCurve::eval(SkScalar x, SkRandom& random) const {
+    SkASSERT(fSegments.count() == fXValues.count() + 1);
+
+    int i = 0;
+    for (; i < fXValues.count(); ++i) {
+        if (x <= fXValues[i]) {
+            break;
+        }
+    }
+
+    SkScalar rangeMin = (i == 0) ? 0.0f : fXValues[i - 1];
+    SkScalar rangeMax = (i == fXValues.count()) ? 1.0f : fXValues[i];
+    SkScalar segmentX = (x - rangeMin) / (rangeMax - rangeMin);
+    if (!SkScalarIsFinite(segmentX)) {
+        segmentX = rangeMin;
+    }
+    SkASSERT(0.0f <= segmentX && segmentX <= 1.0f);
+    return fSegments[i].eval(segmentX, random);
+}
+
+void SkColorCurve::visitFields(SkFieldVisitor* v) {
+    v->visit("XValues", fXValues);
+    v->visit("Segments", fSegments);
+
+    // Validate and fixup
+    if (fSegments.empty()) {
+        fSegments.push_back().setConstant(SkColor4f{ 1.0f, 1.0f, 1.0f, 1.0f });
+    }
+    fXValues.resize_back(fSegments.count() - 1);
+    for (int i = 0; i < fXValues.count(); ++i) {
+        fXValues[i] = SkTPin(fXValues[i], i > 0 ? fXValues[i - 1] : 0.0f, 1.0f);
+    }
+}
diff --git a/modules/particles/src/SkParticleAffector.cpp b/modules/particles/src/SkParticleAffector.cpp
index 24a84d1..7cdf0cb 100644
--- a/modules/particles/src/SkParticleAffector.cpp
+++ b/modules/particles/src/SkParticleAffector.cpp
@@ -22,15 +22,15 @@
 
     REFLECTED(SkLinearVelocityAffector, SkParticleAffector)
 
-    void apply(SkParticleUpdateParams& params, SkParticlePoseAndVelocity& pv) override {
-        float angle = fAngle.eval(params.fParticleT, *params.fStableRandom);
+    void apply(SkParticleUpdateParams& params, SkParticleState& ps) override {
+        float angle = fAngle.eval(params.fParticleT, ps.fStableRandom);
         SkScalar c, s = SkScalarSinCos(SkDegreesToRadians(angle), &c);
-        float strength = fStrength.eval(params.fParticleT, *params.fStableRandom);
+        float strength = fStrength.eval(params.fParticleT, ps.fStableRandom);
         SkVector force = { c * strength, s * strength };
         if (fForce) {
-            pv.fVelocity.fLinear += force * params.fDeltaTime;
+            ps.fVelocity.fLinear += force * params.fDeltaTime;
         } else {
-            pv.fVelocity.fLinear = force;
+            ps.fVelocity.fLinear = force;
         }
     }
 
@@ -54,11 +54,11 @@
 
     REFLECTED(SkPointForceAffector, SkParticleAffector)
 
-    void apply(SkParticleUpdateParams& params, SkParticlePoseAndVelocity& pv) override {
-        SkVector toPoint = fPoint - pv.fPose.fPosition;
+    void apply(SkParticleUpdateParams& params, SkParticleState& ps) override {
+        SkVector toPoint = fPoint - ps.fPose.fPosition;
         SkScalar lenSquare = toPoint.dot(toPoint);
         toPoint.normalize();
-        pv.fVelocity.fLinear += toPoint * (fConstant + (fInvSquare/lenSquare)) * params.fDeltaTime;
+        ps.fVelocity.fLinear += toPoint * (fConstant + (fInvSquare/lenSquare)) * params.fDeltaTime;
     }
 
     void visitFields(SkFieldVisitor* v) override {
@@ -79,12 +79,12 @@
 
     REFLECTED(SkOrientAlongVelocityAffector, SkParticleAffector)
 
-    void apply(SkParticleUpdateParams& params, SkParticlePoseAndVelocity& pv) override {
-        SkVector heading = pv.fVelocity.fLinear;
+    void apply(SkParticleUpdateParams& params, SkParticleState& ps) override {
+        SkVector heading = ps.fVelocity.fLinear;
         if (!heading.normalize()) {
             heading.set(0, -1);
         }
-        pv.fPose.fHeading = heading;
+        ps.fPose.fHeading = heading;
     }
 
     void visitFields(SkFieldVisitor*) override {}
@@ -96,8 +96,8 @@
 
     REFLECTED(SkSizeAffector, SkParticleAffector)
 
-    void apply(SkParticleUpdateParams& params, SkParticlePoseAndVelocity& pv) override {
-        pv.fPose.fScale = fCurve.eval(params.fParticleT, *params.fStableRandom);
+    void apply(SkParticleUpdateParams& params, SkParticleState& ps) override {
+        ps.fPose.fScale = fCurve.eval(params.fParticleT, ps.fStableRandom);
     }
 
     void visitFields(SkFieldVisitor* v) override {
@@ -108,12 +108,51 @@
     SkCurve fCurve;
 };
 
+class SkFrameAffector : public SkParticleAffector {
+public:
+    SkFrameAffector(const SkCurve& curve = 1.0f) : fCurve(curve) {}
+
+    REFLECTED(SkFrameAffector, SkParticleAffector)
+
+    void apply(SkParticleUpdateParams& params, SkParticleState& ps) override {
+        ps.fFrame = fCurve.eval(params.fParticleT, ps.fStableRandom);
+    }
+
+    void visitFields(SkFieldVisitor* v) override {
+        v->visit("Curve", fCurve);
+    }
+
+private:
+    SkCurve fCurve;
+};
+
+class SkColorAffector : public SkParticleAffector {
+public:
+    SkColorAffector(const SkColorCurve& curve = SkColor4f{ 1.0f, 1.0f, 1.0f, 1.0f })
+        : fCurve(curve) {}
+
+    REFLECTED(SkColorAffector, SkParticleAffector)
+
+    void apply(SkParticleUpdateParams& params, SkParticleState& ps) override {
+        ps.fColor = fCurve.eval(params.fParticleT, ps.fStableRandom);
+    }
+
+    void visitFields(SkFieldVisitor* v) override {
+        v->visit("Curve", fCurve);
+    }
+
+private:
+    SkColorCurve fCurve;
+};
+
 void SkParticleAffector::RegisterAffectorTypes() {
     REGISTER_REFLECTED(SkParticleAffector);
     REGISTER_REFLECTED(SkLinearVelocityAffector);
     REGISTER_REFLECTED(SkPointForceAffector);
     REGISTER_REFLECTED(SkOrientAlongVelocityAffector);
     REGISTER_REFLECTED(SkSizeAffector);
+    REGISTER_REFLECTED(SkFrameAffector);
+    REGISTER_REFLECTED(SkColorAffector);
 }
 
 sk_sp<SkParticleAffector> SkParticleAffector::MakeLinearVelocity(const SkCurve& angle,
@@ -131,6 +170,14 @@
     return sk_sp<SkParticleAffector>(new SkOrientAlongVelocityAffector());
 }
 
-sk_sp<SkParticleAffector> SkParticleAffector::MakeSizeAffector(const SkCurve& curve) {
+sk_sp<SkParticleAffector> SkParticleAffector::MakeSize(const SkCurve& curve) {
     return sk_sp<SkParticleAffector>(new SkSizeAffector(curve));
 }
+
+sk_sp<SkParticleAffector> SkParticleAffector::MakeFrame(const SkCurve& curve) {
+    return sk_sp<SkParticleAffector>(new SkFrameAffector(curve));
+}
+
+sk_sp<SkParticleAffector> SkParticleAffector::MakeColor(const SkColorCurve& curve) {
+    return sk_sp<SkParticleAffector>(new SkColorAffector(curve));
+}
diff --git a/modules/particles/src/SkParticleDrawable.cpp b/modules/particles/src/SkParticleDrawable.cpp
index fb0c150..0adc87d 100644
--- a/modules/particles/src/SkParticleDrawable.cpp
+++ b/modules/particles/src/SkParticleDrawable.cpp
@@ -12,6 +12,7 @@
 #include "SkCanvas.h"
 #include "SkImage.h"
 #include "SkPaint.h"
+#include "SkParticleData.h"
 #include "SkRect.h"
 #include "SkSurface.h"
 #include "SkString.h"
@@ -27,6 +28,22 @@
     return surface->makeImageSnapshot();
 }
 
+struct DrawAtlasArrays {
+    DrawAtlasArrays(const SkParticleState particles[], int count, SkPoint center)
+            : fXforms(count)
+            , fRects(count)
+            , fColors(count) {
+        for (int i = 0; i < count; ++i) {
+            fXforms[i] = particles[i].fPose.asRSXform(center);
+            fColors[i] = particles[i].fColor.toSkColor();
+        }
+    }
+
+    SkAutoTMalloc<SkRSXform> fXforms;
+    SkAutoTMalloc<SkRect>    fRects;
+    SkAutoTMalloc<SkColor>   fColors;
+};
+
 class SkCircleDrawable : public SkParticleDrawable {
 public:
     SkCircleDrawable(int radius = 1)
@@ -36,18 +53,15 @@
 
     REFLECTED(SkCircleDrawable, SkParticleDrawable)
 
-    void draw(SkCanvas* canvas, const SkRSXform xform[], const float tex[], const SkColor colors[],
-              int count, const SkPaint* paint) override {
-        SkAutoTMalloc<SkRect> texRects(count);
+    void draw(SkCanvas* canvas, const SkParticleState particles[], int count,
+              const SkPaint* paint) override {
+        SkPoint center = { SkIntToScalar(fRadius), SkIntToScalar(fRadius) };
+        DrawAtlasArrays arrays(particles, count, center);
         for (int i = 0; i < count; ++i) {
-            texRects[i].set(0.0f, 0.0f, fImage->width(), fImage->height());
+            arrays.fRects[i].set(0.0f, 0.0f, fImage->width(), fImage->height());
         }
-        canvas->drawAtlas(fImage, xform, texRects.get(), colors, count,
-                          SkBlendMode::kModulate, nullptr, paint);
-    }
-
-    SkPoint center() const override {
-        return { SkIntToScalar(fRadius), SkIntToScalar(fRadius) };
+        canvas->drawAtlas(fImage, arrays.fXforms.get(), arrays.fRects.get(), arrays.fColors.get(),
+                          count, SkBlendMode::kModulate, nullptr, paint);
     }
 
     void visitFields(SkFieldVisitor* v) override {
@@ -80,27 +94,22 @@
 
     REFLECTED(SkImageDrawable, SkParticleDrawable)
 
-    void draw(SkCanvas* canvas, const SkRSXform xform[], const float tex[], const SkColor colors[],
-              int count, const SkPaint* paint) override {
-        SkAutoTMalloc<SkRect> texRects(count);
-
+    void draw(SkCanvas* canvas, const SkParticleState particles[], int count,
+              const SkPaint* paint) override {
         SkRect baseRect = getBaseRect();
-        int frameCount = fCols * fRows;
+        SkPoint center = { baseRect.width() * 0.5f, baseRect.height() * 0.5f };
+        DrawAtlasArrays arrays(particles, count, center);
 
+        int frameCount = fCols * fRows;
         for (int i = 0; i < count; ++i) {
-            int frame = static_cast<int>(tex[i] * frameCount + 0.5f);
+            int frame = static_cast<int>(particles[i].fFrame * frameCount + 0.5f);
             frame = SkTPin(frame, 0, frameCount - 1);
             int row = frame / fCols;
             int col = frame % fCols;
-            texRects[i] = baseRect.makeOffset(col * baseRect.width(), row * baseRect.height());
+            arrays.fRects[i] = baseRect.makeOffset(col * baseRect.width(), row * baseRect.height());
         }
-        canvas->drawAtlas(fImage, xform, texRects.get(), colors, count,
-                          SkBlendMode::kModulate, nullptr, paint);
-    }
-
-    SkPoint center() const override {
-        SkRect baseRect = getBaseRect();
-        return { baseRect.width() * 0.5f, baseRect.height() * 0.5f };
+        canvas->drawAtlas(fImage, arrays.fXforms.get(), arrays.fRects.get(), arrays.fColors.get(),
+                          count, SkBlendMode::kModulate, nullptr, paint);
     }
 
     void visitFields(SkFieldVisitor* v) override {
diff --git a/modules/particles/src/SkParticleEffect.cpp b/modules/particles/src/SkParticleEffect.cpp
index 39b8dd6..4dfb7c2 100644
--- a/modules/particles/src/SkParticleEffect.cpp
+++ b/modules/particles/src/SkParticleEffect.cpp
@@ -11,7 +11,6 @@
 #include "SkAnimTimer.h"
 #include "SkCanvas.h"
 #include "SkColorData.h"
-#include "SkNx.h"
 #include "SkPaint.h"
 #include "SkParticleAffector.h"
 #include "SkParticleDrawable.h"
@@ -24,8 +23,6 @@
     v->visit("Duration", fEffectDuration);
     v->visit("Rate", fRate);
     v->visit("Life", fLifetime);
-    v->visit("StartColor", fStartColor);
-    v->visit("EndColor", fEndColor);
 
     v->visit("Drawable", fDrawable);
     v->visit("Emitter", fEmitter);
@@ -66,9 +63,6 @@
     float deltaTime = static_cast<float>(now - fLastTime);
     fLastTime = now;
 
-    Sk4f startColor = Sk4f::Load(fParams->fStartColor.vec());
-    Sk4f colorScale = Sk4f::Load(fParams->fEndColor.vec()) - startColor;
-
     SkParticleUpdateParams updateParams;
     updateParams.fDeltaTime = deltaTime;
     updateParams.fRandom = &fRandom;
@@ -77,9 +71,8 @@
     for (int i = 0; i < fCount; ++i) {
         if (now > fParticles[i].fTimeOfDeath) {
             // NOTE: This is fast, but doesn't preserve drawing order. Could be a problem...
-            fParticles[i] = fParticles[fCount - 1];
-            fFrames[i]    = fFrames[fCount - 1];
-            fColors[i]    = fColors[fCount - 1];
+            fParticles[i]     = fParticles[fCount - 1];
+            fStableRandoms[i] = fStableRandoms[fCount - 1];
             --i;
             --fCount;
         }
@@ -91,24 +84,24 @@
     fSpawnRemainder = desired - numToSpawn;
     numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount);
     if (fParams->fEmitter) {
-        // No, this isn't "stable", but spawn affectors are only run once anyway.
-        // Would it ever make sense to give the same random to all particles spawned on a given
-        // frame? Having a hard time thinking when that would be useful.
-        updateParams.fStableRandom = &fRandom;
-        // ... and this isn't "particle" t, it's effect t.
+        // This isn't "particle" t, it's effect t.
         double t = (now - fSpawnTime) / fParams->fEffectDuration;
         updateParams.fParticleT = static_cast<float>(fLooping ? fmod(t, 1.0) : SkTPin(t, 0.0, 1.0));
 
         for (int i = 0; i < numToSpawn; ++i) {
+            // Mutate our SkRandom so each particle definitely gets a different stable generator
+            fRandom.nextU();
+
             fParticles[fCount].fTimeOfBirth = now;
             fParticles[fCount].fTimeOfDeath = now + fParams->fLifetime.eval(updateParams.fParticleT,
                                                                             fRandom);
-            fParticles[fCount].fPV.fPose = fParams->fEmitter->emit(fRandom);
-            fParticles[fCount].fPV.fVelocity.fLinear = { 0.0f, 0.0f };
-            fParticles[fCount].fPV.fVelocity.fAngular = 0.0f;
+            fParticles[fCount].fPose = fParams->fEmitter->emit(fRandom);
+            fParticles[fCount].fVelocity.fLinear = { 0.0f, 0.0f };
+            fParticles[fCount].fVelocity.fAngular = 0.0f;
+            fParticles[fCount].fColor = { 1.0f, 1.0f, 1.0f, 1.0f };
+            fParticles[fCount].fFrame = 0.0f;
 
-            fParticles[fCount].fStableRandom = fRandom;
-            fFrames[fCount] = 0.0f;
+            fParticles[fCount].fStableRandom = fStableRandoms[fCount] = fRandom;
             fCount++;
         }
 
@@ -116,47 +109,37 @@
         for (int i = fCount - numToSpawn; i < fCount; ++i) {
             for (auto affector : fParams->fSpawnAffectors) {
                 if (affector) {
-                    affector->apply(updateParams, fParticles[i].fPV);
+                    affector->apply(updateParams, fParticles[i]);
                 }
             }
         }
     }
 
+    // Restore all stable random generators so update affectors get consistent behavior each frame
+    for (int i = 0; i < fCount; ++i) {
+        fParticles[i].fStableRandom = fStableRandoms[i];
+    }
+
     // Apply update rules
     for (int i = 0; i < fCount; ++i) {
         // Compute fraction of lifetime that's elapsed
-        float t = static_cast<float>((now - fParticles[i].fTimeOfBirth) /
-            (fParticles[i].fTimeOfDeath - fParticles[i].fTimeOfBirth));
-
-        SkRandom stableRandom = fParticles[i].fStableRandom;
-        updateParams.fStableRandom = &stableRandom;
-        updateParams.fParticleT = t;
-
-        // Set sprite frame by lifetime (TODO: Remove, add affector)
-        fFrames[i] = t;
-
-        // Set color by lifetime
-        fColors[i] = Sk4f_toL32(swizzle_rb(startColor + (colorScale * t)));
+        updateParams.fParticleT =
+            static_cast<float>((now - fParticles[i].fTimeOfBirth) /
+                               (fParticles[i].fTimeOfDeath - fParticles[i].fTimeOfBirth));
 
         for (auto affector : fParams->fUpdateAffectors) {
             if (affector) {
-                affector->apply(updateParams, fParticles[i].fPV);
+                affector->apply(updateParams, fParticles[i]);
             }
         }
 
         // Integrate position / orientation
-        fParticles[i].fPV.fPose.fPosition += fParticles[i].fPV.fVelocity.fLinear * deltaTime;
+        fParticles[i].fPose.fPosition += fParticles[i].fVelocity.fLinear * deltaTime;
 
-        SkScalar c, s = SkScalarSinCos(fParticles[i].fPV.fVelocity.fAngular * deltaTime, &c);
-        SkVector oldHeading = fParticles[i].fPV.fPose.fHeading;
-        fParticles[i].fPV.fPose.fHeading = { oldHeading.fX * c - oldHeading.fY * s,
-                                             oldHeading.fX * s + oldHeading.fY * c };
-    }
-
-    // Re-generate all xforms
-    SkPoint ofs = fParams->fDrawable ? fParams->fDrawable->center() : SkPoint{ 0.0f, 0.0f };
-    for (int i = 0; i < fCount; ++i) {
-        fXforms[i] = fParticles[i].fPV.fPose.asRSXform(ofs);
+        SkScalar c, s = SkScalarSinCos(fParticles[i].fVelocity.fAngular * deltaTime, &c);
+        SkVector oldHeading = fParticles[i].fPose.fHeading;
+        fParticles[i].fPose.fHeading = { oldHeading.fX * c - oldHeading.fY * s,
+                                         oldHeading.fX * s + oldHeading.fY * c };
     }
 
     // Mark effect as dead if we've reached the end (and are not looping)
@@ -169,16 +152,13 @@
     if (this->isAlive() && fParams->fDrawable) {
         SkPaint paint;
         paint.setFilterQuality(SkFilterQuality::kMedium_SkFilterQuality);
-        fParams->fDrawable->draw(
-                canvas, fXforms.get(), fFrames.get(), fColors.get(), fCount, &paint);
+        fParams->fDrawable->draw(canvas, fParticles.get(), fCount, &paint);
     }
 }
 
 void SkParticleEffect::setCapacity(int capacity) {
     fParticles.realloc(capacity);
-    fXforms.realloc(capacity);
-    fFrames.realloc(capacity);
-    fColors.realloc(capacity);
+    fStableRandoms.realloc(capacity);
 
     fCapacity = capacity;
     fCount = SkTMin(fCount, fCapacity);