@motion-script/player 0.2.4 → 1.0.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.
package/dist/App.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKnE,wBAAgB,SAAS,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,aAAa,CAAA;CAAE,+BAiBlG"}
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKnE,wBAAgB,SAAS,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,aAAa,CAAA;CAAE,+BAgBlG"}
@@ -1 +1 @@
1
- {"version":3,"file":"scene-panel.d.ts","sourceRoot":"","sources":["../../../src/components/layout/scene-panel.tsx"],"names":[],"mappings":"AAMA,wBAAgB,UAAU,gCAkHzB"}
1
+ {"version":3,"file":"scene-panel.d.ts","sourceRoot":"","sources":["../../../src/components/layout/scene-panel.tsx"],"names":[],"mappings":"AAMA,wBAAgB,UAAU,gCA8HzB"}
@@ -9,6 +9,7 @@ export declare const RULER_HEIGHT = 28;
9
9
  export declare const SCENE_ROW_HEIGHT = 28;
10
10
  export declare const INDENT_PX = 16;
11
11
  export declare const BAR_PADDING_Y = 5;
12
+ export declare const BAR_MARGIN_X = 2;
12
13
  export declare const FRAME_MODE_PX_THRESHOLD = 8;
13
14
  export declare const ROW_OVERSCAN = 4;
14
15
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/components/timeline/constants.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAC5C,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,eAAe,MAAM,CAAC;AACnC,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,eAAO,MAAM,aAAa,IAAI,CAAC;AAG/B,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAGzC,eAAO,MAAM,YAAY,IAAI,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/components/timeline/constants.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAC5C,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,eAAe,MAAM,CAAC;AACnC,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,eAAO,MAAM,aAAa,IAAI,CAAC;AAI/B,eAAO,MAAM,YAAY,IAAI,CAAC;AAG9B,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAGzC,eAAO,MAAM,YAAY,IAAI,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../../../src/components/timeline/timeline.tsx"],"names":[],"mappings":"AAiCA,KAAK,kBAAkB,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,UAAgC,GACjC,EAAE,kBAAkB,+BAqgBpB"}
1
+ {"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../../../src/components/timeline/timeline.tsx"],"names":[],"mappings":"AAkCA,KAAK,kBAAkB,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,UAAgC,GACjC,EAAE,kBAAkB,+BAqgBpB"}
@@ -1563,6 +1563,17 @@ function resolveShadowArray(prop, previous) {
1563
1563
  }
1564
1564
  //#endregion
1565
1565
  //#region ../core/dist/attributes/shape/stroke/mapper.js
1566
+ /** Resolve a loose `align` prop to a clamped number in [-1, 1]. */
1567
+ function resolveStrokeAlign(align, fallback) {
1568
+ if (align == null) return fallback;
1569
+ if (typeof align === "number") return Math.max(-1, Math.min(1, align));
1570
+ switch (align) {
1571
+ case "inside": return -1;
1572
+ case "center": return 0;
1573
+ case "outside": return 1;
1574
+ }
1575
+ return fallback;
1576
+ }
1566
1577
  function resolveStroke(prop, previous) {
1567
1578
  let dash;
1568
1579
  if (prop.dash != null) {
@@ -1573,7 +1584,8 @@ function resolveStroke(prop, previous) {
1573
1584
  weight: prop.weight ?? previous?.weight ?? 1,
1574
1585
  fill: prop.fill != null ? resolveFill(prop.fill) : previous?.fill ?? resolveFill("transparent"),
1575
1586
  dash,
1576
- dashOffset: prop.dashOffset ?? previous?.dashOffset ?? 0
1587
+ dashOffset: prop.dashOffset ?? previous?.dashOffset ?? 0,
1588
+ align: resolveStrokeAlign(prop.align, previous?.align ?? -1)
1577
1589
  };
1578
1590
  }
1579
1591
  function resolveStrokeArray(prop, previous) {
@@ -2125,6 +2137,10 @@ var AssetManager = class {
2125
2137
  lastAudioRequestKey = "";
2126
2138
  lastAudioSrcs = /* @__PURE__ */ new Set();
2127
2139
  audioFetching = /* @__PURE__ */ new Map();
2140
+ /** Disposers for loaders whose load has resolved, keyed by loader key. */
2141
+ loaderDisposers = /* @__PURE__ */ new Map();
2142
+ /** In-flight loader runs, keyed by loader key, to dedupe concurrent calls. */
2143
+ loaderInFlight = /* @__PURE__ */ new Map();
2128
2144
  constructor(precomp, storageAdapter, audioDevice) {
2129
2145
  this.precomp = precomp;
2130
2146
  this.storageAdapter = storageAdapter;
@@ -2136,7 +2152,14 @@ var AssetManager = class {
2136
2152
  */
2137
2153
  async loadAt(frame) {
2138
2154
  const jobs = [];
2139
- for (const [key, track] of this.precomp.assets) if (track.cacheAt <= frame && track.record.endFrame >= frame) jobs.push(this.storageAdapter.loadAsset(key, track.record));
2155
+ for (const [key, track] of this.precomp.assets) {
2156
+ if (track.record.type === "loader") {
2157
+ const job = this.syncLoader(key, track, frame);
2158
+ if (job) jobs.push(job);
2159
+ continue;
2160
+ }
2161
+ if (track.cacheAt <= frame && track.record.endFrame >= frame) jobs.push(this.storageAdapter.loadAsset(key, track.record));
2162
+ }
2140
2163
  await Promise.all(jobs);
2141
2164
  this.syncAudio(frame);
2142
2165
  }
@@ -2145,9 +2168,54 @@ var AssetManager = class {
2145
2168
  * loads for assets whose cacheAt window has been reached without blocking.
2146
2169
  */
2147
2170
  prefetch(frame) {
2148
- for (const [key, track] of this.precomp.assets) if (track.cacheAt <= frame && track.record.endFrame >= frame) this.storageAdapter.loadAsset(key, track.record).catch((err) => {
2149
- console.error(`[AssetManager] prefetch failed for ${key}:`, err);
2171
+ for (const [key, track] of this.precomp.assets) {
2172
+ if (track.record.type === "loader") {
2173
+ this.syncLoader(key, track, frame)?.catch((err) => {
2174
+ console.error(`[AssetManager] loader failed for ${key}:`, err);
2175
+ });
2176
+ continue;
2177
+ }
2178
+ if (track.cacheAt <= frame && track.record.endFrame >= frame) this.storageAdapter.loadAsset(key, track.record).catch((err) => {
2179
+ console.error(`[AssetManager] prefetch failed for ${key}:`, err);
2180
+ });
2181
+ }
2182
+ }
2183
+ /**
2184
+ * Drive a single loader track toward the state `frame` requires. If `frame`
2185
+ * is inside the loader's `[cacheAt, discardAt]` window, run its callback
2186
+ * (deduped — at most one in-flight run, skipped once loaded) and return the
2187
+ * pending job, if any. If `frame` is outside the window and the loader was
2188
+ * loaded, dispose it. Returns the in-flight load promise so a blocking
2189
+ * caller can await it; `undefined` when there's nothing to wait for.
2190
+ */
2191
+ syncLoader(key, track, frame) {
2192
+ if (track.record.type !== "loader") return void 0;
2193
+ const discardAt = track.discardAt ?? Infinity;
2194
+ if (!(track.cacheAt <= frame && frame <= discardAt)) {
2195
+ const dispose = this.loaderDisposers.get(key);
2196
+ if (dispose) {
2197
+ this.loaderDisposers.delete(key);
2198
+ dispose();
2199
+ }
2200
+ return;
2201
+ }
2202
+ if (this.loaderDisposers.has(key)) return void 0;
2203
+ const existing = this.loaderInFlight.get(key);
2204
+ if (existing) return existing;
2205
+ const load = track.record.load;
2206
+ const job = load().then((disposer) => {
2207
+ this.loaderDisposers.set(key, disposer);
2208
+ }).finally(() => {
2209
+ this.loaderInFlight.delete(key);
2150
2210
  });
2211
+ this.loaderInFlight.set(key, job);
2212
+ return job;
2213
+ }
2214
+ /** Run every outstanding loader disposer and clear loader state. */
2215
+ dispose() {
2216
+ for (const dispose of this.loaderDisposers.values()) dispose();
2217
+ this.loaderDisposers.clear();
2218
+ this.loaderInFlight.clear();
2151
2219
  }
2152
2220
  /**
2153
2221
  * Push the current audio working set to the AudioDevice. Called after each
@@ -2372,21 +2440,29 @@ var AssetTracker = class {
2372
2440
  get catalog() {
2373
2441
  return this._catalog;
2374
2442
  }
2375
- /** Registers an arbitrary async loader to be awaited during the next load pass. */
2376
- requestLoader(fn) {
2377
- this.loaders.push(fn);
2443
+ /**
2444
+ * Register an opaque async loader needed at the current frame, tracked on the
2445
+ * timeline by `key` so the {@link AssetManager} runs it once its cache window
2446
+ * opens and disposes it when the window closes. Deduped by `key` (like
2447
+ * {@link requestFont} by family): the first registration's `load` callback is
2448
+ * kept and its frame range extends as later frames re-request the same key.
2449
+ */
2450
+ requestLoader(key, load) {
2451
+ this.upsertAsset(key, (frame) => ({
2452
+ type: "loader",
2453
+ src: key,
2454
+ startFrame: frame,
2455
+ endFrame: frame,
2456
+ load
2457
+ }));
2378
2458
  }
2379
2459
  /** Pending requests collected during the current frame's load pass. */
2380
2460
  requestedAssets = /* @__PURE__ */ new Map();
2381
- loaders = [];
2382
2461
  _audioRequests = [];
2383
2462
  _audioIds = /* @__PURE__ */ new Set();
2384
2463
  get assets() {
2385
2464
  return this.requestedAssets;
2386
2465
  }
2387
- get pendingLoaders() {
2388
- return this.loaders;
2389
- }
2390
2466
  get audioRequests() {
2391
2467
  return this._audioRequests;
2392
2468
  }
@@ -2519,17 +2595,15 @@ var AssetTracker = class {
2519
2595
  discardOutside(startFrame, endFrame) {
2520
2596
  for (const [key, entry] of this.requestedAssets) if (entry.endFrame < startFrame || entry.startFrame > endFrame) this.requestedAssets.delete(key);
2521
2597
  }
2522
- /** Clear all tracked entries and pending loaders without releasing the instance. */
2598
+ /** Clear all tracked entries (including loaders) without releasing the instance. */
2523
2599
  clear() {
2524
2600
  this.requestedAssets.clear();
2525
- this.loaders.length = 0;
2526
2601
  this._audioRequests.length = 0;
2527
2602
  this._audioIds.clear();
2528
2603
  }
2529
2604
  /** Release all state; the instance should not be used after this call. */
2530
2605
  dispose() {
2531
2606
  this.requestedAssets.clear();
2532
- this.loaders.length = 0;
2533
2607
  this._audioRequests.length = 0;
2534
2608
  this._audioIds.clear();
2535
2609
  }
@@ -2717,6 +2791,13 @@ function buildAssetMap(registry, fps) {
2717
2791
  discardAt: null
2718
2792
  });
2719
2793
  break;
2794
+ case "loader":
2795
+ out.set(key, {
2796
+ record: entry,
2797
+ cacheAt: Math.max(0, entry.startFrame - MAX_LEAD),
2798
+ discardAt: entry.endFrame + TAIL_FRAMES
2799
+ });
2800
+ break;
2720
2801
  }
2721
2802
  return out;
2722
2803
  }
@@ -2745,6 +2826,8 @@ var PlaybackController = class {
2745
2826
  stateEvaluator;
2746
2827
  assetManager;
2747
2828
  audioDevice;
2829
+ /** Set by dispose(); once true the controller must never touch its (now-freed) render context again. */
2830
+ disposed = false;
2748
2831
  fps;
2749
2832
  viewport;
2750
2833
  precomp;
@@ -2787,6 +2870,7 @@ var PlaybackController = class {
2787
2870
  const frame = this.fps * currentTime;
2788
2871
  if (frame >= this.totalFrames) this.masterClock.pause();
2789
2872
  await this.assetManager.loadAt(frame);
2873
+ if (this.disposed) return;
2790
2874
  this.audioDevice.syncTo(currentTime);
2791
2875
  this.renderAt(frame);
2792
2876
  this.assetManager.prefetch(frame);
@@ -2821,6 +2905,7 @@ var PlaybackController = class {
2821
2905
  * `screenshot` to ensure the surface is up-to-date.
2822
2906
  */
2823
2907
  renderAt(frame) {
2908
+ if (this.disposed) return;
2824
2909
  this.stateEvaluator.stateAt(frame);
2825
2910
  this.stateEvaluator.layout(this.measureScope);
2826
2911
  this.renderContext.render(() => {
@@ -2832,10 +2917,12 @@ var PlaybackController = class {
2832
2917
  * load before rendering, then prefetches upcoming frames.
2833
2918
  */
2834
2919
  async seek(frame) {
2920
+ if (this.disposed) return;
2835
2921
  const clamped = Math.max(0, Math.min(frame, this.totalFrames));
2836
2922
  this.masterClock.pause();
2837
2923
  this.masterClock.seek(clamped / this.fps);
2838
2924
  await this.assetManager.loadAt(clamped);
2925
+ if (this.disposed) return;
2839
2926
  this.renderAt(clamped);
2840
2927
  this.assetManager.prefetch(clamped);
2841
2928
  }
@@ -2897,9 +2984,11 @@ var PlaybackController = class {
2897
2984
  this.audioDevice.setMuted(muted);
2898
2985
  }
2899
2986
  dispose() {
2987
+ this.disposed = true;
2900
2988
  this.masterClock.dispose();
2901
2989
  this.audioDevice.dispose();
2902
2990
  this.stateEvaluator.dispose();
2991
+ this.assetManager.dispose();
2903
2992
  }
2904
2993
  };
2905
2994
  function nodeToTreeState(node, path, lifespans, sceneStart = 0) {
@@ -3084,6 +3173,7 @@ var StorageAdapter = class {
3084
3173
  break;
3085
3174
  }
3086
3175
  case "audio": break;
3176
+ case "loader": throw new Error("Loader records must not be routed through StorageAdapter.loadAsset");
3087
3177
  default: throw new Error(`Unsupported asset type: ${value.type}`);
3088
3178
  }
3089
3179
  this.cachedAssets.add(key);
@@ -3288,7 +3378,8 @@ function layoutParagraph(canvasKit, fontMgr, segments, opts) {
3288
3378
  const paragraph = builder.build();
3289
3379
  const wrapping = isFinite(opts.maxWidth) && opts.maxWidth > 0;
3290
3380
  paragraph.layout(wrapping ? opts.maxWidth : 1e7);
3291
- const layoutWidth = wrapping ? opts.maxWidth : Math.ceil(paragraph.getMaxIntrinsicWidth()) + 1;
3381
+ const intrinsicWidth = Math.ceil(paragraph.getMaxIntrinsicWidth()) + 1;
3382
+ const layoutWidth = wrapping ? opts.maxWidth : Math.max(intrinsicWidth, opts.boxWidth ?? 0);
3292
3383
  paragraph.layout(layoutWidth);
3293
3384
  const blockWidth = paragraph.getLongestLine();
3294
3385
  const blockHeight = paragraph.getHeight();
@@ -3381,6 +3472,7 @@ function layoutRichText(canvasKit, fontMgr, state) {
3381
3472
  align,
3382
3473
  lineHeight,
3383
3474
  maxWidth: width > 0 ? width : Infinity,
3475
+ boxWidth: width,
3384
3476
  originX: 0,
3385
3477
  originY: 0
3386
3478
  });
@@ -3983,7 +4075,7 @@ function computeImageMatrix(imgW, imgH, fill, bounds) {
3983
4075
  return fill.transform.flat();
3984
4076
  }
3985
4077
  if (bounds) {
3986
- const mode = fill.mode ?? "fill";
4078
+ const mode = fill.mode ?? "fit";
3987
4079
  const shapeW = bounds.right - bounds.left;
3988
4080
  const shapeH = bounds.bottom - bounds.top;
3989
4081
  let sx, sy, tx, ty;
@@ -4037,7 +4129,7 @@ function computeImageMatrix(imgW, imgH, fill, bounds) {
4037
4129
  }
4038
4130
  /** Shared image-shader builder used by both image and video renderers. */
4039
4131
  function makeImageShader(img, fill, ck, bounds) {
4040
- const mode = fill.mode ?? "fill";
4132
+ const mode = fill.mode ?? "fit";
4041
4133
  const tileMode = mode === "tile" ? ck.TileMode.Repeat : mode === "fit" ? ck.TileMode.Decal : ck.TileMode.Clamp;
4042
4134
  const matrix = computeImageMatrix(img.width(), img.height(), fill, bounds);
4043
4135
  return img.makeShaderOptions(tileMode, tileMode, ck.FilterMode.Linear, ck.MipmapMode.None, matrix);
@@ -6221,10 +6313,11 @@ var StrokeHandler = class {
6221
6313
  }
6222
6314
  const hasDash = !!(stroke.dash && stroke.dash.length > 0);
6223
6315
  const dashFit = hasDash && shape.ckPath ? this.measureDashFit(shape.ckPath, stroke.dash) : null;
6316
+ const align = stroke.align ?? 0;
6224
6317
  if (intDeviceWidth > 0 && shape.ckPath) {
6225
6318
  const source = hasDash ? this.buildDashedPath(shape.ckPath, stroke.dash, stroke.dashOffset ?? 0, dashFit ?? void 0) : shape.ckPath;
6226
6319
  if (source) {
6227
- const filled = source.makeStroked({ width: logicalWidth });
6320
+ const filled = this.alignedStrokeBand(source, logicalWidth, align, shape.ckPath);
6228
6321
  if (source !== shape.ckPath) source.delete();
6229
6322
  if (filled) {
6230
6323
  paint.setStyle(this.canvasKit.PaintStyle.Fill);
@@ -6235,11 +6328,22 @@ var StrokeHandler = class {
6235
6328
  }
6236
6329
  }
6237
6330
  }
6331
+ if (align !== 0 && !hasDash && shape.ckPath) {
6332
+ const band = this.alignedStrokeBand(shape.ckPath, logicalWidth, align, shape.ckPath);
6333
+ if (band) {
6334
+ paint.setStyle(this.canvasKit.PaintStyle.Fill);
6335
+ canvas.drawPath(band, paint);
6336
+ paint.setStyle(this.canvasKit.PaintStyle.Stroke);
6337
+ band.delete();
6338
+ return;
6339
+ }
6340
+ }
6238
6341
  if (hasDash && shape.ckPath) {
6239
6342
  const effect = this.makeUniformDashEffect(shape.ckPath, stroke.dash, stroke.dashOffset ?? 0, dashFit ?? void 0);
6240
6343
  if (effect) {
6241
6344
  paint.setPathEffect(effect);
6242
- canvas.drawPath(shape.ckPath, paint);
6345
+ if (align !== 0 && intDeviceWidth <= 0 && this.isClosedPath(shape.ckPath)) this.drawAlignedDashedStroke(canvas, paint, shape.ckPath, align);
6346
+ else canvas.drawPath(shape.ckPath, paint);
6243
6347
  paint.setPathEffect(null);
6244
6348
  effect.delete();
6245
6349
  return;
@@ -6247,6 +6351,42 @@ var StrokeHandler = class {
6247
6351
  }
6248
6352
  shape.draw(paint);
6249
6353
  }
6354
+ alignedStrokeBand(source, width, align, interior) {
6355
+ if (align === 0 || !this.isClosedPath(interior)) return source.makeStroked({ width });
6356
+ const widened = width * (1 + Math.abs(align));
6357
+ const band = source.makeStroked({ width: widened });
6358
+ if (!band) return null;
6359
+ const op = align < 0 ? this.canvasKit.PathOp.Intersect : this.canvasKit.PathOp.Difference;
6360
+ const clipped = this.canvasKit.Path.MakeFromOp(band, interior, op);
6361
+ band.delete();
6362
+ return clipped;
6363
+ }
6364
+ isClosedPath(path) {
6365
+ const iter = new this.canvasKit.ContourMeasureIter(path, false, 1);
6366
+ let contour = iter.next();
6367
+ if (!contour) {
6368
+ iter.delete();
6369
+ return false;
6370
+ }
6371
+ let allClosed = true;
6372
+ while (contour) {
6373
+ if (!contour.isClosed()) allClosed = false;
6374
+ contour.delete();
6375
+ contour = iter.next();
6376
+ }
6377
+ iter.delete();
6378
+ return allClosed;
6379
+ }
6380
+ drawAlignedDashedStroke(canvas, paint, interior, align) {
6381
+ canvas.save();
6382
+ const baseWidth = paint.getStrokeWidth();
6383
+ paint.setStrokeWidth(baseWidth * 2);
6384
+ const op = align < 0 ? this.canvasKit.ClipOp.Intersect : this.canvasKit.ClipOp.Difference;
6385
+ canvas.clipPath(interior, op, true);
6386
+ canvas.drawPath(interior, paint);
6387
+ canvas.restore();
6388
+ paint.setStrokeWidth(baseWidth);
6389
+ }
6250
6390
  drawTextUnionStroke(canvas, paint, shape, stroke, logicalWidth) {
6251
6391
  const hasDash = !!(stroke.dash && stroke.dash.length > 0);
6252
6392
  let dashEffect = null;
@@ -6372,6 +6512,7 @@ function buildText(canvasKit, canvas, fontMgr, state) {
6372
6512
  align: fullState.align,
6373
6513
  lineHeight: fullState.lineHeight,
6374
6514
  maxWidth: wrap ? fullState.width : Infinity,
6515
+ boxWidth: fullState.width,
6375
6516
  originX: x,
6376
6517
  originY: y
6377
6518
  });
@@ -6749,10 +6890,8 @@ var ShapeHandler = class {
6749
6890
  });
6750
6891
  }
6751
6892
  }
6752
- if (!isolated) {
6753
- shape.ensurePath();
6754
- if (shape.ckPath) this.boolean.contributeToPathCollection(shape.ckPath);
6755
- }
6893
+ shape.ensurePath();
6894
+ if (!isolated && shape.ckPath) this.boolean.contributeToPathCollection(shape.ckPath);
6756
6895
  this.shapes.push(shape.toCurrentShape(isolated));
6757
6896
  }
6758
6897
  rect(state) {
@@ -7059,6 +7198,7 @@ var WebRenderContext = class extends RenderContext {
7059
7198
  }
7060
7199
  pixelRatio = 1;
7061
7200
  renderPass(callback) {
7201
+ if (!this.mounted || !this.surface) return;
7062
7202
  this.currentCanvas = this.surface.getCanvas();
7063
7203
  this.currentCanvas.clear(this.canvasKit.BLACK);
7064
7204
  this.currentCanvas.save();
@@ -7824,6 +7964,10 @@ var WebImagePaintContext = class {
7824
7964
  };
7825
7965
  //#endregion
7826
7966
  //#region ../web/dist/storage-adapter.js
7967
+ /** Whether a src points at an SVG (by extension, ignoring any query/hash). */
7968
+ function isSvgSrc(src) {
7969
+ return /\.svg(?:[?#]|$)/i.test(src);
7970
+ }
7827
7971
  /**
7828
7972
  * Browser implementation of {@link StorageAdapter} — owns all async asset
7829
7973
  * decoding (images, video, audio, fonts) so the render loop can stay
@@ -7863,9 +8007,19 @@ var WebStorageAdapter = class extends StorageAdapter {
7863
8007
  getCanvasKit() {
7864
8008
  return this.canvasKit;
7865
8009
  }
7866
- /** Fetches and decodes `src`, optionally downscaled to the on-screen target size, caching raw RGBA pixels for later upload via {@link getCKImage}. No-op if already cached. */
8010
+ /**
8011
+ * Fetches and decodes `src` to RGBA pixels (cached for later GPU upload via
8012
+ * {@link getCKImage}), no-op if already cached. SVGs are rasterized at the
8013
+ * on-screen target size so they stay crisp when scaled up (see
8014
+ * {@link rasterizeSvg}); raster formats decode via `createImageBitmap`,
8015
+ * downscaled to target when that saves memory.
8016
+ */
7867
8017
  async loadImage(src, width, height) {
7868
8018
  if (this.imagePixels.has(src)) return;
8019
+ if (isSvgSrc(src)) {
8020
+ this.imagePixels.set(src, await this.rasterizeSvg(src, width, height));
8021
+ return;
8022
+ }
7869
8023
  const target = this.imageTargetPixels(src, width, height);
7870
8024
  const blob = await (await fetch(src)).blob();
7871
8025
  const bitmap = target ? await createImageBitmap(blob, {
@@ -7888,6 +8042,35 @@ var WebStorageAdapter = class extends StorageAdapter {
7888
8042
  pixels: new Uint8Array(data)
7889
8043
  });
7890
8044
  }
8045
+ /**
8046
+ * Rasterize an SVG to RGBA pixels. Unlike raster formats, SVG is vector, so
8047
+ * we render it at the size it's *drawn* on screen rather than its nominal
8048
+ * `viewBox` size — a 48×48 logo placed in a 512px box must rasterize at
8049
+ * ~512px (× devicePixelRatio) to stay sharp. `createImageBitmap` can't
8050
+ * decode SVG reliably across browsers (Chrome throws on an SVG Blob), so we
8051
+ * decode through an `HTMLImageElement`, which every browser supports.
8052
+ */
8053
+ async rasterizeSvg(src, width, height) {
8054
+ const text = await (await fetch(src)).text();
8055
+ const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(text)}`;
8056
+ const img = new Image();
8057
+ img.decoding = "sync";
8058
+ await new Promise((resolve, reject) => {
8059
+ img.onload = () => resolve();
8060
+ img.onerror = () => reject(/* @__PURE__ */ new Error(`loadImage(${src}): failed to decode SVG`));
8061
+ img.src = url;
8062
+ });
8063
+ const target = this.svgTargetPixels(width, height, img.naturalWidth, img.naturalHeight);
8064
+ const ctx = new OffscreenCanvas(target.width, target.height).getContext("2d", { willReadFrequently: true });
8065
+ if (!ctx) throw new Error(`loadImage(${src}): could not get 2d context`);
8066
+ ctx.drawImage(img, 0, 0, target.width, target.height);
8067
+ const data = ctx.getImageData(0, 0, target.width, target.height).data;
8068
+ return {
8069
+ width: target.width,
8070
+ height: target.height,
8071
+ pixels: new Uint8Array(data)
8072
+ };
8073
+ }
7891
8074
  imageTargetPixels(src, width, height) {
7892
8075
  let meta;
7893
8076
  try {
@@ -7908,6 +8091,33 @@ var WebStorageAdapter = class extends StorageAdapter {
7908
8091
  height: Math.max(1, Math.round(meta.height * ratio))
7909
8092
  };
7910
8093
  }
8094
+ /**
8095
+ * Pick the raster size for an SVG, *preserving its intrinsic aspect ratio*.
8096
+ * The decoded image must keep the SVG's true proportions so the downstream
8097
+ * fit/crop/fill shader matrix (see computeImageMatrix) can position it like
8098
+ * any raster image — if we instead stretched the SVG to the layout box here,
8099
+ * its aspect ratio would already be baked in and `fit` would be a no-op.
8100
+ *
8101
+ * We compute one uniform scale: how big the SVG renders on screen (the
8102
+ * larger of the box's two dimensions, since `fit` may letterbox but `crop`
8103
+ * may overflow), times devicePixelRatio so it stays sharp when zoomed in,
8104
+ * clamped to the viewport so a huge box can't blow up memory. That factor is
8105
+ * applied to the intrinsic width/height, keeping proportions intact.
8106
+ */
8107
+ svgTargetPixels(width, height, naturalWidth, naturalHeight) {
8108
+ const intrinsicW = naturalWidth > 0 ? naturalWidth : 300;
8109
+ const intrinsicH = naturalHeight > 0 ? naturalHeight : 300;
8110
+ const dpr = typeof devicePixelRatio === "number" && devicePixelRatio > 0 ? devicePixelRatio : 1;
8111
+ const boxW = width > 0 ? Math.min(width, this.viewport.width) : 0;
8112
+ const boxH = height > 0 ? Math.min(height, this.viewport.height) : 0;
8113
+ const onScreen = Math.max(boxW, boxH);
8114
+ const longestIntrinsic = Math.max(intrinsicW, intrinsicH);
8115
+ const scale = onScreen > 0 ? onScreen * dpr / longestIntrinsic : 1;
8116
+ return {
8117
+ width: Math.max(1, Math.round(intrinsicW * scale)),
8118
+ height: Math.max(1, Math.round(intrinsicH * scale))
8119
+ };
8120
+ }
7911
8121
  /** Lazily uploads cached pixels for `url` to a GPU-resident CanvasKit image, memoizing the result; returns null until {@link loadImage} has completed. */
7912
8122
  getCKImage(url) {
7913
8123
  const cached = this.imagePixels.get(url);
@@ -37249,8 +37459,8 @@ function TrackRows({ nodes, window, fullContentWidth, effectiveScrollLeft, paddi
37249
37459
  children: node.waveform.map((clip, ci) => {
37250
37460
  const startFrame = sceneOffset + clip.startTime * fps;
37251
37461
  const endFrame = clip.endTime != null ? sceneOffset + clip.endTime * fps : totalFrameCount;
37252
- const startPx = paddingX + startFrame * computedPxPerUnit;
37253
- const barWidth = Math.max(4, (endFrame - startFrame) * computedPxPerUnit);
37462
+ const startPx = paddingX + startFrame * computedPxPerUnit + 2;
37463
+ const barWidth = Math.max(4, (endFrame - startFrame) * computedPxPerUnit - 4);
37254
37464
  return /* @__PURE__ */ jsx("div", {
37255
37465
  className: "absolute rounded-xs overflow-hidden bg-card",
37256
37466
  style: {
@@ -37273,8 +37483,8 @@ function TrackRows({ nodes, window, fullContentWidth, effectiveScrollLeft, paddi
37273
37483
  const hasSpan = node.startFrame != null && node.endFrame != null;
37274
37484
  const spanStart = hasSpan ? node.startFrame : 0;
37275
37485
  const spanFrames = hasSpan ? node.endFrame - node.startFrame + 1 : totalFrameCount;
37276
- const startPx = paddingX + spanStart * computedPxPerUnit;
37277
- const barWidth = Math.max(4, spanFrames * computedPxPerUnit);
37486
+ const startPx = paddingX + spanStart * computedPxPerUnit + 2;
37487
+ const barWidth = Math.max(4, spanFrames * computedPxPerUnit - 4);
37278
37488
  return /* @__PURE__ */ jsx("div", {
37279
37489
  className: "border-b border-border/40",
37280
37490
  style: {
@@ -37715,12 +37925,12 @@ function TimelineRuler({ width, minorTicks = 0 }) {
37715
37925
  const endFrame = sceneStartFrames[i + 1] ?? totalFrameCount;
37716
37926
  const isActive = i === activeSceneIndex;
37717
37927
  const sceneName = scenes[i]?.name ?? `Scene ${i + 1}`;
37718
- const barWidth = Math.max(4, (endFrame - startFrame) * computedPxPerUnit);
37928
+ const barWidth = Math.max(4, (endFrame - startFrame) * computedPxPerUnit - 4);
37719
37929
  return /* @__PURE__ */ jsx("div", {
37720
37930
  title: sceneName,
37721
37931
  className: `group/scenebar absolute rounded-xs overflow-hidden flex items-center px-1.5 ${isActive ? "bg-primary/70" : "bg-card"}`,
37722
37932
  style: {
37723
- left: paddingX + startFrame * computedPxPerUnit - effectiveScrollLeft,
37933
+ left: paddingX + startFrame * computedPxPerUnit - effectiveScrollLeft + 2,
37724
37934
  top: 5,
37725
37935
  width: barWidth,
37726
37936
  height: 18
@@ -38338,6 +38548,28 @@ function ScenePanel() {
38338
38548
  className: "hover:underline",
38339
38549
  children: "Documentation"
38340
38550
  })]
38551
+ }),
38552
+ /* @__PURE__ */ jsxs("a", {
38553
+ href: "https://github.com/motion-script/motion-script/issues",
38554
+ target: "_blank",
38555
+ rel: "noopener noreferrer",
38556
+ className: "flex items-center gap-2.5 px-3 py-3 border-t shrink-0 text-sm font-medium text-muted-foreground bg-muted/30 hover:bg-muted/50 hover:text-foreground transition-colors",
38557
+ children: [/* @__PURE__ */ jsx("svg", {
38558
+ xmlns: "http://www.w3.org/2000/svg",
38559
+ fill: "none",
38560
+ viewBox: "0 0 24 24",
38561
+ strokeWidth: 1.5,
38562
+ stroke: "currentColor",
38563
+ className: "size-6 shrink-0",
38564
+ children: /* @__PURE__ */ jsx("path", {
38565
+ strokeLinecap: "round",
38566
+ strokeLinejoin: "round",
38567
+ d: "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
38568
+ })
38569
+ }), /* @__PURE__ */ jsx("span", {
38570
+ className: "hover:underline",
38571
+ children: "Report New Issue"
38572
+ })]
38341
38573
  })
38342
38574
  ]
38343
38575
  }), /* @__PURE__ */ jsx(ErrorsDialog, {