@pixerate/editor 0.124.0 → 0.124.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 (41) hide show
  1. package/dist/{chunk-IWSMDQE5.cjs → chunk-4QQZV5Y5.cjs} +18 -29
  2. package/dist/chunk-4QQZV5Y5.cjs.map +1 -0
  3. package/dist/{chunk-IOXKXQNW.cjs → chunk-CN4PJHUG.cjs} +59 -3
  4. package/dist/chunk-CN4PJHUG.cjs.map +1 -0
  5. package/dist/{chunk-TK53AB4Q.cjs → chunk-FHYDQOPC.cjs} +3 -3
  6. package/dist/{chunk-TK53AB4Q.cjs.map → chunk-FHYDQOPC.cjs.map} +1 -1
  7. package/dist/{chunk-VFAKN46Z.js → chunk-JRXJTPES.js} +2 -2
  8. package/dist/{chunk-YVVSTD3B.js → chunk-ORHMVUR6.js} +8 -19
  9. package/dist/chunk-ORHMVUR6.js.map +1 -0
  10. package/dist/{chunk-TGKT5QAB.js → chunk-QEG3GRN2.js} +58 -2
  11. package/dist/chunk-QEG3GRN2.js.map +1 -0
  12. package/dist/effects/index.cjs +51 -34
  13. package/dist/effects/index.cjs.map +1 -1
  14. package/dist/effects/index.d.cts +35 -3
  15. package/dist/effects/index.d.ts +35 -3
  16. package/dist/effects/index.js +20 -3
  17. package/dist/effects/index.js.map +1 -1
  18. package/dist/{export-8RnlngAv.d.ts → export-BGtXgMJ3.d.ts} +3 -1
  19. package/dist/{export-DM3h7QEI.d.ts → export-BdfY_lH0.d.ts} +4 -2
  20. package/dist/{export-BzSRXyH6.d.cts → export-CbpjwxaI.d.cts} +4 -2
  21. package/dist/{export-TqXMOlDt.d.cts → export-WmYoWmLM.d.cts} +3 -1
  22. package/dist/image-editor/index.cjs +110 -121
  23. package/dist/image-editor/index.cjs.map +1 -1
  24. package/dist/image-editor/index.d.cts +2 -2
  25. package/dist/image-editor/index.d.ts +2 -2
  26. package/dist/image-editor/index.js +7 -18
  27. package/dist/image-editor/index.js.map +1 -1
  28. package/dist/paint-editor/index.cjs +3 -3
  29. package/dist/paint-editor/index.d.cts +2 -2
  30. package/dist/paint-editor/index.d.ts +2 -2
  31. package/dist/paint-editor/index.js +2 -2
  32. package/dist/video-editor/index.cjs +270 -270
  33. package/dist/video-editor/index.d.cts +2 -2
  34. package/dist/video-editor/index.d.ts +2 -2
  35. package/dist/video-editor/index.js +3 -3
  36. package/package.json +1 -1
  37. package/dist/chunk-IOXKXQNW.cjs.map +0 -1
  38. package/dist/chunk-IWSMDQE5.cjs.map +0 -1
  39. package/dist/chunk-TGKT5QAB.js.map +0 -1
  40. package/dist/chunk-YVVSTD3B.js.map +0 -1
  41. /package/dist/{chunk-VFAKN46Z.js.map → chunk-JRXJTPES.js.map} +0 -0
@@ -155,6 +155,61 @@ var EffectsSlot = class {
155
155
  }
156
156
  };
157
157
 
158
+ // src/effects/pool.ts
159
+ var EffectRendererPool = class {
160
+ entries = /* @__PURE__ */ new Map();
161
+ maxRenderers;
162
+ constructor(options = {}) {
163
+ this.maxRenderers = Math.max(1, options.maxRenderers ?? 4);
164
+ }
165
+ /** Live renderers now. */
166
+ get size() {
167
+ return this.entries.size;
168
+ }
169
+ /** Renders `request` with the provider's renderer for this effect, size and depth (made on first use). */
170
+ render(provider, id, size, request) {
171
+ const depth = size.depth ?? 8;
172
+ const key = `${provider.id}:${id}@${size.width}x${size.height}x${depth}`;
173
+ let entry = this.entries.get(key);
174
+ if (entry) {
175
+ this.entries.delete(key);
176
+ this.entries.set(key, entry);
177
+ } else {
178
+ const renderer = Promise.resolve().then(() => provider.createRenderer(id, { width: size.width, height: size.height, depth }));
179
+ renderer.catch(() => {
180
+ if (this.entries.get(key)?.renderer === renderer) this.entries.delete(key);
181
+ });
182
+ entry = { renderer, queue: Promise.resolve() };
183
+ this.entries.set(key, entry);
184
+ this.evict();
185
+ }
186
+ const e = entry;
187
+ const next = e.queue.then(() => e.renderer).then((r) => r.render(request));
188
+ e.queue = next.catch(() => void 0);
189
+ return next;
190
+ }
191
+ /** Disposes the least recently used renderers over the limit, after their queued renders. */
192
+ evict() {
193
+ while (this.entries.size > this.maxRenderers) {
194
+ const [key, entry] = this.entries.entries().next().value;
195
+ this.entries.delete(key);
196
+ dispose(entry);
197
+ }
198
+ }
199
+ /** Disposes every renderer (after its queued renders). */
200
+ clear() {
201
+ for (const entry of this.entries.values()) dispose(entry);
202
+ this.entries.clear();
203
+ }
204
+ };
205
+ function dispose(entry) {
206
+ entry.queue.then(() => entry.renderer).then(
207
+ (r) => r.dispose(),
208
+ () => {
209
+ }
210
+ );
211
+ }
212
+
158
213
  export {
159
214
  EFFECT_ERROR_CODES,
160
215
  EffectError,
@@ -173,6 +228,7 @@ export {
173
228
  linearToSrgb,
174
229
  encodeEffectFloat,
175
230
  applyEffectMask,
176
- EffectsSlot
231
+ EffectsSlot,
232
+ EffectRendererPool
177
233
  };
178
- //# sourceMappingURL=chunk-TGKT5QAB.js.map
234
+ //# sourceMappingURL=chunk-QEG3GRN2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/effects/errors.ts","../src/effects/helpers.ts","../src/effects/pool.ts"],"sourcesContent":["import type { SchemaValidationError } from '../tools/schema.js';\n\nexport type EffectErrorCode = 'unknown_effect' | 'invalid_values' | 'unsupported' | 'disposed' | 'no_provider';\n\nexport const EFFECT_ERROR_CODES: readonly EffectErrorCode[] = ['unknown_effect', 'invalid_values', 'unsupported', 'disposed', 'no_provider'];\n\n/**\n * Base of every effects error. Check `code` (or use `isEffectError`) rather than `instanceof`\n * across packages, since a provider may be bundled with its own copy of this class.\n */\nexport class EffectError extends Error {\n\treadonly code: EffectErrorCode;\n\treadonly effectId?: string;\n\n\tconstructor(code: EffectErrorCode, message: string, effectId?: string) {\n\t\tsuper(message);\n\t\tthis.name = 'EffectError';\n\t\tthis.code = code;\n\t\tthis.effectId = effectId;\n\t}\n}\n\n/** No effect has this id. */\nexport class UnknownEffectError extends EffectError {\n\tconstructor(effectId: string, providerName?: string) {\n\t\tsuper(\n\t\t\t'unknown_effect',\n\t\t\t`Unknown effect \"${effectId}\"${providerName ? ` in ${providerName}` : ''}. Call search_effects or list() for valid ids.`,\n\t\t\teffectId\n\t\t);\n\t\tthis.name = 'UnknownEffectError';\n\t}\n}\n\n/** Control values do not match the effect's control schema (unknown control, wrong type, out of range). */\nexport class EffectControlError extends EffectError {\n\treadonly details: readonly SchemaValidationError[];\n\n\tconstructor(effectId: string, details: readonly SchemaValidationError[]) {\n\t\tconst lines = details.map((d) => `- ${d.path || '/'}: ${d.message}`).join('\\n');\n\t\tsuper('invalid_values', `Invalid values for effect \"${effectId}\":\\n${lines}`, effectId);\n\t\tthis.name = 'EffectControlError';\n\t\tthis.details = details;\n\t}\n}\n\n/** The provider cannot do this (a vector effect asked for pixels, a size above `maxSize`, a missing input...). */\nexport class EffectUnsupportedError extends EffectError {\n\tconstructor(message: string, effectId?: string) {\n\t\tsuper('unsupported', message, effectId);\n\t\tthis.name = 'EffectUnsupportedError';\n\t}\n}\n\n/** The renderer was disposed. Create a new one. */\nexport class EffectDisposedError extends EffectError {\n\tconstructor(effectId: string) {\n\t\tsuper('disposed', `The renderer for \"${effectId}\" was disposed. Create a new renderer.`, effectId);\n\t\tthis.name = 'EffectDisposedError';\n\t}\n}\n\n/** No effects provider is registered with this editor. */\nexport class NoEffectsProviderError extends EffectError {\n\tconstructor() {\n\t\tsuper('no_provider', 'No effects provider is registered with this editor. The host must call setEffectsProvider(provider) first.');\n\t\tthis.name = 'NoEffectsProviderError';\n\t}\n}\n\n/** True for any effects error, including one thrown by another copy of this module. */\nexport function isEffectError(error: unknown): error is EffectError {\n\tif (error instanceof EffectError) return true;\n\tconst code = error instanceof Error ? (error as unknown as { code?: unknown }).code : undefined;\n\treturn typeof code === 'string' && EFFECT_ERROR_CODES.includes(code as EffectErrorCode);\n}\n","/**\n * Helpers for providers and editors: value validation, catalog search, pixel buffers, masking,\n * and a provider slot editors hold.\n */\n\nimport { validateJsonSchema, type SchemaValidationError } from '../tools/schema.js';\nimport { EffectControlError, NoEffectsProviderError } from './errors.js';\nimport type {\n\tEffectDescription,\n\tEffectImageSource,\n\tEffectPixels,\n\tEffectProvider,\n\tEffectSearchOptions,\n\tEffectSummary,\n\tEffectValues\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Values\n// ---------------------------------------------------------------------------\n\n/** Problems with `values` against an effect's control schema (unknown controls, types, ranges). Partial values are fine. */\nexport function effectValueErrors(description: Pick<EffectDescription, 'controls'>, values: unknown): SchemaValidationError[] {\n\tif (values === undefined) return [];\n\treturn validateJsonSchema(values, { ...description.controls, required: [] });\n}\n\n/**\n * `values` over the effect's defaults, validated against its control schema. Throws\n * `EffectControlError` naming every bad control. Providers call this in `render`; editors call\n * it before saving values.\n */\nexport function resolveEffectValues(description: Pick<EffectDescription, 'id' | 'controls' | 'defaults'>, values?: EffectValues): Record<string, EffectValues[string]> {\n\tconst errors = effectValueErrors(description, values);\n\tif (errors.length) throw new EffectControlError(description.id, errors);\n\treturn { ...description.defaults, ...(values ?? {}) };\n}\n\n// ---------------------------------------------------------------------------\n// Search\n// ---------------------------------------------------------------------------\n\nconst matchesFilters = (e: EffectSummary, o: EffectSearchOptions) =>\n\t(!o.kind || e.kind === o.kind) && (!o.category || e.category === o.category) && (!o.use || e.uses.includes(o.use));\n\n/**\n * Filters a catalog and ranks it against `query` (every word must appear in the id, name,\n * category, tags or description; name matches rank first). An empty query keeps catalog order.\n * Providers can implement `list` and `search` with it.\n */\nexport function searchEffectSummaries<T extends EffectSummary>(catalog: readonly T[], query = '', options: EffectSearchOptions = {}): T[] {\n\tconst words = query.toLowerCase().split(/\\s+/).filter(Boolean);\n\tconst scored: { e: T; score: number; i: number }[] = [];\n\tcatalog.forEach((e, i) => {\n\t\tif (!matchesFilters(e, options)) return;\n\t\tconst name = e.name.toLowerCase();\n\t\tconst hay = [e.id, e.name, e.category, ...(e.tags ?? []), e.description ?? ''].join(' ').toLowerCase();\n\t\tlet score = 0;\n\t\tfor (const w of words) {\n\t\t\tif (!hay.includes(w)) return;\n\t\t\tscore += name.startsWith(w) ? 3 : name.includes(w) ? 2 : 1;\n\t\t}\n\t\tscored.push({ e, score, i });\n\t});\n\tscored.sort((a, b) => b.score - a.score || a.i - b.i);\n\tconst out = scored.map((s) => s.e);\n\treturn options.limit !== undefined ? out.slice(0, Math.max(0, options.limit)) : out;\n}\n\n// ---------------------------------------------------------------------------\n// Pixels\n// ---------------------------------------------------------------------------\n\n/** A transparent picture, with float samples when `float` is set. */\nexport function createEffectPixels(width: number, height: number, float = false): EffectPixels {\n\tconst n = Math.max(0, width * height * 4);\n\treturn { width, height, data: new Uint8ClampedArray(n), ...(float ? { float: new Float32Array(n) } : {}) };\n}\n\n/** True for an `EffectPixels` (rather than a host image object). */\nexport function isEffectPixels(source: EffectImageSource | unknown): source is EffectPixels {\n\treturn (\n\t\t!!source &&\n\t\ttypeof source === 'object' &&\n\t\t(source as EffectPixels).data instanceof Uint8ClampedArray &&\n\t\ttypeof (source as EffectPixels).width === 'number' &&\n\t\ttypeof (source as EffectPixels).height === 'number'\n\t);\n}\n\n/** sRGB-encoded 0–1 to linear light. */\nexport const srgbToLinear = (v: number) => (v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));\n/** Linear light to sRGB-encoded (clamped below at 0; may exceed 1). */\nexport const linearToSrgb = (v: number) => (v > 0 ? (v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055) : 0);\n\n/** Fills `pixels.data` from `pixels.float` (linear float to clamped 8-bit sRGB). */\nexport function encodeEffectFloat(pixels: EffectPixels): EffectPixels {\n\tconst f = pixels.float;\n\tif (!f) return pixels;\n\tconst d = pixels.data;\n\tfor (let i = 0; i < d.length; i += 4) {\n\t\td[i] = Math.round(linearToSrgb(f[i]) * 255);\n\t\td[i + 1] = Math.round(linearToSrgb(f[i + 1]) * 255);\n\t\td[i + 2] = Math.round(linearToSrgb(f[i + 2]) * 255);\n\t\td[i + 3] = Math.round(Math.min(1, Math.max(0, f[i + 3])) * 255);\n\t}\n\treturn pixels;\n}\n\n/**\n * Blends an effect `result` back over its `source` by `mask` coverage (alpha channel), in place\n * on `result`: where the mask is 0 the source shows, where it is 255 the result. Without a\n * source (generators), the result's alpha is multiplied by coverage. Editors use it when the\n * provider does not `supportsMask`. All three must have the same size.\n */\nexport function applyEffectMask(result: EffectPixels, mask: EffectPixels, source?: EffectPixels): EffectPixels {\n\tconst { data } = result;\n\tconst m = mask.data;\n\tfor (let i = 0; i < data.length; i += 4) {\n\t\tconst k = m[i + 3] / 255;\n\t\tif (k >= 1) continue;\n\t\tif (source) {\n\t\t\tfor (let c = 0; c < 4; c++) data[i + c] = source.data[i + c] + (data[i + c] - source.data[i + c]) * k;\n\t\t\tif (result.float && source.float) {\n\t\t\t\tfor (let c = 0; c < 4; c++) result.float[i + c] = source.float[i + c] + (result.float[i + c] - source.float[i + c]) * k;\n\t\t\t}\n\t\t} else {\n\t\t\tdata[i + 3] = data[i + 3] * k;\n\t\t\tif (result.float) result.float[i + 3] *= k;\n\t\t}\n\t}\n\treturn result;\n}\n\n// ---------------------------------------------------------------------------\n// The slot an editor keeps\n// ---------------------------------------------------------------------------\n\n/**\n * Holds an editor's effects provider. Controllers expose it as `effectsProvider`,\n * `setEffectsProvider(provider | null)` and `useEffects(provider)`; listeners hear every change.\n */\nexport class EffectsSlot {\n\tprivate current: EffectProvider | null = null;\n\tprivate readonly listeners = new Set<(provider: EffectProvider | null) => void>();\n\n\tget provider(): EffectProvider | null {\n\t\treturn this.current;\n\t}\n\n\tset(provider: EffectProvider | null): void {\n\t\tif (provider === this.current) return;\n\t\tthis.current = provider;\n\t\tfor (const l of [...this.listeners]) l(provider);\n\t}\n\n\t/** Sets `provider` and returns a function that clears it again (if it is still the one set). */\n\tuse(provider: EffectProvider): () => void {\n\t\tthis.set(provider);\n\t\treturn () => {\n\t\t\tif (this.current === provider) this.set(null);\n\t\t};\n\t}\n\n\t/** The provider, or a `NoEffectsProviderError`. */\n\trequire(): EffectProvider {\n\t\tif (!this.current) throw new NoEffectsProviderError();\n\t\treturn this.current;\n\t}\n\n\tsubscribe(listener: (provider: EffectProvider | null) => void): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => this.listeners.delete(listener);\n\t}\n}\n","/**\n * `EffectRendererPool`: keeps a few live renderers, reused per effect, size and depth.\n *\n * GPU providers hold one graphics context per renderer, and browsers allow only about sixteen\n * WebGL contexts at a time (the oldest is lost after that). So the pool keeps at most\n * `maxRenderers` renderers. It disposes the least recently used one once its renders finish, and it\n * runs one render at a time per renderer, as the contract requires.\n */\n\nimport type { EffectPixels, EffectProvider, EffectRenderer, EffectRenderRequest } from './types.js';\n\ninterface PoolEntry {\n\trenderer: Promise<EffectRenderer>;\n\t/** The last render queued on it. */\n\tqueue: Promise<unknown>;\n}\n\nexport interface EffectRendererPoolOptions {\n\t/** Most live renderers (default 4). */\n\tmaxRenderers?: number;\n}\n\nexport class EffectRendererPool {\n\tprivate readonly entries = new Map<string, PoolEntry>();\n\tprivate readonly maxRenderers: number;\n\n\tconstructor(options: EffectRendererPoolOptions = {}) {\n\t\tthis.maxRenderers = Math.max(1, options.maxRenderers ?? 4);\n\t}\n\n\t/** Live renderers now. */\n\tget size(): number {\n\t\treturn this.entries.size;\n\t}\n\n\t/** Renders `request` with the provider's renderer for this effect, size and depth (made on first use). */\n\trender(provider: EffectProvider, id: string, size: { width: number; height: number; depth?: 8 | 16 | 32 }, request: EffectRenderRequest): Promise<EffectPixels> {\n\t\tconst depth = size.depth ?? 8;\n\t\tconst key = `${provider.id}:${id}@${size.width}x${size.height}x${depth}`;\n\t\tlet entry = this.entries.get(key);\n\t\tif (entry) {\n\t\t\tthis.entries.delete(key);\n\t\t\tthis.entries.set(key, entry);\n\t\t} else {\n\t\t\tconst renderer = Promise.resolve().then(() => provider.createRenderer(id, { width: size.width, height: size.height, depth }));\n\t\t\trenderer.catch(() => {\n\t\t\t\tif (this.entries.get(key)?.renderer === renderer) this.entries.delete(key);\n\t\t\t});\n\t\t\tentry = { renderer, queue: Promise.resolve() };\n\t\t\tthis.entries.set(key, entry);\n\t\t\tthis.evict();\n\t\t}\n\t\tconst e = entry;\n\t\tconst next = e.queue.then(() => e.renderer).then((r) => r.render(request));\n\t\te.queue = next.catch(() => undefined);\n\t\treturn next;\n\t}\n\n\t/** Disposes the least recently used renderers over the limit, after their queued renders. */\n\tprivate evict(): void {\n\t\twhile (this.entries.size > this.maxRenderers) {\n\t\t\tconst [key, entry] = this.entries.entries().next().value!;\n\t\t\tthis.entries.delete(key);\n\t\t\tdispose(entry);\n\t\t}\n\t}\n\n\t/** Disposes every renderer (after its queued renders). */\n\tclear(): void {\n\t\tfor (const entry of this.entries.values()) dispose(entry);\n\t\tthis.entries.clear();\n\t}\n}\n\nfunction dispose(entry: PoolEntry) {\n\tentry.queue\n\t\t.then(() => entry.renderer)\n\t\t.then(\n\t\t\t(r) => r.dispose(),\n\t\t\t() => {}\n\t\t);\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkTK53AB4Qcjs = require('../chunk-TK53AB4Q.cjs');
5
+ var _chunkFHYDQOPCcjs = require('../chunk-FHYDQOPC.cjs');
6
6
 
7
7
 
8
8
 
@@ -22,11 +22,12 @@ var _chunkTK53AB4Qcjs = require('../chunk-TK53AB4Q.cjs');
22
22
 
23
23
 
24
24
 
25
- var _chunkIOXKXQNWcjs = require('../chunk-IOXKXQNW.cjs');
25
+
26
+ var _chunkCN4PJHUGcjs = require('../chunk-CN4PJHUG.cjs');
26
27
  require('../chunk-RIWPYSKU.cjs');
27
28
 
28
29
  // src/effects/conformance.ts
29
- var codeOf = (e) => _chunkIOXKXQNWcjs.isEffectError.call(void 0, e) ? e.code : e instanceof Error ? `untyped ${e.name}: ${e.message}` : String(e);
30
+ var codeOf = (e) => _chunkCN4PJHUGcjs.isEffectError.call(void 0, e) ? e.code : e instanceof Error ? `untyped ${e.name}: ${e.message}` : String(e);
30
31
  async function rejectsWith(code, run) {
31
32
  try {
32
33
  await run();
@@ -40,10 +41,10 @@ function checkSummary(e, issues) {
40
41
  const at = (message) => issues.push({ effectId: e.id, message });
41
42
  if (!e.id || typeof e.id !== "string") at("summary has no id");
42
43
  if (!e.name) at("summary has no name");
43
- if (!_chunkTK53AB4Qcjs.EFFECT_KINDS.includes(e.kind)) at(`kind "${e.kind}" is not one of ${_chunkTK53AB4Qcjs.EFFECT_KINDS.join(", ")}`);
44
+ if (!_chunkFHYDQOPCcjs.EFFECT_KINDS.includes(e.kind)) at(`kind "${e.kind}" is not one of ${_chunkFHYDQOPCcjs.EFFECT_KINDS.join(", ")}`);
44
45
  if (!e.category) at("summary has no category");
45
46
  if (!Array.isArray(e.uses) || !e.uses.length) at("summary has no uses");
46
- for (const u of _nullishCoalesce(e.uses, () => ( []))) if (!_chunkTK53AB4Qcjs.EFFECT_USES.includes(u)) at(`use "${u}" is not one of ${_chunkTK53AB4Qcjs.EFFECT_USES.join(", ")}`);
47
+ for (const u of _nullishCoalesce(e.uses, () => ( []))) if (!_chunkFHYDQOPCcjs.EFFECT_USES.includes(u)) at(`use "${u}" is not one of ${_chunkFHYDQOPCcjs.EFFECT_USES.join(", ")}`);
47
48
  if (typeof e.transparent !== "boolean") at("transparent is not a boolean");
48
49
  if (typeof e.loop !== "boolean") at("loop is not a boolean");
49
50
  if (e.duration !== void 0 && !(e.duration > 0)) at("duration must be a positive number of ms when present");
@@ -59,8 +60,8 @@ function checkDescription(id, d, issues) {
59
60
  if (!(name in (_nullishCoalesce(d.defaults, () => ( {}))))) at(`control "${name}" has no default`);
60
61
  }
61
62
  for (const name of Object.keys(_nullishCoalesce(d.defaults, () => ( {})))) if (!(name in (_nullishCoalesce(_optionalChain([c, 'optionalAccess', _3 => _3.properties]), () => ( {}))))) at(`default "${name}" is not a control`);
62
- for (const e of _chunkIOXKXQNWcjs.effectValueErrors.call(void 0, d, d.defaults)) at(`default ${e.path}: ${e.message}`);
63
- for (const p of _nullishCoalesce(d.presets, () => ( []))) for (const e of _chunkIOXKXQNWcjs.effectValueErrors.call(void 0, d, p.values)) at(`preset "${p.name}" ${e.path}: ${e.message}`);
63
+ for (const e of _chunkCN4PJHUGcjs.effectValueErrors.call(void 0, d, d.defaults)) at(`default ${e.path}: ${e.message}`);
64
+ for (const p of _nullishCoalesce(d.presets, () => ( []))) for (const e of _chunkCN4PJHUGcjs.effectValueErrors.call(void 0, d, p.values)) at(`preset "${p.name}" ${e.path}: ${e.message}`);
64
65
  for (const input2 of _nullishCoalesce(d.inputs, () => ( []))) {
65
66
  if (!input2.name || !input2.description) at("every input needs a name and a description");
66
67
  if (input2.kind !== "image" && input2.kind !== "audio") at(`input "${input2.name}" has kind "${input2.kind}"`);
@@ -71,7 +72,7 @@ function checkDescription(id, d, issues) {
71
72
  }
72
73
  }
73
74
  function grey(size) {
74
- const p = _chunkIOXKXQNWcjs.createEffectPixels.call(void 0, size, size);
75
+ const p = _chunkCN4PJHUGcjs.createEffectPixels.call(void 0, size, size);
75
76
  p.data.fill(128);
76
77
  return p;
77
78
  }
@@ -87,7 +88,7 @@ async function checkRender(provider, d, size, issues) {
87
88
  }
88
89
  try {
89
90
  const out = await renderer.render({ time: 0, inputs });
90
- if (!_chunkIOXKXQNWcjs.isEffectPixels.call(void 0, out)) at("render did not return EffectPixels");
91
+ if (!_chunkCN4PJHUGcjs.isEffectPixels.call(void 0, out)) at("render did not return EffectPixels");
91
92
  else if (out.width !== size || out.height !== size || out.data.length !== size * size * 4) at(`render returned ${out.width}\xD7${out.height}, not ${size}\xD7${size}`);
92
93
  } catch (e) {
93
94
  at(`render failed: ${codeOf(e)}`);
@@ -302,15 +303,15 @@ var smooth = (t) => t * t * (3 - 2 * t);
302
303
  var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
303
304
  function input(request, name, id, width, height) {
304
305
  const src = _optionalChain([request, 'access', _9 => _9.inputs, 'optionalAccess', _10 => _10[name]]);
305
- if (!src) throw new (0, _chunkIOXKXQNWcjs.EffectUnsupportedError)(`Effect "${id}" needs an input named "${name}".`, id);
306
- if (!_chunkIOXKXQNWcjs.isEffectPixels.call(void 0, src)) throw new (0, _chunkIOXKXQNWcjs.EffectUnsupportedError)(`The fake provider only takes EffectPixels inputs ("${name}").`, id);
306
+ if (!src) throw new (0, _chunkCN4PJHUGcjs.EffectUnsupportedError)(`Effect "${id}" needs an input named "${name}".`, id);
307
+ if (!_chunkCN4PJHUGcjs.isEffectPixels.call(void 0, src)) throw new (0, _chunkCN4PJHUGcjs.EffectUnsupportedError)(`The fake provider only takes EffectPixels inputs ("${name}").`, id);
307
308
  if (src.width !== width || src.height !== height) {
308
- throw new (0, _chunkIOXKXQNWcjs.EffectUnsupportedError)(`Input "${name}" is ${src.width}\xD7${src.height}; the renderer is ${width}\xD7${height}.`, id);
309
+ throw new (0, _chunkCN4PJHUGcjs.EffectUnsupportedError)(`Input "${name}" is ${src.width}\xD7${src.height}; the renderer is ${width}\xD7${height}.`, id);
309
310
  }
310
311
  return src;
311
312
  }
312
313
  function renderPixels(d, v, request, width, height) {
313
- const out = _chunkIOXKXQNWcjs.createEffectPixels.call(void 0, width, height);
314
+ const out = _chunkCN4PJHUGcjs.createEffectPixels.call(void 0, width, height);
314
315
  const o = out.data;
315
316
  switch (d.id) {
316
317
  case "fake.invert": {
@@ -376,23 +377,23 @@ function renderPixels(d, v, request, width, height) {
376
377
  break;
377
378
  }
378
379
  default:
379
- throw new (0, _chunkIOXKXQNWcjs.EffectUnsupportedError)(`"${d.id}" is a ${d.kind} effect; use applyVectorEffect.`, d.id);
380
+ throw new (0, _chunkCN4PJHUGcjs.EffectUnsupportedError)(`"${d.id}" is a ${d.kind} effect; use applyVectorEffect.`, d.id);
380
381
  }
381
382
  return out;
382
383
  }
383
384
  function toFloat(p) {
384
385
  const f = new Float32Array(p.data.length);
385
386
  for (let i = 0; i < f.length; i += 4) {
386
- f[i] = _chunkIOXKXQNWcjs.srgbToLinear.call(void 0, p.data[i] / 255);
387
- f[i + 1] = _chunkIOXKXQNWcjs.srgbToLinear.call(void 0, p.data[i + 1] / 255);
388
- f[i + 2] = _chunkIOXKXQNWcjs.srgbToLinear.call(void 0, p.data[i + 2] / 255);
387
+ f[i] = _chunkCN4PJHUGcjs.srgbToLinear.call(void 0, p.data[i] / 255);
388
+ f[i + 1] = _chunkCN4PJHUGcjs.srgbToLinear.call(void 0, p.data[i + 1] / 255);
389
+ f[i + 2] = _chunkCN4PJHUGcjs.srgbToLinear.call(void 0, p.data[i + 2] / 255);
389
390
  f[i + 3] = p.data[i + 3] / 255;
390
391
  }
391
- return _chunkIOXKXQNWcjs.encodeEffectFloat.call(void 0, { ...p, float: f });
392
+ return _chunkCN4PJHUGcjs.encodeEffectFloat.call(void 0, { ...p, float: f });
392
393
  }
393
394
  function maskResult(out, mask, source, id) {
394
- if (!_chunkIOXKXQNWcjs.isEffectPixels.call(void 0, mask) || mask.width !== out.width || mask.height !== out.height) {
395
- throw new (0, _chunkIOXKXQNWcjs.EffectUnsupportedError)("The mask must be EffectPixels the size of the renderer.", id);
395
+ if (!_chunkCN4PJHUGcjs.isEffectPixels.call(void 0, mask) || mask.width !== out.width || mask.height !== out.height) {
396
+ throw new (0, _chunkCN4PJHUGcjs.EffectUnsupportedError)("The mask must be EffectPixels the size of the renderer.", id);
396
397
  }
397
398
  const o = out.data;
398
399
  for (let i = 0; i < o.length; i += 4) {
@@ -414,7 +415,7 @@ function createFakeEffectProvider(options = {}) {
414
415
  const byId = new Map(DESCRIPTIONS.map((d) => [d.id, d]));
415
416
  const get = (id) => {
416
417
  const d = byId.get(id);
417
- if (!d) throw new (0, _chunkIOXKXQNWcjs.UnknownEffectError)(id, "the fake provider");
418
+ if (!d) throw new (0, _chunkCN4PJHUGcjs.UnknownEffectError)(id, "the fake provider");
418
419
  return d;
419
420
  };
420
421
  const summaries = DESCRIPTIONS.map(summaryOf);
@@ -423,16 +424,16 @@ function createFakeEffectProvider(options = {}) {
423
424
  name: "Fake effects",
424
425
  version: "1",
425
426
  capabilities,
426
- list: (o) => _chunkIOXKXQNWcjs.searchEffectSummaries.call(void 0, summaries, "", o),
427
- search: (q, o) => _chunkIOXKXQNWcjs.searchEffectSummaries.call(void 0, summaries, q, o),
427
+ list: (o) => _chunkCN4PJHUGcjs.searchEffectSummaries.call(void 0, summaries, "", o),
428
+ search: (q, o) => _chunkCN4PJHUGcjs.searchEffectSummaries.call(void 0, summaries, q, o),
428
429
  describe: (id) => structuredClone(get(id)),
429
430
  createRenderer(id, ro) {
430
431
  const d = get(id);
431
- if (d.kind === "motion") throw new (0, _chunkIOXKXQNWcjs.EffectUnsupportedError)(`"${id}" is a motion effect; use applyVectorEffect.`, id);
432
+ if (d.kind === "motion") throw new (0, _chunkCN4PJHUGcjs.EffectUnsupportedError)(`"${id}" is a motion effect; use applyVectorEffect.`, id);
432
433
  const { width, height } = ro;
433
434
  const depth = _nullishCoalesce(ro.depth, () => ( 8));
434
435
  if (!(width > 0 && height > 0) || width > capabilities.maxSize || height > capabilities.maxSize) {
435
- throw new (0, _chunkIOXKXQNWcjs.EffectUnsupportedError)(`Renderer size ${width}\xD7${height} is outside 1\u2013${capabilities.maxSize}.`, id);
436
+ throw new (0, _chunkCN4PJHUGcjs.EffectUnsupportedError)(`Renderer size ${width}\xD7${height} is outside 1\u2013${capabilities.maxSize}.`, id);
436
437
  }
437
438
  let disposed = false;
438
439
  return {
@@ -441,15 +442,15 @@ function createFakeEffectProvider(options = {}) {
441
442
  height,
442
443
  depth,
443
444
  async render(request) {
444
- if (disposed) throw new (0, _chunkIOXKXQNWcjs.EffectDisposedError)(id);
445
+ if (disposed) throw new (0, _chunkCN4PJHUGcjs.EffectDisposedError)(id);
445
446
  if (!Number.isFinite(request.time) || request.time < 0) {
446
- throw new (0, _chunkIOXKXQNWcjs.EffectUnsupportedError)(`time must be a finite number of ms \u2265 0, got ${request.time}.`, id);
447
+ throw new (0, _chunkCN4PJHUGcjs.EffectUnsupportedError)(`time must be a finite number of ms \u2265 0, got ${request.time}.`, id);
447
448
  }
448
- const values = _chunkIOXKXQNWcjs.resolveEffectValues.call(void 0, d, request.values);
449
+ const values = _chunkCN4PJHUGcjs.resolveEffectValues.call(void 0, d, request.values);
449
450
  _optionalChain([options, 'access', _11 => _11.onRender, 'optionalCall', _12 => _12(id, request)]);
450
451
  const out = renderPixels(d, values, request, width, height);
451
452
  if (request.mask) {
452
- if (!capabilities.supportsMask) throw new (0, _chunkIOXKXQNWcjs.EffectUnsupportedError)("This provider does not take masks.", id);
453
+ if (!capabilities.supportsMask) throw new (0, _chunkCN4PJHUGcjs.EffectUnsupportedError)("This provider does not take masks.", id);
453
454
  const source = d.uses.includes("filter") ? _optionalChain([request, 'access', _13 => _13.inputs, 'optionalAccess', _14 => _14.source]) : void 0;
454
455
  maskResult(out, request.mask, source, id);
455
456
  }
@@ -462,8 +463,8 @@ function createFakeEffectProvider(options = {}) {
462
463
  },
463
464
  applyVectorEffect(id, target, values) {
464
465
  const d = get(id);
465
- if (d.kind !== "motion" && d.kind !== "vector") throw new (0, _chunkIOXKXQNWcjs.EffectUnsupportedError)(`"${id}" is a ${d.kind} effect, not a vector one.`, id);
466
- const v = _chunkIOXKXQNWcjs.resolveEffectValues.call(void 0, d, values);
466
+ if (d.kind !== "motion" && d.kind !== "vector") throw new (0, _chunkCN4PJHUGcjs.EffectUnsupportedError)(`"${id}" is a ${d.kind} effect, not a vector one.`, id);
467
+ const v = _chunkCN4PJHUGcjs.resolveEffectValues.call(void 0, d, values);
467
468
  const svg = typeof target === "string" ? target : target.svg;
468
469
  const ids = typeof target === "string" || !target.nodeIds ? [...svg.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]) : [...target.nodeIds];
469
470
  const ms = v.ms;
@@ -520,7 +521,7 @@ function effectParameterFields(description) {
520
521
  }
521
522
  var effectFieldValue = (field, value) => value === void 0 || value === null || typeof value === "object" && !Array.isArray(value) ? field.default : value;
522
523
  function effectSamplePicture(width, height, variant = 0) {
523
- const p = _chunkIOXKXQNWcjs.createEffectPixels.call(void 0, width, height);
524
+ const p = _chunkCN4PJHUGcjs.createEffectPixels.call(void 0, width, height);
524
525
  for (let y = 0, i = 0; y < height; y++) {
525
526
  for (let x = 0; x < width; x++, i += 4) {
526
527
  const u = x / Math.max(1, width - 1);
@@ -532,7 +533,22 @@ function effectSamplePicture(width, height, variant = 0) {
532
533
  }
533
534
  return p;
534
535
  }
535
- async function renderEffectThumbnail(provider, effect, size = { width: 96, height: 54 }) {
536
+ function renderEffectThumbnail(provider, effect, size = { width: 96, height: 54 }) {
537
+ let cache = thumbnails.get(provider);
538
+ if (!cache) thumbnails.set(provider, cache = /* @__PURE__ */ new Map());
539
+ const key = `${effect.id}@${size.width}x${size.height}`;
540
+ let out = cache.get(key);
541
+ if (!out) {
542
+ out = thumbnailQueue.then(() => renderThumbnail(provider, effect, size));
543
+ thumbnailQueue = out.catch(() => void 0);
544
+ out.catch(() => cache.delete(key));
545
+ cache.set(key, out);
546
+ }
547
+ return out;
548
+ }
549
+ var thumbnails = /* @__PURE__ */ new WeakMap();
550
+ var thumbnailQueue = Promise.resolve();
551
+ async function renderThumbnail(provider, effect, size) {
536
552
  const d = "inputs" in effect ? effect : await provider.describe(effect.id);
537
553
  const renderer = await provider.createRenderer(d.id, size);
538
554
  try {
@@ -584,5 +600,6 @@ function groupEffectsByCategory(effects) {
584
600
 
585
601
 
586
602
 
587
- exports.EFFECT_ERROR_CODES = _chunkIOXKXQNWcjs.EFFECT_ERROR_CODES; exports.EFFECT_KINDS = _chunkTK53AB4Qcjs.EFFECT_KINDS; exports.EFFECT_USES = _chunkTK53AB4Qcjs.EFFECT_USES; exports.EFFECT_USE_LABELS = EFFECT_USE_LABELS; exports.EffectControlError = _chunkIOXKXQNWcjs.EffectControlError; exports.EffectDisposedError = _chunkIOXKXQNWcjs.EffectDisposedError; exports.EffectError = _chunkIOXKXQNWcjs.EffectError; exports.EffectUnsupportedError = _chunkIOXKXQNWcjs.EffectUnsupportedError; exports.EffectsSlot = _chunkIOXKXQNWcjs.EffectsSlot; exports.NoEffectsProviderError = _chunkIOXKXQNWcjs.NoEffectsProviderError; exports.UnknownEffectError = _chunkIOXKXQNWcjs.UnknownEffectError; exports.applyEffectMask = _chunkIOXKXQNWcjs.applyEffectMask; exports.checkEffectProvider = checkEffectProvider; exports.createEffectPixels = _chunkIOXKXQNWcjs.createEffectPixels; exports.createEffectToolProvider = _chunkTK53AB4Qcjs.createEffectToolProvider; exports.createFakeEffectProvider = createFakeEffectProvider; exports.effectControlField = effectControlField; exports.effectFieldValue = effectFieldValue; exports.effectParameterFields = effectParameterFields; exports.effectSamplePicture = effectSamplePicture; exports.effectValueErrors = _chunkIOXKXQNWcjs.effectValueErrors; exports.encodeEffectFloat = _chunkIOXKXQNWcjs.encodeEffectFloat; exports.groupEffectsByCategory = groupEffectsByCategory; exports.isEffectError = _chunkIOXKXQNWcjs.isEffectError; exports.isEffectPixels = _chunkIOXKXQNWcjs.isEffectPixels; exports.linearToSrgb = _chunkIOXKXQNWcjs.linearToSrgb; exports.renderEffectThumbnail = renderEffectThumbnail; exports.resolveEffectValues = _chunkIOXKXQNWcjs.resolveEffectValues; exports.searchEffectSummaries = _chunkIOXKXQNWcjs.searchEffectSummaries; exports.srgbToLinear = _chunkIOXKXQNWcjs.srgbToLinear;
603
+
604
+ exports.EFFECT_ERROR_CODES = _chunkCN4PJHUGcjs.EFFECT_ERROR_CODES; exports.EFFECT_KINDS = _chunkFHYDQOPCcjs.EFFECT_KINDS; exports.EFFECT_USES = _chunkFHYDQOPCcjs.EFFECT_USES; exports.EFFECT_USE_LABELS = EFFECT_USE_LABELS; exports.EffectControlError = _chunkCN4PJHUGcjs.EffectControlError; exports.EffectDisposedError = _chunkCN4PJHUGcjs.EffectDisposedError; exports.EffectError = _chunkCN4PJHUGcjs.EffectError; exports.EffectRendererPool = _chunkCN4PJHUGcjs.EffectRendererPool; exports.EffectUnsupportedError = _chunkCN4PJHUGcjs.EffectUnsupportedError; exports.EffectsSlot = _chunkCN4PJHUGcjs.EffectsSlot; exports.NoEffectsProviderError = _chunkCN4PJHUGcjs.NoEffectsProviderError; exports.UnknownEffectError = _chunkCN4PJHUGcjs.UnknownEffectError; exports.applyEffectMask = _chunkCN4PJHUGcjs.applyEffectMask; exports.checkEffectProvider = checkEffectProvider; exports.createEffectPixels = _chunkCN4PJHUGcjs.createEffectPixels; exports.createEffectToolProvider = _chunkFHYDQOPCcjs.createEffectToolProvider; exports.createFakeEffectProvider = createFakeEffectProvider; exports.effectControlField = effectControlField; exports.effectFieldValue = effectFieldValue; exports.effectParameterFields = effectParameterFields; exports.effectSamplePicture = effectSamplePicture; exports.effectValueErrors = _chunkCN4PJHUGcjs.effectValueErrors; exports.encodeEffectFloat = _chunkCN4PJHUGcjs.encodeEffectFloat; exports.groupEffectsByCategory = groupEffectsByCategory; exports.isEffectError = _chunkCN4PJHUGcjs.isEffectError; exports.isEffectPixels = _chunkCN4PJHUGcjs.isEffectPixels; exports.linearToSrgb = _chunkCN4PJHUGcjs.linearToSrgb; exports.renderEffectThumbnail = renderEffectThumbnail; exports.resolveEffectValues = _chunkCN4PJHUGcjs.resolveEffectValues; exports.searchEffectSummaries = _chunkCN4PJHUGcjs.searchEffectSummaries; exports.srgbToLinear = _chunkCN4PJHUGcjs.srgbToLinear;
588
605
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/effects/index.cjs","../../src/effects/conformance.ts","../../src/effects/fake.ts","../../src/effects/ui.ts"],"names":["input"],"mappings":"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;AAClF,MAAA;AACK,MAAA;AACX,MAAA;AACN,QAAA;AACsF,QAAA;AACzC,QAAA;AACvC,QAAA;AACP,MAAA;AACD,IAAA;AACD,EAAA;AACO,EAAA;AACR;AF4GqG;AACA;AGrd/B;AAC7D,EAAA;AACI,EAAA;AACD,EAAA;AACF,EAAA;AACF,EAAA;AACC,EAAA;AACK,EAAA;AACd;AAEY;AACoF;AAO+B;AACxB,EAAA;AACvE,EAAA;AACL,EAAA;AAC4B,EAAA;AACyD,IAAA;AACf,IAAA;AAChG,EAAA;AAC6C,EAAA;AACP,IAAA;AACA,IAAA;AACkD,IAAA;AACxF,EAAA;AACiF,EAAA;AAC1D,EAAA;AACoE,IAAA;AACF,IAAA;AACzF,EAAA;AAC+F,EAAA;AAC/D,IAAA;AAC2E,IAAA;AAC3G,EAAA;AACO,EAAA;AACR;AAGuH;AAEzE,EAAA;AAE9C;AAI4D;AAMkC;AACnD,EAAA;AACF,EAAA;AACC,IAAA;AACJ,MAAA;AACC,MAAA;AACoB,MAAA;AACsC,MAAA;AACA,MAAA;AAC/F,IAAA;AACD,EAAA;AACO,EAAA;AACR;AAQ6M;AACnI,EAAA;AAChB,EAAA;AACrD,EAAA;AACwC,IAAA;AACG,IAAA;AACiC,IAAA;AACkB,IAAA;AAChG,EAAA;AACgB,IAAA;AAClB,EAAA;AACD;AAG6H;AACxF,EAAA;AACX,EAAA;AACO,IAAA;AAChB,IAAA;AACgB,IAAA;AAChC,EAAA;AAC0E,EAAA;AAC3E;AH2bqG;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/effects/index.cjs","sourcesContent":[null,"/**\n * `checkEffectProvider`: a conformance check any provider can run in its own tests, so editors\n * can rely on the contract (strict control schemas, valid defaults, typed errors, renderers that\n * return the size they were asked for).\n */\n\nimport { isEffectError } from './errors.js';\nimport { createEffectPixels, effectValueErrors, isEffectPixels } from './helpers.js';\nimport { EFFECT_KINDS, EFFECT_USES, type EffectDescription, type EffectPixels, type EffectProvider, type EffectSummary } from './types.js';\n\nexport interface EffectConformanceIssue {\n\teffectId?: string;\n\tmessage: string;\n}\n\nexport interface EffectConformanceOptions {\n\t/** Effects to check in depth (default: every listed effect). */\n\tids?: readonly string[];\n\t/** Also create renderers and render one small frame of each pixel effect (default true). */\n\trender?: boolean;\n\t/** Renderer size for the render check (default 8). */\n\tsize?: number;\n}\n\nconst codeOf = (e: unknown) => (isEffectError(e) ? e.code : e instanceof Error ? `untyped ${e.name}: ${e.message}` : String(e));\n\nasync function rejectsWith(code: string, run: () => unknown): Promise<string | null> {\n\ttry {\n\t\tawait run();\n\t\treturn `did not fail (expected ${code})`;\n\t} catch (e) {\n\t\tconst got = codeOf(e);\n\t\treturn got === code ? null : `failed with ${got} (expected ${code})`;\n\t}\n}\n\nfunction checkSummary(e: EffectSummary, issues: EffectConformanceIssue[]) {\n\tconst at = (message: string) => issues.push({ effectId: e.id, message });\n\tif (!e.id || typeof e.id !== 'string') at('summary has no id');\n\tif (!e.name) at('summary has no name');\n\tif (!EFFECT_KINDS.includes(e.kind)) at(`kind \"${e.kind}\" is not one of ${EFFECT_KINDS.join(', ')}`);\n\tif (!e.category) at('summary has no category');\n\tif (!Array.isArray(e.uses) || !e.uses.length) at('summary has no uses');\n\tfor (const u of e.uses ?? []) if (!EFFECT_USES.includes(u)) at(`use \"${u}\" is not one of ${EFFECT_USES.join(', ')}`);\n\tif (typeof e.transparent !== 'boolean') at('transparent is not a boolean');\n\tif (typeof e.loop !== 'boolean') at('loop is not a boolean');\n\tif (e.duration !== undefined && !(e.duration > 0)) at('duration must be a positive number of ms when present');\n}\n\nfunction checkDescription(id: string, d: EffectDescription, issues: EffectConformanceIssue[]) {\n\tconst at = (message: string) => issues.push({ effectId: id, message });\n\tif (d.id !== id) at(`describe(\"${id}\") returned id \"${d.id}\"`);\n\tconst c = d.controls;\n\tif (!c || c.type !== 'object') at('controls is not an object schema');\n\tif (c?.additionalProperties !== false) at('controls must set additionalProperties: false');\n\tfor (const [name, schema] of Object.entries(c?.properties ?? {})) {\n\t\tif (!schema.description || !String(schema.description).trim()) at(`control \"${name}\" has no description`);\n\t\tif (!(name in (d.defaults ?? {}))) at(`control \"${name}\" has no default`);\n\t}\n\tfor (const name of Object.keys(d.defaults ?? {})) if (!(name in (c?.properties ?? {}))) at(`default \"${name}\" is not a control`);\n\tfor (const e of effectValueErrors(d, d.defaults)) at(`default ${e.path}: ${e.message}`);\n\tfor (const p of d.presets ?? []) for (const e of effectValueErrors(d, p.values)) at(`preset \"${p.name}\" ${e.path}: ${e.message}`);\n\tfor (const input of d.inputs ?? []) {\n\t\tif (!input.name || !input.description) at('every input needs a name and a description');\n\t\tif (input.kind !== 'image' && input.kind !== 'audio') at(`input \"${input.name}\" has kind \"${input.kind}\"`);\n\t}\n\tif (d.uses.includes('filter') && !d.inputs.some((i) => i.name === 'source')) at('a filter must read an input named \"source\"');\n\tif (d.uses.includes('transition') && !(d.inputs.some((i) => i.name === 'from') && d.inputs.some((i) => i.name === 'to'))) {\n\t\tat('a transition must read inputs named \"from\" and \"to\"');\n\t}\n}\n\nfunction grey(size: number): EffectPixels {\n\tconst p = createEffectPixels(size, size);\n\tp.data.fill(128);\n\treturn p;\n}\n\nasync function checkRender(provider: EffectProvider, d: EffectDescription, size: number, issues: EffectConformanceIssue[]) {\n\tconst at = (message: string) => issues.push({ effectId: d.id, message });\n\tconst inputs = Object.fromEntries(d.inputs.filter((i) => i.kind === 'image').map((i) => [i.name, grey(size)]));\n\tlet renderer;\n\ttry {\n\t\trenderer = await provider.createRenderer(d.id, { width: size, height: size });\n\t} catch (e) {\n\t\t// A vector effect may need the host's help to become pixels (a Worker has no DOM to lay out SVG).\n\t\tif (!(d.kind === 'vector' && codeOf(e) === 'unsupported')) at(`createRenderer failed: ${codeOf(e)}`);\n\t\treturn;\n\t}\n\ttry {\n\t\tconst out = await renderer.render({ time: 0, inputs });\n\t\tif (!isEffectPixels(out)) at('render did not return EffectPixels');\n\t\telse if (out.width !== size || out.height !== size || out.data.length !== size * size * 4) at(`render returned ${out.width}×${out.height}, not ${size}×${size}`);\n\t} catch (e) {\n\t\tat(`render failed: ${codeOf(e)}`);\n\t}\n\tconst bad = await rejectsWith('invalid_values', () => renderer.render({ time: 0, inputs, values: { __not_a_control__: 1 } }));\n\tif (bad) at(`render with an unknown control ${bad}`);\n\trenderer.dispose();\n\tconst after = await rejectsWith('disposed', () => renderer.render({ time: 0, inputs }));\n\tif (after) at(`render after dispose ${after}`);\n}\n\n/** Runs the contract checks against `provider` and returns every issue found (empty when it conforms). */\nexport async function checkEffectProvider(provider: EffectProvider, options: EffectConformanceOptions = {}): Promise<EffectConformanceIssue[]> {\n\tconst issues: EffectConformanceIssue[] = [];\n\tconst top = (message: string) => issues.push({ message });\n\tif (!provider.id) top('provider has no id');\n\tconst caps = provider.capabilities;\n\tfor (const k of ['supportsWorker', 'supportsFloat', 'supportsMask', 'supportsHostImages', 'supportsVector'] as const) {\n\t\tif (typeof caps?.[k] !== 'boolean') top(`capabilities.${k} is not a boolean`);\n\t}\n\tif (caps?.supportsVector && typeof provider.applyVectorEffect !== 'function') top('supportsVector is set but applyVectorEffect is missing');\n\n\tconst list = await provider.list();\n\tconst ids = new Set<string>();\n\tfor (const e of list) {\n\t\tif (ids.has(e.id)) top(`duplicate id \"${e.id}\"`);\n\t\tids.add(e.id);\n\t\tcheckSummary(e, issues);\n\t}\n\tconst all = await provider.search('');\n\tif (all.length !== list.length) top(`search(\"\") returned ${all.length} effects, list() ${list.length}`);\n\tconst firstUse = list[0]?.uses[0];\n\tif (firstUse) {\n\t\tconst filtered = await provider.search('', { use: firstUse });\n\t\tif (filtered.some((e) => !e.uses.includes(firstUse))) top(`search(\"\", { use: \"${firstUse}\" }) returned effects without that use`);\n\t}\n\tconst unknown = await rejectsWith('unknown_effect', () => provider.describe('__no_such_effect__'));\n\tif (unknown) top(`describe of an unknown id ${unknown}`);\n\tconst unknownRenderer = await rejectsWith('unknown_effect', () => provider.createRenderer('__no_such_effect__', { width: 4, height: 4 }));\n\tif (unknownRenderer) top(`createRenderer of an unknown id ${unknownRenderer}`);\n\n\tconst size = options.size ?? 8;\n\tfor (const id of options.ids ?? [...ids]) {\n\t\tlet d: EffectDescription;\n\t\ttry {\n\t\t\td = await provider.describe(id);\n\t\t} catch (e) {\n\t\t\tissues.push({ effectId: id, message: `describe failed: ${codeOf(e)}` });\n\t\t\tcontinue;\n\t\t}\n\t\tcheckDescription(id, d, issues);\n\t\tif (options.render !== false && d.kind !== 'motion') await checkRender(provider, d, size, issues);\n\t\tif ((d.kind === 'vector' || d.kind === 'motion') && caps.supportsVector && provider.applyVectorEffect) {\n\t\t\ttry {\n\t\t\t\tconst 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>');\n\t\t\t\tif (typeof m.svg !== 'string' || !m.svg.includes('<svg')) issues.push({ effectId: id, message: 'applyVectorEffect returned no SVG' });\n\t\t\t\tif (!(m.duration >= 0)) issues.push({ effectId: id, message: 'applyVectorEffect returned no duration' });\n\t\t\t} catch (e) {\n\t\t\t\t// The probe target is a bare <rect>; an effect that needs more (a path to follow, a fill colour) may refuse it.\n\t\t\t\tif (codeOf(e) !== 'unsupported') issues.push({ effectId: id, message: `applyVectorEffect failed: ${codeOf(e)}` });\n\t\t\t}\n\t\t\tconst bad = await rejectsWith('invalid_values', () => provider.applyVectorEffect!(id, '<svg xmlns=\"http://www.w3.org/2000/svg\"/>', { __not_a_control__: 1 }));\n\t\t\tif (bad) issues.push({ effectId: id, message: `applyVectorEffect with an unknown control ${bad}` });\n\t\t}\n\t}\n\treturn issues;\n}\n","/**\n * `createFakeEffectProvider`: a small, deterministic, CPU-only provider implementing the whole\n * contract. Tests and the demo use it; it is also a reference for real providers.\n *\n * Effects (ids prefixed `fake.`):\n * - `invert` (filter), `tint` (filter)\n * - `gradient` (generator, loops)\n * - `bar` (title / overlay: a lower-third bar that slides in, transparent)\n * - `crossfade` (transition)\n * - `fade-in` (motion, vector only)\n */\n\nimport { EffectDisposedError, EffectUnsupportedError, UnknownEffectError } from './errors.js';\nimport { createEffectPixels, encodeEffectFloat, isEffectPixels, resolveEffectValues, searchEffectSummaries, srgbToLinear } from './helpers.js';\nimport type {\n\tEffectDescription,\n\tEffectImageSource,\n\tEffectPixels,\n\tEffectProvider,\n\tEffectProviderCapabilities,\n\tEffectRenderer,\n\tEffectRendererOptions,\n\tEffectRenderRequest,\n\tEffectSummary,\n\tEffectValues,\n\tEffectVectorMotion,\n\tEffectVectorTarget\n} from './types.js';\n\nconst HEX = '^#[0-9a-fA-F]{6}$';\nconst colour = (description: string) => ({ type: 'string' as const, pattern: HEX, description });\nconst amount = (description: string) => ({ type: 'number' as const, minimum: 0, maximum: 1, description });\n\nconst DESCRIPTIONS: readonly EffectDescription[] = [\n\t{\n\t\tid: 'fake.invert',\n\t\tname: 'Invert',\n\t\tkind: 'shader',\n\t\tcategory: 'image',\n\t\tuses: ['filter'],\n\t\tdescription: 'Inverts the colours of the source.',\n\t\ttags: ['negative', 'colour'],\n\t\ttransparent: false,\n\t\tloop: false,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: { amount: amount('How far to invert, 0 (unchanged) to 1 (fully inverted). Default 1.') }\n\t\t},\n\t\tdefaults: { amount: 1 },\n\t\tpresets: [{ name: 'Half', values: { amount: 0.5 } }],\n\t\tinputs: [{ name: 'source', kind: 'image', description: 'The picture to invert.', required: true }]\n\t},\n\t{\n\t\tid: 'fake.tint',\n\t\tname: 'Tint',\n\t\tkind: 'shader',\n\t\tcategory: 'image',\n\t\tuses: ['filter'],\n\t\tdescription: 'Mixes the source towards a colour.',\n\t\ttags: ['colour', 'wash'],\n\t\ttransparent: false,\n\t\tloop: false,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: {\n\t\t\t\tcolor: colour('Tint colour as #rrggbb. Default #ff8800.'),\n\t\t\t\tamount: amount('How far to mix towards the colour, 0–1. Default 0.5.')\n\t\t\t}\n\t\t},\n\t\tdefaults: { color: '#ff8800', amount: 0.5 },\n\t\tpresets: [\n\t\t\t{ name: 'Sepia', values: { color: '#704214', amount: 0.4 } },\n\t\t\t{ name: 'Cool', values: { color: '#2060ff', amount: 0.25 } }\n\t\t],\n\t\tinputs: [{ name: 'source', kind: 'image', description: 'The picture to tint.', required: true }]\n\t},\n\t{\n\t\tid: 'fake.gradient',\n\t\tname: 'Gradient',\n\t\tkind: 'shader',\n\t\tcategory: 'gradient',\n\t\tuses: ['generator'],\n\t\tdescription: 'A linear gradient between two colours that can turn over time.',\n\t\ttags: ['background'],\n\t\ttransparent: false,\n\t\tloop: true,\n\t\tduration: 4000,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: {\n\t\t\t\tfrom: colour('Start colour as #rrggbb. Default #1e3a8a.'),\n\t\t\t\tto: colour('End colour as #rrggbb. Default #f472b6.'),\n\t\t\t\tangle: { type: 'number', minimum: 0, maximum: 360, description: 'Direction in degrees, 0 = left to right, 90 = top to bottom. Default 90.' },\n\t\t\t\tspin: { type: 'number', minimum: -360, maximum: 360, description: 'Turn per loop (4 s) in degrees. Default 0.' }\n\t\t\t}\n\t\t},\n\t\tdefaults: { from: '#1e3a8a', to: '#f472b6', angle: 90, spin: 0 },\n\t\tpresets: [{ name: 'Sunset', values: { from: '#f97316', to: '#7c3aed' } }],\n\t\tinputs: []\n\t},\n\t{\n\t\tid: 'fake.bar',\n\t\tname: 'Lower-third bar',\n\t\tkind: 'shader',\n\t\tcategory: 'broadcast',\n\t\tuses: ['title', 'overlay'],\n\t\tdescription: 'A coloured bar that slides in from the left in the lower third. Transparent elsewhere.',\n\t\ttags: ['lower third', 'title'],\n\t\ttransparent: true,\n\t\tloop: false,\n\t\tduration: 2000,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: {\n\t\t\t\tcolor: colour('Bar colour as #rrggbb. Default #0ea5e9.'),\n\t\t\t\theight: { type: 'number', minimum: 0.02, maximum: 0.5, description: 'Bar height as a fraction of the picture height. Default 0.12.' },\n\t\t\t\tslide: { type: 'number', minimum: 0, maximum: 5000, description: 'Slide-in time (ms). Default 500.' }\n\t\t\t}\n\t\t},\n\t\tdefaults: { color: '#0ea5e9', height: 0.12, slide: 500 },\n\t\tpresets: [],\n\t\tinputs: []\n\t},\n\t{\n\t\tid: 'fake.crossfade',\n\t\tname: 'Crossfade',\n\t\tkind: 'shader',\n\t\tcategory: 'transition',\n\t\tuses: ['transition'],\n\t\tdescription: 'Dissolves from one picture to the next.',\n\t\ttags: ['dissolve', 'mix'],\n\t\ttransparent: false,\n\t\tloop: false,\n\t\tduration: 1000,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: { curve: { type: 'string', enum: ['linear', 'smooth'], description: 'Mix curve: linear, or smooth (ease in and out). Default smooth.' } }\n\t\t},\n\t\tdefaults: { curve: 'smooth' },\n\t\tpresets: [],\n\t\tinputs: [\n\t\t\t{ name: 'from', kind: 'image', description: 'The outgoing picture.', required: true },\n\t\t\t{ name: 'to', kind: 'image', description: 'The incoming picture.', required: true }\n\t\t]\n\t},\n\t{\n\t\tid: 'fake.fade-in',\n\t\tname: 'Fade in',\n\t\tkind: 'motion',\n\t\tcategory: 'morph',\n\t\tuses: ['motion'],\n\t\tdescription: 'Fades vector elements in from transparent, one after another.',\n\t\ttags: ['appear', 'entrance'],\n\t\ttransparent: true,\n\t\tloop: false,\n\t\tduration: 600,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: {\n\t\t\t\tms: { type: 'number', minimum: 0, maximum: 10000, description: 'Fade time per element (ms). Default 600.' },\n\t\t\t\tstagger: { type: 'number', minimum: 0, maximum: 5000, description: 'Delay between elements (ms). Default 0.' }\n\t\t\t}\n\t\t},\n\t\tdefaults: { ms: 600, stagger: 0 },\n\t\tpresets: [{ name: 'Cascade', values: { ms: 400, stagger: 120 } }],\n\t\tinputs: []\n\t}\n];\n\nconst summaryOf = ({ controls: _c, defaults: _d, presets: _p, inputs: _i, ...summary }: EffectDescription): EffectSummary => summary;\n\nconst hexToRgb = (hex: string): [number, number, number] => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)) as [number, number, number];\nconst smooth = (t: number) => t * t * (3 - 2 * t);\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\n\nfunction input(request: EffectRenderRequest, name: string, id: string, width: number, height: number): EffectPixels {\n\tconst src: EffectImageSource | undefined = request.inputs?.[name];\n\tif (!src) throw new EffectUnsupportedError(`Effect \"${id}\" needs an input named \"${name}\".`, id);\n\tif (!isEffectPixels(src)) throw new EffectUnsupportedError(`The fake provider only takes EffectPixels inputs (\"${name}\").`, id);\n\tif (src.width !== width || src.height !== height) {\n\t\tthrow new EffectUnsupportedError(`Input \"${name}\" is ${src.width}×${src.height}; the renderer is ${width}×${height}.`, id);\n\t}\n\treturn src;\n}\n\n/** Per-pixel colour work in 8-bit sRGB; float output (when asked) is derived from it. */\nfunction renderPixels(d: EffectDescription, v: Record<string, EffectValues[string]>, request: EffectRenderRequest, width: number, height: number): EffectPixels {\n\tconst out = createEffectPixels(width, height);\n\tconst o = out.data;\n\tswitch (d.id) {\n\t\tcase 'fake.invert': {\n\t\t\tconst s = input(request, 'source', d.id, width, height).data;\n\t\t\tconst k = v.amount as number;\n\t\t\tfor (let i = 0; i < o.length; i += 4) {\n\t\t\t\tfor (let c = 0; c < 3; c++) o[i + c] = s[i + c] + (255 - 2 * s[i + c]) * k;\n\t\t\t\to[i + 3] = s[i + 3];\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'fake.tint': {\n\t\t\tconst s = input(request, 'source', d.id, width, height).data;\n\t\t\tconst rgb = hexToRgb(v.color as string);\n\t\t\tconst k = v.amount as number;\n\t\t\tfor (let i = 0; i < o.length; i += 4) {\n\t\t\t\tfor (let c = 0; c < 3; c++) o[i + c] = s[i + c] + (rgb[c] - s[i + c]) * k;\n\t\t\t\to[i + 3] = s[i + 3];\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'fake.gradient': {\n\t\t\tconst a = hexToRgb(v.from as string);\n\t\t\tconst b = hexToRgb(v.to as string);\n\t\t\tconst turn = ((request.time % (d.duration ?? 4000)) / (d.duration ?? 4000)) * (v.spin as number);\n\t\t\tconst rad = (((v.angle as number) + turn) * Math.PI) / 180;\n\t\t\tconst dx = Math.cos(rad);\n\t\t\tconst dy = Math.sin(rad);\n\t\t\tconst half = (Math.abs(dx) * width + Math.abs(dy) * height) / 2 || 1;\n\t\t\tfor (let y = 0, i = 0; y < height; y++) {\n\t\t\t\tfor (let x = 0; x < width; x++, i += 4) {\n\t\t\t\t\tconst t = clamp01(((x + 0.5 - width / 2) * dx + (y + 0.5 - height / 2) * dy) / (2 * half) + 0.5);\n\t\t\t\t\tfor (let c = 0; c < 3; c++) o[i + c] = a[c] + (b[c] - a[c]) * t;\n\t\t\t\t\to[i + 3] = 255;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'fake.bar': {\n\t\t\tconst rgb = hexToRgb(v.color as string);\n\t\t\tconst slide = v.slide as number;\n\t\t\tconst shown = slide > 0 ? smooth(clamp01(request.time / slide)) : 1;\n\t\t\tconst top = Math.round(height * (0.75 - (v.height as number) / 2));\n\t\t\tconst bottom = Math.round(height * (0.75 + (v.height as number) / 2));\n\t\t\tconst right = Math.round(width * 0.7 * shown);\n\t\t\tfor (let y = Math.max(0, top); y < Math.min(height, bottom); y++) {\n\t\t\t\tfor (let x = 0; x < right; x++) {\n\t\t\t\t\tconst i = (y * width + x) * 4;\n\t\t\t\t\to[i] = rgb[0];\n\t\t\t\t\to[i + 1] = rgb[1];\n\t\t\t\t\to[i + 2] = rgb[2];\n\t\t\t\t\to[i + 3] = 255;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'fake.crossfade': {\n\t\t\tconst from = input(request, 'from', d.id, width, height).data;\n\t\t\tconst to = input(request, 'to', d.id, width, height).data;\n\t\t\tconst p = clamp01(request.progress ?? request.time / (d.duration ?? 1000));\n\t\t\tconst k = v.curve === 'smooth' ? smooth(p) : p;\n\t\t\tfor (let i = 0; i < o.length; i++) o[i] = from[i] + (to[i] - from[i]) * k;\n\t\t\tbreak;\n\t\t}\n\t\tdefault:\n\t\t\tthrow new EffectUnsupportedError(`\"${d.id}\" is a ${d.kind} effect; use applyVectorEffect.`, d.id);\n\t}\n\treturn out;\n}\n\nfunction toFloat(p: EffectPixels): EffectPixels {\n\tconst f = new Float32Array(p.data.length);\n\tfor (let i = 0; i < f.length; i += 4) {\n\t\tf[i] = srgbToLinear(p.data[i] / 255);\n\t\tf[i + 1] = srgbToLinear(p.data[i + 1] / 255);\n\t\tf[i + 2] = srgbToLinear(p.data[i + 2] / 255);\n\t\tf[i + 3] = p.data[i + 3] / 255;\n\t}\n\treturn encodeEffectFloat({ ...p, float: f });\n}\n\nfunction maskResult(out: EffectPixels, mask: EffectImageSource, source: EffectPixels | undefined, id: string): void {\n\tif (!isEffectPixels(mask) || mask.width !== out.width || mask.height !== out.height) {\n\t\tthrow new EffectUnsupportedError('The mask must be EffectPixels the size of the renderer.', id);\n\t}\n\tconst o = out.data;\n\tfor (let i = 0; i < o.length; i += 4) {\n\t\tconst k = mask.data[i + 3] / 255;\n\t\tif (source) for (let c = 0; c < 4; c++) o[i + c] = source.data[i + c] + (o[i + c] - source.data[i + c]) * k;\n\t\telse o[i + 3] *= k;\n\t}\n}\n\nexport interface FakeEffectProviderOptions {\n\t/** Overrides of the capability flags, to test how editors degrade. */\n\tcapabilities?: Partial<EffectProviderCapabilities>;\n\t/** Called on every render (tests count frames with it). */\n\tonRender?: (effectId: string, request: EffectRenderRequest) => void;\n}\n\n/** A deterministic CPU effects provider with six effects (see the module comment). */\nexport function createFakeEffectProvider(options: FakeEffectProviderOptions = {}): EffectProvider {\n\tconst capabilities: EffectProviderCapabilities = {\n\t\tsupportsWorker: false,\n\t\tsupportsFloat: true,\n\t\tsupportsMask: true,\n\t\tsupportsHostImages: false,\n\t\tsupportsVector: true,\n\t\tmaxSize: 8192,\n\t\t...options.capabilities\n\t};\n\tconst byId = new Map(DESCRIPTIONS.map((d) => [d.id, d]));\n\tconst get = (id: string) => {\n\t\tconst d = byId.get(id);\n\t\tif (!d) throw new UnknownEffectError(id, 'the fake provider');\n\t\treturn d;\n\t};\n\tconst summaries = DESCRIPTIONS.map(summaryOf);\n\n\tconst provider: EffectProvider = {\n\t\tid: 'fake',\n\t\tname: 'Fake effects',\n\t\tversion: '1',\n\t\tcapabilities,\n\t\tlist: (o) => searchEffectSummaries(summaries, '', o),\n\t\tsearch: (q, o) => searchEffectSummaries(summaries, q, o),\n\t\tdescribe: (id) => structuredClone(get(id)),\n\t\tcreateRenderer(id: string, ro: EffectRendererOptions): EffectRenderer {\n\t\t\tconst d = get(id);\n\t\t\tif (d.kind === 'motion') throw new EffectUnsupportedError(`\"${id}\" is a motion effect; use applyVectorEffect.`, id);\n\t\t\tconst { width, height } = ro;\n\t\t\tconst depth = ro.depth ?? 8;\n\t\t\tif (!(width > 0 && height > 0) || width > capabilities.maxSize! || height > capabilities.maxSize!) {\n\t\t\t\tthrow new EffectUnsupportedError(`Renderer size ${width}×${height} is outside 1–${capabilities.maxSize}.`, id);\n\t\t\t}\n\t\t\tlet disposed = false;\n\t\t\treturn {\n\t\t\t\teffectId: id,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tdepth,\n\t\t\t\tasync render(request) {\n\t\t\t\t\tif (disposed) throw new EffectDisposedError(id);\n\t\t\t\t\tif (!Number.isFinite(request.time) || request.time < 0) {\n\t\t\t\t\t\tthrow new EffectUnsupportedError(`time must be a finite number of ms ≥ 0, got ${request.time}.`, id);\n\t\t\t\t\t}\n\t\t\t\t\tconst values = resolveEffectValues(d, request.values);\n\t\t\t\t\toptions.onRender?.(id, request);\n\t\t\t\t\tconst out = renderPixels(d, values, request, width, height);\n\t\t\t\t\tif (request.mask) {\n\t\t\t\t\t\tif (!capabilities.supportsMask) throw new EffectUnsupportedError('This provider does not take masks.', id);\n\t\t\t\t\t\tconst source = d.uses.includes('filter') ? (request.inputs?.source as EffectPixels) : undefined;\n\t\t\t\t\t\tmaskResult(out, request.mask, source, id);\n\t\t\t\t\t}\n\t\t\t\t\treturn depth > 8 && capabilities.supportsFloat ? toFloat(out) : out;\n\t\t\t\t},\n\t\t\t\tdispose() {\n\t\t\t\t\tdisposed = true;\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\tapplyVectorEffect(id: string, target: EffectVectorTarget, values?: EffectValues): EffectVectorMotion {\n\t\t\tconst d = get(id);\n\t\t\tif (d.kind !== 'motion' && d.kind !== 'vector') throw new EffectUnsupportedError(`\"${id}\" is a ${d.kind} effect, not a vector one.`, id);\n\t\t\tconst v = resolveEffectValues(d, values);\n\t\t\tconst svg = typeof target === 'string' ? target : target.svg;\n\t\t\tconst ids = typeof target === 'string' || !target.nodeIds ? [...svg.matchAll(/\\sid=\"([^\"]+)\"/g)].map((m) => m[1]) : [...target.nodeIds];\n\t\t\tconst ms = v.ms as number;\n\t\t\tconst stagger = v.stagger as number;\n\t\t\treturn {\n\t\t\t\tsvg,\n\t\t\t\tanimate: ids.map((elementId, i) => ({ id: elementId, delay: i * stagger, ms, attrs: { opacity: [0, 1] as const } })),\n\t\t\t\tduration: ms + Math.max(0, ids.length - 1) * stagger,\n\t\t\t\tloop: false\n\t\t\t};\n\t\t}\n\t};\n\treturn provider;\n}\n","/**\n * Framework-free helpers for effects UIs (the React and Svelte effects browser and parameter\n * panel): control schemas as parameter fields, preview thumbnails from a provider's renderer,\n * and labels.\n */\n\nimport type { JsonSchema } from '../tools/schema.js';\nimport type { ParameterField, ParameterValue } from '../parameters/types.js';\nimport { createEffectPixels } from './helpers.js';\nimport type { EffectDescription, EffectPixels, EffectProvider, EffectSummary, EffectUse, EffectValue } from './types.js';\n\n/** Names for the uses, for filters and badges. */\nexport const EFFECT_USE_LABELS: Readonly<Record<EffectUse, string>> = {\n\tfilter: 'Filter',\n\ttransition: 'Transition',\n\tgenerator: 'Generator',\n\toverlay: 'Overlay',\n\ttitle: 'Title',\n\tmotion: 'Motion',\n\tinteractive: 'Interactive'\n};\n\nconst HEX = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;\nconst typeOf = (s: JsonSchema) => (Array.isArray(s.type) ? s.type.find((t) => t !== 'null') : s.type);\n\n/**\n * One control as a parameter field: numbers and integers (sliders when bounded), switches,\n * choices (`enum`), colours (strings whose default is `#rrggbb`), vectors (2–4 numbers) and text.\n * `null` for a control no field can edit (objects, long lists).\n */\nexport function effectControlField(name: string, schema: JsonSchema, fallback: EffectValue | undefined): ParameterField | null {\n\tconst base = { id: name, ...(schema.title ? { label: schema.title } : {}), ...(schema.description ? { description: schema.description } : {}) };\n\tconst def = (schema.default ?? fallback) as EffectValue | undefined;\n\tconst type = typeOf(schema);\n\tif (Array.isArray(schema.enum) && schema.enum.length) {\n\t\tconst options = schema.enum.filter((v): v is string | number => typeof v === 'string' || typeof v === 'number');\n\t\treturn { ...base, type: 'choice', options, default: (typeof def === 'string' || typeof def === 'number' ? def : options[0]) as string | number, control: options.length <= 4 ? 'segmented' : 'select' };\n\t}\n\tif (type === 'number' || type === 'integer') {\n\t\tconst min = schema.minimum ?? schema.exclusiveMinimum;\n\t\tconst max = schema.maximum ?? schema.exclusiveMaximum;\n\t\treturn { ...base, type: type === 'integer' ? 'int' : 'number', default: typeof def === 'number' ? def : (min ?? 0), ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}), ...(schema.multipleOf ? { step: schema.multipleOf } : {}) };\n\t}\n\tif (type === 'boolean') return { ...base, type: 'boolean', default: def === true };\n\tif (type === 'string') {\n\t\tif (typeof def === 'string' && HEX.test(def)) return { ...base, type: 'color', alpha: def.length === 9, default: def };\n\t\treturn { ...base, type: 'text', default: typeof def === 'string' ? def : '', ...(schema.maxLength ? { maxLength: schema.maxLength } : {}) };\n\t}\n\tif (type === 'array' && Array.isArray(def) && def.length >= 2 && def.length <= 4 && def.every((n) => typeof n === 'number')) {\n\t\tconst items = schema.items ?? {};\n\t\treturn { ...base, type: 'vector', size: def.length as 2 | 3 | 4, default: [...def], ...(items.minimum !== undefined ? { min: items.minimum } : {}), ...(items.maximum !== undefined ? { max: items.maximum } : {}) };\n\t}\n\treturn null;\n}\n\n/** An effect's controls as parameter fields, in schema order (controls no field can edit are left out). */\nexport function effectParameterFields(description: Pick<EffectDescription, 'controls' | 'defaults'>): ParameterField[] {\n\treturn Object.entries(description.controls.properties)\n\t\t.map(([name, schema]) => effectControlField(name, schema, description.defaults[name]))\n\t\t.filter((f): f is ParameterField => !!f);\n}\n\n/** A saved value as a field shows it (`ParameterValue`), or the field's default. */\nexport const effectFieldValue = (field: ParameterField, value: unknown): ParameterValue =>\n\tvalue === undefined || value === null || (typeof value === 'object' && !Array.isArray(value)) ? field.default : (value as ParameterValue);\n\n/**\n * A colourful, deterministic picture for previews of filters and transitions (`variant` 0 and 1\n * differ, for a transition's two sides).\n */\nexport function effectSamplePicture(width: number, height: number, variant = 0): EffectPixels {\n\tconst p = createEffectPixels(width, height);\n\tfor (let y = 0, i = 0; y < height; y++) {\n\t\tfor (let x = 0; x < width; x++, i += 4) {\n\t\t\tconst u = x / Math.max(1, width - 1);\n\t\t\tconst v = y / Math.max(1, height - 1);\n\t\t\tconst check = (Math.floor(u * 4) + Math.floor(v * 4)) % 2;\n\t\t\tif (variant === 0) p.data.set([Math.round(40 + 200 * u), Math.round(80 + 120 * v), Math.round(200 - 120 * u) + check * 30, 255], i);\n\t\t\telse p.data.set([Math.round(230 - 60 * v), Math.round(190 - 100 * u), 60 + check * 40, 255], i);\n\t\t}\n\t}\n\treturn p;\n}\n\n/**\n * A small preview of an effect from the provider's own renderer: filters run on a sample picture,\n * transitions go half way between two, generators and overlays draw at `time` (default half\n * their length, or 1 s). The renderer is disposed afterwards. Rejects like the provider does\n * (motion effects have no pixels).\n */\nexport async function renderEffectThumbnail(provider: EffectProvider, effect: EffectSummary | EffectDescription, size: { width: number; height: number } = { width: 96, height: 54 }): Promise<EffectPixels> {\n\tconst d = 'inputs' in effect ? effect : await provider.describe(effect.id);\n\tconst renderer = await provider.createRenderer(d.id, size);\n\ttry {\n\t\tconst time = d.duration ? d.duration / 2 : 1000;\n\t\tconst inputs: Record<string, EffectPixels> = {};\n\t\tfor (const input of d.inputs) if (input.kind === 'image') inputs[input.name] = effectSamplePicture(size.width, size.height, input.name === 'to' ? 1 : 0);\n\t\treturn await renderer.render({ time, inputs, ...(d.uses.includes('transition') ? { progress: 0.5 } : {}) });\n\t} finally {\n\t\trenderer.dispose();\n\t}\n}\n\n/** Effects grouped by category, categories in first-seen order. */\nexport function groupEffectsByCategory<T extends EffectSummary>(effects: readonly T[]): { category: string; effects: T[] }[] {\n\tconst groups = new Map<string, T[]>();\n\tfor (const e of effects) {\n\t\tconst g = groups.get(e.category);\n\t\tif (g) g.push(e);\n\t\telse groups.set(e.category, [e]);\n\t}\n\treturn [...groups].map(([category, list]) => ({ category, effects: list }));\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/effects/index.cjs","../../src/effects/conformance.ts","../../src/effects/fake.ts","../../src/effects/ui.ts"],"names":["input"],"mappings":"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,MAAA;AAClF,MAAA;AACK,MAAA;AACX,MAAA;AACN,QAAA;AACsF,QAAA;AACzC,QAAA;AACvC,QAAA;AACP,MAAA;AACD,IAAA;AACD,EAAA;AACO,EAAA;AACR;AF6GqG;AACA;AGtd/B;AAC7D,EAAA;AACI,EAAA;AACD,EAAA;AACF,EAAA;AACF,EAAA;AACC,EAAA;AACK,EAAA;AACd;AAEY;AACoF;AAO+B;AACxB,EAAA;AACvE,EAAA;AACL,EAAA;AAC4B,EAAA;AACyD,IAAA;AACf,IAAA;AAChG,EAAA;AAC6C,EAAA;AACP,IAAA;AACA,IAAA;AACkD,IAAA;AACxF,EAAA;AACiF,EAAA;AAC1D,EAAA;AACoE,IAAA;AACF,IAAA;AACzF,EAAA;AAC+F,EAAA;AAC/D,IAAA;AAC2E,IAAA;AAC3G,EAAA;AACO,EAAA;AACR;AAGuH;AAEzE,EAAA;AAE9C;AAI4D;AAMkC;AACnD,EAAA;AACF,EAAA;AACC,IAAA;AACJ,MAAA;AACC,MAAA;AACoB,MAAA;AACsC,MAAA;AACA,MAAA;AAC/F,IAAA;AACD,EAAA;AACO,EAAA;AACR;AASuM;AACnK,EAAA;AACqB,EAAA;AACH,EAAA;AAC9B,EAAA;AACb,EAAA;AAE8D,IAAA;AAC7B,IAAA;AACR,IAAA;AAChB,IAAA;AACnB,EAAA;AACO,EAAA;AACR;AAEmF;AAC5B;AAE6G;AAC1F,EAAA;AAChB,EAAA;AACrD,EAAA;AACwC,IAAA;AACG,IAAA;AACiC,IAAA;AACkB,IAAA;AAChG,EAAA;AACgB,IAAA;AAClB,EAAA;AACD;AAG6H;AACxF,EAAA;AACX,EAAA;AACO,IAAA;AAChB,IAAA;AACgB,IAAA;AAChC,EAAA;AAC0E,EAAA;AAC3E;AHwbqG;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/effects/index.cjs","sourcesContent":[null,"/**\n * `checkEffectProvider`: a conformance check any provider can run in its own tests, so editors\n * can rely on the contract (strict control schemas, valid defaults, typed errors, renderers that\n * return the size they were asked for).\n */\n\nimport { isEffectError } from './errors.js';\nimport { createEffectPixels, effectValueErrors, isEffectPixels } from './helpers.js';\nimport { EFFECT_KINDS, EFFECT_USES, type EffectDescription, type EffectPixels, type EffectProvider, type EffectSummary } from './types.js';\n\nexport interface EffectConformanceIssue {\n\teffectId?: string;\n\tmessage: string;\n}\n\nexport interface EffectConformanceOptions {\n\t/** Effects to check in depth (default: every listed effect). */\n\tids?: readonly string[];\n\t/** Also create renderers and render one small frame of each pixel effect (default true). */\n\trender?: boolean;\n\t/** Renderer size for the render check (default 8). */\n\tsize?: number;\n}\n\nconst codeOf = (e: unknown) => (isEffectError(e) ? e.code : e instanceof Error ? `untyped ${e.name}: ${e.message}` : String(e));\n\nasync function rejectsWith(code: string, run: () => unknown): Promise<string | null> {\n\ttry {\n\t\tawait run();\n\t\treturn `did not fail (expected ${code})`;\n\t} catch (e) {\n\t\tconst got = codeOf(e);\n\t\treturn got === code ? null : `failed with ${got} (expected ${code})`;\n\t}\n}\n\nfunction checkSummary(e: EffectSummary, issues: EffectConformanceIssue[]) {\n\tconst at = (message: string) => issues.push({ effectId: e.id, message });\n\tif (!e.id || typeof e.id !== 'string') at('summary has no id');\n\tif (!e.name) at('summary has no name');\n\tif (!EFFECT_KINDS.includes(e.kind)) at(`kind \"${e.kind}\" is not one of ${EFFECT_KINDS.join(', ')}`);\n\tif (!e.category) at('summary has no category');\n\tif (!Array.isArray(e.uses) || !e.uses.length) at('summary has no uses');\n\tfor (const u of e.uses ?? []) if (!EFFECT_USES.includes(u)) at(`use \"${u}\" is not one of ${EFFECT_USES.join(', ')}`);\n\tif (typeof e.transparent !== 'boolean') at('transparent is not a boolean');\n\tif (typeof e.loop !== 'boolean') at('loop is not a boolean');\n\tif (e.duration !== undefined && !(e.duration > 0)) at('duration must be a positive number of ms when present');\n}\n\nfunction checkDescription(id: string, d: EffectDescription, issues: EffectConformanceIssue[]) {\n\tconst at = (message: string) => issues.push({ effectId: id, message });\n\tif (d.id !== id) at(`describe(\"${id}\") returned id \"${d.id}\"`);\n\tconst c = d.controls;\n\tif (!c || c.type !== 'object') at('controls is not an object schema');\n\tif (c?.additionalProperties !== false) at('controls must set additionalProperties: false');\n\tfor (const [name, schema] of Object.entries(c?.properties ?? {})) {\n\t\tif (!schema.description || !String(schema.description).trim()) at(`control \"${name}\" has no description`);\n\t\tif (!(name in (d.defaults ?? {}))) at(`control \"${name}\" has no default`);\n\t}\n\tfor (const name of Object.keys(d.defaults ?? {})) if (!(name in (c?.properties ?? {}))) at(`default \"${name}\" is not a control`);\n\tfor (const e of effectValueErrors(d, d.defaults)) at(`default ${e.path}: ${e.message}`);\n\tfor (const p of d.presets ?? []) for (const e of effectValueErrors(d, p.values)) at(`preset \"${p.name}\" ${e.path}: ${e.message}`);\n\tfor (const input of d.inputs ?? []) {\n\t\tif (!input.name || !input.description) at('every input needs a name and a description');\n\t\tif (input.kind !== 'image' && input.kind !== 'audio') at(`input \"${input.name}\" has kind \"${input.kind}\"`);\n\t}\n\tif (d.uses.includes('filter') && !d.inputs.some((i) => i.name === 'source')) at('a filter must read an input named \"source\"');\n\tif (d.uses.includes('transition') && !(d.inputs.some((i) => i.name === 'from') && d.inputs.some((i) => i.name === 'to'))) {\n\t\tat('a transition must read inputs named \"from\" and \"to\"');\n\t}\n}\n\nfunction grey(size: number): EffectPixels {\n\tconst p = createEffectPixels(size, size);\n\tp.data.fill(128);\n\treturn p;\n}\n\nasync function checkRender(provider: EffectProvider, d: EffectDescription, size: number, issues: EffectConformanceIssue[]) {\n\tconst at = (message: string) => issues.push({ effectId: d.id, message });\n\tconst inputs = Object.fromEntries(d.inputs.filter((i) => i.kind === 'image').map((i) => [i.name, grey(size)]));\n\tlet renderer;\n\ttry {\n\t\trenderer = await provider.createRenderer(d.id, { width: size, height: size });\n\t} catch (e) {\n\t\t// A vector effect may need the host's help to become pixels (a Worker has no DOM to lay out SVG).\n\t\tif (!(d.kind === 'vector' && codeOf(e) === 'unsupported')) at(`createRenderer failed: ${codeOf(e)}`);\n\t\treturn;\n\t}\n\ttry {\n\t\tconst out = await renderer.render({ time: 0, inputs });\n\t\tif (!isEffectPixels(out)) at('render did not return EffectPixels');\n\t\telse if (out.width !== size || out.height !== size || out.data.length !== size * size * 4) at(`render returned ${out.width}×${out.height}, not ${size}×${size}`);\n\t} catch (e) {\n\t\tat(`render failed: ${codeOf(e)}`);\n\t}\n\tconst bad = await rejectsWith('invalid_values', () => renderer.render({ time: 0, inputs, values: { __not_a_control__: 1 } }));\n\tif (bad) at(`render with an unknown control ${bad}`);\n\trenderer.dispose();\n\tconst after = await rejectsWith('disposed', () => renderer.render({ time: 0, inputs }));\n\tif (after) at(`render after dispose ${after}`);\n}\n\n/** Runs the contract checks against `provider` and returns every issue found (empty when it conforms). */\nexport async function checkEffectProvider(provider: EffectProvider, options: EffectConformanceOptions = {}): Promise<EffectConformanceIssue[]> {\n\tconst issues: EffectConformanceIssue[] = [];\n\tconst top = (message: string) => issues.push({ message });\n\tif (!provider.id) top('provider has no id');\n\tconst caps = provider.capabilities;\n\tfor (const k of ['supportsWorker', 'supportsFloat', 'supportsMask', 'supportsHostImages', 'supportsVector'] as const) {\n\t\tif (typeof caps?.[k] !== 'boolean') top(`capabilities.${k} is not a boolean`);\n\t}\n\tif (caps?.supportsVector && typeof provider.applyVectorEffect !== 'function') top('supportsVector is set but applyVectorEffect is missing');\n\n\tconst list = await provider.list();\n\tconst ids = new Set<string>();\n\tfor (const e of list) {\n\t\tif (ids.has(e.id)) top(`duplicate id \"${e.id}\"`);\n\t\tids.add(e.id);\n\t\tcheckSummary(e, issues);\n\t}\n\tconst all = await provider.search('');\n\tif (all.length !== list.length) top(`search(\"\") returned ${all.length} effects, list() ${list.length}`);\n\tconst firstUse = list[0]?.uses[0];\n\tif (firstUse) {\n\t\tconst filtered = await provider.search('', { use: firstUse });\n\t\tif (filtered.some((e) => !e.uses.includes(firstUse))) top(`search(\"\", { use: \"${firstUse}\" }) returned effects without that use`);\n\t}\n\tconst unknown = await rejectsWith('unknown_effect', () => provider.describe('__no_such_effect__'));\n\tif (unknown) top(`describe of an unknown id ${unknown}`);\n\tconst unknownRenderer = await rejectsWith('unknown_effect', () => provider.createRenderer('__no_such_effect__', { width: 4, height: 4 }));\n\tif (unknownRenderer) top(`createRenderer of an unknown id ${unknownRenderer}`);\n\n\tconst size = options.size ?? 8;\n\tfor (const id of options.ids ?? [...ids]) {\n\t\tlet d: EffectDescription;\n\t\ttry {\n\t\t\td = await provider.describe(id);\n\t\t} catch (e) {\n\t\t\tissues.push({ effectId: id, message: `describe failed: ${codeOf(e)}` });\n\t\t\tcontinue;\n\t\t}\n\t\tcheckDescription(id, d, issues);\n\t\tif (options.render !== false && d.kind !== 'motion') await checkRender(provider, d, size, issues);\n\t\tif ((d.kind === 'vector' || d.kind === 'motion') && caps.supportsVector && provider.applyVectorEffect) {\n\t\t\ttry {\n\t\t\t\tconst 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>');\n\t\t\t\tif (typeof m.svg !== 'string' || !m.svg.includes('<svg')) issues.push({ effectId: id, message: 'applyVectorEffect returned no SVG' });\n\t\t\t\tif (!(m.duration >= 0)) issues.push({ effectId: id, message: 'applyVectorEffect returned no duration' });\n\t\t\t} catch (e) {\n\t\t\t\t// The probe target is a bare <rect>; an effect that needs more (a path to follow, a fill colour) may refuse it.\n\t\t\t\tif (codeOf(e) !== 'unsupported') issues.push({ effectId: id, message: `applyVectorEffect failed: ${codeOf(e)}` });\n\t\t\t}\n\t\t\tconst bad = await rejectsWith('invalid_values', () => provider.applyVectorEffect!(id, '<svg xmlns=\"http://www.w3.org/2000/svg\"/>', { __not_a_control__: 1 }));\n\t\t\tif (bad) issues.push({ effectId: id, message: `applyVectorEffect with an unknown control ${bad}` });\n\t\t}\n\t}\n\treturn issues;\n}\n","/**\n * `createFakeEffectProvider`: a small, deterministic, CPU-only provider implementing the whole\n * contract. Tests and the demo use it; it is also a reference for real providers.\n *\n * Effects (ids prefixed `fake.`):\n * - `invert` (filter), `tint` (filter)\n * - `gradient` (generator, loops)\n * - `bar` (title / overlay: a lower-third bar that slides in, transparent)\n * - `crossfade` (transition)\n * - `fade-in` (motion, vector only)\n */\n\nimport { EffectDisposedError, EffectUnsupportedError, UnknownEffectError } from './errors.js';\nimport { createEffectPixels, encodeEffectFloat, isEffectPixels, resolveEffectValues, searchEffectSummaries, srgbToLinear } from './helpers.js';\nimport type {\n\tEffectDescription,\n\tEffectImageSource,\n\tEffectPixels,\n\tEffectProvider,\n\tEffectProviderCapabilities,\n\tEffectRenderer,\n\tEffectRendererOptions,\n\tEffectRenderRequest,\n\tEffectSummary,\n\tEffectValues,\n\tEffectVectorMotion,\n\tEffectVectorTarget\n} from './types.js';\n\nconst HEX = '^#[0-9a-fA-F]{6}$';\nconst colour = (description: string) => ({ type: 'string' as const, pattern: HEX, description });\nconst amount = (description: string) => ({ type: 'number' as const, minimum: 0, maximum: 1, description });\n\nconst DESCRIPTIONS: readonly EffectDescription[] = [\n\t{\n\t\tid: 'fake.invert',\n\t\tname: 'Invert',\n\t\tkind: 'shader',\n\t\tcategory: 'image',\n\t\tuses: ['filter'],\n\t\tdescription: 'Inverts the colours of the source.',\n\t\ttags: ['negative', 'colour'],\n\t\ttransparent: false,\n\t\tloop: false,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: { amount: amount('How far to invert, 0 (unchanged) to 1 (fully inverted). Default 1.') }\n\t\t},\n\t\tdefaults: { amount: 1 },\n\t\tpresets: [{ name: 'Half', values: { amount: 0.5 } }],\n\t\tinputs: [{ name: 'source', kind: 'image', description: 'The picture to invert.', required: true }]\n\t},\n\t{\n\t\tid: 'fake.tint',\n\t\tname: 'Tint',\n\t\tkind: 'shader',\n\t\tcategory: 'image',\n\t\tuses: ['filter'],\n\t\tdescription: 'Mixes the source towards a colour.',\n\t\ttags: ['colour', 'wash'],\n\t\ttransparent: false,\n\t\tloop: false,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: {\n\t\t\t\tcolor: colour('Tint colour as #rrggbb. Default #ff8800.'),\n\t\t\t\tamount: amount('How far to mix towards the colour, 0–1. Default 0.5.')\n\t\t\t}\n\t\t},\n\t\tdefaults: { color: '#ff8800', amount: 0.5 },\n\t\tpresets: [\n\t\t\t{ name: 'Sepia', values: { color: '#704214', amount: 0.4 } },\n\t\t\t{ name: 'Cool', values: { color: '#2060ff', amount: 0.25 } }\n\t\t],\n\t\tinputs: [{ name: 'source', kind: 'image', description: 'The picture to tint.', required: true }]\n\t},\n\t{\n\t\tid: 'fake.gradient',\n\t\tname: 'Gradient',\n\t\tkind: 'shader',\n\t\tcategory: 'gradient',\n\t\tuses: ['generator'],\n\t\tdescription: 'A linear gradient between two colours that can turn over time.',\n\t\ttags: ['background'],\n\t\ttransparent: false,\n\t\tloop: true,\n\t\tduration: 4000,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: {\n\t\t\t\tfrom: colour('Start colour as #rrggbb. Default #1e3a8a.'),\n\t\t\t\tto: colour('End colour as #rrggbb. Default #f472b6.'),\n\t\t\t\tangle: { type: 'number', minimum: 0, maximum: 360, description: 'Direction in degrees, 0 = left to right, 90 = top to bottom. Default 90.' },\n\t\t\t\tspin: { type: 'number', minimum: -360, maximum: 360, description: 'Turn per loop (4 s) in degrees. Default 0.' }\n\t\t\t}\n\t\t},\n\t\tdefaults: { from: '#1e3a8a', to: '#f472b6', angle: 90, spin: 0 },\n\t\tpresets: [{ name: 'Sunset', values: { from: '#f97316', to: '#7c3aed' } }],\n\t\tinputs: []\n\t},\n\t{\n\t\tid: 'fake.bar',\n\t\tname: 'Lower-third bar',\n\t\tkind: 'shader',\n\t\tcategory: 'broadcast',\n\t\tuses: ['title', 'overlay'],\n\t\tdescription: 'A coloured bar that slides in from the left in the lower third. Transparent elsewhere.',\n\t\ttags: ['lower third', 'title'],\n\t\ttransparent: true,\n\t\tloop: false,\n\t\tduration: 2000,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: {\n\t\t\t\tcolor: colour('Bar colour as #rrggbb. Default #0ea5e9.'),\n\t\t\t\theight: { type: 'number', minimum: 0.02, maximum: 0.5, description: 'Bar height as a fraction of the picture height. Default 0.12.' },\n\t\t\t\tslide: { type: 'number', minimum: 0, maximum: 5000, description: 'Slide-in time (ms). Default 500.' }\n\t\t\t}\n\t\t},\n\t\tdefaults: { color: '#0ea5e9', height: 0.12, slide: 500 },\n\t\tpresets: [],\n\t\tinputs: []\n\t},\n\t{\n\t\tid: 'fake.crossfade',\n\t\tname: 'Crossfade',\n\t\tkind: 'shader',\n\t\tcategory: 'transition',\n\t\tuses: ['transition'],\n\t\tdescription: 'Dissolves from one picture to the next.',\n\t\ttags: ['dissolve', 'mix'],\n\t\ttransparent: false,\n\t\tloop: false,\n\t\tduration: 1000,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: { curve: { type: 'string', enum: ['linear', 'smooth'], description: 'Mix curve: linear, or smooth (ease in and out). Default smooth.' } }\n\t\t},\n\t\tdefaults: { curve: 'smooth' },\n\t\tpresets: [],\n\t\tinputs: [\n\t\t\t{ name: 'from', kind: 'image', description: 'The outgoing picture.', required: true },\n\t\t\t{ name: 'to', kind: 'image', description: 'The incoming picture.', required: true }\n\t\t]\n\t},\n\t{\n\t\tid: 'fake.fade-in',\n\t\tname: 'Fade in',\n\t\tkind: 'motion',\n\t\tcategory: 'morph',\n\t\tuses: ['motion'],\n\t\tdescription: 'Fades vector elements in from transparent, one after another.',\n\t\ttags: ['appear', 'entrance'],\n\t\ttransparent: true,\n\t\tloop: false,\n\t\tduration: 600,\n\t\tcontrols: {\n\t\t\ttype: 'object',\n\t\t\tadditionalProperties: false,\n\t\t\tproperties: {\n\t\t\t\tms: { type: 'number', minimum: 0, maximum: 10000, description: 'Fade time per element (ms). Default 600.' },\n\t\t\t\tstagger: { type: 'number', minimum: 0, maximum: 5000, description: 'Delay between elements (ms). Default 0.' }\n\t\t\t}\n\t\t},\n\t\tdefaults: { ms: 600, stagger: 0 },\n\t\tpresets: [{ name: 'Cascade', values: { ms: 400, stagger: 120 } }],\n\t\tinputs: []\n\t}\n];\n\nconst summaryOf = ({ controls: _c, defaults: _d, presets: _p, inputs: _i, ...summary }: EffectDescription): EffectSummary => summary;\n\nconst hexToRgb = (hex: string): [number, number, number] => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)) as [number, number, number];\nconst smooth = (t: number) => t * t * (3 - 2 * t);\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\n\nfunction input(request: EffectRenderRequest, name: string, id: string, width: number, height: number): EffectPixels {\n\tconst src: EffectImageSource | undefined = request.inputs?.[name];\n\tif (!src) throw new EffectUnsupportedError(`Effect \"${id}\" needs an input named \"${name}\".`, id);\n\tif (!isEffectPixels(src)) throw new EffectUnsupportedError(`The fake provider only takes EffectPixels inputs (\"${name}\").`, id);\n\tif (src.width !== width || src.height !== height) {\n\t\tthrow new EffectUnsupportedError(`Input \"${name}\" is ${src.width}×${src.height}; the renderer is ${width}×${height}.`, id);\n\t}\n\treturn src;\n}\n\n/** Per-pixel colour work in 8-bit sRGB; float output (when asked) is derived from it. */\nfunction renderPixels(d: EffectDescription, v: Record<string, EffectValues[string]>, request: EffectRenderRequest, width: number, height: number): EffectPixels {\n\tconst out = createEffectPixels(width, height);\n\tconst o = out.data;\n\tswitch (d.id) {\n\t\tcase 'fake.invert': {\n\t\t\tconst s = input(request, 'source', d.id, width, height).data;\n\t\t\tconst k = v.amount as number;\n\t\t\tfor (let i = 0; i < o.length; i += 4) {\n\t\t\t\tfor (let c = 0; c < 3; c++) o[i + c] = s[i + c] + (255 - 2 * s[i + c]) * k;\n\t\t\t\to[i + 3] = s[i + 3];\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'fake.tint': {\n\t\t\tconst s = input(request, 'source', d.id, width, height).data;\n\t\t\tconst rgb = hexToRgb(v.color as string);\n\t\t\tconst k = v.amount as number;\n\t\t\tfor (let i = 0; i < o.length; i += 4) {\n\t\t\t\tfor (let c = 0; c < 3; c++) o[i + c] = s[i + c] + (rgb[c] - s[i + c]) * k;\n\t\t\t\to[i + 3] = s[i + 3];\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'fake.gradient': {\n\t\t\tconst a = hexToRgb(v.from as string);\n\t\t\tconst b = hexToRgb(v.to as string);\n\t\t\tconst turn = ((request.time % (d.duration ?? 4000)) / (d.duration ?? 4000)) * (v.spin as number);\n\t\t\tconst rad = (((v.angle as number) + turn) * Math.PI) / 180;\n\t\t\tconst dx = Math.cos(rad);\n\t\t\tconst dy = Math.sin(rad);\n\t\t\tconst half = (Math.abs(dx) * width + Math.abs(dy) * height) / 2 || 1;\n\t\t\tfor (let y = 0, i = 0; y < height; y++) {\n\t\t\t\tfor (let x = 0; x < width; x++, i += 4) {\n\t\t\t\t\tconst t = clamp01(((x + 0.5 - width / 2) * dx + (y + 0.5 - height / 2) * dy) / (2 * half) + 0.5);\n\t\t\t\t\tfor (let c = 0; c < 3; c++) o[i + c] = a[c] + (b[c] - a[c]) * t;\n\t\t\t\t\to[i + 3] = 255;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'fake.bar': {\n\t\t\tconst rgb = hexToRgb(v.color as string);\n\t\t\tconst slide = v.slide as number;\n\t\t\tconst shown = slide > 0 ? smooth(clamp01(request.time / slide)) : 1;\n\t\t\tconst top = Math.round(height * (0.75 - (v.height as number) / 2));\n\t\t\tconst bottom = Math.round(height * (0.75 + (v.height as number) / 2));\n\t\t\tconst right = Math.round(width * 0.7 * shown);\n\t\t\tfor (let y = Math.max(0, top); y < Math.min(height, bottom); y++) {\n\t\t\t\tfor (let x = 0; x < right; x++) {\n\t\t\t\t\tconst i = (y * width + x) * 4;\n\t\t\t\t\to[i] = rgb[0];\n\t\t\t\t\to[i + 1] = rgb[1];\n\t\t\t\t\to[i + 2] = rgb[2];\n\t\t\t\t\to[i + 3] = 255;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'fake.crossfade': {\n\t\t\tconst from = input(request, 'from', d.id, width, height).data;\n\t\t\tconst to = input(request, 'to', d.id, width, height).data;\n\t\t\tconst p = clamp01(request.progress ?? request.time / (d.duration ?? 1000));\n\t\t\tconst k = v.curve === 'smooth' ? smooth(p) : p;\n\t\t\tfor (let i = 0; i < o.length; i++) o[i] = from[i] + (to[i] - from[i]) * k;\n\t\t\tbreak;\n\t\t}\n\t\tdefault:\n\t\t\tthrow new EffectUnsupportedError(`\"${d.id}\" is a ${d.kind} effect; use applyVectorEffect.`, d.id);\n\t}\n\treturn out;\n}\n\nfunction toFloat(p: EffectPixels): EffectPixels {\n\tconst f = new Float32Array(p.data.length);\n\tfor (let i = 0; i < f.length; i += 4) {\n\t\tf[i] = srgbToLinear(p.data[i] / 255);\n\t\tf[i + 1] = srgbToLinear(p.data[i + 1] / 255);\n\t\tf[i + 2] = srgbToLinear(p.data[i + 2] / 255);\n\t\tf[i + 3] = p.data[i + 3] / 255;\n\t}\n\treturn encodeEffectFloat({ ...p, float: f });\n}\n\nfunction maskResult(out: EffectPixels, mask: EffectImageSource, source: EffectPixels | undefined, id: string): void {\n\tif (!isEffectPixels(mask) || mask.width !== out.width || mask.height !== out.height) {\n\t\tthrow new EffectUnsupportedError('The mask must be EffectPixels the size of the renderer.', id);\n\t}\n\tconst o = out.data;\n\tfor (let i = 0; i < o.length; i += 4) {\n\t\tconst k = mask.data[i + 3] / 255;\n\t\tif (source) for (let c = 0; c < 4; c++) o[i + c] = source.data[i + c] + (o[i + c] - source.data[i + c]) * k;\n\t\telse o[i + 3] *= k;\n\t}\n}\n\nexport interface FakeEffectProviderOptions {\n\t/** Overrides of the capability flags, to test how editors degrade. */\n\tcapabilities?: Partial<EffectProviderCapabilities>;\n\t/** Called on every render (tests count frames with it). */\n\tonRender?: (effectId: string, request: EffectRenderRequest) => void;\n}\n\n/** A deterministic CPU effects provider with six effects (see the module comment). */\nexport function createFakeEffectProvider(options: FakeEffectProviderOptions = {}): EffectProvider {\n\tconst capabilities: EffectProviderCapabilities = {\n\t\tsupportsWorker: false,\n\t\tsupportsFloat: true,\n\t\tsupportsMask: true,\n\t\tsupportsHostImages: false,\n\t\tsupportsVector: true,\n\t\tmaxSize: 8192,\n\t\t...options.capabilities\n\t};\n\tconst byId = new Map(DESCRIPTIONS.map((d) => [d.id, d]));\n\tconst get = (id: string) => {\n\t\tconst d = byId.get(id);\n\t\tif (!d) throw new UnknownEffectError(id, 'the fake provider');\n\t\treturn d;\n\t};\n\tconst summaries = DESCRIPTIONS.map(summaryOf);\n\n\tconst provider: EffectProvider = {\n\t\tid: 'fake',\n\t\tname: 'Fake effects',\n\t\tversion: '1',\n\t\tcapabilities,\n\t\tlist: (o) => searchEffectSummaries(summaries, '', o),\n\t\tsearch: (q, o) => searchEffectSummaries(summaries, q, o),\n\t\tdescribe: (id) => structuredClone(get(id)),\n\t\tcreateRenderer(id: string, ro: EffectRendererOptions): EffectRenderer {\n\t\t\tconst d = get(id);\n\t\t\tif (d.kind === 'motion') throw new EffectUnsupportedError(`\"${id}\" is a motion effect; use applyVectorEffect.`, id);\n\t\t\tconst { width, height } = ro;\n\t\t\tconst depth = ro.depth ?? 8;\n\t\t\tif (!(width > 0 && height > 0) || width > capabilities.maxSize! || height > capabilities.maxSize!) {\n\t\t\t\tthrow new EffectUnsupportedError(`Renderer size ${width}×${height} is outside 1–${capabilities.maxSize}.`, id);\n\t\t\t}\n\t\t\tlet disposed = false;\n\t\t\treturn {\n\t\t\t\teffectId: id,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tdepth,\n\t\t\t\tasync render(request) {\n\t\t\t\t\tif (disposed) throw new EffectDisposedError(id);\n\t\t\t\t\tif (!Number.isFinite(request.time) || request.time < 0) {\n\t\t\t\t\t\tthrow new EffectUnsupportedError(`time must be a finite number of ms ≥ 0, got ${request.time}.`, id);\n\t\t\t\t\t}\n\t\t\t\t\tconst values = resolveEffectValues(d, request.values);\n\t\t\t\t\toptions.onRender?.(id, request);\n\t\t\t\t\tconst out = renderPixels(d, values, request, width, height);\n\t\t\t\t\tif (request.mask) {\n\t\t\t\t\t\tif (!capabilities.supportsMask) throw new EffectUnsupportedError('This provider does not take masks.', id);\n\t\t\t\t\t\tconst source = d.uses.includes('filter') ? (request.inputs?.source as EffectPixels) : undefined;\n\t\t\t\t\t\tmaskResult(out, request.mask, source, id);\n\t\t\t\t\t}\n\t\t\t\t\treturn depth > 8 && capabilities.supportsFloat ? toFloat(out) : out;\n\t\t\t\t},\n\t\t\t\tdispose() {\n\t\t\t\t\tdisposed = true;\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\tapplyVectorEffect(id: string, target: EffectVectorTarget, values?: EffectValues): EffectVectorMotion {\n\t\t\tconst d = get(id);\n\t\t\tif (d.kind !== 'motion' && d.kind !== 'vector') throw new EffectUnsupportedError(`\"${id}\" is a ${d.kind} effect, not a vector one.`, id);\n\t\t\tconst v = resolveEffectValues(d, values);\n\t\t\tconst svg = typeof target === 'string' ? target : target.svg;\n\t\t\tconst ids = typeof target === 'string' || !target.nodeIds ? [...svg.matchAll(/\\sid=\"([^\"]+)\"/g)].map((m) => m[1]) : [...target.nodeIds];\n\t\t\tconst ms = v.ms as number;\n\t\t\tconst stagger = v.stagger as number;\n\t\t\treturn {\n\t\t\t\tsvg,\n\t\t\t\tanimate: ids.map((elementId, i) => ({ id: elementId, delay: i * stagger, ms, attrs: { opacity: [0, 1] as const } })),\n\t\t\t\tduration: ms + Math.max(0, ids.length - 1) * stagger,\n\t\t\t\tloop: false\n\t\t\t};\n\t\t}\n\t};\n\treturn provider;\n}\n","/**\n * Framework-free helpers for effects UIs (the React and Svelte effects browser and parameter\n * panel): control schemas as parameter fields, preview thumbnails from a provider's renderer,\n * and labels.\n */\n\nimport type { JsonSchema } from '../tools/schema.js';\nimport type { ParameterField, ParameterValue } from '../parameters/types.js';\nimport { createEffectPixels } from './helpers.js';\nimport type { EffectDescription, EffectPixels, EffectProvider, EffectSummary, EffectUse, EffectValue } from './types.js';\n\n/** Names for the uses, for filters and badges. */\nexport const EFFECT_USE_LABELS: Readonly<Record<EffectUse, string>> = {\n\tfilter: 'Filter',\n\ttransition: 'Transition',\n\tgenerator: 'Generator',\n\toverlay: 'Overlay',\n\ttitle: 'Title',\n\tmotion: 'Motion',\n\tinteractive: 'Interactive'\n};\n\nconst HEX = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;\nconst typeOf = (s: JsonSchema) => (Array.isArray(s.type) ? s.type.find((t) => t !== 'null') : s.type);\n\n/**\n * One control as a parameter field: numbers and integers (sliders when bounded), switches,\n * choices (`enum`), colours (strings whose default is `#rrggbb`), vectors (2–4 numbers) and text.\n * `null` for a control no field can edit (objects, long lists).\n */\nexport function effectControlField(name: string, schema: JsonSchema, fallback: EffectValue | undefined): ParameterField | null {\n\tconst base = { id: name, ...(schema.title ? { label: schema.title } : {}), ...(schema.description ? { description: schema.description } : {}) };\n\tconst def = (schema.default ?? fallback) as EffectValue | undefined;\n\tconst type = typeOf(schema);\n\tif (Array.isArray(schema.enum) && schema.enum.length) {\n\t\tconst options = schema.enum.filter((v): v is string | number => typeof v === 'string' || typeof v === 'number');\n\t\treturn { ...base, type: 'choice', options, default: (typeof def === 'string' || typeof def === 'number' ? def : options[0]) as string | number, control: options.length <= 4 ? 'segmented' : 'select' };\n\t}\n\tif (type === 'number' || type === 'integer') {\n\t\tconst min = schema.minimum ?? schema.exclusiveMinimum;\n\t\tconst max = schema.maximum ?? schema.exclusiveMaximum;\n\t\treturn { ...base, type: type === 'integer' ? 'int' : 'number', default: typeof def === 'number' ? def : (min ?? 0), ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}), ...(schema.multipleOf ? { step: schema.multipleOf } : {}) };\n\t}\n\tif (type === 'boolean') return { ...base, type: 'boolean', default: def === true };\n\tif (type === 'string') {\n\t\tif (typeof def === 'string' && HEX.test(def)) return { ...base, type: 'color', alpha: def.length === 9, default: def };\n\t\treturn { ...base, type: 'text', default: typeof def === 'string' ? def : '', ...(schema.maxLength ? { maxLength: schema.maxLength } : {}) };\n\t}\n\tif (type === 'array' && Array.isArray(def) && def.length >= 2 && def.length <= 4 && def.every((n) => typeof n === 'number')) {\n\t\tconst items = schema.items ?? {};\n\t\treturn { ...base, type: 'vector', size: def.length as 2 | 3 | 4, default: [...def], ...(items.minimum !== undefined ? { min: items.minimum } : {}), ...(items.maximum !== undefined ? { max: items.maximum } : {}) };\n\t}\n\treturn null;\n}\n\n/** An effect's controls as parameter fields, in schema order (controls no field can edit are left out). */\nexport function effectParameterFields(description: Pick<EffectDescription, 'controls' | 'defaults'>): ParameterField[] {\n\treturn Object.entries(description.controls.properties)\n\t\t.map(([name, schema]) => effectControlField(name, schema, description.defaults[name]))\n\t\t.filter((f): f is ParameterField => !!f);\n}\n\n/** A saved value as a field shows it (`ParameterValue`), or the field's default. */\nexport const effectFieldValue = (field: ParameterField, value: unknown): ParameterValue =>\n\tvalue === undefined || value === null || (typeof value === 'object' && !Array.isArray(value)) ? field.default : (value as ParameterValue);\n\n/**\n * A colourful, deterministic picture for previews of filters and transitions (`variant` 0 and 1\n * differ, for a transition's two sides).\n */\nexport function effectSamplePicture(width: number, height: number, variant = 0): EffectPixels {\n\tconst p = createEffectPixels(width, height);\n\tfor (let y = 0, i = 0; y < height; y++) {\n\t\tfor (let x = 0; x < width; x++, i += 4) {\n\t\t\tconst u = x / Math.max(1, width - 1);\n\t\t\tconst v = y / Math.max(1, height - 1);\n\t\t\tconst check = (Math.floor(u * 4) + Math.floor(v * 4)) % 2;\n\t\t\tif (variant === 0) p.data.set([Math.round(40 + 200 * u), Math.round(80 + 120 * v), Math.round(200 - 120 * u) + check * 30, 255], i);\n\t\t\telse p.data.set([Math.round(230 - 60 * v), Math.round(190 - 100 * u), 60 + check * 40, 255], i);\n\t\t}\n\t}\n\treturn p;\n}\n\n/**\n * A small preview of an effect from the provider's own renderer: filters run on a sample picture,\n * transitions go half way between two, generators and overlays draw at `time` (default half\n * their length, or 1 s). Thumbnails render one at a time (each renderer is disposed before the\n * next is made) and are kept per provider. Rejects like the provider does (motion effects have\n * no pixels).\n */\nexport function renderEffectThumbnail(provider: EffectProvider, effect: EffectSummary | EffectDescription, size: { width: number; height: number } = { width: 96, height: 54 }): Promise<EffectPixels> {\n\tlet cache = thumbnails.get(provider);\n\tif (!cache) thumbnails.set(provider, (cache = new Map()));\n\tconst key = `${effect.id}@${size.width}x${size.height}`;\n\tlet out = cache.get(key);\n\tif (!out) {\n\t\t// One at a time: a browser full of previews would otherwise open a graphics context each.\n\t\tout = thumbnailQueue.then(() => renderThumbnail(provider, effect, size));\n\t\tthumbnailQueue = out.catch(() => undefined);\n\t\tout.catch(() => cache!.delete(key));\n\t\tcache.set(key, out);\n\t}\n\treturn out;\n}\n\nconst thumbnails = new WeakMap<EffectProvider, Map<string, Promise<EffectPixels>>>();\nlet thumbnailQueue: Promise<unknown> = Promise.resolve();\n\nasync function renderThumbnail(provider: EffectProvider, effect: EffectSummary | EffectDescription, size: { width: number; height: number }): Promise<EffectPixels> {\n\tconst d = 'inputs' in effect ? effect : await provider.describe(effect.id);\n\tconst renderer = await provider.createRenderer(d.id, size);\n\ttry {\n\t\tconst time = d.duration ? d.duration / 2 : 1000;\n\t\tconst inputs: Record<string, EffectPixels> = {};\n\t\tfor (const input of d.inputs) if (input.kind === 'image') inputs[input.name] = effectSamplePicture(size.width, size.height, input.name === 'to' ? 1 : 0);\n\t\treturn await renderer.render({ time, inputs, ...(d.uses.includes('transition') ? { progress: 0.5 } : {}) });\n\t} finally {\n\t\trenderer.dispose();\n\t}\n}\n\n/** Effects grouped by category, categories in first-seen order. */\nexport function groupEffectsByCategory<T extends EffectSummary>(effects: readonly T[]): { category: string; effects: T[] }[] {\n\tconst groups = new Map<string, T[]>();\n\tfor (const e of effects) {\n\t\tconst g = groups.get(e.category);\n\t\tif (g) g.push(e);\n\t\telse groups.set(e.category, [e]);\n\t}\n\treturn [...groups].map(([category, list]) => ({ category, effects: list }));\n}\n"]}
@@ -115,8 +115,9 @@ declare function effectSamplePicture(width: number, height: number, variant?: nu
115
115
  /**
116
116
  * A small preview of an effect from the provider's own renderer: filters run on a sample picture,
117
117
  * transitions go half way between two, generators and overlays draw at `time` (default half
118
- * their length, or 1 s). The renderer is disposed afterwards. Rejects like the provider does
119
- * (motion effects have no pixels).
118
+ * their length, or 1 s). Thumbnails render one at a time (each renderer is disposed before the
119
+ * next is made) and are kept per provider. Rejects like the provider does (motion effects have
120
+ * no pixels).
120
121
  */
121
122
  declare function renderEffectThumbnail(provider: EffectProvider, effect: EffectSummary | EffectDescription, size?: {
122
123
  width: number;
@@ -128,4 +129,35 @@ declare function groupEffectsByCategory<T extends EffectSummary>(effects: readon
128
129
  effects: T[];
129
130
  }[];
130
131
 
131
- export { EFFECT_ERROR_CODES, EFFECT_USE_LABELS, type EffectConformanceIssue, type EffectConformanceOptions, EffectControlError, EffectDescription, EffectDisposedError, EffectError, type EffectErrorCode, EffectPixels, EffectProvider, EffectProviderCapabilities, EffectRenderRequest, EffectSummary, EffectUnsupportedError, EffectUse, EffectValue, type FakeEffectProviderOptions, NoEffectsProviderError, UnknownEffectError, checkEffectProvider, createFakeEffectProvider, effectControlField, effectFieldValue, effectParameterFields, effectSamplePicture, groupEffectsByCategory, isEffectError, renderEffectThumbnail };
132
+ /**
133
+ * `EffectRendererPool`: keeps a few live renderers, reused per effect, size and depth.
134
+ *
135
+ * GPU providers hold one graphics context per renderer, and browsers allow only about sixteen
136
+ * WebGL contexts at a time (the oldest is lost after that). So the pool keeps at most
137
+ * `maxRenderers` renderers. It disposes the least recently used one once its renders finish, and it
138
+ * runs one render at a time per renderer, as the contract requires.
139
+ */
140
+
141
+ interface EffectRendererPoolOptions {
142
+ /** Most live renderers (default 4). */
143
+ maxRenderers?: number;
144
+ }
145
+ declare class EffectRendererPool {
146
+ private readonly entries;
147
+ private readonly maxRenderers;
148
+ constructor(options?: EffectRendererPoolOptions);
149
+ /** Live renderers now. */
150
+ get size(): number;
151
+ /** Renders `request` with the provider's renderer for this effect, size and depth (made on first use). */
152
+ render(provider: EffectProvider, id: string, size: {
153
+ width: number;
154
+ height: number;
155
+ depth?: 8 | 16 | 32;
156
+ }, request: EffectRenderRequest): Promise<EffectPixels>;
157
+ /** Disposes the least recently used renderers over the limit, after their queued renders. */
158
+ private evict;
159
+ /** Disposes every renderer (after its queued renders). */
160
+ clear(): void;
161
+ }
162
+
163
+ export { EFFECT_ERROR_CODES, EFFECT_USE_LABELS, type EffectConformanceIssue, type EffectConformanceOptions, EffectControlError, EffectDescription, EffectDisposedError, EffectError, type EffectErrorCode, EffectPixels, EffectProvider, EffectProviderCapabilities, EffectRenderRequest, EffectRendererPool, type EffectRendererPoolOptions, EffectSummary, EffectUnsupportedError, EffectUse, EffectValue, type FakeEffectProviderOptions, NoEffectsProviderError, UnknownEffectError, checkEffectProvider, createFakeEffectProvider, effectControlField, effectFieldValue, effectParameterFields, effectSamplePicture, groupEffectsByCategory, isEffectError, renderEffectThumbnail };