@pixerate/editor 0.240.0 → 0.242.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/animation-editor/controller.d.cts +10 -0
  2. package/dist/animation-editor/controller.d.ts +10 -0
  3. package/dist/animation-editor/effects.d.cts +1 -1
  4. package/dist/animation-editor/effects.d.ts +1 -1
  5. package/dist/animation-editor/index.cjs +195 -146
  6. package/dist/animation-editor/index.js +63 -14
  7. package/dist/{brushstrokeRender-WB67GMUP.js → brushstrokeRender-MOYGEXOC.js} +2 -2
  8. package/dist/{brushstrokeRender-MPOM7BFW.cjs → brushstrokeRender-UKADOV5D.cjs} +5 -5
  9. package/dist/canvas/index.cjs +15 -15
  10. package/dist/canvas/index.js +2 -2
  11. package/dist/{chunk-NKTNLJUJ.cjs → chunk-5BR7LC24.cjs} +99 -11
  12. package/dist/{chunk-ZXNO3ZOX.cjs → chunk-7F4G74UQ.cjs} +35 -35
  13. package/dist/{chunk-A7PQNIBX.cjs → chunk-7V7QMFTI.cjs} +14 -14
  14. package/dist/{chunk-AD7GA3TL.cjs → chunk-AFLMXITL.cjs} +23 -23
  15. package/dist/{chunk-JAOSL334.cjs → chunk-GHCSC2WP.cjs} +38 -16
  16. package/dist/{chunk-PP6BXVRL.cjs → chunk-HGGJ36HR.cjs} +1 -1
  17. package/dist/{chunk-BBUE3UJL.cjs → chunk-HW72ABGR.cjs} +86 -86
  18. package/dist/{chunk-7YGBSFC6.js → chunk-ILM6GYAX.js} +1 -1
  19. package/dist/{chunk-JWCKJNDR.cjs → chunk-K5DS2JTA.cjs} +6 -6
  20. package/dist/{chunk-THXFU3JB.cjs → chunk-KCMV2534.cjs} +2 -2
  21. package/dist/{chunk-WDDUPWVG.js → chunk-L3JFOTBQ.js} +1 -1
  22. package/dist/{chunk-3E36VDMJ.js → chunk-NE6OOKWJ.js} +1 -1
  23. package/dist/{chunk-SIK3LT6M.js → chunk-OFH6Q67U.js} +28 -6
  24. package/dist/{chunk-RV5DKZHU.js → chunk-QP2B3MID.js} +1 -1
  25. package/dist/{chunk-G3RUKJ5C.js → chunk-RXZL4NG5.js} +1 -1
  26. package/dist/{chunk-UUBO6RB5.js → chunk-UDBDYZET.js} +1 -1
  27. package/dist/{chunk-TEFQISBY.js → chunk-X3ZINLIP.js} +1 -1
  28. package/dist/{chunk-PJYG6FCF.js → chunk-XBZK47LV.js} +98 -10
  29. package/dist/{chunk-ZQIWBJGH.cjs → chunk-XNZFVYC2.cjs} +7 -7
  30. package/dist/{chunk-XK4SST43.js → chunk-ZDPLZDTT.js} +1 -1
  31. package/dist/effects/fake.d.cts +4 -0
  32. package/dist/effects/fake.d.ts +4 -0
  33. package/dist/effects/helpers.d.cts +6 -0
  34. package/dist/effects/helpers.d.ts +6 -0
  35. package/dist/effects/index.cjs +222 -94
  36. package/dist/effects/index.js +131 -3
  37. package/dist/effects/types.d.cts +31 -0
  38. package/dist/effects/types.d.ts +31 -0
  39. package/dist/image-editor/index.cjs +788 -788
  40. package/dist/image-editor/index.js +5 -5
  41. package/dist/paint-editor/controller.d.cts +67 -0
  42. package/dist/paint-editor/controller.d.ts +67 -0
  43. package/dist/paint-editor/effect-paint.d.cts +60 -3
  44. package/dist/paint-editor/effect-paint.d.ts +60 -3
  45. package/dist/paint-editor/index.cjs +682 -436
  46. package/dist/paint-editor/index.js +284 -38
  47. package/dist/paint-editor/stickers.d.cts +6 -0
  48. package/dist/paint-editor/stickers.d.ts +6 -0
  49. package/dist/paint-editor/thick.d.cts +4 -0
  50. package/dist/paint-editor/thick.d.ts +4 -0
  51. package/dist/paint-editor/wet.d.cts +8 -0
  52. package/dist/paint-editor/wet.d.ts +8 -0
  53. package/dist/vector/index.cjs +968 -968
  54. package/dist/vector/index.js +5 -5
  55. package/dist/video-editor/controller.d.cts +49 -1
  56. package/dist/video-editor/controller.d.ts +49 -1
  57. package/dist/video-editor/effect-path.d.cts +66 -0
  58. package/dist/video-editor/effect-path.d.ts +66 -0
  59. package/dist/video-editor/index.cjs +616 -427
  60. package/dist/video-editor/index.d.cts +1 -0
  61. package/dist/video-editor/index.d.ts +1 -0
  62. package/dist/video-editor/index.js +193 -4
  63. package/package.json +1 -1
  64. package/skills/pixerate-animation-editor/SKILL.md +1 -1
  65. package/skills/pixerate-paint-editor/SKILL.md +17 -21
  66. package/skills/pixerate-paint-editor/references/effect-paint.md +25 -0
  67. package/skills/pixerate-video-editor/SKILL.md +9 -2
@@ -18,6 +18,7 @@ export * from './evaluate.cjs';
18
18
  export * from './audio.cjs';
19
19
  export * from './mix.cjs';
20
20
  export * from './mask.cjs';
21
+ export * from './effect-path.cjs';
21
22
  export * from './plan.cjs';
22
23
  export * from './grade.cjs';
23
24
  export * from './lut.cjs';
@@ -18,6 +18,7 @@ export * from './evaluate.js';
18
18
  export * from './audio.js';
19
19
  export * from './mix.js';
20
20
  export * from './mask.js';
21
+ export * from './effect-path.js';
21
22
  export * from './plan.js';
22
23
  export * from './grade.js';
23
24
  export * from './lut.js';
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createEffectAuthoringSession
3
- } from "../chunk-TEFQISBY.js";
3
+ } from "../chunk-X3ZINLIP.js";
4
4
  import {
5
5
  VIDEO_ADJUSTMENT_SCOPES,
6
6
  VIDEO_BLEND_MODES,
@@ -149,7 +149,7 @@ import {
149
149
  videoTransitionEase,
150
150
  videoValueAt,
151
151
  videoWipeRect
152
- } from "../chunk-WDDUPWVG.js";
152
+ } from "../chunk-L3JFOTBQ.js";
153
153
  import {
154
154
  namedWithSuggestion
155
155
  } from "../chunk-EUKY4KFX.js";
@@ -159,7 +159,7 @@ import {
159
159
  effectSequenceTiming,
160
160
  effectTemplateTools,
161
161
  fillEffectTemplate
162
- } from "../chunk-G3RUKJ5C.js";
162
+ } from "../chunk-RXZL4NG5.js";
163
163
  import "../chunk-EHLOCVWB.js";
164
164
  import {
165
165
  CAMERA_MOVE_MAX_MS,
@@ -168,9 +168,10 @@ import {
168
168
  cameraMoveParts,
169
169
  createEffectToolProvider,
170
170
  isEffectError,
171
+ pathControlsOf,
171
172
  resolveCameraMoveSpec,
172
173
  sampleEffectCameraMove
173
- } from "../chunk-SIK3LT6M.js";
174
+ } from "../chunk-OFH6Q67U.js";
174
175
  import "../chunk-V2ZCJRDM.js";
175
176
  import {
176
177
  defineEditorDocument
@@ -2205,6 +2206,73 @@ var VideoAudioPeaksCache = class {
2205
2206
  }
2206
2207
  };
2207
2208
 
2209
+ // src/video-editor/effect-path.ts
2210
+ function videoClipPicture(project, clip) {
2211
+ const asset = clip.type === "media" ? project.assets.find((a) => a.id === clip.asset) : void 0;
2212
+ const size = asset ? { width: asset.width ?? project.width, height: asset.height ?? project.height } : { width: project.width, height: project.height };
2213
+ const fit = clip.fit ?? (clip.type === "media" || clip.type === "sequence" ? "contain" : "fill");
2214
+ const rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, clip.transform, 0);
2215
+ return { size, rect, rotation: videoTransformAt(clip.transform, 0).rotation };
2216
+ }
2217
+ function turn(x, y, cx, cy, deg) {
2218
+ if (!deg) return [x, y];
2219
+ const a = deg * Math.PI / 180;
2220
+ const dx = x - cx;
2221
+ const dy = y - cy;
2222
+ return [cx + dx * Math.cos(a) - dy * Math.sin(a), cy + dx * Math.sin(a) + dy * Math.cos(a)];
2223
+ }
2224
+ function framePointsToPicture(points, picture) {
2225
+ const { rect, size, rotation } = picture;
2226
+ const cx = rect.x + rect.width / 2;
2227
+ const cy = rect.y + rect.height / 2;
2228
+ const out = [];
2229
+ for (let i = 0; i + 1 < points.length; i += 2) {
2230
+ const [x, y] = turn(points[i], points[i + 1], cx, cy, -rotation);
2231
+ out.push(Math.round((x - rect.x) / rect.width * size.width * 100) / 100, Math.round((y - rect.y) / rect.height * size.height * 100) / 100);
2232
+ }
2233
+ return out;
2234
+ }
2235
+ function videoClipMaskOutline(project, clip) {
2236
+ if (!clip.mask) return null;
2237
+ const picture = videoClipPicture(project, clip);
2238
+ const { rect, rotation } = picture;
2239
+ const cx = rect.x + rect.width / 2;
2240
+ const cy = rect.y + rect.height / 2;
2241
+ const polygon = videoMaskPolygon(clip.mask, rect).map(([x, y]) => turn(x, y, cx, cy, rotation));
2242
+ if (polygon.length < 2) return null;
2243
+ return [...polygon, polygon[0]].flatMap(([x, y]) => [Math.round(x * 100) / 100, Math.round(y * 100) / 100]);
2244
+ }
2245
+ var VideoPathGesture = class {
2246
+ constructor(frame, spacing = 3) {
2247
+ this.frame = frame;
2248
+ this.spacing = spacing;
2249
+ }
2250
+ frame;
2251
+ spacing;
2252
+ points = [];
2253
+ drawing = false;
2254
+ at(e, box) {
2255
+ return [Math.round((e.clientX - box.left) / box.width * this.frame.width * 100) / 100, Math.round((e.clientY - box.top) / box.height * this.frame.height * 100) / 100];
2256
+ }
2257
+ down(e, box) {
2258
+ this.drawing = true;
2259
+ this.points = this.at(e, box);
2260
+ }
2261
+ move(e, box) {
2262
+ if (!this.drawing) return false;
2263
+ const [x, y] = this.at(e, box);
2264
+ const n = this.points.length;
2265
+ if (Math.hypot(x - this.points[n - 2], y - this.points[n - 1]) < this.spacing) return false;
2266
+ this.points.push(x, y);
2267
+ return true;
2268
+ }
2269
+ /** Ends the stroke; returns its points, or `null` for a tap (fewer than two points). */
2270
+ up() {
2271
+ this.drawing = false;
2272
+ return this.points.length >= 4 ? [...this.points] : null;
2273
+ }
2274
+ };
2275
+
2208
2276
  // src/video-editor/plan.ts
2209
2277
  function clipIndex(project) {
2210
2278
  const out = /* @__PURE__ */ new Map();
@@ -3660,6 +3728,7 @@ var VideoEditorController = class _VideoEditorController {
3660
3728
  previewScale: this.previewScale,
3661
3729
  effectsProvider: this.effects.provider,
3662
3730
  plans: this.plans,
3731
+ pathDrawing: this.pathDrawing,
3663
3732
  revision: this.revision
3664
3733
  };
3665
3734
  }
@@ -4176,6 +4245,81 @@ var VideoEditorController = class _VideoEditorController {
4176
4245
  return { edit: { op: "set_transition", clip, transition: { ...t, effect: { ...t.effect, values: merged } } }, label: `Change transition ${d.name}`, clip };
4177
4246
  }, options.merge);
4178
4247
  }
4248
+ /**
4249
+ * Gives an effect that draws along a path (see `pathControlsOf`) its path: on an effect clip
4250
+ * (`instance` "generator") or a clip effect (its id). The path is frame points (`points`
4251
+ * `[x0, y0, …]`, mapped into the pixels of the picture the effect renders: the project frame for
4252
+ * an effect clip, the clip's own picture for a clip effect), SVG path data (`d`, which the effect
4253
+ * fits as its control says; `""` clears it), the outline of a clip's mask (`mask`), or words the
4254
+ * provider writes as a single-stroke path (`text`). `control` picks the path control when there
4255
+ * are several. Checked before anything changes; one undo step. Returns the edit, the control and
4256
+ * the value it took.
4257
+ */
4258
+ async setEffectPath(clip, instance, path, options = {}) {
4259
+ const fail2 = (error) => ({ ok: false, error });
4260
+ try {
4261
+ const current = this.clipOrThrow(clip);
4262
+ const effect = instance === "generator" ? current.type === "effect" ? current.effect : null : current.effects?.find((e) => e.id === instance)?.effect;
4263
+ if (!effect) {
4264
+ const ids = ["generator"].filter(() => current.type === "effect").concat((current.effects ?? []).map((e) => e.id));
4265
+ return fail2(`Clip "${clip}" has no effect "${instance}"; its effects are ${ids.map((i) => `"${i}"`).join(", ") || "none"} ("generator" is an effect clip's own picture).`);
4266
+ }
4267
+ const d = await this.describeEffect(effect);
4268
+ const controls = pathControlsOf(d);
4269
+ if (!controls.length) return fail2(`"${d.id}" does not draw along a path. Find one that does with search_effects accepts ["path"].`);
4270
+ const control = options.control ?? (controls.length === 1 ? controls[0] : void 0);
4271
+ if (!control) return fail2(`"${d.id}" has several path controls (${controls.join(", ")}); pass control.`);
4272
+ if (!controls.includes(control)) return fail2(`"${control}" is not a path control of "${d.id}"; its path controls are ${controls.join(", ")}.`);
4273
+ const picture = videoClipPicture(this.project, current);
4274
+ let value;
4275
+ if ("points" in path) {
4276
+ const pts = path.points;
4277
+ if (!Array.isArray(pts) || pts.length < 4 || pts.length % 2 || !pts.every(Number.isFinite)) return fail2("points must be at least two points as [x0, y0, x1, y1, \u2026] in frame pixels.");
4278
+ value = framePointsToPicture(pts, picture);
4279
+ } else if ("d" in path) {
4280
+ if (typeof path.d !== "string") return fail2('d must be SVG path data (a string), or "" to clear the path.');
4281
+ value = path.d;
4282
+ } else if ("mask" in path) {
4283
+ const from = findVideoClip(this.project, path.mask.clip)?.clip;
4284
+ if (!from) return fail2(`There is no clip "${path.mask.clip}" to take a mask from.`);
4285
+ const outline = videoClipMaskOutline(this.project, from);
4286
+ if (!outline) return fail2(`Clip "${path.mask.clip}" has no mask; add one with set_video_clip_mask, or pass points or d.`);
4287
+ value = framePointsToPicture(outline, picture);
4288
+ } else if ("text" in path) {
4289
+ const provider = this.effects.require();
4290
+ if (!provider.pathFromText) return fail2(`The effects provider "${provider.id}" cannot write words as a path; pass points or d.`);
4291
+ if (typeof path.text !== "string" || !path.text.trim()) return fail2("text is empty; pass the words to write.");
4292
+ value = await provider.pathFromText(path.text, { ...path.font ? { font: path.font } : {}, ...path.align ? { align: path.align } : {} });
4293
+ } else return fail2("Pass the path as one of points, d, mask or text.");
4294
+ const r = instance === "generator" ? await this.updateEffectValues(clip, "generator", { [control]: value }) : await this.updateClipEffect(clip, instance, { values: { [control]: value } });
4295
+ return r.ok ? { ...r, control, value } : r;
4296
+ } catch (e) {
4297
+ return fail2(e instanceof Error ? e.message : String(e));
4298
+ }
4299
+ }
4300
+ pathDrawing = null;
4301
+ /**
4302
+ * Starts drawing a path on the preview for an effect that draws along one (the preview then
4303
+ * takes the pointer; `finishPathDrawing` with the frame points applies it). Editor state.
4304
+ */
4305
+ startPathDrawing(target) {
4306
+ this.pathDrawing = { ...target };
4307
+ this.notify();
4308
+ }
4309
+ /** Stops drawing a path without changing anything. */
4310
+ cancelPathDrawing() {
4311
+ if (!this.pathDrawing) return;
4312
+ this.pathDrawing = null;
4313
+ this.notify();
4314
+ }
4315
+ /** Applies the drawn path (frame points) to the effect being drawn for, and stops drawing. One undo step. */
4316
+ async finishPathDrawing(points) {
4317
+ const target = this.pathDrawing;
4318
+ this.pathDrawing = null;
4319
+ this.notify();
4320
+ if (!target) return { ok: false, error: "No path is being drawn; start one from the effect's Draw path button." };
4321
+ return this.setEffectPath(target.clip, target.instance, { points }, { control: target.control });
4322
+ }
4179
4323
  /** Keys for one numeric control of a clip effect (clip-relative ms), or `null` to stop animating it (keeping its value at the clip's start). */
4180
4324
  setClipEffectKeyframes(clip, instance, control, keys) {
4181
4325
  const current = findVideoClip(this.project, clip)?.clip;
@@ -6117,6 +6261,47 @@ function createVideoEffectsToolProvider(controller, options = {}) {
6117
6261
  return { clip: args.clip, transition: clipOf(args.clip).transition, effects: videoClipAppliedEffects(clipOf(args.clip)) };
6118
6262
  }
6119
6263
  },
6264
+ {
6265
+ name: "set_video_effect_path",
6266
+ title: "Run an effect along a path",
6267
+ description: `Gives an effect that draws along a path (search_effects with accepts ["path"]; describe_effect lists its pathControls) the path to draw along: on an effect clip (instance "generator", added with add_video_effect_clip) or a clip effect (its id, from apply_effect). Pass exactly one of: points (frame pixels from the top left, as on the preview; mapped into the pixels of the picture the effect renders, so a clip effect lines up with its moved or scaled clip), d (SVG path data the effect fits into its picture; "" clears the path), mask (use the outline of a clip's mask, in frame pixels), or text (words the provider writes as a single-stroke path, when it can). Refuses an effect without a path control, naming how to find one. One undo step. Returns the clip, instance, the control set and the value it took (points in picture pixels, or path data).`,
6268
+ inputSchema: {
6269
+ type: "object",
6270
+ properties: {
6271
+ clip: { type: "string", minLength: 1, description: "The clip id from get_video_project." },
6272
+ instance: { type: "string", minLength: 1, description: `"generator" for an effect clip's own picture, or a clip effect id from list_applied_effects.` },
6273
+ control: { type: "string", minLength: 1, description: "Which path control (default: the effect's only one; describe_effect pathControls lists them)." },
6274
+ points: { type: "array", minItems: 4, maxItems: 4096, items: { type: "number", description: "x or y, frame pixels." }, description: "The path as frame points [x0, y0, x1, y1, \u2026] in pixels from the frame's top left." },
6275
+ d: { type: "string", description: `SVG path data, fitted into the effect's picture as its control says; "" clears the path.` },
6276
+ mask: { type: "object", properties: { clip: { type: "string", minLength: 1, description: "The clip whose mask outline to use." } }, required: ["clip"], additionalProperties: false, description: "Use the outline of this clip's mask (set_video_clip_mask), in frame pixels." },
6277
+ text: {
6278
+ type: "object",
6279
+ properties: {
6280
+ text: { type: "string", minLength: 1, description: "The words to write." },
6281
+ font: { type: "string", minLength: 1, description: "A single-stroke font the provider offers (default its first)." },
6282
+ align: { type: "string", enum: ["start", "middle", "end"], description: "Where the line sits (default start)." }
6283
+ },
6284
+ required: ["text"],
6285
+ additionalProperties: false,
6286
+ description: "Words the provider writes as a single-stroke path (a signature, handwriting)."
6287
+ }
6288
+ },
6289
+ required: ["clip", "instance"],
6290
+ oneOf: [{ required: ["points"] }, { required: ["d"] }, { required: ["mask"] }, { required: ["text"] }],
6291
+ additionalProperties: false
6292
+ },
6293
+ annotations: { title: "Run an effect along a path", ...ctx.annotations.write },
6294
+ handler: async (args) => {
6295
+ ctx.requireProvider();
6296
+ const given = ["points", "d", "mask", "text"].filter((k2) => args[k2] !== void 0);
6297
+ if (given.length !== 1) throw new Error(`Pass exactly one of points, d, mask or text${given.length ? ` (got ${given.join(", ")})` : ""}.`);
6298
+ const k = given[0];
6299
+ const path = k === "text" ? { ...args.text } : { [k]: args[k] };
6300
+ const r = await controller.setEffectPath(args.clip, args.instance, path, { control: args.control });
6301
+ if (!r.ok) throw new Error(r.error);
6302
+ return { clip: args.clip, instance: args.instance, control: r.control, value: r.value };
6303
+ }
6304
+ },
6120
6305
  {
6121
6306
  name: "set_video_effect_keyframes",
6122
6307
  title: "Keyframe an effect control",
@@ -6675,6 +6860,7 @@ export {
6675
6860
  VideoEffectsEngine,
6676
6861
  VideoFrameCache,
6677
6862
  VideoLutCache,
6863
+ VideoPathGesture,
6678
6864
  VideoPreviewEngine,
6679
6865
  VideoPreviewPixels,
6680
6866
  allVideoClipIds,
@@ -6718,6 +6904,7 @@ export {
6718
6904
  formatVideoSeconds,
6719
6905
  formatVideoTimecode,
6720
6906
  formatVideoTimecodeFrames,
6907
+ framePointsToPicture,
6721
6908
  isVideoDropFrameRate,
6722
6909
  isVideoKeyframes,
6723
6910
  measureVideoLoudness,
@@ -6809,7 +6996,9 @@ export {
6809
6996
  videoClipHeading,
6810
6997
  videoClipKeyframes,
6811
6998
  videoClipLabel,
6999
+ videoClipMaskOutline,
6812
7000
  videoClipOverhang,
7001
+ videoClipPicture,
6813
7002
  videoClipShowsWaveform,
6814
7003
  videoClipSoundGain,
6815
7004
  videoClipSourceRange,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.240.0",
3
+ "version": "0.242.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -161,7 +161,7 @@ refuse.
161
161
  | `apply_effect` | Animate a node (`target`, a group animates as one) with a motion effect: the changes become ordinary commands, tagged with the effect, in a new shot after the last. Returns the instance id. |
162
162
  | `update_effect` | Make an applied effect again with new `values`, in the same place (`effect` is its instance id). |
163
163
  | `remove_effect` | Remove an applied effect: its commands, shots it alone filled and a vector effect's group. |
164
- | `add_vector_effect` | Add a vector effect's own picture as an artwork group (fitted into `box`) with its motion as new shots. Returns the instance and group ids. |
164
+ | `add_vector_effect` | Add a vector effect's own picture as an artwork group (fitted into `box`) with its motion as new shots. For an effect that draws along a path (`search_effects` `accepts: ["path"]`: a signature, a route), `path: { node }` runs it along a path node and lands it on the node, `path: { text }` writes words. Use it when the effect should trace the path itself; use `follow` or a draw-on to move or reveal your own art. Returns the instance and group ids. |
165
165
  | `list_camera_moves` | List the provider's camera moves for a `role` (`transition`: how the camera gets into a shot; `hold`: how it moves while the shot holds), with `durationMs` and `cut`. Returns `available: false` without camera moves. Call before `set_shot_camera_move`; `describe_effect` gives a move's controls and presets. |
166
166
  | `set_shot_camera_move` | Give a shot a camera move (`shot`, `role`, `move`, optional `ms` for a transition, `preset`, `values`): it is saved on the shot's camera as `in` or `during`. Returns the shot's camera and the `window` (ms) the move plays in. A wrong-role id is refused naming the moves that fit. |
167
167
  | `clear_shot_camera_move` | Remove a shot's move for a `role`, keeping its framing. Returns the shot's camera (null when nothing is left). |
@@ -235,21 +235,19 @@ for the controls and presets.
235
235
 
236
236
  ## Effect paint
237
237
 
238
- `set_effect_paint` makes a generator, overlay or title effect the paint. It is rendered once at
239
- the canvas size, and every stroke then takes each pixel's colour from that picture, fixed to the
240
- canvas: painting reveals a hidden image where the brush goes. Its transparency paints nothing, so
241
- an overlay paints only where it has pixels.
238
+ `set_effect_paint` makes a generator, overlay or title effect the paint: rendered once, every
239
+ stroke takes each pixel's colour from it, fixed to the canvas (painting reveals a hidden image).
240
+ Its transparency paints nothing.
242
241
 
243
242
  1. `search_effects` (`use` generator, overlay or title) and `describe_effect` for the controls.
244
- 2. `set_effect_paint` with `effect`, `values` and `time` (ms, default 0). Wait for it: strokes that
245
- would use it are refused while it renders.
246
- 3. Paint: dab, particle and bristle brushes take the colour per pixel (the most faithful); the
247
- Mixer Brush and dab brushes with `behavior.pickup` take it per dab at its centre. Wet and
248
- impasto brushes take it per pixel too, shifted by any paint they pick up: in watercolour and
249
- gouache the pattern then flows, blooms and granulates with the water; impasto keeps its relief.
243
+ 2. `set_effect_paint` with `effect`, `values`, `time` (ms, default 0). Wait for it to return.
244
+ 3. Paint: dab, particle and bristle brushes take the colour per pixel; the Mixer Brush and pick-up
245
+ dab brushes take it per dab at its centre. Wet and impasto brushes take it per pixel, shifted by
246
+ paint they pick up (in watercolour and gouache it flows with the water; impasto keeps its relief).
250
247
  Sprayed stickers each take the colour at their centre (they stay recolourable).
251
- 4. `set_color` or `set_paint` goes back to colour; `remove_effect` (target and effect `paint`)
252
- does too, and `update_effect` (target and effect `paint`, `values`) renders new values.
248
+ 4. `set_color`, `set_paint` or `remove_effect` (target and effect `paint`) go back to colour;
249
+ `update_effect` (target and effect `paint`, `values`) renders new values.
250
+ 5. Along each stroke (`map`) or playing on stroke layers (`live`): [references/effect-paint.md](references/effect-paint.md).
253
251
 
254
252
  When to use which:
255
253
 
@@ -259,6 +257,7 @@ When to use which:
259
257
  | The effect in a wash that flows, or in thick paint | `set_effect_paint` and a watercolour, gouache, oil or impasto brush |
260
258
  | The effect inside an exact shape, unchanged by water | Paint the shape, dry it, then `fill_layer_with_effect` with `lockTransparency: true` |
261
259
  | The effect over the whole sheet | `set_paper_effect` or `add_effect_layer` |
260
+ | A pattern that follows the stroke, or paint that moves | `map: { by: "stroke" }`, `live: true` on a stroke layer |
262
261
 
263
262
  ## Canvas-hosted layer
264
263
 
@@ -402,7 +401,8 @@ view), so work with the tools you are given.
402
401
  | `set_paper_effect` | Draw a generator over the paper (`values`, `opacity`, `blend` default `multiply`, `time`); `effect: null` removes it. One undo step. Returns the effect paper. |
403
402
  | `add_effect_layer` | Add a layer drawn by a generator, overlay or title effect (`values`, `name`, `aboveId`, `opacity`, `blend`, `time`); it becomes active. Returns the layer and its effect. |
404
403
  | `fill_layer_with_effect` | Composite a generator, overlay or title effect onto a layer (`layerId`, `values`, `opacity`, `lockTransparency`, `time`). One undo step. Returns the layer id and the effect. |
405
- | `set_effect_paint` | Paint with a generator, overlay or title effect (`effect`, `values`, `time` ms default 0): rendered once at the canvas size, strokes take each pixel's colour from it. Call after `describe_effect`, then `paint_stroke` with a dry brush. Editor state, not an undo step. Returns `effectPaint` (`effect`, `provider`, `values`, `time`). |
404
+ | `set_effect_paint` | Paint with a generator, overlay or title effect (`effect`, `values`, `time` ms default 0): rendered once at the canvas size, strokes take each pixel's colour from it; `map` `{ by: "stroke", length, repeat }` lays it along each stroke; `live` keeps it playing on stroke layers. Call after `describe_effect`, then `paint_stroke`. Editor state, not an undo step. Returns `effectPaint`. |
405
+ | `set_effect_playback` | Play or pause live effect paint in the view (`playing`) and cap its frame rate (`fps` 1..60). Editor state. Returns `playing`, `fps`, `time` and `animation` (what plays, or null). Export a moment with `export_image` / `export_psd` `time`. |
406
406
 
407
407
  ### Guides
408
408
 
@@ -495,13 +495,9 @@ The host may add the shared document tools (see `pixerate-agent-tools`). A paint
495
495
  strokes record it, and replays render it again first, so they need the same provider (an
496
496
  error names the effect and provider when it is missing; `color` on `replay_strokes` repaints
497
497
  in one colour instead).
498
- - **Effect paint in wet paint moves with the water**: the pattern bleeds and blooms like any
499
- pigment, so fine detail softens. For a crisp pattern in an exact shape, paint the shape, dry it,
500
- and `fill_layer_with_effect` with `lockTransparency: true`. Erasers, blenders and smudges ignore
501
- effect paint; a brush with its own gradient, a stroke `color` or `paint`, or `set_color` /
502
- `set_paint` paints colour instead.
503
- - **Effect paint is fixed to the canvas, not the stroke**: a replayed or moved stroke shows the
504
- part of the effect under its new place. Wait for `set_effect_paint` to return before painting;
505
- strokes are refused while it renders.
498
+ - **Effect paint is fixed to the canvas by default** (`map: { by: "stroke" }` follows the stroke),
499
+ and in wet paint it flows with the water; for a crisp pattern in an exact shape, paint and dry
500
+ the shape, then `fill_layer_with_effect` (`lockTransparency: true`). Erasers, blenders and
501
+ smudges ignore it. Wait for `set_effect_paint` to return; strokes are refused while it renders.
506
502
  - **Recorded strokes are kept in saved documents** (`save_paint_document`, `get_paint_document`),
507
503
  so a painting can be replayed and repainted at a higher resolution.
@@ -0,0 +1,25 @@
1
+ # Effect paint along the stroke, and live effect paint
2
+
3
+ - **Along the stroke** (`set_effect_paint` `map: { by: "stroke", length, repeat }`): the effect is
4
+ rendered as a strip `length` px long (16..4096, default 300) and 256 px across. Each pixel takes
5
+ the colour at its distance along the stroke and its place across the tip, so the pattern follows
6
+ the stroke's turns instead of staying fixed to the canvas. `repeat` starts the strip again
7
+ (default), `mirror` runs it back, `clamp` holds the last colour. Use it for ribbons, ropes,
8
+ flames and textures that should flow with the mark; use the default canvas mapping to reveal one
9
+ hidden picture across many strokes. Every colour-laying brush takes it (watercolour, gouache, oil
10
+ and impasto too; sprayed stickers at their centre); particle and bristle marks take the place of
11
+ their pen dab.
12
+ Scaled replays (`export_high_res`) scale `length` with the canvas.
13
+ - **Live** (`live: true`): on a stroke layer (`add_layer {type: "strokes"}`) each stroke keeps
14
+ playing the effect, so the paint moves. Paint the strokes, then `set_effect_playback`
15
+ (`playing`, `fps` 1..60, default 30) plays or pauses them in the view. The document keeps the
16
+ frame at the effect's own `time`; `export_image` and `export_psd` take `time` (ms) to export
17
+ another moment. Live strokes stay editable like any stroke on a stroke layer
18
+ (`update_stroke` with `paint` changes the effect).
19
+ - **Where it applies**: live only plays on stroke layers; on a pixel layer the stroke bakes the
20
+ frame at `time`. A painting plays at most 8 distinct live effects (each needs a renderer per
21
+ frame); paint more strokes with one of them, or bake new ones (`live: false`).
22
+ - **Cost**: each frame redraws the live strokes (and any stroke overlapping them), and renders
23
+ each live effect once. On a large canvas with many live strokes, lower `fps`.
24
+ - **Without the provider** (a document opened elsewhere): the saved pixels show the frame as
25
+ painted, and nothing plays until the provider is registered.
@@ -82,7 +82,9 @@ the pitfalls, and uses the tools below.
82
82
  controls, then `apply_effect` (filters and overlays on a clip), `set_video_effect_transition`
83
83
  or `add_video_effect_clip` (generators and titles). Tune with `update_effect` and
84
84
  `set_video_effect_keyframes`; check with `get_video_frame` (layers list `effects`) and
85
- `list_applied_effects`.
85
+ `list_applied_effects`. **An effect along a path** (a beam along a border, a trail, words
86
+ written on): `search_effects` with `accepts: ["path"]`, add it as a clip or clip effect, then
87
+ `set_video_effect_path` with frame `points`, path data `d`, a clip's `mask` outline or `text`.
86
88
  12. **Camera moves** (a provider with camera moves, such as Flair): a clip is the shot.
87
89
  `list_video_camera_moves` with a `role`, then `set_clip_camera_move`: `transition` moves
88
90
  from `from` to `framing` from the clip's start (a glide, whip pan, punch zoom); `hold`
@@ -298,7 +300,7 @@ and every write tool is refused with the reason.
298
300
 
299
301
  | Tool | Use it to |
300
302
  | --- | --- |
301
- | `search_effects` | Find effects: words in `query`, filtered by `kind`, `category` or `use` (filter, overlay for clips; transition; generator, title). Returns ids, names, uses, whether they are transparent or loop, and their length. Camera moves (`kind` camera) are listed but go on clips with `set_clip_camera_move`, not `apply_effect`. Call `describe_effect` next. |
303
+ | `search_effects` | Find effects: words in `query`, filtered by `kind`, `category`, `use` (filter, overlay for clips; transition; generator, title) or `accepts: ["path"]` (effects that draw along a path you give). Returns ids, names, uses, whether they are transparent or loop, and their length. Camera moves (`kind` camera) are listed but go on clips with `set_clip_camera_move`, not `apply_effect`. Call `describe_effect` next. |
302
304
  | `describe_effect` | Read one effect's `controls` (a strict JSON Schema: names, types, ranges, units), `defaults`, `presets` and `inputs`, and whether it `appliesHere` (as a clip effect). Use the names and ranges for values. |
303
305
  | `list_applied_effects` | See the effects in the project per clip, in order: clip effects (`fx1`...), an effect clip's own effect (`generator`) and an effect transition (`transition`), with their saved values and `available` (false when the provider lacks it: drawn as a placeholder). |
304
306
  | `apply_effect` | Add a filter or overlay effect to a clip's stack (`target` clip id, `effect`, `values`, `index`, `enabled`, `opacity`). Effects run in order on the clip's picture before its grade and placement. Returns the effect's new id and the clip's effects. |
@@ -306,6 +308,7 @@ and every write tool is refused with the reason.
306
308
  | `remove_effect` | Take an effect off a clip (`transition` removes an effect transition; an effect clip is deleted with `delete_video_clips`). Returns what was removed and what is left. |
307
309
  | `add_video_effect_clip` | Put a generator, title or overlay effect on a video `track` as a clip filling the frame (`start`, `duration` default the effect's own length, `values`, `mode`). Transparent effects show the tracks beneath. Returns the clip id. |
308
310
  | `set_video_effect_transition` | Use a transition effect from the effects provider into a `clip` from the one ending where it starts (`duration` centred on the cut, `values`). Needs handles like a crossfade. For a cross dissolve, dip, wipe, push, slide or iris use the built-in `set_video_transition` instead. |
311
+ | `set_video_effect_path` | Give an effect that draws along a path (`describe_effect` `pathControls`) its path, on an effect clip (`instance` "generator") or a clip effect: frame `points` `[x0, y0, …]` (mapped into the picture the effect renders, so a clip effect follows its moved or scaled clip), path data `d` (`""` clears), a clip's `mask` outline, or `text` the provider writes. One undo step. Returns the control and the value it took. |
309
312
  | `set_video_effect_keyframes` | Animate one number `control` of a clip effect (or an effect clip's `generator`) with `keys` in clip ms, or `null` to stop animating it. |
310
313
  | `list_video_camera_moves` | List the provider's camera moves for a `role` (`transition` into a clip, `hold` across it) with `durationMs` and `cut`. Returns `available: false` without camera moves. Call before `set_clip_camera_move`. |
311
314
  | `set_clip_camera_move` | Play a camera move on a clip's picture (`clip`, `role`, `move`, `ms` for a transition, `preset`, `values`, `from` and `framing` as `{ zoom, x, y }`): baked into its transform keys, one per frame, two at a cut. Returns the camera, key counts, `cuts`, `windows` and `notes` (tilt, turn and blur cannot be baked). Refuses a clip keyed by hand. |
@@ -328,6 +331,10 @@ The host may add the shared document tools (see `pixerate-agent-tools`). A video
328
331
 
329
332
  ## Pitfalls
330
333
 
334
+ - **Path points are frame pixels**, as on the preview: `set_video_effect_path` maps them into the
335
+ picture the effect renders. Do not pass a clip effect points in its source's pixels yourself, and
336
+ give it the path again after moving or scaling the clip a lot (it maps the clip's first frame).
337
+
331
338
  - **Camera moves are baked keys.** After trimming or splitting a clip, `check_video_project`
332
339
  reports `camera-stale`: set the move again to re-bake it. Editing the clip's x, y, scale or
333
340
  rotation keys by hand leaves the moves no longer describing them; clear the moves first.