@pixerate/flair 0.2.0 → 0.4.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 (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +44 -8
  3. package/dist/catalog/index.d.ts +5 -4
  4. package/dist/catalog/index.js +12 -4
  5. package/dist/{chunk-3FAUZOGL.js → chunk-FIJOMGPG.js} +2 -2
  6. package/dist/{chunk-JWZCPMFG.js → chunk-GL37YCCM.js} +134 -680
  7. package/dist/chunk-GL37YCCM.js.map +1 -0
  8. package/dist/{chunk-UQQNZSYL.js → chunk-GTIAOYOK.js} +211 -7
  9. package/dist/chunk-GTIAOYOK.js.map +1 -0
  10. package/dist/{chunk-MWVTQFV5.js → chunk-IWKM5FJK.js} +410 -59
  11. package/dist/chunk-IWKM5FJK.js.map +1 -0
  12. package/dist/{chunk-HKXOHRCD.js → chunk-VA74HBPK.js} +2 -2
  13. package/dist/chunk-WAMA45G3.js +998 -0
  14. package/dist/chunk-WAMA45G3.js.map +1 -0
  15. package/dist/{chunk-J55UBNAT.js → chunk-WC3VADZC.js} +42 -36
  16. package/dist/chunk-WC3VADZC.js.map +1 -0
  17. package/dist/cli.js +12 -11
  18. package/dist/cli.js.map +1 -1
  19. package/dist/{controls-DdXMbovi.d.ts → controls-Syr6drzO.d.ts} +4 -3
  20. package/dist/editor/index.d.ts +12 -8
  21. package/dist/editor/index.js +26 -16
  22. package/dist/editor/index.js.map +1 -1
  23. package/dist/{export-FEBPEEO7.js → export-DH2HMACO.js} +4 -3
  24. package/dist/index.d.ts +193 -17
  25. package/dist/index.js +69 -33
  26. package/dist/index.js.map +1 -1
  27. package/dist/node.d.ts +1 -1
  28. package/dist/node.js +2 -2
  29. package/dist/recorder-KEX3U35D.js +10 -0
  30. package/dist/{renderer-Oyynz-fs.d.ts → renderer-DtnFkpkf.d.ts} +7 -3
  31. package/dist/types-CsFUZZIv.d.ts +102 -0
  32. package/package.json +1 -1
  33. package/dist/chunk-36DTNSHU.js +0 -160
  34. package/dist/chunk-36DTNSHU.js.map +0 -1
  35. package/dist/chunk-J55UBNAT.js.map +0 -1
  36. package/dist/chunk-JWZCPMFG.js.map +0 -1
  37. package/dist/chunk-MWVTQFV5.js.map +0 -1
  38. package/dist/chunk-UQQNZSYL.js.map +0 -1
  39. package/dist/recorder-WJTWMEQN.js +0 -10
  40. package/dist/types-CkKkavvB.d.ts +0 -71
  41. /package/dist/{chunk-3FAUZOGL.js.map → chunk-FIJOMGPG.js.map} +0 -0
  42. /package/dist/{chunk-HKXOHRCD.js.map → chunk-VA74HBPK.js.map} +0 -0
  43. /package/dist/{export-FEBPEEO7.js.map → export-DH2HMACO.js.map} +0 -0
  44. /package/dist/{recorder-WJTWMEQN.js.map → recorder-KEX3U35D.js.map} +0 -0
@@ -8,31 +8,21 @@ import {
8
8
  drawFrame,
9
9
  resetGlCaches,
10
10
  wrapSource
11
- } from "./chunk-HKXOHRCD.js";
12
- import {
13
- ManualClock
14
- } from "./chunk-WMZQCUFJ.js";
11
+ } from "./chunk-VA74HBPK.js";
15
12
  import {
16
13
  CAMERA_MS,
17
- DEFAULT_STAGE,
18
- areaOf,
19
- between,
20
- boundsOf,
21
- calculateFramingRect,
22
- easeInOutCubic,
23
- frame,
24
- isDeleteCommand,
25
- isElementCommand,
26
- isUpdateCommand,
27
- overlapArea,
28
- viewAt,
29
- visibleRect
30
- } from "./chunk-36DTNSHU.js";
14
+ CAMERA_STEP_MS,
15
+ EFFECT_TYPES,
16
+ MAX_TILT,
17
+ cameraAt,
18
+ cameraTrack,
19
+ sceneStage
20
+ } from "./chunk-WAMA45G3.js";
31
21
  import {
32
22
  controlValue,
33
23
  imageUrls,
34
24
  parseControls
35
- } from "./chunk-J55UBNAT.js";
25
+ } from "./chunk-WC3VADZC.js";
36
26
 
37
27
  // src/shader/renderer.ts
38
28
  var ShaderRenderer = class {
@@ -48,6 +38,7 @@ var ShaderRenderer = class {
48
38
  frame = 0;
49
39
  fpsFrames = 0;
50
40
  fpsSince = 0;
41
+ ownsCanvas = false;
51
42
  resizeObserver;
52
43
  mouse = [0, 0, 0, 0];
53
44
  disposed = false;
@@ -66,11 +57,21 @@ var ShaderRenderer = class {
66
57
  onContextLost;
67
58
  /** Called when an image control's URL cannot be loaded; the control shows black. */
68
59
  onImageError;
69
- /** Creates a canvas in `host` (with `className`) and starts the animation loop. Size the canvas with CSS. */
70
- constructor(host, className = "") {
71
- const canvas = document.createElement("canvas");
72
- canvas.className = className;
73
- host.appendChild(canvas);
60
+ /**
61
+ * Creates a canvas in `target` (with `className`), or attaches to an existing
62
+ * `HTMLCanvasElement`, and starts the animation loop. Size the canvas with CSS.
63
+ */
64
+ constructor(target, className = "") {
65
+ let canvas;
66
+ if (target instanceof HTMLCanvasElement || "tagName" in target && target.tagName === "CANVAS") {
67
+ canvas = target;
68
+ if (className) canvas.className = className;
69
+ } else {
70
+ canvas = document.createElement("canvas");
71
+ if (className) canvas.className = className;
72
+ target.appendChild(canvas);
73
+ this.ownsCanvas = true;
74
+ }
74
75
  this.canvas = canvas;
75
76
  this.gl = createContext(canvas, true);
76
77
  this.textures = this.gl && this.textureCache(this.gl);
@@ -190,7 +191,7 @@ var ShaderRenderer = class {
190
191
  this.textures = null;
191
192
  this.gl?.getExtension("WEBGL_lose_context")?.loseContext();
192
193
  this.gl = null;
193
- this.canvas.remove();
194
+ if (this.ownsCanvas) this.canvas.remove();
194
195
  }
195
196
  resize() {
196
197
  const w = Math.max(1, Math.round(this.canvas.clientWidth * this.pixelRatio));
@@ -1039,228 +1040,6 @@ function getGlslBlendFunction(mode) {
1039
1040
  }
1040
1041
  }
1041
1042
 
1042
- // src/time/noise.ts
1043
- var GOLDEN_GAMMA = 0x9e3779b97f4a7c15n;
1044
- var MULT1 = 0xbf58476d1ce4e5b9n;
1045
- var MULT2 = 0x94d049bb133111ebn;
1046
- var MASK64 = 0xffffffffffffffffn;
1047
- var BASE_MIX = 0x2545f4914f6cdd1dn;
1048
- var POW2_53 = 9007199254740992;
1049
- function hash64(x) {
1050
- let v = x + GOLDEN_GAMMA & MASK64;
1051
- v = (v ^ v >> 30n) * MULT1 & MASK64;
1052
- v = (v ^ v >> 27n) * MULT2 & MASK64;
1053
- return (v ^ v >> 31n) & MASK64;
1054
- }
1055
- function mixSeeds(parts) {
1056
- let h = BASE_MIX;
1057
- for (const p of parts) {
1058
- const val = typeof p === "bigint" ? p : BigInt(Math.floor(p));
1059
- h = hash64(h ^ hash64(val & MASK64));
1060
- }
1061
- return h;
1062
- }
1063
- function unitFloat(seed) {
1064
- const s = typeof seed === "bigint" ? seed : BigInt(Math.floor(seed));
1065
- const hashed = hash64(s);
1066
- const shifted = Number(hashed >> 11n);
1067
- return shifted / POW2_53;
1068
- }
1069
- function quinticFade(t) {
1070
- return t * t * t * (t * (t * 6 - 15) + 10);
1071
- }
1072
- function lattice(seed, i) {
1073
- const mixed = seed ^ hash64(i & MASK64);
1074
- return unitFloat(mixed) * 2 - 1;
1075
- }
1076
- function noise1d(seed, x) {
1077
- if (!Number.isFinite(x)) return 0;
1078
- const s = typeof seed === "bigint" ? seed : BigInt(Math.floor(seed));
1079
- const floorX = Math.floor(x);
1080
- const u = quinticFade(x - floorX);
1081
- const i = BigInt(Math.floor(floorX));
1082
- const a = lattice(s, i);
1083
- const b = lattice(s, i + 1n);
1084
- return a + (b - a) * u;
1085
- }
1086
- function noise3d(x, y, z, seed = 0) {
1087
- if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
1088
- return 0;
1089
- }
1090
- const s = typeof seed === "bigint" ? seed : BigInt(Math.floor(seed));
1091
- const fx = Math.floor(x);
1092
- const fy = Math.floor(y);
1093
- const fz = Math.floor(z);
1094
- const u = quinticFade(x - fx);
1095
- const v = quinticFade(y - fy);
1096
- const w = quinticFade(z - fz);
1097
- const ix = BigInt(fx);
1098
- const iy = BigInt(fy);
1099
- const iz = BigInt(fz);
1100
- const g = (dx, dy, dz) => {
1101
- const combined = mixSeeds([s, ix + dx, iy + dy, iz + dz]);
1102
- return unitFloat(combined) * 2 - 1;
1103
- };
1104
- const lerp = (a, b, t) => a + (b - a) * t;
1105
- const x00 = lerp(g(0n, 0n, 0n), g(1n, 0n, 0n), u);
1106
- const x10 = lerp(g(0n, 1n, 0n), g(1n, 1n, 0n), u);
1107
- const x01 = lerp(g(0n, 0n, 1n), g(1n, 0n, 1n), u);
1108
- const x11 = lerp(g(0n, 1n, 1n), g(1n, 1n, 1n), u);
1109
- const y0 = lerp(x00, x10, v);
1110
- const y1 = lerp(x01, x11, v);
1111
- return lerp(y0, y1, w);
1112
- }
1113
- function fractalWiggle(freq, amp, octaves = 3, ampMult = 0.5, t = 0, seed = 42, dimension = 0) {
1114
- const oct = Math.max(1, Math.min(16, Math.floor(octaves)));
1115
- let currentAmp = amp;
1116
- let currentFreq = freq;
1117
- let sum = 0;
1118
- const baseSeed = BigInt(seed);
1119
- for (let k = 0; k < oct; k++) {
1120
- const s = mixSeeds([baseSeed, BigInt(dimension), BigInt(k)]);
1121
- const phaseOffset = unitFloat(s) * 1e3;
1122
- sum += currentAmp * noise1d(s, currentFreq * t + phaseOffset);
1123
- currentAmp *= ampMult;
1124
- currentFreq *= 2;
1125
- }
1126
- return sum;
1127
- }
1128
-
1129
- // src/scene/effects.ts
1130
- var EFFECT_TYPES = [
1131
- "gradient",
1132
- "flow",
1133
- "glow",
1134
- "spotlight",
1135
- "shimmer",
1136
- "ripple",
1137
- "timer",
1138
- "counter",
1139
- "highlight",
1140
- "blur",
1141
- "burst",
1142
- "media"
1143
- ];
1144
- var EFFECT_FADE_MS = 300;
1145
- function effectGone(running, now) {
1146
- if (running.until !== void 0) {
1147
- return now >= running.until + EFFECT_FADE_MS;
1148
- }
1149
- if (running.effect.duration !== void 0) {
1150
- return now >= running.from + running.effect.duration + EFFECT_FADE_MS;
1151
- }
1152
- return false;
1153
- }
1154
- function effectOpacity(running, now) {
1155
- if (now < running.from) return 0;
1156
- const inFade = Math.min(1, (now - running.from) / 150);
1157
- if (running.until !== void 0) {
1158
- const out = Math.max(0, 1 - (now - running.until) / EFFECT_FADE_MS);
1159
- return inFade * out;
1160
- }
1161
- if (running.effect.duration !== void 0) {
1162
- const remaining = running.from + running.effect.duration - now;
1163
- if (remaining < 0) {
1164
- const out = Math.max(0, 1 + remaining / EFFECT_FADE_MS);
1165
- return inFade * out;
1166
- }
1167
- }
1168
- return inFade;
1169
- }
1170
-
1171
- // src/scene/check.ts
1172
- var NO_CHECK = { framing: [], overlaps: [], edges: [], underTitle: [] };
1173
- var SMALL_TEXT_PX = 24;
1174
- function titleBox(title, stage) {
1175
- const width = Math.min(stage.width, title.length * 20 + 64);
1176
- return { x: (stage.width - width) / 2, y: 24, width, height: 60 };
1177
- }
1178
- function touchedIds(shot) {
1179
- const ids = /* @__PURE__ */ new Set();
1180
- for (const cmd of shot.run ?? []) {
1181
- if (isElementCommand(cmd)) {
1182
- ids.add(cmd.element.id);
1183
- } else if ("id" in cmd && typeof cmd.id === "string") {
1184
- ids.add(cmd.id);
1185
- }
1186
- }
1187
- return [...ids];
1188
- }
1189
- var IN_FRAME = 0.95;
1190
- function checkShot(shot, elements, view, stage, title) {
1191
- const frameRect = visibleRect(view, stage);
1192
- const touched = new Set(touchedIds(shot));
1193
- const framing = [];
1194
- const edges = [];
1195
- const overlaps = [];
1196
- const activeElements = elements.filter((e) => !e.isDeleted && (e.opacity === void 0 || e.opacity > 0));
1197
- for (const el of activeElements) {
1198
- const box = boundsOf(el);
1199
- const overlap = overlapArea(box, frameRect);
1200
- const boxArea = areaOf(box);
1201
- if (touched.has(el.id)) {
1202
- if (overlap === 0) {
1203
- framing.push({ id: el.id, how: "outside" });
1204
- } else if (boxArea > 0 && overlap / boxArea < IN_FRAME) {
1205
- framing.push({ id: el.id, how: "cut" });
1206
- }
1207
- } else if (overlap > 0 && boxArea > 0 && overlap / boxArea < IN_FRAME) {
1208
- edges.push(el.id);
1209
- }
1210
- }
1211
- const touchedList = activeElements.filter((e) => touched.has(e.id));
1212
- for (let i = 0; i < touchedList.length; i++) {
1213
- for (let j = i + 1; j < touchedList.length; j++) {
1214
- const a = touchedList[i];
1215
- const b = touchedList[j];
1216
- const aBox = boundsOf(a);
1217
- const bBox = boundsOf(b);
1218
- const ol = overlapArea(aBox, bBox);
1219
- const minArea = Math.min(areaOf(aBox), areaOf(bBox));
1220
- if (minArea > 0 && ol / minArea > 0.3) {
1221
- overlaps.push([a.id, b.id]);
1222
- }
1223
- }
1224
- }
1225
- const smallTexts = [];
1226
- let smallestText;
1227
- for (const el of activeElements) {
1228
- if (el.text && el.fontSize) {
1229
- const px = Math.round(el.fontSize * view.zoom);
1230
- const item = { id: el.id, text: el.text, px };
1231
- if (!smallestText || px < smallestText.px) {
1232
- smallestText = item;
1233
- }
1234
- if (px < SMALL_TEXT_PX) {
1235
- smallTexts.push(item);
1236
- }
1237
- }
1238
- }
1239
- const underTitle = [];
1240
- if (title) {
1241
- const tBox = titleBox(title, stage);
1242
- const sceneTBox = {
1243
- x: tBox.x / view.zoom - view.scrollX,
1244
- y: tBox.y / view.zoom - view.scrollY,
1245
- width: tBox.width / view.zoom,
1246
- height: tBox.height / view.zoom
1247
- };
1248
- for (const el of activeElements) {
1249
- if (overlapArea(boundsOf(el), sceneTBox) > 0) {
1250
- underTitle.push(el.id);
1251
- }
1252
- }
1253
- }
1254
- return {
1255
- framing,
1256
- overlaps,
1257
- smallestText,
1258
- smallTexts: smallTexts.length ? smallTexts : void 0,
1259
- edges,
1260
- underTitle
1261
- };
1262
- }
1263
-
1264
1043
  // src/scene/validate.ts
1265
1044
  var LONG_CAPTION = 120;
1266
1045
  var LONG_TITLE = 60;
@@ -1297,9 +1076,22 @@ function validateScene(scene) {
1297
1076
  if (!Array.isArray(shot.camera.frame) || shot.camera.frame.length === 0) {
1298
1077
  say(shotNum, "Dynamic camera framing requires a non-empty frame array of element IDs.");
1299
1078
  }
1300
- } else if (shot.camera.width <= 0 || shot.camera.height <= 0) {
1079
+ } else if ("width" in shot.camera && (shot.camera.width <= 0 || shot.camera.height <= 0)) {
1301
1080
  say(shotNum, "Camera rectangle width and height must be positive.");
1302
1081
  }
1082
+ const cam = shot.camera;
1083
+ const angle = (name, max) => {
1084
+ const v = cam[name];
1085
+ if (v !== void 0 && !(typeof v === "number" && Math.abs(v) <= max))
1086
+ say(shotNum, `Camera ${name} is degrees between -${max} and ${max}.`);
1087
+ };
1088
+ angle("tilt", MAX_TILT);
1089
+ angle("turn", MAX_TILT);
1090
+ angle("roll", 180);
1091
+ if (cam.perspective !== void 0 && !(typeof cam.perspective === "number" && cam.perspective >= 0.3))
1092
+ say(shotNum, "Camera perspective is the lens distance as a multiple of the stage width, 0.3 or more (1.2 by default).");
1093
+ if (cam.motionBlur !== void 0 && !(typeof cam.motionBlur === "number" && cam.motionBlur >= 0 && cam.motionBlur <= 3))
1094
+ say(shotNum, "Camera motionBlur is 0 (none) to 3 (a whip); 1 is natural.");
1303
1095
  }
1304
1096
  if (shot.hold !== void 0 && shot.hold < 0) {
1305
1097
  say(shotNum, "Shot hold cannot be negative.");
@@ -1347,381 +1139,19 @@ function validateScene(scene) {
1347
1139
  return issues;
1348
1140
  }
1349
1141
 
1350
- // src/scene/report.ts
1351
- function reviewScene(scene, logs, options = {}) {
1352
- const warnings = [];
1353
- const notes = [];
1354
- const minTextPx = options.minTextPx ?? 18;
1355
- const totalMs = logs.length > 0 ? logs[logs.length - 1].end : 0;
1356
- const totalSec = (totalMs / 1e3).toFixed(1);
1357
- if (scene.target) {
1358
- const [minS, maxS] = scene.target;
1359
- const durS = totalMs / 1e3;
1360
- if (durS < minS) {
1361
- warnings.push(`Scene is ${totalSec}s, shorter than target minimum (${minS}s).`);
1362
- } else if (durS > maxS) {
1363
- warnings.push(`Scene is ${totalSec}s, longer than target maximum (${maxS}s).`);
1364
- }
1365
- }
1366
- for (const log of logs) {
1367
- const shotPrefix = `Shot ${log.index}:`;
1368
- if (log.failed.length > 0) {
1369
- warnings.push(`${shotPrefix} ${log.failed.length} command(s) failed: ${log.failed.join(", ")}`);
1370
- }
1371
- for (const f of log.framing) {
1372
- warnings.push(`${shotPrefix} element "${f.id}" is ${f.how} of the camera frame.`);
1373
- }
1374
- for (const [a, b] of log.overlaps) {
1375
- notes.push(`${shotPrefix} elements "${a}" and "${b}" overlap significantly.`);
1376
- }
1377
- if (log.smallTexts) {
1378
- for (const t of log.smallTexts) {
1379
- if (t.px < minTextPx) {
1380
- warnings.push(`${shotPrefix} text "${t.text}" is only ${t.px}px on screen (less than ${minTextPx}px minimum).`);
1381
- }
1382
- }
1383
- }
1384
- for (const id of log.underTitle) {
1385
- warnings.push(`${shotPrefix} element "${id}" is partially covered by the title bar.`);
1386
- }
1387
- }
1388
- const lines = [
1389
- `# Scene Preview Review: ${scene.title || scene.name}`,
1390
- `**Total Duration:** ${totalSec}s (${logs.length} shots)`,
1391
- ""
1392
- ];
1393
- if (warnings.length > 0) {
1394
- lines.push("### \u26A0\uFE0F Warnings to Address:");
1395
- for (const w of warnings) lines.push(`- ${w}`);
1396
- lines.push("");
1397
- } else {
1398
- lines.push("\u2705 No critical layout or framing warnings.");
1399
- lines.push("");
1400
- }
1401
- if (notes.length > 0) {
1402
- lines.push("### \u2139\uFE0F Layout Notes:");
1403
- for (const n of notes) lines.push(`- ${n}`);
1404
- lines.push("");
1405
- }
1406
- lines.push("### Shot Breakdown");
1407
- lines.push("| Shot | Start | End | Duration | Title / Caption |");
1408
- lines.push("|:---:|:---:|:---:|:---:|:---|");
1409
- for (const log of logs) {
1410
- const dur = ((log.end - log.start) / 1e3).toFixed(1);
1411
- const text = log.title || log.caption || "\u2014";
1412
- lines.push(`| ${log.index} | ${(log.start / 1e3).toFixed(1)}s | ${(log.end / 1e3).toFixed(1)}s | ${dur}s | ${text.slice(0, 40)} |`);
1413
- }
1414
- return {
1415
- totalMs,
1416
- warnings,
1417
- notes,
1418
- markdown: lines.join("\n")
1419
- };
1420
- }
1421
-
1422
- // src/scene/behavior.ts
1423
- function evaluateWriggle(t, freq, amount, octaves = 3, seed = 42) {
1424
- return fractalWiggle(freq, amount, octaves, 0.5, t, seed, 0);
1425
- }
1426
- function evaluateBehavior(behavior, elapsedSec, durationSec, baseValue = 0) {
1427
- const t = Math.max(0, elapsedSec);
1428
- switch (behavior.type) {
1429
- case "oscillate": {
1430
- const freq = behavior.frequency;
1431
- const phase = behavior.phase ?? 0;
1432
- const angle = 2 * Math.PI * freq * t + phase;
1433
- let waveVal = 0;
1434
- switch (behavior.wave) {
1435
- case "square":
1436
- waveVal = Math.sin(angle) >= 0 ? 1 : -1;
1437
- break;
1438
- case "triangle":
1439
- waveVal = 2 / Math.PI * Math.asin(Math.sin(angle));
1440
- break;
1441
- case "sawtooth": {
1442
- const norm = angle / (2 * Math.PI) % 1;
1443
- waveVal = 2 * (norm < 0 ? norm + 1 : norm) - 1;
1444
- break;
1445
- }
1446
- case "sine":
1447
- default:
1448
- waveVal = Math.sin(angle);
1449
- break;
1450
- }
1451
- const offset = waveVal * behavior.amplitude;
1452
- return behavior.center !== void 0 ? behavior.center + offset : baseValue + offset;
1453
- }
1454
- case "ramp": {
1455
- const from = behavior.from ?? baseValue;
1456
- const to = behavior.to;
1457
- const p = durationSec > 0 ? Math.min(1, Math.max(0, t / durationSec)) : 1;
1458
- let eased = p;
1459
- switch (behavior.ease) {
1460
- case "easeIn":
1461
- eased = p * p;
1462
- break;
1463
- case "easeOut":
1464
- eased = p * (2 - p);
1465
- break;
1466
- case "easeInOut":
1467
- eased = p < 0.5 ? 2 * p * p : -1 + (4 - 2 * p) * p;
1468
- break;
1469
- case "linear":
1470
- default:
1471
- eased = p;
1472
- break;
1473
- }
1474
- return from + (to - from) * eased;
1475
- }
1476
- case "rate": {
1477
- return baseValue + behavior.rate * t;
1478
- }
1479
- case "wriggle": {
1480
- const wander = evaluateWriggle(
1481
- t,
1482
- behavior.frequency,
1483
- behavior.amount,
1484
- behavior.octaves ?? 3,
1485
- behavior.seed ?? 42
1486
- );
1487
- return baseValue + wander;
1488
- }
1489
- case "overshoot": {
1490
- const from = behavior.startValue ?? baseValue;
1491
- const target = behavior.targetValue;
1492
- const cycles = behavior.cycles ?? 3;
1493
- const damping = behavior.damping ?? 4;
1494
- if (durationSec <= 0) return target;
1495
- const p = Math.min(1, t / durationSec);
1496
- if (p >= 1) return target;
1497
- const envelope = Math.exp(-damping * p);
1498
- const oscillation = Math.cos(cycles * Math.PI * p);
1499
- const progress = 1 - envelope * oscillation;
1500
- return from + (target - from) * progress;
1501
- }
1502
- case "throw": {
1503
- const v0 = behavior.initialVelocity;
1504
- const k = Math.max(1e-3, behavior.friction ?? 2);
1505
- const displacement = v0 / k * (1 - Math.exp(-k * t));
1506
- return baseValue + displacement;
1507
- }
1508
- }
1509
- }
1510
-
1511
- // src/scene/player.ts
1512
- var MemorySceneBoard = class {
1513
- view;
1514
- elMap = /* @__PURE__ */ new Map();
1515
- currentTitle = "";
1516
- currentCaption = "";
1517
- runningEffects = [];
1518
- constructor(stage = DEFAULT_STAGE) {
1519
- this.view = viewAt(stage.width / 2, stage.height / 2, 1, stage);
1520
- }
1521
- getView() {
1522
- return this.view;
1523
- }
1524
- setView(view) {
1525
- this.view = view;
1526
- }
1527
- elements() {
1528
- return Array.from(this.elMap.values());
1529
- }
1530
- runCommand(cmd) {
1531
- if (isElementCommand(cmd)) {
1532
- this.elMap.set(cmd.element.id, { ...cmd.element });
1533
- } else if (isUpdateCommand(cmd)) {
1534
- const existing = this.elMap.get(cmd.id);
1535
- if (existing) {
1536
- this.elMap.set(cmd.id, { ...existing, ...cmd.props });
1537
- }
1538
- } else if (isDeleteCommand(cmd)) {
1539
- this.elMap.delete(cmd.id);
1540
- } else if (cmd.type === "clear") {
1541
- this.elMap.clear();
1542
- }
1543
- }
1544
- setTitle(text) {
1545
- this.currentTitle = text;
1546
- }
1547
- setCaption(text) {
1548
- this.currentCaption = text;
1549
- }
1550
- setEffects(effects) {
1551
- this.runningEffects = effects;
1552
- }
1553
- };
1554
- async function playScene(scene, board, clock, options = {}) {
1555
- const stage = {
1556
- width: scene.stage?.width ?? DEFAULT_STAGE.width,
1557
- height: scene.stage?.height ?? DEFAULT_STAGE.height,
1558
- caption: scene.stage?.caption ?? DEFAULT_STAGE.caption
1559
- };
1560
- const shots = [];
1561
- const begin = clock.now();
1562
- let runningEffects = [];
1563
- const endsAt = /* @__PURE__ */ new Map();
1564
- const updateEffects = (next) => {
1565
- runningEffects = next.filter((r) => !effectGone(r, clock.now()));
1566
- board.setEffects?.(runningEffects);
1567
- };
1568
- for (const [idx, shot] of scene.shots.entries()) {
1569
- const shotStart = clock.now() - begin;
1570
- const failedCmds = [];
1571
- const stopIds = new Set(shot.stopEffects ?? []);
1572
- if (stopIds.size > 0 || runningEffects.some((r) => endsAt.get(r) === idx)) {
1573
- const now = clock.now();
1574
- updateEffects(
1575
- runningEffects.map(
1576
- (r) => r.until === void 0 && (stopIds.has(r.effect.id) || endsAt.get(r) === idx) ? { ...r, until: now } : r
1577
- )
1578
- );
1579
- }
1580
- if (shot.camera) {
1581
- let targetRect;
1582
- if ("frame" in shot.camera) {
1583
- const frameIds = new Set(shot.camera.frame);
1584
- const targetElements = board.elements().filter((el) => frameIds.has(el.id));
1585
- if (targetElements.length === 0 && shot.run) {
1586
- for (const cmd of shot.run) {
1587
- if (isElementCommand(cmd) && frameIds.has(cmd.element.id)) {
1588
- targetElements.push(cmd.element);
1589
- }
1590
- }
1591
- }
1592
- targetRect = calculateFramingRect(targetElements, stage, {
1593
- padding: shot.camera.padding,
1594
- minWidth: shot.camera.minWidth,
1595
- minHeight: shot.camera.minHeight
1596
- });
1597
- } else {
1598
- targetRect = shot.camera;
1599
- }
1600
- const targetView = frame(targetRect, stage);
1601
- if (idx === 0) {
1602
- board.setView(targetView);
1603
- } else {
1604
- const fromView = board.getView();
1605
- const steps = 15;
1606
- const stepMs = CAMERA_MS / steps;
1607
- for (let s = 1; s <= steps; s++) {
1608
- const t = easeInOutCubic(s / steps);
1609
- board.setView(between(fromView, targetView, t, stage));
1610
- await clock.sleep(stepMs);
1611
- }
1612
- }
1613
- }
1614
- if (shot.title !== void 0) {
1615
- board.setTitle?.(shot.title, 1);
1616
- }
1617
- if (shot.caption !== void 0) {
1618
- board.setCaption?.(shot.caption);
1619
- }
1620
- const startEffects = (shot.effects ?? []).filter((e) => e.at === "start");
1621
- if (startEffects.length > 0) {
1622
- const now = clock.now();
1623
- const added = startEffects.map((effect) => ({ effect, from: now }));
1624
- for (const r of added) if (r.effect.shots) endsAt.set(r, idx + r.effect.shots);
1625
- updateEffects([...runningEffects, ...added]);
1626
- }
1627
- for (const cmd of shot.run ?? []) {
1628
- try {
1629
- await board.runCommand(cmd);
1630
- } catch (err) {
1631
- failedCmds.push(err.message || String(err));
1632
- }
1633
- }
1634
- const drawnEffects = (shot.effects ?? []).filter((e) => e.at !== "start");
1635
- if (drawnEffects.length > 0) {
1636
- const now = clock.now();
1637
- const added = drawnEffects.map((effect) => ({ effect, from: now }));
1638
- for (const r of added) if (r.effect.shots) endsAt.set(r, idx + r.effect.shots);
1639
- updateEffects([...runningEffects, ...added]);
1640
- }
1641
- const drawnTime = clock.now() - begin;
1642
- const check = options.check ? checkShot(shot, board.elements(), board.getView(), stage, shot.title) : { framing: [], overlaps: [], edges: [], underTitle: [] };
1643
- const holdMs = shot.hold ?? 800;
1644
- const behaviors = shot.behaviors ?? [];
1645
- if (behaviors.length > 0) {
1646
- const initialProps = /* @__PURE__ */ new Map();
1647
- for (const b of behaviors) {
1648
- const key = `${b.target}:${b.property}`;
1649
- const el = board.elements().find((e) => e.id === b.target);
1650
- if (el && typeof el[b.property] === "number") {
1651
- initialProps.set(key, el[b.property]);
1652
- } else {
1653
- initialProps.set(key, 0);
1654
- }
1655
- }
1656
- const stepMs = 50;
1657
- const steps = Math.max(1, Math.round(holdMs / stepMs));
1658
- const actualStepMs = holdMs / steps;
1659
- const durationSec = holdMs / 1e3;
1660
- for (let s = 1; s <= steps; s++) {
1661
- await clock.sleep(actualStepMs);
1662
- const elapsedSec = s * actualStepMs / 1e3;
1663
- for (const b of behaviors) {
1664
- const baseVal = initialProps.get(`${b.target}:${b.property}`) ?? 0;
1665
- const nextVal = evaluateBehavior(b, elapsedSec, durationSec, baseVal);
1666
- board.runCommand({
1667
- type: "update",
1668
- id: b.target,
1669
- props: { [b.property]: nextVal }
1670
- });
1671
- }
1672
- }
1673
- } else {
1674
- await clock.sleep(holdMs);
1675
- }
1676
- const shotEnd = clock.now() - begin;
1677
- const log = {
1678
- ...check,
1679
- index: idx + 1,
1680
- start: shotStart,
1681
- drawn: drawnTime,
1682
- end: shotEnd,
1683
- caption: shot.caption,
1684
- title: shot.title,
1685
- failed: failedCmds,
1686
- effects: runningEffects.map((r) => r.effect.id)
1687
- };
1688
- shots.push(log);
1689
- if (options.afterShot) {
1690
- await options.afterShot(log);
1691
- }
1692
- }
1693
- const duration = clock.now() - begin;
1694
- const ok = shots.every((s) => s.failed.length === 0 && s.framing.every((f) => f.how !== "outside"));
1695
- return { ok, duration, shots };
1696
- }
1697
- async function simulateScene(scene, options = {}) {
1698
- const clock = new ManualClock();
1699
- const board = new MemorySceneBoard();
1700
- let done = false;
1701
- let playError = null;
1702
- const playPromise = playScene(scene, board, clock, { check: true }).then((res) => {
1703
- done = true;
1704
- return res;
1705
- }).catch((err) => {
1706
- done = true;
1707
- playError = err;
1708
- throw err;
1709
- });
1710
- for (let i = 0; i < 5e3 && !done; i++) {
1711
- await clock.advance(50);
1712
- }
1713
- if (playError) throw playError;
1714
- const result = await playPromise;
1715
- const review = reviewScene(scene, result.shots, {
1716
- target: scene.target,
1717
- minTextPx: options.minTextPx
1142
+ // src/scene/standalone.ts
1143
+ function planShots(scene, stage) {
1144
+ return cameraTrack(scene).map((entry) => {
1145
+ const steps = Math.round(entry.moveMs / CAMERA_STEP_MS);
1146
+ const move = Array.from({ length: Math.max(0, steps - 1) }, (_, i) => cameraAt(entry, (i + 1) * CAMERA_STEP_MS, stage));
1147
+ move.push(entry.to);
1148
+ return { move, elements: entry.elements };
1718
1149
  });
1719
- return { result, review };
1720
1150
  }
1721
-
1722
- // src/scene/standalone.ts
1723
1151
  function standaloneSceneHtml(scene) {
1152
+ const stage = sceneStage(scene);
1724
1153
  const serializedScene = JSON.stringify(scene).replace(/</g, "\\u003c");
1154
+ const serializedPlan = JSON.stringify({ stage, cameraMs: CAMERA_MS, shots: planShots(scene, stage) }).replace(/</g, "\\u003c");
1725
1155
  const title = scene.title || scene.name;
1726
1156
  return `<!DOCTYPE html>
1727
1157
  <html lang="en">
@@ -1747,7 +1177,7 @@ function standaloneSceneHtml(scene) {
1747
1177
  position: relative;
1748
1178
  width: 100%;
1749
1179
  max-width: 1200px;
1750
- aspect-ratio: 16 / 9;
1180
+ aspect-ratio: ${stage.width} / ${stage.height};
1751
1181
  background: #000;
1752
1182
  border-radius: 12px;
1753
1183
  overflow: hidden;
@@ -1762,6 +1192,12 @@ function standaloneSceneHtml(scene) {
1762
1192
  height: 100%;
1763
1193
  overflow: hidden;
1764
1194
  }
1195
+ .board {
1196
+ position: absolute;
1197
+ inset: 0;
1198
+ transform-origin: 50% 50%;
1199
+ will-change: transform, filter;
1200
+ }
1765
1201
  canvas.layer {
1766
1202
  position: absolute;
1767
1203
  top: 0;
@@ -1858,7 +1294,8 @@ function standaloneSceneHtml(scene) {
1858
1294
  <div class="player-container">
1859
1295
  <div class="viewport" id="viewport">
1860
1296
  <canvas id="shaderCanvas" class="layer"></canvas>
1861
- <canvas id="sceneCanvas" class="layer"></canvas>
1297
+ <div id="board" class="board"><canvas id="sceneCanvas" class="layer"></canvas></div>
1298
+ <svg width="0" height="0" style="position:absolute" aria-hidden="true"><filter id="motionBlur" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur id="motionBlurAmount" stdDeviation="0 0"/></filter></svg>
1862
1299
  <div id="titleEl" class="overlay-title"></div>
1863
1300
  <div id="captionEl" class="overlay-caption"></div>
1864
1301
  </div>
@@ -1875,6 +1312,9 @@ function standaloneSceneHtml(scene) {
1875
1312
 
1876
1313
  <script>
1877
1314
  const scene = ${serializedScene};
1315
+ // Each shot's camera move (zoom, scroll, 3D angle and blur at each step) and elements, worked out in advance.
1316
+ const plan = ${serializedPlan};
1317
+ const boardEl = document.getElementById('board');
1878
1318
  const sceneCanvas = document.getElementById('sceneCanvas');
1879
1319
  const shaderCanvas = document.getElementById('shaderCanvas');
1880
1320
  const ctx = sceneCanvas.getContext('2d');
@@ -1888,7 +1328,8 @@ function standaloneSceneHtml(scene) {
1888
1328
 
1889
1329
  let currentShotIdx = 0;
1890
1330
  let isPlaying = false;
1891
- let shotStartTime = 0;
1331
+ let shotStartTime = performance.now();
1332
+ let moveFrom = performance.now();
1892
1333
  let gl = null;
1893
1334
  let shaderProgram = null;
1894
1335
 
@@ -1970,64 +1411,98 @@ function standaloneSceneHtml(scene) {
1970
1411
  progressEl.style.width = pct + '%';
1971
1412
  }
1972
1413
 
1414
+ // The camera at time 'now': a step of the current shot's move, or where it settled.
1415
+ function cameraNow(now) {
1416
+ const move = plan.shots[currentShotIdx].move;
1417
+ const u = Math.min(1, (now - moveFrom) / plan.cameraMs);
1418
+ return move[Math.min(move.length - 1, Math.floor(u * move.length))];
1419
+ }
1420
+
1421
+ function applyCamera(view) {
1422
+ const w = boardEl.clientWidth || plan.stage.width;
1423
+ const angled = view.tilt || view.turn || view.roll;
1424
+ boardEl.style.transform = angled
1425
+ ? 'perspective(' + (view.perspective || 1.2) * w + 'px) rotateX(' + (view.tilt || 0) + 'deg) rotateY(' + (view.turn || 0) + 'deg) rotateZ(' + (view.roll || 0) + 'deg)'
1426
+ : 'none';
1427
+ // Motion blur streaks along a pan (taken back through the roll, as the board is blurred before it turns).
1428
+ const blur = (view.blur || 0) * w / plan.stage.width;
1429
+ if (blur >= 0.05) {
1430
+ let sx = blur, sy = blur;
1431
+ if (view.blurAngle !== undefined) {
1432
+ const a = (view.blurAngle - (view.roll || 0)) * Math.PI / 180;
1433
+ sx = Math.max(blur * 0.15, blur * Math.abs(Math.cos(a)));
1434
+ sy = Math.max(blur * 0.15, blur * Math.abs(Math.sin(a)));
1435
+ }
1436
+ document.getElementById('motionBlurAmount').setAttribute('stdDeviation', sx.toFixed(2) + ' ' + sy.toFixed(2));
1437
+ boardEl.style.filter = 'url(#motionBlur)';
1438
+ } else {
1439
+ boardEl.style.filter = 'none';
1440
+ }
1441
+ }
1442
+
1973
1443
  function draw() {
1974
- const now = performance.now() / 1000;
1975
- renderShader(now);
1444
+ const now = performance.now();
1445
+ renderShader(now / 1000);
1976
1446
 
1447
+ const view = cameraNow(now);
1448
+ applyCamera(view);
1449
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1977
1450
  ctx.clearRect(0, 0, sceneCanvas.width, sceneCanvas.height);
1978
- ctx.save();
1979
- const shot = scene.shots[currentShotIdx];
1980
-
1981
- // Draw shot elements
1982
- if (shot && shot.run) {
1983
- for (const cmd of shot.run) {
1984
- if (cmd.type === 'element' && cmd.element) {
1985
- const el = cmd.element;
1986
- ctx.fillStyle = el.backgroundColor || 'rgba(59, 130, 246, 0.4)';
1987
- ctx.strokeStyle = el.strokeColor || '#3b82f6';
1988
- ctx.lineWidth = 3;
1989
- ctx.strokeRect(el.x, el.y, el.width, el.height);
1990
- ctx.fillRect(el.x, el.y, el.width, el.height);
1451
+ const k = (sceneCanvas.width / plan.stage.width) * view.zoom;
1452
+ ctx.setTransform(k, 0, 0, k, k * view.scrollX, k * view.scrollY);
1991
1453
 
1992
- if (el.text) {
1993
- ctx.fillStyle = '#fff';
1994
- ctx.font = '24px sans-serif';
1995
- ctx.fillText(el.text, el.x + 16, el.y + 40);
1996
- }
1997
- }
1454
+ for (const el of plan.shots[currentShotIdx].elements) {
1455
+ ctx.fillStyle = el.backgroundColor || 'rgba(59, 130, 246, 0.4)';
1456
+ ctx.strokeStyle = el.strokeColor || '#3b82f6';
1457
+ ctx.lineWidth = 3;
1458
+ ctx.fillRect(el.x, el.y, el.width, el.height);
1459
+ ctx.strokeRect(el.x, el.y, el.width, el.height);
1460
+ if (el.text) {
1461
+ ctx.fillStyle = '#fff';
1462
+ ctx.font = '24px sans-serif';
1463
+ ctx.fillText(el.text, el.x + 16, el.y + 40);
1998
1464
  }
1999
1465
  }
2000
- ctx.restore();
2001
1466
 
2002
- if (isPlaying) {
1467
+ // Playing moves on to the next shot once this one's move and hold are over.
1468
+ const shot = scene.shots[currentShotIdx];
1469
+ if (isPlaying && now - shotStartTime > plan.cameraMs + (shot.hold ?? 800) && currentShotIdx < scene.shots.length - 1) {
1470
+ goTo(currentShotIdx + 1, true);
1471
+ }
1472
+ if (isPlaying || now - moveFrom < plan.cameraMs) {
2003
1473
  requestAnimationFrame(draw);
2004
1474
  }
2005
1475
  }
2006
1476
 
1477
+ // Shows a shot: forwards it plays the camera's move into it, otherwise it cuts.
1478
+ function goTo(idx, animate) {
1479
+ currentShotIdx = idx;
1480
+ shotStartTime = performance.now();
1481
+ moveFrom = animate ? shotStartTime : shotStartTime - plan.cameraMs;
1482
+ updateShotUI();
1483
+ requestAnimationFrame(draw);
1484
+ }
1485
+
2007
1486
  playBtn.addEventListener('click', () => {
2008
1487
  isPlaying = !isPlaying;
2009
1488
  playBtn.textContent = isPlaying ? 'Pause' : 'Play';
2010
- if (isPlaying) requestAnimationFrame(draw);
1489
+ // The current shot counts as settled, so its hold runs from now.
1490
+ if (isPlaying) {
1491
+ shotStartTime = performance.now() - plan.cameraMs;
1492
+ requestAnimationFrame(draw);
1493
+ }
2011
1494
  });
2012
1495
 
2013
1496
  prevBtn.addEventListener('click', () => {
2014
- if (currentShotIdx > 0) {
2015
- currentShotIdx--;
2016
- updateShotUI();
2017
- draw();
2018
- }
1497
+ if (currentShotIdx > 0) goTo(currentShotIdx - 1, false);
2019
1498
  });
2020
1499
 
2021
1500
  nextBtn.addEventListener('click', () => {
2022
- if (currentShotIdx < scene.shots.length - 1) {
2023
- currentShotIdx++;
2024
- updateShotUI();
2025
- draw();
2026
- }
1501
+ if (currentShotIdx < scene.shots.length - 1) goTo(currentShotIdx + 1, true);
2027
1502
  });
2028
1503
 
2029
- updateShotUI();
2030
- draw();
1504
+ window.addEventListener('resize', () => requestAnimationFrame(draw));
1505
+ goTo(0, false);
2031
1506
  </script>
2032
1507
  </body>
2033
1508
  </html>`;
@@ -2047,28 +1522,7 @@ export {
2047
1522
  ALL_BLEND_MODES,
2048
1523
  blendRgba,
2049
1524
  getGlslBlendFunction,
2050
- hash64,
2051
- mixSeeds,
2052
- unitFloat,
2053
- quinticFade,
2054
- noise1d,
2055
- noise3d,
2056
- fractalWiggle,
2057
- EFFECT_TYPES,
2058
- EFFECT_FADE_MS,
2059
- effectGone,
2060
- effectOpacity,
2061
- NO_CHECK,
2062
- SMALL_TEXT_PX,
2063
- titleBox,
2064
- touchedIds,
2065
- checkShot,
2066
1525
  validateScene,
2067
- reviewScene,
2068
- evaluateBehavior,
2069
- MemorySceneBoard,
2070
- playScene,
2071
- simulateScene,
2072
1526
  standaloneSceneHtml
2073
1527
  };
2074
- //# sourceMappingURL=chunk-JWZCPMFG.js.map
1528
+ //# sourceMappingURL=chunk-GL37YCCM.js.map