@pixerate/editor 0.231.1 → 0.232.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 (36) hide show
  1. package/dist/animation-editor/index.cjs +25 -25
  2. package/dist/animation-editor/index.js +1 -1
  3. package/dist/canvas/index.cjs +7 -7
  4. package/dist/canvas/index.js +1 -1
  5. package/dist/{chunk-OPSK22DL.cjs → chunk-2K5TELHV.cjs} +19 -2
  6. package/dist/{chunk-CSM5T6SE.js → chunk-6QLTUGMO.js} +1 -1
  7. package/dist/{chunk-5T3H7CGG.cjs → chunk-A653N65Q.cjs} +7 -7
  8. package/dist/{chunk-SBPGIZQN.js → chunk-MUSZYMBX.js} +1 -1
  9. package/dist/{chunk-TCZ2J7PR.js → chunk-PQYDJ2AV.js} +18 -1
  10. package/dist/{chunk-C73U5UI3.cjs → chunk-RG4O6K2A.cjs} +14 -14
  11. package/dist/effects/conformance.d.cts +4 -1
  12. package/dist/effects/conformance.d.ts +4 -1
  13. package/dist/effects/errors.d.cts +6 -1
  14. package/dist/effects/errors.d.ts +6 -1
  15. package/dist/effects/fake-templates.d.cts +17 -0
  16. package/dist/effects/fake-templates.d.ts +17 -0
  17. package/dist/effects/fake.d.cts +6 -1
  18. package/dist/effects/fake.d.ts +6 -1
  19. package/dist/effects/index.cjs +537 -74
  20. package/dist/effects/index.d.cts +1 -0
  21. package/dist/effects/index.d.ts +1 -0
  22. package/dist/effects/index.js +466 -3
  23. package/dist/effects/templates.d.cts +56 -0
  24. package/dist/effects/templates.d.ts +56 -0
  25. package/dist/effects/types.d.cts +212 -0
  26. package/dist/effects/types.d.ts +212 -0
  27. package/dist/image-editor/index.cjs +13 -13
  28. package/dist/image-editor/index.js +1 -1
  29. package/dist/paint-editor/index.cjs +7 -7
  30. package/dist/paint-editor/index.js +2 -2
  31. package/dist/vector/index.cjs +6 -6
  32. package/dist/vector/index.js +1 -1
  33. package/dist/video-editor/index.cjs +329 -329
  34. package/dist/video-editor/index.js +3 -3
  35. package/package.json +1 -1
  36. package/skills/pixerate-animation-editor/SKILL.md +37 -0
@@ -7,6 +7,7 @@
7
7
  export * from './types.cjs';
8
8
  export * from './errors.cjs';
9
9
  export * from './helpers.cjs';
10
+ export * from './templates.cjs';
10
11
  export * from './conformance.cjs';
11
12
  export * from './fake.cjs';
12
13
  export * from './tools.cjs';
@@ -7,6 +7,7 @@
7
7
  export * from './types.js';
8
8
  export * from './errors.js';
9
9
  export * from './helpers.js';
10
+ export * from './templates.js';
10
11
  export * from './conformance.js';
11
12
  export * from './fake.js';
12
13
  export * from './tools.js';
@@ -3,7 +3,7 @@ import {
3
3
  } from "../chunk-N4ZXW3WB.js";
4
4
  import {
5
5
  createEffectAuthoringSession
6
- } from "../chunk-SBPGIZQN.js";
6
+ } from "../chunk-MUSZYMBX.js";
7
7
  import {
8
8
  EffectRendererPool
9
9
  } from "../chunk-EHLOCVWB.js";
@@ -12,6 +12,10 @@ import {
12
12
  CAMERA_MOVE_ROLES,
13
13
  EFFECT_ERROR_CODES,
14
14
  EFFECT_KINDS,
15
+ EFFECT_LAYER_ROLES,
16
+ EFFECT_SUBJECT_KINDS,
17
+ EFFECT_TEMPLATE_SLOT_TYPES,
18
+ EFFECT_TRANSITION_MS,
15
19
  EFFECT_USES,
16
20
  EffectControlError,
17
21
  EffectDisposedError,
@@ -20,6 +24,7 @@ import {
20
24
  EffectsSlot,
21
25
  NoEffectsProviderError,
22
26
  UnknownEffectError,
27
+ UnknownTemplateError,
23
28
  applyEffectMask,
24
29
  cameraBoxOfView,
25
30
  cameraMoveField,
@@ -45,13 +50,242 @@ import {
45
50
  sampleEffectCameraMove,
46
51
  searchEffectSummaries,
47
52
  srgbToLinear
48
- } from "../chunk-TCZ2J7PR.js";
53
+ } from "../chunk-PQYDJ2AV.js";
49
54
  import "../chunk-V2ZCJRDM.js";
50
55
  import "../chunk-RNSANXPT.js";
51
56
  import {
52
57
  ANIMATION_EASING_PRESETS
53
58
  } from "../chunk-5UJHB3BW.js";
54
59
 
60
+ // src/effects/templates.ts
61
+ var SLOT_NAME = /^[A-Za-z][\w-]*$/;
62
+ var COLOR = /^#[0-9a-fA-F]{6}$/;
63
+ var EFFECT_SEQUENCE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
64
+ var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
65
+ var isSubjectTemplateSlot = (slot) => slot.type === "image" || slot.type === "video" || slot.type === "svg";
66
+ function effectSubjectKind(subject) {
67
+ if (!isObject(subject)) return void 0;
68
+ for (const k of ["slot", "image", "video", "svg", "text"]) if (typeof subject[k] === "string") return k;
69
+ return void 0;
70
+ }
71
+ var typeName = (value) => value === null ? "null" : Array.isArray(value) ? "a list" : typeof value === "object" ? "an object" : typeof value === "string" ? `the words ${JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}\u2026` : value)}` : `${typeof value === "number" ? "the number" : "the value"} ${String(value)}`;
72
+ function range(slot) {
73
+ const whole = slot.integer ? "whole number" : "number";
74
+ if (slot.min !== void 0 && slot.max !== void 0) return `a ${whole} from ${slot.min} to ${slot.max}`;
75
+ if (slot.min !== void 0) return `a ${whole} of at least ${slot.min}`;
76
+ if (slot.max !== void 0) return `a ${whole} of at most ${slot.max}`;
77
+ return `a ${whole}`;
78
+ }
79
+ function templateSlotTakes(slot) {
80
+ switch (slot.type) {
81
+ case "text":
82
+ return `words${slot.maxLength !== void 0 ? ` (up to ${slot.maxLength} characters)` : ""}`;
83
+ case "color":
84
+ return `a colour written #RRGGBB, like "${slot.default}"`;
85
+ case "number":
86
+ return range(slot);
87
+ case "video":
88
+ return "a video URL";
89
+ case "svg":
90
+ return "SVG markup or an SVG URL";
91
+ default:
92
+ return "a picture: a URL, a data: URL or <svg> markup";
93
+ }
94
+ }
95
+ function templateValueProblem(slot, value) {
96
+ const no = (extra = "") => `Slot "${slot.name}" takes ${templateSlotTakes(slot)}${extra}, not ${typeName(value)}.`;
97
+ switch (slot.type) {
98
+ case "text":
99
+ if (typeof value !== "string") return no();
100
+ if (slot.maxLength !== void 0 && value.length > slot.maxLength) return `Slot "${slot.name}" takes up to ${slot.maxLength} characters; the value has ${value.length}. Shorten it.`;
101
+ return void 0;
102
+ case "color":
103
+ return typeof value === "string" && COLOR.test(value) ? void 0 : no();
104
+ case "number":
105
+ if (typeof value !== "number" || !Number.isFinite(value)) return no();
106
+ if (slot.integer && !Number.isInteger(value)) return `Slot "${slot.name}" takes ${range(slot)}; ${value} is not whole.`;
107
+ if (slot.min !== void 0 && value < slot.min || slot.max !== void 0 && value > slot.max) return `Slot "${slot.name}" takes ${range(slot)}; ${value} is out of range.`;
108
+ return void 0;
109
+ default: {
110
+ const host = ` (or { "slot": "${slot.name}" } to leave it for the host)`;
111
+ if (typeof value === "string") return value.trim() ? void 0 : no(host);
112
+ const kind = effectSubjectKind(value);
113
+ if (!kind) return no(host);
114
+ if (kind === "slot") return SLOT_NAME.test(value.slot) ? void 0 : `Slot "${slot.name}" left for the host is { "slot": name }, the name letters, digits, "-" and "_", starting with a letter.`;
115
+ const fits = kind === slot.type || slot.type === "image" && kind === "svg";
116
+ if (!fits) return `Slot "${slot.name}" takes { "${slot.type}": \u2026 }${slot.type === "image" ? ' or { "svg": \u2026 }' : ""}, not { "${kind}": \u2026 }.`;
117
+ return value[kind].trim() ? void 0 : `Slot "${slot.name}"'s "${kind}" is a non-empty string.`;
118
+ }
119
+ }
120
+ }
121
+ var slotList = (slots) => slots.length ? `the slots are ${slots.map((s) => `${s.name} (${s.type})`).join(", ")}` : "the template has no slots";
122
+ function checkTemplateValues(slots, values) {
123
+ if (values === void 0) return [];
124
+ if (!isObject(values)) return [`Values are an object of slot names to values; ${slotList(slots)}.`];
125
+ const byName = new Map(slots.map((s) => [s.name, s]));
126
+ const errors = [];
127
+ for (const [name, value] of Object.entries(values)) {
128
+ const slot = byName.get(name);
129
+ if (!slot) errors.push(`There is no slot "${name}"; ${slotList(slots)}.`);
130
+ else if (value !== void 0) {
131
+ const problem = templateValueProblem(slot, value);
132
+ if (problem) errors.push(problem);
133
+ }
134
+ }
135
+ return errors;
136
+ }
137
+ function templateSlotSchema(slot) {
138
+ const about = `${slot.label}${slot.description ? `: ${slot.description}` : ""}.`;
139
+ switch (slot.type) {
140
+ case "text":
141
+ return { type: "string", ...slot.maxLength !== void 0 ? { maxLength: slot.maxLength } : {}, description: `${about} Words${slot.maxLength !== void 0 ? `, up to ${slot.maxLength} characters` : ""}. Default ${JSON.stringify(slot.default)}.` };
142
+ case "color":
143
+ return { type: "string", pattern: COLOR.source, description: `${about} A colour as #RRGGBB. Default ${slot.default}.` };
144
+ case "number":
145
+ return {
146
+ type: slot.integer ? "integer" : "number",
147
+ ...slot.min !== void 0 ? { minimum: slot.min } : {},
148
+ ...slot.max !== void 0 ? { maximum: slot.max } : {},
149
+ description: `${about} ${range(slot).replace(/^a /, "A ")}. Default ${slot.default}.`
150
+ };
151
+ default: {
152
+ const key = slot.type;
153
+ const object2 = (k, what) => ({ type: "object", properties: { [k]: { type: "string", description: what } }, required: [k], additionalProperties: false, description: `{ "${k}": \u2026 }` });
154
+ return {
155
+ anyOf: [
156
+ { type: "string", minLength: 1, description: key === "video" ? "A video URL." : key === "svg" ? "SVG markup or an SVG URL." : "A picture URL, a data: URL or <svg> markup." },
157
+ object2(key, key === "video" ? "A video URL." : key === "svg" ? "SVG markup or an SVG URL." : "A picture URL or a data: URL."),
158
+ ...key === "image" ? [object2("svg", "SVG markup.")] : [],
159
+ object2("slot", "A slot name, to leave the subject for the host to fill.")
160
+ ],
161
+ description: `${about} ${templateSlotTakes(slot).replace(/^a /, "A ")}. ${slot.hasDefault ? "Default: a placeholder picture." : "No default: left out, the host fills it."}`
162
+ };
163
+ }
164
+ }
165
+ }
166
+ function templateValuesSchema(slots) {
167
+ return {
168
+ type: "object",
169
+ properties: Object.fromEntries(slots.map((s) => [s.name, templateSlotSchema(s)])),
170
+ additionalProperties: false,
171
+ description: `Slot values by name; slots left out take their defaults. ${slots.length ? `Slots: ${slots.map((s) => s.name).join(", ")}.` : "This template has no slots."}`
172
+ };
173
+ }
174
+ function templateDefaultValues(slots) {
175
+ const out = {};
176
+ for (const s of slots) if (!isSubjectTemplateSlot(s)) out[s.name] = s.default;
177
+ return out;
178
+ }
179
+ function templateSlotSummary(slot) {
180
+ const fallback = isSubjectTemplateSlot(slot) ? slot.hasDefault ? "a placeholder by default" : "the host fills it when none is given" : `default ${JSON.stringify(slot.default)}`;
181
+ return `${slot.name} (${slot.type}): ${slot.label}; takes ${templateSlotTakes(slot)}; ${fallback}.`;
182
+ }
183
+ function searchTemplateSummaries(catalog, options = {}) {
184
+ const words = (options.query ?? "").toLowerCase().split(/\s+/).filter(Boolean);
185
+ const scored = [];
186
+ for (const t of catalog) {
187
+ if (!hasEffectTags(t, options.tags)) continue;
188
+ let score = 0;
189
+ const name = t.name.toLowerCase();
190
+ const rest = [t.id, ...t.tags ?? [], t.description].join(" ").toLowerCase();
191
+ if (!words.every((w) => name.includes(w) || rest.includes(w))) continue;
192
+ for (const w of words) score += name.includes(w) ? 2 : 1;
193
+ scored.push({ t, score });
194
+ }
195
+ scored.sort((a, b) => b.score - a.score);
196
+ const out = scored.map((s) => s.t);
197
+ return options.limit !== void 0 ? out.slice(0, Math.max(0, options.limit)) : out;
198
+ }
199
+ var effectLayerRole = (layer) => layer.role ?? (layer.rect ? "subject" : "background");
200
+ function effectLayerInput(description) {
201
+ const images = description.inputs.filter((i) => i.kind === "image");
202
+ return (images.find((i) => i.name === "source") ?? images.find((i) => i.name !== "from" && i.name !== "to"))?.name;
203
+ }
204
+ var EFFECT_LAYER_USES = ["generator", "filter", "overlay", "title"];
205
+ var effectLayerOffered = (e) => (e.kind === "shader" || e.kind === "vector") && e.uses.some((u) => EFFECT_LAYER_USES.includes(u));
206
+ var isRect = (r) => isObject(r) && ["x", "y", "width", "height"].every((k) => Number.isFinite(r[k])) && r.width > 0 && r.height > 0;
207
+ var isLayerTime = (t) => typeof t === "number" && Number.isFinite(t) && t >= 0 || isObject(t) && Number.isInteger(t.shots) && t.shots >= 1;
208
+ function effectSubjectProblem(subject) {
209
+ const kind = effectSubjectKind(subject);
210
+ if (!kind) return `a subject is one of { image }, { video }, { svg }, { text } or { slot }, not ${typeName(subject)}`;
211
+ if (!subject[kind].trim()) return `a subject's "${kind}" is a non-empty string`;
212
+ return void 0;
213
+ }
214
+ function checkEffectSequence(sequence, catalog) {
215
+ const problems = [];
216
+ if (!isObject(sequence)) return ["A sequence is an object with name, stage and shots."];
217
+ if (typeof sequence.name !== "string" || !sequence.name) problems.push("The sequence needs a name.");
218
+ const stage = sequence.stage;
219
+ if (!isObject(stage) || !(Number(stage.width) > 0) || !(Number(stage.height) > 0)) problems.push("The sequence needs a stage { width, height } in pixels, both > 0.");
220
+ if (!Array.isArray(sequence.shots) || !sequence.shots.length) return [...problems, "The sequence needs at least one shot."];
221
+ const byId = catalog ? new Map(catalog.map((e) => [e.id, e])) : void 0;
222
+ const ids = /* @__PURE__ */ new Set();
223
+ sequence.shots.forEach((shot, i) => {
224
+ const at = `shot ${i + 1}`;
225
+ if (!isObject(shot)) return void problems.push(`${at} is not an object.`);
226
+ if (!(Number.isFinite(shot.hold) && shot.hold >= 0)) problems.push(`${at}: hold must be ms \u2265 0.`);
227
+ for (const k of ["title", "caption"]) if (shot[k] !== void 0 && typeof shot[k] !== "string") problems.push(`${at}: ${k} must be words.`);
228
+ if (shot.camera !== void 0) {
229
+ const camera = shot.camera;
230
+ if (!isObject(camera)) problems.push(`${at}: camera must be an object.`);
231
+ else if (camera.framing !== void 0 && !isRect(camera.framing)) problems.push(`${at}: camera.framing must be { x, y, width, height } with width and height > 0.`);
232
+ }
233
+ if (!Array.isArray(shot.layers)) problems.push(`${at}: layers must be a list (it may be empty).`);
234
+ else
235
+ shot.layers.forEach((layer, j) => {
236
+ const where = `${at}, layer ${j + 1}`;
237
+ if (!isObject(layer)) return void problems.push(`${where} is not an object.`);
238
+ if (layer.id !== void 0) {
239
+ if (typeof layer.id !== "string" || !layer.id) problems.push(`${where}: id must be a non-empty string.`);
240
+ else if (ids.has(layer.id)) problems.push(`${where}: id "${layer.id}" is used twice.`);
241
+ else ids.add(layer.id);
242
+ }
243
+ if (layer.effect === void 0 && layer.subject === void 0) problems.push(`${where} has neither an effect nor a subject.`);
244
+ if (layer.effect !== void 0 && (typeof layer.effect !== "string" || !layer.effect)) problems.push(`${where}: effect must be an effect id.`);
245
+ else if (typeof layer.effect === "string" && byId) {
246
+ const e = byId.get(layer.effect);
247
+ if (!e) problems.push(`${where}: the provider has no effect "${layer.effect}".`);
248
+ else if (!effectLayerOffered(e)) problems.push(`${where}: "${layer.effect}" (${e.kind}, ${e.uses.join("/")}) cannot be a layer; a layer's effect is a generator, filter, overlay or title.`);
249
+ }
250
+ if (layer.subject !== void 0) {
251
+ const p = effectSubjectProblem(layer.subject);
252
+ if (p) problems.push(`${where}: ${p}.`);
253
+ }
254
+ if (layer.role !== void 0 && !EFFECT_LAYER_ROLES.includes(layer.role)) problems.push(`${where}: role is one of ${EFFECT_LAYER_ROLES.join(", ")}.`);
255
+ if (layer.rect !== void 0 && !isRect(layer.rect)) problems.push(`${where}: rect must be { x, y, width, height } with width and height > 0.`);
256
+ for (const k of ["from", "until"]) if (layer[k] !== void 0 && !isLayerTime(layer[k])) problems.push(`${where}: ${k} is ms \u2265 0 or { shots: n } with n \u2265 1.`);
257
+ if (layer.opacity !== void 0 && !(typeof layer.opacity === "number" && layer.opacity >= 0 && layer.opacity <= 1)) problems.push(`${where}: opacity is 0\u20131.`);
258
+ if (layer.fade !== void 0 && !(typeof layer.fade === "number" && layer.fade >= 0)) problems.push(`${where}: fade is ms \u2265 0.`);
259
+ if (layer.values !== void 0 && !isObject(layer.values)) problems.push(`${where}: values must be an object of control values.`);
260
+ });
261
+ if (shot.transition !== void 0) {
262
+ const t = shot.transition;
263
+ if (i === 0) problems.push(`${at} has a transition, but there is no shot before it.`);
264
+ if (!isObject(t) || typeof t.effect !== "string" || !t.effect) problems.push(`${at}: transition needs an effect id.`);
265
+ else {
266
+ if (t.ms !== void 0 && !(typeof t.ms === "number" && t.ms > 0)) problems.push(`${at}: transition ms must be > 0.`);
267
+ const e = byId?.get(t.effect);
268
+ if (byId && !e) problems.push(`${at}: the provider has no transition "${t.effect}".`);
269
+ else if (e && !e.uses.includes("transition")) problems.push(`${at}: "${t.effect}" is not a transition (its uses are ${e.uses.join(", ")}).`);
270
+ }
271
+ }
272
+ });
273
+ return problems;
274
+ }
275
+ function effectSequenceEffects(sequence) {
276
+ const out = /* @__PURE__ */ new Set();
277
+ for (const shot of sequence.shots) {
278
+ for (const l of shot.layers) if (l.effect) out.add(l.effect);
279
+ if (shot.transition) out.add(shot.transition.effect);
280
+ }
281
+ return [...out];
282
+ }
283
+ function effectSequenceSlots(sequence) {
284
+ const out = /* @__PURE__ */ new Set();
285
+ for (const shot of sequence.shots) for (const l of shot.layers) if (l.subject && "slot" in l.subject) out.add(l.subject.slot);
286
+ return [...out];
287
+ }
288
+
55
289
  // src/effects/conformance.ts
56
290
  var codeOf = (e) => isEffectError(e) ? e.code : e instanceof Error ? `untyped ${e.name}: ${e.message}` : String(e);
57
291
  async function rejectsWith(code, run) {
@@ -191,6 +425,87 @@ async function checkCamera(provider, d, issues) {
191
425
  }
192
426
  }
193
427
  }
428
+ var wrongValue = (slot) => slot.type === "number" ? "not a number" : slot.type === "text" ? 42 : slot.type === "color" ? "blue" : 42;
429
+ function checkSlot(templateId, slot, issues) {
430
+ const at = (message) => issues.push({ templateId, message: `slot ${JSON.stringify(slot?.name)}: ${message}` });
431
+ if (!slot || typeof slot.name !== "string" || !/^[A-Za-z][\w-]*$/.test(slot.name)) return at('needs a name of letters, digits, "-" and "_", starting with a letter');
432
+ if (!EFFECT_TEMPLATE_SLOT_TYPES.includes(slot.type)) return at(`type "${slot.type}" is not one of ${EFFECT_TEMPLATE_SLOT_TYPES.join(", ")}`);
433
+ if (typeof slot.label !== "string" || !slot.label) at("needs a label");
434
+ if (isSubjectTemplateSlot(slot)) {
435
+ if (typeof slot.hasDefault !== "boolean") at("a subject slot needs hasDefault (a boolean)");
436
+ if ("default" in slot) at("a subject slot describes hasDefault, not its default");
437
+ return;
438
+ }
439
+ const fallback = slot.default;
440
+ if (fallback === void 0) return at("needs a default");
441
+ const problem = templateValueProblem(slot, fallback);
442
+ if (problem) at(`its default does not fit: ${problem}`);
443
+ if (slot.type === "number" && slot.min !== void 0 && slot.max !== void 0 && slot.min > slot.max) at("min is above max");
444
+ }
445
+ async function checkTemplates(provider, list, issues) {
446
+ const top = (message) => issues.push({ message });
447
+ const templates = await provider.listTemplates();
448
+ if (!Array.isArray(templates)) return top("listTemplates did not return a list");
449
+ const seen = /* @__PURE__ */ new Set();
450
+ for (const t of templates) if (seen.has(t.id)) top(`duplicate template id "${t.id}"`);
451
+ else seen.add(t.id);
452
+ const unknownDescribe = await rejectsWith("unknown_template", () => provider.describeTemplate("__no_such_template__"));
453
+ if (unknownDescribe) top(`describeTemplate of an unknown id ${unknownDescribe}`);
454
+ const unknownFill = await rejectsWith("unknown_template", () => provider.instantiateTemplate("__no_such_template__"));
455
+ if (unknownFill) top(`instantiateTemplate of an unknown id ${unknownFill}`);
456
+ for (const summary of templates) {
457
+ const id = summary.id;
458
+ const at = (message) => issues.push({ templateId: id, message });
459
+ if (!id || typeof id !== "string") top("a template has no id");
460
+ if (!summary.name) at("template has no name");
461
+ if (!summary.description) at("template has no description");
462
+ if (!(Number.isInteger(summary.shots) && summary.shots >= 1)) at("shots must be a whole number \u2265 1");
463
+ if (summary.durationMs !== void 0 && !(summary.durationMs > 0)) at("durationMs must be > 0 when present");
464
+ let slots;
465
+ try {
466
+ const d = await provider.describeTemplate(id);
467
+ if (d.id !== id) at(`describeTemplate returned id "${d.id}"`);
468
+ if (!Array.isArray(d.slots)) {
469
+ at("describeTemplate gave no slots list");
470
+ continue;
471
+ }
472
+ slots = d.slots;
473
+ const names = /* @__PURE__ */ new Set();
474
+ for (const slot of slots) {
475
+ if (names.has(slot?.name)) at(`slot "${slot.name}" is declared twice`);
476
+ names.add(slot?.name);
477
+ checkSlot(id, slot, issues);
478
+ }
479
+ } catch (e) {
480
+ at(`describeTemplate failed: ${codeOf(e)}`);
481
+ continue;
482
+ }
483
+ try {
484
+ const result = await provider.instantiateTemplate(id);
485
+ if (!result.ok) at(`instantiateTemplate with the defaults failed: ${result.errors.join(" ")}`);
486
+ else {
487
+ for (const p of checkEffectSequence(result.sequence, list)) at(`with the defaults, ${p}`);
488
+ if (result.sequence?.shots?.length !== summary.shots) at(`listTemplates says ${summary.shots} shots; the sequence has ${result.sequence?.shots?.length}`);
489
+ if (!Array.isArray(result.openSlots) || !Array.isArray(result.notes)) at("instantiateTemplate needs openSlots and notes lists");
490
+ else {
491
+ const open = Array.isArray(result.sequence?.shots) ? effectSequenceSlots(result.sequence) : [];
492
+ if (open.some((s) => !result.openSlots.includes(s))) at(`openSlots ${JSON.stringify(result.openSlots)} misses slots the sequence leaves open (${open.join(", ")})`);
493
+ }
494
+ }
495
+ } catch (e) {
496
+ at(`instantiateTemplate with the defaults failed: ${codeOf(e)}`);
497
+ }
498
+ const bad = { __no_such_slot__: "x", ...slots[0] ? { [slots[0].name]: wrongValue(slots[0]) } : {} };
499
+ try {
500
+ const result = await provider.instantiateTemplate(id, bad);
501
+ if (result.ok) at("instantiateTemplate accepted an unknown slot and a value of the wrong type");
502
+ else if (!result.errors.length || result.errors.some((e) => typeof e !== "string" || !e)) at("instantiateTemplate refused bad values without a sentence for each");
503
+ else if (slots[0] && result.errors.length < 2) at("instantiateTemplate should refuse every bad value at once (an unknown slot and a wrong type gave one error)");
504
+ } catch (e) {
505
+ at(`instantiateTemplate with bad values threw (${codeOf(e)}) instead of returning { ok: false, errors }`);
506
+ }
507
+ }
508
+ }
194
509
  async function checkEffectProvider(provider, options = {}) {
195
510
  const issues = [];
196
511
  const top = (message) => issues.push({ message });
@@ -205,6 +520,12 @@ async function checkEffectProvider(provider, options = {}) {
205
520
  if (caps?.supportsCamera !== void 0 && typeof caps.supportsCamera !== "boolean") top("capabilities.supportsCamera is not a boolean");
206
521
  if (caps?.supportsCamera && typeof provider.cameraView !== "function") top("supportsCamera is set but cameraView is missing");
207
522
  if (provider.sampleCameraMove && typeof provider.cameraView !== "function") top("sampleCameraMove is present but cameraView is missing");
523
+ if (caps?.supportsTemplates !== void 0 && typeof caps.supportsTemplates !== "boolean") top("capabilities.supportsTemplates is not a boolean");
524
+ const templateMethods = ["listTemplates", "describeTemplate", "instantiateTemplate"];
525
+ if (caps?.supportsTemplates) {
526
+ for (const m of templateMethods) if (typeof provider[m] !== "function") top(`supportsTemplates is set but ${m} is missing`);
527
+ }
528
+ if (!caps?.supportsTemplates && templateMethods.some((m) => typeof provider[m] === "function")) top("template methods are present but capabilities.supportsTemplates is not set");
208
529
  const list = await provider.list();
209
530
  const ids = /* @__PURE__ */ new Set();
210
531
  for (const e of list) {
@@ -307,9 +628,125 @@ async function checkEffectProvider(provider, options = {}) {
307
628
  }
308
629
  }
309
630
  }
631
+ if (caps?.supportsTemplates && options.templates !== false && templateMethods.every((m) => typeof provider[m] === "function")) await checkTemplates(provider, list, issues);
310
632
  return issues;
311
633
  }
312
634
 
635
+ // src/effects/fake-templates.ts
636
+ var PLACEHOLDER = /\{\{([A-Za-z][\w-]*)\}\}/g;
637
+ var WHOLE = /^\{\{([A-Za-z][\w-]*)\}\}$/;
638
+ var picture = (label, tint) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 90" width="160" height="90"><rect width="160" height="90" fill="${tint}"/><text x="80" y="50" text-anchor="middle" font-family="sans-serif" font-size="14" fill="#fff">${label}</text></svg>`;
639
+ var TEMPLATES = [
640
+ {
641
+ summary: { id: "fake.title-sequence", name: "Title sequence", description: "A gradient behind a title with a lower-third bar, then a crossfade to a closing shot with a caption.", tags: ["title", "intro"], shots: 2, durationMs: 4600 },
642
+ slots: [
643
+ { name: "title", type: "text", label: "Title", default: "Hello", maxLength: 40 },
644
+ { name: "subtitle", type: "text", label: "Closing caption", default: "See you soon", maxLength: 60 },
645
+ { name: "accent", type: "color", label: "Accent colour", default: "#0ea5e9" },
646
+ { name: "holdMs", type: "number", label: "Time on each shot (ms)", default: 2e3, min: 500, max: 1e4, integer: true }
647
+ ],
648
+ sequence: {
649
+ name: "title-sequence",
650
+ title: "{{title}}",
651
+ stage: { width: 1280, height: 720 },
652
+ shots: [
653
+ {
654
+ hold: "{{holdMs}}",
655
+ title: "{{title}}",
656
+ layers: [
657
+ { id: "backdrop", effect: "fake.gradient", role: "background", values: { from: "{{accent}}" }, until: { shots: 1 } },
658
+ { id: "bar", effect: "fake.bar", role: "overlay", values: { color: "{{accent}}" }, from: 200 }
659
+ ]
660
+ },
661
+ {
662
+ hold: "{{holdMs}}",
663
+ caption: "{{subtitle}}",
664
+ transition: { effect: "fake.crossfade", ms: 600 },
665
+ layers: [{ id: "closing", effect: "fake.gradient", role: "background", values: { from: "#111827", to: "{{accent}}" } }]
666
+ }
667
+ ]
668
+ }
669
+ },
670
+ {
671
+ summary: { id: "fake.picture-reveal", name: "Picture reveal", description: "A tinted picture, then a crossfade to the picture in full colour with a logo over it and a caption.", tags: ["product", "reveal"], shots: 2, durationMs: 4800 },
672
+ slots: [
673
+ { name: "picture", type: "image", label: "Picture", description: "The picture to reveal.", hasDefault: true, subjectDefault: picture("Picture", "#64748b") },
674
+ { name: "logo", type: "image", label: "Logo", description: "A logo shown over the second shot; the host fills it when none is given.", hasDefault: false },
675
+ { name: "caption", type: "text", label: "Caption", default: "Now in colour", maxLength: 50 },
676
+ { name: "holdMs", type: "number", label: "Time on each shot (ms)", default: 2e3, min: 500, max: 1e4, integer: true }
677
+ ],
678
+ sequence: {
679
+ name: "picture-reveal",
680
+ stage: { width: 1280, height: 720 },
681
+ shots: [
682
+ { hold: "{{holdMs}}", layers: [{ id: "tinted", effect: "fake.tint", subject: { slot: "picture" }, values: { color: "#704214", amount: 0.6 } }] },
683
+ {
684
+ hold: "{{holdMs}}",
685
+ caption: "{{caption}}",
686
+ transition: { effect: "fake.crossfade", ms: 800 },
687
+ layers: [
688
+ { id: "picture", subject: { slot: "picture" }, role: "background" },
689
+ { id: "logo", subject: { slot: "logo" }, role: "overlay", rect: { x: 1040, y: 40, width: 200, height: 112 }, fade: 300 }
690
+ ]
691
+ }
692
+ ]
693
+ }
694
+ }
695
+ ];
696
+ var describeOf = (t) => ({
697
+ ...t.summary,
698
+ slots: t.slots.map(({ subjectDefault: _d, ...slot }) => slot)
699
+ });
700
+ function subjectOf(type, value) {
701
+ if (typeof value !== "string") return value;
702
+ if (value.trimStart().startsWith("<svg") || type === "svg") return { svg: value };
703
+ return type === "video" ? { video: value } : { image: value };
704
+ }
705
+ function fill(t, values) {
706
+ const slots = new Map(t.slots.map((s) => [s.name, s]));
707
+ const valueOf = (name) => {
708
+ const s = slots.get(name);
709
+ return values[name] ?? (isSubjectTemplateSlot(s) ? s.subjectDefault : s.default);
710
+ };
711
+ const walk = (v, key) => {
712
+ if (typeof v === "string") {
713
+ const whole = WHOLE.exec(v);
714
+ if (whole && slots.has(whole[1])) return valueOf(whole[1]);
715
+ return v.replace(PLACEHOLDER, (m, name) => slots.has(name) ? String(valueOf(name)) : m);
716
+ }
717
+ if (Array.isArray(v)) return v.map((x) => walk(x));
718
+ if (!v || typeof v !== "object") return v;
719
+ if (key === "subject" && typeof v.slot === "string") {
720
+ const s = slots.get(v.slot);
721
+ const value = s ? valueOf(s.name) : void 0;
722
+ return s && value !== void 0 ? subjectOf(s.type, value) : v;
723
+ }
724
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x, k)]));
725
+ };
726
+ return walk(t.sequence);
727
+ }
728
+ function fakeTemplateMethods() {
729
+ const byId = new Map(TEMPLATES.map((t) => [t.summary.id, t]));
730
+ const get = (id) => {
731
+ const t = byId.get(id);
732
+ if (!t) throw new UnknownTemplateError(id, "the fake provider");
733
+ return t;
734
+ };
735
+ return {
736
+ listTemplates: (options) => searchTemplateSummaries(TEMPLATES.map((t) => t.summary), options).map((s) => structuredClone(s)),
737
+ describeTemplate: (id) => structuredClone(describeOf(get(id))),
738
+ instantiateTemplate(id, values = {}, options = {}) {
739
+ const t = get(id);
740
+ const errors = checkTemplateValues(describeOf(t).slots, values);
741
+ if (options.name !== void 0 && (typeof options.name !== "string" || !EFFECT_SEQUENCE_NAME.test(options.name))) errors.push(`The sequence name is lowercase words and dashes (like "${t.summary.id.replace(/^fake\./, "")}").`);
742
+ if (errors.length) return { ok: false, errors };
743
+ const sequence = fill(t, values);
744
+ if (options.name) sequence.name = options.name;
745
+ return { ok: true, sequence, openSlots: effectSequenceSlots(sequence), notes: [] };
746
+ }
747
+ };
748
+ }
749
+
313
750
  // src/effects/fake.ts
314
751
  var HEX = "^#[0-9a-fA-F]{6}$";
315
752
  var colour = (description) => ({ type: "string", pattern: HEX, description });
@@ -689,6 +1126,7 @@ function createFakeEffectProvider(options = {}) {
689
1126
  supportsVector: true,
690
1127
  supportsAuthoring,
691
1128
  ...options.camera ? { supportsCamera: true } : {},
1129
+ ...options.templates ? { supportsTemplates: true } : {},
692
1130
  maxSize: 8192,
693
1131
  ...options.capabilities
694
1132
  };
@@ -775,6 +1213,7 @@ function createFakeEffectProvider(options = {}) {
775
1213
  return sampleCameraViews((u) => camera(id, { from, to, u, ms, stage }, values), { ms, fps, cut: d.camera?.cut });
776
1214
  }
777
1215
  } : {},
1216
+ ...options.templates ? fakeTemplateMethods() : {},
778
1217
  ...supportsAuthoring ? {
779
1218
  openAuthoring: (from) => createEffectAuthoringSession(from, { provider })
780
1219
  } : {}
@@ -1304,6 +1743,12 @@ export {
1304
1743
  CAMERA_MOVE_ROLES,
1305
1744
  EFFECT_ERROR_CODES,
1306
1745
  EFFECT_KINDS,
1746
+ EFFECT_LAYER_ROLES,
1747
+ EFFECT_LAYER_USES,
1748
+ EFFECT_SEQUENCE_NAME,
1749
+ EFFECT_SUBJECT_KINDS,
1750
+ EFFECT_TEMPLATE_SLOT_TYPES,
1751
+ EFFECT_TRANSITION_MS,
1307
1752
  EFFECT_USES,
1308
1753
  EFFECT_USE_LABELS,
1309
1754
  EffectControlError,
@@ -1314,6 +1759,7 @@ export {
1314
1759
  EffectsSlot,
1315
1760
  NoEffectsProviderError,
1316
1761
  UnknownEffectError,
1762
+ UnknownTemplateError,
1317
1763
  applyEffectMask,
1318
1764
  cameraBoxOfView,
1319
1765
  cameraMoveField,
@@ -1322,6 +1768,8 @@ export {
1322
1768
  cameraMovesFor,
1323
1769
  cameraViewOfBox,
1324
1770
  checkEffectProvider,
1771
+ checkEffectSequence,
1772
+ checkTemplateValues,
1325
1773
  countEffectTags,
1326
1774
  createEffectAuthoringSession,
1327
1775
  createEffectAuthoringToolProvider,
@@ -1331,10 +1779,17 @@ export {
1331
1779
  effectControlField,
1332
1780
  effectFieldValue,
1333
1781
  effectFontFamilies,
1782
+ effectLayerInput,
1783
+ effectLayerOffered,
1784
+ effectLayerRole,
1334
1785
  effectOffered,
1335
1786
  effectParameterFields,
1336
1787
  effectSampleInputs,
1337
1788
  effectSamplePicture,
1789
+ effectSequenceEffects,
1790
+ effectSequenceSlots,
1791
+ effectSubjectKind,
1792
+ effectSubjectProblem,
1338
1793
  effectValueErrors,
1339
1794
  encodeEffectFloat,
1340
1795
  groupEffectsByCategory,
@@ -1343,6 +1798,7 @@ export {
1343
1798
  isEffectError,
1344
1799
  isEffectFontControl,
1345
1800
  isEffectPixels,
1801
+ isSubjectTemplateSlot,
1346
1802
  linearToSrgb,
1347
1803
  normaliseEffectTag,
1348
1804
  renderEffectThumbnail,
@@ -1351,5 +1807,12 @@ export {
1351
1807
  sampleCameraViews,
1352
1808
  sampleEffectCameraMove,
1353
1809
  searchEffectSummaries,
1354
- srgbToLinear
1810
+ searchTemplateSummaries,
1811
+ srgbToLinear,
1812
+ templateDefaultValues,
1813
+ templateSlotSchema,
1814
+ templateSlotSummary,
1815
+ templateSlotTakes,
1816
+ templateValueProblem,
1817
+ templateValuesSchema
1355
1818
  };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Helpers for sequence templates and the sequences they fill to (see `EffectSequence`): the
3
+ * values a template takes as a JSON Schema, checking values before asking the provider, checking
4
+ * a sequence before loading it, and which input a layer's subject feeds. Dependency-free.
5
+ */
6
+ import type { JsonSchema } from '../tools/schema.cjs';
7
+ import { type EffectDescription, type EffectLayer, type EffectLayerRole, type EffectSequence, type EffectSubjectKind, type EffectSummary, type EffectTemplateSearchOptions, type EffectTemplateSlot, type EffectTemplateSummary } from './types.cjs';
8
+ export declare const EFFECT_SEQUENCE_NAME: RegExp;
9
+ /** True for a subject slot (`image`, `video`, `svg`). */
10
+ export declare const isSubjectTemplateSlot: (slot: Pick<EffectTemplateSlot, "type">) => boolean;
11
+ /** The kind of a subject (`image`, `video`, `svg`, `text`, `slot`), or undefined when it is none of them. */
12
+ export declare function effectSubjectKind(subject: unknown): EffectSubjectKind | undefined;
13
+ /** What a slot takes, in a few words ("words (up to 40 characters)", "a colour like #FF5A36"). */
14
+ export declare function templateSlotTakes(slot: EffectTemplateSlot): string;
15
+ /** The problem with one value for a slot, as a sentence, or undefined when it fits. */
16
+ export declare function templateValueProblem(slot: EffectTemplateSlot, value: unknown): string | undefined;
17
+ /**
18
+ * Every problem with `values` for a template's slots, as sentences: unknown slots, wrong types,
19
+ * out-of-range numbers, text too long. Empty when they fit. Slots left out take their defaults.
20
+ */
21
+ export declare function checkTemplateValues(slots: readonly EffectTemplateSlot[], values: unknown): string[];
22
+ /** One slot's value as a JSON Schema. */
23
+ export declare function templateSlotSchema(slot: EffectTemplateSlot): JsonSchema;
24
+ /** The values a template takes as a strict JSON Schema (every slot optional; `additionalProperties: false`). */
25
+ export declare function templateValuesSchema(slots: readonly EffectTemplateSlot[]): JsonSchema & {
26
+ type: 'object';
27
+ };
28
+ /** The defaults of a template's text, colour and number slots: an example `values` object to change. */
29
+ export declare function templateDefaultValues(slots: readonly EffectTemplateSlot[]): Record<string, string | number>;
30
+ /** One line on a slot: its name, type, what it takes and its default. */
31
+ export declare function templateSlotSummary(slot: EffectTemplateSlot): string;
32
+ /** Templates matching every word of `query` (in name, id, tags or description) and every tag, best first. */
33
+ export declare function searchTemplateSummaries<T extends EffectTemplateSummary>(catalog: readonly T[], options?: EffectTemplateSearchOptions): T[];
34
+ /** Where a layer sits: its `role`, else `subject` when it has a `rect`, else `background`. */
35
+ export declare const effectLayerRole: (layer: Pick<EffectLayer, "role" | "rect">) => EffectLayerRole;
36
+ /**
37
+ * The image input a layer's subject feeds: `source` when the effect has it, else its first image
38
+ * input; undefined when it takes no picture (a generator: then the layer cannot have a subject).
39
+ */
40
+ export declare function effectLayerInput(description: Pick<EffectDescription, 'inputs'>): string | undefined;
41
+ /** The uses a layer's effect may have (a transition belongs on `EffectSequenceShot.transition`, a camera move on `camera`). */
42
+ export declare const EFFECT_LAYER_USES: readonly ["generator", "filter", "overlay", "title"];
43
+ /** True for an effect a layer can show. */
44
+ export declare const effectLayerOffered: (e: Pick<EffectSummary, "kind" | "uses">) => boolean;
45
+ /** Problems with one subject, or undefined when it is sound. */
46
+ export declare function effectSubjectProblem(subject: unknown): string | undefined;
47
+ /**
48
+ * Every problem with a sequence's shape, as sentences ("shot 2, layer 1: …"). With `catalog`, also
49
+ * effect ids it does not list, layer effects that cannot be layers and transitions without the
50
+ * use `transition`. Empty when the sequence is sound.
51
+ */
52
+ export declare function checkEffectSequence(sequence: unknown, catalog?: readonly Pick<EffectSummary, 'id' | 'kind' | 'uses'>[]): string[];
53
+ /** Every effect id a sequence names (layers and transitions), each once. */
54
+ export declare function effectSequenceEffects(sequence: EffectSequence): string[];
55
+ /** Subject slots a sequence leaves for the host (`{ slot }` subjects), each once. */
56
+ export declare function effectSequenceSlots(sequence: EffectSequence): string[];