@redwilly/anima 0.1.25 → 0.1.26

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 (3) hide show
  1. package/dist/index.d.ts +457 -247
  2. package/dist/index.js +1926 -1430
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,32 +1,32 @@
1
- // src/core/math/Vector2/Vector2.ts
2
- var Vector2 = class _Vector2 {
1
+ // src/core/math/vector/Vector.ts
2
+ var Vector = class _Vector {
3
3
  x;
4
4
  y;
5
- constructor(x, y) {
5
+ z;
6
+ constructor(x, y, z = 0) {
6
7
  this.x = x;
7
8
  this.y = y;
9
+ this.z = z;
8
10
  }
9
11
  add(other) {
10
- return new _Vector2(this.x + other.x, this.y + other.y);
12
+ return new _Vector(this.x + other.x, this.y + other.y, this.z + other.z);
11
13
  }
12
14
  subtract(other) {
13
- return new _Vector2(this.x - other.x, this.y - other.y);
15
+ return new _Vector(this.x - other.x, this.y - other.y, this.z - other.z);
14
16
  }
15
17
  multiply(scalar) {
16
- return new _Vector2(this.x * scalar, this.y * scalar);
18
+ return new _Vector(this.x * scalar, this.y * scalar, this.z * scalar);
17
19
  }
18
20
  dot(other) {
19
- return this.x * other.x + this.y * other.y;
21
+ return this.x * other.x + this.y * other.y + this.z * other.z;
20
22
  }
21
- /** Magnitude (length) of the vector. */
22
23
  length() {
23
- return Math.sqrt(this.x * this.x + this.y * this.y);
24
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
24
25
  }
25
- /** Returns a normalized unit vector. Returns ZERO for zero-length vectors. */
26
26
  normalize() {
27
27
  const len = this.length();
28
28
  if (len === 0) {
29
- return _Vector2.ZERO;
29
+ return _Vector.ZERO;
30
30
  }
31
31
  return this.multiply(1 / len);
32
32
  }
@@ -34,164 +34,232 @@ var Vector2 = class _Vector2 {
34
34
  return this.multiply(1 - t).add(other.multiply(t));
35
35
  }
36
36
  equals(other, tolerance = 1e-6) {
37
- return Math.abs(this.x - other.x) < tolerance && Math.abs(this.y - other.y) < tolerance;
37
+ return Math.abs(this.x - other.x) < tolerance && Math.abs(this.y - other.y) < tolerance && Math.abs(this.z - other.z) < tolerance;
38
38
  }
39
- static ZERO = new _Vector2(0, 0);
40
- static UP = new _Vector2(0, -1);
41
- static DOWN = new _Vector2(0, 1);
42
- static LEFT = new _Vector2(-1, 0);
43
- static RIGHT = new _Vector2(1, 0);
39
+ toPlanar() {
40
+ return new _Vector(this.x, this.y, 0);
41
+ }
42
+ static fromPlanar(v, z = 0) {
43
+ return new _Vector(v.x, v.y, z);
44
+ }
45
+ static ZERO = new _Vector(0, 0, 0);
46
+ static UP = new _Vector(0, -1, 0);
47
+ static DOWN = new _Vector(0, 1, 0);
48
+ static LEFT = new _Vector(-1, 0, 0);
49
+ static RIGHT = new _Vector(1, 0, 0);
44
50
  };
45
51
 
46
- // src/core/math/matrix/factories.ts
47
- function createIdentity() {
48
- return new Float32Array([
49
- 1,
50
- 0,
51
- 0,
52
- 0,
53
- 1,
54
- 0,
55
- 0,
56
- 0,
57
- 1
58
- ]);
59
- }
60
- function createTranslation(tx, ty) {
61
- return new Float32Array([
62
- 1,
63
- 0,
64
- tx,
65
- 0,
66
- 1,
67
- ty,
68
- 0,
69
- 0,
70
- 1
71
- ]);
72
- }
73
- function createRotation(angle) {
74
- const c = Math.cos(angle);
75
- const s = Math.sin(angle);
76
- return new Float32Array([
77
- c,
78
- -s,
79
- 0,
80
- s,
81
- c,
82
- 0,
83
- 0,
84
- 0,
85
- 1
86
- ]);
87
- }
88
- function createScale(sx, sy) {
89
- return new Float32Array([
90
- sx,
91
- 0,
92
- 0,
93
- 0,
94
- sy,
95
- 0,
96
- 0,
97
- 0,
98
- 1
99
- ]);
100
- }
101
- function createShear(shx, shy) {
102
- return new Float32Array([
103
- 1,
104
- shx,
105
- 0,
106
- shy,
107
- 1,
108
- 0,
109
- 0,
110
- 0,
111
- 1
112
- ]);
113
- }
114
-
115
- // src/core/math/matrix/Matrix3x3.ts
116
- var Matrix3x3 = class _Matrix3x3 {
52
+ // src/core/math/matrix/Matrix4x4.ts
53
+ var Matrix4x4 = class _Matrix4x4 {
117
54
  values;
118
55
  constructor(values) {
119
- if (values.length !== 9) {
120
- throw new Error("Matrix3x3 requires 9 values");
56
+ if (values.length !== 16) {
57
+ throw new Error("Matrix4x4 requires 16 values");
121
58
  }
122
59
  this.values = new Float32Array(values);
123
60
  }
124
61
  multiply(other) {
125
62
  const a = this.values;
126
63
  const b = other.values;
127
- const out = new Float32Array(9);
128
- const a00 = a[0], a01 = a[1], a02 = a[2];
129
- const a10 = a[3], a11 = a[4], a12 = a[5];
130
- const a20 = a[6], a21 = a[7], a22 = a[8];
131
- const b00 = b[0], b01 = b[1], b02 = b[2];
132
- const b10 = b[3], b11 = b[4], b12 = b[5];
133
- const b20 = b[6], b21 = b[7], b22 = b[8];
134
- out[0] = a00 * b00 + a01 * b10 + a02 * b20;
135
- out[1] = a00 * b01 + a01 * b11 + a02 * b21;
136
- out[2] = a00 * b02 + a01 * b12 + a02 * b22;
137
- out[3] = a10 * b00 + a11 * b10 + a12 * b20;
138
- out[4] = a10 * b01 + a11 * b11 + a12 * b21;
139
- out[5] = a10 * b02 + a11 * b12 + a12 * b22;
140
- out[6] = a20 * b00 + a21 * b10 + a22 * b20;
141
- out[7] = a20 * b01 + a21 * b11 + a22 * b21;
142
- out[8] = a20 * b02 + a21 * b12 + a22 * b22;
143
- return new _Matrix3x3(out);
144
- }
145
- /** Transforms a Vector2 point (assumes z=1). */
64
+ const out = new Float32Array(16);
65
+ for (let row = 0; row < 4; row++) {
66
+ for (let col = 0; col < 4; col++) {
67
+ let sum = 0;
68
+ for (let k = 0; k < 4; k++) {
69
+ sum += a[row * 4 + k] * b[k * 4 + col];
70
+ }
71
+ out[row * 4 + col] = sum;
72
+ }
73
+ }
74
+ return new _Matrix4x4(out);
75
+ }
146
76
  transformPoint(point) {
147
77
  const m = this.values;
148
78
  const x = point.x;
149
79
  const y = point.y;
150
- const m00 = m[0], m01 = m[1], m02 = m[2];
151
- const m10 = m[3], m11 = m[4], m12 = m[5];
152
- const tx = m00 * x + m01 * y + m02;
153
- const ty = m10 * x + m11 * y + m12;
154
- return new Vector2(tx, ty);
80
+ const z = point.z;
81
+ const tx = m[0] * x + m[1] * y + m[2] * z + m[3];
82
+ const ty = m[4] * x + m[5] * y + m[6] * z + m[7];
83
+ const tz = m[8] * x + m[9] * y + m[10] * z + m[11];
84
+ const tw = m[12] * x + m[13] * y + m[14] * z + m[15];
85
+ if (Math.abs(tw) < 1e-10 || Math.abs(tw - 1) < 1e-10) {
86
+ return new Vector(tx, ty, tz);
87
+ }
88
+ return new Vector(tx / tw, ty / tw, tz / tw);
89
+ }
90
+ transformPoint2D(point) {
91
+ const transformed = this.transformPoint(Vector.fromPlanar(point, 0));
92
+ return transformed.toPlanar();
155
93
  }
156
- /** @throws Error if the matrix is not invertible. */
157
94
  inverse() {
158
95
  const m = this.values;
159
- const m00 = m[0], m01 = m[1], m02 = m[2];
160
- const m10 = m[3], m11 = m[4], m12 = m[5];
161
- const m20 = m[6], m21 = m[7], m22 = m[8];
162
- const det = m00 * (m11 * m22 - m12 * m21) - m01 * (m10 * m22 - m12 * m20) + m02 * (m10 * m21 - m11 * m20);
96
+ const out = new Float32Array(16);
97
+ const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
98
+ const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
99
+ const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
100
+ const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
101
+ const b00 = m00 * m11 - m01 * m10;
102
+ const b01 = m00 * m12 - m02 * m10;
103
+ const b02 = m00 * m13 - m03 * m10;
104
+ const b03 = m01 * m12 - m02 * m11;
105
+ const b04 = m01 * m13 - m03 * m11;
106
+ const b05 = m02 * m13 - m03 * m12;
107
+ const b06 = m20 * m31 - m21 * m30;
108
+ const b07 = m20 * m32 - m22 * m30;
109
+ const b08 = m20 * m33 - m23 * m30;
110
+ const b09 = m21 * m32 - m22 * m31;
111
+ const b10 = m21 * m33 - m23 * m31;
112
+ const b11 = m22 * m33 - m23 * m32;
113
+ const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
163
114
  if (Math.abs(det) < 1e-10) {
164
115
  throw new Error("Matrix is not invertible");
165
116
  }
166
117
  const invDet = 1 / det;
167
- const out = new Float32Array(9);
168
- out[0] = (m11 * m22 - m12 * m21) * invDet;
169
- out[1] = (m02 * m21 - m01 * m22) * invDet;
170
- out[2] = (m01 * m12 - m02 * m11) * invDet;
171
- out[3] = (m12 * m20 - m10 * m22) * invDet;
172
- out[4] = (m00 * m22 - m02 * m20) * invDet;
173
- out[5] = (m02 * m10 - m00 * m12) * invDet;
174
- out[6] = (m10 * m21 - m11 * m20) * invDet;
175
- out[7] = (m01 * m20 - m00 * m21) * invDet;
176
- out[8] = (m00 * m11 - m01 * m10) * invDet;
177
- return new _Matrix3x3(out);
118
+ out[0] = (m11 * b11 - m12 * b10 + m13 * b09) * invDet;
119
+ out[1] = (m02 * b10 - m01 * b11 - m03 * b09) * invDet;
120
+ out[2] = (m31 * b05 - m32 * b04 + m33 * b03) * invDet;
121
+ out[3] = (m22 * b04 - m21 * b05 - m23 * b03) * invDet;
122
+ out[4] = (m12 * b08 - m10 * b11 - m13 * b07) * invDet;
123
+ out[5] = (m00 * b11 - m02 * b08 + m03 * b07) * invDet;
124
+ out[6] = (m32 * b02 - m30 * b05 - m33 * b01) * invDet;
125
+ out[7] = (m20 * b05 - m22 * b02 + m23 * b01) * invDet;
126
+ out[8] = (m10 * b10 - m11 * b08 + m13 * b06) * invDet;
127
+ out[9] = (m01 * b08 - m00 * b10 - m03 * b06) * invDet;
128
+ out[10] = (m30 * b04 - m31 * b02 + m33 * b00) * invDet;
129
+ out[11] = (m21 * b02 - m20 * b04 - m23 * b00) * invDet;
130
+ out[12] = (m11 * b07 - m10 * b09 - m12 * b06) * invDet;
131
+ out[13] = (m00 * b09 - m01 * b07 + m02 * b06) * invDet;
132
+ out[14] = (m31 * b01 - m30 * b03 - m32 * b00) * invDet;
133
+ out[15] = (m20 * b03 - m21 * b01 + m22 * b00) * invDet;
134
+ return new _Matrix4x4(out);
178
135
  }
179
136
  static identity() {
180
- return _Matrix3x3.IDENTITY;
137
+ return _Matrix4x4.IDENTITY;
138
+ }
139
+ static translation(tx, ty, tz = 0) {
140
+ return new _Matrix4x4([
141
+ 1,
142
+ 0,
143
+ 0,
144
+ tx,
145
+ 0,
146
+ 1,
147
+ 0,
148
+ ty,
149
+ 0,
150
+ 0,
151
+ 1,
152
+ tz,
153
+ 0,
154
+ 0,
155
+ 0,
156
+ 1
157
+ ]);
181
158
  }
182
- static translation(tx, ty) {
183
- return new _Matrix3x3(createTranslation(tx, ty));
159
+ static rotationX(angle) {
160
+ const c = Math.cos(angle);
161
+ const s = Math.sin(angle);
162
+ return new _Matrix4x4([
163
+ 1,
164
+ 0,
165
+ 0,
166
+ 0,
167
+ 0,
168
+ c,
169
+ -s,
170
+ 0,
171
+ 0,
172
+ s,
173
+ c,
174
+ 0,
175
+ 0,
176
+ 0,
177
+ 0,
178
+ 1
179
+ ]);
184
180
  }
185
- static rotation(angle) {
186
- return new _Matrix3x3(createRotation(angle));
181
+ static rotationY(angle) {
182
+ const c = Math.cos(angle);
183
+ const s = Math.sin(angle);
184
+ return new _Matrix4x4([
185
+ c,
186
+ 0,
187
+ s,
188
+ 0,
189
+ 0,
190
+ 1,
191
+ 0,
192
+ 0,
193
+ -s,
194
+ 0,
195
+ c,
196
+ 0,
197
+ 0,
198
+ 0,
199
+ 0,
200
+ 1
201
+ ]);
187
202
  }
188
- static scale(sx, sy) {
189
- return new _Matrix3x3(createScale(sx, sy));
203
+ static rotationZ(angle) {
204
+ const c = Math.cos(angle);
205
+ const s = Math.sin(angle);
206
+ return new _Matrix4x4([
207
+ c,
208
+ -s,
209
+ 0,
210
+ 0,
211
+ s,
212
+ c,
213
+ 0,
214
+ 0,
215
+ 0,
216
+ 0,
217
+ 1,
218
+ 0,
219
+ 0,
220
+ 0,
221
+ 0,
222
+ 1
223
+ ]);
190
224
  }
191
- static shear(shx, shy) {
192
- return new _Matrix3x3(createShear(shx, shy));
225
+ static scale(sx, sy, sz = 1) {
226
+ return new _Matrix4x4([
227
+ sx,
228
+ 0,
229
+ 0,
230
+ 0,
231
+ 0,
232
+ sy,
233
+ 0,
234
+ 0,
235
+ 0,
236
+ 0,
237
+ sz,
238
+ 0,
239
+ 0,
240
+ 0,
241
+ 0,
242
+ 1
243
+ ]);
193
244
  }
194
- static IDENTITY = new _Matrix3x3(createIdentity());
245
+ static IDENTITY = new _Matrix4x4([
246
+ 1,
247
+ 0,
248
+ 0,
249
+ 0,
250
+ 0,
251
+ 1,
252
+ 0,
253
+ 0,
254
+ 0,
255
+ 0,
256
+ 1,
257
+ 0,
258
+ 0,
259
+ 0,
260
+ 0,
261
+ 1
262
+ ]);
195
263
  };
196
264
 
197
265
  // src/core/math/color/conversions.ts
@@ -383,8 +451,8 @@ function getCubicLength(p0, p1, p2, p3) {
383
451
  // src/core/math/bezier/sampling.ts
384
452
  function getPathLength(commands) {
385
453
  let length = 0;
386
- let cursor = new Vector2(0, 0);
387
- let subpathStart = new Vector2(0, 0);
454
+ let cursor = new Vector(0, 0);
455
+ let subpathStart = new Vector(0, 0);
388
456
  for (const cmd of commands) {
389
457
  switch (cmd.type) {
390
458
  case "Move":
@@ -417,12 +485,12 @@ function getPathLength(commands) {
417
485
  }
418
486
  function getTangentAtPath(commands, t) {
419
487
  const totalLength = getPathLength(commands);
420
- if (totalLength === 0) return Vector2.RIGHT;
488
+ if (totalLength === 0) return Vector.RIGHT;
421
489
  t = Math.max(0, Math.min(1, t));
422
490
  const targetDistance = t * totalLength;
423
491
  let currentDistance = 0;
424
- let cursor = new Vector2(0, 0);
425
- let subpathStart = new Vector2(0, 0);
492
+ let cursor = new Vector(0, 0);
493
+ let subpathStart = new Vector(0, 0);
426
494
  for (const cmd of commands) {
427
495
  let segmentLength = 0;
428
496
  switch (cmd.type) {
@@ -473,14 +541,14 @@ function getTangentAtPath(commands, t) {
473
541
  }
474
542
  currentDistance += segmentLength;
475
543
  }
476
- return Vector2.RIGHT;
544
+ return Vector.RIGHT;
477
545
  }
478
546
 
479
547
  // src/core/math/bezier/morphing.ts
480
548
  function toCubicCommands(commands) {
481
549
  const result = [];
482
- let cursor = new Vector2(0, 0);
483
- let subpathStart = new Vector2(0, 0);
550
+ let cursor = new Vector(0, 0);
551
+ let subpathStart = new Vector(0, 0);
484
552
  for (const cmd of commands) {
485
553
  switch (cmd.type) {
486
554
  case "Move":
@@ -573,7 +641,7 @@ function subdividePath(commands, targetCount) {
573
641
  return currentCommands;
574
642
  }
575
643
  let splitsPerformed = 0;
576
- let cursor = new Vector2(0, 0);
644
+ let cursor = new Vector(0, 0);
577
645
  for (let i = 0; i < currentCommands.length; i++) {
578
646
  const cmd = currentCommands[i];
579
647
  if (cmd.type === "Move") {
@@ -610,8 +678,8 @@ function subdividePath(commands, targetCount) {
610
678
  // src/core/math/bezier/BezierPath.ts
611
679
  var BezierPath = class _BezierPath {
612
680
  commands = [];
613
- currentPoint = Vector2.ZERO;
614
- startPoint = Vector2.ZERO;
681
+ currentPoint = Vector.ZERO;
682
+ startPoint = Vector.ZERO;
615
683
  // Caching for O(1) length and O(log N) point sampling
616
684
  cachedLength = null;
617
685
  segmentLengths = [];
@@ -628,8 +696,8 @@ var BezierPath = class _BezierPath {
628
696
  this.segmentLengths = [];
629
697
  this.segmentCDF = [];
630
698
  let totalLength = 0;
631
- let cursor = Vector2.ZERO;
632
- let subpathStart = Vector2.ZERO;
699
+ let cursor = Vector.ZERO;
700
+ let subpathStart = Vector.ZERO;
633
701
  for (const cmd of this.commands) {
634
702
  let segmentLength = 0;
635
703
  switch (cmd.type) {
@@ -708,7 +776,7 @@ var BezierPath = class _BezierPath {
708
776
  this.ensureCache();
709
777
  const totalLength = this.cachedLength;
710
778
  if (totalLength === 0 || this.commands.length === 0) {
711
- return this.commands.length > 0 ? this.commands[this.commands.length - 1].end : Vector2.ZERO;
779
+ return this.commands.length > 0 ? this.commands[this.commands.length - 1].end : Vector.ZERO;
712
780
  }
713
781
  t = Math.max(0, Math.min(1, t));
714
782
  const targetDistance = t * totalLength;
@@ -724,9 +792,9 @@ var BezierPath = class _BezierPath {
724
792
  }
725
793
  const segmentIndex = low;
726
794
  const cmd = this.commands[segmentIndex];
727
- if (!cmd) return Vector2.ZERO;
728
- let cursor = Vector2.ZERO;
729
- let subpathStart = Vector2.ZERO;
795
+ if (!cmd) return Vector.ZERO;
796
+ let cursor = Vector.ZERO;
797
+ let subpathStart = Vector.ZERO;
730
798
  for (let i = 0; i < segmentIndex; i++) {
731
799
  const c = this.commands[i];
732
800
  if (c.type === "Move") {
@@ -1233,13 +1301,13 @@ var FadeOut = class extends ExitAnimation {
1233
1301
  var MoveTo = class extends TransformativeAnimation {
1234
1302
  startPosition;
1235
1303
  endPosition;
1236
- constructor(target, xOrDestination, y) {
1304
+ constructor(target, xOrDestination, y, z) {
1237
1305
  super(target);
1238
- if (xOrDestination instanceof Vector2) {
1306
+ if (typeof xOrDestination !== "number") {
1239
1307
  this.endPosition = xOrDestination;
1240
- } else {
1241
- this.endPosition = new Vector2(xOrDestination, y ?? 0);
1308
+ return;
1242
1309
  }
1310
+ this.endPosition = new Vector(xOrDestination, y ?? 0, z ?? 0);
1243
1311
  }
1244
1312
  captureStartState() {
1245
1313
  this.startPosition = this.target.position;
@@ -1247,7 +1315,7 @@ var MoveTo = class extends TransformativeAnimation {
1247
1315
  interpolate(progress) {
1248
1316
  this.ensureInitialized();
1249
1317
  const newPosition = this.startPosition.lerp(this.endPosition, progress);
1250
- this.target.pos(newPosition.x, newPosition.y);
1318
+ this.target.pos(newPosition.x, newPosition.y, newPosition.z);
1251
1319
  }
1252
1320
  /** Returns the target position of the move animation. */
1253
1321
  getDestination() {
@@ -1287,7 +1355,7 @@ var Scale = class extends TransformativeAnimation {
1287
1355
  super(target);
1288
1356
  const endX = factorX;
1289
1357
  const endY = factorY ?? factorX;
1290
- this.endScale = new Vector2(endX, endY);
1358
+ this.endScale = new Vector(endX, endY);
1291
1359
  }
1292
1360
  captureStartState() {
1293
1361
  this.startScale = this.target.scale;
@@ -1349,8 +1417,8 @@ function getPartialPath(path, t) {
1349
1417
  const commands = path.getCommands();
1350
1418
  const result = new BezierPath();
1351
1419
  let accumulatedLength = 0;
1352
- let cursor = Vector2.ZERO;
1353
- let subpathStart = Vector2.ZERO;
1420
+ let cursor = Vector.ZERO;
1421
+ let subpathStart = Vector.ZERO;
1354
1422
  for (const cmd of commands) {
1355
1423
  const segmentLength = getSegmentLength(cmd, cursor, subpathStart);
1356
1424
  const newAccumulated = accumulatedLength + segmentLength;
@@ -1837,13 +1905,16 @@ function createFadeIn(target, durationSeconds) {
1837
1905
  function createFadeOut(target, durationSeconds) {
1838
1906
  return withDuration(new FadeOut(target), durationSeconds);
1839
1907
  }
1840
- function createMoveTo(target, x, y, durationSeconds) {
1841
- return withDuration(new MoveTo(target, x, y), durationSeconds);
1908
+ function createMoveTo(target, xOrDestination, yOrDuration, durationSeconds) {
1909
+ if (typeof xOrDestination !== "number") {
1910
+ return withDuration(new MoveTo(target, xOrDestination), yOrDuration);
1911
+ }
1912
+ return withDuration(new MoveTo(target, xOrDestination, yOrDuration ?? 0), durationSeconds);
1842
1913
  }
1843
1914
  function createRotate(target, angle, durationSeconds) {
1844
1915
  return withDuration(new Rotate(target, angle), durationSeconds);
1845
1916
  }
1846
- function createScale2(target, factorX, factorY = factorX, durationSeconds) {
1917
+ function createScale(target, factorX, factorY = factorX, durationSeconds) {
1847
1918
  return withDuration(new Scale(target, factorX, factorY), durationSeconds);
1848
1919
  }
1849
1920
  function createParallel(animations) {
@@ -1974,10 +2045,18 @@ var Mobject = class {
1974
2045
  localMatrix;
1975
2046
  opacityValue;
1976
2047
  animQueue = null;
2048
+ pointCloud = [];
2049
+ submobjects = [];
1977
2050
  savedStates = [];
2051
+ logicalRotation = 0;
2052
+ logicalScale = new Vector(1, 1);
2053
+ updaters = [];
2054
+ nextUpdaterId = 1;
2055
+ nextUpdaterOrder = 0;
2056
+ updatersEnabled = true;
1978
2057
  parent = null;
1979
2058
  constructor() {
1980
- this.localMatrix = Matrix3x3.identity();
2059
+ this.localMatrix = Matrix4x4.identity();
1981
2060
  this.opacityValue = 0;
1982
2061
  }
1983
2062
  getQueue() {
@@ -1991,34 +2070,78 @@ var Mobject = class {
1991
2070
  return this.localMatrix;
1992
2071
  }
1993
2072
  getWorldMatrix() {
2073
+ if (this.usesGeometryTransforms()) {
2074
+ return this.localMatrix;
2075
+ }
1994
2076
  if (this.parent === null) {
1995
2077
  return this.localMatrix;
1996
2078
  }
1997
2079
  return this.parent.getWorldMatrix().multiply(this.localMatrix);
1998
2080
  }
2081
+ /**
2082
+ * Matrix used by renderer.
2083
+ * Geometry-driven mobjects bake their own transform into points,
2084
+ * so only ancestor matrix transforms should be applied at draw time.
2085
+ */
2086
+ getRenderMatrix() {
2087
+ if (this.usesGeometryTransforms()) {
2088
+ if (this.parent === null) {
2089
+ return Matrix4x4.identity();
2090
+ }
2091
+ return this.parent.getRenderMatrix();
2092
+ }
2093
+ if (this.parent === null) {
2094
+ return this.localMatrix;
2095
+ }
2096
+ return this.parent.getRenderMatrix().multiply(this.localMatrix);
2097
+ }
1999
2098
  get position() {
2099
+ if (this.usesGeometryTransforms()) {
2100
+ const center = this.getGeometryCenter();
2101
+ if (center) {
2102
+ return center;
2103
+ }
2104
+ }
2000
2105
  const m = this.localMatrix.values;
2001
- return new Vector2(m[2], m[5]);
2106
+ return new Vector(m[3], m[7], m[11]);
2002
2107
  }
2003
2108
  get rotation() {
2109
+ if (this.usesGeometryTransforms()) {
2110
+ return this.logicalRotation;
2111
+ }
2004
2112
  const m = this.localMatrix.values;
2005
- return Math.atan2(m[3], m[0]);
2113
+ return Math.atan2(m[4], m[0]);
2006
2114
  }
2007
2115
  get scale() {
2116
+ if (this.usesGeometryTransforms()) {
2117
+ return this.logicalScale;
2118
+ }
2008
2119
  const m = this.localMatrix.values;
2009
- const sx = Math.sqrt(m[0] * m[0] + m[3] * m[3]);
2010
- const sy = Math.sqrt(m[1] * m[1] + m[4] * m[4]);
2011
- return new Vector2(sx, sy);
2120
+ const sx = Math.sqrt(m[0] * m[0] + m[4] * m[4]);
2121
+ const sy = Math.sqrt(m[1] * m[1] + m[5] * m[5]);
2122
+ return new Vector(sx, sy);
2012
2123
  }
2013
2124
  get opacity() {
2014
2125
  return this.opacityValue;
2015
2126
  }
2016
2127
  // ========== Immediate State Setters ==========
2017
- pos(x, y) {
2128
+ pos(x, y, z) {
2129
+ const targetZ = z ?? this.position.z;
2130
+ if (this.usesGeometryTransforms()) {
2131
+ const current = this.position;
2132
+ const dx = x - current.x;
2133
+ const dy = y - current.y;
2134
+ const dz = targetZ - current.z;
2135
+ if (Math.abs(dx) > 1e-12 || Math.abs(dy) > 1e-12 || Math.abs(dz) > 1e-12) {
2136
+ this.applyMatrix(Matrix4x4.translation(dx, dy, dz));
2137
+ }
2138
+ return this;
2139
+ }
2018
2140
  const newValues = new Float32Array(this.localMatrix.values);
2019
- newValues[2] = x;
2020
- newValues[5] = y;
2021
- this.localMatrix = new Matrix3x3(newValues);
2141
+ newValues[3] = x;
2142
+ newValues[7] = y;
2143
+ newValues[11] = targetZ;
2144
+ this.setLocalMatrix(new Matrix4x4(newValues));
2022
2145
  return this;
2023
2146
  }
2024
2147
  show() {
@@ -2034,52 +2157,227 @@ var Mobject = class {
2034
2157
  return this;
2035
2158
  }
2036
2159
  setRotation(angle) {
2160
+ if (this.usesGeometryTransforms()) {
2161
+ const delta = angle - this.logicalRotation;
2162
+ if (Math.abs(delta) < 1e-12) {
2163
+ return this;
2164
+ }
2165
+ const pivot = this.position;
2166
+ const transform = Matrix4x4.translation(pivot.x, pivot.y, pivot.z).multiply(Matrix4x4.rotationZ(delta)).multiply(Matrix4x4.translation(-pivot.x, -pivot.y, -pivot.z));
2167
+ this.applyMatrix(transform);
2168
+ this.logicalRotation = angle;
2169
+ this.syncLocalMatrixFromGeometry();
2170
+ return this;
2171
+ }
2037
2172
  const m = this.localMatrix.values;
2038
- const posX = m[2];
2039
- const posY = m[5];
2173
+ const posX = m[3];
2174
+ const posY = m[7];
2175
+ const posZ = m[11];
2040
2176
  const currentScale = this.scale;
2041
2177
  const cos = Math.cos(angle);
2042
2178
  const sin = Math.sin(angle);
2043
- const newValues = new Float32Array(9);
2179
+ const newValues = new Float32Array(16);
2044
2180
  newValues[0] = cos * currentScale.x;
2045
2181
  newValues[1] = -sin * currentScale.y;
2046
- newValues[2] = posX;
2047
- newValues[3] = sin * currentScale.x;
2048
- newValues[4] = cos * currentScale.y;
2049
- newValues[5] = posY;
2050
- newValues[8] = 1;
2051
- this.localMatrix = new Matrix3x3(newValues);
2182
+ newValues[3] = posX;
2183
+ newValues[4] = sin * currentScale.x;
2184
+ newValues[5] = cos * currentScale.y;
2185
+ newValues[7] = posY;
2186
+ newValues[10] = 1;
2187
+ newValues[11] = posZ;
2188
+ newValues[15] = 1;
2189
+ this.setLocalMatrix(new Matrix4x4(newValues));
2052
2190
  return this;
2053
2191
  }
2054
2192
  setScale(sx, sy) {
2193
+ if (this.usesGeometryTransforms()) {
2194
+ const current = this.logicalScale;
2195
+ const deltaX = current.x === 0 ? sx : sx / current.x;
2196
+ const deltaY = current.y === 0 ? sy : sy / current.y;
2197
+ if (Math.abs(deltaX - 1) < 1e-12 && Math.abs(deltaY - 1) < 1e-12) {
2198
+ return this;
2199
+ }
2200
+ const pivot = this.position;
2201
+ const transform = Matrix4x4.translation(pivot.x, pivot.y, pivot.z).multiply(Matrix4x4.scale(deltaX, deltaY, 1)).multiply(Matrix4x4.translation(-pivot.x, -pivot.y, -pivot.z));
2202
+ this.applyMatrix(transform);
2203
+ this.logicalScale = new Vector(sx, sy);
2204
+ this.syncLocalMatrixFromGeometry();
2205
+ return this;
2206
+ }
2055
2207
  const m = this.localMatrix.values;
2056
- const posX = m[2];
2057
- const posY = m[5];
2208
+ const posX = m[3];
2209
+ const posY = m[7];
2210
+ const posZ = m[11];
2058
2211
  const currentRotation = this.rotation;
2059
2212
  const cos = Math.cos(currentRotation);
2060
2213
  const sin = Math.sin(currentRotation);
2061
- const newValues = new Float32Array(9);
2214
+ const newValues = new Float32Array(16);
2062
2215
  newValues[0] = cos * sx;
2063
2216
  newValues[1] = -sin * sy;
2064
- newValues[2] = posX;
2065
- newValues[3] = sin * sx;
2066
- newValues[4] = cos * sy;
2067
- newValues[5] = posY;
2068
- newValues[8] = 1;
2069
- this.localMatrix = new Matrix3x3(newValues);
2217
+ newValues[3] = posX;
2218
+ newValues[4] = sin * sx;
2219
+ newValues[5] = cos * sy;
2220
+ newValues[7] = posY;
2221
+ newValues[10] = 1;
2222
+ newValues[11] = posZ;
2223
+ newValues[15] = 1;
2224
+ this.setLocalMatrix(new Matrix4x4(newValues));
2070
2225
  return this;
2071
2226
  }
2072
2227
  applyMatrix(m) {
2073
- this.localMatrix = m.multiply(this.localMatrix);
2228
+ if (this.usesGeometryTransforms()) {
2229
+ this.applyMatrixToOwnGeometry(m);
2230
+ for (const child of this.submobjects) {
2231
+ child.applyMatrix(m);
2232
+ }
2233
+ this.updateLogicalStateFromMatrix(m);
2234
+ this.syncLocalMatrixFromGeometry();
2235
+ return this;
2236
+ }
2237
+ this.setLocalMatrix(m.multiply(this.localMatrix));
2238
+ return this;
2239
+ }
2240
+ // ========== Scene Graph / Geometry Primitives ==========
2241
+ addSubmobjects(...mobjects) {
2242
+ for (const mob of mobjects) {
2243
+ if (mob === this) continue;
2244
+ if (this.submobjects.includes(mob)) continue;
2245
+ if (mob.parent) {
2246
+ mob.parent.removeSubmobject(mob);
2247
+ }
2248
+ mob.parent = this;
2249
+ this.submobjects.push(mob);
2250
+ }
2251
+ this.syncLocalMatrixFromGeometry();
2252
+ return this;
2253
+ }
2254
+ removeSubmobject(mobject) {
2255
+ const index = this.submobjects.indexOf(mobject);
2256
+ if (index >= 0) {
2257
+ this.submobjects.splice(index, 1);
2258
+ mobject.parent = null;
2259
+ }
2260
+ this.syncLocalMatrixFromGeometry();
2261
+ return this;
2262
+ }
2263
+ clearSubmobjects() {
2264
+ for (const child of this.submobjects) {
2265
+ child.parent = null;
2266
+ }
2267
+ this.submobjects = [];
2268
+ this.syncLocalMatrixFromGeometry();
2269
+ return this;
2270
+ }
2271
+ getSubmobjects() {
2272
+ return [...this.submobjects];
2273
+ }
2274
+ // ========== Updaters ==========
2275
+ addUpdater(fn, options = {}) {
2276
+ if (typeof fn !== "function") {
2277
+ throw new Error("addUpdater() requires a function");
2278
+ }
2279
+ const priority = options.priority ?? 0;
2280
+ if (!Number.isFinite(priority)) {
2281
+ throw new Error("Updater priority must be a finite number");
2282
+ }
2283
+ const record = {
2284
+ id: this.nextUpdaterId++,
2285
+ order: this.nextUpdaterOrder++,
2286
+ name: options.name,
2287
+ fn,
2288
+ priority,
2289
+ enabled: options.enabled ?? true
2290
+ };
2291
+ this.updaters.push(record);
2292
+ return { id: record.id };
2293
+ }
2294
+ removeUpdater(handleOrFn) {
2295
+ if (typeof handleOrFn === "function") {
2296
+ const fn = handleOrFn;
2297
+ this.updaters = this.updaters.filter((u) => u.fn !== fn);
2298
+ return this;
2299
+ }
2300
+ this.updaters = this.updaters.filter((u) => u.id !== handleOrFn.id);
2301
+ return this;
2302
+ }
2303
+ clearUpdaters() {
2304
+ this.updaters = [];
2305
+ return this;
2306
+ }
2307
+ suspendUpdaters() {
2308
+ this.updatersEnabled = false;
2309
+ return this;
2310
+ }
2311
+ resumeUpdaters() {
2312
+ this.updatersEnabled = true;
2313
+ return this;
2314
+ }
2315
+ enableUpdater(handleOrFn) {
2316
+ this.setUpdaterEnabled(handleOrFn, true);
2317
+ return this;
2318
+ }
2319
+ disableUpdater(handleOrFn) {
2320
+ this.setUpdaterEnabled(handleOrFn, false);
2074
2321
  return this;
2075
2322
  }
2323
+ hasActiveUpdaters(recursive = false) {
2324
+ if (this.updatersEnabled && this.updaters.some((u) => u.enabled)) {
2325
+ return true;
2326
+ }
2327
+ if (!recursive) {
2328
+ return false;
2329
+ }
2330
+ return this.submobjects.some((child) => child.hasActiveUpdaters(true));
2331
+ }
2332
+ /**
2333
+ * Internal API used by UpdaterEngine.
2334
+ * Returns a deterministic snapshot for current-frame execution.
2335
+ */
2336
+ getUpdaterRecordsSnapshot() {
2337
+ if (!this.updatersEnabled) {
2338
+ return [];
2339
+ }
2340
+ const active = this.updaters.filter((u) => u.enabled);
2341
+ active.sort((a, b) => {
2342
+ if (a.priority !== b.priority) {
2343
+ return b.priority - a.priority;
2344
+ }
2345
+ return a.order - b.order;
2346
+ });
2347
+ return active.map((u) => ({ ...u }));
2348
+ }
2349
+ setUpdaterEnabled(handleOrFn, enabled) {
2350
+ if (typeof handleOrFn === "function") {
2351
+ const fn = handleOrFn;
2352
+ for (const updater of this.updaters) {
2353
+ if (updater.fn === fn) {
2354
+ updater.enabled = enabled;
2355
+ }
2356
+ }
2357
+ return;
2358
+ }
2359
+ for (const updater of this.updaters) {
2360
+ if (updater.id === handleOrFn.id) {
2361
+ updater.enabled = enabled;
2362
+ }
2363
+ }
2364
+ }
2365
+ setPointCloud(points) {
2366
+ this.pointCloud = points.map(
2367
+ (p) => p instanceof Vector ? new Vector(p.x, p.y, p.z) : Vector.fromPlanar(p, 0)
2368
+ );
2369
+ this.syncLocalMatrixFromGeometry();
2370
+ }
2371
+ getPointCloud() {
2372
+ return this.pointCloud.map((p) => new Vector(p.x, p.y, p.z));
2373
+ }
2076
2374
  // ========== State Save/Restore ==========
2077
2375
  saveState() {
2078
2376
  const pos = this.position;
2079
2377
  const scl = this.scale;
2080
2378
  this.savedStates.push({
2081
- position: new Vector2(pos.x, pos.y),
2082
- scale: new Vector2(scl.x, scl.y),
2379
+ position: new Vector(pos.x, pos.y, pos.z),
2380
+ scale: new Vector(scl.x, scl.y),
2083
2381
  rotation: this.rotation
2084
2382
  });
2085
2383
  return this;
@@ -2101,8 +2399,8 @@ var Mobject = class {
2101
2399
  if (!state) {
2102
2400
  throw new Error("restore() called but no state was saved. Call saveState() first.");
2103
2401
  }
2104
- const moveAnim = createMoveTo(this, state.position.x, state.position.y, durationSeconds);
2105
- const scaleAnim = createScale2(this, state.scale.x, state.scale.y, durationSeconds);
2402
+ const moveAnim = createMoveTo(this, state.position, durationSeconds);
2403
+ const scaleAnim = createScale(this, state.scale.x, state.scale.y, durationSeconds);
2106
2404
  const rotateAnim = createRotate(this, state.rotation - this.rotation, durationSeconds);
2107
2405
  const parallelAnim = createParallel([moveAnim, scaleAnim, rotateAnim]);
2108
2406
  this.getQueue().enqueueAnimation(parallelAnim);
@@ -2121,8 +2419,20 @@ var Mobject = class {
2121
2419
  this.getQueue().enqueueAnimation(animation);
2122
2420
  return this;
2123
2421
  }
2124
- moveTo(x, y, durationSeconds) {
2125
- const animation = createMoveTo(this, x, y, durationSeconds);
2422
+ moveTo(xOrDestination, yOrDuration, zOrDuration, durationSeconds) {
2423
+ let animation;
2424
+ if (typeof xOrDestination !== "number") {
2425
+ animation = createMoveTo(this, xOrDestination, yOrDuration);
2426
+ } else {
2427
+ const x = xOrDestination;
2428
+ const y = yOrDuration ?? 0;
2429
+ if (durationSeconds !== void 0) {
2430
+ const z = zOrDuration ?? this.position.z;
2431
+ animation = createMoveTo(this, new Vector(x, y, z), durationSeconds);
2432
+ } else {
2433
+ animation = createMoveTo(this, x, y, zOrDuration);
2434
+ }
2435
+ }
2126
2436
  this.getQueue().enqueueAnimation(animation);
2127
2437
  return this;
2128
2438
  }
@@ -2132,12 +2442,12 @@ var Mobject = class {
2132
2442
  return this;
2133
2443
  }
2134
2444
  scaleTo(factor, durationSeconds) {
2135
- const animation = createScale2(this, factor, factor, durationSeconds);
2445
+ const animation = createScale(this, factor, factor, durationSeconds);
2136
2446
  this.getQueue().enqueueAnimation(animation);
2137
2447
  return this;
2138
2448
  }
2139
2449
  scaleToXY(factorX, factorY, durationSeconds) {
2140
- const animation = createScale2(this, factorX, factorY, durationSeconds);
2450
+ const animation = createScale(this, factorX, factorY, durationSeconds);
2141
2451
  this.getQueue().enqueueAnimation(animation);
2142
2452
  return this;
2143
2453
  }
@@ -2194,223 +2504,561 @@ var Mobject = class {
2194
2504
  * Used by the segment cache to detect changes.
2195
2505
  */
2196
2506
  computeHash() {
2507
+ const pointData = new Float32Array(this.pointCloud.length * 3);
2508
+ for (let i = 0; i < this.pointCloud.length; i++) {
2509
+ const p = this.pointCloud[i];
2510
+ pointData[i * 3] = p.x;
2511
+ pointData[i * 3 + 1] = p.y;
2512
+ pointData[i * 3 + 2] = p.z;
2513
+ }
2514
+ const childHashes = this.submobjects.map((child) => child.computeHash());
2197
2515
  return hashCompose(
2198
2516
  hashFloat32Array(this.localMatrix.values),
2199
- hashNumber(this.opacityValue)
2517
+ hashNumber(this.opacityValue),
2518
+ hashFloat32Array(pointData),
2519
+ ...childHashes
2200
2520
  );
2201
2521
  }
2202
- };
2203
-
2204
- // src/core/mobjects/VMobject.ts
2205
- var VMobject = class extends Mobject {
2206
- pathList = [];
2207
- /** Stroke color. Only rendered if strokeWidth > 0. */
2208
- strokeColor = Color.WHITE;
2209
- /** Stroke width. Default 2 for visibility. */
2210
- strokeWidth = 2;
2211
- /** Fill color. Only rendered if fillOpacity > 0. */
2212
- fillColor = Color.TRANSPARENT;
2213
- /** Fill opacity. Default 0 means no fill is rendered. */
2214
- fillOpacity = 0;
2215
- /** Tracks whether stroke() was explicitly called. */
2216
- strokeExplicitlySet = false;
2217
- constructor() {
2218
- super();
2522
+ applyMatrixToOwnGeometry(m) {
2523
+ if (this.pointCloud.length === 0) return;
2524
+ this.pointCloud = this.pointCloud.map((point) => m.transformPoint(point));
2219
2525
  }
2220
- get paths() {
2221
- return this.pathList;
2526
+ usesGeometryTransforms() {
2527
+ return this.pointCloud.length > 0 || this.submobjects.length > 0;
2222
2528
  }
2223
- set paths(value) {
2224
- this.pathList = value;
2529
+ collectGeometryPoints(out) {
2530
+ out.push(...this.pointCloud);
2531
+ for (const child of this.submobjects) {
2532
+ child.collectGeometryPoints(out);
2533
+ }
2225
2534
  }
2226
- /**
2227
- * Gets the stroke color.
2228
- */
2229
- getStrokeColor() {
2230
- return this.strokeColor;
2535
+ getGeometryCenter() {
2536
+ const points = [];
2537
+ this.collectGeometryPoints(points);
2538
+ if (points.length === 0) {
2539
+ return null;
2540
+ }
2541
+ let minX = Infinity;
2542
+ let minY = Infinity;
2543
+ let maxX = -Infinity;
2544
+ let maxY = -Infinity;
2545
+ let minZ = Infinity;
2546
+ let maxZ = -Infinity;
2547
+ for (const p of points) {
2548
+ if (p.x < minX) minX = p.x;
2549
+ if (p.x > maxX) maxX = p.x;
2550
+ if (p.y < minY) minY = p.y;
2551
+ if (p.y > maxY) maxY = p.y;
2552
+ if (p.z < minZ) minZ = p.z;
2553
+ if (p.z > maxZ) maxZ = p.z;
2554
+ }
2555
+ return new Vector((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2);
2556
+ }
2557
+ syncLocalMatrixFromGeometry() {
2558
+ if (!this.usesGeometryTransforms()) {
2559
+ return;
2560
+ }
2561
+ const center = this.getGeometryCenter() ?? Vector.ZERO;
2562
+ const sx = this.logicalScale.x;
2563
+ const sy = this.logicalScale.y;
2564
+ const cos = Math.cos(this.logicalRotation);
2565
+ const sin = Math.sin(this.logicalRotation);
2566
+ const values = new Float32Array(16);
2567
+ values[0] = cos * sx;
2568
+ values[1] = -sin * sy;
2569
+ values[3] = center.x;
2570
+ values[4] = sin * sx;
2571
+ values[5] = cos * sy;
2572
+ values[7] = center.y;
2573
+ values[10] = 1;
2574
+ values[11] = center.z;
2575
+ values[15] = 1;
2576
+ this.setLocalMatrix(new Matrix4x4(values));
2577
+ }
2578
+ updateLogicalStateFromMatrix(m) {
2579
+ const values = m.values;
2580
+ const sx = Math.sqrt(values[0] * values[0] + values[4] * values[4]);
2581
+ const sy = Math.sqrt(values[1] * values[1] + values[5] * values[5]);
2582
+ const rot = Math.atan2(values[4], values[0]);
2583
+ this.logicalScale = new Vector(this.logicalScale.x * sx, this.logicalScale.y * sy);
2584
+ this.logicalRotation += rot;
2585
+ }
2586
+ setLocalMatrix(m4) {
2587
+ this.localMatrix = m4;
2231
2588
  }
2589
+ };
2590
+
2591
+ // src/core/animations/composition/Sequence.ts
2592
+ var Sequence = class extends Animation {
2593
+ children;
2594
+ childDurations;
2595
+ totalChildDuration;
2232
2596
  /**
2233
- * Gets the stroke width.
2597
+ * The lifecycle of Sequence is determined by its FIRST child animation.
2598
+ * If the first animation is introductory, it will register the target,
2599
+ * allowing subsequent transformative animations to work.
2234
2600
  */
2235
- getStrokeWidth() {
2236
- return this.strokeWidth;
2601
+ lifecycle;
2602
+ constructor(animations) {
2603
+ super(new Mobject());
2604
+ this.children = animations;
2605
+ this.childDurations = animations.map((a) => a.getDuration());
2606
+ this.totalChildDuration = this.childDurations.reduce((sum, d) => sum + d, 0);
2607
+ this.durationSeconds = this.totalChildDuration;
2608
+ const first = animations[0];
2609
+ this.lifecycle = first?.lifecycle === "introductory" ? "introductory" : "transformative";
2237
2610
  }
2238
- /**
2239
- * Gets the fill color.
2240
- */
2241
- getFillColor() {
2242
- return this.fillColor;
2611
+ getDuration() {
2612
+ return this.durationSeconds;
2243
2613
  }
2244
- /**
2245
- * Gets the fill opacity.
2246
- */
2247
- getFillOpacity() {
2248
- return this.fillOpacity;
2614
+ getChildren() {
2615
+ return this.children;
2249
2616
  }
2250
2617
  /**
2251
- * Adds a new path to the VMobject.
2252
- * @param path - The BezierPath to add.
2253
- * @returns this for chaining.
2618
+ * Initializes only the first child.
2619
+ * Later children are initialized when they become active in interpolate().
2254
2620
  */
2255
- addPath(path) {
2256
- this.pathList.push(path);
2257
- return this;
2621
+ ensureInitialized() {
2622
+ if (this.children.length > 0) {
2623
+ this.children[0].ensureInitialized();
2624
+ }
2258
2625
  }
2259
- /**
2260
- * Sets the stroke color and width.
2261
- * @param color - The stroke color.
2262
- * @param width - The stroke width. Default is 2.
2263
- * @returns this for chaining.
2264
- */
2265
- stroke(color, width = 2) {
2266
- this.strokeColor = color;
2267
- this.strokeWidth = width;
2268
- this.strokeExplicitlySet = true;
2269
- return this;
2626
+ reset() {
2627
+ for (const child of this.children) {
2628
+ child.reset();
2629
+ }
2270
2630
  }
2271
2631
  /**
2272
- * Sets the fill color and opacity.
2273
- * If stroke was not explicitly set, the default stroke is disabled.
2274
- * @param color - The fill color.
2275
- * @param opacity - The fill opacity. Defaults to the color's alpha value.
2276
- * @returns this for chaining.
2632
+ * Interpolates the sequence at the given progress.
2633
+ * Maps global progress to the correct child animation.
2634
+ *
2635
+ * IMPORTANT: We only update children that have started or completed.
2636
+ * Children that haven't started yet are NOT updated to avoid
2637
+ * premature initialization with incorrect state.
2277
2638
  */
2278
- fill(color, opacity) {
2279
- this.fillColor = color;
2280
- this.fillOpacity = opacity ?? color.a;
2281
- if (!this.strokeExplicitlySet) {
2282
- this.strokeWidth = 0;
2639
+ interpolate(progress) {
2640
+ if (this.children.length === 0 || this.totalChildDuration === 0) {
2641
+ return;
2642
+ }
2643
+ const globalTime = progress * this.totalChildDuration;
2644
+ let accumulatedTime = 0;
2645
+ for (let i = 0; i < this.children.length; i++) {
2646
+ const child = this.children[i];
2647
+ const childDuration = this.childDurations[i];
2648
+ if (child === void 0 || childDuration === void 0) {
2649
+ continue;
2650
+ }
2651
+ const childStart = accumulatedTime;
2652
+ const childEnd = accumulatedTime + childDuration;
2653
+ if (globalTime < childStart) {
2654
+ } else if (globalTime >= childEnd) {
2655
+ child.update(1);
2656
+ } else {
2657
+ const localProgress = (globalTime - childStart) / childDuration;
2658
+ child.update(localProgress);
2659
+ }
2660
+ accumulatedTime = childEnd;
2283
2661
  }
2284
- return this;
2285
2662
  }
2286
- getPoints() {
2287
- const commands = [];
2288
- for (const path of this.pathList) {
2289
- commands.push(...path.getCommands());
2663
+ update(progress) {
2664
+ const clampedProgress = Math.max(0, Math.min(1, progress));
2665
+ this.interpolate(clampedProgress);
2666
+ }
2667
+ };
2668
+
2669
+ // src/core/animations/keyframes/types.ts
2670
+ function lerpNumber(a, b, t) {
2671
+ return a + (b - a) * t;
2672
+ }
2673
+
2674
+ // src/core/animations/keyframes/KeyframeTrack.ts
2675
+ var KeyframeTrack = class {
2676
+ keyframes = [];
2677
+ interpolator;
2678
+ /**
2679
+ * Creates a new KeyframeTrack with the specified interpolator.
2680
+ * Defaults to linear number interpolation.
2681
+ */
2682
+ constructor(interpolator) {
2683
+ this.interpolator = interpolator ?? lerpNumber;
2684
+ }
2685
+ /**
2686
+ * Adds a keyframe at the specified normalized time.
2687
+ * Time must be in [0, 1]. Replaces existing keyframe at same time.
2688
+ */
2689
+ addKeyframe(time, value, easing) {
2690
+ if (time < 0 || time > 1) {
2691
+ throw new Error("Keyframe time must be in [0, 1]");
2290
2692
  }
2291
- return commands;
2693
+ this.keyframes = this.keyframes.filter((kf) => kf.time !== time);
2694
+ const keyframe = { time, value, easing };
2695
+ this.keyframes.push(keyframe);
2696
+ this.keyframes.sort((a, b) => a.time - b.time);
2697
+ return this;
2292
2698
  }
2293
- setPoints(commands) {
2294
- if (commands.length === 0) {
2295
- this.pathList = [];
2296
- return this;
2699
+ removeKeyframe(time) {
2700
+ const initialLength = this.keyframes.length;
2701
+ this.keyframes = this.keyframes.filter((kf) => kf.time !== time);
2702
+ return this.keyframes.length < initialLength;
2703
+ }
2704
+ /**
2705
+ * Gets the keyframe at the specified time.
2706
+ * Returns undefined if no keyframe exists at that time.
2707
+ */
2708
+ getKeyframe(time) {
2709
+ return this.keyframes.find((kf) => kf.time === time);
2710
+ }
2711
+ /** All keyframes sorted by time. */
2712
+ getKeyframes() {
2713
+ return this.keyframes;
2714
+ }
2715
+ /** Interpolated value at normalized time (uses target keyframe easing). */
2716
+ getValueAt(time) {
2717
+ if (this.keyframes.length === 0) {
2718
+ throw new Error("KeyframeTrack has no keyframes");
2297
2719
  }
2298
- const path = new BezierPath();
2299
- for (const cmd of commands) {
2300
- switch (cmd.type) {
2301
- case "Move":
2302
- path.moveTo(cmd.end);
2303
- break;
2304
- case "Line":
2305
- path.lineTo(cmd.end);
2306
- break;
2307
- case "Quadratic":
2308
- if (cmd.control1) {
2309
- path.quadraticTo(cmd.control1, cmd.end);
2310
- }
2311
- break;
2312
- case "Cubic":
2313
- if (cmd.control1 && cmd.control2) {
2314
- path.cubicTo(cmd.control1, cmd.control2, cmd.end);
2315
- }
2316
- break;
2317
- case "Close":
2318
- path.closePath();
2319
- break;
2720
+ const clampedTime = Math.max(0, Math.min(1, time));
2721
+ let prevKeyframe;
2722
+ let nextKeyframe;
2723
+ for (const kf of this.keyframes) {
2724
+ if (kf.time <= clampedTime) {
2725
+ prevKeyframe = kf;
2726
+ }
2727
+ if (kf.time >= clampedTime && nextKeyframe === void 0) {
2728
+ nextKeyframe = kf;
2320
2729
  }
2321
2730
  }
2322
- this.pathList = [path];
2323
- return this;
2324
- }
2325
- getPointsAsVectors() {
2326
- const points = [];
2327
- for (const cmd of this.getPoints()) {
2328
- if (cmd.control1) points.push(cmd.control1);
2329
- if (cmd.control2) points.push(cmd.control2);
2330
- points.push(cmd.end);
2731
+ if (prevKeyframe === void 0) {
2732
+ return this.keyframes[0].value;
2331
2733
  }
2332
- return points;
2333
- }
2334
- getBoundingBox() {
2335
- const points = this.getPointsAsVectors();
2336
- const worldMatrix = this.getWorldMatrix();
2337
- if (points.length === 0) {
2338
- const pos = this.position;
2339
- return { minX: pos.x, maxX: pos.x, minY: pos.y, maxY: pos.y };
2734
+ if (nextKeyframe === void 0) {
2735
+ return prevKeyframe.value;
2340
2736
  }
2341
- let minX = Infinity;
2342
- let maxX = -Infinity;
2343
- let minY = Infinity;
2344
- let maxY = -Infinity;
2345
- for (const point of points) {
2346
- const transformed = worldMatrix.transformPoint(point);
2347
- if (transformed.x < minX) minX = transformed.x;
2348
- if (transformed.x > maxX) maxX = transformed.x;
2349
- if (transformed.y < minY) minY = transformed.y;
2350
- if (transformed.y > maxY) maxY = transformed.y;
2737
+ if (prevKeyframe === nextKeyframe) {
2738
+ return prevKeyframe.value;
2351
2739
  }
2352
- return { minX, maxX, minY, maxY };
2740
+ const span = nextKeyframe.time - prevKeyframe.time;
2741
+ const localProgress = (clampedTime - prevKeyframe.time) / span;
2742
+ const easing = nextKeyframe.easing ?? linear;
2743
+ const easedProgress = easing(localProgress);
2744
+ return this.interpolator(prevKeyframe.value, nextKeyframe.value, easedProgress);
2353
2745
  }
2354
- // ========== VMobject-Specific Fluent Animation API ==========
2746
+ getKeyframeCount() {
2747
+ return this.keyframes.length;
2748
+ }
2749
+ };
2750
+
2751
+ // src/core/animations/keyframes/KeyframeAnimation.ts
2752
+ var KeyframeAnimation = class extends Animation {
2753
+ tracks = /* @__PURE__ */ new Map();
2754
+ lifecycle = "transformative";
2355
2755
  /**
2356
- * Progressively draws the VMobject's paths from start to end.
2357
- * Preserves fill throughout the animation.
2358
- * @param durationSeconds - Animation duration in seconds.
2359
- * @returns this for chaining.
2756
+ * Adds a named keyframe track with its property setter.
2757
+ * The setter is called during interpolation to apply values to the target.
2360
2758
  */
2361
- write(durationSeconds) {
2362
- const animation = createWrite(this, durationSeconds);
2363
- this.getQueue().enqueueAnimation(animation);
2759
+ addTrack(name, track, setter) {
2760
+ this.tracks.set(name, {
2761
+ track,
2762
+ setter
2763
+ });
2364
2764
  return this;
2365
2765
  }
2766
+ /** Gets a track by name. */
2767
+ getTrack(name) {
2768
+ const entry = this.tracks.get(name);
2769
+ return entry?.track;
2770
+ }
2771
+ /** All track names. */
2772
+ getTrackNames() {
2773
+ return Array.from(this.tracks.keys());
2774
+ }
2366
2775
  /**
2367
- * Progressively removes the VMobject's paths (reverse of write).
2368
- * @param durationSeconds - Animation duration in seconds.
2369
- * @returns this for chaining.
2776
+ * Interpolates all tracks at the given progress and applies values to target.
2370
2777
  */
2371
- unwrite(durationSeconds) {
2372
- const animation = createUnwrite(this, durationSeconds);
2373
- this.getQueue().enqueueAnimation(animation);
2374
- return this;
2778
+ interpolate(progress) {
2779
+ for (const entry of this.tracks.values()) {
2780
+ const value = entry.track.getValueAt(progress);
2781
+ entry.setter(this.target, value);
2782
+ }
2375
2783
  }
2784
+ };
2785
+
2786
+ // src/core/animations/camera/Follow.ts
2787
+ var Follow = class _Follow extends Animation {
2788
+ lifecycle = "transformative";
2789
+ followTarget;
2790
+ offset;
2791
+ damping;
2376
2792
  /**
2377
- * First draws the stroke progressively (0-50%), then fades in the fill (50-100%).
2378
- * @param durationSeconds - Animation duration in seconds.
2379
- * @returns this for chaining.
2793
+ * Creates a new Follow animation.
2794
+ *
2795
+ * @param frame - The CameraFrame to animate
2796
+ * @param target - The Mobject to follow
2797
+ * @param config - Configuration options
2798
+ * @throws Error if frame is null or undefined
2799
+ * @throws Error if target is null or undefined
2800
+ *
2801
+ * @example
2802
+ * const follow = new Follow(scene.frame, player, { damping: 0.7 });
2803
+ * this.play(follow.duration(10));
2380
2804
  */
2381
- draw(durationSeconds) {
2382
- const animation = createDraw(this, durationSeconds);
2383
- this.getQueue().enqueueAnimation(animation);
2384
- return this;
2805
+ constructor(frame, target, config = {}) {
2806
+ if (!frame) {
2807
+ throw new Error("Follow animation requires a CameraFrame");
2808
+ }
2809
+ if (!target) {
2810
+ throw new Error("Follow animation requires a target Mobject");
2811
+ }
2812
+ super(frame);
2813
+ this.followTarget = target;
2814
+ this.offset = _Follow.normalizeOffset(config.offset);
2815
+ this.damping = config.damping ?? 0;
2385
2816
  }
2386
2817
  /**
2387
- * Extends parent hash with VMobject-specific state:
2388
- * stroke/fill colors, widths, opacity, and path geometry.
2818
+ * Updates the camera position each frame to track the target.
2819
+ * @param progress - Animation progress (0 to 1)
2389
2820
  */
2390
- computeHash() {
2391
- const parentHash = super.computeHash();
2392
- const pathHash = hashString(
2393
- this.pathList.map(
2394
- (p) => p.getCommands().map(
2395
- (c) => `${c.type}:${c.end.x},${c.end.y}` + (c.control1 ? `:${c.control1.x},${c.control1.y}` : "") + (c.control2 ? `:${c.control2.x},${c.control2.y}` : "")
2396
- ).join("|")
2397
- ).join("||")
2398
- );
2399
- return hashCompose(
2400
- parentHash,
2401
- hashNumber(this.strokeColor.r),
2402
- hashNumber(this.strokeColor.g),
2403
- hashNumber(this.strokeColor.b),
2404
- hashNumber(this.strokeColor.a),
2405
- hashNumber(this.strokeWidth),
2406
- hashNumber(this.fillColor.r),
2407
- hashNumber(this.fillColor.g),
2408
- hashNumber(this.fillColor.b),
2409
- hashNumber(this.fillColor.a),
2410
- hashNumber(this.fillOpacity),
2411
- pathHash
2821
+ interpolate(progress) {
2822
+ const targetPos = this.followTarget.position.add(this.offset);
2823
+ const currentPos = this.target.position;
2824
+ if (this.damping > 0 && this.damping < 1) {
2825
+ const lerpFactor = 1 - this.damping;
2826
+ const newPos = currentPos.lerp(targetPos, lerpFactor);
2827
+ this.target.pos(newPos.x, newPos.y);
2828
+ } else {
2829
+ this.target.pos(targetPos.x, targetPos.y);
2830
+ }
2831
+ }
2832
+ static normalizeOffset(offset) {
2833
+ if (!offset) {
2834
+ return Vector.ZERO;
2835
+ }
2836
+ if (offset instanceof Vector) {
2837
+ return offset;
2838
+ }
2839
+ if (offset.length === 2) {
2840
+ return new Vector(offset[0], offset[1], 0);
2841
+ }
2842
+ return new Vector(offset[0], offset[1], offset[2]);
2843
+ }
2844
+ };
2845
+
2846
+ // src/core/animations/camera/Shake.ts
2847
+ var Shake = class extends TransformativeAnimation {
2848
+ originalPosition;
2849
+ intensity;
2850
+ frequency;
2851
+ decay;
2852
+ seedX;
2853
+ seedY;
2854
+ /**
2855
+ * Creates a new Shake animation.
2856
+ *
2857
+ * @param frame - The CameraFrame to shake
2858
+ * @param config - Configuration options for intensity, frequency, and decay
2859
+ *
2860
+ * @example
2861
+ * const shake = new Shake(scene.frame, { intensity: 0.3 });
2862
+ * this.play(shake.duration(0.5));
2863
+ */
2864
+ constructor(frame, config = {}) {
2865
+ super(frame);
2866
+ this.intensity = config.intensity ?? 0.2;
2867
+ this.frequency = config.frequency ?? 10;
2868
+ this.decay = config.decay ?? 1;
2869
+ this.seedX = Math.random() * 1e3;
2870
+ this.seedY = Math.random() * 1e3;
2871
+ }
2872
+ /**
2873
+ * Captures the original position before shake begins.
2874
+ */
2875
+ captureStartState() {
2876
+ this.originalPosition = this.target.position;
2877
+ }
2878
+ /**
2879
+ * Applies procedural shake displacement each frame.
2880
+ * @param progress - Animation progress (0 to 1)
2881
+ */
2882
+ interpolate(progress) {
2883
+ this.ensureInitialized();
2884
+ if (progress >= 1) {
2885
+ this.target.pos(this.originalPosition.x, this.originalPosition.y);
2886
+ return;
2887
+ }
2888
+ const decayFactor = 1 - Math.pow(progress, this.decay);
2889
+ const time = progress * this.durationSeconds * this.frequency;
2890
+ const offsetX = this.noise(time, this.seedX) * this.intensity * decayFactor;
2891
+ const offsetY = this.noise(time, this.seedY) * this.intensity * decayFactor;
2892
+ this.target.pos(
2893
+ this.originalPosition.x + offsetX,
2894
+ this.originalPosition.y + offsetY
2412
2895
  );
2413
2896
  }
2897
+ /**
2898
+ * Generates pseudo-random noise using layered sine waves.
2899
+ * @param t - Time value
2900
+ * @param seed - Random seed for variation
2901
+ * @returns Noise value between -1 and 1
2902
+ */
2903
+ noise(t, seed) {
2904
+ return Math.sin(t * 2 + seed) * 0.5 + Math.sin(t * 3.7 + seed * 1.3) * 0.3 + Math.sin(t * 7.1 + seed * 0.7) * 0.2;
2905
+ }
2906
+ };
2907
+
2908
+ // src/core/animations/introspection.ts
2909
+ function hasAnimationChildren(animation) {
2910
+ return "getChildren" in animation && typeof animation.getChildren === "function";
2911
+ }
2912
+ function getAnimationChildren(animation) {
2913
+ if (hasAnimationChildren(animation)) {
2914
+ return animation.getChildren();
2915
+ }
2916
+ return [];
2917
+ }
2918
+ function getAnimationTotalTime(animation) {
2919
+ return animation.getDuration() + animation.getDelay();
2920
+ }
2921
+ function getLongestAnimationTotalTime(animations) {
2922
+ let longest = 0;
2923
+ for (const animation of animations) {
2924
+ const totalTime = getAnimationTotalTime(animation);
2925
+ if (totalTime > longest) {
2926
+ longest = totalTime;
2927
+ }
2928
+ }
2929
+ return longest;
2930
+ }
2931
+
2932
+ // src/core/timeline/Timeline.ts
2933
+ var Timeline = class {
2934
+ scheduled = [];
2935
+ config;
2936
+ currentTime = 0;
2937
+ constructor(config = {}) {
2938
+ this.config = {
2939
+ loop: config.loop ?? false
2940
+ };
2941
+ }
2942
+ /**
2943
+ * Schedule an animation to start at a specific time.
2944
+ * @param animation The animation to schedule
2945
+ * @param startTime Start time in seconds (default: 0)
2946
+ */
2947
+ schedule(animation, startTime = 0) {
2948
+ if (startTime < 0) {
2949
+ throw new Error("Start time must be non-negative");
2950
+ }
2951
+ this.scheduled.push({ animation, startTime });
2952
+ return this;
2953
+ }
2954
+ /**
2955
+ * Schedule multiple animations to play in sequence.
2956
+ * First animation starts at the given startTime, subsequent
2957
+ * animations start after the previous one ends.
2958
+ * @param animations Animations to schedule sequentially
2959
+ * @param startTime Start time for the first animation (default: 0)
2960
+ */
2961
+ scheduleSequence(animations, startTime = 0) {
2962
+ let currentStart = startTime;
2963
+ for (const anim of animations) {
2964
+ this.schedule(anim, currentStart);
2965
+ currentStart += getAnimationTotalTime(anim);
2966
+ }
2967
+ return this;
2968
+ }
2969
+ /**
2970
+ * Schedule multiple animations to play in parallel.
2971
+ * All animations start at the same time.
2972
+ * @param animations Animations to schedule in parallel
2973
+ * @param startTime Start time for all animations (default: 0)
2974
+ */
2975
+ scheduleParallel(animations, startTime = 0) {
2976
+ for (const anim of animations) {
2977
+ this.schedule(anim, startTime);
2978
+ }
2979
+ return this;
2980
+ }
2981
+ /**
2982
+ * Get all scheduled animations with resolved timing information.
2983
+ */
2984
+ getResolved() {
2985
+ return this.scheduled.map((s) => {
2986
+ const delay = s.animation.getDelay();
2987
+ const duration = s.animation.getDuration();
2988
+ const effectiveStartTime = s.startTime + delay;
2989
+ return {
2990
+ animation: s.animation,
2991
+ effectiveStartTime,
2992
+ duration,
2993
+ endTime: effectiveStartTime + duration
2994
+ };
2995
+ });
2996
+ }
2997
+ /**
2998
+ * Get total duration of the timeline.
2999
+ * Returns the end time of the last animation to finish.
3000
+ */
3001
+ getTotalDuration() {
3002
+ const resolved = this.getResolved();
3003
+ if (resolved.length === 0) return 0;
3004
+ return Math.max(...resolved.map((r) => r.endTime));
3005
+ }
3006
+ /**
3007
+ * Seek to a specific time and update all animations.
3008
+ * @param time Time in seconds to seek to
3009
+ */
3010
+ seek(time) {
3011
+ const clampedTime = Math.max(0, time);
3012
+ this.currentTime = clampedTime;
3013
+ const resolved = this.getResolved();
3014
+ for (const r of resolved) {
3015
+ const { animation, effectiveStartTime, duration, endTime } = r;
3016
+ if (clampedTime < effectiveStartTime) {
3017
+ continue;
3018
+ } else if (clampedTime >= endTime) {
3019
+ animation.update(1);
3020
+ } else {
3021
+ const elapsed = clampedTime - effectiveStartTime;
3022
+ const progress = duration > 0 ? elapsed / duration : 1;
3023
+ animation.update(progress);
3024
+ }
3025
+ }
3026
+ }
3027
+ /**
3028
+ * Get the timeline state at a specific time without modifying the
3029
+ * current playhead position.
3030
+ * @param time Time in seconds
3031
+ */
3032
+ getStateAt(time) {
3033
+ const savedTime = this.currentTime;
3034
+ this.seek(time);
3035
+ this.currentTime = savedTime;
3036
+ }
3037
+ /**
3038
+ * Get the current time of the timeline.
3039
+ */
3040
+ getCurrentTime() {
3041
+ return this.currentTime;
3042
+ }
3043
+ /**
3044
+ * Get all scheduled animations.
3045
+ */
3046
+ getScheduled() {
3047
+ return this.scheduled;
3048
+ }
3049
+ /**
3050
+ * Check if timeline is configured to loop.
3051
+ */
3052
+ isLooping() {
3053
+ return this.config.loop;
3054
+ }
3055
+ /**
3056
+ * Clear all scheduled animations.
3057
+ */
3058
+ clear() {
3059
+ this.scheduled.length = 0;
3060
+ this.currentTime = 0;
3061
+ }
2414
3062
  };
2415
3063
 
2416
3064
  // src/core/camera/types.ts
@@ -2798,9 +3446,9 @@ var Camera = class {
2798
3446
  const frameRotation = this.frame.rotation;
2799
3447
  const zoomX = 1 / frameScale.x;
2800
3448
  const zoomY = 1 / frameScale.y;
2801
- const translate = Matrix3x3.translation(-framePos.x, -framePos.y);
2802
- const rotate = Matrix3x3.rotation(-frameRotation);
2803
- const scale = Matrix3x3.scale(zoomX, zoomY);
3449
+ const translate = Matrix4x4.translation(-framePos.x, -framePos.y, 0);
3450
+ const rotate = Matrix4x4.rotationZ(-frameRotation);
3451
+ const scale = Matrix4x4.scale(zoomX, zoomY, 1);
2804
3452
  return scale.multiply(rotate).multiply(translate);
2805
3453
  }
2806
3454
  // ========== Coordinate Transforms ==========
@@ -2819,10 +3467,10 @@ var Camera = class {
2819
3467
  */
2820
3468
  worldToScreen(pos) {
2821
3469
  const viewMatrix = this.getViewMatrix();
2822
- const transformed = viewMatrix.transformPoint(pos);
3470
+ const transformed = viewMatrix.transformPoint2D(pos);
2823
3471
  const screenX = (transformed.x + 1) * 0.5 * this.config.pixelWidth;
2824
3472
  const screenY = (1 - transformed.y) * 0.5 * this.config.pixelHeight;
2825
- return new Vector2(screenX, screenY);
3473
+ return new Vector(screenX, screenY);
2826
3474
  }
2827
3475
  /**
2828
3476
  * Transforms a screen-space (pixel) position to world coordinates.
@@ -2833,14 +3481,14 @@ var Camera = class {
2833
3481
  *
2834
3482
  * @example
2835
3483
  * // Convert a mouse click position to world coordinates
2836
- * const worldPos = camera.screenToWorld(new Vector2(mouseX, mouseY));
3484
+ * const worldPos = camera.screenToWorld(new Vector(mouseX, mouseY));
2837
3485
  */
2838
3486
  screenToWorld(pos) {
2839
3487
  const ndcX = pos.x / this.config.pixelWidth * 2 - 1;
2840
3488
  const ndcY = 1 - pos.y / this.config.pixelHeight * 2;
2841
3489
  const viewMatrix = this.getViewMatrix();
2842
3490
  const inverseView = viewMatrix.inverse();
2843
- return inverseView.transformPoint(new Vector2(ndcX, ndcY));
3491
+ return inverseView.transformPoint2D(new Vector(ndcX, ndcY));
2844
3492
  }
2845
3493
  /**
2846
3494
  * Checks if a world-space position is currently visible within the camera frame.
@@ -2877,17 +3525,283 @@ var Camera = class {
2877
3525
  }
2878
3526
  };
2879
3527
 
2880
- // src/core/mobjects/VGroup/layout.ts
2881
- var defaultCamera = new Camera();
2882
- function centerGroup(group) {
2883
- const bounds = group.getBoundingBox();
2884
- const centerX = (bounds.minX + bounds.maxX) / 2;
2885
- const centerY = (bounds.minY + bounds.maxY) / 2;
2886
- const translation = Matrix3x3.translation(-centerX, -centerY);
2887
- for (const child of group.getChildren()) {
2888
- child.applyMatrix(translation);
2889
- }
2890
- return group;
3528
+ // src/core/mobjects/VMobject.ts
3529
+ var VMobject = class extends Mobject {
3530
+ pathList = [];
3531
+ /** Stroke color. Only rendered if strokeWidth > 0. */
3532
+ strokeColor = Color.WHITE;
3533
+ /** Stroke width. Default 2 for visibility. */
3534
+ strokeWidth = 2;
3535
+ /** Fill color. Only rendered if fillOpacity > 0. */
3536
+ fillColor = Color.TRANSPARENT;
3537
+ /** Fill opacity. Default 0 means no fill is rendered. */
3538
+ fillOpacity = 0;
3539
+ /** Tracks whether stroke() was explicitly called. */
3540
+ strokeExplicitlySet = false;
3541
+ constructor() {
3542
+ super();
3543
+ }
3544
+ get paths() {
3545
+ return this.pathList;
3546
+ }
3547
+ set paths(value) {
3548
+ this.pathList = value;
3549
+ this.syncPointCloudFromPaths();
3550
+ }
3551
+ /**
3552
+ * Gets the stroke color.
3553
+ */
3554
+ getStrokeColor() {
3555
+ return this.strokeColor;
3556
+ }
3557
+ /**
3558
+ * Gets the stroke width.
3559
+ */
3560
+ getStrokeWidth() {
3561
+ return this.strokeWidth;
3562
+ }
3563
+ /**
3564
+ * Gets the fill color.
3565
+ */
3566
+ getFillColor() {
3567
+ return this.fillColor;
3568
+ }
3569
+ /**
3570
+ * Gets the fill opacity.
3571
+ */
3572
+ getFillOpacity() {
3573
+ return this.fillOpacity;
3574
+ }
3575
+ /**
3576
+ * Adds a new path to the VMobject.
3577
+ * @param path - The BezierPath to add.
3578
+ * @returns this for chaining.
3579
+ */
3580
+ addPath(path) {
3581
+ this.pathList.push(path);
3582
+ this.syncPointCloudFromPaths();
3583
+ return this;
3584
+ }
3585
+ /**
3586
+ * Sets the stroke color and width.
3587
+ * @param color - The stroke color.
3588
+ * @param width - The stroke width. Default is 2.
3589
+ * @returns this for chaining.
3590
+ */
3591
+ stroke(color, width = 2) {
3592
+ this.strokeColor = color;
3593
+ this.strokeWidth = width;
3594
+ this.strokeExplicitlySet = true;
3595
+ return this;
3596
+ }
3597
+ /**
3598
+ * Sets the fill color and opacity.
3599
+ * If stroke was not explicitly set, the default stroke is disabled.
3600
+ * @param color - The fill color.
3601
+ * @param opacity - The fill opacity. Defaults to the color's alpha value.
3602
+ * @returns this for chaining.
3603
+ */
3604
+ fill(color, opacity) {
3605
+ this.fillColor = color;
3606
+ this.fillOpacity = opacity ?? color.a;
3607
+ if (!this.strokeExplicitlySet) {
3608
+ this.strokeWidth = 0;
3609
+ }
3610
+ return this;
3611
+ }
3612
+ getPoints() {
3613
+ const commands = [];
3614
+ for (const path of this.pathList) {
3615
+ commands.push(...path.getCommands());
3616
+ }
3617
+ return commands;
3618
+ }
3619
+ setPoints(commands) {
3620
+ if (commands.length === 0) {
3621
+ this.pathList = [];
3622
+ this.setPointCloud([]);
3623
+ return this;
3624
+ }
3625
+ const path = new BezierPath();
3626
+ for (const cmd of commands) {
3627
+ switch (cmd.type) {
3628
+ case "Move":
3629
+ path.moveTo(cmd.end);
3630
+ break;
3631
+ case "Line":
3632
+ path.lineTo(cmd.end);
3633
+ break;
3634
+ case "Quadratic":
3635
+ if (cmd.control1) {
3636
+ path.quadraticTo(cmd.control1, cmd.end);
3637
+ }
3638
+ break;
3639
+ case "Cubic":
3640
+ if (cmd.control1 && cmd.control2) {
3641
+ path.cubicTo(cmd.control1, cmd.control2, cmd.end);
3642
+ }
3643
+ break;
3644
+ case "Close":
3645
+ path.closePath();
3646
+ break;
3647
+ }
3648
+ }
3649
+ this.pathList = [path];
3650
+ this.syncPointCloudFromPaths();
3651
+ return this;
3652
+ }
3653
+ getPointsAsVectors() {
3654
+ const points = [];
3655
+ for (const cmd of this.getPoints()) {
3656
+ if (cmd.control1) points.push(cmd.control1);
3657
+ if (cmd.control2) points.push(cmd.control2);
3658
+ points.push(cmd.end);
3659
+ }
3660
+ return points;
3661
+ }
3662
+ getBoundingBox() {
3663
+ const points = this.getPointsAsVectors();
3664
+ if (points.length === 0) {
3665
+ const pos = this.position;
3666
+ return { minX: pos.x, maxX: pos.x, minY: pos.y, maxY: pos.y };
3667
+ }
3668
+ let minX = Infinity;
3669
+ let maxX = -Infinity;
3670
+ let minY = Infinity;
3671
+ let maxY = -Infinity;
3672
+ for (const point of points) {
3673
+ if (point.x < minX) minX = point.x;
3674
+ if (point.x > maxX) maxX = point.x;
3675
+ if (point.y < minY) minY = point.y;
3676
+ if (point.y > maxY) maxY = point.y;
3677
+ }
3678
+ return { minX, maxX, minY, maxY };
3679
+ }
3680
+ applyMatrixToOwnGeometry(m) {
3681
+ this.pathList = this.pathList.map((path) => {
3682
+ const transformedCommands = path.getCommands().map((cmd) => {
3683
+ const next = {
3684
+ ...cmd,
3685
+ end: m.transformPoint(Vector.fromPlanar(cmd.end, 0)).toPlanar()
3686
+ };
3687
+ if (cmd.control1) {
3688
+ next.control1 = m.transformPoint(Vector.fromPlanar(cmd.control1, 0)).toPlanar();
3689
+ }
3690
+ if (cmd.control2) {
3691
+ next.control2 = m.transformPoint(Vector.fromPlanar(cmd.control2, 0)).toPlanar();
3692
+ }
3693
+ return next;
3694
+ });
3695
+ const transformedPath = new BezierPath();
3696
+ for (const cmd of transformedCommands) {
3697
+ switch (cmd.type) {
3698
+ case "Move":
3699
+ transformedPath.moveTo(cmd.end);
3700
+ break;
3701
+ case "Line":
3702
+ transformedPath.lineTo(cmd.end);
3703
+ break;
3704
+ case "Quadratic":
3705
+ if (cmd.control1) {
3706
+ transformedPath.quadraticTo(cmd.control1, cmd.end);
3707
+ }
3708
+ break;
3709
+ case "Cubic":
3710
+ if (cmd.control1 && cmd.control2) {
3711
+ transformedPath.cubicTo(cmd.control1, cmd.control2, cmd.end);
3712
+ }
3713
+ break;
3714
+ case "Close":
3715
+ transformedPath.closePath();
3716
+ break;
3717
+ }
3718
+ }
3719
+ return transformedPath;
3720
+ });
3721
+ this.syncPointCloudFromPaths();
3722
+ }
3723
+ // ========== VMobject-Specific Fluent Animation API ==========
3724
+ /**
3725
+ * Progressively draws the VMobject's paths from start to end.
3726
+ * Preserves fill throughout the animation.
3727
+ * @param durationSeconds - Animation duration in seconds.
3728
+ * @returns this for chaining.
3729
+ */
3730
+ write(durationSeconds) {
3731
+ const animation = createWrite(this, durationSeconds);
3732
+ this.getQueue().enqueueAnimation(animation);
3733
+ return this;
3734
+ }
3735
+ /**
3736
+ * Progressively removes the VMobject's paths (reverse of write).
3737
+ * @param durationSeconds - Animation duration in seconds.
3738
+ * @returns this for chaining.
3739
+ */
3740
+ unwrite(durationSeconds) {
3741
+ const animation = createUnwrite(this, durationSeconds);
3742
+ this.getQueue().enqueueAnimation(animation);
3743
+ return this;
3744
+ }
3745
+ /**
3746
+ * First draws the stroke progressively (0-50%), then fades in the fill (50-100%).
3747
+ * @param durationSeconds - Animation duration in seconds.
3748
+ * @returns this for chaining.
3749
+ */
3750
+ draw(durationSeconds) {
3751
+ const animation = createDraw(this, durationSeconds);
3752
+ this.getQueue().enqueueAnimation(animation);
3753
+ return this;
3754
+ }
3755
+ /**
3756
+ * Extends parent hash with VMobject-specific state:
3757
+ * stroke/fill colors, widths, opacity, and path geometry.
3758
+ */
3759
+ computeHash() {
3760
+ const parentHash = super.computeHash();
3761
+ const pathHash = hashString(
3762
+ this.pathList.map(
3763
+ (p) => p.getCommands().map(
3764
+ (c) => `${c.type}:${c.end.x},${c.end.y}` + (c.control1 ? `:${c.control1.x},${c.control1.y}` : "") + (c.control2 ? `:${c.control2.x},${c.control2.y}` : "")
3765
+ ).join("|")
3766
+ ).join("||")
3767
+ );
3768
+ return hashCompose(
3769
+ parentHash,
3770
+ hashNumber(this.strokeColor.r),
3771
+ hashNumber(this.strokeColor.g),
3772
+ hashNumber(this.strokeColor.b),
3773
+ hashNumber(this.strokeColor.a),
3774
+ hashNumber(this.strokeWidth),
3775
+ hashNumber(this.fillColor.r),
3776
+ hashNumber(this.fillColor.g),
3777
+ hashNumber(this.fillColor.b),
3778
+ hashNumber(this.fillColor.a),
3779
+ hashNumber(this.fillOpacity),
3780
+ pathHash
3781
+ );
3782
+ }
3783
+ syncPointCloudFromPaths() {
3784
+ const points = [];
3785
+ for (const path of this.pathList) {
3786
+ for (const cmd of path.getCommands()) {
3787
+ if (cmd.control1) points.push(Vector.fromPlanar(cmd.control1, 0));
3788
+ if (cmd.control2) points.push(Vector.fromPlanar(cmd.control2, 0));
3789
+ points.push(Vector.fromPlanar(cmd.end, 0));
3790
+ }
3791
+ }
3792
+ this.setPointCloud(points);
3793
+ }
3794
+ };
3795
+
3796
+ // src/core/mobjects/VGroup/layout.ts
3797
+ var defaultCamera = new Camera();
3798
+ function centerGroup(group) {
3799
+ const bounds = group.getBoundingBox();
3800
+ const centerX = (bounds.minX + bounds.maxX) / 2;
3801
+ const centerY = (bounds.minY + bounds.maxY) / 2;
3802
+ const translation = Matrix4x4.translation(-centerX, -centerY, 0);
3803
+ group.applyMatrix(translation);
3804
+ return group;
2891
3805
  }
2892
3806
  function toCorner(group, corner, buff = 0, camera = defaultCamera) {
2893
3807
  const bounds = group.getBoundingBox();
@@ -2919,10 +3833,8 @@ function toCorner(group, corner, buff = 0, camera = defaultCamera) {
2919
3833
  const currentCenterY = (bounds.minY + bounds.maxY) / 2;
2920
3834
  const shiftX = targetX - currentCenterX;
2921
3835
  const shiftY = targetY - currentCenterY;
2922
- const translation = Matrix3x3.translation(shiftX, shiftY);
2923
- for (const child of group.getChildren()) {
2924
- child.applyMatrix(translation);
2925
- }
3836
+ const translation = Matrix4x4.translation(shiftX, shiftY, 0);
3837
+ group.applyMatrix(translation);
2926
3838
  return group;
2927
3839
  }
2928
3840
  function arrangeChildren(group, direction = "RIGHT", buff = 0.25, shouldCenter = true) {
@@ -2964,7 +3876,7 @@ function arrangeChildren(group, direction = "RIGHT", buff = 0.25, shouldCenter =
2964
3876
  const childCenterX = (childBounds.minX + childBounds.maxX) / 2;
2965
3877
  shiftX = prevCenterX - childCenterX;
2966
3878
  }
2967
- child.applyMatrix(Matrix3x3.translation(shiftX, shiftY));
3879
+ child.applyMatrix(Matrix4x4.translation(shiftX, shiftY, 0));
2968
3880
  previousChild = child;
2969
3881
  }
2970
3882
  if (shouldCenter) {
@@ -2991,899 +3903,126 @@ function alignToTarget(group, target, edge) {
2991
3903
  shiftX = targetBounds.maxX - myBounds.maxX;
2992
3904
  break;
2993
3905
  }
2994
- const translation = Matrix3x3.translation(shiftX, shiftY);
2995
- for (const child of group.getChildren()) {
2996
- child.applyMatrix(translation);
2997
- }
3906
+ const translation = Matrix4x4.translation(shiftX, shiftY, 0);
3907
+ group.applyMatrix(translation);
2998
3908
  return group;
2999
3909
  }
3000
3910
 
3001
3911
  // src/core/mobjects/VGroup/VGroup.ts
3002
3912
  var VGroup = class extends VMobject {
3003
- children = [];
3004
3913
  constructor(...mobjects) {
3005
3914
  super();
3006
3915
  this.add(...mobjects);
3007
3916
  }
3008
- get length() {
3009
- return this.children.length;
3010
- }
3011
- add(...mobjects) {
3012
- for (const mob of mobjects) {
3013
- if (!this.children.includes(mob)) {
3014
- mob.parent = this;
3015
- this.children.push(mob);
3016
- }
3017
- }
3018
- return this;
3019
- }
3020
- remove(mobject) {
3021
- const index = this.children.indexOf(mobject);
3022
- if (index > -1) {
3023
- mobject.parent = null;
3024
- this.children.splice(index, 1);
3025
- }
3026
- return this;
3027
- }
3028
- clear() {
3029
- for (const child of this.children) {
3030
- child.parent = null;
3031
- }
3032
- this.children = [];
3033
- return this;
3034
- }
3035
- getChildren() {
3036
- return [...this.children];
3037
- }
3038
- get(index) {
3039
- return this.children[index];
3040
- }
3041
- // Note: applyMatrix is NOT overridden - transforms stay on this group's local matrix.
3042
- // Children inherit via getWorldMatrix() during rendering.
3043
- pos(x, y) {
3044
- return super.pos(x, y);
3045
- }
3046
- show() {
3047
- super.show();
3048
- for (const child of this.children) {
3049
- child.show();
3050
- }
3051
- return this;
3052
- }
3053
- hide() {
3054
- super.hide();
3055
- for (const child of this.children) {
3056
- child.hide();
3057
- }
3058
- return this;
3059
- }
3060
- /**
3061
- * Sets the opacity for this VGroup and all children.
3062
- * @param value - The opacity value (0-1).
3063
- * @returns this for chaining.
3064
- */
3065
- setOpacity(value) {
3066
- super.setOpacity(value);
3067
- for (const child of this.children) {
3068
- child.setOpacity(value);
3069
- }
3070
- return this;
3071
- }
3072
- /**
3073
- * Sets the stroke color and width for this VGroup and all children.
3074
- * @param color - The stroke color.
3075
- * @param width - The stroke width. Default is 2.
3076
- * @returns this for chaining.
3077
- */
3078
- stroke(color, width = 2) {
3079
- super.stroke(color, width);
3080
- for (const child of this.children) {
3081
- child.stroke(color, width);
3082
- }
3083
- return this;
3084
- }
3085
- /**
3086
- * Sets the fill color and opacity for this VGroup and all children.
3087
- * @param color - The fill color.
3088
- * @param opacity - The fill opacity. Default is 1 (fully opaque).
3089
- * @returns this for chaining.
3090
- */
3091
- fill(color, opacity = 1) {
3092
- super.fill(color, opacity);
3093
- for (const child of this.children) {
3094
- child.fill(color, opacity);
3095
- }
3096
- return this;
3097
- }
3098
- getBoundingBox() {
3099
- if (this.children.length === 0) {
3100
- return super.getBoundingBox();
3101
- }
3102
- let minX = Infinity;
3103
- let maxX = -Infinity;
3104
- let minY = Infinity;
3105
- let maxY = -Infinity;
3106
- for (const child of this.children) {
3107
- const bounds = child.getBoundingBox();
3108
- if (bounds.minX < minX) minX = bounds.minX;
3109
- if (bounds.maxX > maxX) maxX = bounds.maxX;
3110
- if (bounds.minY < minY) minY = bounds.minY;
3111
- if (bounds.maxY > maxY) maxY = bounds.maxY;
3112
- }
3113
- return { minX, maxX, minY, maxY };
3114
- }
3115
- center() {
3116
- centerGroup(this);
3117
- return this;
3118
- }
3119
- toCorner(corner, buff = 0) {
3120
- toCorner(this, corner, buff);
3121
- return this;
3122
- }
3123
- arrange(direction = "RIGHT", buff = 0.25, shouldCenter = true) {
3124
- arrangeChildren(this, direction, buff, shouldCenter);
3125
- return this;
3126
- }
3127
- alignTo(target, edge) {
3128
- alignToTarget(this, target, edge);
3129
- return this;
3130
- }
3131
- /**
3132
- * Recursively hashes this VGroup and all children.
3133
- * Any child state change invalidates segments containing this group.
3134
- */
3135
- computeHash() {
3136
- const childHashes = this.children.map((c) => c.computeHash());
3137
- return hashCompose(super.computeHash(), ...childHashes);
3138
- }
3139
- };
3140
-
3141
- // src/core/animations/composition/Sequence.ts
3142
- var Sequence = class extends Animation {
3143
- children;
3144
- childDurations;
3145
- totalChildDuration;
3146
- /**
3147
- * The lifecycle of Sequence is determined by its FIRST child animation.
3148
- * If the first animation is introductory, it will register the target,
3149
- * allowing subsequent transformative animations to work.
3150
- */
3151
- lifecycle;
3152
- constructor(animations) {
3153
- super(new Mobject());
3154
- this.children = animations;
3155
- this.childDurations = animations.map((a) => a.getDuration());
3156
- this.totalChildDuration = this.childDurations.reduce((sum, d) => sum + d, 0);
3157
- this.durationSeconds = this.totalChildDuration;
3158
- const first = animations[0];
3159
- this.lifecycle = first?.lifecycle === "introductory" ? "introductory" : "transformative";
3160
- }
3161
- getDuration() {
3162
- return this.durationSeconds;
3163
- }
3164
- getChildren() {
3165
- return this.children;
3166
- }
3167
- /**
3168
- * Initializes only the first child.
3169
- * Later children are initialized when they become active in interpolate().
3170
- */
3171
- ensureInitialized() {
3172
- if (this.children.length > 0) {
3173
- this.children[0].ensureInitialized();
3174
- }
3175
- }
3176
- reset() {
3177
- for (const child of this.children) {
3178
- child.reset();
3179
- }
3180
- }
3181
- /**
3182
- * Interpolates the sequence at the given progress.
3183
- * Maps global progress to the correct child animation.
3184
- *
3185
- * IMPORTANT: We only update children that have started or completed.
3186
- * Children that haven't started yet are NOT updated to avoid
3187
- * premature initialization with incorrect state.
3188
- */
3189
- interpolate(progress) {
3190
- if (this.children.length === 0 || this.totalChildDuration === 0) {
3191
- return;
3192
- }
3193
- const globalTime = progress * this.totalChildDuration;
3194
- let accumulatedTime = 0;
3195
- for (let i = 0; i < this.children.length; i++) {
3196
- const child = this.children[i];
3197
- const childDuration = this.childDurations[i];
3198
- if (child === void 0 || childDuration === void 0) {
3199
- continue;
3200
- }
3201
- const childStart = accumulatedTime;
3202
- const childEnd = accumulatedTime + childDuration;
3203
- if (globalTime < childStart) {
3204
- } else if (globalTime >= childEnd) {
3205
- child.update(1);
3206
- } else {
3207
- const localProgress = (globalTime - childStart) / childDuration;
3208
- child.update(localProgress);
3209
- }
3210
- accumulatedTime = childEnd;
3211
- }
3212
- }
3213
- update(progress) {
3214
- const clampedProgress = Math.max(0, Math.min(1, progress));
3215
- this.interpolate(clampedProgress);
3216
- }
3217
- };
3218
-
3219
- // src/core/animations/keyframes/types.ts
3220
- function lerpNumber(a, b, t) {
3221
- return a + (b - a) * t;
3222
- }
3223
-
3224
- // src/core/animations/keyframes/KeyframeTrack.ts
3225
- var KeyframeTrack = class {
3226
- keyframes = [];
3227
- interpolator;
3228
- /**
3229
- * Creates a new KeyframeTrack with the specified interpolator.
3230
- * Defaults to linear number interpolation.
3231
- */
3232
- constructor(interpolator) {
3233
- this.interpolator = interpolator ?? lerpNumber;
3234
- }
3235
- /**
3236
- * Adds a keyframe at the specified normalized time.
3237
- * Time must be in [0, 1]. Replaces existing keyframe at same time.
3238
- */
3239
- addKeyframe(time, value, easing) {
3240
- if (time < 0 || time > 1) {
3241
- throw new Error("Keyframe time must be in [0, 1]");
3242
- }
3243
- this.keyframes = this.keyframes.filter((kf) => kf.time !== time);
3244
- const keyframe = { time, value, easing };
3245
- this.keyframes.push(keyframe);
3246
- this.keyframes.sort((a, b) => a.time - b.time);
3247
- return this;
3248
- }
3249
- removeKeyframe(time) {
3250
- const initialLength = this.keyframes.length;
3251
- this.keyframes = this.keyframes.filter((kf) => kf.time !== time);
3252
- return this.keyframes.length < initialLength;
3253
- }
3254
- /**
3255
- * Gets the keyframe at the specified time.
3256
- * Returns undefined if no keyframe exists at that time.
3257
- */
3258
- getKeyframe(time) {
3259
- return this.keyframes.find((kf) => kf.time === time);
3260
- }
3261
- /** All keyframes sorted by time. */
3262
- getKeyframes() {
3263
- return this.keyframes;
3264
- }
3265
- /** Interpolated value at normalized time (uses target keyframe easing). */
3266
- getValueAt(time) {
3267
- if (this.keyframes.length === 0) {
3268
- throw new Error("KeyframeTrack has no keyframes");
3269
- }
3270
- const clampedTime = Math.max(0, Math.min(1, time));
3271
- let prevKeyframe;
3272
- let nextKeyframe;
3273
- for (const kf of this.keyframes) {
3274
- if (kf.time <= clampedTime) {
3275
- prevKeyframe = kf;
3276
- }
3277
- if (kf.time >= clampedTime && nextKeyframe === void 0) {
3278
- nextKeyframe = kf;
3279
- }
3280
- }
3281
- if (prevKeyframe === void 0) {
3282
- return this.keyframes[0].value;
3283
- }
3284
- if (nextKeyframe === void 0) {
3285
- return prevKeyframe.value;
3286
- }
3287
- if (prevKeyframe === nextKeyframe) {
3288
- return prevKeyframe.value;
3289
- }
3290
- const span = nextKeyframe.time - prevKeyframe.time;
3291
- const localProgress = (clampedTime - prevKeyframe.time) / span;
3292
- const easing = nextKeyframe.easing ?? linear;
3293
- const easedProgress = easing(localProgress);
3294
- return this.interpolator(prevKeyframe.value, nextKeyframe.value, easedProgress);
3295
- }
3296
- getKeyframeCount() {
3297
- return this.keyframes.length;
3298
- }
3299
- };
3300
-
3301
- // src/core/animations/keyframes/KeyframeAnimation.ts
3302
- var KeyframeAnimation = class extends Animation {
3303
- tracks = /* @__PURE__ */ new Map();
3304
- lifecycle = "transformative";
3305
- /**
3306
- * Adds a named keyframe track with its property setter.
3307
- * The setter is called during interpolation to apply values to the target.
3308
- */
3309
- addTrack(name, track, setter) {
3310
- this.tracks.set(name, {
3311
- track,
3312
- setter
3313
- });
3314
- return this;
3315
- }
3316
- /** Gets a track by name. */
3317
- getTrack(name) {
3318
- const entry = this.tracks.get(name);
3319
- return entry?.track;
3320
- }
3321
- /** All track names. */
3322
- getTrackNames() {
3323
- return Array.from(this.tracks.keys());
3324
- }
3325
- /**
3326
- * Interpolates all tracks at the given progress and applies values to target.
3327
- */
3328
- interpolate(progress) {
3329
- for (const entry of this.tracks.values()) {
3330
- const value = entry.track.getValueAt(progress);
3331
- entry.setter(this.target, value);
3332
- }
3333
- }
3334
- };
3335
-
3336
- // src/core/animations/camera/Follow.ts
3337
- var Follow = class extends Animation {
3338
- lifecycle = "transformative";
3339
- followTarget;
3340
- offset;
3341
- damping;
3342
- /**
3343
- * Creates a new Follow animation.
3344
- *
3345
- * @param frame - The CameraFrame to animate
3346
- * @param target - The Mobject to follow
3347
- * @param config - Configuration options
3348
- * @throws Error if frame is null or undefined
3349
- * @throws Error if target is null or undefined
3350
- *
3351
- * @example
3352
- * const follow = new Follow(scene.frame, player, { damping: 0.7 });
3353
- * this.play(follow.duration(10));
3354
- */
3355
- constructor(frame, target, config = {}) {
3356
- if (!frame) {
3357
- throw new Error("Follow animation requires a CameraFrame");
3358
- }
3359
- if (!target) {
3360
- throw new Error("Follow animation requires a target Mobject");
3361
- }
3362
- super(frame);
3363
- this.followTarget = target;
3364
- this.offset = config.offset ?? Vector2.ZERO;
3365
- this.damping = config.damping ?? 0;
3366
- }
3367
- /**
3368
- * Updates the camera position each frame to track the target.
3369
- * @param progress - Animation progress (0 to 1)
3370
- */
3371
- interpolate(progress) {
3372
- const targetPos = this.followTarget.position.add(this.offset);
3373
- const currentPos = this.target.position;
3374
- if (this.damping > 0 && this.damping < 1) {
3375
- const lerpFactor = 1 - this.damping;
3376
- const newPos = currentPos.lerp(targetPos, lerpFactor);
3377
- this.target.pos(newPos.x, newPos.y);
3378
- } else {
3379
- this.target.pos(targetPos.x, targetPos.y);
3380
- }
3381
- }
3382
- };
3383
-
3384
- // src/core/animations/camera/Shake.ts
3385
- var Shake = class extends TransformativeAnimation {
3386
- originalPosition;
3387
- intensity;
3388
- frequency;
3389
- decay;
3390
- seedX;
3391
- seedY;
3392
- /**
3393
- * Creates a new Shake animation.
3394
- *
3395
- * @param frame - The CameraFrame to shake
3396
- * @param config - Configuration options for intensity, frequency, and decay
3397
- *
3398
- * @example
3399
- * const shake = new Shake(scene.frame, { intensity: 0.3 });
3400
- * this.play(shake.duration(0.5));
3401
- */
3402
- constructor(frame, config = {}) {
3403
- super(frame);
3404
- this.intensity = config.intensity ?? 0.2;
3405
- this.frequency = config.frequency ?? 10;
3406
- this.decay = config.decay ?? 1;
3407
- this.seedX = Math.random() * 1e3;
3408
- this.seedY = Math.random() * 1e3;
3409
- }
3410
- /**
3411
- * Captures the original position before shake begins.
3412
- */
3413
- captureStartState() {
3414
- this.originalPosition = this.target.position;
3415
- }
3416
- /**
3417
- * Applies procedural shake displacement each frame.
3418
- * @param progress - Animation progress (0 to 1)
3419
- */
3420
- interpolate(progress) {
3421
- this.ensureInitialized();
3422
- if (progress >= 1) {
3423
- this.target.pos(this.originalPosition.x, this.originalPosition.y);
3424
- return;
3425
- }
3426
- const decayFactor = 1 - Math.pow(progress, this.decay);
3427
- const time = progress * this.durationSeconds * this.frequency;
3428
- const offsetX = this.noise(time, this.seedX) * this.intensity * decayFactor;
3429
- const offsetY = this.noise(time, this.seedY) * this.intensity * decayFactor;
3430
- this.target.pos(
3431
- this.originalPosition.x + offsetX,
3432
- this.originalPosition.y + offsetY
3433
- );
3434
- }
3435
- /**
3436
- * Generates pseudo-random noise using layered sine waves.
3437
- * @param t - Time value
3438
- * @param seed - Random seed for variation
3439
- * @returns Noise value between -1 and 1
3440
- */
3441
- noise(t, seed) {
3442
- return Math.sin(t * 2 + seed) * 0.5 + Math.sin(t * 3.7 + seed * 1.3) * 0.3 + Math.sin(t * 7.1 + seed * 0.7) * 0.2;
3443
- }
3444
- };
3445
-
3446
- // src/core/animations/introspection.ts
3447
- function hasAnimationChildren(animation) {
3448
- return "getChildren" in animation && typeof animation.getChildren === "function";
3449
- }
3450
- function getAnimationChildren(animation) {
3451
- if (hasAnimationChildren(animation)) {
3452
- return animation.getChildren();
3453
- }
3454
- return [];
3455
- }
3456
- function getAnimationTotalTime(animation) {
3457
- return animation.getDuration() + animation.getDelay();
3458
- }
3459
- function getLongestAnimationTotalTime(animations) {
3460
- let longest = 0;
3461
- for (const animation of animations) {
3462
- const totalTime = getAnimationTotalTime(animation);
3463
- if (totalTime > longest) {
3464
- longest = totalTime;
3465
- }
3466
- }
3467
- return longest;
3468
- }
3469
-
3470
- // src/core/timeline/Timeline.ts
3471
- var Timeline = class {
3472
- scheduled = [];
3473
- config;
3474
- currentTime = 0;
3475
- constructor(config = {}) {
3476
- this.config = {
3477
- loop: config.loop ?? false
3478
- };
3479
- }
3480
- /**
3481
- * Schedule an animation to start at a specific time.
3482
- * @param animation The animation to schedule
3483
- * @param startTime Start time in seconds (default: 0)
3484
- */
3485
- schedule(animation, startTime = 0) {
3486
- if (startTime < 0) {
3487
- throw new Error("Start time must be non-negative");
3488
- }
3489
- this.scheduled.push({ animation, startTime });
3490
- return this;
3491
- }
3492
- /**
3493
- * Schedule multiple animations to play in sequence.
3494
- * First animation starts at the given startTime, subsequent
3495
- * animations start after the previous one ends.
3496
- * @param animations Animations to schedule sequentially
3497
- * @param startTime Start time for the first animation (default: 0)
3498
- */
3499
- scheduleSequence(animations, startTime = 0) {
3500
- let currentStart = startTime;
3501
- for (const anim of animations) {
3502
- this.schedule(anim, currentStart);
3503
- currentStart += getAnimationTotalTime(anim);
3504
- }
3505
- return this;
3506
- }
3507
- /**
3508
- * Schedule multiple animations to play in parallel.
3509
- * All animations start at the same time.
3510
- * @param animations Animations to schedule in parallel
3511
- * @param startTime Start time for all animations (default: 0)
3512
- */
3513
- scheduleParallel(animations, startTime = 0) {
3514
- for (const anim of animations) {
3515
- this.schedule(anim, startTime);
3516
- }
3517
- return this;
3518
- }
3519
- /**
3520
- * Get all scheduled animations with resolved timing information.
3521
- */
3522
- getResolved() {
3523
- return this.scheduled.map((s) => {
3524
- const delay = s.animation.getDelay();
3525
- const duration = s.animation.getDuration();
3526
- const effectiveStartTime = s.startTime + delay;
3527
- return {
3528
- animation: s.animation,
3529
- effectiveStartTime,
3530
- duration,
3531
- endTime: effectiveStartTime + duration
3532
- };
3533
- });
3534
- }
3535
- /**
3536
- * Get total duration of the timeline.
3537
- * Returns the end time of the last animation to finish.
3538
- */
3539
- getTotalDuration() {
3540
- const resolved = this.getResolved();
3541
- if (resolved.length === 0) return 0;
3542
- return Math.max(...resolved.map((r) => r.endTime));
3543
- }
3544
- /**
3545
- * Seek to a specific time and update all animations.
3546
- * @param time Time in seconds to seek to
3547
- */
3548
- seek(time) {
3549
- const clampedTime = Math.max(0, time);
3550
- this.currentTime = clampedTime;
3551
- const resolved = this.getResolved();
3552
- for (const r of resolved) {
3553
- const { animation, effectiveStartTime, duration, endTime } = r;
3554
- if (clampedTime < effectiveStartTime) {
3555
- continue;
3556
- } else if (clampedTime >= endTime) {
3557
- animation.update(1);
3558
- } else {
3559
- const elapsed = clampedTime - effectiveStartTime;
3560
- const progress = duration > 0 ? elapsed / duration : 1;
3561
- animation.update(progress);
3562
- }
3563
- }
3564
- }
3565
- /**
3566
- * Get the timeline state at a specific time without modifying the
3567
- * current playhead position.
3568
- * @param time Time in seconds
3569
- */
3570
- getStateAt(time) {
3571
- const savedTime = this.currentTime;
3572
- this.seek(time);
3573
- this.currentTime = savedTime;
3574
- }
3575
- /**
3576
- * Get the current time of the timeline.
3577
- */
3578
- getCurrentTime() {
3579
- return this.currentTime;
3580
- }
3581
- /**
3582
- * Get all scheduled animations.
3583
- */
3584
- getScheduled() {
3585
- return this.scheduled;
3586
- }
3587
- /**
3588
- * Check if timeline is configured to loop.
3589
- */
3590
- isLooping() {
3591
- return this.config.loop;
3592
- }
3593
- /**
3594
- * Clear all scheduled animations.
3595
- */
3596
- clear() {
3597
- this.scheduled.length = 0;
3598
- this.currentTime = 0;
3599
- }
3600
- };
3601
-
3602
- // src/core/errors/AnimationErrors.ts
3603
- var AnimationTargetNotInSceneError = class extends Error {
3604
- animationName;
3605
- targetType;
3606
- constructor(animation, target) {
3607
- const animName = animation.constructor.name;
3608
- const targetType = target.constructor.name;
3609
- super(
3610
- `Cannot apply '${animName}' animation to ${targetType}: target is not in scene.
3611
-
3612
- This animation requires the target to already exist in the scene because it transforms an existing object rather than introducing a new one.
3613
-
3614
- Solutions:
3615
- 1. Call scene.add(target) before this animation
3616
- 2. Use an introductory animation first:
3617
- - FadeIn: Fades the object in from transparent
3618
- - Create: Draws the object's path progressively
3619
- - Draw: Draws border then fills
3620
- - Write: Draws text progressively
3621
-
3622
- Example:
3623
- // Option 1: Use add() for immediate visibility
3624
- scene.add(circle);
3625
- scene.play(new MoveTo(circle, 2, 0));
3626
-
3627
- // Option 2: Use introductory animation
3628
- scene.play(new FadeIn(circle));
3629
- scene.play(new MoveTo(circle, 2, 0));`
3630
- );
3631
- this.name = "AnimationTargetNotInSceneError";
3632
- this.animationName = animName;
3633
- this.targetType = targetType;
3634
- }
3635
- };
3636
-
3637
- // src/core/scene/Scene.ts
3638
- var Scene = class {
3639
- config;
3640
- mobjects = /* @__PURE__ */ new Set();
3641
- timeline;
3642
- _camera;
3643
- segmentList = [];
3644
- playheadTime = 0;
3645
- constructor(config = {}) {
3646
- this.config = {
3647
- width: config.width ?? 1920,
3648
- height: config.height ?? 1080,
3649
- backgroundColor: config.backgroundColor ?? Color.BLACK,
3650
- frameRate: config.frameRate ?? 60
3651
- };
3652
- this.timeline = new Timeline();
3653
- this._camera = new Camera({
3654
- pixelWidth: this.config.width,
3655
- pixelHeight: this.config.height
3656
- });
3657
- }
3658
- // ========== Camera Shortcuts ==========
3659
- get camera() {
3660
- return this._camera;
3661
- }
3662
- get frame() {
3663
- return this._camera.frame;
3664
- }
3665
- // ========== Configuration Getters ==========
3666
- /** Get scene width in pixels. */
3667
- getWidth() {
3668
- return this.config.width;
3669
- }
3670
- /** Get scene height in pixels. */
3671
- getHeight() {
3672
- return this.config.height;
3673
- }
3674
- /** Get scene background color. */
3675
- getBackgroundColor() {
3676
- return this.config.backgroundColor;
3677
- }
3678
- /** Get scene frame rate. */
3679
- getFrameRate() {
3680
- return this.config.frameRate;
3917
+ get length() {
3918
+ return this.getSubmobjects().length;
3681
3919
  }
3682
- // ========== Mobject Management ==========
3683
- /**
3684
- * Add mobjects to the scene and make them immediately visible.
3685
- * Use this for static elements or backgrounds that should be visible
3686
- * before any animations begin.
3687
- */
3688
3920
  add(...mobjects) {
3689
- for (const m of mobjects) {
3690
- this.mobjects.add(m);
3691
- m.setOpacity(1);
3692
- }
3921
+ this.addSubmobjects(...mobjects);
3693
3922
  return this;
3694
3923
  }
3695
- /**
3696
- * Remove mobjects from the scene.
3697
- */
3698
- remove(...mobjects) {
3699
- for (const m of mobjects) {
3700
- this.mobjects.delete(m);
3701
- }
3924
+ remove(mobject) {
3925
+ this.removeSubmobject(mobject);
3702
3926
  return this;
3703
3927
  }
3704
- /**
3705
- * Check if a mobject is registered with this scene.
3706
- */
3707
- has(mobject) {
3708
- return this.mobjects.has(mobject);
3928
+ clear() {
3929
+ this.clearSubmobjects();
3930
+ return this;
3709
3931
  }
3710
- /**
3711
- * Get all mobjects in the scene.
3712
- */
3713
- getMobjects() {
3714
- return [...this.mobjects];
3932
+ getChildren() {
3933
+ return this.getSubmobjects().filter((m) => m instanceof VMobject);
3715
3934
  }
3716
- // ========== Animation Scheduling ==========
3717
- /**
3718
- * Schedule animations to play at the current playhead position.
3719
- *
3720
- * Accepts either Animation objects or Mobjects with queued fluent animations.
3721
- * When a Mobject is passed, its queued animation chain is automatically extracted.
3722
- *
3723
- * - Introductory animations (FadeIn, Create, Draw, Write) auto-register
3724
- * their targets with the scene if not already present.
3725
- * - Transformative animations (MoveTo, Rotate, Scale) require the target
3726
- * to already be in the scene, otherwise an error is thrown.
3727
- *
3728
- * @example
3729
- * // ProAPI style
3730
- * this.play(new FadeIn(circle), new MoveTo(rect, 2, 0));
3731
- *
3732
- * // FluentAPI style
3733
- * circle.fadeIn(1).moveTo(2, 0, 1);
3734
- * this.play(circle);
3735
- *
3736
- * // Mixed
3737
- * circle.fadeIn(1);
3738
- * this.play(circle, new FadeIn(rect));
3739
- */
3740
- play(...items) {
3741
- if (items.length === 0) {
3742
- return this;
3935
+ get(index) {
3936
+ return this.getChildren()[index];
3937
+ }
3938
+ show() {
3939
+ super.show();
3940
+ for (const child of this.getChildren()) {
3941
+ child.show();
3743
3942
  }
3744
- const animations = items.map((item) => {
3745
- if (item instanceof Mobject) {
3746
- return item.toAnimation();
3747
- }
3748
- return item;
3749
- });
3750
- for (const anim of animations) {
3751
- this.validateAndRegisterAnimation(anim);
3943
+ return this;
3944
+ }
3945
+ hide() {
3946
+ super.hide();
3947
+ for (const child of this.getChildren()) {
3948
+ child.hide();
3752
3949
  }
3753
- this.timeline.scheduleParallel(animations, this.playheadTime);
3754
- const maxDuration = getLongestAnimationTotalTime(animations);
3755
- const endTime = this.playheadTime + maxDuration;
3756
- this.segmentList.push({
3757
- index: this.segmentList.length,
3758
- startTime: this.playheadTime,
3759
- endTime,
3760
- animations,
3761
- hash: this.computeSegmentHash(animations, this.playheadTime, endTime)
3762
- });
3763
- this.playheadTime = endTime;
3764
3950
  return this;
3765
3951
  }
3766
3952
  /**
3767
- * Add a delay before the next play() call.
3768
- * @param seconds Number of seconds to wait
3953
+ * Sets the opacity for this VGroup and all children.
3954
+ * @param value - The opacity value (0-1).
3955
+ * @returns this for chaining.
3769
3956
  */
3770
- wait(seconds) {
3771
- if (seconds < 0) {
3772
- throw new Error("Wait duration must be non-negative");
3957
+ setOpacity(value) {
3958
+ super.setOpacity(value);
3959
+ for (const child of this.getChildren()) {
3960
+ child.setOpacity(value);
3773
3961
  }
3774
- const endTime = this.playheadTime + seconds;
3775
- this.segmentList.push({
3776
- index: this.segmentList.length,
3777
- startTime: this.playheadTime,
3778
- endTime,
3779
- animations: [],
3780
- hash: this.computeSegmentHash([], this.playheadTime, endTime)
3781
- });
3782
- this.playheadTime = endTime;
3783
3962
  return this;
3784
3963
  }
3785
3964
  /**
3786
- * Get the current playhead time.
3965
+ * Sets the stroke color and width for this VGroup and all children.
3966
+ * @param color - The stroke color.
3967
+ * @param width - The stroke width. Default is 2.
3968
+ * @returns this for chaining.
3787
3969
  */
3788
- getCurrentTime() {
3789
- return this.playheadTime;
3970
+ stroke(color, width = 2) {
3971
+ super.stroke(color, width);
3972
+ for (const child of this.getChildren()) {
3973
+ child.stroke(color, width);
3974
+ }
3975
+ return this;
3790
3976
  }
3791
3977
  /**
3792
- * Get the total duration of all scheduled animations.
3978
+ * Sets the fill color and opacity for this VGroup and all children.
3979
+ * @param color - The fill color.
3980
+ * @param opacity - The fill opacity. Default is 1 (fully opaque).
3981
+ * @returns this for chaining.
3793
3982
  */
3794
- getTotalDuration() {
3795
- return this.timeline.getTotalDuration();
3983
+ fill(color, opacity = 1) {
3984
+ super.fill(color, opacity);
3985
+ for (const child of this.getChildren()) {
3986
+ child.fill(color, opacity);
3987
+ }
3988
+ return this;
3796
3989
  }
3797
- // ========== ProAPI Access ==========
3798
- /**
3799
- * Get the underlying Timeline for advanced control.
3800
- * Use this for direct manipulation of animation timing.
3801
- */
3802
- getTimeline() {
3803
- return this.timeline;
3990
+ getBoundingBox() {
3991
+ const children = this.getChildren();
3992
+ if (children.length === 0) {
3993
+ return super.getBoundingBox();
3994
+ }
3995
+ let minX = Infinity;
3996
+ let maxX = -Infinity;
3997
+ let minY = Infinity;
3998
+ let maxY = -Infinity;
3999
+ for (const child of children) {
4000
+ const bounds = child.getBoundingBox();
4001
+ if (bounds.minX < minX) minX = bounds.minX;
4002
+ if (bounds.maxX > maxX) maxX = bounds.maxX;
4003
+ if (bounds.minY < minY) minY = bounds.minY;
4004
+ if (bounds.maxY > maxY) maxY = bounds.maxY;
4005
+ }
4006
+ return { minX, maxX, minY, maxY };
3804
4007
  }
3805
- /**
3806
- * Get the Camera for view control and frame dimensions.
3807
- * Camera calculates Manim-compatible frame dimensions from pixel resolution.
3808
- */
3809
- getCamera() {
3810
- return this._camera;
4008
+ center() {
4009
+ centerGroup(this);
4010
+ return this;
3811
4011
  }
3812
- /**
3813
- * Get the list of segments emitted by play() and wait() calls.
3814
- * Used by the Renderer for cache-aware segmented rendering.
3815
- */
3816
- getSegments() {
3817
- return this.segmentList;
4012
+ toCorner(corner, buff = 0) {
4013
+ toCorner(this, corner, buff);
4014
+ return this;
3818
4015
  }
3819
- // ========== Private Helpers ==========
3820
- /**
3821
- * Validates and registers animation targets based on lifecycle.
3822
- * Handles composition animations (Sequence, Parallel) by processing children.
3823
- */
3824
- validateAndRegisterAnimation(anim) {
3825
- const children = getAnimationChildren(anim);
3826
- if (children.length > 0) {
3827
- for (const child of children) {
3828
- this.validateAndRegisterAnimation(child);
3829
- }
3830
- return;
3831
- }
3832
- const target = anim.getTarget();
3833
- switch (anim.lifecycle) {
3834
- case "introductory":
3835
- if (!this.mobjects.has(target)) {
3836
- this.mobjects.add(target);
3837
- }
3838
- break;
3839
- case "transformative":
3840
- case "exit":
3841
- if (target instanceof CameraFrame) {
3842
- break;
3843
- }
3844
- if (!this.isInScene(target)) {
3845
- throw new AnimationTargetNotInSceneError(anim, target);
3846
- }
3847
- break;
3848
- }
4016
+ arrange(direction = "RIGHT", buff = 0.25, shouldCenter = true) {
4017
+ arrangeChildren(this, direction, buff, shouldCenter);
4018
+ return this;
3849
4019
  }
3850
- /**
3851
- * Checks if a Mobject is in the scene.
3852
- * An object is "in scene" if:
3853
- * - It's directly registered in this.mobjects, OR
3854
- * - Any ancestor in its parent chain is registered
3855
- *
3856
- * This respects the scene graph hierarchy - children of registered
3857
- * VGroups are implicitly in scene via their parent.
3858
- */
3859
- isInScene(mobject) {
3860
- if (this.mobjects.has(mobject)) {
3861
- return true;
3862
- }
3863
- let current = mobject.parent;
3864
- while (current !== null) {
3865
- if (this.mobjects.has(current)) {
3866
- return true;
3867
- }
3868
- current = current.parent;
3869
- }
3870
- return false;
4020
+ alignTo(target, edge) {
4021
+ alignToTarget(this, target, edge);
4022
+ return this;
3871
4023
  }
3872
- /**
3873
- * Computes a holistic CRC32 hash for a segment.
3874
- * Includes camera state, all current mobjects, animations, and timing.
3875
- */
3876
- computeSegmentHash(animations, startTime, endTime) {
3877
- const cameraHash = this._camera.computeHash();
3878
- const mobjectHashes = [...this.mobjects].map((m) => m.computeHash());
3879
- const animHashes = animations.map((a) => a.computeHash());
3880
- return hashCompose(
3881
- cameraHash,
3882
- ...mobjectHashes,
3883
- ...animHashes,
3884
- hashNumber(startTime),
3885
- hashNumber(endTime)
3886
- );
4024
+ computeHash() {
4025
+ return super.computeHash();
3887
4026
  }
3888
4027
  };
3889
4028
 
@@ -3907,27 +4046,39 @@ var Arc = class extends VMobject {
3907
4046
  let currentAngle = this.startAngle;
3908
4047
  const startX = this.radius * Math.cos(currentAngle);
3909
4048
  const startY = this.radius * Math.sin(currentAngle);
3910
- path.moveTo(new Vector2(startX, startY));
4049
+ path.moveTo(new Vector(startX, startY));
3911
4050
  for (let i = 0; i < numSegments; i++) {
3912
4051
  const alpha = currentAngle;
3913
4052
  const nextAngle = currentAngle + stepAngle;
3914
- const p3 = new Vector2(
4053
+ const p3 = new Vector(
3915
4054
  this.radius * Math.cos(nextAngle),
3916
4055
  this.radius * Math.sin(nextAngle)
3917
4056
  );
3918
- const cp1 = new Vector2(
4057
+ const cp1 = new Vector(
3919
4058
  this.radius * Math.cos(alpha),
3920
4059
  this.radius * Math.sin(alpha)
3921
4060
  ).add(
3922
- new Vector2(-Math.sin(alpha), Math.cos(alpha)).multiply(this.radius * k)
4061
+ new Vector(-Math.sin(alpha), Math.cos(alpha)).multiply(this.radius * k)
3923
4062
  );
3924
4063
  const cp2 = p3.subtract(
3925
- new Vector2(-Math.sin(nextAngle), Math.cos(nextAngle)).multiply(this.radius * k)
4064
+ new Vector(-Math.sin(nextAngle), Math.cos(nextAngle)).multiply(this.radius * k)
3926
4065
  );
3927
4066
  path.cubicTo(cp1, cp2, p3);
3928
4067
  currentAngle += stepAngle;
3929
4068
  }
3930
- this.pathList = [path];
4069
+ this.paths = [path];
4070
+ }
4071
+ };
4072
+
4073
+ // src/core/mobjects/geometry/Circle.ts
4074
+ var Circle = class extends Arc {
4075
+ constructor(radius = 1) {
4076
+ super(radius, 0, Math.PI * 2);
4077
+ const firstPath = this.paths[0];
4078
+ if (firstPath) {
4079
+ firstPath.closePath();
4080
+ this.paths = [firstPath];
4081
+ }
3931
4082
  }
3932
4083
  };
3933
4084
 
@@ -3937,51 +4088,15 @@ var Line = class extends VMobject {
3937
4088
  end;
3938
4089
  constructor(x1 = 0, y1 = 0, x2 = 1, y2 = 0) {
3939
4090
  super();
3940
- this.start = new Vector2(x1, y1);
3941
- this.end = new Vector2(x2, y2);
4091
+ this.start = new Vector(x1, y1);
4092
+ this.end = new Vector(x2, y2);
3942
4093
  this.generatePath();
3943
4094
  }
3944
4095
  generatePath() {
3945
4096
  const path = new BezierPath();
3946
4097
  path.moveTo(this.start);
3947
4098
  path.lineTo(this.end);
3948
- this.pathList = [path];
3949
- }
3950
- };
3951
-
3952
- // src/core/mobjects/geometry/Arrow.ts
3953
- var Arrow = class extends Line {
3954
- constructor(x1 = 0, y1 = 0, x2 = 1, y2 = 0, tipLength = 0.25, tipAngle = Math.PI / 6) {
3955
- super(x1, y1, x2, y2);
3956
- this.tipLength = tipLength;
3957
- this.tipAngle = tipAngle;
3958
- this.addTip();
3959
- }
3960
- addTip() {
3961
- const direction = this.end.subtract(this.start);
3962
- if (direction.length() === 0) return;
3963
- const normalizedDir = direction.normalize();
3964
- const angle = Math.atan2(normalizedDir.y, normalizedDir.x);
3965
- const angle1 = angle + Math.PI - this.tipAngle;
3966
- const angle2 = angle + Math.PI + this.tipAngle;
3967
- const p1 = this.end.add(new Vector2(Math.cos(angle1), Math.sin(angle1)).multiply(this.tipLength));
3968
- const p2 = this.end.add(new Vector2(Math.cos(angle2), Math.sin(angle2)).multiply(this.tipLength));
3969
- const tipPath = new BezierPath();
3970
- tipPath.moveTo(this.end);
3971
- tipPath.lineTo(p1);
3972
- tipPath.lineTo(p2);
3973
- tipPath.closePath();
3974
- this.addPath(tipPath);
3975
- }
3976
- };
3977
-
3978
- // src/core/mobjects/geometry/Circle.ts
3979
- var Circle = class extends Arc {
3980
- constructor(radius = 1) {
3981
- super(radius, 0, Math.PI * 2);
3982
- if (this.pathList.length > 0) {
3983
- this.pathList[0].closePath();
3984
- }
4099
+ this.paths = [path];
3985
4100
  }
3986
4101
  };
3987
4102
 
@@ -3990,7 +4105,7 @@ var Polygon = class extends VMobject {
3990
4105
  vertices;
3991
4106
  constructor(...points) {
3992
4107
  super();
3993
- this.vertices = points.map(([x, y]) => new Vector2(x, y));
4108
+ this.vertices = points.map(([x, y]) => new Vector(x, y));
3994
4109
  this.generatePath();
3995
4110
  }
3996
4111
  generatePath() {
@@ -4001,7 +4116,7 @@ var Polygon = class extends VMobject {
4001
4116
  path.lineTo(this.vertices[i]);
4002
4117
  }
4003
4118
  path.closePath();
4004
- this.pathList = [path];
4119
+ this.paths = [path];
4005
4120
  }
4006
4121
  };
4007
4122
 
@@ -4025,6 +4140,32 @@ var Rectangle = class extends Polygon {
4025
4140
  }
4026
4141
  };
4027
4142
 
4143
+ // src/core/mobjects/geometry/Arrow.ts
4144
+ var Arrow = class extends Line {
4145
+ constructor(x1 = 0, y1 = 0, x2 = 1, y2 = 0, tipLength = 0.25, tipAngle = Math.PI / 6) {
4146
+ super(x1, y1, x2, y2);
4147
+ this.tipLength = tipLength;
4148
+ this.tipAngle = tipAngle;
4149
+ this.addTip();
4150
+ }
4151
+ addTip() {
4152
+ const direction = this.end.subtract(this.start);
4153
+ if (direction.length() === 0) return;
4154
+ const normalizedDir = direction.normalize();
4155
+ const angle = Math.atan2(normalizedDir.y, normalizedDir.x);
4156
+ const angle1 = angle + Math.PI - this.tipAngle;
4157
+ const angle2 = angle + Math.PI + this.tipAngle;
4158
+ const p1 = this.end.add(new Vector(Math.cos(angle1), Math.sin(angle1)).multiply(this.tipLength));
4159
+ const p2 = this.end.add(new Vector(Math.cos(angle2), Math.sin(angle2)).multiply(this.tipLength));
4160
+ const tipPath = new BezierPath();
4161
+ tipPath.moveTo(this.end);
4162
+ tipPath.lineTo(p1);
4163
+ tipPath.lineTo(p2);
4164
+ tipPath.closePath();
4165
+ this.addPath(tipPath);
4166
+ }
4167
+ };
4168
+
4028
4169
  // src/core/mobjects/text/Text.ts
4029
4170
  import * as fontkit from "fontkit";
4030
4171
  import { join as join2, resolve } from "path";
@@ -4041,18 +4182,18 @@ function convertFontkitPath(fontkitPath, scale, offsetX, offsetY) {
4041
4182
  if (type === "moveTo" && args.length >= 2) {
4042
4183
  const x = args[0] ?? 0;
4043
4184
  const y = args[1] ?? 0;
4044
- path.moveTo(new Vector2(transformX(x), transformY(y)));
4185
+ path.moveTo(new Vector(transformX(x), transformY(y)));
4045
4186
  } else if (type === "lineTo" && args.length >= 2) {
4046
4187
  const x = args[0] ?? 0;
4047
4188
  const y = args[1] ?? 0;
4048
- path.lineTo(new Vector2(transformX(x), transformY(y)));
4189
+ path.lineTo(new Vector(transformX(x), transformY(y)));
4049
4190
  } else if (type === "quadraticCurveTo" && args.length >= 4) {
4050
4191
  const cpx = args[0] ?? 0;
4051
4192
  const cpy = args[1] ?? 0;
4052
4193
  const x = args[2] ?? 0;
4053
4194
  const y = args[3] ?? 0;
4054
- const cp = new Vector2(transformX(cpx), transformY(cpy));
4055
- const end = new Vector2(transformX(x), transformY(y));
4195
+ const cp = new Vector(transformX(cpx), transformY(cpy));
4196
+ const end = new Vector(transformX(x), transformY(y));
4056
4197
  path.quadraticTo(cp, end);
4057
4198
  } else if (type === "bezierCurveTo" && args.length >= 6) {
4058
4199
  const cp1x = args[0] ?? 0;
@@ -4061,9 +4202,9 @@ function convertFontkitPath(fontkitPath, scale, offsetX, offsetY) {
4061
4202
  const cp2y = args[3] ?? 0;
4062
4203
  const x = args[4] ?? 0;
4063
4204
  const y = args[5] ?? 0;
4064
- const cp1 = new Vector2(transformX(cp1x), transformY(cp1y));
4065
- const cp2 = new Vector2(transformX(cp2x), transformY(cp2y));
4066
- const end = new Vector2(transformX(x), transformY(y));
4205
+ const cp1 = new Vector(transformX(cp1x), transformY(cp1y));
4206
+ const cp2 = new Vector(transformX(cp2x), transformY(cp2y));
4207
+ const end = new Vector(transformX(x), transformY(y));
4067
4208
  path.cubicTo(cp1, cp2, end);
4068
4209
  } else if (type === "closePath") {
4069
4210
  path.closePath();
@@ -4160,9 +4301,6 @@ var GraphNode = class extends VMobject {
4160
4301
  super();
4161
4302
  this.id = id;
4162
4303
  this.nodeRadius = config.radius ?? DEFAULT_RADIUS;
4163
- if (config.position) {
4164
- this.pos(config.position.x, config.position.y);
4165
- }
4166
4304
  if (config.strokeColor !== void 0) {
4167
4305
  this.strokeColor = config.strokeColor;
4168
4306
  }
@@ -4176,6 +4314,9 @@ var GraphNode = class extends VMobject {
4176
4314
  this.fillOpacity = config.fillOpacity;
4177
4315
  }
4178
4316
  this.generateCirclePath();
4317
+ if (config.position) {
4318
+ this.pos(config.position.x, config.position.y);
4319
+ }
4179
4320
  }
4180
4321
  get radius() {
4181
4322
  return this.nodeRadius;
@@ -4188,29 +4329,29 @@ var GraphNode = class extends VMobject {
4188
4329
  const path = new BezierPath();
4189
4330
  const r = this.nodeRadius;
4190
4331
  const k = 0.5522847498;
4191
- path.moveTo(new Vector2(r, 0));
4332
+ path.moveTo(new Vector(r, 0));
4192
4333
  path.cubicTo(
4193
- new Vector2(r, r * k),
4194
- new Vector2(r * k, r),
4195
- new Vector2(0, r)
4334
+ new Vector(r, r * k),
4335
+ new Vector(r * k, r),
4336
+ new Vector(0, r)
4196
4337
  );
4197
4338
  path.cubicTo(
4198
- new Vector2(-r * k, r),
4199
- new Vector2(-r, r * k),
4200
- new Vector2(-r, 0)
4339
+ new Vector(-r * k, r),
4340
+ new Vector(-r, r * k),
4341
+ new Vector(-r, 0)
4201
4342
  );
4202
4343
  path.cubicTo(
4203
- new Vector2(-r, -r * k),
4204
- new Vector2(-r * k, -r),
4205
- new Vector2(0, -r)
4344
+ new Vector(-r, -r * k),
4345
+ new Vector(-r * k, -r),
4346
+ new Vector(0, -r)
4206
4347
  );
4207
4348
  path.cubicTo(
4208
- new Vector2(r * k, -r),
4209
- new Vector2(r, -r * k),
4210
- new Vector2(r, 0)
4349
+ new Vector(r * k, -r),
4350
+ new Vector(r, -r * k),
4351
+ new Vector(r, 0)
4211
4352
  );
4212
4353
  path.closePath();
4213
- this.pathList = [path];
4354
+ this.paths = [path];
4214
4355
  }
4215
4356
  getCenter() {
4216
4357
  return this.position;
@@ -4249,14 +4390,14 @@ var GraphEdge = class extends VMobject {
4249
4390
  if (this.curved) {
4250
4391
  const mid = startPos.lerp(endPos, 0.5);
4251
4392
  const direction = endPos.subtract(startPos);
4252
- const perpendicular = new Vector2(-direction.y, direction.x).normalize();
4393
+ const perpendicular = new Vector(-direction.y, direction.x).normalize();
4253
4394
  const curveOffset = direction.length() * 0.2;
4254
4395
  const controlPoint = mid.add(perpendicular.multiply(curveOffset));
4255
4396
  path.quadraticTo(controlPoint, endPos);
4256
4397
  } else {
4257
4398
  path.lineTo(endPos);
4258
4399
  }
4259
- this.pathList = [path];
4400
+ this.paths = [path];
4260
4401
  }
4261
4402
  getPath() {
4262
4403
  return this.pathList[0];
@@ -4284,7 +4425,7 @@ function circularLayout(nodes, config = {}) {
4284
4425
  const y = radius * Math.sin(angle);
4285
4426
  const node = nodes[i];
4286
4427
  if (node) {
4287
- positions.set(node.id, new Vector2(x, y));
4428
+ positions.set(node.id, new Vector(x, y));
4288
4429
  }
4289
4430
  }
4290
4431
  return positions;
@@ -4344,7 +4485,7 @@ function assignPositions(node, leftBound, spacing) {
4344
4485
  }
4345
4486
  }
4346
4487
  function collectPositions(node, levelHeight, positions) {
4347
- positions.set(node.id, new Vector2(node.x, node.depth * levelHeight));
4488
+ positions.set(node.id, new Vector(node.x, node.depth * levelHeight));
4348
4489
  for (const child of node.children) {
4349
4490
  collectPositions(child, levelHeight, positions);
4350
4491
  }
@@ -4362,7 +4503,7 @@ function treeLayout(nodes, edges, config = {}) {
4362
4503
  const maxX = Math.max(...allX);
4363
4504
  const centerOffset = (minX + maxX) / 2;
4364
4505
  for (const [id, pos] of positions) {
4365
- positions.set(id, new Vector2(pos.x - centerOffset, pos.y));
4506
+ positions.set(id, new Vector(pos.x - centerOffset, pos.y));
4366
4507
  }
4367
4508
  return positions;
4368
4509
  }
@@ -4388,13 +4529,13 @@ function forceDirectedLayout(nodes, edges, config = {}) {
4388
4529
  const node = nodes[i];
4389
4530
  if (node) {
4390
4531
  const angle = 2 * Math.PI * i / nodes.length;
4391
- const initialPos = new Vector2(
4532
+ const initialPos = new Vector(
4392
4533
  Math.cos(angle) * 2,
4393
4534
  Math.sin(angle) * 2
4394
4535
  );
4395
4536
  states.set(node.id, {
4396
4537
  position: initialPos,
4397
- velocity: Vector2.ZERO
4538
+ velocity: Vector.ZERO
4398
4539
  });
4399
4540
  }
4400
4541
  }
@@ -4406,7 +4547,7 @@ function forceDirectedLayout(nodes, edges, config = {}) {
4406
4547
  for (let iter = 0; iter < iterations; iter++) {
4407
4548
  const forces = /* @__PURE__ */ new Map();
4408
4549
  for (const node of nodes) {
4409
- forces.set(node.id, Vector2.ZERO);
4550
+ forces.set(node.id, Vector.ZERO);
4410
4551
  }
4411
4552
  for (let i = 0; i < nodes.length; i++) {
4412
4553
  for (let j = i + 1; j < nodes.length; j++) {
@@ -4419,8 +4560,8 @@ function forceDirectedLayout(nodes, edges, config = {}) {
4419
4560
  const delta = stateA.position.subtract(stateB.position);
4420
4561
  const distance = Math.max(delta.length(), minDistance);
4421
4562
  const force = delta.normalize().multiply(repulsion / (distance * distance));
4422
- const forceA = forces.get(nodeA.id) ?? Vector2.ZERO;
4423
- const forceB = forces.get(nodeB.id) ?? Vector2.ZERO;
4563
+ const forceA = forces.get(nodeA.id) ?? Vector.ZERO;
4564
+ const forceB = forces.get(nodeB.id) ?? Vector.ZERO;
4424
4565
  forces.set(nodeA.id, forceA.add(force));
4425
4566
  forces.set(nodeB.id, forceB.subtract(force));
4426
4567
  }
@@ -4433,8 +4574,8 @@ function forceDirectedLayout(nodes, edges, config = {}) {
4433
4574
  const distance = delta.length();
4434
4575
  const displacement = distance - springLength;
4435
4576
  const force = delta.normalize().multiply(displacement * attraction);
4436
- const forceA = forces.get(edge.source) ?? Vector2.ZERO;
4437
- const forceB = forces.get(edge.target) ?? Vector2.ZERO;
4577
+ const forceA = forces.get(edge.source) ?? Vector.ZERO;
4578
+ const forceB = forces.get(edge.target) ?? Vector.ZERO;
4438
4579
  forces.set(edge.source, forceA.add(force));
4439
4580
  forces.set(edge.target, forceB.subtract(force));
4440
4581
  }
@@ -4483,87 +4624,448 @@ var Graph = class extends VGroup {
4483
4624
  for (const edge of connectedEdges) {
4484
4625
  this.removeEdgeInternal(edge);
4485
4626
  }
4486
- this.nodes.delete(id);
4487
- this.remove(node);
4627
+ this.nodes.delete(id);
4628
+ this.remove(node);
4629
+ return this;
4630
+ }
4631
+ getNode(id) {
4632
+ return this.nodes.get(id);
4633
+ }
4634
+ getNodes() {
4635
+ return Array.from(this.nodes.values());
4636
+ }
4637
+ addEdge(sourceId, targetId, config = {}) {
4638
+ const sourceNode = this.nodes.get(sourceId);
4639
+ const targetNode = this.nodes.get(targetId);
4640
+ if (!sourceNode || !targetNode) {
4641
+ return void 0;
4642
+ }
4643
+ const existing = this.edges.find(
4644
+ (e) => e.source === sourceId && e.target === targetId || e.source === targetId && e.target === sourceId
4645
+ );
4646
+ if (existing) return existing;
4647
+ const edge = new GraphEdge(sourceNode, targetNode, config);
4648
+ this.edges.push(edge);
4649
+ this.add(edge);
4650
+ return edge;
4651
+ }
4652
+ removeEdge(sourceId, targetId) {
4653
+ const edge = this.edges.find(
4654
+ (e) => e.source === sourceId && e.target === targetId || e.source === targetId && e.target === sourceId
4655
+ );
4656
+ if (edge) {
4657
+ this.removeEdgeInternal(edge);
4658
+ }
4659
+ return this;
4660
+ }
4661
+ removeEdgeInternal(edge) {
4662
+ const index = this.edges.indexOf(edge);
4663
+ if (index > -1) {
4664
+ this.edges.splice(index, 1);
4665
+ this.remove(edge);
4666
+ }
4667
+ }
4668
+ getEdgePath(sourceId, targetId) {
4669
+ const edge = this.edges.find(
4670
+ (e) => e.source === sourceId && e.target === targetId || e.source === targetId && e.target === sourceId
4671
+ );
4672
+ return edge?.getPath();
4673
+ }
4674
+ getEdges() {
4675
+ return [...this.edges];
4676
+ }
4677
+ /** Applies a layout algorithm to reposition all nodes. */
4678
+ layout(type, config = {}) {
4679
+ const nodeArray = this.getNodes();
4680
+ let positions;
4681
+ switch (type) {
4682
+ case "circular":
4683
+ positions = circularLayout(nodeArray, config);
4684
+ break;
4685
+ case "tree":
4686
+ positions = treeLayout(nodeArray, this.edges, config);
4687
+ break;
4688
+ case "force-directed":
4689
+ positions = forceDirectedLayout(nodeArray, this.edges, config);
4690
+ break;
4691
+ default:
4692
+ positions = /* @__PURE__ */ new Map();
4693
+ }
4694
+ for (const [id, position] of positions) {
4695
+ const node = this.nodes.get(id);
4696
+ if (node) {
4697
+ node.pos(position.x, position.y);
4698
+ }
4699
+ }
4700
+ this.updateEdges();
4701
+ return this;
4702
+ }
4703
+ updateEdges() {
4704
+ for (const edge of this.edges) {
4705
+ edge.updatePath();
4706
+ }
4707
+ return this;
4708
+ }
4709
+ };
4710
+
4711
+ // src/core/updaters/UpdaterEngine.ts
4712
+ var UpdaterEngine = class {
4713
+ lastTime = null;
4714
+ frame = 0;
4715
+ reset() {
4716
+ this.lastTime = null;
4717
+ this.frame = 0;
4718
+ }
4719
+ run(scene, roots, time) {
4720
+ const clampedTime = Math.max(0, time);
4721
+ const rawDelta = this.lastTime === null ? 0 : clampedTime - this.lastTime;
4722
+ const discontinuous = this.lastTime === null || rawDelta < 0;
4723
+ const dt = discontinuous ? 0 : rawDelta;
4724
+ const ctx = {
4725
+ time: clampedTime,
4726
+ dt,
4727
+ frame: this.frame,
4728
+ scene,
4729
+ discontinuous
4730
+ };
4731
+ const visited = /* @__PURE__ */ new Set();
4732
+ for (const root of roots) {
4733
+ this.runMobjectTree(root, ctx, visited);
4734
+ }
4735
+ this.lastTime = clampedTime;
4736
+ this.frame += 1;
4737
+ }
4738
+ runMobjectTree(mobject, ctx, visited) {
4739
+ if (visited.has(mobject)) {
4740
+ return;
4741
+ }
4742
+ visited.add(mobject);
4743
+ const updaters = mobject.getUpdaterRecordsSnapshot();
4744
+ for (const updater of updaters) {
4745
+ updater.fn(mobject, ctx);
4746
+ }
4747
+ const children = mobject.getSubmobjects();
4748
+ for (const child of children) {
4749
+ this.runMobjectTree(child, ctx, visited);
4750
+ }
4751
+ }
4752
+ };
4753
+
4754
+ // src/core/errors/AnimationErrors.ts
4755
+ var AnimationTargetNotInSceneError = class extends Error {
4756
+ animationName;
4757
+ targetType;
4758
+ constructor(animation, target) {
4759
+ const animName = animation.constructor.name;
4760
+ const targetType = target.constructor.name;
4761
+ super(
4762
+ `Cannot apply '${animName}' animation to ${targetType}: target is not in scene.
4763
+
4764
+ This animation requires the target to already exist in the scene because it transforms an existing object rather than introducing a new one.
4765
+
4766
+ Solutions:
4767
+ 1. Call scene.add(target) before this animation
4768
+ 2. Use an introductory animation first:
4769
+ - FadeIn: Fades the object in from transparent
4770
+ - Create: Draws the object's path progressively
4771
+ - Draw: Draws border then fills
4772
+ - Write: Draws text progressively
4773
+
4774
+ Example:
4775
+ // Option 1: Use add() for immediate visibility
4776
+ scene.add(circle);
4777
+ scene.play(new MoveTo(circle, 2, 0));
4778
+
4779
+ // Option 2: Use introductory animation
4780
+ scene.play(new FadeIn(circle));
4781
+ scene.play(new MoveTo(circle, 2, 0));`
4782
+ );
4783
+ this.name = "AnimationTargetNotInSceneError";
4784
+ this.animationName = animName;
4785
+ this.targetType = targetType;
4786
+ }
4787
+ };
4788
+
4789
+ // src/core/scene/Scene.ts
4790
+ var Scene = class {
4791
+ config;
4792
+ mobjects = /* @__PURE__ */ new Set();
4793
+ timeline;
4794
+ _camera;
4795
+ updaterEngine;
4796
+ segmentList = [];
4797
+ playheadTime = 0;
4798
+ constructor(config = {}) {
4799
+ this.config = {
4800
+ width: config.width ?? 1920,
4801
+ height: config.height ?? 1080,
4802
+ backgroundColor: config.backgroundColor ?? Color.BLACK,
4803
+ frameRate: config.frameRate ?? 60
4804
+ };
4805
+ this.timeline = new Timeline();
4806
+ this._camera = new Camera({
4807
+ pixelWidth: this.config.width,
4808
+ pixelHeight: this.config.height
4809
+ });
4810
+ this.updaterEngine = new UpdaterEngine();
4811
+ }
4812
+ // ========== Camera Shortcuts ==========
4813
+ get camera() {
4814
+ return this._camera;
4815
+ }
4816
+ get frame() {
4817
+ return this._camera.frame;
4818
+ }
4819
+ // ========== Configuration Getters ==========
4820
+ /** Get scene width in pixels. */
4821
+ getWidth() {
4822
+ return this.config.width;
4823
+ }
4824
+ /** Get scene height in pixels. */
4825
+ getHeight() {
4826
+ return this.config.height;
4827
+ }
4828
+ /** Get scene background color. */
4829
+ getBackgroundColor() {
4830
+ return this.config.backgroundColor;
4831
+ }
4832
+ /** Get scene frame rate. */
4833
+ getFrameRate() {
4834
+ return this.config.frameRate;
4835
+ }
4836
+ // ========== Mobject Management ==========
4837
+ /**
4838
+ * Add mobjects to the scene and make them immediately visible.
4839
+ * Use this for static elements or backgrounds that should be visible
4840
+ * before any animations begin.
4841
+ */
4842
+ add(...mobjects) {
4843
+ for (const m of mobjects) {
4844
+ this.mobjects.add(m);
4845
+ m.setOpacity(1);
4846
+ }
4488
4847
  return this;
4489
4848
  }
4490
- getNode(id) {
4491
- return this.nodes.get(id);
4849
+ /**
4850
+ * Remove mobjects from the scene.
4851
+ */
4852
+ remove(...mobjects) {
4853
+ for (const m of mobjects) {
4854
+ this.mobjects.delete(m);
4855
+ }
4856
+ return this;
4492
4857
  }
4493
- getNodes() {
4494
- return Array.from(this.nodes.values());
4858
+ /**
4859
+ * Check if a mobject is registered with this scene.
4860
+ */
4861
+ has(mobject) {
4862
+ return this.mobjects.has(mobject);
4495
4863
  }
4496
- addEdge(sourceId, targetId, config = {}) {
4497
- const sourceNode = this.nodes.get(sourceId);
4498
- const targetNode = this.nodes.get(targetId);
4499
- if (!sourceNode || !targetNode) {
4500
- return void 0;
4864
+ /**
4865
+ * Get all mobjects in the scene.
4866
+ */
4867
+ getMobjects() {
4868
+ return [...this.mobjects];
4869
+ }
4870
+ // ========== Animation Scheduling ==========
4871
+ /**
4872
+ * Schedule animations to play at the current playhead position.
4873
+ *
4874
+ * Accepts either Animation objects or Mobjects with queued fluent animations.
4875
+ * When a Mobject is passed, its queued animation chain is automatically extracted.
4876
+ *
4877
+ * - Introductory animations (FadeIn, Create, Draw, Write) auto-register
4878
+ * their targets with the scene if not already present.
4879
+ * - Transformative animations (MoveTo, Rotate, Scale) require the target
4880
+ * to already be in the scene, otherwise an error is thrown.
4881
+ *
4882
+ * @example
4883
+ * // ProAPI style
4884
+ * this.play(new FadeIn(circle), new MoveTo(rect, 2, 0));
4885
+ *
4886
+ * // FluentAPI style
4887
+ * circle.fadeIn(1).moveTo(2, 0, 1);
4888
+ * this.play(circle);
4889
+ *
4890
+ * // Mixed
4891
+ * circle.fadeIn(1);
4892
+ * this.play(circle, new FadeIn(rect));
4893
+ */
4894
+ play(...items) {
4895
+ if (items.length === 0) {
4896
+ return this;
4501
4897
  }
4502
- const existing = this.edges.find(
4503
- (e) => e.source === sourceId && e.target === targetId || e.source === targetId && e.target === sourceId
4504
- );
4505
- if (existing) return existing;
4506
- const edge = new GraphEdge(sourceNode, targetNode, config);
4507
- this.edges.push(edge);
4508
- this.add(edge);
4509
- return edge;
4898
+ const animations = items.map((item) => {
4899
+ if (item instanceof Mobject) {
4900
+ return item.toAnimation();
4901
+ }
4902
+ return item;
4903
+ });
4904
+ for (const anim of animations) {
4905
+ this.validateAndRegisterAnimation(anim);
4906
+ }
4907
+ this.timeline.scheduleParallel(animations, this.playheadTime);
4908
+ const maxDuration = getLongestAnimationTotalTime(animations);
4909
+ const endTime = this.playheadTime + maxDuration;
4910
+ this.segmentList.push({
4911
+ index: this.segmentList.length,
4912
+ startTime: this.playheadTime,
4913
+ endTime,
4914
+ animations,
4915
+ hash: this.computeSegmentHash(animations, this.playheadTime, endTime)
4916
+ });
4917
+ this.playheadTime = endTime;
4918
+ return this;
4510
4919
  }
4511
- removeEdge(sourceId, targetId) {
4512
- const edge = this.edges.find(
4513
- (e) => e.source === sourceId && e.target === targetId || e.source === targetId && e.target === sourceId
4514
- );
4515
- if (edge) {
4516
- this.removeEdgeInternal(edge);
4920
+ /**
4921
+ * Add a delay before the next play() call.
4922
+ * @param seconds Number of seconds to wait
4923
+ */
4924
+ wait(seconds) {
4925
+ if (seconds < 0) {
4926
+ throw new Error("Wait duration must be non-negative");
4517
4927
  }
4928
+ const endTime = this.playheadTime + seconds;
4929
+ this.segmentList.push({
4930
+ index: this.segmentList.length,
4931
+ startTime: this.playheadTime,
4932
+ endTime,
4933
+ animations: [],
4934
+ hash: this.computeSegmentHash([], this.playheadTime, endTime)
4935
+ });
4936
+ this.playheadTime = endTime;
4518
4937
  return this;
4519
4938
  }
4520
- removeEdgeInternal(edge) {
4521
- const index = this.edges.indexOf(edge);
4522
- if (index > -1) {
4523
- this.edges.splice(index, 1);
4524
- this.remove(edge);
4939
+ /**
4940
+ * Get the current playhead time.
4941
+ */
4942
+ getCurrentTime() {
4943
+ return this.playheadTime;
4944
+ }
4945
+ /**
4946
+ * Get the total duration of all scheduled animations.
4947
+ */
4948
+ getTotalDuration() {
4949
+ return this.timeline.getTotalDuration();
4950
+ }
4951
+ /**
4952
+ * Evaluates scene state at an absolute time.
4953
+ * Execution order is deterministic:
4954
+ * 1) timeline animations
4955
+ * 2) mobject updaters
4956
+ */
4957
+ evaluateFrame(time) {
4958
+ const clampedTime = Math.max(0, time);
4959
+ this.timeline.seek(clampedTime);
4960
+ const roots = [...this.mobjects];
4961
+ roots.push(this._camera.frame);
4962
+ this.updaterEngine.run(this, roots, clampedTime);
4963
+ }
4964
+ /**
4965
+ * Returns true if any scene mobject (or camera frame) has active updaters.
4966
+ * Used by renderer to disable unsafe segment caching.
4967
+ */
4968
+ hasActiveUpdaters() {
4969
+ if (this._camera.frame.hasActiveUpdaters(true)) {
4970
+ return true;
4525
4971
  }
4972
+ for (const mobject of this.mobjects) {
4973
+ if (mobject.hasActiveUpdaters(true)) {
4974
+ return true;
4975
+ }
4976
+ }
4977
+ return false;
4526
4978
  }
4527
- getEdgePath(sourceId, targetId) {
4528
- const edge = this.edges.find(
4529
- (e) => e.source === sourceId && e.target === targetId || e.source === targetId && e.target === sourceId
4530
- );
4531
- return edge?.getPath();
4979
+ // ========== ProAPI Access ==========
4980
+ /**
4981
+ * Get the underlying Timeline for advanced control.
4982
+ * Use this for direct manipulation of animation timing.
4983
+ */
4984
+ getTimeline() {
4985
+ return this.timeline;
4532
4986
  }
4533
- getEdges() {
4534
- return [...this.edges];
4987
+ /**
4988
+ * Get the Camera for view control and frame dimensions.
4989
+ * Camera calculates Manim-compatible frame dimensions from pixel resolution.
4990
+ */
4991
+ getCamera() {
4992
+ return this._camera;
4535
4993
  }
4536
- /** Applies a layout algorithm to reposition all nodes. */
4537
- layout(type, config = {}) {
4538
- const nodeArray = this.getNodes();
4539
- let positions;
4540
- switch (type) {
4541
- case "circular":
4542
- positions = circularLayout(nodeArray, config);
4543
- break;
4544
- case "tree":
4545
- positions = treeLayout(nodeArray, this.edges, config);
4994
+ /**
4995
+ * Get the list of segments emitted by play() and wait() calls.
4996
+ * Used by the Renderer for cache-aware segmented rendering.
4997
+ */
4998
+ getSegments() {
4999
+ return this.segmentList;
5000
+ }
5001
+ // ========== Private Helpers ==========
5002
+ /**
5003
+ * Validates and registers animation targets based on lifecycle.
5004
+ * Handles composition animations (Sequence, Parallel) by processing children.
5005
+ */
5006
+ validateAndRegisterAnimation(anim) {
5007
+ const children = getAnimationChildren(anim);
5008
+ if (children.length > 0) {
5009
+ for (const child of children) {
5010
+ this.validateAndRegisterAnimation(child);
5011
+ }
5012
+ return;
5013
+ }
5014
+ const target = anim.getTarget();
5015
+ switch (anim.lifecycle) {
5016
+ case "introductory":
5017
+ if (!this.mobjects.has(target)) {
5018
+ this.mobjects.add(target);
5019
+ }
4546
5020
  break;
4547
- case "force-directed":
4548
- positions = forceDirectedLayout(nodeArray, this.edges, config);
5021
+ case "transformative":
5022
+ case "exit":
5023
+ if (target instanceof CameraFrame) {
5024
+ break;
5025
+ }
5026
+ if (!this.isInScene(target)) {
5027
+ throw new AnimationTargetNotInSceneError(anim, target);
5028
+ }
4549
5029
  break;
4550
- default:
4551
- positions = /* @__PURE__ */ new Map();
4552
5030
  }
4553
- for (const [id, position] of positions) {
4554
- const node = this.nodes.get(id);
4555
- if (node) {
4556
- node.pos(position.x, position.y);
5031
+ }
5032
+ /**
5033
+ * Checks if a Mobject is in the scene.
5034
+ * An object is "in scene" if:
5035
+ * - It's directly registered in this.mobjects, OR
5036
+ * - Any ancestor in its parent chain is registered
5037
+ *
5038
+ * This respects the scene graph hierarchy - children of registered
5039
+ * VGroups are implicitly in scene via their parent.
5040
+ */
5041
+ isInScene(mobject) {
5042
+ if (this.mobjects.has(mobject)) {
5043
+ return true;
5044
+ }
5045
+ let current = mobject.parent;
5046
+ while (current !== null) {
5047
+ if (this.mobjects.has(current)) {
5048
+ return true;
4557
5049
  }
5050
+ current = current.parent;
4558
5051
  }
4559
- this.updateEdges();
4560
- return this;
5052
+ return false;
4561
5053
  }
4562
- updateEdges() {
4563
- for (const edge of this.edges) {
4564
- edge.updatePath();
4565
- }
4566
- return this;
5054
+ /**
5055
+ * Computes a holistic CRC32 hash for a segment.
5056
+ * Includes camera state, all current mobjects, animations, and timing.
5057
+ */
5058
+ computeSegmentHash(animations, startTime, endTime) {
5059
+ const cameraHash = this._camera.computeHash();
5060
+ const mobjectHashes = [...this.mobjects].map((m) => m.computeHash());
5061
+ const animHashes = animations.map((a) => a.computeHash());
5062
+ return hashCompose(
5063
+ cameraHash,
5064
+ ...mobjectHashes,
5065
+ ...animHashes,
5066
+ hashNumber(startTime),
5067
+ hashNumber(endTime)
5068
+ );
4567
5069
  }
4568
5070
  };
4569
5071
 
@@ -4573,17 +5075,12 @@ import { createCanvas } from "@napi-rs/canvas";
4573
5075
  // src/core/renderer/drawMobject.ts
4574
5076
  function drawMobject(ctx, mobject, worldToScreen) {
4575
5077
  if (mobject.opacity <= 0) return;
4576
- if (mobject instanceof VGroup) {
4577
- drawVGroup(ctx, mobject, worldToScreen);
4578
- } else if (mobject instanceof VMobject) {
5078
+ ctx.save();
5079
+ ctx.globalAlpha *= mobject.opacity;
5080
+ if (mobject instanceof VMobject) {
4579
5081
  drawVMobject(ctx, mobject, worldToScreen);
4580
5082
  }
4581
- }
4582
- function drawVGroup(ctx, vgroup, worldToScreen) {
4583
- if (vgroup.opacity <= 0) return;
4584
- ctx.save();
4585
- ctx.globalAlpha *= vgroup.opacity;
4586
- for (const child of vgroup.getChildren()) {
5083
+ for (const child of mobject.getSubmobjects()) {
4587
5084
  drawMobject(ctx, child, worldToScreen);
4588
5085
  }
4589
5086
  ctx.restore();
@@ -4591,10 +5088,7 @@ function drawVGroup(ctx, vgroup, worldToScreen) {
4591
5088
  function drawVMobject(ctx, vmobject, worldToScreen) {
4592
5089
  const paths = vmobject.paths;
4593
5090
  if (paths.length === 0) return;
4594
- const mobjectWorld = vmobject.getWorldMatrix();
4595
- const transform = worldToScreen.multiply(mobjectWorld);
4596
- ctx.save();
4597
- ctx.globalAlpha *= vmobject.opacity;
5091
+ const transform = worldToScreen.multiply(vmobject.getRenderMatrix());
4598
5092
  for (const path of paths) {
4599
5093
  const commands = path.getCommands();
4600
5094
  if (commands.length === 0) continue;
@@ -4614,34 +5108,33 @@ function drawVMobject(ctx, vmobject, worldToScreen) {
4614
5108
  ctx.stroke();
4615
5109
  }
4616
5110
  }
4617
- ctx.restore();
4618
5111
  }
4619
5112
  function applyPathCommands(ctx, commands, transform) {
4620
5113
  for (const cmd of commands) {
4621
5114
  switch (cmd.type) {
4622
5115
  case "Move": {
4623
- const p = transform.transformPoint(cmd.end);
5116
+ const p = transform.transformPoint2D(cmd.end);
4624
5117
  ctx.moveTo(p.x, p.y);
4625
5118
  break;
4626
5119
  }
4627
5120
  case "Line": {
4628
- const p = transform.transformPoint(cmd.end);
5121
+ const p = transform.transformPoint2D(cmd.end);
4629
5122
  ctx.lineTo(p.x, p.y);
4630
5123
  break;
4631
5124
  }
4632
5125
  case "Quadratic": {
4633
5126
  if (cmd.control1) {
4634
- const cp = transform.transformPoint(cmd.control1);
4635
- const ep = transform.transformPoint(cmd.end);
5127
+ const cp = transform.transformPoint2D(cmd.control1);
5128
+ const ep = transform.transformPoint2D(cmd.end);
4636
5129
  ctx.quadraticCurveTo(cp.x, cp.y, ep.x, ep.y);
4637
5130
  }
4638
5131
  break;
4639
5132
  }
4640
5133
  case "Cubic": {
4641
5134
  if (cmd.control1 && cmd.control2) {
4642
- const cp1 = transform.transformPoint(cmd.control1);
4643
- const cp2 = transform.transformPoint(cmd.control2);
4644
- const ep = transform.transformPoint(cmd.end);
5135
+ const cp1 = transform.transformPoint2D(cmd.control1);
5136
+ const cp2 = transform.transformPoint2D(cmd.control2);
5137
+ const ep = transform.transformPoint2D(cmd.end);
4645
5138
  ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, ep.x, ep.y);
4646
5139
  }
4647
5140
  break;
@@ -4681,10 +5174,11 @@ var FrameRenderer = class {
4681
5174
  const camera = this.scene.getCamera();
4682
5175
  const viewMatrix = camera.getViewMatrix();
4683
5176
  const scale = this.height / MANIM_FRAME_HEIGHT;
4684
- const scaleMatrix = Matrix3x3.scale(scale, -scale);
4685
- const translateToCenter = Matrix3x3.translation(
5177
+ const scaleMatrix = Matrix4x4.scale(scale, -scale, 1);
5178
+ const translateToCenter = Matrix4x4.translation(
4686
5179
  this.width / 2,
4687
- this.height / 2
5180
+ this.height / 2,
5181
+ 0
4688
5182
  );
4689
5183
  return translateToCenter.multiply(scaleMatrix).multiply(viewMatrix);
4690
5184
  }
@@ -4694,8 +5188,7 @@ var FrameRenderer = class {
4694
5188
  renderFrame(time) {
4695
5189
  const canvas = createCanvas(this.width, this.height);
4696
5190
  const ctx = canvas.getContext("2d");
4697
- const timeline = this.scene.getTimeline();
4698
- timeline.seek(time);
5191
+ this.scene.evaluateFrame(time);
4699
5192
  const bgColor = this.scene.getBackgroundColor();
4700
5193
  ctx.fillStyle = `rgb(${bgColor.r}, ${bgColor.g}, ${bgColor.b})`;
4701
5194
  ctx.fillRect(0, 0, this.width, this.height);
@@ -4927,7 +5420,7 @@ var Renderer = class {
4927
5420
  const progressReporter = new ProgressReporter(totalFrames, resolved.onProgress);
4928
5421
  const segments = scene.getSegments();
4929
5422
  const isVideoFormat = resolved.format === "mp4" || resolved.format === "webp" || resolved.format === "gif";
4930
- const useCache = resolved.cache && isVideoFormat && segments.length > 0;
5423
+ const useCache = resolved.cache && isVideoFormat && segments.length > 0 && !scene.hasActiveUpdaters();
4931
5424
  if (useCache) {
4932
5425
  await this.renderSegmented(
4933
5426
  scene,
@@ -5144,6 +5637,7 @@ export {
5144
5637
  FadeIn,
5145
5638
  FadeOut,
5146
5639
  Follow,
5640
+ FrameRenderer,
5147
5641
  Glyph,
5148
5642
  Graph,
5149
5643
  GraphEdge,
@@ -5156,12 +5650,14 @@ export {
5156
5650
  MoveTo,
5157
5651
  Parallel,
5158
5652
  Polygon,
5653
+ ProgressReporter,
5159
5654
  Rectangle,
5160
5655
  Renderer,
5161
5656
  Resolution,
5162
5657
  Rotate,
5163
5658
  Scale,
5164
5659
  Scene,
5660
+ SegmentCache,
5165
5661
  Sequence,
5166
5662
  Shake,
5167
5663
  Text,
@@ -5169,7 +5665,7 @@ export {
5169
5665
  Unwrite,
5170
5666
  VGroup,
5171
5667
  VMobject,
5172
- Vector2,
5668
+ Vector,
5173
5669
  Write,
5174
5670
  clearRegistry,
5175
5671
  smooth as defaultEasing,