@pixerate/editor 0.253.0 → 0.254.1

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 (33) hide show
  1. package/dist/animation-editor/index.cjs +53 -53
  2. package/dist/animation-editor/index.js +2 -2
  3. package/dist/canvas/changeFeed.d.cts +6 -0
  4. package/dist/canvas/changeFeed.d.ts +6 -0
  5. package/dist/canvas/index.cjs +16 -9
  6. package/dist/canvas/index.js +10 -3
  7. package/dist/{chunk-QKXO6JPP.cjs → chunk-2OQGPEHH.cjs} +14 -14
  8. package/dist/{chunk-Z3BRH4JK.cjs → chunk-557BDWKO.cjs} +6 -6
  9. package/dist/{chunk-ZBCT5MEA.cjs → chunk-CBVNCWJM.cjs} +7 -7
  10. package/dist/{chunk-226QDRO3.js → chunk-DHQPRKXF.js} +1 -1
  11. package/dist/{chunk-ABL7IWFZ.js → chunk-GBEF2D4P.js} +1 -1
  12. package/dist/{chunk-ISIO63Q5.cjs → chunk-L2JTLW6B.cjs} +150 -25
  13. package/dist/{chunk-JA57JIT5.js → chunk-MV7CYFMJ.js} +133 -8
  14. package/dist/{chunk-5E2Q7ZHI.js → chunk-MWUZCEFT.js} +1 -1
  15. package/dist/effects/conformance.d.cts +1 -0
  16. package/dist/effects/conformance.d.ts +1 -0
  17. package/dist/effects/fake.d.cts +7 -1
  18. package/dist/effects/fake.d.ts +7 -1
  19. package/dist/effects/helpers.d.cts +36 -4
  20. package/dist/effects/helpers.d.ts +36 -4
  21. package/dist/effects/index.cjs +331 -102
  22. package/dist/effects/index.js +238 -9
  23. package/dist/effects/types.d.cts +84 -5
  24. package/dist/effects/types.d.ts +84 -5
  25. package/dist/image-editor/index.cjs +15 -15
  26. package/dist/image-editor/index.js +1 -1
  27. package/dist/paint-editor/index.cjs +7 -7
  28. package/dist/paint-editor/index.js +2 -2
  29. package/dist/vector/index.cjs +6 -6
  30. package/dist/vector/index.js +1 -1
  31. package/dist/video-editor/index.cjs +344 -344
  32. package/dist/video-editor/index.js +4 -4
  33. package/package.json +1 -1
@@ -3,7 +3,7 @@ import {
3
3
  } from "../chunk-N4ZXW3WB.js";
4
4
  import {
5
5
  createEffectAuthoringSession
6
- } from "../chunk-ABL7IWFZ.js";
6
+ } from "../chunk-GBEF2D4P.js";
7
7
  import {
8
8
  EFFECT_LAYER_USES,
9
9
  EFFECT_SEQUENCE_NAME,
@@ -28,7 +28,7 @@ import {
28
28
  templateSlotTakes,
29
29
  templateValueProblem,
30
30
  templateValuesSchema
31
- } from "../chunk-226QDRO3.js";
31
+ } from "../chunk-DHQPRKXF.js";
32
32
  import {
33
33
  EffectRendererPool
34
34
  } from "../chunk-EHLOCVWB.js";
@@ -50,15 +50,22 @@ import {
50
50
  EffectUnsupportedError,
51
51
  EffectsSlot,
52
52
  NoEffectsProviderError,
53
+ TIMING_BLENDS,
54
+ TIMING_DEFAULT_FPS,
55
+ TIMING_MAX_FPS,
56
+ TIMING_MAX_FRAMES,
53
57
  UnknownEffectError,
54
58
  UnknownTemplateError,
55
59
  applyEffectMask,
60
+ blendTimingFrames,
56
61
  cameraBoxOfView,
57
62
  cameraMoveField,
58
63
  cameraMoveIdList,
59
64
  cameraMoveParts,
60
65
  cameraMovesFor,
61
66
  cameraViewOfBox,
67
+ checkTimingFootage,
68
+ checkTimingRequest,
62
69
  countEffectTags,
63
70
  createEffectPixels,
64
71
  createEffectToolProvider,
@@ -72,14 +79,16 @@ import {
72
79
  layerImageInput,
73
80
  linearToSrgb,
74
81
  normaliseEffectTag,
82
+ normaliseTimingSample,
75
83
  pathControlsOf,
76
84
  resolveCameraMoveSpec,
77
85
  resolveEffectValues,
78
86
  sampleCameraViews,
79
87
  sampleEffectCameraMove,
80
88
  searchEffectSummaries,
81
- srgbToLinear
82
- } from "../chunk-JA57JIT5.js";
89
+ srgbToLinear,
90
+ timingSampleProblems
91
+ } from "../chunk-MV7CYFMJ.js";
83
92
  import "../chunk-V2ZCJRDM.js";
84
93
  import "../chunk-NS62CQ2B.js";
85
94
  import {
@@ -124,6 +133,10 @@ function checkSummary(e, issues) {
124
133
  if (e.uses?.includes("camera")) at('only camera effects may have the use "camera"');
125
134
  if (e.camera !== void 0) at("only camera effects may have camera");
126
135
  }
136
+ if (e.kind === "timing") {
137
+ if (e.uses?.length !== 1 || e.uses[0] !== "timing") at('a timing effect must have exactly the use "timing"');
138
+ if (e.duration !== void 0) at("a timing effect has no duration: its length depends on its footage (timingLength)");
139
+ } else if (e.uses?.includes("timing")) at('only timing effects may have the use "timing"');
127
140
  }
128
141
  function checkDescription(id, d, issues) {
129
142
  const at = (message) => issues.push({ effectId: id, message });
@@ -167,7 +180,7 @@ async function checkRender(provider, d, size, issues) {
167
180
  try {
168
181
  renderer = await provider.createRenderer(d.id, { width: size, height: size });
169
182
  } catch (e) {
170
- if (!((d.kind === "vector" || d.kind === "camera") && codeOf(e) === "unsupported")) at(`createRenderer failed: ${codeOf(e)}`);
183
+ if (!((d.kind === "vector" || d.kind === "camera" || d.kind === "timing") && codeOf(e) === "unsupported")) at(`createRenderer failed: ${codeOf(e)}`);
171
184
  else if (!(e instanceof Error && e.message.trim())) at("createRenderer refused without saying why");
172
185
  return;
173
186
  }
@@ -229,6 +242,51 @@ async function checkCamera(provider, d, issues) {
229
242
  }
230
243
  }
231
244
  }
245
+ var FOOTAGE = [{ footage: 3e3 }, { footage: 1e3, fps: 24 }];
246
+ var isPromise = (v) => !!v && typeof v.then === "function";
247
+ async function checkTiming(provider, d, issues) {
248
+ const at = (message) => issues.push({ effectId: d.id, message });
249
+ if (d.inputs.length) at("a timing effect takes no inputs: the host decodes its footage");
250
+ for (const probe of FOOTAGE) {
251
+ const fpsText = probe.fps ? ` at ${probe.fps} fps` : "";
252
+ let length;
253
+ try {
254
+ length = provider.timingLength(d.id, probe.footage);
255
+ if (isPromise(length)) return at("timingLength must be synchronous; it returned a Promise");
256
+ if (!(typeof length === "number" && Number.isFinite(length) && length > 0)) {
257
+ at(`timingLength for ${probe.footage} ms of footage is ${String(length)}; it must be a number of ms > 0`);
258
+ continue;
259
+ }
260
+ if (provider.timingLength(d.id, probe.footage) !== length) at(`timingLength is not pure: two calls for ${probe.footage} ms gave different lengths`);
261
+ } catch (e) {
262
+ at(`timingLength for ${probe.footage} ms of footage failed: ${codeOf(e)}`);
263
+ continue;
264
+ }
265
+ for (const share of [0, 0.25, 0.5, 0.75, 1]) {
266
+ const request2 = { time: share * length, ...probe };
267
+ try {
268
+ const sample = provider.timingSample(d.id, request2);
269
+ if (isPromise(sample)) return at("timingSample must be synchronous; it returned a Promise");
270
+ const problems = timingSampleProblems(sample, request2);
271
+ for (const p of problems) at(`timingSample at ${+request2.time.toFixed(3)} ms of ${probe.footage} ms footage${fpsText}: ${p}`);
272
+ if (!problems.length && JSON.stringify(provider.timingSample(d.id, request2)) !== JSON.stringify(sample)) at(`timingSample is not pure: two calls at ${+request2.time.toFixed(3)} ms gave different samples`);
273
+ } catch (e) {
274
+ at(`timingSample at ${+request2.time.toFixed(3)} ms of ${probe.footage} ms footage${fpsText} failed: ${codeOf(e)}`);
275
+ }
276
+ }
277
+ }
278
+ const request = { time: 0, footage: 1e3 };
279
+ const badLength = await rejectsWith("invalid_values", () => provider.timingLength(d.id, 1e3, { __not_a_control__: 1 }));
280
+ if (badLength) at(`timingLength with an unknown control ${badLength}`);
281
+ const badSample = await rejectsWith("invalid_values", () => provider.timingSample(d.id, request, { __not_a_control__: 1 }));
282
+ if (badSample) at(`timingSample with an unknown control ${badSample}`);
283
+ const noFootage = await rejectsWith("unsupported", () => provider.timingLength(d.id, 0));
284
+ if (noFootage) at(`timingLength of 0 ms of footage ${noFootage}`);
285
+ const badFps = await rejectsWith("unsupported", () => provider.timingSample(d.id, { ...request, fps: 0 }));
286
+ if (badFps) at(`timingSample at 0 fps ${badFps}`);
287
+ const badTime = await rejectsWith("unsupported", () => provider.timingSample(d.id, { ...request, time: Number.NaN }));
288
+ if (badTime) at(`timingSample at time NaN ${badTime}`);
289
+ }
232
290
  var wrongValue = (slot) => slot.type === "number" ? "not a number" : slot.type === "text" ? 42 : slot.type === "color" ? "blue" : 42;
233
291
  function checkSlot(templateId, slot, issues) {
234
292
  const at = (message) => issues.push({ templateId, message: `slot ${JSON.stringify(slot?.name)}: ${message}` });
@@ -324,6 +382,8 @@ async function checkEffectProvider(provider, options = {}) {
324
382
  if (caps?.supportsCamera !== void 0 && typeof caps.supportsCamera !== "boolean") top("capabilities.supportsCamera is not a boolean");
325
383
  if (caps?.supportsCamera && typeof provider.cameraView !== "function") top("supportsCamera is set but cameraView is missing");
326
384
  if (provider.sampleCameraMove && typeof provider.cameraView !== "function") top("sampleCameraMove is present but cameraView is missing");
385
+ if (caps?.supportsTiming !== void 0 && typeof caps.supportsTiming !== "boolean") top("capabilities.supportsTiming is not a boolean");
386
+ for (const m of ["timingLength", "timingSample"]) if (caps?.supportsTiming && typeof provider[m] !== "function") top(`supportsTiming is set but ${m} is missing`);
327
387
  if (caps?.supportsTemplates !== void 0 && typeof caps.supportsTemplates !== "boolean") top("capabilities.supportsTemplates is not a boolean");
328
388
  const templateMethods = ["listTemplates", "describeTemplate", "instantiateTemplate"];
329
389
  if (caps?.supportsTemplates) {
@@ -338,6 +398,7 @@ async function checkEffectProvider(provider, options = {}) {
338
398
  checkSummary(e, issues);
339
399
  }
340
400
  if (!caps?.supportsCamera && list.some((e) => e.kind === "camera")) top("the catalog lists camera effects but capabilities.supportsCamera is not set");
401
+ if (!caps?.supportsTiming && list.some((e) => e.kind === "timing")) top("the catalog lists timing effects but capabilities.supportsTiming is not set");
341
402
  const all = await provider.search("");
342
403
  if (all.length !== list.length) top(`search("") returned ${all.length} effects, list() ${list.length}`);
343
404
  const firstUse = list[0]?.uses[0];
@@ -357,6 +418,21 @@ async function checkEffectProvider(provider, options = {}) {
357
418
  const wrongKind = notCamera && await rejectsWith("unsupported", () => provider.cameraView(notCamera.id, probe));
358
419
  if (wrongKind) issues.push({ effectId: notCamera.id, message: `cameraView of a ${notCamera.kind} effect ${wrongKind}` });
359
420
  }
421
+ const timing = caps?.supportsTiming && provider.timingLength && provider.timingSample;
422
+ if (timing) {
423
+ const request = { time: 0, footage: 1e3 };
424
+ const unknownLength = await rejectsWith("unknown_effect", () => provider.timingLength("__no_such_effect__", 1e3));
425
+ if (unknownLength) top(`timingLength of an unknown id ${unknownLength}`);
426
+ const unknownSample = await rejectsWith("unknown_effect", () => provider.timingSample("__no_such_effect__", request));
427
+ if (unknownSample) top(`timingSample of an unknown id ${unknownSample}`);
428
+ const notTiming = list.find((e) => e.kind !== "timing");
429
+ if (notTiming) {
430
+ const wrongLength = await rejectsWith("unsupported", () => provider.timingLength(notTiming.id, 1e3));
431
+ if (wrongLength) issues.push({ effectId: notTiming.id, message: `timingLength of a ${notTiming.kind} effect ${wrongLength}` });
432
+ const wrongSample = await rejectsWith("unsupported", () => provider.timingSample(notTiming.id, request));
433
+ if (wrongSample) issues.push({ effectId: notTiming.id, message: `timingSample of a ${notTiming.kind} effect ${wrongSample}` });
434
+ }
435
+ }
360
436
  const size = options.size ?? 8;
361
437
  for (const id of options.ids ?? [...ids]) {
362
438
  let d;
@@ -369,6 +445,7 @@ async function checkEffectProvider(provider, options = {}) {
369
445
  checkDescription(id, d, issues);
370
446
  if (options.render !== false && d.kind !== "motion") await checkRender(provider, d, size, issues);
371
447
  if (d.kind === "camera" && d.camera && caps.supportsCamera && provider.cameraView) await checkCamera(provider, d, issues);
448
+ if (d.kind === "timing" && timing) await checkTiming(provider, d, issues);
372
449
  if ((d.kind === "vector" || d.kind === "motion") && caps.supportsVector && provider.applyVectorEffect) {
373
450
  try {
374
451
  const m = await provider.applyVectorEffect(id, '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><rect id="r" width="10" height="10"/></svg>');
@@ -383,7 +460,7 @@ async function checkEffectProvider(provider, options = {}) {
383
460
  }
384
461
  if (caps?.supportsAuthoring && options.authoring !== false && typeof provider.openAuthoring === "function") {
385
462
  const targetEffect = (await Promise.all((options.ids ?? [...ids]).map((id) => provider.describe(id)))).find(
386
- (d) => d.kind !== "motion" && d.kind !== "vector" && d.kind !== "camera"
463
+ (d) => d.kind !== "motion" && d.kind !== "vector" && d.kind !== "camera" && d.kind !== "timing"
387
464
  );
388
465
  if (targetEffect) {
389
466
  try {
@@ -862,6 +939,79 @@ var CAMERA_DESCRIPTIONS = [
862
939
  inputs: []
863
940
  }
864
941
  ];
942
+ var TIMING_DESCRIPTIONS = [
943
+ {
944
+ id: "fake.speed",
945
+ name: "Speed change",
946
+ kind: "timing",
947
+ category: "timing",
948
+ uses: ["timing"],
949
+ description: "Footage played slower or faster throughout; slowed frames blend the two either side, sped-up ones average the frames they pass over.",
950
+ tags: ["retime", "slow motion", "time-lapse"],
951
+ transparent: false,
952
+ loop: false,
953
+ controls: {
954
+ type: "object",
955
+ additionalProperties: false,
956
+ properties: {
957
+ speed: { type: "number", minimum: 0.1, maximum: 8, description: "Playback speed as a multiple of real time, 0.1\u20138 (0.5 is half speed). Default 0.5." },
958
+ smooth: { type: "boolean", description: "Blend frames: in-betweens when slowed, motion blur when sped up. Default true." }
959
+ }
960
+ },
961
+ defaults: { speed: 0.5, smooth: true },
962
+ presets: [
963
+ { name: "Slow motion", values: { speed: 0.25 } },
964
+ { name: "Double speed", values: { speed: 2 } }
965
+ ],
966
+ inputs: []
967
+ },
968
+ {
969
+ id: "fake.freeze",
970
+ name: "Freeze frame",
971
+ kind: "timing",
972
+ category: "timing",
973
+ uses: ["timing"],
974
+ description: "Footage that stops on one frame, holds it, then plays on from where it stopped.",
975
+ tags: ["retime", "freeze", "hold"],
976
+ transparent: false,
977
+ loop: false,
978
+ controls: {
979
+ type: "object",
980
+ additionalProperties: false,
981
+ properties: {
982
+ at: { type: "number", minimum: 0, maximum: 1, description: "Where it freezes, as a share of the footage, 0\u20131. Default 0.5." },
983
+ hold: { type: "number", minimum: 0, maximum: 1e4, description: "How long it holds (ms). Default 1000." }
984
+ }
985
+ },
986
+ defaults: { at: 0.5, hold: 1e3 },
987
+ presets: [{ name: "Opening still", values: { at: 0, hold: 2e3 } }],
988
+ inputs: []
989
+ },
990
+ {
991
+ id: "fake.echo",
992
+ name: "Frame echo",
993
+ kind: "timing",
994
+ category: "timing",
995
+ uses: ["timing"],
996
+ description: "Each frame with fading copies of the frames just before it, so moving things leave a trail.",
997
+ tags: ["retime", "echo", "trail"],
998
+ transparent: false,
999
+ loop: false,
1000
+ controls: {
1001
+ type: "object",
1002
+ additionalProperties: false,
1003
+ properties: {
1004
+ echoes: { type: "integer", minimum: 1, maximum: 8, description: "Copies of earlier frames, 1\u20138. Default 3." },
1005
+ spacing: { type: "number", minimum: 10, maximum: 500, description: "Footage time between copies (ms). Default 100." },
1006
+ decay: { type: "number", minimum: 0.1, maximum: 0.95, description: "Each copy's weight as a share of the one after it, 0.1\u20130.95. Default 0.5." },
1007
+ lighten: { type: "boolean", description: "Keep the brightest of the copies (light trails) instead of averaging them. Default false." }
1008
+ }
1009
+ },
1010
+ defaults: { echoes: 3, spacing: 100, decay: 0.5, lighten: false },
1011
+ presets: [{ name: "Light trails", values: { echoes: 6, spacing: 50, decay: 0.8, lighten: true } }],
1012
+ inputs: []
1013
+ }
1014
+ ];
865
1015
  var summaryOf = ({ controls: _c, defaults: _d, presets: _p, inputs: _i, ...summary }) => summary;
866
1016
  var hexToRgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
867
1017
  var smooth = (t) => t * t * (3 - 2 * t);
@@ -998,6 +1148,53 @@ function cameraViewOf(d, ctx, v) {
998
1148
  throw new EffectUnsupportedError(`"${d.id}" is a ${d.kind} effect, not a camera move.`, d.id);
999
1149
  }
1000
1150
  }
1151
+ function timingLengthOf(d, footage, v) {
1152
+ switch (d.id) {
1153
+ case "fake.speed":
1154
+ return footage / v.speed;
1155
+ case "fake.freeze":
1156
+ return footage + v.hold;
1157
+ case "fake.echo":
1158
+ return footage;
1159
+ default:
1160
+ throw new EffectUnsupportedError(`"${d.id}" is a ${d.kind} effect, not a timing effect.`, d.id);
1161
+ }
1162
+ }
1163
+ function timingFramesOf(d, request, v) {
1164
+ const { time, fps } = request;
1165
+ const frameMs = 1e3 / fps;
1166
+ switch (d.id) {
1167
+ case "fake.speed": {
1168
+ const speed = v.speed;
1169
+ const s = time * speed;
1170
+ if (!v.smooth) return { frames: [{ time: s, weight: 1 }] };
1171
+ if (speed <= 1) {
1172
+ const i = s / frameMs;
1173
+ const f = Math.floor(i + 1e-6);
1174
+ const frac = Math.max(0, i - f);
1175
+ return { frames: [{ time: f * frameMs, weight: 1 - frac }, { time: (f + 1) * frameMs, weight: frac }] };
1176
+ }
1177
+ const n = Math.ceil(speed);
1178
+ return { frames: Array.from({ length: n }, (_, k) => ({ time: s + k * speed * frameMs / n, weight: 1 })) };
1179
+ }
1180
+ case "fake.freeze": {
1181
+ const at = Math.floor(v.at * request.footage / frameMs + 1e-6) * frameMs;
1182
+ const hold = v.hold;
1183
+ return { frames: [{ time: time < at ? time : time < at + hold ? at : time - hold, weight: 1 }] };
1184
+ }
1185
+ case "fake.echo": {
1186
+ const frames = [];
1187
+ for (let k = 0; k <= v.echoes; k++) {
1188
+ const t = time - k * v.spacing;
1189
+ if (t < 0) break;
1190
+ frames.push({ time: t, weight: Math.pow(v.decay, k) });
1191
+ }
1192
+ return { frames, lighten: v.lighten };
1193
+ }
1194
+ default:
1195
+ throw new EffectUnsupportedError(`"${d.id}" is a ${d.kind} effect, not a timing effect.`, d.id);
1196
+ }
1197
+ }
1001
1198
  function toFloat(p) {
1002
1199
  const f = new Float32Array(p.data.length);
1003
1200
  for (let i = 0; i < f.length; i += 4) {
@@ -1029,11 +1226,12 @@ function createFakeEffectProvider(options = {}) {
1029
1226
  supportsVector: true,
1030
1227
  supportsAuthoring,
1031
1228
  ...options.camera ? { supportsCamera: true } : {},
1229
+ ...options.timing ? { supportsTiming: true } : {},
1032
1230
  ...options.templates ? { supportsTemplates: true } : {},
1033
1231
  maxSize: 8192,
1034
1232
  ...options.capabilities
1035
1233
  };
1036
- const all = options.camera ? [...DESCRIPTIONS, ...CAMERA_DESCRIPTIONS] : DESCRIPTIONS;
1234
+ const all = [...DESCRIPTIONS, ...options.camera ? CAMERA_DESCRIPTIONS : [], ...options.timing ? TIMING_DESCRIPTIONS : []];
1037
1235
  const byId = new Map(all.map((d) => [d.id, d]));
1038
1236
  const get = (id) => {
1039
1237
  const d = byId.get(id);
@@ -1047,6 +1245,25 @@ function createFakeEffectProvider(options = {}) {
1047
1245
  if (!(ctx.u >= 0 && ctx.u <= 1)) throw new EffectUnsupportedError(`u must be 0\u20131 through the move, got ${ctx.u}.`, id);
1048
1246
  return cameraViewOf(d, ctx, resolveEffectValues(d, values));
1049
1247
  };
1248
+ const timingEffect = (id) => {
1249
+ const d = get(id);
1250
+ if (d.kind !== "timing") {
1251
+ const ids = TIMING_DESCRIPTIONS.map((t) => `"${t.id}"`).join(", ");
1252
+ throw new EffectUnsupportedError(`"${id}" is a ${d.kind} effect, not a timing effect; the timing effects are ${ids}.`, id);
1253
+ }
1254
+ return d;
1255
+ };
1256
+ const timingLength = (id, footageMs, values) => {
1257
+ const d = timingEffect(id);
1258
+ checkTimingFootage(footageMs, id);
1259
+ return timingLengthOf(d, footageMs, resolveEffectValues(d, values));
1260
+ };
1261
+ const timingSample = (id, request, values) => {
1262
+ const d = timingEffect(id);
1263
+ const fps = checkTimingRequest(request, id);
1264
+ const raw = timingFramesOf(d, { time: request.time, footage: request.footage, fps }, resolveEffectValues(d, values));
1265
+ return normaliseTimingSample(raw.frames, { ...request, fps }, raw.lighten ? "lighten" : "average");
1266
+ };
1050
1267
  const provider = {
1051
1268
  id: "fake",
1052
1269
  name: "Fake effects",
@@ -1058,6 +1275,7 @@ function createFakeEffectProvider(options = {}) {
1058
1275
  createRenderer(id, ro) {
1059
1276
  const d = get(id);
1060
1277
  if (d.kind === "camera") throw new EffectUnsupportedError(`"${id}" is a camera move: it moves a scene's camera and draws no pixels. Use cameraView.`, id);
1278
+ if (d.kind === "timing") throw new EffectUnsupportedError(`"${id}" is a timing effect: it retimes video footage and draws no pixels of its own. Use timingSample and blend the footage frames it names.`, id);
1061
1279
  if (d.kind !== "shader") throw new EffectUnsupportedError(`"${id}" is a ${d.kind} effect; use applyVectorEffect.`, id);
1062
1280
  const { width, height } = ro;
1063
1281
  const depth = ro.depth ?? 8;
@@ -1135,6 +1353,7 @@ function createFakeEffectProvider(options = {}) {
1135
1353
  return sampleCameraViews((u) => camera(id, { from, to, u, ms, stage }, values), { ms, fps, cut: d.camera?.cut });
1136
1354
  }
1137
1355
  } : {},
1356
+ ...options.timing ? { timingLength, timingSample } : {},
1138
1357
  ...options.templates ? fakeTemplateMethods() : {},
1139
1358
  ...supportsAuthoring ? {
1140
1359
  openAuthoring: (from) => createEffectAuthoringSession(from, { provider })
@@ -1152,7 +1371,8 @@ var EFFECT_USE_LABELS = {
1152
1371
  title: "Title",
1153
1372
  motion: "Motion",
1154
1373
  interactive: "Interactive",
1155
- camera: "Camera"
1374
+ camera: "Camera",
1375
+ timing: "Timing"
1156
1376
  };
1157
1377
  var HEX2 = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;
1158
1378
  var typeOf = (s) => Array.isArray(s.type) ? s.type.find((t) => t !== "null") : s.type;
@@ -1682,9 +1902,14 @@ export {
1682
1902
  EffectUnsupportedError,
1683
1903
  EffectsSlot,
1684
1904
  NoEffectsProviderError,
1905
+ TIMING_BLENDS,
1906
+ TIMING_DEFAULT_FPS,
1907
+ TIMING_MAX_FPS,
1908
+ TIMING_MAX_FRAMES,
1685
1909
  UnknownEffectError,
1686
1910
  UnknownTemplateError,
1687
1911
  applyEffectMask,
1912
+ blendTimingFrames,
1688
1913
  cameraBoxOfView,
1689
1914
  cameraMoveField,
1690
1915
  cameraMoveIdList,
@@ -1694,6 +1919,8 @@ export {
1694
1919
  checkEffectProvider,
1695
1920
  checkEffectSequence,
1696
1921
  checkTemplateValues,
1922
+ checkTimingFootage,
1923
+ checkTimingRequest,
1697
1924
  countEffectTags,
1698
1925
  createEffectAuthoringSession,
1699
1926
  createEffectAuthoringToolProvider,
@@ -1730,6 +1957,7 @@ export {
1730
1957
  layerImageInput,
1731
1958
  linearToSrgb,
1732
1959
  normaliseEffectTag,
1960
+ normaliseTimingSample,
1733
1961
  pathControlsOf,
1734
1962
  renderEffectThumbnail,
1735
1963
  resolveCameraMoveSpec,
@@ -1744,5 +1972,6 @@ export {
1744
1972
  templateSlotSummary,
1745
1973
  templateSlotTakes,
1746
1974
  templateValueProblem,
1747
- templateValuesSchema
1975
+ templateValuesSchema,
1976
+ timingSampleProblems
1748
1977
  };
@@ -11,9 +11,10 @@ import type { FlairSvgAnimation } from '../animation-editor/flair.cjs';
11
11
  import type { EffectAuthoringSession, EffectAuthoringSource } from './authoring.cjs';
12
12
  /**
13
13
  * How an effect is drawn: a pixel shader, an animated vector picture, motion applied to someone
14
- * else's vector art, or a camera move, which moves a scene's camera and draws no pixels.
14
+ * else's vector art, a camera move, which moves a scene's camera and draws no pixels, or a
15
+ * timing effect, which retimes video footage and draws no pixels of its own.
15
16
  */
16
- export type EffectKind = 'shader' | 'vector' | 'motion' | 'camera';
17
+ export type EffectKind = 'shader' | 'vector' | 'motion' | 'camera' | 'timing';
17
18
  /**
18
19
  * What an effect is for. An effect may have several uses.
19
20
  *
@@ -26,8 +27,10 @@ export type EffectKind = 'shader' | 'vector' | 'motion' | 'camera';
26
27
  * - `interactive`: reacts to pointer, scroll or audio input at run time.
27
28
  * - `camera`: moves a scene's camera (`cameraView`). Only `camera` effects have it, and they have
28
29
  * no other use, so editors that draw pixels never offer them.
30
+ * - `timing`: retimes video footage (`timingSample`): slow motion, a reverse, a freeze, a long
31
+ * exposure. Only `timing` effects have it, and they have no other use.
29
32
  */
30
- export type EffectUse = 'filter' | 'transition' | 'generator' | 'overlay' | 'title' | 'motion' | 'interactive' | 'camera';
33
+ export type EffectUse = 'filter' | 'transition' | 'generator' | 'overlay' | 'title' | 'motion' | 'interactive' | 'camera' | 'timing';
31
34
  export declare const EFFECT_KINDS: readonly EffectKind[];
32
35
  export declare const EFFECT_USES: readonly EffectUse[];
33
36
  /**
@@ -66,7 +69,10 @@ export interface EffectSummary {
66
69
  transparent: boolean;
67
70
  /** True when the effect loops; its `duration` is then one cycle. */
68
71
  loop: boolean;
69
- /** Length of one play (ms). Absent for effects that do not change over time. */
72
+ /**
73
+ * Length of one play (ms). Absent for effects that do not change over time, and for `timing`
74
+ * effects, whose length depends on their footage (`timingLength`).
75
+ */
70
76
  duration?: number;
71
77
  /**
72
78
  * The control (a number from 0 to 1) setting how opaque the effect's own background is: at 0
@@ -300,6 +306,59 @@ export interface CameraMoveSample {
300
306
  /** Ms of the cut, when the move has one. */
301
307
  cut?: number;
302
308
  }
309
+ /** The frame rate footage is taken to have when a timing request does not say. */
310
+ export declare const TIMING_DEFAULT_FPS = 30;
311
+ /** Most footage frames one retimed frame blends (`TimingSample.frames`). */
312
+ export declare const TIMING_MAX_FRAMES = 128;
313
+ /** Highest footage frame rate a timing request may give. */
314
+ export declare const TIMING_MAX_FPS = 240;
315
+ /**
316
+ * How a retimed frame's footage frames combine (straight-alpha RGBA in, straight alpha out;
317
+ * `blendTimingFrames` is the reference):
318
+ *
319
+ * - `average`: the weighted mean in premultiplied alpha, so transparent edges do not darken.
320
+ * Colour is Σ(c·a·w) / Σ(a·w) and alpha Σ(a·w) / Σw, with `a` the frame's alpha (0–1).
321
+ * - `lighten`: each channel's brightest. Every frame is first scaled by `weight / maxWeight`
322
+ * (`k`): colour by `a·k`, alpha by `k`. The result keeps the largest scaled colour and the
323
+ * largest scaled alpha, and its colour is not divided back up by its alpha, so a faded trail
324
+ * stays dimmer.
325
+ *
326
+ * A single frame is shown as it is either way.
327
+ */
328
+ export type TimingBlend = 'average' | 'lighten';
329
+ export declare const TIMING_BLENDS: readonly TimingBlend[];
330
+ /** Where a retimed frame is: what `timingSample` takes. */
331
+ export interface TimingSampleRequest {
332
+ /** Ms into the retimed clip (any finite number; frames are held within the footage). */
333
+ time: number;
334
+ /** Length of the footage (ms); > 0. */
335
+ footage: number;
336
+ /** Footage frames per second, > 0 and at most `TIMING_MAX_FPS`. Default `TIMING_DEFAULT_FPS`. */
337
+ fps?: number;
338
+ }
339
+ /** One footage frame of a retimed frame. */
340
+ export interface TimingFrame {
341
+ /** Footage time (ms): the start of a frame, `index * 1000 / fps`. */
342
+ time: number;
343
+ /** Share of the blend, > 0; a sample's weights add up to 1. */
344
+ weight: number;
345
+ }
346
+ /**
347
+ * The footage frames one retimed frame is made of, and how to blend them. The host decodes the
348
+ * frames from its own footage and blends them (`blendTimingFrames` does it for `EffectPixels`).
349
+ *
350
+ * - Each `time` is a footage frame's start, `index * 1000 / fps` with a whole `index` from 0 to
351
+ * the footage's last frame (`ceil(footage * fps / 1000) - 1`): a time before the footage is held
352
+ * on its first frame and one after it on its last.
353
+ * - Times are distinct (frames that land on the same footage frame are merged, their weights
354
+ * added), sorted earliest first, and at most `TIMING_MAX_FRAMES` (spaced evenly when an effect
355
+ * wants more).
356
+ * - Weights are > 0 and add up to 1.
357
+ */
358
+ export interface TimingSample {
359
+ frames: readonly TimingFrame[];
360
+ blend: TimingBlend;
361
+ }
303
362
  /** A box in a sequence's stage units (pixels of `EffectSequence.stage`). */
304
363
  export interface EffectRect {
305
364
  x: number;
@@ -516,6 +575,8 @@ export interface EffectProviderCapabilities {
516
575
  supportsAuthoring?: boolean;
517
576
  /** Lists `camera` effects and implements `cameraView` (and maybe `sampleCameraMove`). */
518
577
  supportsCamera?: boolean;
578
+ /** Lists `timing` effects and implements `timingLength` and `timingSample`. */
579
+ supportsTiming?: boolean;
519
580
  /** Implements `listTemplates`, `describeTemplate` and `instantiateTemplate`. */
520
581
  supportsTemplates?: boolean;
521
582
  }
@@ -539,7 +600,8 @@ export interface EffectProvider {
539
600
  describe(id: string): MaybePromise<EffectDescription>;
540
601
  /**
541
602
  * A pixel renderer. A `vector` effect may refuse with `EffectUnsupportedError` where it cannot
542
- * lay out SVG (a Worker); a `camera` effect draws no pixels and may refuse with one saying so.
603
+ * lay out SVG (a Worker); `camera` and `timing` effects draw no pixels of their own and may
604
+ * refuse with one saying so.
543
605
  */
544
606
  createRenderer(id: string, options: EffectRendererOptions): MaybePromise<EffectRenderer>;
545
607
  /**
@@ -573,6 +635,23 @@ export interface EffectProvider {
573
635
  * `sampleEffectCameraMove` falls back to `cameraView` per frame. Errors as for `cameraView`.
574
636
  */
575
637
  sampleCameraMove?(id: string, request: CameraMoveSampleRequest): MaybePromise<CameraMoveSample>;
638
+ /**
639
+ * Ms a timing effect's retimed clip lasts for footage `footageMs` long (a speed change stretches
640
+ * or squeezes it, a freeze adds its hold, a boomerang goes there and back); > 0. Present when
641
+ * `capabilities.supportsTiming`. Synchronous and pure. Unknown ids throw `UnknownEffectError`,
642
+ * invalid values `EffectControlError`, and an effect that is not a timing effect (or a footage
643
+ * length that is not a number of ms > 0) `EffectUnsupportedError`, each with a sentence naming
644
+ * what is valid.
645
+ */
646
+ timingLength?(id: string, footageMs: number, values?: EffectValues): number;
647
+ /**
648
+ * The footage frames retimed frame `request.time` shows and how to blend them (see
649
+ * `TimingSample` for exactly what it holds). Present when `capabilities.supportsTiming`.
650
+ * Synchronous and pure: a frame can be drawn on its own, in any order, and the same id,
651
+ * request and values always give the same sample. Errors as for `timingLength`; a time that is
652
+ * not finite or an `fps` outside 0–`TIMING_MAX_FPS` also throw `EffectUnsupportedError`.
653
+ */
654
+ timingSample?(id: string, request: TimingSampleRequest, values?: EffectValues): TimingSample;
576
655
  /** Sequence templates, best match first. Present when `capabilities.supportsTemplates`. */
577
656
  listTemplates?(options?: EffectTemplateSearchOptions): MaybePromise<readonly EffectTemplateSummary[]>;
578
657
  /** A template with its slots. Unknown ids throw `UnknownTemplateError`. */