@pixerate/editor 0.252.0 → 0.254.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation-editor/index.cjs +53 -53
- package/dist/animation-editor/index.js +2 -2
- package/dist/brushes/index.cjs +229 -2014
- package/dist/brushes/index.d.cts +5 -1
- package/dist/brushes/index.d.ts +5 -1
- package/dist/brushes/index.js +236 -2021
- package/dist/brushes/patch.d.cts +23 -0
- package/dist/brushes/patch.d.ts +23 -0
- package/dist/brushes/ui.d.cts +105 -0
- package/dist/brushes/ui.d.ts +105 -0
- package/dist/canvas/index.cjs +7 -7
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-QKXO6JPP.cjs → chunk-2OQGPEHH.cjs} +14 -14
- package/dist/chunk-34KLWUH4.cjs +2284 -0
- package/dist/{chunk-Z3BRH4JK.cjs → chunk-557BDWKO.cjs} +6 -6
- package/dist/{chunk-ZBCT5MEA.cjs → chunk-CBVNCWJM.cjs} +7 -7
- package/dist/{chunk-226QDRO3.js → chunk-DHQPRKXF.js} +1 -1
- package/dist/{chunk-ABL7IWFZ.js → chunk-GBEF2D4P.js} +1 -1
- package/dist/{chunk-ISIO63Q5.cjs → chunk-L2JTLW6B.cjs} +150 -25
- package/dist/{chunk-JA57JIT5.js → chunk-MV7CYFMJ.js} +133 -8
- package/dist/{chunk-5E2Q7ZHI.js → chunk-MWUZCEFT.js} +1 -1
- package/dist/chunk-QPHNVZHO.js +2284 -0
- package/dist/effects/conformance.d.cts +1 -0
- package/dist/effects/conformance.d.ts +1 -0
- package/dist/effects/fake.d.cts +7 -1
- package/dist/effects/fake.d.ts +7 -1
- package/dist/effects/helpers.d.cts +36 -4
- package/dist/effects/helpers.d.ts +36 -4
- package/dist/effects/index.cjs +331 -102
- package/dist/effects/index.js +238 -9
- package/dist/effects/types.d.cts +84 -5
- package/dist/effects/types.d.ts +84 -5
- package/dist/image-editor/index.cjs +15 -15
- package/dist/image-editor/index.js +1 -1
- package/dist/paint-editor/brush-controls.d.cts +57 -0
- package/dist/paint-editor/brush-controls.d.ts +57 -0
- package/dist/paint-editor/brush-library-tools.d.cts +15 -0
- package/dist/paint-editor/brush-library-tools.d.ts +15 -0
- package/dist/paint-editor/brush-library.d.cts +29 -0
- package/dist/paint-editor/brush-library.d.ts +29 -0
- package/dist/paint-editor/controller.d.cts +76 -17
- package/dist/paint-editor/controller.d.ts +76 -17
- package/dist/paint-editor/index.cjs +543 -858
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +329 -644
- package/dist/paint-editor/serialize.d.cts +7 -4
- package/dist/paint-editor/serialize.d.ts +7 -4
- package/dist/paint-editor/sticker-tools.d.cts +1 -1
- package/dist/paint-editor/sticker-tools.d.ts +1 -1
- package/dist/paint-editor/strokes.d.cts +10 -1
- package/dist/paint-editor/strokes.d.ts +10 -1
- package/dist/paint-editor/tools.d.cts +1 -1
- package/dist/paint-editor/tools.d.ts +1 -1
- package/dist/paint-editor/ui.d.cts +3 -51
- package/dist/paint-editor/ui.d.ts +3 -51
- package/dist/paint-editor/wet-tools.d.cts +1 -1
- package/dist/paint-editor/wet-tools.d.ts +1 -1
- package/dist/vector/index.cjs +6 -6
- package/dist/vector/index.js +1 -1
- package/dist/video-editor/index.cjs +344 -344
- package/dist/video-editor/index.js +4 -4
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +5 -3
- package/skills/pixerate-paint-editor/SKILL.md +26 -24
- package/skills/pixerate-paint-editor/references/brush-facets.md +30 -0
- package/skills/pixerate-paint-editor/references/gradient-paint.md +1 -1
- package/skills/pixerate-paint-editor/references/light-paint.md +2 -2
- package/skills/pixerate-paint-editor/references/oils.md +1 -1
- package/skills/pixerate-paint-editor/references/particle-brushes.md +8 -5
- package/skills/pixerate-paint-oil-impasto/SKILL.md +1 -1
- package/skills/pixerate-stickers/SKILL.md +1 -1
- package/skills/pixerate-video-editor/SKILL.md +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunkL2JTLW6Bcjs = require('./chunk-L2JTLW6B.cjs');
|
|
8
8
|
|
|
9
9
|
// src/effects/templates.ts
|
|
10
10
|
var SLOT_NAME = /^[A-Za-z][\w-]*$/;
|
|
@@ -133,7 +133,7 @@ function searchTemplateSummaries(catalog, options = {}) {
|
|
|
133
133
|
const words = (_nullishCoalesce(options.query, () => ( ""))).toLowerCase().split(/\s+/).filter(Boolean);
|
|
134
134
|
const scored = [];
|
|
135
135
|
for (const t of catalog) {
|
|
136
|
-
if (!
|
|
136
|
+
if (!_chunkL2JTLW6Bcjs.hasEffectTags.call(void 0, t, options.tags)) continue;
|
|
137
137
|
let score = 0;
|
|
138
138
|
const name = t.name.toLowerCase();
|
|
139
139
|
const rest = [t.id, ..._nullishCoalesce(t.tags, () => ( [])), t.description].join(" ").toLowerCase();
|
|
@@ -200,7 +200,7 @@ function checkEffectSequence(sequence, catalog) {
|
|
|
200
200
|
const p = effectSubjectProblem(layer.subject);
|
|
201
201
|
if (p) problems.push(`${where}: ${p}.`);
|
|
202
202
|
}
|
|
203
|
-
if (layer.role !== void 0 && !
|
|
203
|
+
if (layer.role !== void 0 && !_chunkL2JTLW6Bcjs.EFFECT_LAYER_ROLES.includes(layer.role)) problems.push(`${where}: role is one of ${_chunkL2JTLW6Bcjs.EFFECT_LAYER_ROLES.join(", ")}.`);
|
|
204
204
|
if (layer.rect !== void 0 && !isRect(layer.rect)) problems.push(`${where}: rect must be { x, y, width, height } with width and height > 0.`);
|
|
205
205
|
for (const k of ["from", "until"]) if (layer[k] !== void 0 && !isLayerTime(layer[k])) problems.push(`${where}: ${k} is ms \u2265 0 or { shots: n } with n \u2265 1.`);
|
|
206
206
|
if (layer.opacity !== void 0 && !(typeof layer.opacity === "number" && layer.opacity >= 0 && layer.opacity <= 1)) problems.push(`${where}: opacity is 0\u20131.`);
|
|
@@ -235,8 +235,8 @@ function effectSequenceSlots(sequence) {
|
|
|
235
235
|
return [...out];
|
|
236
236
|
}
|
|
237
237
|
async function fillEffectTemplate(provider, id, values = {}, options = {}) {
|
|
238
|
-
if (!provider) throw new (0,
|
|
239
|
-
if (!provider.capabilities.supportsTemplates || !provider.describeTemplate || !provider.instantiateTemplate) throw new (0,
|
|
238
|
+
if (!provider) throw new (0, _chunkL2JTLW6Bcjs.NoEffectsProviderError)();
|
|
239
|
+
if (!provider.capabilities.supportsTemplates || !provider.describeTemplate || !provider.instantiateTemplate) throw new (0, _chunkL2JTLW6Bcjs.EffectUnsupportedError)(`${provider.name} has no sequence templates (capabilities.supportsTemplates is not set).`);
|
|
240
240
|
const template = await provider.describeTemplate(id);
|
|
241
241
|
const errors = checkTemplateValues(template.slots, values);
|
|
242
242
|
if (errors.length) throw new Error(`${errors.join(" ")} describe_template lists what each slot takes.`);
|
|
@@ -256,7 +256,7 @@ function effectSequenceTiming(sequence) {
|
|
|
256
256
|
const leadIns = [];
|
|
257
257
|
let t = 0;
|
|
258
258
|
sequence.shots.forEach((shot, i) => {
|
|
259
|
-
const transition = i > 0 && shot.transition ? _nullishCoalesce(shot.transition.ms, () => (
|
|
259
|
+
const transition = i > 0 && shot.transition ? _nullishCoalesce(shot.transition.ms, () => ( _chunkL2JTLW6Bcjs.EFFECT_TRANSITION_MS)) : 0;
|
|
260
260
|
const move = typeof _optionalChain([shot, 'access', _4 => _4.camera, 'optionalAccess', _5 => _5.in]) === "object" && i > 0 ? _nullishCoalesce(shot.camera.in.ms, () => ( 0)) : 0;
|
|
261
261
|
const lead = Math.max(transition, move);
|
|
262
262
|
starts.push(t);
|
|
@@ -7,7 +7,7 @@ var _chunkPY4GWSE2cjs = require('./chunk-PY4GWSE2.cjs');
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
var
|
|
10
|
+
var _chunkL2JTLW6Bcjs = require('./chunk-L2JTLW6B.cjs');
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
var _chunkWOFXZ3XUcjs = require('./chunk-WOFXZ3XU.cjs');
|
|
@@ -1316,12 +1316,12 @@ function videoEffectLayerKey(layer, width, height) {
|
|
|
1316
1316
|
}
|
|
1317
1317
|
function mixEffectPixels(a, b, k) {
|
|
1318
1318
|
if (k >= 1) return b;
|
|
1319
|
-
const out =
|
|
1319
|
+
const out = _chunkL2JTLW6Bcjs.createEffectPixels.call(void 0, a.width, a.height);
|
|
1320
1320
|
for (let i = 0; i < out.data.length; i++) out.data[i] = a.data[i] + (b.data[i] - a.data[i]) * k;
|
|
1321
1321
|
return out;
|
|
1322
1322
|
}
|
|
1323
1323
|
function overEffectPixels(base, top, opacity = 1) {
|
|
1324
|
-
const out =
|
|
1324
|
+
const out = _chunkL2JTLW6Bcjs.createEffectPixels.call(void 0, base.width, base.height);
|
|
1325
1325
|
const o = out.data;
|
|
1326
1326
|
const b = base.data;
|
|
1327
1327
|
const t = top.data;
|
|
@@ -1366,7 +1366,7 @@ var VideoEffectsEngine = (_class2 = class {
|
|
|
1366
1366
|
__init10() {this.generation = 0}
|
|
1367
1367
|
|
|
1368
1368
|
constructor(options = {}) {;_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this);_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);
|
|
1369
|
-
this.slot = _nullishCoalesce(options.slot, () => ( new (0,
|
|
1369
|
+
this.slot = _nullishCoalesce(options.slot, () => ( new (0, _chunkL2JTLW6Bcjs.EffectsSlot)()));
|
|
1370
1370
|
this.maxEntries = _nullishCoalesce(options.maxEntries, () => ( 120));
|
|
1371
1371
|
this.toImage = _nullishCoalesce(options.toImage, () => ( defaultToImage));
|
|
1372
1372
|
this.pool = new (0, _chunkPY4GWSE2cjs.EffectRendererPool)({ maxRenderers: options.maxRenderers });
|
|
@@ -1508,7 +1508,7 @@ function checkVideoEffectPlace(d, place) {
|
|
|
1508
1508
|
if (d.kind === "motion" || !d.uses.some((u) => allowed.includes(u))) {
|
|
1509
1509
|
const where = place === "clip" ? "on a clip" : place === "transition" ? "as a transition" : "as an effect clip";
|
|
1510
1510
|
const instead = d.uses.includes("transition") ? "set_video_effect_transition" : d.uses.some((u) => VIDEO_EFFECT_PLACES.generator.includes(u)) ? "add_video_effect_clip" : d.uses.some((u) => u === "filter") ? "apply_effect" : null;
|
|
1511
|
-
throw new (0,
|
|
1511
|
+
throw new (0, _chunkL2JTLW6Bcjs.EffectUnsupportedError)(
|
|
1512
1512
|
`"${d.id}" (${d.name}) is a ${d.uses.join(" / ")} effect and cannot be used ${where}, which takes ${allowed.join(" or ")} effects${instead ? `; use ${instead} instead` : ""}. search_effects with use "${allowed[0]}" lists ones that fit.`,
|
|
1513
1513
|
d.id
|
|
1514
1514
|
);
|
|
@@ -1529,12 +1529,12 @@ function checkVideoEffectValues(d, values, options = {}) {
|
|
|
1529
1529
|
else if (type !== "number" && type !== "integer") errors.push({ path: `/${k}`, message: "only number controls can be keyframed." });
|
|
1530
1530
|
else for (const [i, key] of v.keys.entries()) for (const e of _chunkWOFXZ3XUcjs.validateJsonSchema.call(void 0, key.v, schema)) errors.push({ path: `/${k}/keys/${i}/v`, message: e.message });
|
|
1531
1531
|
}
|
|
1532
|
-
for (const e of
|
|
1532
|
+
for (const e of _chunkL2JTLW6Bcjs.effectValueErrors.call(void 0, d, plain)) {
|
|
1533
1533
|
const name = e.path.split("/")[1];
|
|
1534
1534
|
const about = name && _optionalChain([d, 'access', _46 => _46.controls, 'access', _47 => _47.properties, 'access', _48 => _48[name], 'optionalAccess', _49 => _49.description]);
|
|
1535
1535
|
errors.push(about ? { path: e.path, message: `${e.message} (${about})` } : e);
|
|
1536
1536
|
}
|
|
1537
|
-
if (errors.length) throw new (0,
|
|
1537
|
+
if (errors.length) throw new (0, _chunkL2JTLW6Bcjs.EffectControlError)(d.id, errors);
|
|
1538
1538
|
return { ..._nullishCoalesce(options.base, () => ( d.defaults)), ..._nullishCoalesce(values, () => ( {})) };
|
|
1539
1539
|
}
|
|
1540
1540
|
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
var _chunkWOFXZ3XUcjs = require('./chunk-WOFXZ3XU.cjs');
|
|
4
4
|
|
|
5
5
|
// src/effects/types.ts
|
|
6
|
-
var EFFECT_KINDS = ["shader", "vector", "motion", "camera"];
|
|
7
|
-
var EFFECT_USES = ["filter", "transition", "generator", "overlay", "title", "motion", "interactive", "camera"];
|
|
6
|
+
var EFFECT_KINDS = ["shader", "vector", "motion", "camera", "timing"];
|
|
7
|
+
var EFFECT_USES = ["filter", "transition", "generator", "overlay", "title", "motion", "interactive", "camera", "timing"];
|
|
8
8
|
var CAMERA_MOVE_ROLES = ["transition", "hold"];
|
|
9
9
|
var EFFECT_ACCEPTS = ["path"];
|
|
10
10
|
var EFFECT_PATH_FORMAT = "svg-path";
|
|
11
|
+
var TIMING_DEFAULT_FPS = 30;
|
|
12
|
+
var TIMING_MAX_FRAMES = 128;
|
|
13
|
+
var TIMING_MAX_FPS = 240;
|
|
14
|
+
var TIMING_BLENDS = ["average", "lighten"];
|
|
11
15
|
var EFFECT_SUBJECT_KINDS = ["image", "video", "svg", "text", "slot"];
|
|
12
16
|
var EFFECT_LAYER_ROLES = ["background", "subject", "overlay"];
|
|
13
17
|
var EFFECT_TRANSITION_MS = 800;
|
|
@@ -87,17 +91,18 @@ function resolveEffectValues(description, values) {
|
|
|
87
91
|
if (errors.length) throw new EffectControlError(description.id, errors);
|
|
88
92
|
return { ...description.defaults, ..._nullishCoalesce(values, () => ( {})) };
|
|
89
93
|
}
|
|
94
|
+
var PIXELLESS_KINDS = ["motion", "camera", "timing"];
|
|
90
95
|
function effectOffered(e, uses) {
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
if (
|
|
96
|
+
const pixelless = PIXELLESS_KINDS.includes(e.kind);
|
|
97
|
+
if (!uses) return !pixelless;
|
|
98
|
+
if (pixelless && !uses.includes(e.kind)) return false;
|
|
94
99
|
return e.uses.some((u) => uses.includes(u));
|
|
95
100
|
}
|
|
96
101
|
function normaliseEffectTag(tag) {
|
|
97
102
|
return tag.trim().toLowerCase().replace(/[\s_-]+/g, "-");
|
|
98
103
|
}
|
|
99
104
|
function hasEffectTags(e, tags) {
|
|
100
|
-
if (!_optionalChain([tags, 'optionalAccess',
|
|
105
|
+
if (!_optionalChain([tags, 'optionalAccess', _2 => _2.length])) return true;
|
|
101
106
|
const own = new Set((_nullishCoalesce(e.tags, () => ( []))).map(normaliseEffectTag));
|
|
102
107
|
return tags.every((t) => own.has(normaliseEffectTag(t)));
|
|
103
108
|
}
|
|
@@ -106,10 +111,10 @@ function countEffectTags(effects) {
|
|
|
106
111
|
for (const e of effects) for (const tag of new Set((_nullishCoalesce(e.tags, () => ( []))).map(normaliseEffectTag))) if (tag) counts.set(tag, (_nullishCoalesce(counts.get(tag), () => ( 0))) + 1);
|
|
107
112
|
return [...counts].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
|
|
108
113
|
}
|
|
109
|
-
var matchesFilters = (e, o) => (!o.kind || e.kind === o.kind) && (!o.category || e.category === o.category) && (!o.use || e.uses.includes(o.use)) && hasEffectTags(e, o.tags) && (!_optionalChain([o, 'access',
|
|
114
|
+
var matchesFilters = (e, o) => (!o.kind || e.kind === o.kind) && (!o.category || e.category === o.category) && (!o.use || e.uses.includes(o.use)) && hasEffectTags(e, o.tags) && (!_optionalChain([o, 'access', _3 => _3.accepts, 'optionalAccess', _4 => _4.length]) || o.accepts.every((a) => _optionalChain([e, 'access', _5 => _5.accepts, 'optionalAccess', _6 => _6.includes, 'call', _7 => _7(a)])));
|
|
110
115
|
function pathControlsOf(d) {
|
|
111
116
|
const isPath = (s) => !!s && (s.format === EFFECT_PATH_FORMAT || [..._nullishCoalesce(s.anyOf, () => ( [])), ..._nullishCoalesce(s.oneOf, () => ( []))].some((x) => isPath(x)));
|
|
112
|
-
return Object.entries(_nullishCoalesce(_optionalChain([d, 'access',
|
|
117
|
+
return Object.entries(_nullishCoalesce(_optionalChain([d, 'access', _8 => _8.controls, 'optionalAccess', _9 => _9.properties]), () => ( {}))).filter(([, s]) => isPath(s)).map(([name]) => name);
|
|
113
118
|
}
|
|
114
119
|
function searchEffectSummaries(catalog, query = "", options = {}) {
|
|
115
120
|
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
@@ -170,7 +175,7 @@ function applyEffectMask(result, mask, source) {
|
|
|
170
175
|
}
|
|
171
176
|
function layerImageInput(d) {
|
|
172
177
|
const images = d.inputs.filter((i) => i.kind === "image");
|
|
173
|
-
return _nullishCoalesce(_optionalChain([(_nullishCoalesce(_nullishCoalesce(images.find((i) => i.name === "source"), () => ( images.find((i) => i.required))), () => ( images[0]))), 'optionalAccess',
|
|
178
|
+
return _nullishCoalesce(_optionalChain([(_nullishCoalesce(_nullishCoalesce(images.find((i) => i.name === "source"), () => ( images.find((i) => i.required))), () => ( images[0]))), 'optionalAccess', _10 => _10.name]), () => ( null));
|
|
174
179
|
}
|
|
175
180
|
var EffectsSlot = (_class = class {constructor() { _class.prototype.__init.call(this);_class.prototype.__init2.call(this); }
|
|
176
181
|
__init() {this.current = null}
|
|
@@ -238,7 +243,7 @@ async function sampleEffectCameraMove(provider, id, request) {
|
|
|
238
243
|
}
|
|
239
244
|
var cameraMoveField = (role) => role === "transition" ? "in" : "during";
|
|
240
245
|
function cameraMovesFor(catalog, role) {
|
|
241
|
-
return catalog.filter((e) => e.kind === "camera" && _optionalChain([e, 'access',
|
|
246
|
+
return catalog.filter((e) => e.kind === "camera" && _optionalChain([e, 'access', _11 => _11.camera, 'optionalAccess', _12 => _12.role]) === role);
|
|
242
247
|
}
|
|
243
248
|
function cameraMoveIdList(catalog, role) {
|
|
244
249
|
const ids = cameraMovesFor(catalog, role).map((e) => JSON.stringify(e.id));
|
|
@@ -299,6 +304,117 @@ function cameraBoxOfView(view, stage) {
|
|
|
299
304
|
const height = stage.height / view.zoom;
|
|
300
305
|
return { x: -view.scrollX, y: -view.scrollY, width, height };
|
|
301
306
|
}
|
|
307
|
+
function checkTimingFootage(footageMs, id) {
|
|
308
|
+
if (typeof footageMs !== "number" || !Number.isFinite(footageMs) || footageMs <= 0) {
|
|
309
|
+
throw new EffectUnsupportedError(`The footage's length is a number of ms above 0; it was ${String(footageMs)}.`, id);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function checkTimingRequest(request, id) {
|
|
313
|
+
if (!request || typeof request !== "object") throw new EffectUnsupportedError("A timing request is { time, footage, fps? }: ms into the retimed clip, the footage's length in ms, and its frame rate.", id);
|
|
314
|
+
checkTimingFootage(request.footage, id);
|
|
315
|
+
const fps = _nullishCoalesce(request.fps, () => ( TIMING_DEFAULT_FPS));
|
|
316
|
+
if (typeof fps !== "number" || !Number.isFinite(fps) || fps <= 0 || fps > TIMING_MAX_FPS) {
|
|
317
|
+
throw new EffectUnsupportedError(`The footage's frame rate is above 0 and at most ${TIMING_MAX_FPS} frames per second; it was ${String(fps)}.`, id);
|
|
318
|
+
}
|
|
319
|
+
if (typeof request.time !== "number" || !Number.isFinite(request.time)) throw new EffectUnsupportedError(`The time is a number of ms into the retimed clip; it was ${String(request.time)}.`, id);
|
|
320
|
+
return fps;
|
|
321
|
+
}
|
|
322
|
+
var lastFrameOf = (footageMs, fps) => Math.max(0, Math.ceil(footageMs * fps / 1e3 - 1e-9) - 1);
|
|
323
|
+
function normaliseTimingSample(raw, request, blend = "average") {
|
|
324
|
+
const fps = _nullishCoalesce(request.fps, () => ( TIMING_DEFAULT_FPS));
|
|
325
|
+
const frameMs = 1e3 / fps;
|
|
326
|
+
const last = lastFrameOf(request.footage, fps);
|
|
327
|
+
const frameOf = (t) => Math.min(last, Math.max(0, Math.floor(t / frameMs + 1e-6)));
|
|
328
|
+
const weights = /* @__PURE__ */ new Map();
|
|
329
|
+
for (const f of raw) {
|
|
330
|
+
if (!Number.isFinite(f.time) || !Number.isFinite(f.weight) || f.weight <= 0) continue;
|
|
331
|
+
const i = frameOf(f.time);
|
|
332
|
+
weights.set(i, (_nullishCoalesce(weights.get(i), () => ( 0))) + f.weight);
|
|
333
|
+
}
|
|
334
|
+
if (!weights.size) weights.set(Number.isFinite(request.time) ? frameOf(request.time) : 0, 1);
|
|
335
|
+
let frames = [...weights.entries()].sort((a, b) => a[0] - b[0]);
|
|
336
|
+
if (frames.length > TIMING_MAX_FRAMES) {
|
|
337
|
+
const stride = frames.length / TIMING_MAX_FRAMES;
|
|
338
|
+
frames = Array.from({ length: TIMING_MAX_FRAMES }, (_, k) => frames[Math.floor(k * stride)]);
|
|
339
|
+
}
|
|
340
|
+
const total = frames.reduce((sum, [, w]) => sum + w, 0);
|
|
341
|
+
return { frames: frames.map(([i, w]) => ({ time: i * 1e3 / fps, weight: w / total })), blend };
|
|
342
|
+
}
|
|
343
|
+
function timingSampleProblems(sample, request) {
|
|
344
|
+
if (!sample || typeof sample !== "object") return ["the sample is not an object { frames, blend }"];
|
|
345
|
+
const { frames, blend } = sample;
|
|
346
|
+
const problems = [];
|
|
347
|
+
if (!TIMING_BLENDS.includes(blend)) problems.push(`blend "${String(blend)}" is not one of ${TIMING_BLENDS.join(", ")}`);
|
|
348
|
+
if (!Array.isArray(frames) || !frames.length) return [...problems, "frames must be a list of at least one { time, weight }"];
|
|
349
|
+
if (frames.length > TIMING_MAX_FRAMES) problems.push(`it has ${frames.length} frames; at most ${TIMING_MAX_FRAMES} are allowed`);
|
|
350
|
+
const fps = _nullishCoalesce(request.fps, () => ( TIMING_DEFAULT_FPS));
|
|
351
|
+
const last = lastFrameOf(request.footage, fps);
|
|
352
|
+
let total = 0;
|
|
353
|
+
let previous = -Infinity;
|
|
354
|
+
for (const f of frames) {
|
|
355
|
+
if (!f || !Number.isFinite(f.time) || !Number.isFinite(f.weight)) {
|
|
356
|
+
problems.push(`frame ${JSON.stringify(f)} needs a finite time and weight`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const index = f.time * fps / 1e3;
|
|
360
|
+
if (Math.abs(index - Math.round(index)) > 1e-6) problems.push(`frame time ${f.time} is not the start of a frame at ${fps} fps (index \xD7 ${+(1e3 / fps).toFixed(6)} ms)`);
|
|
361
|
+
else if (Math.round(index) < 0 || Math.round(index) > last) problems.push(`frame time ${f.time} is outside the footage (frames 0 to ${last}, the last at ${+(last * 1e3 / fps).toFixed(6)} ms)`);
|
|
362
|
+
if (f.time <= previous) problems.push(`frame times must be distinct and earliest first; ${f.time} follows ${previous}`);
|
|
363
|
+
previous = f.time;
|
|
364
|
+
if (!(f.weight > 0)) problems.push(`frame at ${f.time} has weight ${f.weight}; weights are > 0`);
|
|
365
|
+
total += f.weight;
|
|
366
|
+
}
|
|
367
|
+
if (Math.abs(total - 1) > 1e-6) problems.push(`weights add up to ${total}, not 1`);
|
|
368
|
+
return problems;
|
|
369
|
+
}
|
|
370
|
+
function blendTimingFrames(frames, blend, out) {
|
|
371
|
+
if (!frames.length) throw new EffectUnsupportedError("There are no frames to blend; a timing sample has at least one.");
|
|
372
|
+
const { width, height } = frames[0].pixels;
|
|
373
|
+
for (const f of frames) {
|
|
374
|
+
if (f.pixels.width !== width || f.pixels.height !== height) throw new EffectUnsupportedError(`A ${f.pixels.width}\xD7${f.pixels.height} frame cannot be blended with ${width}\xD7${height} frames.`);
|
|
375
|
+
}
|
|
376
|
+
const target = _nullishCoalesce(out, () => ( createEffectPixels(width, height)));
|
|
377
|
+
if (target.width !== width || target.height !== height) throw new EffectUnsupportedError(`The output is ${target.width}\xD7${target.height}; the frames are ${width}\xD7${height}.`);
|
|
378
|
+
const o = target.data;
|
|
379
|
+
if (frames.length === 1) {
|
|
380
|
+
o.set(frames[0].pixels.data);
|
|
381
|
+
return target;
|
|
382
|
+
}
|
|
383
|
+
const s = new Float64Array(o.length);
|
|
384
|
+
if (blend === "lighten") {
|
|
385
|
+
const heaviest = Math.max(...frames.map((f) => f.weight));
|
|
386
|
+
for (const f of frames) {
|
|
387
|
+
const k = heaviest > 0 ? f.weight / heaviest : 1;
|
|
388
|
+
const p = f.pixels.data;
|
|
389
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
390
|
+
const a = p[i + 3] / 255 * k;
|
|
391
|
+
for (let c = 0; c < 3; c++) if (p[i + c] * a > s[i + c]) s[i + c] = p[i + c] * a;
|
|
392
|
+
if (a > s[i + 3]) s[i + 3] = a;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
396
|
+
for (let c = 0; c < 3; c++) o[i + c] = Math.round(s[i + c]);
|
|
397
|
+
o[i + 3] = Math.round(s[i + 3] * 255);
|
|
398
|
+
}
|
|
399
|
+
return target;
|
|
400
|
+
}
|
|
401
|
+
let total = 0;
|
|
402
|
+
for (const f of frames) {
|
|
403
|
+
total += f.weight;
|
|
404
|
+
const p = f.pixels.data;
|
|
405
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
406
|
+
const a = p[i + 3] * f.weight;
|
|
407
|
+
for (let c = 0; c < 3; c++) s[i + c] += p[i + c] * a;
|
|
408
|
+
s[i + 3] += a;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
412
|
+
const a = s[i + 3];
|
|
413
|
+
for (let c = 0; c < 3; c++) o[i + c] = a > 0 ? Math.round(s[i + c] / a) : 0;
|
|
414
|
+
o[i + 3] = total > 0 ? Math.round(a / total) : 0;
|
|
415
|
+
}
|
|
416
|
+
return target;
|
|
417
|
+
}
|
|
302
418
|
|
|
303
419
|
// src/effects/tools.ts
|
|
304
420
|
var ANNOTATIONS = {
|
|
@@ -328,12 +444,12 @@ var summaryOut = (e) => ({
|
|
|
328
444
|
category: e.category,
|
|
329
445
|
uses: e.uses,
|
|
330
446
|
...e.description ? { description: e.description } : {},
|
|
331
|
-
..._optionalChain([e, 'access',
|
|
447
|
+
..._optionalChain([e, 'access', _13 => _13.tags, 'optionalAccess', _14 => _14.length]) ? { tags: e.tags } : {},
|
|
332
448
|
transparent: e.transparent,
|
|
333
449
|
loop: e.loop,
|
|
334
450
|
...e.duration !== void 0 ? { duration: e.duration } : {},
|
|
335
451
|
...e.camera ? { camera: e.camera } : {},
|
|
336
|
-
..._optionalChain([e, 'access',
|
|
452
|
+
..._optionalChain([e, 'access', _15 => _15.accepts, 'optionalAccess', _16 => _16.length]) ? { accepts: e.accepts } : {}
|
|
337
453
|
});
|
|
338
454
|
function createEffectToolProvider(host) {
|
|
339
455
|
let catalog = null;
|
|
@@ -341,7 +457,7 @@ function createEffectToolProvider(host) {
|
|
|
341
457
|
const notify = () => listeners.forEach((l) => l());
|
|
342
458
|
const catalogFor = (provider) => {
|
|
343
459
|
if (!provider) return null;
|
|
344
|
-
if (_optionalChain([catalog, 'optionalAccess',
|
|
460
|
+
if (_optionalChain([catalog, 'optionalAccess', _17 => _17.provider]) !== provider) {
|
|
345
461
|
const entry = { provider, effects: null };
|
|
346
462
|
catalog = entry;
|
|
347
463
|
Promise.resolve().then(() => provider.list()).then((effects) => {
|
|
@@ -378,7 +494,7 @@ function createEffectToolProvider(host) {
|
|
|
378
494
|
});
|
|
379
495
|
const effectIdSchema = (uses) => {
|
|
380
496
|
const open = host.applyImageInputs && uses === host.applyUses;
|
|
381
|
-
const effects = open ? void 0 : _optionalChain([catalogFor, 'call',
|
|
497
|
+
const effects = open ? void 0 : _optionalChain([catalogFor, 'call', _18 => _18(host.provider()), 'optionalAccess', _19 => _19.filter, 'call', _20 => _20((e) => effectOffered(e, uses))]);
|
|
382
498
|
const base = `An effect id from search_effects (use ${uses.map((u) => `"${u}"`).join(" or ")}${open ? `, or any effect whose describe_effect inputs take an image: the ${host.target.noun} is its picture` : ""}).`;
|
|
383
499
|
return effects && effects.length && effects.length <= ENUM_LIMIT ? { type: "string", enum: effects.map((e) => e.id), description: `${base} One of: ${effects.map((e) => `${e.id} (${e.name})`).join(", ")}.` } : { type: "string", minLength: 1, description: base };
|
|
384
500
|
};
|
|
@@ -392,12 +508,12 @@ function createEffectToolProvider(host) {
|
|
|
392
508
|
{
|
|
393
509
|
name: "search_effects",
|
|
394
510
|
title: "Search effects",
|
|
395
|
-
description: `Searches the effects provider's catalog. Every word of query must appear in an effect's id, name, category, tags or description; filter by kind (shader, vector, motion, camera), category, use (${EFFECT_USES.join(", ")}) or tags (effects carrying every one), and accepts ["path"] for effects that draw along a path you give. For this editor, apply_effect takes uses ${host.applyUses.join(" or ")}. Returns { provider, effects: [{ id, name, kind, category, uses, description, tags, transparent, loop, duration (ms), camera: { role, durationMs, cut } for camera moves, accepts }], total, tags: [{ tag, count }] }: tags lists the tags of every match, the most used first, so a broad search (or none) shows the tags to narrow by. A tag the catalog does not have comes back in unknownTags with the nearest ones. With no provider registered it returns available: false and the reason. Call describe_effect next for an effect's controls.`,
|
|
511
|
+
description: `Searches the effects provider's catalog. Every word of query must appear in an effect's id, name, category, tags or description; filter by kind (shader, vector, motion, camera, timing), category, use (${EFFECT_USES.join(", ")}) or tags (effects carrying every one), and accepts ["path"] for effects that draw along a path you give. For this editor, apply_effect takes uses ${host.applyUses.join(" or ")}. Returns { provider, effects: [{ id, name, kind, category, uses, description, tags, transparent, loop, duration (ms), camera: { role, durationMs, cut } for camera moves, accepts }] (timing effects, kind timing, retime video footage and have no duration: their length depends on the footage), total, tags: [{ tag, count }] }: tags lists the tags of every match, the most used first, so a broad search (or none) shows the tags to narrow by. A tag the catalog does not have comes back in unknownTags with the nearest ones. With no provider registered it returns available: false and the reason. Call describe_effect next for an effect's controls.`,
|
|
396
512
|
inputSchema: object({
|
|
397
513
|
query: { type: "string", description: "Words to match (default: every effect)." },
|
|
398
|
-
kind: { type: "string", enum: [...EFFECT_KINDS], description: "shader (pixels), vector (animated SVG), motion (animates vector art)
|
|
514
|
+
kind: { type: "string", enum: [...EFFECT_KINDS], description: "shader (pixels), vector (animated SVG), motion (animates vector art), camera (moves a scene's camera; no pixels) or timing (retimes video footage: slow motion, reverse, freeze, long exposure; no pixels of its own)." },
|
|
399
515
|
category: { type: "string", minLength: 1, description: `The provider's category, e.g. "gradient", "light", "transition".` },
|
|
400
|
-
use: { type: "string", enum: [...EFFECT_USES], description: "What the effect is for: filter changes a picture, transition blends two, generator draws a background, overlay draws over content, title draws a title, motion animates vector art, interactive reacts to input, camera moves a scene's camera." },
|
|
516
|
+
use: { type: "string", enum: [...EFFECT_USES], description: "What the effect is for: filter changes a picture, transition blends two, generator draws a background, overlay draws over content, title draws a title, motion animates vector art, interactive reacts to input, camera moves a scene's camera, timing retimes video footage." },
|
|
401
517
|
tags: { type: "array", items: { type: "string", minLength: 1 }, description: "Only effects carrying every one of these tags, as search_effects lists them (any case; spaces count as hyphens)." },
|
|
402
518
|
limit: { type: "integer", minimum: 1, maximum: 200, description: "Most effects to return (default 20)." },
|
|
403
519
|
accepts: { type: "array", items: { type: "string", enum: [...EFFECT_ACCEPTS], description: "path: the effect has a control that draws along a path given at run time." }, description: "Only effects that take every one of these (path: effects that draw along a path you give; describe_effect names the controls in pathControls)." }
|
|
@@ -408,8 +524,8 @@ function createEffectToolProvider(host) {
|
|
|
408
524
|
if (!p) return { available: false, reason: NO_PROVIDER, effects: [], total: 0 };
|
|
409
525
|
const tags = args.tags;
|
|
410
526
|
const accepts = args.accepts;
|
|
411
|
-
let found = (await explain(async () => p.search(_nullishCoalesce(args.query, () => ( "")), { kind: args.kind, category: args.category, use: args.use, tags, ..._optionalChain([accepts, 'optionalAccess',
|
|
412
|
-
if (_optionalChain([accepts, 'optionalAccess',
|
|
527
|
+
let found = (await explain(async () => p.search(_nullishCoalesce(args.query, () => ( "")), { kind: args.kind, category: args.category, use: args.use, tags, ..._optionalChain([accepts, 'optionalAccess', _21 => _21.length]) ? { accepts } : {} }))).filter((e) => hasEffectTags(e, tags));
|
|
528
|
+
if (_optionalChain([accepts, 'optionalAccess', _22 => _22.includes, 'call', _23 => _23("path")])) {
|
|
413
529
|
const kept = [];
|
|
414
530
|
for (const e of found) {
|
|
415
531
|
if (e.accepts) {
|
|
@@ -420,7 +536,7 @@ function createEffectToolProvider(host) {
|
|
|
420
536
|
}
|
|
421
537
|
const limit = _nullishCoalesce(args.limit, () => ( 20));
|
|
422
538
|
const out = { available: true, provider: { id: p.id, name: p.name, capabilities: p.capabilities }, effects: found.slice(0, limit).map(summaryOut), total: found.length, tags: countEffectTags(found).slice(0, TAG_LIMIT) };
|
|
423
|
-
if (_optionalChain([tags, 'optionalAccess',
|
|
539
|
+
if (_optionalChain([tags, 'optionalAccess', _24 => _24.length]) && !found.length) {
|
|
424
540
|
const known = countEffectTags(await explain(async () => p.list()));
|
|
425
541
|
const have = new Set(known.map((k) => k.tag));
|
|
426
542
|
const unknown = tags.filter((t) => !have.has(normaliseEffectTag(t)));
|
|
@@ -536,16 +652,16 @@ function createEffectToolProvider(host) {
|
|
|
536
652
|
...host.description ? { description: host.description } : {},
|
|
537
653
|
...host.skill ? { skill: host.skill } : {},
|
|
538
654
|
tools: () => {
|
|
539
|
-
const extra = _nullishCoalesce(_optionalChain([host, 'access',
|
|
540
|
-
if (host.readOnlyReason()) return [...readTools(), ...extra.filter((t) => _optionalChain([t, 'access',
|
|
655
|
+
const extra = _nullishCoalesce(_optionalChain([host, 'access', _25 => _25.extraTools, 'optionalCall', _26 => _26(context)]), () => ( []));
|
|
656
|
+
if (host.readOnlyReason()) return [...readTools(), ...extra.filter((t) => _optionalChain([t, 'access', _27 => _27.annotations, 'optionalAccess', _28 => _28.readOnlyHint]))];
|
|
541
657
|
return [...readTools(), ...writeTools(), ...extra];
|
|
542
658
|
},
|
|
543
659
|
subscribe: (l) => {
|
|
544
660
|
listeners.add(l);
|
|
545
|
-
const off = _optionalChain([host, 'access',
|
|
661
|
+
const off = _optionalChain([host, 'access', _29 => _29.subscribe, 'optionalCall', _30 => _30(l)]);
|
|
546
662
|
return () => {
|
|
547
663
|
listeners.delete(l);
|
|
548
|
-
_optionalChain([off, 'optionalCall',
|
|
664
|
+
_optionalChain([off, 'optionalCall', _31 => _31()]);
|
|
549
665
|
};
|
|
550
666
|
}
|
|
551
667
|
};
|
|
@@ -598,4 +714,13 @@ function createEffectToolProvider(host) {
|
|
|
598
714
|
|
|
599
715
|
|
|
600
716
|
|
|
601
|
-
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
exports.EFFECT_KINDS = EFFECT_KINDS; exports.EFFECT_USES = EFFECT_USES; exports.CAMERA_MOVE_ROLES = CAMERA_MOVE_ROLES; exports.EFFECT_ACCEPTS = EFFECT_ACCEPTS; exports.EFFECT_PATH_FORMAT = EFFECT_PATH_FORMAT; exports.TIMING_DEFAULT_FPS = TIMING_DEFAULT_FPS; exports.TIMING_MAX_FRAMES = TIMING_MAX_FRAMES; exports.TIMING_MAX_FPS = TIMING_MAX_FPS; exports.TIMING_BLENDS = TIMING_BLENDS; exports.EFFECT_SUBJECT_KINDS = EFFECT_SUBJECT_KINDS; exports.EFFECT_LAYER_ROLES = EFFECT_LAYER_ROLES; exports.EFFECT_TRANSITION_MS = EFFECT_TRANSITION_MS; exports.EFFECT_TEMPLATE_SLOT_TYPES = EFFECT_TEMPLATE_SLOT_TYPES; exports.EFFECT_ERROR_CODES = EFFECT_ERROR_CODES; exports.EffectError = EffectError; exports.UnknownEffectError = UnknownEffectError; exports.UnknownTemplateError = UnknownTemplateError; exports.EffectControlError = EffectControlError; exports.EffectUnsupportedError = EffectUnsupportedError; exports.EffectDisposedError = EffectDisposedError; exports.NoEffectsProviderError = NoEffectsProviderError; exports.isEffectError = isEffectError; exports.effectValueErrors = effectValueErrors; exports.resolveEffectValues = resolveEffectValues; exports.effectOffered = effectOffered; exports.normaliseEffectTag = normaliseEffectTag; exports.hasEffectTags = hasEffectTags; exports.countEffectTags = countEffectTags; exports.pathControlsOf = pathControlsOf; exports.searchEffectSummaries = searchEffectSummaries; exports.createEffectPixels = createEffectPixels; exports.isEffectPixels = isEffectPixels; exports.srgbToLinear = srgbToLinear; exports.linearToSrgb = linearToSrgb; exports.encodeEffectFloat = encodeEffectFloat; exports.applyEffectMask = applyEffectMask; exports.layerImageInput = layerImageInput; exports.EffectsSlot = EffectsSlot; exports.isCameraView = isCameraView; exports.sampleCameraViews = sampleCameraViews; exports.sampleEffectCameraMove = sampleEffectCameraMove; exports.cameraMoveField = cameraMoveField; exports.cameraMovesFor = cameraMovesFor; exports.cameraMoveIdList = cameraMoveIdList; exports.cameraMoveParts = cameraMoveParts; exports.CAMERA_MOVE_MAX_MS = CAMERA_MOVE_MAX_MS; exports.resolveCameraMoveSpec = resolveCameraMoveSpec; exports.cameraViewOfBox = cameraViewOfBox; exports.cameraBoxOfView = cameraBoxOfView; exports.checkTimingFootage = checkTimingFootage; exports.checkTimingRequest = checkTimingRequest; exports.normaliseTimingSample = normaliseTimingSample; exports.timingSampleProblems = timingSampleProblems; exports.blendTimingFrames = blendTimingFrames; exports.createEffectToolProvider = createEffectToolProvider;
|
|
@@ -3,11 +3,15 @@ import {
|
|
|
3
3
|
} from "./chunk-V2ZCJRDM.js";
|
|
4
4
|
|
|
5
5
|
// src/effects/types.ts
|
|
6
|
-
var EFFECT_KINDS = ["shader", "vector", "motion", "camera"];
|
|
7
|
-
var EFFECT_USES = ["filter", "transition", "generator", "overlay", "title", "motion", "interactive", "camera"];
|
|
6
|
+
var EFFECT_KINDS = ["shader", "vector", "motion", "camera", "timing"];
|
|
7
|
+
var EFFECT_USES = ["filter", "transition", "generator", "overlay", "title", "motion", "interactive", "camera", "timing"];
|
|
8
8
|
var CAMERA_MOVE_ROLES = ["transition", "hold"];
|
|
9
9
|
var EFFECT_ACCEPTS = ["path"];
|
|
10
10
|
var EFFECT_PATH_FORMAT = "svg-path";
|
|
11
|
+
var TIMING_DEFAULT_FPS = 30;
|
|
12
|
+
var TIMING_MAX_FRAMES = 128;
|
|
13
|
+
var TIMING_MAX_FPS = 240;
|
|
14
|
+
var TIMING_BLENDS = ["average", "lighten"];
|
|
11
15
|
var EFFECT_SUBJECT_KINDS = ["image", "video", "svg", "text", "slot"];
|
|
12
16
|
var EFFECT_LAYER_ROLES = ["background", "subject", "overlay"];
|
|
13
17
|
var EFFECT_TRANSITION_MS = 800;
|
|
@@ -87,10 +91,11 @@ function resolveEffectValues(description, values) {
|
|
|
87
91
|
if (errors.length) throw new EffectControlError(description.id, errors);
|
|
88
92
|
return { ...description.defaults, ...values ?? {} };
|
|
89
93
|
}
|
|
94
|
+
var PIXELLESS_KINDS = ["motion", "camera", "timing"];
|
|
90
95
|
function effectOffered(e, uses) {
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
if (
|
|
96
|
+
const pixelless = PIXELLESS_KINDS.includes(e.kind);
|
|
97
|
+
if (!uses) return !pixelless;
|
|
98
|
+
if (pixelless && !uses.includes(e.kind)) return false;
|
|
94
99
|
return e.uses.some((u) => uses.includes(u));
|
|
95
100
|
}
|
|
96
101
|
function normaliseEffectTag(tag) {
|
|
@@ -299,6 +304,117 @@ function cameraBoxOfView(view, stage) {
|
|
|
299
304
|
const height = stage.height / view.zoom;
|
|
300
305
|
return { x: -view.scrollX, y: -view.scrollY, width, height };
|
|
301
306
|
}
|
|
307
|
+
function checkTimingFootage(footageMs, id) {
|
|
308
|
+
if (typeof footageMs !== "number" || !Number.isFinite(footageMs) || footageMs <= 0) {
|
|
309
|
+
throw new EffectUnsupportedError(`The footage's length is a number of ms above 0; it was ${String(footageMs)}.`, id);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function checkTimingRequest(request, id) {
|
|
313
|
+
if (!request || typeof request !== "object") throw new EffectUnsupportedError("A timing request is { time, footage, fps? }: ms into the retimed clip, the footage's length in ms, and its frame rate.", id);
|
|
314
|
+
checkTimingFootage(request.footage, id);
|
|
315
|
+
const fps = request.fps ?? TIMING_DEFAULT_FPS;
|
|
316
|
+
if (typeof fps !== "number" || !Number.isFinite(fps) || fps <= 0 || fps > TIMING_MAX_FPS) {
|
|
317
|
+
throw new EffectUnsupportedError(`The footage's frame rate is above 0 and at most ${TIMING_MAX_FPS} frames per second; it was ${String(fps)}.`, id);
|
|
318
|
+
}
|
|
319
|
+
if (typeof request.time !== "number" || !Number.isFinite(request.time)) throw new EffectUnsupportedError(`The time is a number of ms into the retimed clip; it was ${String(request.time)}.`, id);
|
|
320
|
+
return fps;
|
|
321
|
+
}
|
|
322
|
+
var lastFrameOf = (footageMs, fps) => Math.max(0, Math.ceil(footageMs * fps / 1e3 - 1e-9) - 1);
|
|
323
|
+
function normaliseTimingSample(raw, request, blend = "average") {
|
|
324
|
+
const fps = request.fps ?? TIMING_DEFAULT_FPS;
|
|
325
|
+
const frameMs = 1e3 / fps;
|
|
326
|
+
const last = lastFrameOf(request.footage, fps);
|
|
327
|
+
const frameOf = (t) => Math.min(last, Math.max(0, Math.floor(t / frameMs + 1e-6)));
|
|
328
|
+
const weights = /* @__PURE__ */ new Map();
|
|
329
|
+
for (const f of raw) {
|
|
330
|
+
if (!Number.isFinite(f.time) || !Number.isFinite(f.weight) || f.weight <= 0) continue;
|
|
331
|
+
const i = frameOf(f.time);
|
|
332
|
+
weights.set(i, (weights.get(i) ?? 0) + f.weight);
|
|
333
|
+
}
|
|
334
|
+
if (!weights.size) weights.set(Number.isFinite(request.time) ? frameOf(request.time) : 0, 1);
|
|
335
|
+
let frames = [...weights.entries()].sort((a, b) => a[0] - b[0]);
|
|
336
|
+
if (frames.length > TIMING_MAX_FRAMES) {
|
|
337
|
+
const stride = frames.length / TIMING_MAX_FRAMES;
|
|
338
|
+
frames = Array.from({ length: TIMING_MAX_FRAMES }, (_, k) => frames[Math.floor(k * stride)]);
|
|
339
|
+
}
|
|
340
|
+
const total = frames.reduce((sum, [, w]) => sum + w, 0);
|
|
341
|
+
return { frames: frames.map(([i, w]) => ({ time: i * 1e3 / fps, weight: w / total })), blend };
|
|
342
|
+
}
|
|
343
|
+
function timingSampleProblems(sample, request) {
|
|
344
|
+
if (!sample || typeof sample !== "object") return ["the sample is not an object { frames, blend }"];
|
|
345
|
+
const { frames, blend } = sample;
|
|
346
|
+
const problems = [];
|
|
347
|
+
if (!TIMING_BLENDS.includes(blend)) problems.push(`blend "${String(blend)}" is not one of ${TIMING_BLENDS.join(", ")}`);
|
|
348
|
+
if (!Array.isArray(frames) || !frames.length) return [...problems, "frames must be a list of at least one { time, weight }"];
|
|
349
|
+
if (frames.length > TIMING_MAX_FRAMES) problems.push(`it has ${frames.length} frames; at most ${TIMING_MAX_FRAMES} are allowed`);
|
|
350
|
+
const fps = request.fps ?? TIMING_DEFAULT_FPS;
|
|
351
|
+
const last = lastFrameOf(request.footage, fps);
|
|
352
|
+
let total = 0;
|
|
353
|
+
let previous = -Infinity;
|
|
354
|
+
for (const f of frames) {
|
|
355
|
+
if (!f || !Number.isFinite(f.time) || !Number.isFinite(f.weight)) {
|
|
356
|
+
problems.push(`frame ${JSON.stringify(f)} needs a finite time and weight`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const index = f.time * fps / 1e3;
|
|
360
|
+
if (Math.abs(index - Math.round(index)) > 1e-6) problems.push(`frame time ${f.time} is not the start of a frame at ${fps} fps (index \xD7 ${+(1e3 / fps).toFixed(6)} ms)`);
|
|
361
|
+
else if (Math.round(index) < 0 || Math.round(index) > last) problems.push(`frame time ${f.time} is outside the footage (frames 0 to ${last}, the last at ${+(last * 1e3 / fps).toFixed(6)} ms)`);
|
|
362
|
+
if (f.time <= previous) problems.push(`frame times must be distinct and earliest first; ${f.time} follows ${previous}`);
|
|
363
|
+
previous = f.time;
|
|
364
|
+
if (!(f.weight > 0)) problems.push(`frame at ${f.time} has weight ${f.weight}; weights are > 0`);
|
|
365
|
+
total += f.weight;
|
|
366
|
+
}
|
|
367
|
+
if (Math.abs(total - 1) > 1e-6) problems.push(`weights add up to ${total}, not 1`);
|
|
368
|
+
return problems;
|
|
369
|
+
}
|
|
370
|
+
function blendTimingFrames(frames, blend, out) {
|
|
371
|
+
if (!frames.length) throw new EffectUnsupportedError("There are no frames to blend; a timing sample has at least one.");
|
|
372
|
+
const { width, height } = frames[0].pixels;
|
|
373
|
+
for (const f of frames) {
|
|
374
|
+
if (f.pixels.width !== width || f.pixels.height !== height) throw new EffectUnsupportedError(`A ${f.pixels.width}\xD7${f.pixels.height} frame cannot be blended with ${width}\xD7${height} frames.`);
|
|
375
|
+
}
|
|
376
|
+
const target = out ?? createEffectPixels(width, height);
|
|
377
|
+
if (target.width !== width || target.height !== height) throw new EffectUnsupportedError(`The output is ${target.width}\xD7${target.height}; the frames are ${width}\xD7${height}.`);
|
|
378
|
+
const o = target.data;
|
|
379
|
+
if (frames.length === 1) {
|
|
380
|
+
o.set(frames[0].pixels.data);
|
|
381
|
+
return target;
|
|
382
|
+
}
|
|
383
|
+
const s = new Float64Array(o.length);
|
|
384
|
+
if (blend === "lighten") {
|
|
385
|
+
const heaviest = Math.max(...frames.map((f) => f.weight));
|
|
386
|
+
for (const f of frames) {
|
|
387
|
+
const k = heaviest > 0 ? f.weight / heaviest : 1;
|
|
388
|
+
const p = f.pixels.data;
|
|
389
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
390
|
+
const a = p[i + 3] / 255 * k;
|
|
391
|
+
for (let c = 0; c < 3; c++) if (p[i + c] * a > s[i + c]) s[i + c] = p[i + c] * a;
|
|
392
|
+
if (a > s[i + 3]) s[i + 3] = a;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
396
|
+
for (let c = 0; c < 3; c++) o[i + c] = Math.round(s[i + c]);
|
|
397
|
+
o[i + 3] = Math.round(s[i + 3] * 255);
|
|
398
|
+
}
|
|
399
|
+
return target;
|
|
400
|
+
}
|
|
401
|
+
let total = 0;
|
|
402
|
+
for (const f of frames) {
|
|
403
|
+
total += f.weight;
|
|
404
|
+
const p = f.pixels.data;
|
|
405
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
406
|
+
const a = p[i + 3] * f.weight;
|
|
407
|
+
for (let c = 0; c < 3; c++) s[i + c] += p[i + c] * a;
|
|
408
|
+
s[i + 3] += a;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
412
|
+
const a = s[i + 3];
|
|
413
|
+
for (let c = 0; c < 3; c++) o[i + c] = a > 0 ? Math.round(s[i + c] / a) : 0;
|
|
414
|
+
o[i + 3] = total > 0 ? Math.round(a / total) : 0;
|
|
415
|
+
}
|
|
416
|
+
return target;
|
|
417
|
+
}
|
|
302
418
|
|
|
303
419
|
// src/effects/tools.ts
|
|
304
420
|
var ANNOTATIONS = {
|
|
@@ -392,12 +508,12 @@ function createEffectToolProvider(host) {
|
|
|
392
508
|
{
|
|
393
509
|
name: "search_effects",
|
|
394
510
|
title: "Search effects",
|
|
395
|
-
description: `Searches the effects provider's catalog. Every word of query must appear in an effect's id, name, category, tags or description; filter by kind (shader, vector, motion, camera), category, use (${EFFECT_USES.join(", ")}) or tags (effects carrying every one), and accepts ["path"] for effects that draw along a path you give. For this editor, apply_effect takes uses ${host.applyUses.join(" or ")}. Returns { provider, effects: [{ id, name, kind, category, uses, description, tags, transparent, loop, duration (ms), camera: { role, durationMs, cut } for camera moves, accepts }], total, tags: [{ tag, count }] }: tags lists the tags of every match, the most used first, so a broad search (or none) shows the tags to narrow by. A tag the catalog does not have comes back in unknownTags with the nearest ones. With no provider registered it returns available: false and the reason. Call describe_effect next for an effect's controls.`,
|
|
511
|
+
description: `Searches the effects provider's catalog. Every word of query must appear in an effect's id, name, category, tags or description; filter by kind (shader, vector, motion, camera, timing), category, use (${EFFECT_USES.join(", ")}) or tags (effects carrying every one), and accepts ["path"] for effects that draw along a path you give. For this editor, apply_effect takes uses ${host.applyUses.join(" or ")}. Returns { provider, effects: [{ id, name, kind, category, uses, description, tags, transparent, loop, duration (ms), camera: { role, durationMs, cut } for camera moves, accepts }] (timing effects, kind timing, retime video footage and have no duration: their length depends on the footage), total, tags: [{ tag, count }] }: tags lists the tags of every match, the most used first, so a broad search (or none) shows the tags to narrow by. A tag the catalog does not have comes back in unknownTags with the nearest ones. With no provider registered it returns available: false and the reason. Call describe_effect next for an effect's controls.`,
|
|
396
512
|
inputSchema: object({
|
|
397
513
|
query: { type: "string", description: "Words to match (default: every effect)." },
|
|
398
|
-
kind: { type: "string", enum: [...EFFECT_KINDS], description: "shader (pixels), vector (animated SVG), motion (animates vector art)
|
|
514
|
+
kind: { type: "string", enum: [...EFFECT_KINDS], description: "shader (pixels), vector (animated SVG), motion (animates vector art), camera (moves a scene's camera; no pixels) or timing (retimes video footage: slow motion, reverse, freeze, long exposure; no pixels of its own)." },
|
|
399
515
|
category: { type: "string", minLength: 1, description: `The provider's category, e.g. "gradient", "light", "transition".` },
|
|
400
|
-
use: { type: "string", enum: [...EFFECT_USES], description: "What the effect is for: filter changes a picture, transition blends two, generator draws a background, overlay draws over content, title draws a title, motion animates vector art, interactive reacts to input, camera moves a scene's camera." },
|
|
516
|
+
use: { type: "string", enum: [...EFFECT_USES], description: "What the effect is for: filter changes a picture, transition blends two, generator draws a background, overlay draws over content, title draws a title, motion animates vector art, interactive reacts to input, camera moves a scene's camera, timing retimes video footage." },
|
|
401
517
|
tags: { type: "array", items: { type: "string", minLength: 1 }, description: "Only effects carrying every one of these tags, as search_effects lists them (any case; spaces count as hyphens)." },
|
|
402
518
|
limit: { type: "integer", minimum: 1, maximum: 200, description: "Most effects to return (default 20)." },
|
|
403
519
|
accepts: { type: "array", items: { type: "string", enum: [...EFFECT_ACCEPTS], description: "path: the effect has a control that draws along a path given at run time." }, description: "Only effects that take every one of these (path: effects that draw along a path you give; describe_effect names the controls in pathControls)." }
|
|
@@ -557,6 +673,10 @@ export {
|
|
|
557
673
|
CAMERA_MOVE_ROLES,
|
|
558
674
|
EFFECT_ACCEPTS,
|
|
559
675
|
EFFECT_PATH_FORMAT,
|
|
676
|
+
TIMING_DEFAULT_FPS,
|
|
677
|
+
TIMING_MAX_FRAMES,
|
|
678
|
+
TIMING_MAX_FPS,
|
|
679
|
+
TIMING_BLENDS,
|
|
560
680
|
EFFECT_SUBJECT_KINDS,
|
|
561
681
|
EFFECT_LAYER_ROLES,
|
|
562
682
|
EFFECT_TRANSITION_MS,
|
|
@@ -597,5 +717,10 @@ export {
|
|
|
597
717
|
resolveCameraMoveSpec,
|
|
598
718
|
cameraViewOfBox,
|
|
599
719
|
cameraBoxOfView,
|
|
720
|
+
checkTimingFootage,
|
|
721
|
+
checkTimingRequest,
|
|
722
|
+
normaliseTimingSample,
|
|
723
|
+
timingSampleProblems,
|
|
724
|
+
blendTimingFrames,
|
|
600
725
|
createEffectToolProvider
|
|
601
726
|
};
|