@it-compiles/anima 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1458 @@
1
+ function isExpr(v) {
2
+ return typeof v === "function";
3
+ }
4
+ function isGroup(obj) {
5
+ return "isGroup" in obj && obj.isGroup === true;
6
+ }
7
+ const Screen = {
8
+ width: 1280,
9
+ height: 720,
10
+ center: () => [640, 360],
11
+ centerX: () => 640,
12
+ centerY: () => 360,
13
+ topLeft: () => [0, 0],
14
+ topRight: () => [1280, 0],
15
+ bottomLeft: () => [0, 720],
16
+ bottomRight: () => [1280, 720]
17
+ };
18
+ const vec = {
19
+ add: ([x1, y1], [x2, y2]) => [x1 + x2, y1 + y2],
20
+ sub: ([x1, y1], [x2, y2]) => [x1 - x2, y1 - y2],
21
+ lerp: ([x1, y1], [x2, y2], u) => [x1 + (x2 - x1) * u, y1 + (y2 - y1) * u]
22
+ };
23
+ const Vec = (x, y) => [x, y];
24
+ const lerp = (a, b, u) => a + (b - a) * u;
25
+ const angle = (deg) => {
26
+ return deg * (Math.PI / 180);
27
+ };
28
+ function applySR([x, y], scale, rotate, scaleX = 1, scaleY = 1) {
29
+ const sx = x * scale * scaleX;
30
+ const sy = y * scale * scaleY;
31
+ const c = Math.cos(rotate);
32
+ const s = Math.sin(rotate);
33
+ return [
34
+ sx * c - sy * s,
35
+ sx * s + sy * c
36
+ ];
37
+ }
38
+ class BBoxHandleImpl {
39
+ paddingValues = { top: 0, right: 0, bottom: 0, left: 0 };
40
+ // Store a function that computes the rect lazily
41
+ computeRect;
42
+ constructor(computeRect) {
43
+ this.computeRect = computeRect;
44
+ }
45
+ pad(p) {
46
+ const parentCompute = this.computeRect;
47
+ const parentPadding = this.paddingValues;
48
+ let newPadding;
49
+ if (typeof p === "number") {
50
+ newPadding = {
51
+ top: parentPadding.top + p,
52
+ right: parentPadding.right + p,
53
+ bottom: parentPadding.bottom + p,
54
+ left: parentPadding.left + p
55
+ };
56
+ } else {
57
+ newPadding = {
58
+ top: parentPadding.top + (p.top ?? 0),
59
+ right: parentPadding.right + (p.right ?? 0),
60
+ bottom: parentPadding.bottom + (p.bottom ?? 0),
61
+ left: parentPadding.left + (p.left ?? 0)
62
+ };
63
+ }
64
+ const result = new BBoxHandleImpl(parentCompute);
65
+ result.paddingValues = newPadding;
66
+ return result;
67
+ }
68
+ getRect() {
69
+ const baseRect = this.computeRect();
70
+ return {
71
+ x: baseRect.x - this.paddingValues.left,
72
+ y: baseRect.y - this.paddingValues.top,
73
+ w: baseRect.w + this.paddingValues.left + this.paddingValues.right,
74
+ h: baseRect.h + this.paddingValues.top + this.paddingValues.bottom
75
+ };
76
+ }
77
+ size() {
78
+ return (_ctx) => {
79
+ const r = this.getRect();
80
+ return [r.w, r.h];
81
+ };
82
+ }
83
+ width() {
84
+ return (_ctx) => {
85
+ return this.getRect().w;
86
+ };
87
+ }
88
+ height() {
89
+ return (_ctx) => {
90
+ return this.getRect().h;
91
+ };
92
+ }
93
+ whSize() {
94
+ return {
95
+ width: this.width(),
96
+ height: this.height()
97
+ };
98
+ }
99
+ center() {
100
+ return (_ctx) => {
101
+ const r = this.getRect();
102
+ return [r.x + r.w / 2, r.y + r.h / 2];
103
+ };
104
+ }
105
+ anchor(a) {
106
+ return (_ctx) => {
107
+ const r = this.getRect();
108
+ let x = r.x, y = r.y;
109
+ if (a === "top" || a === "center" || a === "bottom" || a === "baseline") {
110
+ x = r.x + r.w / 2;
111
+ } else if (a === "topRight" || a === "right" || a === "bottomRight") {
112
+ x = r.x + r.w;
113
+ }
114
+ if (a === "left" || a === "center" || a === "right") {
115
+ y = r.y + r.h / 2;
116
+ } else if (a === "bottomLeft" || a === "bottom" || a === "bottomRight" || a === "baselineLeft" || a === "baseline") {
117
+ y = r.y + r.h;
118
+ }
119
+ return [x, y];
120
+ };
121
+ }
122
+ toRect() {
123
+ return (_ctx) => {
124
+ return this.getRect();
125
+ };
126
+ }
127
+ }
128
+ class AnimCtxImpl {
129
+ base = /* @__PURE__ */ new Map();
130
+ channels = /* @__PURE__ */ new Map();
131
+ pins = /* @__PURE__ */ new Map();
132
+ scene = null;
133
+ debugGroups = [];
134
+ /** Resolve a Vec2 expression to a concrete Vec2 */
135
+ resolveVec2(expr) {
136
+ if (isExpr(expr)) {
137
+ const ctx = this.createResolvedCtx();
138
+ return expr(ctx);
139
+ }
140
+ return expr;
141
+ }
142
+ /** Resolve a number expression to a concrete number */
143
+ resolveNum(expr) {
144
+ if (isExpr(expr)) {
145
+ const ctx = this.createResolvedCtx();
146
+ return expr(ctx);
147
+ }
148
+ return expr;
149
+ }
150
+ /** Create a ResolvedCtx based on current state */
151
+ createResolvedCtx() {
152
+ if (!this.scene) throw new Error("Scene not set");
153
+ return this.solve(this.scene);
154
+ }
155
+ reset(scene) {
156
+ this.base.clear();
157
+ this.channels.clear();
158
+ this.pins.clear();
159
+ this.debugGroups = [];
160
+ this.scene = scene;
161
+ for (const obj of scene.objects()) {
162
+ this.base.set(obj, scene.getInitialTransform(obj));
163
+ }
164
+ }
165
+ getDebugGroups() {
166
+ return this.debugGroups;
167
+ }
168
+ getDebugPins() {
169
+ return this.pins;
170
+ }
171
+ /** Get the current world position of a specific anchor on an object */
172
+ getCurrentAnchorPosition(obj, anchor) {
173
+ if (!this.scene) throw new Error("Scene not set");
174
+ const existingPin = this.pins.get(obj);
175
+ if (existingPin && existingPin.anchor === anchor) {
176
+ return existingPin.world;
177
+ }
178
+ const base = this.base.get(obj);
179
+ const ch = this.channels.get(obj);
180
+ const scale = ch?.scale ?? base.scale;
181
+ const scaleX = (ch?.scaleX ?? 1) * scale;
182
+ const scaleY = (ch?.scaleY ?? 1) * scale;
183
+ const rotate = ch?.rotate ?? base.rotate;
184
+ let originPos;
185
+ if (existingPin) {
186
+ const pinnedLocal = this.scene.resolveAnchor(obj, existingPin.anchor);
187
+ const cos2 = Math.cos(rotate);
188
+ const sin2 = Math.sin(rotate);
189
+ const transformedX2 = pinnedLocal[0] * scaleX * cos2 - pinnedLocal[1] * scaleY * sin2;
190
+ const transformedY2 = pinnedLocal[0] * scaleX * sin2 + pinnedLocal[1] * scaleY * cos2;
191
+ originPos = [existingPin.world[0] - transformedX2, existingPin.world[1] - transformedY2];
192
+ } else {
193
+ originPos = base.translate;
194
+ }
195
+ const local = this.scene.resolveAnchor(obj, anchor);
196
+ const cos = Math.cos(rotate);
197
+ const sin = Math.sin(rotate);
198
+ const transformedX = local[0] * scaleX * cos - local[1] * scaleY * sin;
199
+ const transformedY = local[0] * scaleX * sin + local[1] * scaleY * cos;
200
+ return [originPos[0] + transformedX, originPos[1] + transformedY];
201
+ }
202
+ getGroupBounds(group) {
203
+ return this.computeGroupWorldBounds(group);
204
+ }
205
+ /** Get the origin position of an object in world space, accounting for any existing pin */
206
+ getOriginPosition(obj) {
207
+ if (!this.scene) throw new Error("Scene not set");
208
+ const pin = this.pins.get(obj);
209
+ if (!pin) {
210
+ return this.base.get(obj).translate;
211
+ }
212
+ const base = this.base.get(obj);
213
+ const ch = this.channels.get(obj);
214
+ const scale = ch?.scale ?? base.scale;
215
+ const scaleX = (ch?.scaleX ?? 1) * scale;
216
+ const scaleY = (ch?.scaleY ?? 1) * scale;
217
+ const rotate = ch?.rotate ?? base.rotate;
218
+ const localAnchor = this.scene.resolveAnchor(obj, pin.anchor);
219
+ const cos = Math.cos(rotate);
220
+ const sin = Math.sin(rotate);
221
+ const transformedX = localAnchor[0] * scaleX * cos - localAnchor[1] * scaleY * sin;
222
+ const transformedY = localAnchor[0] * scaleX * sin + localAnchor[1] * scaleY * cos;
223
+ return [pin.world[0] - transformedX, pin.world[1] - transformedY];
224
+ }
225
+ resolveGroupAnchor(group, anchor) {
226
+ const bounds = this.getGroupBounds(group);
227
+ const anchorMap = {
228
+ origin: [bounds.x, bounds.y],
229
+ // For groups, origin is same as topLeft
230
+ topLeft: [bounds.x, bounds.y],
231
+ top: [bounds.x + bounds.w / 2, bounds.y],
232
+ topRight: [bounds.x + bounds.w, bounds.y],
233
+ left: [bounds.x, bounds.y + bounds.h / 2],
234
+ center: [bounds.x + bounds.w / 2, bounds.y + bounds.h / 2],
235
+ right: [bounds.x + bounds.w, bounds.y + bounds.h / 2],
236
+ bottomLeft: [bounds.x, bounds.y + bounds.h],
237
+ bottom: [bounds.x + bounds.w / 2, bounds.y + bounds.h],
238
+ bottomRight: [bounds.x + bounds.w, bounds.y + bounds.h],
239
+ baselineLeft: [bounds.x, bounds.y + bounds.h],
240
+ // baseline defaults to bottom
241
+ baseline: [bounds.x + bounds.w / 2, bounds.y + bounds.h]
242
+ };
243
+ return anchorMap[anchor];
244
+ }
245
+ getGroupCenter(group) {
246
+ const bounds = this.getGroupBounds(group);
247
+ return [bounds.x + bounds.w / 2, bounds.y + bounds.h / 2];
248
+ }
249
+ scale(obj) {
250
+ if (isGroup(obj)) {
251
+ return {
252
+ to: (vExpr, u) => {
253
+ const v = this.resolveNum(vExpr);
254
+ const groupCenter = this.getGroupCenter(obj);
255
+ const scaleFactor = lerp(1, v, u);
256
+ for (const member of obj.members) {
257
+ const base = this.base.get(member);
258
+ const ch = this.channels.get(member) ?? {};
259
+ const currentScale = ch.scale ?? base.scale;
260
+ ch.scale = currentScale * scaleFactor;
261
+ const originPos = this.getOriginPosition(member);
262
+ const offsetFromCenter = vec.sub(originPos, groupCenter);
263
+ const scaledOffset = [offsetFromCenter[0] * scaleFactor, offsetFromCenter[1] * scaleFactor];
264
+ const newOriginPos = vec.add(groupCenter, scaledOffset);
265
+ this.pins.set(member, {
266
+ anchor: "origin",
267
+ world: newOriginPos
268
+ });
269
+ this.channels.set(member, ch);
270
+ }
271
+ },
272
+ by: (deltaExpr, u) => {
273
+ const delta = this.resolveNum(deltaExpr);
274
+ const groupCenter = this.getGroupCenter(obj);
275
+ const scaleFactor = lerp(1, 1 + delta, u);
276
+ for (const member of obj.members) {
277
+ const base = this.base.get(member);
278
+ const ch = this.channels.get(member) ?? {};
279
+ const currentScale = ch.scale ?? base.scale;
280
+ ch.scale = currentScale * scaleFactor;
281
+ const originPos = this.getOriginPosition(member);
282
+ const offsetFromCenter = vec.sub(originPos, groupCenter);
283
+ const scaledOffset = [offsetFromCenter[0] * scaleFactor, offsetFromCenter[1] * scaleFactor];
284
+ const newOriginPos = vec.add(groupCenter, scaledOffset);
285
+ this.pins.set(member, {
286
+ anchor: "origin",
287
+ world: newOriginPos
288
+ });
289
+ this.channels.set(member, ch);
290
+ }
291
+ }
292
+ };
293
+ }
294
+ return {
295
+ to: (vExpr, u) => {
296
+ const v = this.resolveNum(vExpr);
297
+ const ch = this.channels.get(obj) ?? {};
298
+ const current = ch.scale ?? this.base.get(obj).scale;
299
+ ch.scale = lerp(current, v, u);
300
+ this.channels.set(obj, ch);
301
+ },
302
+ by: (deltaExpr, u) => {
303
+ const delta = this.resolveNum(deltaExpr);
304
+ const ch = this.channels.get(obj) ?? {};
305
+ const current = ch.scale ?? this.base.get(obj).scale;
306
+ ch.scale = current * lerp(1, 1 + delta, u);
307
+ this.channels.set(obj, ch);
308
+ }
309
+ };
310
+ }
311
+ scaleX(obj) {
312
+ return {
313
+ to: (vExpr, u) => {
314
+ const v = this.resolveNum(vExpr);
315
+ const ch = this.channels.get(obj) ?? {};
316
+ const current = ch.scaleX ?? 1;
317
+ ch.scaleX = lerp(current, v, u);
318
+ this.channels.set(obj, ch);
319
+ },
320
+ by: (deltaExpr, u) => {
321
+ const delta = this.resolveNum(deltaExpr);
322
+ const ch = this.channels.get(obj) ?? {};
323
+ const current = ch.scaleX ?? 1;
324
+ ch.scaleX = current * lerp(1, 1 + delta, u);
325
+ this.channels.set(obj, ch);
326
+ }
327
+ };
328
+ }
329
+ scaleY(obj) {
330
+ return {
331
+ to: (vExpr, u) => {
332
+ const v = this.resolveNum(vExpr);
333
+ const ch = this.channels.get(obj) ?? {};
334
+ const current = ch.scaleY ?? 1;
335
+ ch.scaleY = lerp(current, v, u);
336
+ this.channels.set(obj, ch);
337
+ },
338
+ by: (deltaExpr, u) => {
339
+ const delta = this.resolveNum(deltaExpr);
340
+ const ch = this.channels.get(obj) ?? {};
341
+ const current = ch.scaleY ?? 1;
342
+ ch.scaleY = current * lerp(1, 1 + delta, u);
343
+ this.channels.set(obj, ch);
344
+ }
345
+ };
346
+ }
347
+ width(obj) {
348
+ if (!this.scene) throw new Error("Scene not set");
349
+ const bounds = this.scene.getBounds(obj);
350
+ const originalWidth = bounds.w;
351
+ return {
352
+ to: (targetPxExpr, u) => {
353
+ const targetPx = this.resolveNum(targetPxExpr);
354
+ const targetScaleX = targetPx / originalWidth;
355
+ const ch = this.channels.get(obj) ?? {};
356
+ const current = ch.scaleX ?? 1;
357
+ ch.scaleX = lerp(current, targetScaleX, u);
358
+ this.channels.set(obj, ch);
359
+ },
360
+ by: (deltaPxExpr, u) => {
361
+ const deltaPx = this.resolveNum(deltaPxExpr);
362
+ const ch = this.channels.get(obj) ?? {};
363
+ const currentScaleX = ch.scaleX ?? 1;
364
+ const currentWidth = originalWidth * currentScaleX;
365
+ const targetWidth = currentWidth + deltaPx;
366
+ const targetScaleX = targetWidth / originalWidth;
367
+ ch.scaleX = lerp(currentScaleX, targetScaleX, u);
368
+ this.channels.set(obj, ch);
369
+ }
370
+ };
371
+ }
372
+ height(obj) {
373
+ if (!this.scene) throw new Error("Scene not set");
374
+ const bounds = this.scene.getBounds(obj);
375
+ const originalHeight = bounds.h;
376
+ return {
377
+ to: (targetPxExpr, u) => {
378
+ const targetPx = this.resolveNum(targetPxExpr);
379
+ const targetScaleY = targetPx / originalHeight;
380
+ const ch = this.channels.get(obj) ?? {};
381
+ const current = ch.scaleY ?? 1;
382
+ ch.scaleY = lerp(current, targetScaleY, u);
383
+ this.channels.set(obj, ch);
384
+ },
385
+ by: (deltaPxExpr, u) => {
386
+ const deltaPx = this.resolveNum(deltaPxExpr);
387
+ const ch = this.channels.get(obj) ?? {};
388
+ const currentScaleY = ch.scaleY ?? 1;
389
+ const currentHeight = originalHeight * currentScaleY;
390
+ const targetHeight = currentHeight + deltaPx;
391
+ const targetScaleY = targetHeight / originalHeight;
392
+ ch.scaleY = lerp(currentScaleY, targetScaleY, u);
393
+ this.channels.set(obj, ch);
394
+ }
395
+ };
396
+ }
397
+ resize(obj) {
398
+ if (!this.scene) throw new Error("Scene not set");
399
+ const bounds = this.scene.getBounds(obj);
400
+ const originalWidth = bounds.w;
401
+ const originalHeight = bounds.h;
402
+ const resolveTarget = (target) => {
403
+ if (typeof target === "number") return target;
404
+ if (typeof target === "function") return this.resolveNum(target);
405
+ if ("fit" in target) return { fit: this.resolveNum(target.fit) };
406
+ if ("width" in target && "height" in target) {
407
+ return { width: this.resolveNum(target.width), height: this.resolveNum(target.height) };
408
+ }
409
+ if ("width" in target) return { width: this.resolveNum(target.width) };
410
+ if ("height" in target) return { height: this.resolveNum(target.height) };
411
+ return target;
412
+ };
413
+ const parseTarget = (target) => {
414
+ if (typeof target === "number") {
415
+ return { scale: target / originalWidth };
416
+ }
417
+ if ("fit" in target) {
418
+ const scaleW = target.fit / originalWidth;
419
+ const scaleH = target.fit / originalHeight;
420
+ return { scale: Math.min(scaleW, scaleH) };
421
+ }
422
+ if ("width" in target && "height" in target) {
423
+ return {
424
+ scaleX: target.width / originalWidth,
425
+ scaleY: target.height / originalHeight
426
+ };
427
+ }
428
+ if ("width" in target) {
429
+ return { scale: target.width / originalWidth };
430
+ }
431
+ if ("height" in target) {
432
+ return { scale: target.height / originalHeight };
433
+ }
434
+ return {};
435
+ };
436
+ return {
437
+ to: (targetExpr, u) => {
438
+ const target = resolveTarget(targetExpr);
439
+ const { scaleX, scaleY, scale } = parseTarget(target);
440
+ const ch = this.channels.get(obj) ?? {};
441
+ if (scale !== void 0) {
442
+ const currentScale = ch.scale ?? this.base.get(obj).scale;
443
+ ch.scale = lerp(currentScale, scale, u);
444
+ } else {
445
+ if (scaleX !== void 0) {
446
+ const current = ch.scaleX ?? 1;
447
+ ch.scaleX = lerp(current, scaleX, u);
448
+ }
449
+ if (scaleY !== void 0) {
450
+ const current = ch.scaleY ?? 1;
451
+ ch.scaleY = lerp(current, scaleY, u);
452
+ }
453
+ }
454
+ this.channels.set(obj, ch);
455
+ },
456
+ by: (deltaExpr, u) => {
457
+ const delta = resolveTarget(deltaExpr);
458
+ const ch = this.channels.get(obj) ?? {};
459
+ if (typeof delta === "number") {
460
+ const currentScale = ch.scale ?? this.base.get(obj).scale;
461
+ const currentWidth = originalWidth * currentScale;
462
+ const targetWidth = currentWidth + delta;
463
+ const targetScale = targetWidth / originalWidth;
464
+ ch.scale = lerp(currentScale, targetScale, u);
465
+ } else if ("fit" in delta) {
466
+ const currentScale = ch.scale ?? this.base.get(obj).scale;
467
+ const currentFit = Math.max(originalWidth, originalHeight) * currentScale;
468
+ const targetFit = currentFit + delta.fit;
469
+ const scaleW = targetFit / originalWidth;
470
+ const scaleH = targetFit / originalHeight;
471
+ const targetScale = Math.min(scaleW, scaleH);
472
+ ch.scale = lerp(currentScale, targetScale, u);
473
+ } else if ("width" in delta && "height" in delta) {
474
+ const currentScaleX = ch.scaleX ?? 1;
475
+ const currentScaleY = ch.scaleY ?? 1;
476
+ const currentWidth = originalWidth * currentScaleX;
477
+ const currentHeight = originalHeight * currentScaleY;
478
+ const targetScaleX = (currentWidth + delta.width) / originalWidth;
479
+ const targetScaleY = (currentHeight + delta.height) / originalHeight;
480
+ ch.scaleX = lerp(currentScaleX, targetScaleX, u);
481
+ ch.scaleY = lerp(currentScaleY, targetScaleY, u);
482
+ } else if ("width" in delta) {
483
+ const currentScale = ch.scale ?? this.base.get(obj).scale;
484
+ const currentWidth = originalWidth * currentScale;
485
+ const targetScale = (currentWidth + delta.width) / originalWidth;
486
+ ch.scale = lerp(currentScale, targetScale, u);
487
+ } else if ("height" in delta) {
488
+ const currentScale = ch.scale ?? this.base.get(obj).scale;
489
+ const currentHeight = originalHeight * currentScale;
490
+ const targetScale = (currentHeight + delta.height) / originalHeight;
491
+ ch.scale = lerp(currentScale, targetScale, u);
492
+ }
493
+ this.channels.set(obj, ch);
494
+ }
495
+ };
496
+ }
497
+ fit(obj) {
498
+ return {
499
+ to: (bbox, u) => {
500
+ this.resize(obj).to(bbox.whSize(), u);
501
+ this.pin(obj, "center").to(bbox.center(), u);
502
+ },
503
+ toBBox: (target, u, opts) => {
504
+ let bbox = this.bbox(target);
505
+ if (opts?.pad !== void 0) {
506
+ bbox = bbox.pad(opts.pad);
507
+ }
508
+ this.resize(obj).to(bbox.whSize(), u);
509
+ this.pin(obj, "center").to(bbox.center(), u);
510
+ }
511
+ };
512
+ }
513
+ computeWorldBounds(obj) {
514
+ if (!this.scene) throw new Error("Scene not set");
515
+ const bounds = this.scene.getBounds(obj);
516
+ const ch = this.channels.get(obj);
517
+ const base = this.base.get(obj);
518
+ const pin = this.pins.get(obj);
519
+ let translate;
520
+ if (pin) {
521
+ const local = this.scene.resolveAnchor(obj, pin.anchor);
522
+ const scale2 = ch?.scale ?? base.scale;
523
+ const scaleX2 = (ch?.scaleX ?? 1) * scale2;
524
+ const scaleY2 = (ch?.scaleY ?? 1) * scale2;
525
+ const rotate = ch?.rotate ?? base.rotate;
526
+ const cos = Math.cos(rotate);
527
+ const sin = Math.sin(rotate);
528
+ const transformedX = local[0] * scaleX2 * cos - local[1] * scaleY2 * sin;
529
+ const transformedY = local[0] * scaleX2 * sin + local[1] * scaleY2 * cos;
530
+ translate = [pin.world[0] - transformedX, pin.world[1] - transformedY];
531
+ } else {
532
+ translate = base.translate;
533
+ }
534
+ const scale = ch?.scale ?? base.scale;
535
+ const scaleX = (ch?.scaleX ?? 1) * scale;
536
+ const scaleY = (ch?.scaleY ?? 1) * scale;
537
+ return {
538
+ x: translate[0] + bounds.x * scaleX,
539
+ y: translate[1] + bounds.y * scaleY,
540
+ w: bounds.w * scaleX,
541
+ h: bounds.h * scaleY
542
+ };
543
+ }
544
+ computeGroupWorldBounds(group) {
545
+ let minX = Infinity, minY = Infinity;
546
+ let maxX = -Infinity, maxY = -Infinity;
547
+ for (const member of group.members) {
548
+ const b = this.computeWorldBounds(member);
549
+ minX = Math.min(minX, b.x);
550
+ minY = Math.min(minY, b.y);
551
+ maxX = Math.max(maxX, b.x + b.w);
552
+ maxY = Math.max(maxY, b.y + b.h);
553
+ }
554
+ if (!isFinite(minX)) {
555
+ return { x: 0, y: 0, w: 0, h: 0 };
556
+ }
557
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
558
+ }
559
+ bbox(obj) {
560
+ if (!this.scene) throw new Error("Scene not set");
561
+ const computeRect = () => {
562
+ return isGroup(obj) ? this.computeGroupWorldBounds(obj) : this.computeWorldBounds(obj);
563
+ };
564
+ return new BBoxHandleImpl(computeRect);
565
+ }
566
+ rotate(obj) {
567
+ if (isGroup(obj)) {
568
+ return {
569
+ to: (radExpr, u) => {
570
+ const rad = this.resolveNum(radExpr);
571
+ const groupCenter = this.getGroupCenter(obj);
572
+ for (const member of obj.members) {
573
+ const base = this.base.get(member);
574
+ const ch = this.channels.get(member) ?? {};
575
+ const currentRot = ch.rotate ?? base.rotate;
576
+ ch.rotate = lerp(currentRot, rad, u);
577
+ const originPos = this.getOriginPosition(member);
578
+ const offsetFromCenter = vec.sub(originPos, groupCenter);
579
+ const angle2 = lerp(0, rad, u);
580
+ const cos = Math.cos(angle2);
581
+ const sin = Math.sin(angle2);
582
+ const rotatedOffset = [
583
+ offsetFromCenter[0] * cos - offsetFromCenter[1] * sin,
584
+ offsetFromCenter[0] * sin + offsetFromCenter[1] * cos
585
+ ];
586
+ const newOriginPos = vec.add(groupCenter, rotatedOffset);
587
+ this.pins.set(member, {
588
+ anchor: "origin",
589
+ world: newOriginPos
590
+ });
591
+ this.channels.set(member, ch);
592
+ }
593
+ },
594
+ by: (deltaExpr, u) => {
595
+ const delta = this.resolveNum(deltaExpr);
596
+ const groupCenter = this.getGroupCenter(obj);
597
+ for (const member of obj.members) {
598
+ const base = this.base.get(member);
599
+ const ch = this.channels.get(member) ?? {};
600
+ const currentRot = ch.rotate ?? base.rotate;
601
+ ch.rotate = currentRot + delta * u;
602
+ const originPos = this.getOriginPosition(member);
603
+ const offsetFromCenter = vec.sub(originPos, groupCenter);
604
+ const angle2 = delta * u;
605
+ const cos = Math.cos(angle2);
606
+ const sin = Math.sin(angle2);
607
+ const rotatedOffset = [
608
+ offsetFromCenter[0] * cos - offsetFromCenter[1] * sin,
609
+ offsetFromCenter[0] * sin + offsetFromCenter[1] * cos
610
+ ];
611
+ const newOriginPos = vec.add(groupCenter, rotatedOffset);
612
+ this.pins.set(member, {
613
+ anchor: "origin",
614
+ world: newOriginPos
615
+ });
616
+ this.channels.set(member, ch);
617
+ }
618
+ }
619
+ };
620
+ }
621
+ return {
622
+ to: (radExpr, u) => {
623
+ const rad = this.resolveNum(radExpr);
624
+ const ch = this.channels.get(obj) ?? {};
625
+ const current = ch.rotate ?? this.base.get(obj).rotate;
626
+ ch.rotate = lerp(current, rad, u);
627
+ this.channels.set(obj, ch);
628
+ },
629
+ by: (deltaExpr, u) => {
630
+ const delta = this.resolveNum(deltaExpr);
631
+ const ch = this.channels.get(obj) ?? {};
632
+ const current = ch.rotate ?? this.base.get(obj).rotate;
633
+ ch.rotate = current + delta * u;
634
+ this.channels.set(obj, ch);
635
+ }
636
+ };
637
+ }
638
+ pin(obj, anchor) {
639
+ if (isGroup(obj)) {
640
+ return {
641
+ to: (posExpr, u) => {
642
+ const pos = this.resolveVec2(posExpr);
643
+ const currentAnchor = this.resolveGroupAnchor(obj, anchor);
644
+ const targetAnchor = vec.lerp(currentAnchor, pos, u);
645
+ const delta = vec.sub(targetAnchor, currentAnchor);
646
+ for (const member of obj.members) {
647
+ const existingPin = this.pins.get(member);
648
+ const memberAnchor = existingPin?.anchor ?? "origin";
649
+ const currentAnchorPos = this.getCurrentAnchorPosition(member, memberAnchor);
650
+ const newPos = vec.add(currentAnchorPos, delta);
651
+ this.pins.set(member, {
652
+ anchor: memberAnchor,
653
+ world: newPos
654
+ });
655
+ }
656
+ },
657
+ by: (deltaExpr, u) => {
658
+ const delta = this.resolveVec2(deltaExpr);
659
+ const interpolatedDelta = vec.lerp([0, 0], delta, u);
660
+ for (const member of obj.members) {
661
+ const existingPin = this.pins.get(member);
662
+ const memberAnchor = existingPin?.anchor ?? "origin";
663
+ const currentAnchorPos = this.getCurrentAnchorPosition(member, memberAnchor);
664
+ const newPos = vec.add(currentAnchorPos, interpolatedDelta);
665
+ this.pins.set(member, {
666
+ anchor: memberAnchor,
667
+ world: newPos
668
+ });
669
+ }
670
+ }
671
+ };
672
+ }
673
+ return {
674
+ to: (posExpr, u) => {
675
+ const pos = this.resolveVec2(posExpr);
676
+ const current = this.getCurrentAnchorPosition(obj, anchor);
677
+ const world = vec.lerp(current, pos, u);
678
+ this.pins.set(obj, {
679
+ anchor,
680
+ world
681
+ });
682
+ },
683
+ by: (deltaExpr, u) => {
684
+ const delta = this.resolveVec2(deltaExpr);
685
+ const current = this.getCurrentAnchorPosition(obj, anchor);
686
+ const world = vec.add(current, vec.lerp([0, 0], delta, u));
687
+ this.pins.set(obj, {
688
+ anchor,
689
+ world
690
+ });
691
+ }
692
+ };
693
+ }
694
+ opacity(obj) {
695
+ if (isGroup(obj)) {
696
+ return {
697
+ to: (vExpr, u) => {
698
+ const v = this.resolveNum(vExpr);
699
+ for (const member of obj.members) {
700
+ const ch = this.channels.get(member) ?? {};
701
+ const current = ch.opacity ?? 1;
702
+ ch.opacity = lerp(current, v, u);
703
+ this.channels.set(member, ch);
704
+ }
705
+ },
706
+ by: (deltaExpr, u) => {
707
+ const delta = this.resolveNum(deltaExpr);
708
+ for (const member of obj.members) {
709
+ const ch = this.channels.get(member) ?? {};
710
+ const current = ch.opacity ?? 1;
711
+ ch.opacity = current * lerp(1, 1 + delta, u);
712
+ this.channels.set(member, ch);
713
+ }
714
+ }
715
+ };
716
+ }
717
+ return {
718
+ to: (vExpr, u) => {
719
+ const v = this.resolveNum(vExpr);
720
+ const ch = this.channels.get(obj) ?? {};
721
+ const current = ch.opacity ?? 1;
722
+ ch.opacity = lerp(current, v, u);
723
+ this.channels.set(obj, ch);
724
+ },
725
+ by: (deltaExpr, u) => {
726
+ const delta = this.resolveNum(deltaExpr);
727
+ const ch = this.channels.get(obj) ?? {};
728
+ const current = ch.opacity ?? 1;
729
+ ch.opacity = current * lerp(1, 1 + delta, u);
730
+ this.channels.set(obj, ch);
731
+ }
732
+ };
733
+ }
734
+ group(...objects) {
735
+ const groupObj = {
736
+ id: /* @__PURE__ */ Symbol("group"),
737
+ members: objects,
738
+ isGroup: true
739
+ };
740
+ const self = this;
741
+ const builder = {
742
+ ...groupObj,
743
+ pin: (anchor) => {
744
+ const methods = self.pin(groupObj, anchor);
745
+ return {
746
+ to: (pos, u) => {
747
+ methods.to(pos, u);
748
+ self.trackGroupDebug(groupObj, u, { anchor, target: pos });
749
+ },
750
+ by: (delta, u) => {
751
+ methods.by(delta, u);
752
+ self.trackGroupDebug(groupObj, u);
753
+ }
754
+ };
755
+ },
756
+ scale: () => {
757
+ const methods = self.scale(groupObj);
758
+ return {
759
+ to: (v, u) => {
760
+ methods.to(v, u);
761
+ self.trackGroupDebug(groupObj, u);
762
+ },
763
+ by: (delta, u) => {
764
+ methods.by(delta, u);
765
+ self.trackGroupDebug(groupObj, u);
766
+ }
767
+ };
768
+ },
769
+ rotate: () => {
770
+ const methods = self.rotate(groupObj);
771
+ return {
772
+ to: (v, u) => {
773
+ methods.to(v, u);
774
+ self.trackGroupDebug(groupObj, u);
775
+ },
776
+ by: (delta, u) => {
777
+ methods.by(delta, u);
778
+ self.trackGroupDebug(groupObj, u);
779
+ }
780
+ };
781
+ },
782
+ opacity: () => {
783
+ const methods = self.opacity(groupObj);
784
+ return {
785
+ to: (v, u) => {
786
+ methods.to(v, u);
787
+ self.trackGroupDebug(groupObj, u);
788
+ },
789
+ by: (delta, u) => {
790
+ methods.by(delta, u);
791
+ self.trackGroupDebug(groupObj, u);
792
+ }
793
+ };
794
+ }
795
+ };
796
+ return builder;
797
+ }
798
+ trackGroupDebug(groupObj, u, pin) {
799
+ if (u < 1) {
800
+ let entry = this.debugGroups.find(
801
+ (g) => g.kind === "group" && g.members === groupObj.members
802
+ );
803
+ if (!entry) {
804
+ entry = {
805
+ members: groupObj.members,
806
+ kind: "group"
807
+ };
808
+ this.debugGroups.push(entry);
809
+ }
810
+ if (pin) {
811
+ entry.pin = pin;
812
+ }
813
+ return entry;
814
+ }
815
+ return null;
816
+ }
817
+ /** Flatten objects for final result (extracts all SceneObjects from groups) */
818
+ flattenToSceneObjects(objects) {
819
+ const result = [];
820
+ for (const obj of objects) {
821
+ if (isGroup(obj)) {
822
+ result.push(...obj.members);
823
+ } else {
824
+ result.push(obj);
825
+ }
826
+ }
827
+ return result;
828
+ }
829
+ /** Get bounds for an ObjRef (single object or group) */
830
+ getObjRefBounds(obj) {
831
+ if (isGroup(obj)) {
832
+ return this.computeGroupWorldBounds(obj);
833
+ }
834
+ return this.computeWorldBounds(obj);
835
+ }
836
+ /** Get the top-left position of an ObjRef (bounds position, not pin position) */
837
+ getObjRefPosition(obj) {
838
+ const bounds = this.getObjRefBounds(obj);
839
+ return [bounds.x, bounds.y];
840
+ }
841
+ /** Move an ObjRef by a delta (moves all members of a group together, preserving pin anchors) */
842
+ moveObjRef(obj, fromPos, toPos, u) {
843
+ const delta = vec.sub(toPos, fromPos);
844
+ const interpolatedDelta = vec.lerp([0, 0], delta, u);
845
+ if (isGroup(obj)) {
846
+ for (const member of obj.members) {
847
+ const existingPin = this.pins.get(member);
848
+ const anchor = existingPin?.anchor ?? "origin";
849
+ const currentAnchorPos = this.getCurrentAnchorPosition(member, anchor);
850
+ const newPos = vec.add(currentAnchorPos, interpolatedDelta);
851
+ this.pins.set(member, {
852
+ anchor,
853
+ world: newPos
854
+ });
855
+ }
856
+ } else {
857
+ const existingPin = this.pins.get(obj);
858
+ const anchor = existingPin?.anchor ?? "origin";
859
+ const currentAnchorPos = this.getCurrentAnchorPosition(obj, anchor);
860
+ const newPos = vec.add(currentAnchorPos, interpolatedDelta);
861
+ this.pins.set(obj, {
862
+ anchor,
863
+ world: newPos
864
+ });
865
+ }
866
+ }
867
+ alignOnCrossAxis(start, size, maxSize, align) {
868
+ if (align === "center") {
869
+ return start + (maxSize - size) / 2;
870
+ }
871
+ if (align === "topLeft" || align === "left" || align === "top") {
872
+ return start;
873
+ }
874
+ if (align === "bottomRight" || align === "right" || align === "bottom") {
875
+ return start + maxSize - size;
876
+ }
877
+ if (align === "baselineLeft" || align === "baseline") {
878
+ return start + maxSize - size;
879
+ }
880
+ return start;
881
+ }
882
+ calculateLayoutPositions(objects, axis, align, gap) {
883
+ if (!this.scene) throw new Error("Scene not set");
884
+ const boundsList = objects.map((obj) => this.getObjRefBounds(obj));
885
+ const startX = Math.min(...boundsList.map((b) => b.x));
886
+ const startY = Math.min(...boundsList.map((b) => b.y));
887
+ const maxH = Math.max(...boundsList.map((b) => b.h));
888
+ const maxW = Math.max(...boundsList.map((b) => b.w));
889
+ const targets = [];
890
+ let cursor = axis === "x" ? startX : startY;
891
+ for (let i = 0; i < objects.length; i++) {
892
+ const b = boundsList[i];
893
+ let x, y;
894
+ if (axis === "x") {
895
+ x = cursor;
896
+ y = this.alignOnCrossAxis(startY, b.h, maxH, align);
897
+ cursor += b.w + gap;
898
+ } else {
899
+ x = this.alignOnCrossAxis(startX, b.w, maxW, align);
900
+ y = cursor;
901
+ cursor += b.h + gap;
902
+ }
903
+ targets.push([x, y]);
904
+ }
905
+ return targets;
906
+ }
907
+ layout(...args) {
908
+ let opts = {};
909
+ let objects;
910
+ const last = args[args.length - 1];
911
+ if (last && typeof last === "object" && !("id" in last)) {
912
+ opts = last;
913
+ objects = args.slice(0, -1);
914
+ } else {
915
+ objects = args;
916
+ }
917
+ const axis = opts.axis ?? "x";
918
+ const align = opts.align ?? "center";
919
+ const gap = opts.gap ?? 0;
920
+ return {
921
+ to: (u) => {
922
+ const targets = this.calculateLayoutPositions(objects, axis, align, gap);
923
+ for (let i = 0; i < objects.length; i++) {
924
+ const obj = objects[i];
925
+ const currentPos = this.getObjRefPosition(obj);
926
+ const targetPos = targets[i];
927
+ this.moveObjRef(obj, currentPos, targetPos, u);
928
+ }
929
+ const allMembers = this.flattenToSceneObjects(objects);
930
+ const groupObj = {
931
+ id: /* @__PURE__ */ Symbol("layout-group"),
932
+ members: allMembers,
933
+ isGroup: true
934
+ };
935
+ let debugEntry = null;
936
+ if (u < 1) {
937
+ debugEntry = {
938
+ members: allMembers,
939
+ kind: "layout"
940
+ };
941
+ this.debugGroups.push(debugEntry);
942
+ }
943
+ const self = this;
944
+ const builder = {
945
+ ...groupObj,
946
+ pin: (anchor) => {
947
+ const methods = self.pin(groupObj, anchor);
948
+ return {
949
+ to: (pos, pinU) => {
950
+ methods.to(pos, pinU);
951
+ if (debugEntry) {
952
+ debugEntry.pin = { anchor, target: pos };
953
+ }
954
+ },
955
+ by: (delta, pinU) => {
956
+ methods.by(delta, pinU);
957
+ }
958
+ };
959
+ },
960
+ scale: () => self.scale(groupObj),
961
+ rotate: () => self.rotate(groupObj),
962
+ opacity: () => self.opacity(groupObj)
963
+ };
964
+ return builder;
965
+ }
966
+ };
967
+ }
968
+ inline(...args) {
969
+ const { objects, gap } = this.parseLayoutArgs(args);
970
+ return this.layout(...objects, { axis: "x", align: "baselineLeft", gap });
971
+ }
972
+ stack(...args) {
973
+ const { objects, gap } = this.parseLayoutArgs(args);
974
+ return this.layout(...objects, { axis: "y", align: "center", gap });
975
+ }
976
+ paragraph(...args) {
977
+ const { objects, gap } = this.parseLayoutArgs(args);
978
+ return this.layout(...objects, { axis: "y", align: "left", gap });
979
+ }
980
+ /** Parse layout args - last arg may be a gap number */
981
+ parseLayoutArgs(args) {
982
+ const last = args[args.length - 1];
983
+ if (typeof last === "number") {
984
+ return { objects: args.slice(0, -1), gap: last };
985
+ }
986
+ return { objects: args, gap: 0 };
987
+ }
988
+ /** Get anchor offset within bounds */
989
+ getAnchorOffsetFromRect(bounds, anchor) {
990
+ let ox = 0, oy = 0;
991
+ if (anchor === "top" || anchor === "center" || anchor === "bottom" || anchor === "baseline") {
992
+ ox = bounds.w / 2;
993
+ } else if (anchor === "topRight" || anchor === "right" || anchor === "bottomRight") {
994
+ ox = bounds.w;
995
+ }
996
+ if (anchor === "left" || anchor === "center" || anchor === "right") {
997
+ oy = bounds.h / 2;
998
+ } else if (anchor === "bottomLeft" || anchor === "bottom" || anchor === "bottomRight" || anchor === "baselineLeft" || anchor === "baseline") {
999
+ oy = bounds.h;
1000
+ }
1001
+ return [ox, oy];
1002
+ }
1003
+ /** Get anchor position in world space for an ObjRef */
1004
+ getObjRefAnchorPosition(obj, anchor) {
1005
+ const bounds = this.getObjRefBounds(obj);
1006
+ const offset = this.getAnchorOffsetFromRect(bounds, anchor);
1007
+ return [bounds.x + offset[0], bounds.y + offset[1]];
1008
+ }
1009
+ align(...args) {
1010
+ let opts = {};
1011
+ let objects;
1012
+ const last = args[args.length - 1];
1013
+ if (last && typeof last === "object" && !("id" in last)) {
1014
+ opts = last;
1015
+ objects = args.slice(0, -1);
1016
+ } else {
1017
+ objects = args;
1018
+ }
1019
+ const anchor = opts.anchor ?? "center";
1020
+ return {
1021
+ to: (u) => {
1022
+ if (!this.scene || objects.length === 0) {
1023
+ const allMembers2 = this.flattenToSceneObjects(objects);
1024
+ return this.group(...allMembers2);
1025
+ }
1026
+ const targetAnchorPos = this.getObjRefAnchorPosition(objects[0], anchor);
1027
+ for (let i = 1; i < objects.length; i++) {
1028
+ const obj = objects[i];
1029
+ const bounds = this.getObjRefBounds(obj);
1030
+ const anchorOffset = this.getAnchorOffsetFromRect(bounds, anchor);
1031
+ const targetPos = [
1032
+ targetAnchorPos[0] - anchorOffset[0],
1033
+ targetAnchorPos[1] - anchorOffset[1]
1034
+ ];
1035
+ const currentPos = this.getObjRefPosition(obj);
1036
+ this.moveObjRef(obj, currentPos, targetPos, u);
1037
+ }
1038
+ const allMembers = this.flattenToSceneObjects(objects);
1039
+ const groupObj = {
1040
+ id: /* @__PURE__ */ Symbol("align-group"),
1041
+ members: allMembers,
1042
+ isGroup: true
1043
+ };
1044
+ let debugEntry = null;
1045
+ if (u < 1) {
1046
+ debugEntry = {
1047
+ members: allMembers,
1048
+ kind: "layout"
1049
+ };
1050
+ this.debugGroups.push(debugEntry);
1051
+ }
1052
+ const self = this;
1053
+ return {
1054
+ ...groupObj,
1055
+ pin: (pinAnchor) => {
1056
+ const methods = self.pin(groupObj, pinAnchor);
1057
+ return {
1058
+ to: (pos, pinU) => {
1059
+ methods.to(pos, pinU);
1060
+ if (debugEntry) {
1061
+ debugEntry.pin = { anchor: pinAnchor, target: pos };
1062
+ }
1063
+ },
1064
+ by: (delta, pinU) => {
1065
+ methods.by(delta, pinU);
1066
+ }
1067
+ };
1068
+ },
1069
+ scale: () => self.scale(groupObj),
1070
+ rotate: () => self.rotate(groupObj),
1071
+ opacity: () => self.opacity(groupObj)
1072
+ };
1073
+ }
1074
+ };
1075
+ }
1076
+ center(...objects) {
1077
+ return this.align(...objects, { anchor: "center" });
1078
+ }
1079
+ solve(scene) {
1080
+ const resolved = /* @__PURE__ */ new Map();
1081
+ const opacity = /* @__PURE__ */ new Map();
1082
+ for (const obj of scene.objects()) {
1083
+ const base = scene.getInitialTransform(obj);
1084
+ const ch = this.channels.get(obj);
1085
+ const scale = ch?.scale ?? base.scale;
1086
+ const scaleX = ch?.scaleX;
1087
+ const scaleY = ch?.scaleY;
1088
+ const rotate = ch?.rotate ?? base.rotate;
1089
+ let translate = base.translate ?? [0, 0];
1090
+ const pin = this.pins.get(obj);
1091
+ if (pin) {
1092
+ const local = scene.resolveAnchor(obj, pin.anchor);
1093
+ const transformed = applySR(local, scale, rotate, scaleX ?? 1, scaleY ?? 1);
1094
+ translate = vec.sub(pin.world, transformed);
1095
+ }
1096
+ resolved.set(obj, {
1097
+ translate,
1098
+ rotate: rotate ?? 0,
1099
+ scale: scale ?? 1,
1100
+ scaleX,
1101
+ scaleY
1102
+ });
1103
+ opacity.set(obj, ch?.opacity ?? 1);
1104
+ }
1105
+ return {
1106
+ getTransform: (o) => resolved.get(o),
1107
+ getOpacity: (o) => opacity.get(o) ?? 1
1108
+ };
1109
+ }
1110
+ }
1111
+ function getAnchorOffset(anchor, bounds) {
1112
+ const { x, y, w, h } = bounds;
1113
+ const left = x;
1114
+ const right = x + w;
1115
+ const top = y;
1116
+ const bottom = y + h;
1117
+ const centerX = x + w / 2;
1118
+ const centerY = y + h / 2;
1119
+ switch (anchor) {
1120
+ case "origin":
1121
+ return [0, 0];
1122
+ case "topLeft":
1123
+ return [left, top];
1124
+ case "top":
1125
+ return [centerX, top];
1126
+ case "topRight":
1127
+ return [right, top];
1128
+ case "left":
1129
+ return [left, centerY];
1130
+ case "center":
1131
+ return [centerX, centerY];
1132
+ case "right":
1133
+ return [right, centerY];
1134
+ case "bottomLeft":
1135
+ return [left, bottom];
1136
+ case "bottom":
1137
+ return [centerX, bottom];
1138
+ case "bottomRight":
1139
+ return [right, bottom];
1140
+ case "baselineLeft":
1141
+ return [left, bottom];
1142
+ case "baseline":
1143
+ return [centerX, bottom];
1144
+ default:
1145
+ return [0, 0];
1146
+ }
1147
+ }
1148
+ function renderDebugOverlay(g, scene, solved, debugGroups, pins, dpr) {
1149
+ g.save();
1150
+ g.scale(dpr, dpr);
1151
+ const debugInfos = [];
1152
+ for (const obj of scene.objects()) {
1153
+ const transform = solved.getTransform(obj);
1154
+ if (!transform || !transform.translate) continue;
1155
+ const bounds = scene.getBounds(obj);
1156
+ const opacity = solved.getOpacity(obj);
1157
+ debugInfos.push({ obj, transform, bounds, opacity });
1158
+ }
1159
+ for (const groupInfo of debugGroups) {
1160
+ const { members, kind, pin } = groupInfo;
1161
+ let minX = Infinity, minY = Infinity;
1162
+ let maxX = -Infinity, maxY = -Infinity;
1163
+ for (const member of members) {
1164
+ const transform = solved.getTransform(member);
1165
+ if (!transform || !transform.translate) continue;
1166
+ const memberBounds = scene.getBounds(member);
1167
+ const x = transform.translate[0] + memberBounds.x;
1168
+ const y = transform.translate[1] + memberBounds.y;
1169
+ minX = Math.min(minX, x);
1170
+ minY = Math.min(minY, y);
1171
+ maxX = Math.max(maxX, x + memberBounds.w * transform.scale);
1172
+ maxY = Math.max(maxY, y + memberBounds.h * transform.scale);
1173
+ }
1174
+ if (!isFinite(minX)) continue;
1175
+ const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1176
+ const color = kind === "group" ? "rgba(255, 200, 0" : "rgba(255, 0, 255";
1177
+ g.strokeStyle = color + ", 0.7)";
1178
+ g.lineWidth = 2;
1179
+ g.setLineDash([6, 4]);
1180
+ g.strokeRect(bounds.x, bounds.y, bounds.w, bounds.h);
1181
+ g.font = "10px monospace";
1182
+ g.fillStyle = color + ", 0.9)";
1183
+ g.fillText(kind, bounds.x + 2, bounds.y - 3);
1184
+ if (pin) {
1185
+ let anchorX = bounds.x, anchorY = bounds.y;
1186
+ const anchor = pin.anchor;
1187
+ if (anchor === "top" || anchor === "center" || anchor === "bottom" || anchor === "baseline") {
1188
+ anchorX = bounds.x + bounds.w / 2;
1189
+ } else if (anchor === "topRight" || anchor === "right" || anchor === "bottomRight") {
1190
+ anchorX = bounds.x + bounds.w;
1191
+ }
1192
+ if (anchor === "left" || anchor === "center" || anchor === "right") {
1193
+ anchorY = bounds.y + bounds.h / 2;
1194
+ } else if (anchor === "bottomLeft" || anchor === "bottom" || anchor === "bottomRight" || anchor === "baselineLeft" || anchor === "baseline") {
1195
+ anchorY = bounds.y + bounds.h;
1196
+ }
1197
+ g.fillStyle = color + ", 0.9)";
1198
+ g.beginPath();
1199
+ g.moveTo(anchorX, anchorY - 5);
1200
+ g.lineTo(anchorX + 5, anchorY);
1201
+ g.lineTo(anchorX, anchorY + 5);
1202
+ g.lineTo(anchorX - 5, anchorY);
1203
+ g.closePath();
1204
+ g.fill();
1205
+ const [tx, ty] = pin.target;
1206
+ g.strokeStyle = color + ", 0.9)";
1207
+ g.lineWidth = 2;
1208
+ g.setLineDash([]);
1209
+ g.beginPath();
1210
+ g.moveTo(tx - 6, ty - 6);
1211
+ g.lineTo(tx + 6, ty + 6);
1212
+ g.moveTo(tx + 6, ty - 6);
1213
+ g.lineTo(tx - 6, ty + 6);
1214
+ g.stroke();
1215
+ g.strokeStyle = color + ", 0.4)";
1216
+ g.lineWidth = 1;
1217
+ g.setLineDash([3, 3]);
1218
+ g.beginPath();
1219
+ g.moveTo(anchorX, anchorY);
1220
+ g.lineTo(tx, ty);
1221
+ g.stroke();
1222
+ g.font = "9px monospace";
1223
+ g.fillStyle = color + ", 0.9)";
1224
+ g.fillText(anchor, anchorX + 7, anchorY - 2);
1225
+ }
1226
+ }
1227
+ for (const { transform, bounds } of debugInfos) {
1228
+ g.save();
1229
+ g.translate(transform.translate[0], transform.translate[1]);
1230
+ g.rotate(transform.rotate);
1231
+ const sx = (transform.scaleX ?? 1) * transform.scale;
1232
+ const sy = (transform.scaleY ?? 1) * transform.scale;
1233
+ g.scale(sx, sy);
1234
+ const avgScale = (sx + sy) / 2;
1235
+ g.strokeStyle = "rgba(0, 200, 255, 0.8)";
1236
+ g.lineWidth = 1 / avgScale;
1237
+ g.setLineDash([4 / avgScale, 4 / avgScale]);
1238
+ g.strokeRect(bounds.x, bounds.y, bounds.w, bounds.h);
1239
+ g.restore();
1240
+ }
1241
+ for (const { obj, transform, bounds } of debugInfos) {
1242
+ const x = transform.translate[0];
1243
+ const y = transform.translate[1];
1244
+ const sx = (transform.scaleX ?? 1) * transform.scale;
1245
+ const sy = (transform.scaleY ?? 1) * transform.scale;
1246
+ const cos = Math.cos(transform.rotate);
1247
+ const sin = Math.sin(transform.rotate);
1248
+ g.fillStyle = "rgba(255, 100, 100, 0.9)";
1249
+ g.beginPath();
1250
+ g.arc(x, y, 4, 0, Math.PI * 2);
1251
+ g.fill();
1252
+ const centerOffX = (bounds.x + bounds.w / 2) * sx;
1253
+ const centerOffY = (bounds.y + bounds.h / 2) * sy;
1254
+ const centerX = x + centerOffX * cos - centerOffY * sin;
1255
+ const centerY = y + centerOffX * sin + centerOffY * cos;
1256
+ g.fillStyle = "rgba(100, 255, 100, 0.9)";
1257
+ g.beginPath();
1258
+ g.arc(centerX, centerY, 3, 0, Math.PI * 2);
1259
+ g.fill();
1260
+ g.strokeStyle = "rgba(255, 100, 100, 0.6)";
1261
+ g.lineWidth = 1;
1262
+ g.setLineDash([]);
1263
+ g.beginPath();
1264
+ g.moveTo(x - 8, y);
1265
+ g.lineTo(x + 8, y);
1266
+ g.moveTo(x, y - 8);
1267
+ g.lineTo(x, y + 8);
1268
+ g.stroke();
1269
+ const baseline = scene.getBaseline(obj);
1270
+ if (baseline !== null) {
1271
+ g.save();
1272
+ g.translate(x, y);
1273
+ g.rotate(transform.rotate);
1274
+ g.scale(transform.scale, transform.scale);
1275
+ g.strokeStyle = "rgba(255, 165, 0, 0.8)";
1276
+ g.lineWidth = 1 / transform.scale;
1277
+ g.setLineDash([2 / transform.scale, 2 / transform.scale]);
1278
+ g.beginPath();
1279
+ g.moveTo(bounds.x - 5, baseline);
1280
+ g.lineTo(bounds.x + bounds.w + 5, baseline);
1281
+ g.stroke();
1282
+ g.restore();
1283
+ }
1284
+ const pin = pins.get(obj);
1285
+ if (pin) {
1286
+ const [anchorOffX, anchorOffY] = getAnchorOffset(pin.anchor, bounds);
1287
+ const scaledOffX = anchorOffX * sx;
1288
+ const scaledOffY = anchorOffY * sy;
1289
+ const anchorX = x + scaledOffX * cos - scaledOffY * sin;
1290
+ const anchorY = y + scaledOffX * sin + scaledOffY * cos;
1291
+ const [tx, ty] = pin.world;
1292
+ g.fillStyle = "rgba(0, 200, 255, 0.9)";
1293
+ g.beginPath();
1294
+ g.moveTo(anchorX, anchorY - 4);
1295
+ g.lineTo(anchorX + 4, anchorY);
1296
+ g.lineTo(anchorX, anchorY + 4);
1297
+ g.lineTo(anchorX - 4, anchorY);
1298
+ g.closePath();
1299
+ g.fill();
1300
+ g.strokeStyle = "rgba(0, 200, 255, 0.9)";
1301
+ g.lineWidth = 2;
1302
+ g.setLineDash([]);
1303
+ g.beginPath();
1304
+ g.moveTo(tx - 5, ty - 5);
1305
+ g.lineTo(tx + 5, ty + 5);
1306
+ g.moveTo(tx + 5, ty - 5);
1307
+ g.lineTo(tx - 5, ty + 5);
1308
+ g.stroke();
1309
+ g.strokeStyle = "rgba(0, 200, 255, 0.4)";
1310
+ g.lineWidth = 1;
1311
+ g.setLineDash([3, 3]);
1312
+ g.beginPath();
1313
+ g.moveTo(anchorX, anchorY);
1314
+ g.lineTo(tx, ty);
1315
+ g.stroke();
1316
+ g.font = "9px monospace";
1317
+ g.fillStyle = "rgba(0, 200, 255, 0.9)";
1318
+ g.fillText(pin.anchor, anchorX + 6, anchorY - 4);
1319
+ }
1320
+ }
1321
+ g.restore();
1322
+ }
1323
+ function makePlayer(canvas, opts) {
1324
+ const ctx = new AnimCtxImpl();
1325
+ const { scene, timeline } = opts;
1326
+ const duration = timeline.duration;
1327
+ let isPlaying = false;
1328
+ let currentTime = 0;
1329
+ let startTime = 0;
1330
+ let animationFrameId = null;
1331
+ let onTimeUpdate = null;
1332
+ let onError = null;
1333
+ let hasError = false;
1334
+ let debugMode = false;
1335
+ function render(timeMs, throwOnError = false) {
1336
+ try {
1337
+ ctx.reset(scene);
1338
+ timeline.evaluate(timeMs, ctx);
1339
+ const solved = ctx.solve(scene);
1340
+ scene.render(solved, canvas);
1341
+ if (debugMode) {
1342
+ const g = canvas.getContext("2d");
1343
+ const dpr = window.devicePixelRatio || 1;
1344
+ renderDebugOverlay(g, scene, solved, ctx.getDebugGroups(), ctx.getDebugPins(), dpr);
1345
+ }
1346
+ return true;
1347
+ } catch (e) {
1348
+ hasError = true;
1349
+ isPlaying = false;
1350
+ if (animationFrameId !== null) {
1351
+ cancelAnimationFrame(animationFrameId);
1352
+ animationFrameId = null;
1353
+ }
1354
+ const error = e instanceof Error ? e : new Error(String(e));
1355
+ if (throwOnError || !onError) {
1356
+ throw error;
1357
+ }
1358
+ onError(error);
1359
+ return false;
1360
+ }
1361
+ }
1362
+ function frame(now) {
1363
+ if (!isPlaying || hasError) return;
1364
+ currentTime = now - startTime;
1365
+ if (currentTime >= duration) {
1366
+ currentTime = duration;
1367
+ isPlaying = false;
1368
+ render(currentTime);
1369
+ onTimeUpdate?.(currentTime);
1370
+ return;
1371
+ }
1372
+ if (render(currentTime)) {
1373
+ onTimeUpdate?.(currentTime);
1374
+ animationFrameId = requestAnimationFrame(frame);
1375
+ }
1376
+ }
1377
+ render(0, true);
1378
+ return {
1379
+ get isPlaying() {
1380
+ return isPlaying;
1381
+ },
1382
+ get currentTime() {
1383
+ return currentTime;
1384
+ },
1385
+ get duration() {
1386
+ return duration;
1387
+ },
1388
+ get onTimeUpdate() {
1389
+ return onTimeUpdate;
1390
+ },
1391
+ set onTimeUpdate(callback) {
1392
+ onTimeUpdate = callback;
1393
+ },
1394
+ get onError() {
1395
+ return onError;
1396
+ },
1397
+ set onError(callback) {
1398
+ onError = callback;
1399
+ },
1400
+ get debug() {
1401
+ return debugMode;
1402
+ },
1403
+ set debug(value) {
1404
+ debugMode = value;
1405
+ if (!isPlaying) {
1406
+ render(currentTime);
1407
+ }
1408
+ },
1409
+ play() {
1410
+ if (isPlaying || hasError) return;
1411
+ if (currentTime >= duration) {
1412
+ currentTime = 0;
1413
+ }
1414
+ isPlaying = true;
1415
+ startTime = performance.now() - currentTime;
1416
+ animationFrameId = requestAnimationFrame(frame);
1417
+ },
1418
+ pause() {
1419
+ isPlaying = false;
1420
+ if (animationFrameId !== null) {
1421
+ cancelAnimationFrame(animationFrameId);
1422
+ animationFrameId = null;
1423
+ }
1424
+ },
1425
+ stop() {
1426
+ this.pause();
1427
+ currentTime = 0;
1428
+ render(0);
1429
+ onTimeUpdate?.(0);
1430
+ },
1431
+ seek(timeMs) {
1432
+ if (hasError) return;
1433
+ currentTime = Math.max(0, Math.min(duration, timeMs));
1434
+ startTime = performance.now() - currentTime;
1435
+ render(currentTime);
1436
+ onTimeUpdate?.(currentTime);
1437
+ },
1438
+ renderAt(timeMs) {
1439
+ if (hasError) return;
1440
+ render(Math.max(0, Math.min(duration, timeMs)));
1441
+ },
1442
+ dispose() {
1443
+ this.pause();
1444
+ }
1445
+ };
1446
+ }
1447
+ export {
1448
+ AnimCtxImpl as A,
1449
+ Screen as S,
1450
+ Vec as V,
1451
+ isExpr as a,
1452
+ angle as b,
1453
+ isGroup as i,
1454
+ lerp as l,
1455
+ makePlayer as m,
1456
+ vec as v
1457
+ };
1458
+ //# sourceMappingURL=player-MRRNy8I9.js.map