@motionscript/browser 0.0.0-stage → 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +201 -0
  3. package/README.md +68 -3
  4. package/dist/asset-bytes.d.ts +50 -0
  5. package/dist/asset-bytes.d.ts.map +1 -0
  6. package/dist/asset-bytes.js +60 -0
  7. package/dist/asset-bytes.js.map +1 -0
  8. package/dist/audio/bus-graph.d.ts +43 -0
  9. package/dist/audio/bus-graph.d.ts.map +1 -0
  10. package/dist/audio/bus-graph.js +104 -0
  11. package/dist/audio/bus-graph.js.map +1 -0
  12. package/dist/audio/filter-graph.d.ts +58 -0
  13. package/dist/audio/filter-graph.d.ts.map +1 -0
  14. package/dist/audio/filter-graph.js +235 -0
  15. package/dist/audio/filter-graph.js.map +1 -0
  16. package/dist/audio/index.d.ts +5 -0
  17. package/dist/audio/index.d.ts.map +1 -0
  18. package/dist/audio/index.js +12 -0
  19. package/dist/audio/index.js.map +1 -0
  20. package/dist/audio/mixer.d.ts +59 -0
  21. package/dist/audio/mixer.d.ts.map +1 -0
  22. package/dist/audio/mixer.js +193 -0
  23. package/dist/audio/mixer.js.map +1 -0
  24. package/dist/audio/player.d.ts +49 -0
  25. package/dist/audio/player.d.ts.map +1 -0
  26. package/dist/audio/player.js +226 -0
  27. package/dist/audio/player.js.map +1 -0
  28. package/dist/browser/audio.js +2 -0
  29. package/dist/browser/audio.js.map +7 -0
  30. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js +2 -0
  31. package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js.map +7 -0
  32. package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js +2 -0
  33. package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js.map +7 -0
  34. package/dist/browser/chunks/chunk-CTLBXZSU.js +4119 -0
  35. package/dist/browser/chunks/chunk-CTLBXZSU.js.map +7 -0
  36. package/dist/browser/chunks/chunk-FQ32PDIU.js +2 -0
  37. package/dist/browser/chunks/chunk-FQ32PDIU.js.map +7 -0
  38. package/dist/browser/chunks/chunk-FY3BTKY4.js +2 -0
  39. package/dist/browser/chunks/chunk-FY3BTKY4.js.map +7 -0
  40. package/dist/browser/chunks/chunk-NSFLRI4P.js +2 -0
  41. package/dist/browser/chunks/chunk-NSFLRI4P.js.map +7 -0
  42. package/dist/browser/chunks/three.module-Z4EOVUWF.js +2 -0
  43. package/dist/browser/chunks/three.module-Z4EOVUWF.js.map +7 -0
  44. package/dist/browser/esbuild.wasm +0 -0
  45. package/dist/browser/index.js +2172 -0
  46. package/dist/browser/index.js.map +7 -0
  47. package/dist/browser/manifest.json +15 -0
  48. package/dist/concat.d.ts +95 -0
  49. package/dist/concat.d.ts.map +1 -0
  50. package/dist/concat.js +174 -0
  51. package/dist/concat.js.map +1 -0
  52. package/dist/decode/image-levels.d.ts +137 -0
  53. package/dist/decode/image-levels.d.ts.map +1 -0
  54. package/dist/decode/image-levels.js +477 -0
  55. package/dist/decode/image-levels.js.map +1 -0
  56. package/dist/decode/levels.d.ts +37 -0
  57. package/dist/decode/levels.d.ts.map +1 -0
  58. package/dist/decode/levels.js +68 -0
  59. package/dist/decode/levels.js.map +1 -0
  60. package/dist/decode/video-levels.d.ts +37 -0
  61. package/dist/decode/video-levels.d.ts.map +1 -0
  62. package/dist/decode/video-levels.js +95 -0
  63. package/dist/decode/video-levels.js.map +1 -0
  64. package/dist/dynamic-nodes.d.ts +9 -0
  65. package/dist/dynamic-nodes.d.ts.map +1 -0
  66. package/dist/dynamic-nodes.js +56 -0
  67. package/dist/dynamic-nodes.js.map +1 -0
  68. package/dist/engine.d.ts +59 -0
  69. package/dist/engine.d.ts.map +1 -0
  70. package/dist/engine.js +171 -0
  71. package/dist/engine.js.map +1 -0
  72. package/dist/exporter.d.ts +163 -0
  73. package/dist/exporter.d.ts.map +1 -0
  74. package/dist/exporter.js +263 -0
  75. package/dist/exporter.js.map +1 -0
  76. package/dist/getter.d.ts +10 -0
  77. package/dist/getter.d.ts.map +1 -0
  78. package/dist/getter.js +38 -0
  79. package/dist/getter.js.map +1 -0
  80. package/dist/index.d.ts +22 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +72 -0
  83. package/dist/index.js.map +1 -0
  84. package/dist/measure-context.d.ts +12 -0
  85. package/dist/measure-context.d.ts.map +1 -0
  86. package/dist/measure-context.js +12 -0
  87. package/dist/measure-context.js.map +1 -0
  88. package/dist/mix-timeline-audio.d.ts +42 -0
  89. package/dist/mix-timeline-audio.d.ts.map +1 -0
  90. package/dist/mix-timeline-audio.js +63 -0
  91. package/dist/mix-timeline-audio.js.map +1 -0
  92. package/dist/output.d.ts +7 -0
  93. package/dist/output.d.ts.map +1 -0
  94. package/dist/output.js +6 -0
  95. package/dist/output.js.map +1 -0
  96. package/dist/render-context.d.ts +30 -0
  97. package/dist/render-context.d.ts.map +1 -0
  98. package/dist/render-context.js +55 -0
  99. package/dist/render-context.js.map +1 -0
  100. package/dist/screenshot.d.ts +45 -0
  101. package/dist/screenshot.d.ts.map +1 -0
  102. package/dist/screenshot.js +32 -0
  103. package/dist/screenshot.js.map +1 -0
  104. package/dist/still.d.ts +151 -0
  105. package/dist/still.d.ts.map +1 -0
  106. package/dist/still.js +222 -0
  107. package/dist/still.js.map +1 -0
  108. package/dist/storage-adapter.d.ts +367 -0
  109. package/dist/storage-adapter.d.ts.map +1 -0
  110. package/dist/storage-adapter.js +1278 -0
  111. package/dist/storage-adapter.js.map +1 -0
  112. package/dist/three/renderer.d.ts +48 -0
  113. package/dist/three/renderer.d.ts.map +1 -0
  114. package/dist/three/renderer.js +177 -0
  115. package/dist/three/renderer.js.map +1 -0
  116. package/dist/ticker.d.ts +42 -0
  117. package/dist/ticker.d.ts.map +1 -0
  118. package/dist/ticker.js +143 -0
  119. package/dist/ticker.js.map +1 -0
  120. package/package.json +74 -3
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The resolutions a source may be decoded at.
3
+ *
4
+ * Levels sit half an octave apart, so a decode is never more than √2 larger than
5
+ * what a draw asked for, and a slow zoom re-decodes a dozen times across the
6
+ * whole range rather than once per frame.
7
+ */
8
+ /**
9
+ * Longest edge any level is decoded at. The conventional maximum GPU texture
10
+ * edge, so pixels past it could not be sampled in one draw anyway; a 4096²
11
+ * RGBA level is 67 MB resident.
12
+ */
13
+ export const MAX_IMAGE_DECODE_EDGE = 4096;
14
+ export const MIN_LEVEL_EDGE = 64;
15
+ /** `64·2^(k/2)` up to {@link MAX_IMAGE_DECODE_EDGE}. */
16
+ export const LEVEL_LADDER = (() => {
17
+ const out = [];
18
+ for (let k = 0;; k++) {
19
+ const edge = Math.round(MIN_LEVEL_EDGE * 2 ** (k / 2));
20
+ if (edge > MAX_IMAGE_DECODE_EDGE)
21
+ break;
22
+ out.push(edge);
23
+ }
24
+ return out;
25
+ })();
26
+ /**
27
+ * The largest level worth holding: the source's own long edge for a raster
28
+ * (there is no more detail to reveal), the ceiling for a vector.
29
+ */
30
+ export function levelCap(intrinsic, vector) {
31
+ if (vector)
32
+ return MAX_IMAGE_DECODE_EDGE;
33
+ const long = Math.max(1, Math.round(Math.max(intrinsic.width, intrinsic.height)));
34
+ return Math.min(long, MAX_IMAGE_DECODE_EDGE);
35
+ }
36
+ /**
37
+ * The level for a draw that needs `demand` device px along the source's long
38
+ * edge: the smallest rung at or above it, snapped to `cap` once a rung is
39
+ * within half an octave of it — a level at 0.8× the source costs almost what
40
+ * the source does and looks worse.
41
+ */
42
+ export function levelFor(demand, cap) {
43
+ const need = Number.isFinite(demand) ? Math.max(0, demand) : cap;
44
+ if (need >= cap)
45
+ return cap;
46
+ const snap = cap / Math.SQRT2;
47
+ for (const edge of LEVEL_LADDER) {
48
+ if (edge >= snap)
49
+ return cap;
50
+ if (edge >= need)
51
+ return edge;
52
+ }
53
+ return cap;
54
+ }
55
+ /** The smallest level a source can be held at. */
56
+ export function floorLevel(cap) {
57
+ return levelFor(0, cap);
58
+ }
59
+ /** Pixel dimensions of `edge`, keeping the intrinsic aspect. */
60
+ export function levelSize(edge, intrinsic) {
61
+ const long = Math.max(intrinsic.width, intrinsic.height, 1);
62
+ const scale = edge / long;
63
+ return {
64
+ width: Math.max(1, Math.round(intrinsic.width * scale)),
65
+ height: Math.max(1, Math.round(intrinsic.height * scale)),
66
+ };
67
+ }
68
+ //# sourceMappingURL=levels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"levels.js","sourceRoot":"","sources":["../../src/decode/levels.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAE1C,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,wDAAwD;AACxD,MAAM,CAAC,MAAM,YAAY,GAAsB,CAAC,GAAG,EAAE;IACjD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,GAAI,CAAC,EAAE,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACvD,IAAI,IAAI,GAAG,qBAAqB;YAAE,MAAM;QACxC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC,CAAC,EAAE,CAAC;AAOL;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,SAAoB,EAAE,MAAe;IAC1D,IAAI,MAAM;QAAE,OAAO,qBAAqB,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAClF,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAc,EAAE,GAAW;IAChD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACjE,IAAI,IAAI,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC;IAC5B,MAAM,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAC9B,IAAI,IAAI,IAAI,IAAI;YAAE,OAAO,GAAG,CAAC;QAC7B,IAAI,IAAI,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;IAClC,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,UAAU,CAAC,GAAW;IAClC,OAAO,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,SAAoB;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,OAAO;QACH,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;QACvD,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC;KAC5D,CAAC;AACN,CAAC","sourcesContent":["/**\n * The resolutions a source may be decoded at.\n *\n * Levels sit half an octave apart, so a decode is never more than √2 larger than\n * what a draw asked for, and a slow zoom re-decodes a dozen times across the\n * whole range rather than once per frame.\n */\n\n/**\n * Longest edge any level is decoded at. The conventional maximum GPU texture\n * edge, so pixels past it could not be sampled in one draw anyway; a 4096²\n * RGBA level is 67 MB resident.\n */\nexport const MAX_IMAGE_DECODE_EDGE = 4096;\n\nexport const MIN_LEVEL_EDGE = 64;\n\n/** `64·2^(k/2)` up to {@link MAX_IMAGE_DECODE_EDGE}. */\nexport const LEVEL_LADDER: readonly number[] = (() => {\n const out: number[] = [];\n for (let k = 0; ; k++) {\n const edge = Math.round(MIN_LEVEL_EDGE * 2 ** (k / 2));\n if (edge > MAX_IMAGE_DECODE_EDGE) break;\n out.push(edge);\n }\n return out;\n})();\n\nexport interface PixelSize {\n width: number;\n height: number;\n}\n\n/**\n * The largest level worth holding: the source's own long edge for a raster\n * (there is no more detail to reveal), the ceiling for a vector.\n */\nexport function levelCap(intrinsic: PixelSize, vector: boolean): number {\n if (vector) return MAX_IMAGE_DECODE_EDGE;\n const long = Math.max(1, Math.round(Math.max(intrinsic.width, intrinsic.height)));\n return Math.min(long, MAX_IMAGE_DECODE_EDGE);\n}\n\n/**\n * The level for a draw that needs `demand` device px along the source's long\n * edge: the smallest rung at or above it, snapped to `cap` once a rung is\n * within half an octave of it — a level at 0.8× the source costs almost what\n * the source does and looks worse.\n */\nexport function levelFor(demand: number, cap: number): number {\n const need = Number.isFinite(demand) ? Math.max(0, demand) : cap;\n if (need >= cap) return cap;\n const snap = cap / Math.SQRT2;\n for (const edge of LEVEL_LADDER) {\n if (edge >= snap) return cap;\n if (edge >= need) return edge;\n }\n return cap;\n}\n\n/** The smallest level a source can be held at. */\nexport function floorLevel(cap: number): number {\n return levelFor(0, cap);\n}\n\n/** Pixel dimensions of `edge`, keeping the intrinsic aspect. */\nexport function levelSize(edge: number, intrinsic: PixelSize): PixelSize {\n const long = Math.max(intrinsic.width, intrinsic.height, 1);\n const scale = edge / long;\n return {\n width: Math.max(1, Math.round(intrinsic.width * scale)),\n height: Math.max(1, Math.round(intrinsic.height * scale)),\n };\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import type { DecodePolicy } from "./image-levels";
2
+ import { type PixelSize } from "./levels";
3
+ /**
4
+ * Decoded frames one clip's window may hold, in bytes. Frames are counted at
5
+ * their decoded level, so a clip drawn small keeps a long window and one drawn
6
+ * full-screen a short one, rather than a fixed count sized for neither.
7
+ */
8
+ export declare const VIDEO_WINDOW_BUDGET_BYTES: number;
9
+ /** Forward and back window lengths for frames of `size`, split 3:1 like the fixed window was. */
10
+ export declare function videoWindowFrames(size: PixelSize | null): {
11
+ forward: number;
12
+ back: number;
13
+ };
14
+ /**
15
+ * The level one clip's frames are snapshotted at, and how it follows demand.
16
+ *
17
+ * A clip has one level at a time, not one per draw: the decoder is sequential,
18
+ * and a frame is resized as it is snapshotted, so changing the level changes
19
+ * what the next snapshot produces without restarting the stream.
20
+ */
21
+ export declare class VideoLevel {
22
+ level: number;
23
+ private _cap;
24
+ private _passNeed;
25
+ private _passDraws;
26
+ private _passAllOver;
27
+ private _overSince;
28
+ constructor(intrinsic: PixelSize, policy: DecodePolicy, lastDemand: number | undefined, surfaceEdge: number);
29
+ get cap(): number;
30
+ /** Record one draw's footprint; absent means the clip's own resolution. */
31
+ note(footprint: number | undefined): void;
32
+ /** Whether a frame snapshotted at `frameLevel` is good enough to count as the frame. */
33
+ accepts(frameLevel: number | undefined, policy: DecodePolicy): boolean;
34
+ /** The pass's demand is complete: move the level, and report whether it moved. */
35
+ endPass(policy: DecodePolicy, playing: boolean, now: number): boolean;
36
+ }
37
+ //# sourceMappingURL=video-levels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-levels.d.ts","sourceRoot":"","sources":["../../src/decode/video-levels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAoB,CAAC;AAK3D,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAS3F;AAED;;;;;;GAMG;AACH,qBAAa,UAAU;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,UAAU,CAAuB;gBAE7B,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM;IAO3G,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,2EAA2E;IAC3E,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IASzC,wFAAwF;IACxF,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO;IAKtE,kFAAkF;IAClF,OAAO,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;CA6BxE"}
@@ -0,0 +1,95 @@
1
+ import { DOWNGRADE_AFTER_MS } from "./image-levels";
2
+ import { floorLevel, levelCap, levelFor } from "./levels";
3
+ /**
4
+ * Decoded frames one clip's window may hold, in bytes. Frames are counted at
5
+ * their decoded level, so a clip drawn small keeps a long window and one drawn
6
+ * full-screen a short one, rather than a fixed count sized for neither.
7
+ */
8
+ export const VIDEO_WINDOW_BUDGET_BYTES = 512 * 1024 * 1024;
9
+ const MAX_FORWARD_FRAMES = 96;
10
+ const MAX_BACK_FRAMES = 32;
11
+ /** Forward and back window lengths for frames of `size`, split 3:1 like the fixed window was. */
12
+ export function videoWindowFrames(size) {
13
+ if (!size || !(size.width > 0) || !(size.height > 0)) {
14
+ return { forward: MAX_FORWARD_FRAMES, back: MAX_BACK_FRAMES };
15
+ }
16
+ const frames = VIDEO_WINDOW_BUDGET_BYTES / (size.width * size.height * 4);
17
+ return {
18
+ forward: Math.max(4, Math.min(MAX_FORWARD_FRAMES, Math.floor(frames * 0.75))),
19
+ back: Math.max(1, Math.min(MAX_BACK_FRAMES, Math.floor(frames * 0.25))),
20
+ };
21
+ }
22
+ /**
23
+ * The level one clip's frames are snapshotted at, and how it follows demand.
24
+ *
25
+ * A clip has one level at a time, not one per draw: the decoder is sequential,
26
+ * and a frame is resized as it is snapshotted, so changing the level changes
27
+ * what the next snapshot produces without restarting the stream.
28
+ */
29
+ export class VideoLevel {
30
+ level;
31
+ _cap;
32
+ _passNeed = 0;
33
+ _passDraws = 0;
34
+ _passAllOver = true;
35
+ _overSince = null;
36
+ constructor(intrinsic, policy, lastDemand, surfaceEdge) {
37
+ this._cap = levelCap(intrinsic, false);
38
+ this.level = policy === "exact"
39
+ ? floorLevel(this._cap)
40
+ : levelFor(lastDemand ?? Math.min(Math.max(intrinsic.width, intrinsic.height), surfaceEdge), this._cap);
41
+ }
42
+ get cap() {
43
+ return this._cap;
44
+ }
45
+ /** Record one draw's footprint; absent means the clip's own resolution. */
46
+ note(footprint) {
47
+ const need = footprint !== undefined && Number.isFinite(footprint) && footprint > 0
48
+ ? Math.min(footprint, this._cap)
49
+ : this._cap;
50
+ this._passNeed = Math.max(this._passNeed, need);
51
+ this._passDraws++;
52
+ if (this.level < 2 * need)
53
+ this._passAllOver = false;
54
+ }
55
+ /** Whether a frame snapshotted at `frameLevel` is good enough to count as the frame. */
56
+ accepts(frameLevel, policy) {
57
+ if (frameLevel === undefined)
58
+ return true;
59
+ return policy === "exact" ? frameLevel === this.level : frameLevel >= this.level;
60
+ }
61
+ /** The pass's demand is complete: move the level, and report whether it moved. */
62
+ endPass(policy, playing, now) {
63
+ const draws = this._passDraws;
64
+ const need = this._passNeed;
65
+ const allOver = this._passAllOver;
66
+ this._passDraws = 0;
67
+ this._passNeed = 0;
68
+ this._passAllOver = true;
69
+ if (draws === 0)
70
+ return false;
71
+ const target = levelFor(need, this._cap);
72
+ const before = this.level;
73
+ if (policy === "exact") {
74
+ this.level = target;
75
+ }
76
+ else if (this.level < need) {
77
+ this.level = target;
78
+ this._overSince = null;
79
+ }
80
+ else if (allOver && !playing) {
81
+ // Only while paused: a playing clip dropping resolution is visible
82
+ // as a pop, and its window would re-decode behind the playhead.
83
+ this._overSince ??= now;
84
+ if (now - this._overSince >= DOWNGRADE_AFTER_MS) {
85
+ this.level = target;
86
+ this._overSince = null;
87
+ }
88
+ }
89
+ else {
90
+ this._overSince = null;
91
+ }
92
+ return this.level !== before;
93
+ }
94
+ }
95
+ //# sourceMappingURL=video-levels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-levels.js","sourceRoot":"","sources":["../../src/decode/video-levels.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DecodePolicy } from \"./image-levels\";\nimport { DOWNGRADE_AFTER_MS } from \"./image-levels\";\nimport { floorLevel, levelCap, levelFor, type PixelSize } from \"./levels\";\n\n/**\n * Decoded frames one clip's window may hold, in bytes. Frames are counted at\n * their decoded level, so a clip drawn small keeps a long window and one drawn\n * full-screen a short one, rather than a fixed count sized for neither.\n */\nexport const VIDEO_WINDOW_BUDGET_BYTES = 512 * 1024 * 1024;\n\nconst MAX_FORWARD_FRAMES = 96;\nconst MAX_BACK_FRAMES = 32;\n\n/** Forward and back window lengths for frames of `size`, split 3:1 like the fixed window was. */\nexport function videoWindowFrames(size: PixelSize | null): { forward: number; back: number } {\n if (!size || !(size.width > 0) || !(size.height > 0)) {\n return { forward: MAX_FORWARD_FRAMES, back: MAX_BACK_FRAMES };\n }\n const frames = VIDEO_WINDOW_BUDGET_BYTES / (size.width * size.height * 4);\n return {\n forward: Math.max(4, Math.min(MAX_FORWARD_FRAMES, Math.floor(frames * 0.75))),\n back: Math.max(1, Math.min(MAX_BACK_FRAMES, Math.floor(frames * 0.25))),\n };\n}\n\n/**\n * The level one clip's frames are snapshotted at, and how it follows demand.\n *\n * A clip has one level at a time, not one per draw: the decoder is sequential,\n * and a frame is resized as it is snapshotted, so changing the level changes\n * what the next snapshot produces without restarting the stream.\n */\nexport class VideoLevel {\n level: number;\n private _cap: number;\n private _passNeed = 0;\n private _passDraws = 0;\n private _passAllOver = true;\n private _overSince: number | null = null;\n\n constructor(intrinsic: PixelSize, policy: DecodePolicy, lastDemand: number | undefined, surfaceEdge: number) {\n this._cap = levelCap(intrinsic, false);\n this.level = policy === \"exact\"\n ? floorLevel(this._cap)\n : levelFor(lastDemand ?? Math.min(Math.max(intrinsic.width, intrinsic.height), surfaceEdge), this._cap);\n }\n\n get cap(): number {\n return this._cap;\n }\n\n /** Record one draw's footprint; absent means the clip's own resolution. */\n note(footprint: number | undefined): void {\n const need = footprint !== undefined && Number.isFinite(footprint) && footprint > 0\n ? Math.min(footprint, this._cap)\n : this._cap;\n this._passNeed = Math.max(this._passNeed, need);\n this._passDraws++;\n if (this.level < 2 * need) this._passAllOver = false;\n }\n\n /** Whether a frame snapshotted at `frameLevel` is good enough to count as the frame. */\n accepts(frameLevel: number | undefined, policy: DecodePolicy): boolean {\n if (frameLevel === undefined) return true;\n return policy === \"exact\" ? frameLevel === this.level : frameLevel >= this.level;\n }\n\n /** The pass's demand is complete: move the level, and report whether it moved. */\n endPass(policy: DecodePolicy, playing: boolean, now: number): boolean {\n const draws = this._passDraws;\n const need = this._passNeed;\n const allOver = this._passAllOver;\n this._passDraws = 0;\n this._passNeed = 0;\n this._passAllOver = true;\n if (draws === 0) return false;\n\n const target = levelFor(need, this._cap);\n const before = this.level;\n if (policy === \"exact\") {\n this.level = target;\n } else if (this.level < need) {\n this.level = target;\n this._overSince = null;\n } else if (allOver && !playing) {\n // Only while paused: a playing clip dropping resolution is visible\n // as a pop, and its window would re-decode behind the playhead.\n this._overSince ??= now;\n if (now - this._overSince >= DOWNGRADE_AFTER_MS) {\n this.level = target;\n this._overSince = null;\n }\n } else {\n this._overSince = null;\n }\n return this.level !== before;\n }\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import type { DynamicNodeConfig } from "@motionscript/core";
2
+ /**
3
+ * Resolves a full `DynamicNodeConfig`, filling in whatever the caller left
4
+ * unset with `browser`'s own default. Synchronous — the WASM import happens
5
+ * on `compile`'s first actual call, not here, so attaching this to every
6
+ * engine costs nothing until a source string is genuinely registered.
7
+ */
8
+ export declare function resolveDynamicNodes(partial: Partial<DynamicNodeConfig> | undefined, compilerWasmUrl?: string): DynamicNodeConfig;
9
+ //# sourceMappingURL=dynamic-nodes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-nodes.d.ts","sourceRoot":"","sources":["../src/dynamic-nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AA+C5D;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,SAAS,EAC/C,eAAe,CAAC,EAAE,MAAM,GACzB,iBAAiB,CAOnB"}
@@ -0,0 +1,56 @@
1
+ import * as sdk from "@motionscript/core/sdk";
2
+ import * as jsxRuntime from "@motionscript/core/jsx/jsx-runtime";
3
+ import * as esbuild from "esbuild-wasm";
4
+ /**
5
+ * `browser`'s real, working default for `Registry.addNodeFromSource` — the
6
+ * `sdk`/`jsxRuntime` halves are cheap (re-exported classes, no WASM) and
7
+ * imported eagerly; the compiler is the expensive half, and stays behind a
8
+ * lazy `import()` so an app that never registers a source-string node never
9
+ * pays for it, however long the engine has been running.
10
+ *
11
+ * `core` has no compiler dependency of its own and cannot import the published
12
+ * `motion-script` sdk barrel (that package depends on `core`, so the reverse
13
+ * would be circular) — `browser` is where this concern actually belongs: the
14
+ * package whose entire job is "what running in an actual browser tab looks
15
+ * like" already owns `getCanvasKit`/`WebAudioSink`/`WebTicker` on exactly that
16
+ * basis.
17
+ */
18
+ // Default to the wasm shipped by esbuild-wasm, resolved lazily — `import.meta.url`
19
+ // is empty in a non-ESM output (an IIFE bundle, which is what the headless page
20
+ // loads), and `new URL("…", "")` throws on import, before a caller has had the
21
+ // chance to pass an explicit URL that would have made the default irrelevant.
22
+ // Mirrors `getCanvasKit`'s own `defaultWasmUrl` in `getter.ts`.
23
+ function defaultCompilerWasmUrl() {
24
+ return new URL("esbuild-wasm/esbuild.wasm", import.meta.url).href;
25
+ }
26
+ let compilerPromise;
27
+ /** Loaded once, memoized process-wide — mirrors how `getCanvasKit` is shared. */
28
+ function defaultCompiler(wasmUrl) {
29
+ return (compilerPromise ??= (async () => {
30
+ await esbuild.initialize({ wasmURL: wasmUrl ?? defaultCompilerWasmUrl() });
31
+ return async (source) => {
32
+ const { code } = await esbuild.transform(source, {
33
+ loader: "tsx",
34
+ format: "cjs",
35
+ target: "es2020",
36
+ tsconfigRaw: { compilerOptions: { experimentalDecorators: true } },
37
+ });
38
+ return code;
39
+ };
40
+ })());
41
+ }
42
+ /**
43
+ * Resolves a full `DynamicNodeConfig`, filling in whatever the caller left
44
+ * unset with `browser`'s own default. Synchronous — the WASM import happens
45
+ * on `compile`'s first actual call, not here, so attaching this to every
46
+ * engine costs nothing until a source string is genuinely registered.
47
+ */
48
+ export function resolveDynamicNodes(partial, compilerWasmUrl) {
49
+ return {
50
+ sdk: partial?.sdk ?? sdk,
51
+ jsxRuntime: partial?.jsxRuntime ?? jsxRuntime,
52
+ modules: partial?.modules,
53
+ compile: partial?.compile ?? (async (source) => (await defaultCompiler(compilerWasmUrl))(source)),
54
+ };
55
+ }
56
+ //# sourceMappingURL=dynamic-nodes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-nodes.js","sourceRoot":"","sources":["../src/dynamic-nodes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,wBAAwB,CAAC;AAC9C,OAAO,KAAK,UAAU,MAAM,oCAAoC,CAAC;AACjE,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AAExC;;;;;;;;;;;;;GAaG;AAEH,mFAAmF;AACnF,gFAAgF;AAChF,+EAA+E;AAC/E,8EAA8E;AAC9E,gEAAgE;AAChE,SAAS,sBAAsB;IAC3B,OAAO,IAAI,GAAG,CAAC,2BAA2B,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED,IAAI,eAAyE,CAAC;AAE9E,iFAAiF;AACjF,SAAS,eAAe,CAAC,OAAgB;IACrC,OAAO,CAAC,eAAe,KAAK,CAAC,KAAK,IAAI,EAAE;QACpC,MAAM,OAAO,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,sBAAsB,EAAE,EAAE,CAAC,CAAC;QAC3E,OAAO,KAAK,EAAE,MAAc,EAAE,EAAE;YAC5B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,MAAM,EAAE;gBAC7C,MAAM,EAAE,KAAK;gBACb,MAAM,EAAE,KAAK;gBACb,MAAM,EAAE,QAAQ;gBAChB,WAAW,EAAE,EAAE,eAAe,EAAE,EAAE,sBAAsB,EAAE,IAAI,EAAE,EAAE;aACrE,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QAChB,CAAC,CAAC;IACN,CAAC,CAAC,EAAE,CAAC,CAAC;AACV,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAC/B,OAA+C,EAC/C,eAAwB;IAExB,OAAO;QACH,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,GAAG;QACxB,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,UAAU;QAC7C,OAAO,EAAE,OAAO,EAAE,OAAO;QACzB,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,eAAe,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;KACpG,CAAC;AACN,CAAC","sourcesContent":["import type { DynamicNodeConfig } from \"@motionscript/core\";\nimport * as sdk from \"@motionscript/core/sdk\";\nimport * as jsxRuntime from \"@motionscript/core/jsx/jsx-runtime\";\nimport * as esbuild from \"esbuild-wasm\";\n\n/**\n * `browser`'s real, working default for `Registry.addNodeFromSource` — the\n * `sdk`/`jsxRuntime` halves are cheap (re-exported classes, no WASM) and\n * imported eagerly; the compiler is the expensive half, and stays behind a\n * lazy `import()` so an app that never registers a source-string node never\n * pays for it, however long the engine has been running.\n *\n * `core` has no compiler dependency of its own and cannot import the published\n * `motion-script` sdk barrel (that package depends on `core`, so the reverse\n * would be circular) — `browser` is where this concern actually belongs: the\n * package whose entire job is \"what running in an actual browser tab looks\n * like\" already owns `getCanvasKit`/`WebAudioSink`/`WebTicker` on exactly that\n * basis.\n */\n\n// Default to the wasm shipped by esbuild-wasm, resolved lazily — `import.meta.url`\n// is empty in a non-ESM output (an IIFE bundle, which is what the headless page\n// loads), and `new URL(\"…\", \"\")` throws on import, before a caller has had the\n// chance to pass an explicit URL that would have made the default irrelevant.\n// Mirrors `getCanvasKit`'s own `defaultWasmUrl` in `getter.ts`.\nfunction defaultCompilerWasmUrl(): string {\n return new URL(\"esbuild-wasm/esbuild.wasm\", import.meta.url).href;\n}\n\nlet compilerPromise: Promise<(source: string) => Promise<string>> | undefined;\n\n/** Loaded once, memoized process-wide — mirrors how `getCanvasKit` is shared. */\nfunction defaultCompiler(wasmUrl?: string): Promise<(source: string) => Promise<string>> {\n return (compilerPromise ??= (async () => {\n await esbuild.initialize({ wasmURL: wasmUrl ?? defaultCompilerWasmUrl() });\n return async (source: string) => {\n const { code } = await esbuild.transform(source, {\n loader: \"tsx\",\n format: \"cjs\",\n target: \"es2020\",\n tsconfigRaw: { compilerOptions: { experimentalDecorators: true } },\n });\n return code;\n };\n })());\n}\n\n/**\n * Resolves a full `DynamicNodeConfig`, filling in whatever the caller left\n * unset with `browser`'s own default. Synchronous — the WASM import happens\n * on `compile`'s first actual call, not here, so attaching this to every\n * engine costs nothing until a source string is genuinely registered.\n */\nexport function resolveDynamicNodes(\n partial: Partial<DynamicNodeConfig> | undefined,\n compilerWasmUrl?: string,\n): DynamicNodeConfig {\n return {\n sdk: partial?.sdk ?? sdk,\n jsxRuntime: partial?.jsxRuntime ?? jsxRuntime,\n modules: partial?.modules,\n compile: partial?.compile ?? (async (source) => (await defaultCompiler(compilerWasmUrl))(source)),\n };\n}\n"]}
@@ -0,0 +1,59 @@
1
+ import { Engine, EngineOptions, type DynamicNodeConfig, type NodeConstructor, type NodeTypeMap } from "@motionscript/core";
2
+ import { WebStorageAdapter } from "./storage-adapter";
3
+ import { type AudioEncoderOptions, type VideoEncoderOptions } from "./exporter";
4
+ export interface BrowserEngineOptions extends Omit<EngineOptions, "nodes" | "dynamicNodes"> {
5
+ /**
6
+ * Node types any document opened here may name — decorated classes as an
7
+ * array or a record naming each key outright, same as `EngineOptions`,
8
+ * plus a TS/JSX source string in the **array** form only. A string
9
+ * self-declares its key via `@node(...)` in its own source — the record
10
+ * form stays class-only, so there is exactly one place a dynamic node's
11
+ * key comes from, not two that could disagree.
12
+ */
13
+ nodes?: readonly (NodeConstructor | string)[] | NodeTypeMap;
14
+ /**
15
+ * Enables a source string in `nodes`, and `engine.registerNode(source)`
16
+ * at any point afterward. Any field left unset falls back to `browser`'s
17
+ * own default — `@motionscript/core/sdk` for `sdk`, a lazily-loaded
18
+ * `esbuild-wasm` for `compile` — so this never needs to be passed at all
19
+ * for the common case. `modules` has no default: only the host knows
20
+ * which loaded packages a script it compiles may reach.
21
+ */
22
+ dynamicNodes?: Partial<DynamicNodeConfig>;
23
+ /**
24
+ * Where `esbuild-wasm`'s own `.wasm` binary is served — only read if the
25
+ * default compiler is actually used. Mirrors `wasmUrl` for CanvasKit.
26
+ */
27
+ compilerWasmUrl?: string;
28
+ /** URL of the CanvasKit `.wasm` binary, forwarded to `getCanvasKit`. */
29
+ wasmUrl?: string;
30
+ /** Default video encoder settings for `session.export()`. Overridable per call. */
31
+ video?: VideoEncoderOptions;
32
+ /** Default audio encoder settings for `session.export()`. Overridable per call. */
33
+ audio?: AudioEncoderOptions;
34
+ }
35
+ /** What `session.export({ encode })` accepts in this environment. */
36
+ export interface BrowserEncodeOptions {
37
+ video?: VideoEncoderOptions;
38
+ audio?: AudioEncoderOptions;
39
+ supersample?: number;
40
+ /** Set false to get the bytes back instead of triggering a browser download. */
41
+ download?: boolean;
42
+ filename?: string;
43
+ }
44
+ /**
45
+ * An engine backed by the browser: WebGL/Skia surfaces, Web Audio, WebCodecs.
46
+ *
47
+ * Async because CanvasKit is a wasm module and every seam below needs it. Await
48
+ * once, at app start, and share the result — the caches underneath (registered
49
+ * fonts, decoded images, shaped paragraphs) are the point of keeping one engine
50
+ * rather than one per canvas.
51
+ *
52
+ * ```ts
53
+ * const engine = await createBrowserEngine({ nodes: { pulseRing: PulseRing } });
54
+ * const session = engine.open({ document, target: canvasEl });
55
+ * session.playback!.play();
56
+ * ```
57
+ */
58
+ export declare function createBrowserEngine(options?: BrowserEngineOptions): Promise<Engine<WebStorageAdapter>>;
59
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AACA,OAAO,EAEH,MAAM,EACN,aAAa,EAWb,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,WAAW,EACnB,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAkC,KAAK,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGhH,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,cAAc,CAAC;IACvF;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,eAAe,GAAG,MAAM,CAAC,EAAE,GAAG,WAAW,CAAC;IAC5D;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1C;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,mFAAmF;IACnF,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAED,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACjC,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AA6FD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACrC,OAAO,GAAE,oBAAyB,GACnC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAgEpC"}
package/dist/engine.js ADDED
@@ -0,0 +1,171 @@
1
+ import { Engine, } from "@motionscript/core";
2
+ import { getCanvasKit } from "./getter";
3
+ import { resolveDynamicNodes } from "./dynamic-nodes";
4
+ import { WebAudioSink } from "./audio/player";
5
+ import { WebTicker } from "./ticker";
6
+ import { WebMeasureContext } from "./measure-context";
7
+ import { WebRenderContext } from "./render-context";
8
+ import { WebStorageAdapter } from "./storage-adapter";
9
+ import { exportVideo as exportVideoFile } from "./exporter";
10
+ import { StillRenderer } from "./still";
11
+ /**
12
+ * The browser's {@link Exporter}: WebCodecs + mediabunny for video, the
13
+ * browser's own image codecs for a still.
14
+ *
15
+ * **It does not reuse a session's storage adapter, and that is deliberate.** An
16
+ * adapter's viewport clamps the resolution images decode at — `StorageAdapter`
17
+ * documents that it has no setter for exactly this reason — so handing an export
18
+ * the preview's adapter would paint a 2× render from decodes sized for 1×, and
19
+ * the result would be soft in a way nothing reports. What is safely shared is
20
+ * CanvasKit itself, which `getCanvasKit` already memoizes process-wide.
21
+ */
22
+ class WebExporter {
23
+ canvasKit;
24
+ defaults;
25
+ wasmUrl;
26
+ still = null;
27
+ stillRegistry = null;
28
+ constructor(canvasKit, defaults, wasmUrl) {
29
+ this.canvasKit = canvasKit;
30
+ this.defaults = defaults;
31
+ this.wasmUrl = wasmUrl;
32
+ }
33
+ async exportVideo(request) {
34
+ const encode = (request.encode ?? {});
35
+ return exportVideoFile({
36
+ document: request.project,
37
+ registry: request.registry,
38
+ scale: request.scale,
39
+ manifest: request.manifest,
40
+ includeAudio: request.includeAudio,
41
+ video: encode.video ?? this.defaults.video,
42
+ audio: encode.audio ?? this.defaults.audio,
43
+ supersample: encode.supersample,
44
+ filename: encode.filename,
45
+ // A `Session` asks for bytes; only an explicit `download: true` takes
46
+ // the DOM anchor path this function offers a page.
47
+ returnBytes: encode.download !== true,
48
+ onProgress: request.onProgress,
49
+ signal: request.signal,
50
+ wasmUrl: this.wasmUrl,
51
+ });
52
+ }
53
+ async exportStill(request) {
54
+ const renderer = this.renderer(request);
55
+ const drawn = await renderer.render(request.project, {
56
+ scale: request.scale,
57
+ ...frameOption(request.frame),
58
+ });
59
+ const bytes = await renderer.toBytes({
60
+ format: request.format,
61
+ quality: request.quality,
62
+ });
63
+ return { bytes, frame: drawn.frame, totalFrames: drawn.totalFrames };
64
+ }
65
+ dispose() {
66
+ this.still?.dispose();
67
+ this.still = null;
68
+ }
69
+ /**
70
+ * One `StillRenderer`, reused across calls and re-viewported when the ask
71
+ * changes — `setViewport` is a no-op when the size already matches, and a
72
+ * full reset when it does not, which is the same trade the class documents.
73
+ */
74
+ renderer(request) {
75
+ const { project, registry } = request;
76
+ if (project.viewport === undefined || project.fps === undefined) {
77
+ throw new Error("exportStill takes a project whose fps and viewport are resolved — see Session.");
78
+ }
79
+ // A renderer compiles against the registry it was built with.
80
+ if (this.still && this.stillRegistry !== registry)
81
+ this.dispose();
82
+ this.still ??= new StillRenderer(this.canvasKit, {
83
+ viewport: project.viewport,
84
+ fps: project.fps,
85
+ manifest: request.manifest,
86
+ registry,
87
+ });
88
+ this.stillRegistry = registry;
89
+ this.still.setViewport(project.viewport);
90
+ if (request.manifest)
91
+ this.still.setManifest(request.manifest);
92
+ return this.still;
93
+ }
94
+ }
95
+ /** `ExportStillRequest.frame` in the shape `StillRenderer.render` accepts. */
96
+ function frameOption(frame) {
97
+ if (frame.kind === "frame")
98
+ return { frame: frame.frame };
99
+ return { frame: frame.kind };
100
+ }
101
+ /**
102
+ * An engine backed by the browser: WebGL/Skia surfaces, Web Audio, WebCodecs.
103
+ *
104
+ * Async because CanvasKit is a wasm module and every seam below needs it. Await
105
+ * once, at app start, and share the result — the caches underneath (registered
106
+ * fonts, decoded images, shaped paragraphs) are the point of keeping one engine
107
+ * rather than one per canvas.
108
+ *
109
+ * ```ts
110
+ * const engine = await createBrowserEngine({ nodes: { pulseRing: PulseRing } });
111
+ * const session = engine.open({ document, target: canvasEl });
112
+ * session.playback!.play();
113
+ * ```
114
+ */
115
+ export async function createBrowserEngine(options = {}) {
116
+ const rawNodes = options.nodes;
117
+ // `Array.isArray` doesn't narrow a `readonly T[] | U` union cleanly against
118
+ // its `arg is any[]` (mutable) predicate — the `.filter` predicate below
119
+ // already guarantees no string survives, so the cast is just recovering
120
+ // what the runtime check already proved.
121
+ const classNodes = (Array.isArray(rawNodes)
122
+ ? rawNodes.filter((n) => typeof n !== "string")
123
+ : rawNodes);
124
+ const sourceNodes = Array.isArray(rawNodes)
125
+ ? rawNodes.filter((n) => typeof n === "string")
126
+ : [];
127
+ // Cheap — see dynamic-nodes.ts: no WASM import happens here, only on
128
+ // `compile`'s first actual call. Attached unconditionally so a later
129
+ // `engine.registerNode(source)` works even when `nodes` carried no
130
+ // string at boot.
131
+ const dynamicNodes = resolveDynamicNodes(options.dynamicNodes, options.compilerWasmUrl);
132
+ const canvasKit = await getCanvasKit(options.wasmUrl);
133
+ const exporter = new WebExporter(canvasKit, { video: options.video, audio: options.audio }, options.wasmUrl);
134
+ const platform = {
135
+ createStorageAdapter({ catalog, viewport, fps }) {
136
+ return new WebStorageAdapter(canvasKit, catalog, viewport, fps);
137
+ },
138
+ createRenderContext(storage) {
139
+ return new WebRenderContext(canvasKit, storage);
140
+ },
141
+ createMeasureContext(storage) {
142
+ return new WebMeasureContext(storage);
143
+ },
144
+ createAudioSink() {
145
+ return new WebAudioSink();
146
+ },
147
+ createTicker(fps, audioDevice) {
148
+ // Shares the device's own `AudioContext`: two independent timebases
149
+ // in one session drift against each other, and sound is the one that
150
+ // cannot be nudged back.
151
+ return new WebTicker({
152
+ context: audioDevice.getContext(),
153
+ fps,
154
+ });
155
+ },
156
+ createExporter() {
157
+ return exporter;
158
+ },
159
+ dispose() {
160
+ exporter.dispose();
161
+ },
162
+ };
163
+ const engine = new Engine(platform, { ...options, nodes: classNodes, dynamicNodes });
164
+ for (const source of sourceNodes) {
165
+ // Sequential — one script's error shouldn't mask another's, and
166
+ // registration order should match array order for determinism.
167
+ await engine.registerNode(source);
168
+ }
169
+ return engine;
170
+ }
171
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CanvasKit } from \"@motionscript/canvaskit\";\nimport {\n AudioSink,\n Engine,\n EngineOptions,\n EnginePlatform,\n EngineRenderContext,\n Exporter,\n ExportStillRequest,\n ExportVideoRequest,\n ExportedStill,\n Registry,\n Ticker,\n MeasureContext2D,\n PlatformDeps,\n type DynamicNodeConfig,\n type NodeConstructor,\n type NodeTypeMap,\n} from \"@motionscript/core\";\n\nimport { getCanvasKit } from \"./getter\";\nimport { resolveDynamicNodes } from \"./dynamic-nodes\";\nimport { WebAudioSink } from \"./audio/player\";\nimport { WebTicker } from \"./ticker\";\nimport { WebMeasureContext } from \"./measure-context\";\nimport { WebRenderContext } from \"./render-context\";\nimport { WebStorageAdapter } from \"./storage-adapter\";\nimport { exportVideo as exportVideoFile, type AudioEncoderOptions, type VideoEncoderOptions } from \"./exporter\";\nimport { StillRenderer } from \"./still\";\n\nexport interface BrowserEngineOptions extends Omit<EngineOptions, \"nodes\" | \"dynamicNodes\"> {\n /**\n * Node types any document opened here may name — decorated classes as an\n * array or a record naming each key outright, same as `EngineOptions`,\n * plus a TS/JSX source string in the **array** form only. A string\n * self-declares its key via `@node(...)` in its own source — the record\n * form stays class-only, so there is exactly one place a dynamic node's\n * key comes from, not two that could disagree.\n */\n nodes?: readonly (NodeConstructor | string)[] | NodeTypeMap;\n /**\n * Enables a source string in `nodes`, and `engine.registerNode(source)`\n * at any point afterward. Any field left unset falls back to `browser`'s\n * own default — `@motionscript/core/sdk` for `sdk`, a lazily-loaded\n * `esbuild-wasm` for `compile` — so this never needs to be passed at all\n * for the common case. `modules` has no default: only the host knows\n * which loaded packages a script it compiles may reach.\n */\n dynamicNodes?: Partial<DynamicNodeConfig>;\n /**\n * Where `esbuild-wasm`'s own `.wasm` binary is served — only read if the\n * default compiler is actually used. Mirrors `wasmUrl` for CanvasKit.\n */\n compilerWasmUrl?: string;\n /** URL of the CanvasKit `.wasm` binary, forwarded to `getCanvasKit`. */\n wasmUrl?: string;\n /** Default video encoder settings for `session.export()`. Overridable per call. */\n video?: VideoEncoderOptions;\n /** Default audio encoder settings for `session.export()`. Overridable per call. */\n audio?: AudioEncoderOptions;\n}\n\n/** What `session.export({ encode })` accepts in this environment. */\nexport interface BrowserEncodeOptions {\n video?: VideoEncoderOptions;\n audio?: AudioEncoderOptions;\n supersample?: number;\n /** Set false to get the bytes back instead of triggering a browser download. */\n download?: boolean;\n filename?: string;\n}\n\n/**\n * The browser's {@link Exporter}: WebCodecs + mediabunny for video, the\n * browser's own image codecs for a still.\n *\n * **It does not reuse a session's storage adapter, and that is deliberate.** An\n * adapter's viewport clamps the resolution images decode at — `StorageAdapter`\n * documents that it has no setter for exactly this reason — so handing an export\n * the preview's adapter would paint a 2× render from decodes sized for 1×, and\n * the result would be soft in a way nothing reports. What is safely shared is\n * CanvasKit itself, which `getCanvasKit` already memoizes process-wide.\n */\nclass WebExporter implements Exporter {\n private still: StillRenderer | null = null;\n private stillRegistry: Registry | null = null;\n\n constructor(\n private readonly canvasKit: CanvasKit,\n private readonly defaults: { video?: VideoEncoderOptions; audio?: AudioEncoderOptions },\n private readonly wasmUrl?: string,\n ) { }\n\n async exportVideo(request: ExportVideoRequest): Promise<Uint8Array | void> {\n const encode = (request.encode ?? {}) as BrowserEncodeOptions;\n return exportVideoFile({\n document: request.project,\n registry: request.registry,\n scale: request.scale,\n manifest: request.manifest,\n includeAudio: request.includeAudio,\n video: encode.video ?? this.defaults.video,\n audio: encode.audio ?? this.defaults.audio,\n supersample: encode.supersample,\n filename: encode.filename,\n // A `Session` asks for bytes; only an explicit `download: true` takes\n // the DOM anchor path this function offers a page.\n returnBytes: encode.download !== true,\n onProgress: request.onProgress,\n signal: request.signal,\n wasmUrl: this.wasmUrl,\n });\n }\n\n async exportStill(request: ExportStillRequest): Promise<ExportedStill> {\n const renderer = this.renderer(request);\n const drawn = await renderer.render(request.project, {\n scale: request.scale,\n ...frameOption(request.frame),\n });\n const bytes = await renderer.toBytes({\n format: request.format,\n quality: request.quality,\n });\n return { bytes, frame: drawn.frame, totalFrames: drawn.totalFrames };\n }\n\n dispose(): void {\n this.still?.dispose();\n this.still = null;\n }\n\n /**\n * One `StillRenderer`, reused across calls and re-viewported when the ask\n * changes — `setViewport` is a no-op when the size already matches, and a\n * full reset when it does not, which is the same trade the class documents.\n */\n private renderer(request: ExportStillRequest): StillRenderer {\n const { project, registry } = request;\n if (project.viewport === undefined || project.fps === undefined) {\n throw new Error(\"exportStill takes a project whose fps and viewport are resolved — see Session.\");\n }\n // A renderer compiles against the registry it was built with.\n if (this.still && this.stillRegistry !== registry) this.dispose();\n this.still ??= new StillRenderer(this.canvasKit, {\n viewport: project.viewport,\n fps: project.fps,\n manifest: request.manifest,\n registry,\n });\n this.stillRegistry = registry;\n this.still.setViewport(project.viewport);\n if (request.manifest) this.still.setManifest(request.manifest);\n return this.still;\n }\n}\n\n/** `ExportStillRequest.frame` in the shape `StillRenderer.render` accepts. */\nfunction frameOption(frame: ExportStillRequest[\"frame\"]): { frame?: number | \"first\" | \"last\" } {\n if (frame.kind === \"frame\") return { frame: frame.frame };\n return { frame: frame.kind };\n}\n\n/**\n * An engine backed by the browser: WebGL/Skia surfaces, Web Audio, WebCodecs.\n *\n * Async because CanvasKit is a wasm module and every seam below needs it. Await\n * once, at app start, and share the result — the caches underneath (registered\n * fonts, decoded images, shaped paragraphs) are the point of keeping one engine\n * rather than one per canvas.\n *\n * ```ts\n * const engine = await createBrowserEngine({ nodes: { pulseRing: PulseRing } });\n * const session = engine.open({ document, target: canvasEl });\n * session.playback!.play();\n * ```\n */\nexport async function createBrowserEngine(\n options: BrowserEngineOptions = {},\n): Promise<Engine<WebStorageAdapter>> {\n const rawNodes = options.nodes;\n // `Array.isArray` doesn't narrow a `readonly T[] | U` union cleanly against\n // its `arg is any[]` (mutable) predicate — the `.filter` predicate below\n // already guarantees no string survives, so the cast is just recovering\n // what the runtime check already proved.\n const classNodes = (\n Array.isArray(rawNodes)\n ? rawNodes.filter((n): n is NodeConstructor => typeof n !== \"string\")\n : rawNodes\n ) as NodeTypeMap | readonly NodeConstructor[] | undefined;\n const sourceNodes = Array.isArray(rawNodes)\n ? rawNodes.filter((n): n is string => typeof n === \"string\")\n : [];\n // Cheap — see dynamic-nodes.ts: no WASM import happens here, only on\n // `compile`'s first actual call. Attached unconditionally so a later\n // `engine.registerNode(source)` works even when `nodes` carried no\n // string at boot.\n const dynamicNodes = resolveDynamicNodes(options.dynamicNodes, options.compilerWasmUrl);\n\n const canvasKit = await getCanvasKit(options.wasmUrl);\n const exporter = new WebExporter(\n canvasKit,\n { video: options.video, audio: options.audio },\n options.wasmUrl,\n );\n\n const platform: EnginePlatform<WebStorageAdapter> = {\n createStorageAdapter({ catalog, viewport, fps }: PlatformDeps): WebStorageAdapter {\n return new WebStorageAdapter(canvasKit, catalog, viewport, fps);\n },\n createRenderContext(storage: WebStorageAdapter): EngineRenderContext {\n return new WebRenderContext(canvasKit, storage);\n },\n createMeasureContext(storage: WebStorageAdapter): MeasureContext2D {\n return new WebMeasureContext(storage);\n },\n createAudioSink(): AudioSink {\n return new WebAudioSink();\n },\n createTicker(fps: number, audioDevice: AudioSink): Ticker {\n // Shares the device's own `AudioContext`: two independent timebases\n // in one session drift against each other, and sound is the one that\n // cannot be nudged back.\n return new WebTicker({\n context: (audioDevice as WebAudioSink).getContext(),\n fps,\n });\n },\n createExporter(): Exporter {\n return exporter;\n },\n dispose(): void {\n exporter.dispose();\n },\n };\n\n const engine = new Engine<WebStorageAdapter>(platform, { ...options, nodes: classNodes, dynamicNodes });\n for (const source of sourceNodes) {\n // Sequential — one script's error shouldn't mask another's, and\n // registration order should match array order for determinism.\n await engine.registerNode(source);\n }\n return engine;\n}\n"]}