@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.
- package/dist/index.d.ts +457 -247
- package/dist/index.js +1926 -1430
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
// src/core/math/
|
|
2
|
-
var
|
|
1
|
+
// src/core/math/vector/Vector.ts
|
|
2
|
+
var Vector = class _Vector {
|
|
3
3
|
x;
|
|
4
4
|
y;
|
|
5
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
static
|
|
43
|
-
|
|
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/
|
|
47
|
-
|
|
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 !==
|
|
120
|
-
throw new Error("
|
|
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(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
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
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const
|
|
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
|
-
|
|
168
|
-
out[
|
|
169
|
-
out[
|
|
170
|
-
out[
|
|
171
|
-
out[
|
|
172
|
-
out[
|
|
173
|
-
out[
|
|
174
|
-
out[
|
|
175
|
-
out[
|
|
176
|
-
out[
|
|
177
|
-
|
|
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
|
|
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
|
|
183
|
-
|
|
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
|
|
186
|
-
|
|
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
|
|
189
|
-
|
|
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
|
|
192
|
-
return new
|
|
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
|
|
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
|
|
387
|
-
let subpathStart = new
|
|
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
|
|
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
|
|
425
|
-
let subpathStart = new
|
|
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
|
|
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
|
|
483
|
-
let subpathStart = new
|
|
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
|
|
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 =
|
|
614
|
-
startPoint =
|
|
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 =
|
|
632
|
-
let subpathStart =
|
|
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 :
|
|
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
|
|
728
|
-
let cursor =
|
|
729
|
-
let subpathStart =
|
|
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
|
|
1306
|
+
if (typeof xOrDestination !== "number") {
|
|
1239
1307
|
this.endPosition = xOrDestination;
|
|
1240
|
-
|
|
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
|
|
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 =
|
|
1353
|
-
let subpathStart =
|
|
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,
|
|
1841
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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[
|
|
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[
|
|
2010
|
-
const sy = Math.sqrt(m[1] * m[1] + m[
|
|
2011
|
-
return new
|
|
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[
|
|
2020
|
-
newValues[
|
|
2021
|
-
|
|
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[
|
|
2039
|
-
const posY = m[
|
|
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(
|
|
2179
|
+
const newValues = new Float32Array(16);
|
|
2044
2180
|
newValues[0] = cos * currentScale.x;
|
|
2045
2181
|
newValues[1] = -sin * currentScale.y;
|
|
2046
|
-
newValues[
|
|
2047
|
-
newValues[
|
|
2048
|
-
newValues[
|
|
2049
|
-
newValues[
|
|
2050
|
-
newValues[
|
|
2051
|
-
|
|
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[
|
|
2057
|
-
const posY = m[
|
|
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(
|
|
2214
|
+
const newValues = new Float32Array(16);
|
|
2062
2215
|
newValues[0] = cos * sx;
|
|
2063
2216
|
newValues[1] = -sin * sy;
|
|
2064
|
-
newValues[
|
|
2065
|
-
newValues[
|
|
2066
|
-
newValues[
|
|
2067
|
-
newValues[
|
|
2068
|
-
newValues[
|
|
2069
|
-
|
|
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
|
-
|
|
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
|
|
2082
|
-
scale: new
|
|
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
|
|
2105
|
-
const scaleAnim =
|
|
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(
|
|
2125
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2221
|
-
return this.
|
|
2526
|
+
usesGeometryTransforms() {
|
|
2527
|
+
return this.pointCloud.length > 0 || this.submobjects.length > 0;
|
|
2222
2528
|
}
|
|
2223
|
-
|
|
2224
|
-
this.
|
|
2529
|
+
collectGeometryPoints(out) {
|
|
2530
|
+
out.push(...this.pointCloud);
|
|
2531
|
+
for (const child of this.submobjects) {
|
|
2532
|
+
child.collectGeometryPoints(out);
|
|
2533
|
+
}
|
|
2225
2534
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
2236
|
-
|
|
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
|
-
|
|
2240
|
-
*/
|
|
2241
|
-
getFillColor() {
|
|
2242
|
-
return this.fillColor;
|
|
2611
|
+
getDuration() {
|
|
2612
|
+
return this.durationSeconds;
|
|
2243
2613
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
*/
|
|
2247
|
-
getFillOpacity() {
|
|
2248
|
-
return this.fillOpacity;
|
|
2614
|
+
getChildren() {
|
|
2615
|
+
return this.children;
|
|
2249
2616
|
}
|
|
2250
2617
|
/**
|
|
2251
|
-
*
|
|
2252
|
-
*
|
|
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
|
-
|
|
2256
|
-
this.
|
|
2257
|
-
|
|
2621
|
+
ensureInitialized() {
|
|
2622
|
+
if (this.children.length > 0) {
|
|
2623
|
+
this.children[0].ensureInitialized();
|
|
2624
|
+
}
|
|
2258
2625
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
-
*
|
|
2273
|
-
*
|
|
2274
|
-
*
|
|
2275
|
-
*
|
|
2276
|
-
*
|
|
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
|
-
|
|
2279
|
-
this.
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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
|
-
|
|
2287
|
-
const
|
|
2288
|
-
|
|
2289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
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
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
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
|
-
|
|
2323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
2357
|
-
*
|
|
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
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
2372
|
-
const
|
|
2373
|
-
|
|
2374
|
-
|
|
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
|
-
*
|
|
2378
|
-
*
|
|
2379
|
-
* @
|
|
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
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
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
|
-
*
|
|
2388
|
-
*
|
|
2818
|
+
* Updates the camera position each frame to track the target.
|
|
2819
|
+
* @param progress - Animation progress (0 to 1)
|
|
2389
2820
|
*/
|
|
2390
|
-
|
|
2391
|
-
const
|
|
2392
|
-
const
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
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 =
|
|
2802
|
-
const rotate =
|
|
2803
|
-
const scale =
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
2881
|
-
var
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
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 =
|
|
2923
|
-
|
|
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(
|
|
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 =
|
|
2995
|
-
|
|
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.
|
|
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
|
-
|
|
3690
|
-
this.mobjects.add(m);
|
|
3691
|
-
m.setOpacity(1);
|
|
3692
|
-
}
|
|
3921
|
+
this.addSubmobjects(...mobjects);
|
|
3693
3922
|
return this;
|
|
3694
3923
|
}
|
|
3695
|
-
|
|
3696
|
-
|
|
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
|
-
|
|
3706
|
-
|
|
3707
|
-
has(mobject) {
|
|
3708
|
-
return this.mobjects.has(mobject);
|
|
3928
|
+
clear() {
|
|
3929
|
+
this.clearSubmobjects();
|
|
3930
|
+
return this;
|
|
3709
3931
|
}
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
*/
|
|
3713
|
-
getMobjects() {
|
|
3714
|
-
return [...this.mobjects];
|
|
3932
|
+
getChildren() {
|
|
3933
|
+
return this.getSubmobjects().filter((m) => m instanceof VMobject);
|
|
3715
3934
|
}
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
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
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
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
|
-
*
|
|
3768
|
-
* @param
|
|
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
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
3789
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
3795
|
-
|
|
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
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
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
|
-
|
|
3807
|
-
|
|
3808
|
-
*/
|
|
3809
|
-
getCamera() {
|
|
3810
|
-
return this._camera;
|
|
4008
|
+
center() {
|
|
4009
|
+
centerGroup(this);
|
|
4010
|
+
return this;
|
|
3811
4011
|
}
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
*/
|
|
3816
|
-
getSegments() {
|
|
3817
|
-
return this.segmentList;
|
|
4012
|
+
toCorner(corner, buff = 0) {
|
|
4013
|
+
toCorner(this, corner, buff);
|
|
4014
|
+
return this;
|
|
3818
4015
|
}
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
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
|
-
|
|
3852
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
4057
|
+
const cp1 = new Vector(
|
|
3919
4058
|
this.radius * Math.cos(alpha),
|
|
3920
4059
|
this.radius * Math.sin(alpha)
|
|
3921
4060
|
).add(
|
|
3922
|
-
new
|
|
4061
|
+
new Vector(-Math.sin(alpha), Math.cos(alpha)).multiply(this.radius * k)
|
|
3923
4062
|
);
|
|
3924
4063
|
const cp2 = p3.subtract(
|
|
3925
|
-
new
|
|
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.
|
|
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
|
|
3941
|
-
this.end = new
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
4055
|
-
const end = new
|
|
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
|
|
4065
|
-
const cp2 = new
|
|
4066
|
-
const end = new
|
|
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
|
|
4332
|
+
path.moveTo(new Vector(r, 0));
|
|
4192
4333
|
path.cubicTo(
|
|
4193
|
-
new
|
|
4194
|
-
new
|
|
4195
|
-
new
|
|
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
|
|
4199
|
-
new
|
|
4200
|
-
new
|
|
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
|
|
4204
|
-
new
|
|
4205
|
-
new
|
|
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
|
|
4209
|
-
new
|
|
4210
|
-
new
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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,
|
|
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) ??
|
|
4423
|
-
const forceB = forces.get(nodeB.id) ??
|
|
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) ??
|
|
4437
|
-
const forceB = forces.get(edge.target) ??
|
|
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
|
-
|
|
4491
|
-
|
|
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
|
-
|
|
4494
|
-
|
|
4858
|
+
/**
|
|
4859
|
+
* Check if a mobject is registered with this scene.
|
|
4860
|
+
*/
|
|
4861
|
+
has(mobject) {
|
|
4862
|
+
return this.mobjects.has(mobject);
|
|
4495
4863
|
}
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
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
|
|
4503
|
-
(
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
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
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
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
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
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
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
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
|
-
|
|
4534
|
-
|
|
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
|
-
/**
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
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 "
|
|
4548
|
-
|
|
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
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
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
|
-
|
|
4560
|
-
return this;
|
|
5052
|
+
return false;
|
|
4561
5053
|
}
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
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
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
4635
|
-
const ep = transform.
|
|
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.
|
|
4643
|
-
const cp2 = transform.
|
|
4644
|
-
const ep = transform.
|
|
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 =
|
|
4685
|
-
const translateToCenter =
|
|
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
|
-
|
|
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
|
-
|
|
5668
|
+
Vector,
|
|
5173
5669
|
Write,
|
|
5174
5670
|
clearRegistry,
|
|
5175
5671
|
smooth as defaultEasing,
|