@pixerate/flair 0.2.0 → 0.3.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 +10 -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-JWZCPMFG.js → chunk-D7NLHC7K.js} +117 -674
  6. package/dist/chunk-D7NLHC7K.js.map +1 -0
  7. package/dist/{chunk-3FAUZOGL.js → chunk-FIJOMGPG.js} +2 -2
  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-mzvhKzKa.d.ts} +1 -1
  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 {
@@ -1039,228 +1029,6 @@ function getGlslBlendFunction(mode) {
1039
1029
  }
1040
1030
  }
1041
1031
 
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
1032
  // src/scene/validate.ts
1265
1033
  var LONG_CAPTION = 120;
1266
1034
  var LONG_TITLE = 60;
@@ -1297,9 +1065,22 @@ function validateScene(scene) {
1297
1065
  if (!Array.isArray(shot.camera.frame) || shot.camera.frame.length === 0) {
1298
1066
  say(shotNum, "Dynamic camera framing requires a non-empty frame array of element IDs.");
1299
1067
  }
1300
- } else if (shot.camera.width <= 0 || shot.camera.height <= 0) {
1068
+ } else if ("width" in shot.camera && (shot.camera.width <= 0 || shot.camera.height <= 0)) {
1301
1069
  say(shotNum, "Camera rectangle width and height must be positive.");
1302
1070
  }
1071
+ const cam = shot.camera;
1072
+ const angle = (name, max) => {
1073
+ const v = cam[name];
1074
+ if (v !== void 0 && !(typeof v === "number" && Math.abs(v) <= max))
1075
+ say(shotNum, `Camera ${name} is degrees between -${max} and ${max}.`);
1076
+ };
1077
+ angle("tilt", MAX_TILT);
1078
+ angle("turn", MAX_TILT);
1079
+ angle("roll", 180);
1080
+ if (cam.perspective !== void 0 && !(typeof cam.perspective === "number" && cam.perspective >= 0.3))
1081
+ say(shotNum, "Camera perspective is the lens distance as a multiple of the stage width, 0.3 or more (1.2 by default).");
1082
+ if (cam.motionBlur !== void 0 && !(typeof cam.motionBlur === "number" && cam.motionBlur >= 0 && cam.motionBlur <= 3))
1083
+ say(shotNum, "Camera motionBlur is 0 (none) to 3 (a whip); 1 is natural.");
1303
1084
  }
1304
1085
  if (shot.hold !== void 0 && shot.hold < 0) {
1305
1086
  say(shotNum, "Shot hold cannot be negative.");
@@ -1347,381 +1128,19 @@ function validateScene(scene) {
1347
1128
  return issues;
1348
1129
  }
1349
1130
 
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
1131
+ // src/scene/standalone.ts
1132
+ function planShots(scene, stage) {
1133
+ return cameraTrack(scene).map((entry) => {
1134
+ const steps = Math.round(entry.moveMs / CAMERA_STEP_MS);
1135
+ const move = Array.from({ length: Math.max(0, steps - 1) }, (_, i) => cameraAt(entry, (i + 1) * CAMERA_STEP_MS, stage));
1136
+ move.push(entry.to);
1137
+ return { move, elements: entry.elements };
1718
1138
  });
1719
- return { result, review };
1720
1139
  }
1721
-
1722
- // src/scene/standalone.ts
1723
1140
  function standaloneSceneHtml(scene) {
1141
+ const stage = sceneStage(scene);
1724
1142
  const serializedScene = JSON.stringify(scene).replace(/</g, "\\u003c");
1143
+ const serializedPlan = JSON.stringify({ stage, cameraMs: CAMERA_MS, shots: planShots(scene, stage) }).replace(/</g, "\\u003c");
1725
1144
  const title = scene.title || scene.name;
1726
1145
  return `<!DOCTYPE html>
1727
1146
  <html lang="en">
@@ -1747,7 +1166,7 @@ function standaloneSceneHtml(scene) {
1747
1166
  position: relative;
1748
1167
  width: 100%;
1749
1168
  max-width: 1200px;
1750
- aspect-ratio: 16 / 9;
1169
+ aspect-ratio: ${stage.width} / ${stage.height};
1751
1170
  background: #000;
1752
1171
  border-radius: 12px;
1753
1172
  overflow: hidden;
@@ -1762,6 +1181,12 @@ function standaloneSceneHtml(scene) {
1762
1181
  height: 100%;
1763
1182
  overflow: hidden;
1764
1183
  }
1184
+ .board {
1185
+ position: absolute;
1186
+ inset: 0;
1187
+ transform-origin: 50% 50%;
1188
+ will-change: transform, filter;
1189
+ }
1765
1190
  canvas.layer {
1766
1191
  position: absolute;
1767
1192
  top: 0;
@@ -1858,7 +1283,8 @@ function standaloneSceneHtml(scene) {
1858
1283
  <div class="player-container">
1859
1284
  <div class="viewport" id="viewport">
1860
1285
  <canvas id="shaderCanvas" class="layer"></canvas>
1861
- <canvas id="sceneCanvas" class="layer"></canvas>
1286
+ <div id="board" class="board"><canvas id="sceneCanvas" class="layer"></canvas></div>
1287
+ <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
1288
  <div id="titleEl" class="overlay-title"></div>
1863
1289
  <div id="captionEl" class="overlay-caption"></div>
1864
1290
  </div>
@@ -1875,6 +1301,9 @@ function standaloneSceneHtml(scene) {
1875
1301
 
1876
1302
  <script>
1877
1303
  const scene = ${serializedScene};
1304
+ // Each shot's camera move (zoom, scroll, 3D angle and blur at each step) and elements, worked out in advance.
1305
+ const plan = ${serializedPlan};
1306
+ const boardEl = document.getElementById('board');
1878
1307
  const sceneCanvas = document.getElementById('sceneCanvas');
1879
1308
  const shaderCanvas = document.getElementById('shaderCanvas');
1880
1309
  const ctx = sceneCanvas.getContext('2d');
@@ -1888,7 +1317,8 @@ function standaloneSceneHtml(scene) {
1888
1317
 
1889
1318
  let currentShotIdx = 0;
1890
1319
  let isPlaying = false;
1891
- let shotStartTime = 0;
1320
+ let shotStartTime = performance.now();
1321
+ let moveFrom = performance.now();
1892
1322
  let gl = null;
1893
1323
  let shaderProgram = null;
1894
1324
 
@@ -1970,64 +1400,98 @@ function standaloneSceneHtml(scene) {
1970
1400
  progressEl.style.width = pct + '%';
1971
1401
  }
1972
1402
 
1403
+ // The camera at time 'now': a step of the current shot's move, or where it settled.
1404
+ function cameraNow(now) {
1405
+ const move = plan.shots[currentShotIdx].move;
1406
+ const u = Math.min(1, (now - moveFrom) / plan.cameraMs);
1407
+ return move[Math.min(move.length - 1, Math.floor(u * move.length))];
1408
+ }
1409
+
1410
+ function applyCamera(view) {
1411
+ const w = boardEl.clientWidth || plan.stage.width;
1412
+ const angled = view.tilt || view.turn || view.roll;
1413
+ boardEl.style.transform = angled
1414
+ ? 'perspective(' + (view.perspective || 1.2) * w + 'px) rotateX(' + (view.tilt || 0) + 'deg) rotateY(' + (view.turn || 0) + 'deg) rotateZ(' + (view.roll || 0) + 'deg)'
1415
+ : 'none';
1416
+ // Motion blur streaks along a pan (taken back through the roll, as the board is blurred before it turns).
1417
+ const blur = (view.blur || 0) * w / plan.stage.width;
1418
+ if (blur >= 0.05) {
1419
+ let sx = blur, sy = blur;
1420
+ if (view.blurAngle !== undefined) {
1421
+ const a = (view.blurAngle - (view.roll || 0)) * Math.PI / 180;
1422
+ sx = Math.max(blur * 0.15, blur * Math.abs(Math.cos(a)));
1423
+ sy = Math.max(blur * 0.15, blur * Math.abs(Math.sin(a)));
1424
+ }
1425
+ document.getElementById('motionBlurAmount').setAttribute('stdDeviation', sx.toFixed(2) + ' ' + sy.toFixed(2));
1426
+ boardEl.style.filter = 'url(#motionBlur)';
1427
+ } else {
1428
+ boardEl.style.filter = 'none';
1429
+ }
1430
+ }
1431
+
1973
1432
  function draw() {
1974
- const now = performance.now() / 1000;
1975
- renderShader(now);
1433
+ const now = performance.now();
1434
+ renderShader(now / 1000);
1976
1435
 
1436
+ const view = cameraNow(now);
1437
+ applyCamera(view);
1438
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1977
1439
  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);
1440
+ const k = (sceneCanvas.width / plan.stage.width) * view.zoom;
1441
+ ctx.setTransform(k, 0, 0, k, k * view.scrollX, k * view.scrollY);
1991
1442
 
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
- }
1443
+ for (const el of plan.shots[currentShotIdx].elements) {
1444
+ ctx.fillStyle = el.backgroundColor || 'rgba(59, 130, 246, 0.4)';
1445
+ ctx.strokeStyle = el.strokeColor || '#3b82f6';
1446
+ ctx.lineWidth = 3;
1447
+ ctx.fillRect(el.x, el.y, el.width, el.height);
1448
+ ctx.strokeRect(el.x, el.y, el.width, el.height);
1449
+ if (el.text) {
1450
+ ctx.fillStyle = '#fff';
1451
+ ctx.font = '24px sans-serif';
1452
+ ctx.fillText(el.text, el.x + 16, el.y + 40);
1998
1453
  }
1999
1454
  }
2000
- ctx.restore();
2001
1455
 
2002
- if (isPlaying) {
1456
+ // Playing moves on to the next shot once this one's move and hold are over.
1457
+ const shot = scene.shots[currentShotIdx];
1458
+ if (isPlaying && now - shotStartTime > plan.cameraMs + (shot.hold ?? 800) && currentShotIdx < scene.shots.length - 1) {
1459
+ goTo(currentShotIdx + 1, true);
1460
+ }
1461
+ if (isPlaying || now - moveFrom < plan.cameraMs) {
2003
1462
  requestAnimationFrame(draw);
2004
1463
  }
2005
1464
  }
2006
1465
 
1466
+ // Shows a shot: forwards it plays the camera's move into it, otherwise it cuts.
1467
+ function goTo(idx, animate) {
1468
+ currentShotIdx = idx;
1469
+ shotStartTime = performance.now();
1470
+ moveFrom = animate ? shotStartTime : shotStartTime - plan.cameraMs;
1471
+ updateShotUI();
1472
+ requestAnimationFrame(draw);
1473
+ }
1474
+
2007
1475
  playBtn.addEventListener('click', () => {
2008
1476
  isPlaying = !isPlaying;
2009
1477
  playBtn.textContent = isPlaying ? 'Pause' : 'Play';
2010
- if (isPlaying) requestAnimationFrame(draw);
1478
+ // The current shot counts as settled, so its hold runs from now.
1479
+ if (isPlaying) {
1480
+ shotStartTime = performance.now() - plan.cameraMs;
1481
+ requestAnimationFrame(draw);
1482
+ }
2011
1483
  });
2012
1484
 
2013
1485
  prevBtn.addEventListener('click', () => {
2014
- if (currentShotIdx > 0) {
2015
- currentShotIdx--;
2016
- updateShotUI();
2017
- draw();
2018
- }
1486
+ if (currentShotIdx > 0) goTo(currentShotIdx - 1, false);
2019
1487
  });
2020
1488
 
2021
1489
  nextBtn.addEventListener('click', () => {
2022
- if (currentShotIdx < scene.shots.length - 1) {
2023
- currentShotIdx++;
2024
- updateShotUI();
2025
- draw();
2026
- }
1490
+ if (currentShotIdx < scene.shots.length - 1) goTo(currentShotIdx + 1, true);
2027
1491
  });
2028
1492
 
2029
- updateShotUI();
2030
- draw();
1493
+ window.addEventListener('resize', () => requestAnimationFrame(draw));
1494
+ goTo(0, false);
2031
1495
  </script>
2032
1496
  </body>
2033
1497
  </html>`;
@@ -2047,28 +1511,7 @@ export {
2047
1511
  ALL_BLEND_MODES,
2048
1512
  blendRgba,
2049
1513
  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
1514
  validateScene,
2067
- reviewScene,
2068
- evaluateBehavior,
2069
- MemorySceneBoard,
2070
- playScene,
2071
- simulateScene,
2072
1515
  standaloneSceneHtml
2073
1516
  };
2074
- //# sourceMappingURL=chunk-JWZCPMFG.js.map
1517
+ //# sourceMappingURL=chunk-D7NLHC7K.js.map