pixi-effects 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +6 -6
  3. package/ai/CHAT.md +1 -1
  4. package/ai/SKILL.md +1 -1
  5. package/ai/chat-template.html +4 -4
  6. package/ai/reference/cheatsheet.md +5 -5
  7. package/ai/reference/pitfalls.md +5 -1
  8. package/ai/reference/recipes.md +65 -0
  9. package/ai/template.html +5 -5
  10. package/ai/tools/check.mjs +14 -3
  11. package/dist/{Base-BQUodYZK.d.ts → Base-B4WbUJO2.d.ts} +1 -1
  12. package/dist/{Base-OHqp8qvX.d.cts → Base-i88XXaiP.d.cts} +1 -1
  13. package/dist/Composition-KYNPRWK3.cjs +14 -0
  14. package/dist/{Composition-6JMXWNVV.cjs.map → Composition-KYNPRWK3.cjs.map} +1 -1
  15. package/dist/Composition-QBKEEJBV.js +5 -0
  16. package/dist/{Composition-MLBHYYFF.js.map → Composition-QBKEEJBV.js.map} +1 -1
  17. package/dist/Controller.d.cts +2 -2
  18. package/dist/Controller.d.ts +2 -2
  19. package/dist/{Movie-DlHQ9C02.d.ts → Movie-8IrqL3kO.d.ts} +6 -1
  20. package/dist/{Movie-K2YuVAdg.d.cts → Movie-CpmdNadg.d.cts} +6 -1
  21. package/dist/Presenter.d.cts +2 -2
  22. package/dist/Presenter.d.ts +2 -2
  23. package/dist/{chunk-UXDR3ZDU.js → chunk-B3YB2TNY.js} +4 -3
  24. package/dist/chunk-B3YB2TNY.js.map +1 -0
  25. package/dist/{chunk-JW67MDHQ.cjs → chunk-CTZRFSVU.cjs} +27 -26
  26. package/dist/chunk-CTZRFSVU.cjs.map +1 -0
  27. package/dist/{chunk-ANO5R2E6.cjs → chunk-VTBT6NNE.cjs} +755 -48
  28. package/dist/chunk-VTBT6NNE.cjs.map +1 -0
  29. package/dist/{chunk-SG3Y66FN.js → chunk-YRXTKOV5.js} +757 -51
  30. package/dist/chunk-YRXTKOV5.js.map +1 -0
  31. package/dist/index.cjs +73 -36
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +5 -5
  34. package/dist/index.d.ts +5 -5
  35. package/dist/index.js +50 -13
  36. package/dist/index.js.map +1 -1
  37. package/dist/loader.css +1 -1
  38. package/dist/three.cjs +5 -5
  39. package/dist/three.d.cts +2 -2
  40. package/dist/three.d.ts +2 -2
  41. package/dist/three.js +1 -1
  42. package/dist/{types-SS51lP6O.d.cts → types-bzLJ7aUx.d.cts} +16 -6
  43. package/dist/{types-SS51lP6O.d.ts → types-bzLJ7aUx.d.ts} +16 -6
  44. package/docs/dsl.md +36 -5
  45. package/llms-full.txt +112 -11
  46. package/package.json +1 -1
  47. package/dist/Composition-6JMXWNVV.cjs +0 -14
  48. package/dist/Composition-MLBHYYFF.js +0 -5
  49. package/dist/chunk-ANO5R2E6.cjs.map +0 -1
  50. package/dist/chunk-JW67MDHQ.cjs.map +0 -1
  51. package/dist/chunk-SG3Y66FN.js.map +0 -1
  52. package/dist/chunk-UXDR3ZDU.js.map +0 -1
@@ -1,6 +1,6 @@
1
- import { suggestName } from './chunk-DKGG7WD4.js';
1
+ import { suggestName, warnUnknownOptions } from './chunk-DKGG7WD4.js';
2
2
  import * as PIXI from 'pixi.js';
3
- import { Rectangle, Container, GraphicsPath, Graphics, Assets, Texture, Sprite, Text, Filter, GlProgram, defaultFilterVert, GpuProgram, UniformGroup, FillGradient, Color } from 'pixi.js';
3
+ import { Rectangle, Container, GraphicsPath, Graphics, Assets, Texture, Sprite, FillGradient, Text, Filter, GlProgram, defaultFilterVert, GpuProgram, UniformGroup, Color } from 'pixi.js';
4
4
  import { gsap } from 'gsap';
5
5
 
6
6
  // src/expr/random.ts
@@ -37,6 +37,113 @@ function noise(x, seed = 0) {
37
37
  const u = f * f * f * (f * (f * 6 - 15) + 10);
38
38
  return a + (b - a) * u;
39
39
  }
40
+ var SETTLE_TOL = 5e-3;
41
+ var DEFAULTS = { mass: 1, stiffness: 100, damping: 10 };
42
+ function springResponse(t, { mass: m, stiffness: k, damping: c }) {
43
+ if (t <= 0) return 0;
44
+ const w0 = Math.sqrt(k / m);
45
+ const zeta = c / (2 * Math.sqrt(k * m));
46
+ if (Math.abs(zeta - 1) < 1e-6) return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
47
+ if (zeta < 1) {
48
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
49
+ return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd * t) + zeta * w0 / wd * Math.sin(wd * t));
50
+ }
51
+ const s = w0 * Math.sqrt(zeta * zeta - 1);
52
+ const r1 = -zeta * w0 + s, r2 = -zeta * w0 - s;
53
+ return 1 - (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r2 - r1);
54
+ }
55
+ function complete(p) {
56
+ return { mass: p.mass ?? DEFAULTS.mass, stiffness: p.stiffness ?? DEFAULTS.stiffness, damping: p.damping ?? DEFAULTS.damping };
57
+ }
58
+ function springDuration(params = {}, tol = SETTLE_TOL) {
59
+ const p = complete(params);
60
+ const w0 = Math.sqrt(p.stiffness / p.mass);
61
+ const zeta = p.damping / (2 * Math.sqrt(p.stiffness * p.mass));
62
+ const bad = (t) => Math.abs(1 - springResponse(t, p)) > tol;
63
+ let lo, hi;
64
+ if (zeta < 1 - 1e-6) {
65
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
66
+ const decay = Math.PI * zeta / Math.sqrt(1 - zeta * zeta);
67
+ const n = Math.max(1, Math.ceil(Math.log(1 / tol) / decay));
68
+ lo = (n - 1) * Math.PI / wd;
69
+ hi = n * Math.PI / wd;
70
+ } else {
71
+ lo = 0;
72
+ hi = 1 / w0;
73
+ while (bad(hi)) {
74
+ lo = hi;
75
+ hi *= 2;
76
+ }
77
+ }
78
+ for (let i = 0; i < 60; i++) {
79
+ const mid = (lo + hi) / 2;
80
+ if (bad(mid)) lo = mid;
81
+ else hi = mid;
82
+ }
83
+ return hi;
84
+ }
85
+ function springEase(params = {}) {
86
+ const sp = complete(params);
87
+ const T = springDuration(sp);
88
+ const end = springResponse(T, sp);
89
+ return (p) => p <= 0 ? 0 : p >= 1 ? 1 : springResponse(p * T, sp) / end;
90
+ }
91
+ var SPRING_PRESETS = {
92
+ gentle: { mass: 1, stiffness: 100, damping: 15 },
93
+ // zeta .75, one soft overshoot
94
+ snappy: { mass: 1, stiffness: 300, damping: 28 },
95
+ // zeta .81, quick, barely rings
96
+ bouncy: { mass: 1, stiffness: 170, damping: 10 },
97
+ // zeta .38
98
+ wobbly: { mass: 1, stiffness: 120, damping: 6 },
99
+ // zeta .27, rings several times
100
+ slow: { mass: 1, stiffness: 60, damping: 18 }
101
+ // zeta 1.16, no overshoot
102
+ };
103
+ var SPRING_RE = /^spring(?:\.(\w+)|\(([^)]*)\))?$/;
104
+ function springProblem(name) {
105
+ const m = SPRING_RE.exec(name.trim());
106
+ if (!m) return 'expected "spring(mass, stiffness, damping)" or "spring.<preset>"';
107
+ if (m[1] && !SPRING_PRESETS[m[1]]) return `no preset "${m[1]}" (presets: ${Object.keys(SPRING_PRESETS).join(", ")})`;
108
+ if (m[2] !== void 0 && m[2].trim() !== "") {
109
+ const nums = m[2].split(",").map((s) => Number(s.trim()));
110
+ if (nums.length > 3 || nums.some((n) => !Number.isFinite(n) || n <= 0)) return "mass, stiffness and damping must be 1 to 3 numbers greater than 0";
111
+ }
112
+ return null;
113
+ }
114
+ function parseSpring(name) {
115
+ if (typeof name !== "string" || !/^\s*spring/.test(name) || springProblem(name)) return null;
116
+ const m = SPRING_RE.exec(name.trim());
117
+ if (m[1]) return SPRING_PRESETS[m[1]];
118
+ const [mass, stiffness, damping] = (m[2] ?? "").split(",").map((s) => s.trim()).filter(Boolean).map(Number);
119
+ return complete({ mass, stiffness, damping });
120
+ }
121
+ var registered = false;
122
+ function registerSpringEases() {
123
+ if (registered) return;
124
+ registered = true;
125
+ const base = springEase();
126
+ const ease = (p) => base(p);
127
+ ease.config = (mass, stiffness, damping) => {
128
+ const bad = [mass, stiffness, damping].some((v) => v !== void 0 && !(typeof v === "number" && v > 0 && Number.isFinite(v)));
129
+ return springEase(bad ? {} : { mass, stiffness, damping });
130
+ };
131
+ gsap.registerEase("spring", ease);
132
+ for (const [name, p] of Object.entries(SPRING_PRESETS)) gsap.registerEase(`spring.${name}`, springEase(p));
133
+ }
134
+ var autoWarned = /* @__PURE__ */ new Set();
135
+ function kfDuration(kf) {
136
+ if (kf.duration !== "auto") return kf.duration ?? 0;
137
+ const sp = parseSpring(kf.ease);
138
+ if (sp) return Math.round(springDuration(sp) * 1e3) / 1e3;
139
+ const name = String(kf.ease ?? "none");
140
+ if (!/^\s*spring/.test(name) && !autoWarned.has(name)) {
141
+ autoWarned.add(name);
142
+ console.warn(`pixi-effects: keyframe duration 'auto' needs a spring ease (ease: 'spring(1, 170, 12)' or 'spring.bouncy'), got ease ${JSON.stringify(name)} \u2014 using 0.5 s.`);
143
+ }
144
+ return 0.5;
145
+ }
146
+ registerSpringEases();
40
147
 
41
148
  // src/expr/Parser.ts
42
149
  var FUNCS = {
@@ -294,6 +401,13 @@ var EASE_NAMES = ["none", ...FAMILIES.flatMap((f) => [`${f}.in`, `${f}.out`, `${
294
401
  var warned = /* @__PURE__ */ new Set();
295
402
  function checkEase(ease, where) {
296
403
  if (typeof ease !== "string") return;
404
+ if (/^\s*spring/.test(ease)) {
405
+ const problem = springProblem(ease);
406
+ if (!problem || warned.has(ease)) return;
407
+ warned.add(ease);
408
+ console.warn(`pixi-effects: ${where}: unknown ease "${ease}" \u2014 ${problem}. GSAP would run it as its default ease, power1.out.`);
409
+ return;
410
+ }
297
411
  const found = gsap.parseEase(ease);
298
412
  if (typeof found === "function") return;
299
413
  if (warned.has(ease)) return;
@@ -334,7 +448,7 @@ function resolveAt(at, duration) {
334
448
  }
335
449
  function normalizeKeyframe(kf, parentDuration) {
336
450
  const at = resolveAt(kf.at, parentDuration);
337
- const duration = kf.duration ?? 0;
451
+ const duration = kfDuration(kf);
338
452
  const ease = kf.ease ?? "none";
339
453
  checkEase(ease, "a keyframe");
340
454
  const hasSet = !!kf.set;
@@ -764,8 +878,217 @@ var ChromaKeyFilter = class extends Filter {
764
878
  u[2] = rgb[2];
765
879
  }
766
880
  };
881
+ var HASH_GL = `
882
+ uint hash(uint x) { x ^= x >> 16; x *= 0x7feb352du; x ^= x >> 15; x *= 0x846ca68bu; x ^= x >> 16; return x; }
883
+ // one gaussian-ish value (sum of the four bytes of a hash), mean 0, std 1
884
+ float gauss(ivec2 p, uint k) {
885
+ uint h = hash(uint(p.x) + hash(uint(p.y) + hash(k)));
886
+ float s = float(h & 255u) + float((h >> 8) & 255u) + float((h >> 16) & 255u) + float(h >> 24);
887
+ return (s / 255.0 - 2.0) * 1.7320508;
888
+ }
889
+ // the grain value at a pixel: cells of size pixels, smoothly blended
890
+ float cell(vec2 pos, float size, uint k) {
891
+ vec2 q = pos / size - 0.5;
892
+ vec2 i = floor(q);
893
+ vec2 f = fract(q);
894
+ ivec2 p = ivec2(i);
895
+ float a = gauss(p, k), b = gauss(p + ivec2(1, 0), k), c = gauss(p + ivec2(0, 1), k), d = gauss(p + ivec2(1, 1), k);
896
+ float v = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
897
+ vec2 wx = vec2(1.0 - f.x, f.x), wy = vec2(1.0 - f.y, f.y);
898
+ return v * inversesqrt(dot(wx, wx) * dot(wy, wy)); // blending lowers the spread: keep std exactly 1 at every pixel
899
+ }`;
900
+ var GL_VERTEX = `#version 300 es
901
+ in vec2 aPosition;
902
+ out vec2 vTextureCoord;
903
+ uniform vec4 uInputSize;
904
+ uniform vec4 uOutputFrame;
905
+ uniform vec4 uOutputTexture;
906
+ void main(void) {
907
+ vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
908
+ position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
909
+ position.y = position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
910
+ gl_Position = vec4(position, 0.0, 1.0);
911
+ vTextureCoord = aPosition * (uOutputFrame.zw * uInputSize.zw);
912
+ }`;
913
+ var GL_FRAGMENT2 = `#version 300 es
914
+ precision highp float;
915
+ precision highp int;
916
+ in vec2 vTextureCoord;
917
+ uniform sampler2D uTexture;
918
+ uniform vec4 uInputSize;
919
+ uniform float uAmount;
920
+ uniform float uSize;
921
+ uniform float uSeed;
922
+ uniform float uFrame;
923
+ uniform float uColor;
924
+ out vec4 finalColor;
925
+ ${HASH_GL}
926
+ void main(void) {
927
+ vec4 raw = texture(uTexture, vTextureCoord);
928
+ vec3 rgb = raw.a > 0.0 ? raw.rgb / raw.a : vec3(0.0);
929
+ float L = dot(rgb, vec3(0.2126, 0.7152, 0.0722));
930
+ float w = clamp(4.0 * L * (1.0 - L), 0.0, 1.0);
931
+ vec2 pos = vTextureCoord * uInputSize.xy;
932
+ uint k = hash(uint(uSeed) * 0x9E3779B1u + uint(uFrame));
933
+ float n0 = cell(pos, uSize, k);
934
+ vec3 n = vec3(n0);
935
+ if (uColor > 0.0) {
936
+ vec3 nc = vec3(cell(pos, uSize, k + 1u), cell(pos, uSize, k + 2u), cell(pos, uSize, k + 3u));
937
+ n = mix(n, nc, uColor);
938
+ }
939
+ rgb = clamp(rgb + n * (uAmount * w), 0.0, 1.0);
940
+ finalColor = vec4(rgb * raw.a, raw.a);
941
+ }`;
942
+ var WGSL_SOURCE2 = `
943
+ struct GlobalFilterUniforms {
944
+ uInputSize: vec4<f32>,
945
+ uInputPixel: vec4<f32>,
946
+ uInputClamp: vec4<f32>,
947
+ uOutputFrame: vec4<f32>,
948
+ uGlobalFrame: vec4<f32>,
949
+ uOutputTexture: vec4<f32>,
950
+ };
951
+ struct GrainUniforms {
952
+ uAmount: f32,
953
+ uSize: f32,
954
+ uSeed: f32,
955
+ uFrame: f32,
956
+ uColor: f32,
957
+ };
958
+ @group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
959
+ @group(0) @binding(1) var uTexture: texture_2d<f32>;
960
+ @group(0) @binding(2) var uSampler : sampler;
961
+ @group(1) @binding(0) var<uniform> grainUniforms : GrainUniforms;
962
+
963
+ struct VSOutput {
964
+ @builtin(position) position: vec4<f32>,
965
+ @location(0) uv : vec2<f32>,
966
+ };
967
+ fn filterVertexPosition(aPosition : vec2<f32>) -> vec4<f32> {
968
+ var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
969
+ position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;
970
+ position.y = position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;
971
+ return vec4<f32>(position, 0.0, 1.0);
972
+ }
973
+ fn filterTextureCoord(aPosition : vec2<f32>) -> vec2<f32> {
974
+ return aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
975
+ }
976
+ @vertex
977
+ fn mainVertex(
978
+ @location(0) aPosition : vec2<f32>,
979
+ ) -> VSOutput {
980
+ return VSOutput(
981
+ filterVertexPosition(aPosition),
982
+ filterTextureCoord(aPosition),
983
+ );
984
+ }
985
+
986
+ fn hash(xIn: u32) -> u32 {
987
+ var x = xIn;
988
+ x = x ^ (x >> 16u); x = x * 0x7feb352du; x = x ^ (x >> 15u); x = x * 0x846ca68bu; x = x ^ (x >> 16u);
989
+ return x;
990
+ }
991
+ fn gauss(p: vec2<i32>, k: u32) -> f32 {
992
+ let h = hash(u32(p.x) + hash(u32(p.y) + hash(k)));
993
+ let s = f32(h & 255u) + f32((h >> 8u) & 255u) + f32((h >> 16u) & 255u) + f32(h >> 24u);
994
+ return (s / 255.0 - 2.0) * 1.7320508;
995
+ }
996
+ fn cell(pos: vec2<f32>, size: f32, k: u32) -> f32 {
997
+ let q = pos / size - vec2<f32>(0.5);
998
+ let i = floor(q);
999
+ let f = fract(q);
1000
+ let p = vec2<i32>(i);
1001
+ let a = gauss(p, k);
1002
+ let b = gauss(p + vec2<i32>(1, 0), k);
1003
+ let c = gauss(p + vec2<i32>(0, 1), k);
1004
+ let d = gauss(p + vec2<i32>(1, 1), k);
1005
+ let v = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
1006
+ let wx = vec2<f32>(1.0 - f.x, f.x);
1007
+ let wy = vec2<f32>(1.0 - f.y, f.y);
1008
+ return v * inverseSqrt(dot(wx, wx) * dot(wy, wy));
1009
+ }
1010
+
1011
+ @fragment
1012
+ fn mainFragment(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> {
1013
+ let raw = textureSample(uTexture, uSampler, uv);
1014
+ var rgb = vec3<f32>(0.0);
1015
+ if (raw.a > 0.0) { rgb = raw.rgb / raw.a; }
1016
+ let L = dot(rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
1017
+ let w = clamp(4.0 * L * (1.0 - L), 0.0, 1.0);
1018
+ let pos = uv * gfu.uInputSize.xy;
1019
+ let k = hash(u32(grainUniforms.uSeed) * 0x9E3779B1u + u32(grainUniforms.uFrame));
1020
+ let n0 = cell(pos, grainUniforms.uSize, k);
1021
+ var n = vec3<f32>(n0);
1022
+ if (grainUniforms.uColor > 0.0) {
1023
+ let nc = vec3<f32>(cell(pos, grainUniforms.uSize, k + 1u), cell(pos, grainUniforms.uSize, k + 2u), cell(pos, grainUniforms.uSize, k + 3u));
1024
+ n = mix(n, nc, grainUniforms.uColor);
1025
+ }
1026
+ rgb = clamp(rgb + n * (grainUniforms.uAmount * w), vec3<f32>(0.0), vec3<f32>(1.0));
1027
+ return vec4<f32>(rgb * raw.a, raw.a);
1028
+ }`;
1029
+ var GRAIN_KEYS = ["amount", "size", "seed", "fps", "color"];
1030
+ var GrainFilter = class extends Filter {
1031
+ _fps;
1032
+ constructor(options = {}) {
1033
+ warnUnknownOptions("grain filter", options, GRAIN_KEYS);
1034
+ const { amount = 0.08, size = 1.5, seed = 0, fps = 24, color = 0 } = options;
1035
+ super({
1036
+ glProgram: GlProgram.from({ vertex: GL_VERTEX, fragment: GL_FRAGMENT2, name: "grain-filter" }),
1037
+ gpuProgram: GpuProgram.from({ vertex: { source: WGSL_SOURCE2, entryPoint: "mainVertex" }, fragment: { source: WGSL_SOURCE2, entryPoint: "mainFragment" } }),
1038
+ resources: {
1039
+ grainUniforms: new UniformGroup({
1040
+ uAmount: { value: amount, type: "f32" },
1041
+ uSize: { value: Math.max(size, 0.25), type: "f32" },
1042
+ uSeed: { value: Math.floor(seed), type: "f32" },
1043
+ uFrame: { value: 0, type: "f32" },
1044
+ uColor: { value: color, type: "f32" }
1045
+ })
1046
+ }
1047
+ });
1048
+ this._fps = fps;
1049
+ }
1050
+ get u() {
1051
+ return this.resources.grainUniforms.uniforms;
1052
+ }
1053
+ get amount() {
1054
+ return this.u.uAmount;
1055
+ }
1056
+ set amount(v) {
1057
+ this.u.uAmount = v;
1058
+ }
1059
+ get size() {
1060
+ return this.u.uSize;
1061
+ }
1062
+ set size(v) {
1063
+ this.u.uSize = Math.max(v, 0.25);
1064
+ }
1065
+ get seed() {
1066
+ return this.u.uSeed;
1067
+ }
1068
+ set seed(v) {
1069
+ this.u.uSeed = Math.floor(v);
1070
+ }
1071
+ get color() {
1072
+ return this.u.uColor;
1073
+ }
1074
+ set color(v) {
1075
+ this.u.uColor = v;
1076
+ }
1077
+ get fps() {
1078
+ return this._fps;
1079
+ }
1080
+ set fps(v) {
1081
+ this._fps = v;
1082
+ }
1083
+ /** The movie calls this with the time (s) of the frame it is about to draw: the grain is new every 1/fps s. */
1084
+ setTime(t) {
1085
+ this.u.uFrame = this._fps > 0 ? Math.floor(t * this._fps + 1e-6) : 0;
1086
+ }
1087
+ };
1088
+
1089
+ // src/filters/named.ts
767
1090
  var CORE = { blur: "BlurFilter", noise: "NoiseFilter", alpha: "AlphaFilter" };
768
- var CORE_TYPES = [...Object.keys(CORE), "colorMatrix"];
1091
+ var CORE_TYPES = [...Object.keys(CORE), "colorMatrix", "grain"];
769
1092
  var LIBRARY_TYPES = [
770
1093
  "adjustment",
771
1094
  "advancedBloom",
@@ -882,6 +1205,7 @@ function colorGradientParams(params) {
882
1205
  }
883
1206
  function createNamedFilter(type, params) {
884
1207
  if (lower(type) === "colormatrix") return colorMatrix(params);
1208
+ if (lower(type) === "grain") return new GrainFilter(params);
885
1209
  const core = Object.entries(CORE).find(([k]) => lower(k) === lower(type));
886
1210
  if (core) return new PIXI[core[1]](params);
887
1211
  if (!isLibraryType(type)) throw unknownType(type);
@@ -1030,6 +1354,140 @@ function collectPropKeys(spec) {
1030
1354
  return out;
1031
1355
  }
1032
1356
 
1357
+ // src/core/layerKeys.ts
1358
+ var COMMON = ["type", "name", "parent", "at", "duration", "initial", "keyframes", "filters", "filterArea", "mask", "maskInverted", "blendMode", "threeD", "hideBehindCamera"];
1359
+ var VIDEO = [...COMMON, "asset", "loop", "audio", "volume"];
1360
+ var IMAGE = [...COMMON, "asset", "colorSpace"];
1361
+ var TEXT = [...COMMON, "text", "format", "style", "colorSpace", "fillGradient"];
1362
+ var AUDIO = [...COMMON, "volume", "asset", "loop", "sfx", "music"];
1363
+ var COMPOSITION = [...COMMON, "width", "height", "sequences", "transitions"];
1364
+ var CAMERA = [...COMMON];
1365
+ var NULL = [...COMMON];
1366
+ var SHAPE = [...COMMON, "shape", "fillGradient", "colorSpace", "strokeCap", "strokeJoin"];
1367
+ var TRIM = ["trimStart", "trimEnd", "trimEach"];
1368
+ var RECT = [...SHAPE, ...TRIM, "width", "height", "cornerRadius", "anchorX", "anchorY"];
1369
+ var CIRCLE = [...SHAPE, ...TRIM, "radius", "anchorX", "anchorY"];
1370
+ var ELLIPSE = [...SHAPE, ...TRIM, "radiusX", "radiusY", "anchorX", "anchorY"];
1371
+ var ARC = [...SHAPE, "radius", "innerRadius", "startAngle", "endAngle"];
1372
+ var LINE = [...SHAPE, ...TRIM, "from", "to"];
1373
+ var POLYGON = [...SHAPE, ...TRIM, "points", "open"];
1374
+ var PATH = [...SHAPE, ...TRIM, "d", "morphTo", "morph", "morphPoints"];
1375
+ var SHAPES = { rect: RECT, circle: CIRCLE, ellipse: ELLIPSE, arc: ARC, line: LINE, polygon: POLYGON, path: PATH };
1376
+ var BY_TYPE = { video: VIDEO, image: IMAGE, text: TEXT, audio: AUDIO, composition: COMPOSITION, camera: CAMERA, null: NULL };
1377
+ function kindName(spec) {
1378
+ return spec.type === "shape" && spec.shape ? `${spec.shape} shape` : `${spec.type} layer`;
1379
+ }
1380
+ function layerKeys(spec) {
1381
+ if (spec.type === "shape") return spec.shape && SHAPES[spec.shape] || null;
1382
+ return BY_TYPE[spec.type] ?? null;
1383
+ }
1384
+ function kindsWithKey(key) {
1385
+ const out = [];
1386
+ for (const [shape, keys] of Object.entries(SHAPES)) if (keys.includes(key)) out.push(`${shape} shape`);
1387
+ for (const [type, keys] of Object.entries(BY_TYPE)) if (keys.includes(key)) out.push(`${type} layer`);
1388
+ return out;
1389
+ }
1390
+ var PROP_KEYS = /* @__PURE__ */ new Set([
1391
+ // transform and look (GSAP + PixiPlugin names)
1392
+ "x",
1393
+ "y",
1394
+ "z",
1395
+ "alpha",
1396
+ "rotation",
1397
+ "rotationX",
1398
+ "rotationY",
1399
+ "width",
1400
+ "height",
1401
+ "visible",
1402
+ "autoAlpha",
1403
+ "scale",
1404
+ "scaleX",
1405
+ "scaleY",
1406
+ "anchor",
1407
+ "anchorX",
1408
+ "anchorY",
1409
+ "pivot",
1410
+ "pivotX",
1411
+ "pivotY",
1412
+ "skew",
1413
+ "skewX",
1414
+ "skewY",
1415
+ "position",
1416
+ "positionX",
1417
+ "positionY",
1418
+ "tint",
1419
+ "colorize",
1420
+ "colorizeAmount",
1421
+ "blur",
1422
+ "blurX",
1423
+ "blurY",
1424
+ "blurPadding",
1425
+ // shapes
1426
+ "fillColor",
1427
+ "fillAlpha",
1428
+ "strokeColor",
1429
+ "strokeAlpha",
1430
+ "strokeWidth",
1431
+ "lineColor",
1432
+ "lineAlpha",
1433
+ "cornerRadius",
1434
+ "radius",
1435
+ "radiusX",
1436
+ "radiusY",
1437
+ "innerRadius",
1438
+ "startAngle",
1439
+ "endAngle",
1440
+ "trimStart",
1441
+ "trimEnd",
1442
+ "trimEach",
1443
+ "morph",
1444
+ "fillGradient",
1445
+ "strokeCap",
1446
+ "strokeJoin",
1447
+ // text, audio, video
1448
+ "fill",
1449
+ "value",
1450
+ "visibleChars",
1451
+ "text",
1452
+ "volume",
1453
+ // camera
1454
+ "lookAtX",
1455
+ "lookAtY",
1456
+ "lookAtZ",
1457
+ "fov",
1458
+ "offsetX",
1459
+ "offsetY",
1460
+ "offsetZ",
1461
+ "lookOffsetX",
1462
+ "lookOffsetY",
1463
+ "lookOffsetZ"
1464
+ ]);
1465
+ var STYLE_KEYS = [
1466
+ "fontSize",
1467
+ "fontFamily",
1468
+ "fontWeight",
1469
+ "fontStyle",
1470
+ "fontVariant",
1471
+ "fill",
1472
+ "stroke",
1473
+ "dropShadow",
1474
+ "letterSpacing",
1475
+ "lineHeight",
1476
+ "leading",
1477
+ "align",
1478
+ "wordWrap",
1479
+ "wordWrapWidth",
1480
+ "breakWords",
1481
+ "whiteSpace",
1482
+ "padding",
1483
+ "textBaseline",
1484
+ "trim",
1485
+ "lineJoin",
1486
+ "miterLimit",
1487
+ "filters",
1488
+ "tagStyles"
1489
+ ];
1490
+
1033
1491
  // src/core/lint.ts
1034
1492
  var defaultWarn = (m) => console.warn(m);
1035
1493
  function describeLayer(spec) {
@@ -1068,6 +1526,38 @@ function lintText(spec, warn = defaultWarn) {
1068
1526
  warn(`pixi-effects: ${describeLayer(spec)}: \`value\` is set or animated but the text has no {value} placeholder, so nothing shows it. Use text: '{value}' (or e.g. '{value} users').`);
1069
1527
  }
1070
1528
  }
1529
+ function lintKeys(spec, warn = defaultWarn) {
1530
+ const who = describeLayer(spec);
1531
+ const valid = layerKeys(spec);
1532
+ if (valid) {
1533
+ for (const key of Object.keys(spec)) {
1534
+ if (valid.includes(key) || spec[key] === void 0) continue;
1535
+ const guess = suggestName(key, valid.filter((k) => k !== "type"));
1536
+ const owners = kindsWithKey(key);
1537
+ const belongs = owners.length ? ` (it belongs to ${owners.slice(0, 3).join(", ")})` : "";
1538
+ const hint = guess ? ` \u2014 did you mean "${guess}"?${belongs}` : belongs;
1539
+ warn(`pixi-effects: ${who}: "${key}" is not a ${kindName(spec)} key${hint}. Valid keys: ${valid.join(", ")}`);
1540
+ }
1541
+ }
1542
+ const said = /* @__PURE__ */ new Set();
1543
+ const bags = [spec.initial, ...(spec.keyframes ?? []).flatMap((kf) => [kf.set, kf.to, kf.from])];
1544
+ for (const bag of bags) {
1545
+ for (const key of Object.keys(bag ?? {})) {
1546
+ if (bag[key] === void 0) continue;
1547
+ if (PROP_KEYS.has(key) || key.includes(".") || /^gradient/i.test(key) || said.has(key)) continue;
1548
+ said.add(key);
1549
+ const guess = suggestName(key, [...PROP_KEYS]);
1550
+ warn(`pixi-effects: ${who}: "${key}" is not an animatable property${guess ? ` \u2014 did you mean "${guess}"?` : ""} (in initial, set, to or from). See the cheatsheet for the properties of this kind of layer`);
1551
+ }
1552
+ }
1553
+ if (spec.type === "text" && spec.style && typeof spec.style === "object") {
1554
+ for (const key of Object.keys(spec.style)) {
1555
+ if (STYLE_KEYS.includes(key) || spec.style[key] === void 0) continue;
1556
+ const guess = suggestName(key, STYLE_KEYS);
1557
+ warn(`pixi-effects: ${who}: style.${key} is not a text style key${guess ? ` \u2014 did you mean "${guess}"?` : ""} (it is ignored). Keys: ${STYLE_KEYS.join(", ")}`);
1558
+ }
1559
+ }
1560
+ }
1071
1561
 
1072
1562
  // src/space/math.ts
1073
1563
  var DEG = Math.PI / 180;
@@ -1393,6 +1883,7 @@ function shortestHueDelta(from, to) {
1393
1883
  }
1394
1884
 
1395
1885
  // src/expr/colorTween.ts
1886
+ var css = (c) => typeof c === "number" ? "#" + c.toString(16).padStart(6, "0") : c;
1396
1887
  function tweenColor(timeline, target, key, fromValue, toValue, duration, ease, at, colorSpace, onUpdate, loop = {}) {
1397
1888
  let interp = null;
1398
1889
  const proxy = { p: 0 };
@@ -1407,7 +1898,7 @@ function tweenColor(timeline, target, key, fromValue, toValue, duration, ease, a
1407
1898
  onStart: () => {
1408
1899
  const start = fromValue !== void 0 ? fromValue : target[key];
1409
1900
  if (start === void 0) return;
1410
- interp = colorSpace === "rgb" ? gsap.utils.interpolate(start, toValue) : buildColorInterp(start, toValue, colorSpace);
1901
+ interp = colorSpace === "rgb" ? gsap.utils.interpolate(css(start), css(toValue)) : buildColorInterp(start, toValue, colorSpace);
1411
1902
  },
1412
1903
  onUpdate: () => {
1413
1904
  if (interp) {
@@ -1451,7 +1942,7 @@ var ImageSequence = class extends Sequence {
1451
1942
  if (initTint !== void 0) this.target.tint = initTint;
1452
1943
  for (const kf of this.spec.keyframes ?? []) {
1453
1944
  const at = startTime + resolveAt(kf.at, this.duration);
1454
- const duration = kf.duration ?? 0;
1945
+ const duration = kfDuration(kf);
1455
1946
  const ease = kf.ease ?? "none";
1456
1947
  const fromTint = kf.from?.tint;
1457
1948
  const toTint = kf.to?.tint;
@@ -1508,6 +1999,207 @@ function stripKeysInKeyframe(kf, keys) {
1508
1999
  from: stripKeys(kf.from, keys)
1509
2000
  };
1510
2001
  }
2002
+
2003
+ // src/sequences/gradient.ts
2004
+ function cssColor(c) {
2005
+ return typeof c === "number" ? "#" + c.toString(16).padStart(6, "0") : c;
2006
+ }
2007
+ function normalizedStops(g) {
2008
+ const stops = g.stops ?? [];
2009
+ if (stops.length < 2) throw new Error('pixi-effects: fillGradient needs at least two colour stops, e.g. stops: [[0, "#000"], [1, "#fff"]]');
2010
+ return stops.map((s) => Array.isArray(s) ? { offset: s[0], color: s[1] } : { offset: s.offset, color: s.color });
2011
+ }
2012
+ function gradientOptions(g) {
2013
+ const colorStops = normalizedStops(g);
2014
+ const a = (g.angle ?? 90) * Math.PI / 180;
2015
+ const dx = Math.cos(a), dy = Math.sin(a);
2016
+ const h = (Math.abs(dx) + Math.abs(dy)) / 2;
2017
+ return {
2018
+ type: "linear",
2019
+ start: { x: 0.5 - dx * h, y: 0.5 - dy * h },
2020
+ end: { x: 0.5 + dx * h, y: 0.5 + dy * h },
2021
+ colorStops,
2022
+ textureSpace: "local"
2023
+ };
2024
+ }
2025
+ function paintRadial(ctx, size, g) {
2026
+ const stops = normalizedStops(g);
2027
+ const [cx, cy] = g.center ?? [0.5, 0.5];
2028
+ const grad = ctx.createRadialGradient(cx * size, cy * size, (g.innerRadius ?? 0) * size, cx * size, cy * size, (g.radius ?? 0.5) * size);
2029
+ for (const s of stops) grad.addColorStop(s.offset, cssColor(s.color));
2030
+ ctx.fillStyle = grad;
2031
+ ctx.fillRect(0, 0, size, size);
2032
+ }
2033
+
2034
+ // src/sequences/gradientAnim.ts
2035
+ var PATCH_KEYS = ["type", "angle", "center", "innerRadius", "radius", "stops"];
2036
+ function gradStateFrom(g) {
2037
+ return {
2038
+ type: g.type ?? "linear",
2039
+ angle: g.angle ?? 90,
2040
+ center: [...g.center ?? [0.5, 0.5]],
2041
+ innerRadius: g.innerRadius ?? 0,
2042
+ radius: g.radius ?? 0.5,
2043
+ stops: (g.stops ?? []).map((s) => Array.isArray(s) ? { offset: s[0], color: cssColor(s[1]) } : { offset: s.offset, color: cssColor(s.color) })
2044
+ };
2045
+ }
2046
+ function mergeGrad(base, patch) {
2047
+ const out = { ...base, center: [...base.center], stops: base.stops.map((s) => ({ ...s })) };
2048
+ if (patch === null || typeof patch !== "object" || Array.isArray(patch)) return out;
2049
+ const p = patch;
2050
+ if (typeof p.angle === "number") out.angle = p.angle;
2051
+ if (Array.isArray(p.center)) out.center = [Number(p.center[0]), Number(p.center[1])];
2052
+ if (typeof p.innerRadius === "number") out.innerRadius = p.innerRadius;
2053
+ if (typeof p.radius === "number") out.radius = p.radius;
2054
+ if (p.stops !== void 0) {
2055
+ const next = gradStateFrom({ stops: p.stops }).stops;
2056
+ if (next.length === base.stops.length) out.stops = next;
2057
+ }
2058
+ return out;
2059
+ }
2060
+ var objectHint = "a keyframe's fillGradient must be an object like { angle: 200, stops: [[0, '#f00'], [1, '#00f']] }";
2061
+ function validateGradientKeyframes(spec, who, warn = (m) => console.warn(m), kind = "shape") {
2062
+ const said = /* @__PURE__ */ new Set();
2063
+ const say = (m) => {
2064
+ if (!said.has(m)) {
2065
+ said.add(m);
2066
+ warn(`pixi-effects: ${who}: ${m}`);
2067
+ }
2068
+ };
2069
+ const startSpec = spec.fillGradient ?? spec.initial?.fillGradient;
2070
+ const start = startSpec && typeof startSpec === "object" && Array.isArray(startSpec.stops) ? gradStateFrom(startSpec) : null;
2071
+ let animated = false;
2072
+ if (kind === "text") {
2073
+ const radial = (g) => !!g && typeof g === "object" && ["type", "center", "innerRadius", "radius"].some((k) => g[k] !== void 0 && !(k === "type" && g.type === "linear"));
2074
+ const asked = radial(startSpec) || (spec.keyframes ?? []).some((kf) => [kf.set, kf.to, kf.from].some((b) => radial(b?.fillGradient)));
2075
+ if (asked) say("a text gradient is linear: type, center, innerRadius and radius are ignored (only angle and stops apply); use a shape for a radial gradient");
2076
+ }
2077
+ for (const kf of spec.keyframes ?? []) {
2078
+ for (const bag of [kf.set, kf.to, kf.from]) {
2079
+ if (!bag) continue;
2080
+ for (const [key, value] of Object.entries(bag)) {
2081
+ if (key === "fillGradient") {
2082
+ animated = true;
2083
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2084
+ say(objectHint);
2085
+ continue;
2086
+ }
2087
+ const p = value;
2088
+ for (const k of Object.keys(p)) {
2089
+ if (PATCH_KEYS.includes(k)) continue;
2090
+ const guess = suggestName(k, PATCH_KEYS.filter((x) => x !== "type"));
2091
+ say(`fillGradient.${k} is not a gradient property${guess ? ` \u2014 did you mean "${guess}"?` : ""} (valid: ${PATCH_KEYS.join(", ")}); it is ignored`);
2092
+ }
2093
+ if (p.type !== void 0 && start && p.type !== start.type) say("fillGradient.type cannot change over time (linear <-> radial); it is ignored");
2094
+ if (p.stops !== void 0 && start) {
2095
+ const n = Array.isArray(p.stops) ? p.stops.length : 0;
2096
+ if (n !== start.stops.length) say(`a fillGradient keyframe must give the same number of stops as the gradient has (${start.stops.length}, got ${n}); the stops are left as they were`);
2097
+ }
2098
+ } else if (/^fillGradient\./i.test(key) || /^gradient/i.test(key)) {
2099
+ const tail = key.replace(/^fillGradient\./i, "").replace(/^gradient/i, "");
2100
+ const sub = tail ? tail[0].toLowerCase() + tail.slice(1) : "angle";
2101
+ say(`"${key}" is not an animatable property: write fillGradient: { ${PATCH_KEYS.includes(sub) ? sub : "angle"} } (the whole gradient is one object) in the keyframe's to / from / set`);
2102
+ }
2103
+ }
2104
+ }
2105
+ }
2106
+ if (animated && !start) say("a keyframe animates fillGradient but the layer has none to start from: give it a fillGradient first (top level or in initial)");
2107
+ }
2108
+ function tweenGradient(timeline, holder, fromPatch, toPatch, duration, ease, at, colorSpace, onChange, loop = {}) {
2109
+ let a = null, b = null;
2110
+ let resting = null;
2111
+ let colors = [];
2112
+ const proxy = { p: 0 };
2113
+ timeline.fromTo(proxy, { p: 0 }, {
2114
+ p: 1,
2115
+ duration,
2116
+ ease,
2117
+ ...loop,
2118
+ onStart: () => {
2119
+ const live = holder.grad;
2120
+ a = fromPatch !== void 0 ? mergeGrad(live, fromPatch) : live;
2121
+ if (toPatch === void 0) resting ??= live;
2122
+ b = toPatch !== void 0 ? mergeGrad(a, toPatch) : resting;
2123
+ colors = a.stops.map((s, i) => colorSpace === "rgb" ? gsap.utils.interpolate(s.color, b.stops[i].color) : buildColorInterp(s.color, b.stops[i].color, colorSpace));
2124
+ },
2125
+ onUpdate: () => {
2126
+ if (!a || !b) return;
2127
+ const t = proxy.p;
2128
+ const l = (x, y) => x + (y - x) * t;
2129
+ holder.grad = {
2130
+ type: a.type,
2131
+ angle: l(a.angle, b.angle),
2132
+ center: [l(a.center[0], b.center[0]), l(a.center[1], b.center[1])],
2133
+ innerRadius: l(a.innerRadius, b.innerRadius),
2134
+ radius: l(a.radius, b.radius),
2135
+ stops: a.stops.map((s, i) => ({ offset: l(s.offset, b.stops[i].offset), color: colors[i](t) }))
2136
+ };
2137
+ onChange();
2138
+ }
2139
+ }, at);
2140
+ }
2141
+ var PAINT_SIZE = 512;
2142
+ var GradientPainter = class {
2143
+ constructor(size = PAINT_SIZE) {
2144
+ this.size = size;
2145
+ this.canvas.width = size;
2146
+ this.canvas.height = size;
2147
+ this.tex = Texture.from(this.canvas);
2148
+ globalThis.__gradientPainters = (globalThis.__gradientPainters ?? 0) + 1;
2149
+ }
2150
+ size;
2151
+ canvas = document.createElement("canvas");
2152
+ tex;
2153
+ /** Paint `g` and return what `Graphics.fill(...)` takes. */
2154
+ paint(g) {
2155
+ const ctx = this.canvas.getContext("2d"), s = this.size;
2156
+ if (!ctx) return new FillGradient(gradientOptions({ angle: g.angle, stops: g.stops.map((x) => [x.offset, x.color]) }));
2157
+ ctx.clearRect(0, 0, s, s);
2158
+ if (g.type === "radial") {
2159
+ paintRadial(ctx, s, { stops: g.stops.map((x) => [x.offset, x.color]), center: g.center, innerRadius: g.innerRadius, radius: g.radius });
2160
+ } else {
2161
+ const a = g.angle * Math.PI / 180, dx = Math.cos(a), dy = Math.sin(a);
2162
+ const h = (Math.abs(dx) + Math.abs(dy)) / 2;
2163
+ const grad = ctx.createLinearGradient((0.5 - dx * h) * s, (0.5 - dy * h) * s, (0.5 + dx * h) * s, (0.5 + dy * h) * s);
2164
+ for (const st of g.stops) grad.addColorStop(Math.min(1, Math.max(0, st.offset)), st.color);
2165
+ ctx.fillStyle = grad;
2166
+ ctx.fillRect(0, 0, s, s);
2167
+ }
2168
+ this.tex.source.update();
2169
+ return { texture: this.tex, textureSpace: "local" };
2170
+ }
2171
+ /** Give the texture and the canvas back (a layer that is destroyed must not leave them). */
2172
+ destroy() {
2173
+ this.tex.destroy(true);
2174
+ this.canvas.width = this.canvas.height = 0;
2175
+ globalThis.__gradientPainters = Math.max(0, (globalThis.__gradientPainters ?? 1) - 1);
2176
+ }
2177
+ };
2178
+ function bindGradientKeyframes(timeline, holder, keyframes, duration, offset, colorSpace, onChange) {
2179
+ const bag = (b) => b?.fillGradient;
2180
+ for (const kf of keyframes) {
2181
+ const at = offset + resolveAt(kf.at, duration);
2182
+ const setV = bag(kf.set), fromV = bag(kf.from), toV = bag(kf.to);
2183
+ if (setV !== void 0) {
2184
+ revertibleSet(
2185
+ timeline,
2186
+ at,
2187
+ () => ({ state: holder.grad }),
2188
+ (v) => {
2189
+ holder.grad = v.state ?? mergeGrad(holder.grad, v.patch);
2190
+ onChange();
2191
+ },
2192
+ { patch: setV }
2193
+ );
2194
+ }
2195
+ if (fromV !== void 0 || toV !== void 0) {
2196
+ tweenGradient(timeline, holder, fromV, toV, kfDuration(kf), kf.ease ?? "none", at, colorSpace, onChange, loopVars(kf));
2197
+ }
2198
+ }
2199
+ }
2200
+ var hasGradientKeys = (keyframes) => (keyframes ?? []).some((kf) => [kf.set, kf.to, kf.from].some((b) => b && b.fillGradient !== void 0));
2201
+
2202
+ // src/sequences/Text.ts
1511
2203
  var STYLE_OPAQUE_KEYS = [
1512
2204
  "fontFamily",
1513
2205
  "fill",
@@ -1542,6 +2234,21 @@ var TextSequence = class extends Sequence {
1542
2234
  /** `visibleChars` is in play (initial or animated): otherwise the whole text shows. */
1543
2235
  _typed = false;
1544
2236
  _lastText = "";
2237
+ /** A gradient fill for the text (`fillGradient`, static or animated): rebuilt as a FillGradient on each change (the text is re-rasterised). */
2238
+ _grad = null;
2239
+ _fillGradient = null;
2240
+ _applyGradient() {
2241
+ const g = this._grad.grad;
2242
+ const next = new FillGradient(gradientOptions({ angle: g.angle, stops: g.stops.map((s) => [s.offset, s.color]) }));
2243
+ this.target.style.fill = next;
2244
+ this._fillGradient?.destroy();
2245
+ this._fillGradient = next;
2246
+ }
2247
+ destroy() {
2248
+ super.destroy();
2249
+ this._fillGradient?.destroy();
2250
+ this._fillGradient = null;
2251
+ }
1545
2252
  /** A typewriter that has typed nothing yet: the layer is empty on purpose (inspect must not call that "no size"). */
1546
2253
  get showsNothingYet() {
1547
2254
  return this._typed && this._lastText === "" && this._template !== "";
@@ -1588,6 +2295,12 @@ var TextSequence = class extends Sequence {
1588
2295
  this.intrinsicWidth = text.width;
1589
2296
  this.intrinsicHeight = text.height;
1590
2297
  }
2298
+ validateGradientKeyframes(this.spec, describeLayer(this.spec), void 0, "text");
2299
+ const gradientSpec = this.spec.fillGradient ?? initialProps.fillGradient;
2300
+ if (gradientSpec) {
2301
+ this._grad = { grad: gradStateFrom(gradientSpec) };
2302
+ this._applyGradient();
2303
+ }
1591
2304
  const scope = this.scope();
1592
2305
  if (initialProps.value !== void 0) this._counters.value = resolveNumber(initialProps.value, scope);
1593
2306
  this._typed = initialProps.visibleChars !== void 0 || animates(this.spec.keyframes, "visibleChars");
@@ -1606,11 +2319,12 @@ var TextSequence = class extends Sequence {
1606
2319
  bindTimeline(timeline, offset = 0) {
1607
2320
  if (!this.target) return;
1608
2321
  const colorSpace = this.spec.colorSpace ?? "rgb";
1609
- const stripped = stripKeys2(this.spec, ["fill", "value", "text", "visibleChars"]);
2322
+ const stripped = stripKeys2(this.spec, ["fill", "value", "text", "visibleChars", "fillGradient"]);
1610
2323
  runSuperWithStrippedSpec(this, stripped, timeline, offset);
1611
2324
  const keyframes = this.spec.keyframes ?? [];
1612
2325
  const origin = offset + this.at;
1613
2326
  bindFillKeyframes(timeline, this.target, keyframes, this.duration, origin, colorSpace);
2327
+ if (this._grad) bindGradientKeyframes(timeline, this._grad, keyframes, this.duration, origin, colorSpace, () => this._applyGradient());
1614
2328
  const refresh = () => this._refreshText();
1615
2329
  bindCounterKeyframes(timeline, this._counters, "value", refresh, keyframes, this.duration, origin, this.scope());
1616
2330
  bindCounterKeyframes(timeline, this._counters, "visibleChars", refresh, keyframes, this.duration, origin, this.scope());
@@ -1660,7 +2374,7 @@ function bindCounterKeyframes(timeline, state, key, refresh, keyframes, parentDu
1660
2374
  const toV = kf.to?.[key];
1661
2375
  if (setV === void 0 && fromV === void 0 && toV === void 0) continue;
1662
2376
  const at = origin + resolveAt(kf.at, parentDuration);
1663
- const duration = kf.duration ?? 0;
2377
+ const duration = kfDuration(kf);
1664
2378
  const ease = kf.ease ?? "none";
1665
2379
  const loop = loopVars(kf);
1666
2380
  if (setV !== void 0) {
@@ -1706,7 +2420,7 @@ function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, co
1706
2420
  };
1707
2421
  for (const kf of keyframes) {
1708
2422
  const at = offset + resolveAt(kf.at, parentDuration);
1709
- const duration = kf.duration ?? 0;
2423
+ const duration = kfDuration(kf);
1710
2424
  const ease = kf.ease ?? "none";
1711
2425
  const setFill = pickFill(kf.set);
1712
2426
  const fromFill = pickFill(kf.from);
@@ -2040,39 +2754,6 @@ var VideoSequence = class extends Sequence {
2040
2754
  super.destroy();
2041
2755
  }
2042
2756
  };
2043
-
2044
- // src/sequences/gradient.ts
2045
- function cssColor(c) {
2046
- return typeof c === "number" ? "#" + c.toString(16).padStart(6, "0") : c;
2047
- }
2048
- function normalizedStops(g) {
2049
- const stops = g.stops ?? [];
2050
- if (stops.length < 2) throw new Error('pixi-effects: fillGradient needs at least two colour stops, e.g. stops: [[0, "#000"], [1, "#fff"]]');
2051
- return stops.map((s) => Array.isArray(s) ? { offset: s[0], color: s[1] } : { offset: s.offset, color: s.color });
2052
- }
2053
- function gradientOptions(g) {
2054
- const colorStops = normalizedStops(g);
2055
- const a = (g.angle ?? 90) * Math.PI / 180;
2056
- const dx = Math.cos(a), dy = Math.sin(a);
2057
- const h = (Math.abs(dx) + Math.abs(dy)) / 2;
2058
- return {
2059
- type: "linear",
2060
- start: { x: 0.5 - dx * h, y: 0.5 - dy * h },
2061
- end: { x: 0.5 + dx * h, y: 0.5 + dy * h },
2062
- colorStops,
2063
- textureSpace: "local"
2064
- };
2065
- }
2066
- function paintRadial(ctx, size, g) {
2067
- const stops = normalizedStops(g);
2068
- const [cx, cy] = g.center ?? [0.5, 0.5];
2069
- const grad = ctx.createRadialGradient(cx * size, cy * size, (g.innerRadius ?? 0) * size, cx * size, cy * size, (g.radius ?? 0.5) * size);
2070
- for (const s of stops) grad.addColorStop(s.offset, cssColor(s.color));
2071
- ctx.fillStyle = grad;
2072
- ctx.fillRect(0, 0, size, size);
2073
- }
2074
-
2075
- // src/sequences/gradientFill.ts
2076
2757
  var RADIAL_SIZE = 256;
2077
2758
  function makeGradientFill(g) {
2078
2759
  if (g.type === "radial") {
@@ -2264,7 +2945,7 @@ function morphAt(pairs, t) {
2264
2945
  }
2265
2946
 
2266
2947
  // src/sequences/Shape.ts
2267
- var STYLE_KEYS = ["fillColor", "fillAlpha", "strokeColor", "strokeAlpha", "strokeWidth"];
2948
+ var STYLE_KEYS2 = ["fillColor", "fillAlpha", "strokeColor", "strokeAlpha", "strokeWidth"];
2268
2949
  var COLOR_KEYS = /* @__PURE__ */ new Set(["fillColor", "strokeColor"]);
2269
2950
  var TRIM_KEYS = ["trimStart", "trimEnd"];
2270
2951
  var GEOMETRY_KEYS_BY_SHAPE = {
@@ -2290,7 +2971,7 @@ var ShapeSequence = class extends Sequence {
2290
2971
  // by `Container.onRender`.
2291
2972
  _drawGeometry = () => {
2292
2973
  };
2293
- _liveKeys = new Set(STYLE_KEYS);
2974
+ _liveKeys = new Set(STYLE_KEYS2);
2294
2975
  async build() {
2295
2976
  const graphics = new Graphics({ label: this.spec.name });
2296
2977
  graphics.cullable = true;
@@ -2316,7 +2997,15 @@ var ShapeSequence = class extends Sequence {
2316
2997
  this._state.trimEnd ??= 1;
2317
2998
  }
2318
2999
  const gradientSpec = this.spec.fillGradient ?? this.spec.initial?.fillGradient;
2319
- if (gradientSpec) this._state.fillStyle = makeGradientFill(gradientSpec);
3000
+ validateGradientKeyframes(this.spec, describeLayer(this.spec));
3001
+ if (gradientSpec) {
3002
+ if (hasGradientKeys(this.spec.keyframes)) {
3003
+ this._grad = { grad: gradStateFrom(gradientSpec) };
3004
+ this._painter = new GradientPainter();
3005
+ this._gradDirty = true;
3006
+ this._liveKeys.add("fillGradient");
3007
+ } else this._state.fillStyle = makeGradientFill(gradientSpec);
3008
+ }
2320
3009
  const morph = this._setupMorph();
2321
3010
  this._drawGeometry = makeDrawGeometry(this.spec, scope, this._state, morph);
2322
3011
  this._outline = makeOutline(this.spec, scope, this._state, morph);
@@ -2375,11 +3064,23 @@ var ShapeSequence = class extends Sequence {
2375
3064
  this._state.morph ??= 0;
2376
3065
  return { pairs, to: String(top.morphTo) };
2377
3066
  }
3067
+ destroy() {
3068
+ super.destroy();
3069
+ this._painter?.destroy();
3070
+ this._painter = null;
3071
+ }
2378
3072
  _fresh = false;
3073
+ _grad = null;
3074
+ _painter = null;
3075
+ _gradDirty = false;
2379
3076
  _outline = null;
2380
3077
  _redraw() {
2381
3078
  const graphics = this.target;
2382
3079
  const s = this._state;
3080
+ if (this._grad && this._gradDirty) {
3081
+ s.fillStyle = this._painter.paint(this._grad.grad);
3082
+ this._gradDirty = false;
3083
+ }
2383
3084
  graphics.clear();
2384
3085
  const trimmed = (s.trimStart ?? 0) > 0 || (s.trimEnd ?? 1) < 1;
2385
3086
  if (!trimmed || !this._outline || !(s.strokeColor !== void 0 && s.strokeWidth > 0)) {
@@ -2425,6 +3126,9 @@ var ShapeSequence = class extends Sequence {
2425
3126
  applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], startTime);
2426
3127
  const colorSpace = this.spec.colorSpace ?? "rgb";
2427
3128
  bindLiveKeyframes(timeline, this._state, this._liveKeys, this.spec.keyframes ?? [], this.duration, scope, startTime, colorSpace);
3129
+ if (this._grad) bindGradientKeyframes(timeline, this._grad, this.spec.keyframes ?? [], this.duration, startTime, colorSpace, () => {
3130
+ this._gradDirty = true;
3131
+ });
2428
3132
  this.absoluteStart = startTime;
2429
3133
  const endTime = startTime + this.duration;
2430
3134
  this.target.renderable = startTime <= 0;
@@ -2461,7 +3165,7 @@ function seedStrokeStyle(state, spec, initial, who) {
2461
3165
  }
2462
3166
  }
2463
3167
  function seedStyle(state, initial, scope) {
2464
- for (const k of STYLE_KEYS) {
3168
+ for (const k of STYLE_KEYS2) {
2465
3169
  const v = initial[k];
2466
3170
  if (v === void 0) continue;
2467
3171
  state[k] = COLOR_KEYS.has(k) ? v : numOrZero(v, scope);
@@ -2684,13 +3388,14 @@ function makeOutline(spec, scope, s, morph) {
2684
3388
  function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration, scope, offset, colorSpace) {
2685
3389
  for (const kf of keyframes) {
2686
3390
  const at = offset + resolveAt(kf.at, parentDuration);
2687
- const duration = kf.duration ?? 0;
3391
+ const duration = kfDuration(kf);
2688
3392
  const ease = kf.ease ?? "none";
2689
3393
  const loop = loopVars(kf);
2690
3394
  if (kf.set) {
2691
3395
  const live = pickLive(kf.set, liveKeys);
2692
3396
  if (live) {
2693
3397
  for (const [k, v] of Object.entries(live)) {
3398
+ if (k === "fillGradient") continue;
2694
3399
  const resolved = resolveLiveValue(k, v, scope);
2695
3400
  const live2 = state;
2696
3401
  revertibleSet(timeline, at, () => live2[k], (v2) => {
@@ -2706,6 +3411,7 @@ function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration,
2706
3411
  ...toLive ? Object.keys(toLive) : []
2707
3412
  ]);
2708
3413
  for (const k of allKeys) {
3414
+ if (k === "fillGradient") continue;
2709
3415
  const key = k;
2710
3416
  const fromRaw = fromLive?.[key];
2711
3417
  const toRaw = toLive?.[key];
@@ -2754,7 +3460,7 @@ function numOrZero(v, scope) {
2754
3460
  var _CompositionSequence = null;
2755
3461
  async function getCompositionSequence() {
2756
3462
  if (!_CompositionSequence) {
2757
- _CompositionSequence = (await import('./Composition-MLBHYYFF.js')).CompositionSequence;
3463
+ _CompositionSequence = (await import('./Composition-QBKEEJBV.js')).CompositionSequence;
2758
3464
  }
2759
3465
  return _CompositionSequence;
2760
3466
  }
@@ -2792,6 +3498,6 @@ async function buildSequenceTree(specs, parent, root) {
2792
3498
  return out;
2793
3499
  }
2794
3500
 
2795
- export { CameraSequence, DEFAULT_FOV, DEG, NEAR, NullSequence, Sequence, buildScope, buildSequenceTree, cameraBasis, checkEase, collectPropKeys, describeLayer, ensureFilterLibrary, evaluateExpr, flattenSvgPath, homeCamera, homeDistance, lintText, lintTiming, noise, projectLayer, rand, random, registerSequenceType, resolveAt, summarizeWarnings };
2796
- //# sourceMappingURL=chunk-SG3Y66FN.js.map
2797
- //# sourceMappingURL=chunk-SG3Y66FN.js.map
3501
+ export { CameraSequence, DEFAULT_FOV, DEG, NEAR, NullSequence, Sequence, buildScope, buildSequenceTree, cameraBasis, checkEase, collectPropKeys, describeLayer, ensureFilterLibrary, evaluateExpr, flattenSvgPath, homeCamera, homeDistance, lintKeys, lintText, lintTiming, noise, projectLayer, rand, random, registerSequenceType, resolveAt, summarizeWarnings };
3502
+ //# sourceMappingURL=chunk-YRXTKOV5.js.map
3503
+ //# sourceMappingURL=chunk-YRXTKOV5.js.map