@lottiefiles/creator-mcp 0.2.0 → 0.2.2
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/index.mjs +73 -102
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -356,8 +356,8 @@ const maskEllipseInputSchema = z.object({
|
|
|
356
356
|
width: z.number().min(0),
|
|
357
357
|
height: z.number().min(0)
|
|
358
358
|
}).strict();
|
|
359
|
-
const maskModeInputSchema = z.enum(["add", "subtract"]).describe("Default 'add'.");
|
|
360
|
-
const maskOpacityInputSchema = z.number().min(0).max(100).describe("Default 100.");
|
|
359
|
+
const maskModeInputSchema = z.enum(["add", "subtract"]).describe("Default 'add'. 'add' = show ONLY the content inside the mask outline (the normal clip/reveal). 'subtract' = cut a HOLE — hide the content inside the outline and show everything outside it.");
|
|
360
|
+
const maskOpacityInputSchema = z.number().min(0).max(100).describe("Mask opacity, not an enable/disable strength. Default 100. For an ADD mask, 100 shows the content inside the outline and 0 makes the masked layer fully transparent. For a SUBTRACT mask, 100 fully cuts the content inside the outline and 0 cuts nothing. Use `remove_mask` to restore unmasked rendering.");
|
|
361
361
|
const createMaskInputSchema = z.object({
|
|
362
362
|
layer_id: z.string(),
|
|
363
363
|
shape: z.enum([
|
|
@@ -453,11 +453,13 @@ const easingInputSchema = z.union([z.object({ type: z.literal("LINEAR") }).stric
|
|
|
453
453
|
y1: z.number(),
|
|
454
454
|
x2: z.number(),
|
|
455
455
|
y2: z.number()
|
|
456
|
-
}).strict()]);
|
|
457
|
-
const
|
|
456
|
+
}).strict().describe("Explicit bezier control points [x1, y1, x2, y2]. **Only `apply_keyframes` and `retime_keyframes` accept the `{ type: 'PRESET', name }` shorthand; `animate_paint`, `animate_property` and `set_group_transform` need these raw coefficients — they silently downgrade an unrecognised easing shape to LINEAR (wrong motion, no error).** Preset equivalents: ease-in 0.42,0,1,1 · ease-out 0,0,0.58,1 · ease-in-out 0.42,0,0.58,1 · gentle-out 0.22,1,0.36,1 · snappy-out 0.16,1,0.3,1 · smooth-in-out 0.65,0,0.35,1 · overshoot-soft 0.34,1.56,0.64,1 · overshoot-hard 0.68,-0.55,0.265,1.55 · anticipate 0.22,-0.4,0.56,1.")]);
|
|
457
|
+
const presetEasingInputSchema = z.object({
|
|
458
458
|
type: z.literal("PRESET"),
|
|
459
459
|
name: easingPresetNameSchema
|
|
460
|
-
}).strict().describe("Named curve — prefer this over hand-picked CUBIC_BEZIER coefficients. Presets expand to the exact bezier values server-side. Each maps to a motion PERSONALITY archetype — pick the one matching your Direction and use it consistently (see motion-design.md → Motion Personality):\n- 'gentle-out' — PREMIUM archetype (elegant/luxury/minimal): smooth ease-out, 0% overshoot, ~350-600ms feel. The reveal default; spreads motion across the full duration.\n- 'snappy-out' — CORPORATE archetype (clean/professional/dashboard): quick, decisive settle, 0-3% overshoot, ~200-400ms.\n- 'overshoot-soft' / 'overshoot-hard' — PLAYFUL archetype (fun/bouncy/cute): pop-in / bouncy settle, 10-20% overshoot, ~150-300ms.\n- 'anticipate' — PLAYFUL/ENERGETIC wind-up: pulls back before moving forward.\n- 'ease-in-out' / 'smooth-in-out' — both ends, cinematic (neutral).\n- 'ease-in' / 'ease-out' — CSS-standard, reliable neutral default.")
|
|
460
|
+
}).strict().describe("Named curve — prefer this over hand-picked CUBIC_BEZIER coefficients. Presets expand to the exact bezier values server-side. Each maps to a motion PERSONALITY archetype — pick the one matching your Direction and use it consistently (see motion-design.md → Motion Personality):\n- 'gentle-out' — PREMIUM archetype (elegant/luxury/minimal): smooth ease-out, 0% overshoot, ~350-600ms feel. The reveal default; spreads motion across the full duration.\n- 'snappy-out' — CORPORATE archetype (clean/professional/dashboard): quick, decisive settle, 0-3% overshoot, ~200-400ms.\n- 'overshoot-soft' / 'overshoot-hard' — PLAYFUL archetype (fun/bouncy/cute): pop-in / bouncy settle, 10-20% overshoot, ~150-300ms.\n- 'anticipate' — PLAYFUL/ENERGETIC wind-up: pulls back before moving forward.\n- 'ease-in-out' / 'smooth-in-out' — both ends, cinematic (neutral).\n- 'ease-in' / 'ease-out' — CSS-standard, reliable neutral default.");
|
|
461
|
+
const keyframeEasingInputSchema = z.union([easingInputSchema, presetEasingInputSchema]);
|
|
462
|
+
const retimeEasingInputSchema = z.union([easingPresetNameSchema, keyframeEasingInputSchema]);
|
|
461
463
|
const coreAnimatedPropertyValues = [
|
|
462
464
|
"position",
|
|
463
465
|
"scale",
|
|
@@ -516,6 +518,21 @@ const layoutShapeSchemaFields = {
|
|
|
516
518
|
outer_roundness: z.number().min(0).max(100).describe("Polygon/star layout item: rounds the outer corners (0 = sharp, 100 = fully rounded). Default 0.").optional(),
|
|
517
519
|
stroke: shapeStrokeInputSchema.describe("Shape layout item stroke outline. Supported on created shape items.").optional()
|
|
518
520
|
};
|
|
521
|
+
const textAlignInputSchema = z.enum([
|
|
522
|
+
"left",
|
|
523
|
+
"center",
|
|
524
|
+
"right",
|
|
525
|
+
"justify_left",
|
|
526
|
+
"justify_center",
|
|
527
|
+
"justify_right",
|
|
528
|
+
"justify_full"
|
|
529
|
+
]).describe("Horizontal alignment. `left` / `center` / `right` are plain alignment — each line is placed at that edge and nothing is stretched; these are what you want for headlines, labels and single-line text. The `justify_*` values FULL-JUSTIFY every line (words are spread to fill the wrap width, so they only do anything on wrapped multi-line text — see `wrap_width`) and differ only in how the LAST line is treated: `justify_left` = last line flush left (the normal book/paragraph look), `justify_center` = last line centred, `justify_right` = last line flush right, `justify_full` = last line also stretched edge-to-edge. When omitted, the layer's existing alignment is preserved (left for new unwrapped text); `wrap_width` without an explicit `align` uses center alignment.");
|
|
530
|
+
const statePlayModeInputSchema = z.enum([
|
|
531
|
+
"Forward",
|
|
532
|
+
"Reverse",
|
|
533
|
+
"Bounce",
|
|
534
|
+
"ReverseBounce"
|
|
535
|
+
]).describe("Playback direction for this state's segment. Default 'Forward'.\n- 'Forward' — plays first frame → last frame.\n- 'Reverse' — plays last frame → first frame.\n- 'Bounce' — PING-PONG: forward, then backward, then forward… This is the answer to 'make it ping-pong', 'make it go back and forth', 'yo-yo it', 'play it in reverse after it finishes'. Combine with `loop: true` for a continuous back-and-forth.\n- 'ReverseBounce' — the same ping-pong, but starting with the BACKWARD pass.");
|
|
519
536
|
/**
|
|
520
537
|
* Client-executed Motion Copilot V3 tools.
|
|
521
538
|
*
|
|
@@ -529,13 +546,13 @@ const allClientTools = {
|
|
|
529
546
|
inputSchema: z.object({}).strict()
|
|
530
547
|
},
|
|
531
548
|
apply_keyframes: {
|
|
532
|
-
description: "Add keyframes to ONE animatable property on ONE layer. This is the preferred tool for simple single-property animations (rotate, fade, slide, pulse, recolor). If you need to animate multiple properties on the same layer, plan separate tool calls in the order they should be applied; Creator serializes native tool execution through the dispatcher.\n\n**POLISH TURNS (snappier / smoother / tighter / less bouncy / fix timing / more premium / etc.) — call `read_keyframes` FIRST on every animated layer in scope BEFORE calling this tool.** The conversation history's view of existing keyframes is stale once the user edits in Creator directly; reading is the only safe source of truth. Then prefer the smallest possible mutation: easing-only change > retiming > amplitude tweak > full rebuild. See motion-design.md → 'Repair / polish protocol' for the full rule.\n\nValue type depends on the property:\n- `position`, `scale` → value is `{x, y}`. **`scale` is a percentage of the layer's AUTHORED size, matching AE / Creator semantics — NOT a percentage of 100.** A layer the user imported or hand-resized can sit at `50`, `200`, `1250`, etc. — and THAT value is the resting state. Animating a layer authored at 1250 → 110 collapses it to ~9% of its on-canvas size and looks like a bug. **Always read the current static value via `read_keyframes` or `read_scene` before writing scale keyframes**, then animate RELATIVE to that authored value (1250 pulses to 1350, not to 110). Same baseline rule applies to `position` and `rotation` — the layer's authored values are the resting state, not 0/0 or 0°. See motion-design.md → 'Baseline awareness'. **TEXT `position` is doubly dangerous: a text layer's transform anchor is the baseline / box origin, so its raw `position` value is NOT its `visual_center` and NOT the `position` arg you gave `create_text` (that arg is the visual center). Feeding `visual_center` in as a `position` keyframe throws the text off-canvas. For a settle-into-place entrance use `animate_entrance` (no coordinate needed); for any other text move, `read_keyframes` the raw `static_value` and animate RELATIVE to it. Never repair a keyframed text layer with `set_anchor_point` → `center_layer` — they stack two shifts and fling it off-canvas.**\n- `rotation`, `opacity` → value is a number (rotation in degrees; opacity 0–100)\n- `fill_color` → value is `{r, g, b}` with 0-255 per channel. **Requires a SOLID fill at `fills[0]`** — animation only works when the layer's first fill is SOLID. Gradient or empty layers will fail with `'Layer has no SOLID fill at fills[0]'`. To animate the color of a layer that currently has a gradient (or no fill), call `set_fill({type:'SOLID', color: <starting color>})` FIRST, then this tool. Verify the fill type via `read_scene` (look at the layer's fill metadata) or just call `set_fill` defensively before animating colors on a layer you didn't create with a solid fill.\n- `trim_start`, `trim_end` → number 0–100 (percent of path). Use for stroke draw-on reveals: animate `trim_end` from 0 → 100 to grow the path in. A trim path is auto-created on first use. If the current playhead stays at a frame where `trim_end` is 0 or opacity is 0, the layer is correctly present but invisible on canvas; after creating a reveal, call `set_current_frame` to leave Creator on a visible frame.\n- `trim_offset` → number 0–360 (degrees around the path). For rotating-around-path effects.\n- `stroke_width` → number (pixels). Requires the layer to already have a stroke — use `set_stroke` first.\n\nTo set a static color (no animation), pass a single keyframe at frame 0 and consider `clear_first: true` to remove any existing color animation.\n\nEasing is optional on any keyframe; omit it for linear-ish default. Use `CUBIC_BEZIER` with explicit control points for polished motion; `LINEAR` for deliberate no-easing.\n\nUse `clear_first: true` to replace existing keyframes on that property; otherwise new keyframes are merged with existing ones (sorted by frame).\n\nFor paint, shape geometry, masks, groups, scene settings, and timing edits, prefer the dedicated typed tools (`animate_paint`, `animate_property`, `create_shape_group`, `update_mask`, `retime_keyframes`) over rebuilding with many generic keyframe calls.",
|
|
549
|
+
description: "Add keyframes to ONE animatable property on ONE layer. This is the preferred tool for simple single-property animations (rotate, fade, slide, pulse, recolor). If you need to animate multiple properties on the same layer, plan separate tool calls in the order they should be applied; Creator serializes native tool execution through the dispatcher.\n\n**POLISH TURNS (snappier / smoother / tighter / less bouncy / fix timing / more premium / etc.) — call `read_keyframes` FIRST on every animated layer in scope BEFORE calling this tool.** The conversation history's view of existing keyframes is stale once the user edits in Creator directly; reading is the only safe source of truth. Then prefer the smallest possible mutation: easing-only change > retiming > amplitude tweak > full rebuild. See motion-design.md → 'Repair / polish protocol' for the full rule.\n\nValue type depends on the property:\n- `position`, `scale` → value is `{x, y}`. **`scale` is a percentage of the layer's AUTHORED size, matching AE / Creator semantics — NOT a percentage of 100.** A layer the user imported or hand-resized can sit at `50`, `200`, `1250`, etc. — and THAT value is the resting state. Animating a layer authored at 1250 → 110 collapses it to ~9% of its on-canvas size and looks like a bug. **Always read the current static value via `read_keyframes` or `read_scene` before writing scale keyframes**, then animate RELATIVE to that authored value (1250 pulses to 1350, not to 110). Same baseline rule applies to `position` and `rotation` — the layer's authored values are the resting state, not 0/0 or 0°. See motion-design.md → 'Baseline awareness'. **TEXT `position` is doubly dangerous: a text layer's transform anchor is the baseline / box origin, so its raw `position` value is NOT its `visual_center` and NOT the `position` arg you gave `create_text` (that arg is the visual center). Feeding `visual_center` in as a `position` keyframe throws the text off-canvas. For a settle-into-place entrance use `animate_entrance` (no coordinate needed); for any other text move, `read_keyframes` the raw `static_value` and animate RELATIVE to it. Never repair a keyframed text layer with `set_anchor_point` → `center_layer` — they stack two shifts and fling it off-canvas.**\n- `rotation`, `opacity` → value is a number (rotation in degrees; opacity 0–100)\n- `fill_color` → value is `{r, g, b}` with 0-255 per channel. **Requires a SOLID fill at `fills[0]`** — animation only works when the layer's first fill is SOLID. Gradient or empty layers will fail with `'Layer has no SOLID fill at fills[0]'`. To animate the color of a layer that currently has a gradient (or no fill), call `set_fill({type:'SOLID', color: <starting color>})` FIRST, then this tool. Verify the fill type via `read_scene` (look at the layer's fill metadata) or just call `set_fill` defensively before animating colors on a layer you didn't create with a solid fill.\n- `trim_start`, `trim_end` → number 0–100 (percent of path). Use for stroke draw-on reveals: animate `trim_end` from 0 → 100 to grow the path in. A trim path is auto-created on first use. If the current playhead stays at a frame where `trim_end` is 0 or opacity is 0, the layer is correctly present but invisible on canvas; after creating a reveal, call `set_current_frame` to leave Creator on a visible frame.\n- `trim_offset` → number 0–360 (degrees around the path). For rotating-around-path effects.\n- `stroke_width` → number (pixels). Requires the layer to already have a stroke — use `set_stroke` first.\n\nTo set a static color (no animation), pass a single keyframe at frame 0 and consider `clear_first: true` to remove any existing color animation.\n\nEasing is optional on any keyframe; omit it for linear-ish default. Use `CUBIC_BEZIER` with explicit control points for polished motion; `LINEAR` for deliberate no-easing.\n\nUse `clear_first: true` to replace existing keyframes on that property; otherwise new keyframes are merged with existing ones (sorted by frame).\n\nFor paint, shape geometry, masks, groups, scene settings, and timing edits, prefer the dedicated typed tools (`animate_paint`, `animate_property`, `create_shape_group`, `update_mask`, `retime_keyframes`) over rebuilding with many generic keyframe calls.\n\n**RESPONSE — verify before saying \"Done\":** returns `{ layer_id, property, added, total_after }`. `added` is how many keyframes THIS call submitted; **`total_after` is a real read-back of how many keyframes the property holds afterwards** — it is re-read from the layer, not echoed from your input.\n- **Write success:** the call did not throw and the requested value was read back. A non-throwing one-keyframe write with `total_after: 1` is a valid static write.\n- **Animation success:** motion requires at least two distinct stored keyframe frames. After a multi-keyframe submission, `total_after: 1` means the write collapsed and the layer will not animate; call `read_keyframes` if the returned count or frames are unexpected.\n- With `clear_first: true`, expect `total_after === added`. A LARGER number means keyframes you did not write survived on that property and the result is a blend of old and new — `read_keyframes` before reporting done.\n- Without `clear_first`, `total_after` is the merged count. If it is lower than (previous + `added`), some of your frames landed on frames that already had keyframes and replaced them rather than adding.\n- Throws (and writes NOTHING) on an unknown `layer_id`, on a property that is not animatable on that layer, and on an unknown easing PRESET `name`.",
|
|
533
550
|
inputSchema: z.object({
|
|
534
551
|
layer_id: z.string().describe("Layer id from `read_scene` (e.g. 'qttDVXo7OQ')."),
|
|
535
552
|
property: coreAnimatedPropertySchema,
|
|
536
553
|
clear_first: z.boolean().describe("If true, clear existing keyframes on this property before adding. Default false.").optional(),
|
|
537
554
|
keyframes: z.array(z.object({
|
|
538
|
-
frame: z.number(),
|
|
555
|
+
frame: z.number().int().min(0).describe("Whole frame number on the scene timeline. Must be an INTEGER >= 0 and <= the scene `duration_frames` from `read_scene` — Creator has no sub-frame keyframes, and a frame past the scene duration is written but never played. Round any computed frame (e.g. a third of a 25-frame beat is 8, not 8.333)."),
|
|
539
556
|
value: z.union([
|
|
540
557
|
z.number(),
|
|
541
558
|
vector2Schema,
|
|
@@ -620,7 +637,7 @@ const allClientTools = {
|
|
|
620
637
|
"action",
|
|
621
638
|
"accent"
|
|
622
639
|
]).describe("Optional. Resolves a DEFAULT slate fill when `fill_color` is OMITTED. 'accent' = slate-700 NEUTRAL default (NOT a chromatic hue — pass `fill` for chroma). An explicit `fill_color` always wins; ignored when `fill_color` is given.").optional(),
|
|
623
|
-
fill: z.string().describe("Optional named Flat-UI accent, e.g. 'peter-river','carrot','emerald','sun-flower','alizarin','belize-hole','pomegranate'. ONE chromatic hue; purple/violet never (unknown names are ignored). Wins over `role`; only applies when `fill_color` is omitted.").optional(),
|
|
640
|
+
fill: z.string().describe("Optional named Flat-UI accent, e.g. 'peter-river','carrot','emerald','sun-flower','alizarin','belize-hole','pomegranate'. ONE chromatic hue; purple/violet never (unknown names are ignored). Wins over `role`; only applies when `fill_color` is omitted. **Name collision: `fill` here is a palette NAME STRING — the `fill` on `set_fill` / `set_stroke` / `add_shape_to_layer` is a PAINT OBJECT (`{ type: 'SOLID', color: {r,g,b} }`). These schemas are strict, so a paint object passed here is rejected; for an explicit colour on this call use `fill_color`.**").optional(),
|
|
624
641
|
path: z.object({
|
|
625
642
|
points: z.array(z.object({
|
|
626
643
|
vertex: z.object({
|
|
@@ -686,7 +703,7 @@ const allClientTools = {
|
|
|
686
703
|
}).strict()
|
|
687
704
|
},
|
|
688
705
|
create_text: {
|
|
689
|
-
description: "**BEFORE CALLING: check the creation order.** New text layers are inserted at the TOP of the render stack. If this turn also creates a card / background / banner, create the background FIRST and THIS text layer LAST — otherwise the text will land beneath the background. (Reorder APIs exist if needed via `reorder_layer`, but planning the order is cheaper.)\n\nCreate a new native TEXT_LAYER with the given content and optional native TextDocument style. Returns `layer_id` so you can immediately animate it with `apply_keyframes` (opacity for fade-in, position for slide, scale for pop). For letter-by-letter or word-by-word staggered animations, emit one text-layer call per unit in the order they should be created; Creator serializes native tool execution.\n\n**Centering is by VISUAL BOUNDS, not layer origin.** Use `center_in_scene: true` for scene center. Use `position: {x,y}` when you want the bounds center at a specific target point (e.g. top-center hero text). Do not manually compensate for text origin/anchor.\n\n**For long body copy that needs to wrap, pass `wrap_width` instead of constructing a text box yourself.** The gateway translates `wrap_width` into a centered wrap box internally — your `position` / `center_in_scene` still controls visual placement. Omit `wrap_width` for headlines, subtitles, footers, single-line text.\n\nNative text styling supports font size, font family/variant or font name, fill color, stroke color/width, alignment, line height, tracking, caps, and `wrap_width`. For \"slight tracking\", use a small value such as 2-8; values around 20-40 are very wide and can make title text exceed the scene.\n\n**RESPONSE — verify these BEFORE saying \"Done\":**\n- `visual_center` is where the rendered text actually centers. Compare against your intent (`position` or scene center).\n- `visual_bounds` is the rendered rectangle in world coords.\n- `scene_fit.clipped` / `scene_fit.off_canvas` / `scene_fit.overflow.{left,right,top,bottom}`: if any overflow > 0, the new text is clipped at the canvas edge. Self-correct (reduce `font_size`, add `wrap_width`, shorten the text) or surface the limitation in one short line. See motion-design.md → \"Post-mutation layout integrity\".",
|
|
706
|
+
description: "**BEFORE CALLING: check the creation order.** New text layers are inserted at the TOP of the render stack. If this turn also creates a card / background / banner, create the background FIRST and THIS text layer LAST — otherwise the text will land beneath the background. (Reorder APIs exist if needed via `reorder_layer`, but planning the order is cheaper.)\n\nCreate a new native TEXT_LAYER with the given content and optional native TextDocument style. Returns `layer_id` so you can immediately animate it with `apply_keyframes` (opacity for fade-in, position for slide, scale for pop). For letter-by-letter or word-by-word staggered animations, emit one text-layer call per unit in the order they should be created; Creator serializes native tool execution.\n\n**Centering is by VISUAL BOUNDS, not layer origin.** Use `center_in_scene: true` for scene center. Use `position: {x,y}` when you want the bounds center at a specific target point (e.g. top-center hero text). Do not manually compensate for text origin/anchor.\n\n**For long body copy that needs to wrap, pass `wrap_width` instead of constructing a text box yourself.** The gateway translates `wrap_width` into a centered wrap box internally — your `position` / `center_in_scene` still controls visual placement. `wrap_width` without an explicit `align` uses center alignment; pass `align: \"left\"` for a conventional paragraph column. Omit `wrap_width` for headlines, subtitles, footers, single-line text.\n\nNative text styling supports font size, font family/variant or font name, fill color, stroke color/width, alignment, line height, tracking, caps, and `wrap_width`. For \"slight tracking\", use a small value such as 2-8; values around 20-40 are very wide and can make title text exceed the scene.\n\n**RESPONSE — verify these BEFORE saying \"Done\":**\n- `visual_center` is where the rendered text actually centers. Compare against your intent (`position` or scene center).\n- `visual_bounds` is the rendered rectangle in world coords.\n- `scene_fit.clipped` / `scene_fit.off_canvas` / `scene_fit.overflow.{left,right,top,bottom}`: if any overflow > 0, the new text is clipped at the canvas edge. Self-correct (reduce `font_size`, add `wrap_width`, shorten the text) or surface the limitation in one short line. See motion-design.md → \"Post-mutation layout integrity\".",
|
|
690
707
|
inputSchema: z.object({
|
|
691
708
|
text: z.string().min(1).describe("The text content to display."),
|
|
692
709
|
name: z.string().describe("Layer name (defaults to the first ~24 chars of `text`).").optional(),
|
|
@@ -695,25 +712,17 @@ const allClientTools = {
|
|
|
695
712
|
y: z.number()
|
|
696
713
|
}).strict().describe("Visual center target for the text bounds. Ignored if center_in_scene is true.").optional(),
|
|
697
714
|
center_in_scene: z.boolean().describe("If true, place the rendered text bounding box at scene center using rendered bounds when available and measured text bounds as fallback. Use whenever the user asks for centered text; overrides `position`.").optional(),
|
|
698
|
-
rotation: z.number().optional(),
|
|
715
|
+
rotation: z.number().describe("Degrees, default 0.").optional(),
|
|
699
716
|
scale: z.object({
|
|
700
717
|
x: z.number(),
|
|
701
718
|
y: z.number()
|
|
702
|
-
}).strict().optional(),
|
|
719
|
+
}).strict().describe("PERCENTAGE, default `{ x: 100, y: 100 }` — matching AE / Creator semantics, NOT a 0–1 multiplier. `{ x: 1, y: 1 }` renders the text at ONE PERCENT of its size (an invisible speck), which is the classic mistake here. Use `font_size` to set type size; only use `scale` when you specifically want a transform-level scale.").optional(),
|
|
703
720
|
opacity: z.number().min(0).max(100).optional(),
|
|
704
721
|
font_size: z.number().gt(0).optional(),
|
|
705
722
|
font_name: z.string().describe("Direct Creator font asset name, e.g. \"Inter Bold\". Prefer font_family/font_variant when known.").optional(),
|
|
706
723
|
font_family: z.string().describe("Font family, e.g. Inter.").optional(),
|
|
707
724
|
font_variant: z.string().describe("Font variant/style, e.g. Bold.").optional(),
|
|
708
|
-
align:
|
|
709
|
-
"left",
|
|
710
|
-
"center",
|
|
711
|
-
"right",
|
|
712
|
-
"justify_left",
|
|
713
|
-
"justify_center",
|
|
714
|
-
"justify_right",
|
|
715
|
-
"justify_full"
|
|
716
|
-
]).optional(),
|
|
725
|
+
align: textAlignInputSchema.optional(),
|
|
717
726
|
line_height: z.number().gt(0).optional(),
|
|
718
727
|
tracking: z.number().describe("Native text tracking / letter spacing value. For \"slight tracking\", use 2-8. Avoid 20+ unless the user explicitly asks for very wide spacing.").optional(),
|
|
719
728
|
text_caps: z.enum([
|
|
@@ -731,7 +740,7 @@ const allClientTools = {
|
|
|
731
740
|
"caption",
|
|
732
741
|
"accent"
|
|
733
742
|
]).describe("Optional. Resolves a DEFAULT slate text fill when `fill_color` is omitted: heading=slate-900, body=slate-600, caption=slate-500. Sets COLOR ONLY (not size/line-height — use `font_size` / create_text_group for typography). 'accent' = slate-700 neutral; pass `fill` for chroma. Explicit `fill_color` always wins.").optional(),
|
|
734
|
-
fill: z.string().describe("Optional named Flat-UI accent (kebab, e.g. 'peter-river'). Wins over `role`; only applies when `fill_color` is omitted; purple/unknown ignored.").optional()
|
|
743
|
+
fill: z.string().describe("Optional named Flat-UI accent (kebab, e.g. 'peter-river'). Wins over `role`; only applies when `fill_color` is omitted; purple/unknown ignored. **Name collision: `fill` here is a palette NAME STRING — the `fill` on `set_fill` / `set_stroke` is a PAINT OBJECT. For an explicit colour on this call use `fill_color`.**").optional()
|
|
735
744
|
}).strict()
|
|
736
745
|
},
|
|
737
746
|
create_text_group: {
|
|
@@ -891,22 +900,14 @@ const allClientTools = {
|
|
|
891
900
|
}).strict()
|
|
892
901
|
},
|
|
893
902
|
set_text_style: {
|
|
894
|
-
description: "Update native TextDocument styling on an existing TEXT_LAYER. Supports font size, font family/variant or font name, fill color, stroke color/width, alignment, line height, tracking, caps, and `wrap_width` (for paragraph copy that needs to wrap inside a fixed-width column). Pass `fill_color: null` to clear the fill override, `stroke_color: null` to clear the stroke color override, and `stroke_width: null` to clear the stroke width override. By default it preserves the current visual bounds center after style changes. For \"slight tracking\", use 2-8, not 20-40.\n\n**RESPONSE — verify before saying \"Done\":** returns `visual_center`, `visual_bounds`, and `scene_fit`. Bumping `font_size` can push the rendered text past the canvas edge — check `scene_fit.clipped` / `scene_fit.overflow.{left,right,top,bottom}` after EVERY set_text_style call. If anything overflows, self-correct (smaller multiplier, add `wrap_width` to wrap, or bump the scene via `set_scene_settings`) or surface the limitation in one short line. See motion-design.md → \"Post-mutation layout integrity\".",
|
|
903
|
+
description: "Update native TextDocument styling on an existing TEXT_LAYER. Supports font size, font family/variant or font name, fill color, stroke color/width, alignment, line height, tracking, caps, and `wrap_width` (for paragraph copy that needs to wrap inside a fixed-width column). Setting `wrap_width` without an explicit `align` uses center alignment; pass `align: \"left\"` to keep a conventional paragraph column. Pass `fill_color: null` to clear the fill override, `stroke_color: null` to clear the stroke color override, and `stroke_width: null` to clear the stroke width override. By default it preserves the current visual bounds center after style changes. For \"slight tracking\", use 2-8, not 20-40.\n\n**RESPONSE — verify before saying \"Done\":** returns `visual_center`, `visual_bounds`, and `scene_fit`. Bumping `font_size` can push the rendered text past the canvas edge — check `scene_fit.clipped` / `scene_fit.overflow.{left,right,top,bottom}` after EVERY set_text_style call. If anything overflows, self-correct (smaller multiplier, add `wrap_width` to wrap, or bump the scene via `set_scene_settings`) or surface the limitation in one short line. See motion-design.md → \"Post-mutation layout integrity\".",
|
|
895
904
|
inputSchema: z.object({
|
|
896
905
|
layer_id: z.string(),
|
|
897
906
|
font_size: z.number().gt(0).optional(),
|
|
898
907
|
font_name: z.string().describe("Direct Creator font asset name, e.g. \"Inter Bold\". Prefer font_family/font_variant when known.").optional(),
|
|
899
908
|
font_family: z.string().optional(),
|
|
900
909
|
font_variant: z.string().optional(),
|
|
901
|
-
align:
|
|
902
|
-
"left",
|
|
903
|
-
"center",
|
|
904
|
-
"right",
|
|
905
|
-
"justify_left",
|
|
906
|
-
"justify_center",
|
|
907
|
-
"justify_right",
|
|
908
|
-
"justify_full"
|
|
909
|
-
]).optional(),
|
|
910
|
+
align: textAlignInputSchema.optional(),
|
|
910
911
|
line_height: z.number().gt(0).optional(),
|
|
911
912
|
tracking: z.number().describe("Native text tracking / letter spacing value. For \"slight tracking\", use 2-8. Avoid 20+ unless the user explicitly asks for very wide spacing.").optional(),
|
|
912
913
|
text_caps: z.enum([
|
|
@@ -987,17 +988,7 @@ const allClientTools = {
|
|
|
987
988
|
per_item_delay_frames: z.number().min(0).describe("Frames between each layer's start. If the user gives a literal number (\"10-frame stagger\") use it verbatim. Default 4.").optional(),
|
|
988
989
|
start_frame: z.number().min(0).describe("Frame the first layer starts. Default 0.").optional(),
|
|
989
990
|
duration_frames: z.number().min(1).describe("Each layer's entrance duration. Default 16.").optional(),
|
|
990
|
-
easing:
|
|
991
|
-
"ease-in",
|
|
992
|
-
"ease-out",
|
|
993
|
-
"ease-in-out",
|
|
994
|
-
"gentle-out",
|
|
995
|
-
"snappy-out",
|
|
996
|
-
"smooth-in-out",
|
|
997
|
-
"overshoot-soft",
|
|
998
|
-
"overshoot-hard",
|
|
999
|
-
"anticipate"
|
|
1000
|
-
]).describe("Easing preset name. Default 'gentle-out'.").optional(),
|
|
991
|
+
easing: easingPresetNameSchema.describe("Easing preset NAME as a bare string, e.g. 'gentle-out' — NOT the `{ type: 'PRESET', name }` object that `apply_keyframes` takes, and no LINEAR / CUBIC_BEZIER here. Default 'gentle-out'. To re-ease afterwards use `retime_keyframes({ operation: 'set_easing' })`, which accepts either shape.").optional(),
|
|
1001
992
|
distance: z.number().min(0).describe("Slide travel in px (preset 'slide' only). Default 40.").optional(),
|
|
1002
993
|
direction: z.enum([
|
|
1003
994
|
"up",
|
|
@@ -1027,17 +1018,7 @@ const allClientTools = {
|
|
|
1027
1018
|
distance: z.number().min(0).describe("Slide travel in px for rise/rise-fade. Default 40.").optional(),
|
|
1028
1019
|
frames: z.number().min(1).describe("Entrance duration in frames. Default 16.").optional(),
|
|
1029
1020
|
start_frame: z.number().min(0).describe("Frame the entrance starts (holds offset/invisible before). Default 0.").optional(),
|
|
1030
|
-
easing:
|
|
1031
|
-
"ease-in",
|
|
1032
|
-
"ease-out",
|
|
1033
|
-
"ease-in-out",
|
|
1034
|
-
"gentle-out",
|
|
1035
|
-
"snappy-out",
|
|
1036
|
-
"smooth-in-out",
|
|
1037
|
-
"overshoot-soft",
|
|
1038
|
-
"overshoot-hard",
|
|
1039
|
-
"anticipate"
|
|
1040
|
-
]).describe("Easing preset name. Default 'gentle-out'.").optional()
|
|
1021
|
+
easing: easingPresetNameSchema.describe("Easing preset NAME as a bare string, e.g. 'gentle-out' — NOT the `{ type: 'PRESET', name }` object that `apply_keyframes` takes. Default 'gentle-out'. To re-ease afterwards use `retime_keyframes({ operation: 'set_easing' })`, which accepts either shape.").optional()
|
|
1041
1022
|
}).strict()
|
|
1042
1023
|
},
|
|
1043
1024
|
list_fonts: {
|
|
@@ -1065,7 +1046,7 @@ const allClientTools = {
|
|
|
1065
1046
|
inputSchema: z.object({
|
|
1066
1047
|
layer_id: z.string(),
|
|
1067
1048
|
replace_first: z.boolean().describe("If true (default), remove fills[0] before adding. If false, append.").optional(),
|
|
1068
|
-
fill: paintInputSchema
|
|
1049
|
+
fill: paintInputSchema.describe("The paint to apply, as an OBJECT: `{ type: 'SOLID', color: {r,g,b} }`, `{ type: 'GRADIENT_LINEAR', stops: [...] }`, or `{ type: 'GRADIENT_RADIAL', stops: [...] }`. **Not the same `fill` as `create_shape` / `create_text`, where `fill` is a palette NAME STRING and a plain colour goes in `fill_color`.** This schema is strict — a bare string or a `{r,g,b}` without a `type` is rejected.")
|
|
1069
1050
|
}).strict()
|
|
1070
1051
|
},
|
|
1071
1052
|
set_stroke: {
|
|
@@ -1074,7 +1055,7 @@ const allClientTools = {
|
|
|
1074
1055
|
layer_id: z.string(),
|
|
1075
1056
|
width: z.number().min(.1).describe("Stroke width in pixels."),
|
|
1076
1057
|
replace_first: z.boolean().describe("If true (default), replace strokes[0]; else append.").optional(),
|
|
1077
|
-
fill: paintInputSchema
|
|
1058
|
+
fill: paintInputSchema.describe("The stroke's paint, as an OBJECT — `{ type: 'SOLID', color: {r,g,b} }` or a gradient, same shape as `set_fill.fill`. Named `fill` because a stroke is painted with a fill; it is NOT the palette NAME STRING that `create_shape.fill` / `create_text.fill` take.")
|
|
1078
1059
|
}).strict()
|
|
1079
1060
|
},
|
|
1080
1061
|
animate_paint: {
|
|
@@ -1101,13 +1082,13 @@ const allClientTools = {
|
|
|
1101
1082
|
}).strict()
|
|
1102
1083
|
},
|
|
1103
1084
|
animate_property: {
|
|
1104
|
-
description: "Keyframe extended properties not covered by apply_keyframes, including paint opacity/color, gradient handles, shape geometry, mask path/opacity, text animator colors, and scene-layer time mapping. Use this when the property is explicitly named and a typed high-level tool is not a better fit.",
|
|
1085
|
+
description: "Keyframe extended properties not covered by apply_keyframes, including paint opacity/color, gradient handles, shape geometry, mask path/opacity, text animator colors, and scene-layer time mapping. Use this when the property is explicitly named and a typed high-level tool is not a better fit.\n\n**Which sub-object gets animated** — the `property` picks a list on the layer, and `target_id`/`target_index` pick one entry in that list. **Both are optional and default to the FIRST entry**, which is what you want on the common single-mask / single-fill / single-shape layer:\n- `mask_path`, `mask_opacity` → the layer's masks (id from `create_mask`).\n- `shape_path`, `shape_size`, `rectangle_roundness`, `outer_radius`, `inner_radius`, `outer_roundness`, `inner_roundness`, `polygon_points`, `star_points` → the layer's shapes (id from `add_shape_to_layer`).\n- `stroke_color`, `stroke_opacity` → the layer's strokes; `fill_opacity`, `gradient_stops`, `gradient_start`, `gradient_end`, `highlight_angle`, `highlight_length` → the layer's fills, falling back to strokes when there is no matching fill.\n- `text_fill_color`, `text_stroke_color`, `text_stroke_width` → the layer's text animators (falls back to the layer itself when it has none).\n- `time_mapping` → the layer itself; both targeting fields are ignored.\n\n**RESPONSE:** returns `{ layer_id, property, target_index, target_id, added, total_after }`. `total_after` is a read-back of the keyframe count on the resolved property — treat `total_after >= 2` as the success predicate for an animation. Throws when the property is not available on the resolved target (e.g. `mask_path` on a layer with no masks).",
|
|
1105
1086
|
inputSchema: z.object({
|
|
1106
1087
|
layer_id: z.string(),
|
|
1107
1088
|
property: extendedOnlyAnimatedPropertySchema,
|
|
1108
|
-
target_index: z.number().min(0).optional(),
|
|
1109
|
-
target_id: z.string().optional(),
|
|
1110
|
-
clear_first: z.boolean().optional(),
|
|
1089
|
+
target_index: z.number().min(0).describe("Zero-based index into the list the `property` selects (masks / shapes / fills / strokes / text animators). **Defaults to 0 — the first entry.** Ignored when `target_id` is given. Use this only when the layer genuinely has several of that thing.").optional(),
|
|
1090
|
+
target_id: z.string().describe("Exact id of the sub-object to animate — a `mask_id` from `create_mask`, a `shape_id` from `add_shape_to_layer`, or a fill/stroke id. Wins over `target_index`. Omit it (and `target_index`) to animate the FIRST entry, which is the normal path since `read_scene` does not list mask / shape / paint ids.").optional(),
|
|
1091
|
+
clear_first: z.boolean().describe("Clear existing keyframes on the resolved property first. **Defaults to TRUE here** (unlike `apply_keyframes`, which defaults to false). Pass false to merge.").optional(),
|
|
1111
1092
|
keyframes: z.array(z.object({
|
|
1112
1093
|
frame: z.number(),
|
|
1113
1094
|
value: extendedPropertyKeyframeValueSchema,
|
|
@@ -1470,12 +1451,7 @@ const allClientTools = {
|
|
|
1470
1451
|
autoplay: z.boolean().describe("Start playing as soon as this state is entered. Default true for most states.").optional(),
|
|
1471
1452
|
loop: z.boolean().describe("Loop the segment indefinitely. Default false.").optional(),
|
|
1472
1453
|
loop_count: z.number().min(1).describe("Number of times to loop. Ignored unless loop is true.").optional(),
|
|
1473
|
-
mode:
|
|
1474
|
-
"Forward",
|
|
1475
|
-
"Reverse",
|
|
1476
|
-
"Bounce",
|
|
1477
|
-
"ReverseBounce"
|
|
1478
|
-
]).describe("Playback direction. Default Forward.").optional(),
|
|
1454
|
+
mode: statePlayModeInputSchema.optional(),
|
|
1479
1455
|
speed: z.number().describe("Playback speed multiplier (e.g. 1.0, 0.5, 2.0). Default 1.").optional(),
|
|
1480
1456
|
must_complete: z.boolean().describe("If true, the segment must finish before any transition can fire. Default false.").optional()
|
|
1481
1457
|
}).strict()
|
|
@@ -1494,17 +1470,12 @@ const allClientTools = {
|
|
|
1494
1470
|
state_id: z.string().describe("State id from `list_states`."),
|
|
1495
1471
|
name: z.string().optional(),
|
|
1496
1472
|
segment: z.string().describe("New segment name. Use empty string to clear.").optional(),
|
|
1497
|
-
autoplay: z.boolean().optional(),
|
|
1498
|
-
loop: z.boolean().optional(),
|
|
1499
|
-
loop_count: z.number().min(1).optional(),
|
|
1500
|
-
mode:
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
"Bounce",
|
|
1504
|
-
"ReverseBounce"
|
|
1505
|
-
]).optional(),
|
|
1506
|
-
speed: z.number().optional(),
|
|
1507
|
-
must_complete: z.boolean().optional()
|
|
1473
|
+
autoplay: z.boolean().describe("Start playing as soon as this state is entered.").optional(),
|
|
1474
|
+
loop: z.boolean().describe("Loop the segment indefinitely.").optional(),
|
|
1475
|
+
loop_count: z.number().min(1).describe("Number of times to loop. Ignored unless `loop` is true.").optional(),
|
|
1476
|
+
mode: statePlayModeInputSchema.optional(),
|
|
1477
|
+
speed: z.number().describe("Playback speed multiplier (e.g. 1.0, 0.5, 2.0).").optional(),
|
|
1478
|
+
must_complete: z.boolean().describe("If true, the segment must finish before any transition can fire.").optional()
|
|
1508
1479
|
}).strict()
|
|
1509
1480
|
},
|
|
1510
1481
|
set_state_actions: {
|
|
@@ -1893,7 +1864,7 @@ const allClientTools = {
|
|
|
1893
1864
|
}).strict()
|
|
1894
1865
|
},
|
|
1895
1866
|
vectorize_image: {
|
|
1896
|
-
description: "Convert an existing PNG/JPG image layer into editable vector shape layers using Creator
|
|
1867
|
+
description: "Convert an existing PNG/JPG image layer into editable vector shape layers using Creator's vectorizer. Use this when the user asks to vectorize, trace, convert an image/logo/photo to vectors, or make an imported raster image editable. Defaults to the selected image layer when `layer_id` is omitted. The generated vector layers are inserted above the source and selected; after at least one vector layer is created, the original raster is preserved but hidden so it does not render or export twice. Restore it with `set_layer_visibility({ layer_id: source_layer_id, visible: true })` when the user wants to compare or revert.",
|
|
1897
1868
|
inputSchema: z.object({ layer_id: z.string().describe("Image layer id from `read_scene`. Optional when an image layer is selected.").optional() }).strict()
|
|
1898
1869
|
},
|
|
1899
1870
|
remove_keyframes: {
|
|
@@ -1960,16 +1931,16 @@ const allClientTools = {
|
|
|
1960
1931
|
}).strict()
|
|
1961
1932
|
},
|
|
1962
1933
|
create_shape_group: {
|
|
1963
|
-
description: "Create an empty group inside an existing SHAPE_LAYER or group. Use this to build composed single-layer components like cards, icons, badges, and shadows without creating many top-level layers.",
|
|
1934
|
+
description: "Create an empty group inside an existing SHAPE_LAYER or group. Use this to build composed single-layer components like cards, icons, badges, and shadows without creating many top-level layers.\n\n**RESPONSE — keep `group_id`, it is the only way to address the group afterwards:** returns `{ group_id, parent_id, name }`. `read_scene` returns LAYERS, not the shape/group tree inside them, so this response is your only source of the id. Feed `group_id` straight into `add_shape_to_layer({ parent_id: group_id })` to fill the group and into `set_group_transform({ group_id })` to move / fade the whole group as one unit.",
|
|
1964
1935
|
inputSchema: z.object({
|
|
1965
|
-
parent_id: z.string(),
|
|
1966
|
-
name: z.string().optional()
|
|
1936
|
+
parent_id: z.string().describe("Container to create the group inside: a SHAPE_LAYER id (the `layer_id` returned by `create_shape`) or another group id (a `group_id` returned by an earlier `create_shape_group`). Non-shape layers are rejected."),
|
|
1937
|
+
name: z.string().describe("Optional group name, e.g. \"card-shadow\". Defaults to Creator's.").optional()
|
|
1967
1938
|
}).strict()
|
|
1968
1939
|
},
|
|
1969
1940
|
add_shape_to_layer: {
|
|
1970
|
-
description: "Add a rectangle, ellipse, polygon, star, or path to an existing SHAPE_LAYER or group. Use with create_shape_group to build one composed unit with internal groups.",
|
|
1941
|
+
description: "Add a rectangle, ellipse, polygon, star, or path to an existing SHAPE_LAYER or group. Use with create_shape_group to build one composed unit with internal groups.\n\n**RESPONSE:** returns `{ parent_id, shape_id, shape, fill_id, stroke_id }` (`fill_id` / `stroke_id` only when you passed `fill` / `stroke`). Keep `shape_id` — it is the `target_id` for `animate_property` on shape geometry (`shape_size`, `rectangle_roundness`, `shape_path`, …), and `read_scene` does not list the shapes inside a layer. If the fill or stroke fails to attach, the whole call is rolled back (the shape is removed) and it throws, so a successful response means every requested piece exists.",
|
|
1971
1942
|
inputSchema: z.object({
|
|
1972
|
-
parent_id: z.string(),
|
|
1943
|
+
parent_id: z.string().describe("Container to add the shape to: a SHAPE_LAYER id (from `create_shape`) or a `group_id` from `create_shape_group`."),
|
|
1973
1944
|
shape: z.enum([
|
|
1974
1945
|
"rectangle",
|
|
1975
1946
|
"ellipse",
|
|
@@ -1988,23 +1959,23 @@ const allClientTools = {
|
|
|
1988
1959
|
outer_roundness: z.number().min(0).max(100).optional(),
|
|
1989
1960
|
inner_roundness: z.number().min(0).max(100).optional(),
|
|
1990
1961
|
path: pathDataInputSchema.optional(),
|
|
1991
|
-
fill: paintInputSchema.optional(),
|
|
1992
|
-
stroke: paintInputWithOptionalWidthSchema.optional(),
|
|
1962
|
+
fill: paintInputSchema.describe("PAINT OBJECT — `{ type: 'SOLID', color: {r,g,b} }` or a gradient, same shape as `set_fill.fill`. NOT the palette NAME STRING that `create_shape.fill` takes.").optional(),
|
|
1963
|
+
stroke: paintInputWithOptionalWidthSchema.describe("PAINT OBJECT plus an optional `width` (px, default 1) — e.g. `{ type: 'SOLID', color: {r,g,b}, width: 2 }`. NOT the `{ color, width }` shape that `create_shape.stroke` takes.").optional(),
|
|
1993
1964
|
name: z.string().optional()
|
|
1994
1965
|
}).strict()
|
|
1995
1966
|
},
|
|
1996
1967
|
set_group_transform: {
|
|
1997
|
-
description: "Set or keyframe a shape group transform/opacity property. Use this when a composed unit has internal groups that should move or fade together.",
|
|
1968
|
+
description: "Set or keyframe a shape group transform/opacity property. Use this when a composed unit has internal groups that should move or fade together.\n\nPass EITHER `value` (a static set) OR `keyframes` (an animation) — the call throws if you pass neither. `keyframes` wins when both are given.\n\nReturns `{ group_id, property, keyframes_added }`; `keyframes_added` is 0 for a static `value` set.",
|
|
1998
1969
|
inputSchema: z.object({
|
|
1999
|
-
group_id: z.string(),
|
|
1970
|
+
group_id: z.string().describe("Group id from the `create_shape_group` response. `read_scene` lists layers, not the groups inside them, so that response (or an earlier `add_shape_to_layer` parent) is where this id comes from."),
|
|
2000
1971
|
property: z.enum([
|
|
2001
1972
|
"position",
|
|
2002
1973
|
"scale",
|
|
2003
1974
|
"rotation",
|
|
2004
1975
|
"opacity"
|
|
2005
1976
|
]),
|
|
2006
|
-
value: z.union([z.number(), vector2Schema]).optional(),
|
|
2007
|
-
clear_first: z.boolean().optional(),
|
|
1977
|
+
value: z.union([z.number(), vector2Schema]).describe("Static value to set (no animation). `{x, y}` for position/scale, a number for rotation (degrees) / opacity (0-100). Omit when passing `keyframes`.").optional(),
|
|
1978
|
+
clear_first: z.boolean().describe("Clear existing keyframes on this group property before writing. **Defaults to TRUE here** (unlike `apply_keyframes`, which defaults to false). Pass false to merge with what is already there.").optional(),
|
|
2008
1979
|
keyframes: z.array(z.object({
|
|
2009
1980
|
frame: z.number(),
|
|
2010
1981
|
value: z.union([z.number(), vector2Schema]),
|
|
@@ -2035,7 +2006,7 @@ const allClientTools = {
|
|
|
2035
2006
|
}).strict()
|
|
2036
2007
|
},
|
|
2037
2008
|
retime_keyframes: {
|
|
2038
|
-
description: "Shift, scale, reverse, evenly distribute, or RE-EASE keyframes on one layer property WITHOUT rebuilding values. Use for timing/feel polish like \"shift this 10 frames later\", \"make the bounce twice as fast\", or \"make it springier\".\n\n**To change the EASING / feel (springy, snappy, smoother, bouncy, more/less overshoot) use `operation: 'set_easing'` with `easing` — NOT apply_keyframes.** set_easing swaps only the easing curve and preserves every frame and value. Rewriting the animation via apply_keyframes forces you to restate all values and risks corrupting positions (e.g. collapsing a staggered word onto itself). For a multi-layer cascade (split-text letters, card grid), call set_easing once PER layer — each call is value-safe, so the layout cannot drift.",
|
|
2009
|
+
description: "Shift, scale, reverse, evenly distribute, or RE-EASE keyframes on one layer property WITHOUT rebuilding values. Use for timing/feel polish like \"shift this 10 frames later\", \"make the bounce twice as fast\", or \"make it springier\".\n\n**To change the EASING / feel (springy, snappy, smoother, bouncy, more/less overshoot) use `operation: 'set_easing'` with `easing` — NOT apply_keyframes.** set_easing swaps only the easing curve and preserves every frame and value. Rewriting the animation via apply_keyframes forces you to restate all values and risks corrupting positions (e.g. collapsing a staggered word onto itself). For a multi-layer cascade (split-text letters, card grid), call set_easing once PER layer — each call is value-safe, so the layout cannot drift.\n\n**Every operation reads the property's CURRENT keyframes, rewrites their frames, and puts the same values back — values are never touched by any operation.** The tool throws if the property has no keyframes yet (use `apply_keyframes` to create them first).\n\n`operation`, and the parameters each one reads (all others are ignored):\n- **`shift_frames`** — moves every keyframe by `offset` frames. Positive = LATER, negative = EARLIER. \"shift this 10 frames later\" = `{ operation: 'shift_frames', offset: 10 }`. **`offset` defaults to 0, so `{ operation: 'shift_frames' }` with no `offset` is schema-valid and does nothing** — always pass it. Frames are clamped at 0, so shifting earlier past the start piles those keyframes onto frame 0 and flattens that part of the motion; check the returned `frames`.\n- **`scale_time`** — multiplies each keyframe's DISTANCE FROM THE ORIGIN by `scale`: `new = origin + (old - origin) * scale`. The origin is `start_frame` if given, otherwise the property's FIRST keyframe. `scale` must be greater than 0; zero and negative values are rejected before any keyframes change. `scale < 1` = faster/shorter, `scale > 1` = slower/longer. \"make the bounce twice as fast\" = `{ operation: 'scale_time', scale: 0.5 }`; \"twice as slow\" = `scale: 2`. `scale` defaults to 1 (no-op) — always pass it. The result is NOT rounded, so a scale that does not divide the spans evenly leaves keyframes on fractional frames (frame 7 × 0.5 = 3.5); prefer factors that land on whole frames.\n- **`reverse`** — mirrors the frames inside a window: `new = start + end - old`, so the motion plays backwards while keeping the same span. The window defaults to the property's first→last keyframe; pass `start_frame`/`end_frame` to mirror inside a different window.\n- **`distribute_evenly`** — respaces the EXISTING keyframes at equal intervals between `start_frame` and `end_frame` (defaults: first→last keyframe), keeping their order and values. Use it to kill uneven timing (\"even out the spacing\", \"make the steps regular\"). It does not add or remove keyframes.\n- **`set_easing`** — replaces the easing curve on every keyframe of the property; frames and values are untouched. Requires `easing` (the call throws without it).\n\n**RESPONSE:** returns `{ layer_id, property, operation, retimed, frames }`. `retimed` is how many keyframes were rewritten (unchanged from before — this operation never adds or drops keyframes) and `frames` is the full new frame list, sorted. **Check `frames` before saying done:** repeated values mean keyframes collided (clamping at 0, or a `scale` small enough to merge them) and that stretch of the animation is now flat.",
|
|
2039
2010
|
inputSchema: z.object({
|
|
2040
2011
|
layer_id: z.string(),
|
|
2041
2012
|
property: animatablePropertySchema,
|
|
@@ -2046,11 +2017,11 @@ const allClientTools = {
|
|
|
2046
2017
|
"distribute_evenly",
|
|
2047
2018
|
"set_easing"
|
|
2048
2019
|
]),
|
|
2049
|
-
offset: z.number().optional(),
|
|
2050
|
-
scale: z.number().optional(),
|
|
2051
|
-
start_frame: z.number().optional(),
|
|
2052
|
-
end_frame: z.number().optional(),
|
|
2053
|
-
easing:
|
|
2020
|
+
offset: z.number().describe("Frames to move every keyframe by, for operation 'shift_frames'. POSITIVE = later, NEGATIVE = earlier. Defaults to 0 (a silent no-op), so always pass it for that operation. Ignored by every other operation.").optional(),
|
|
2021
|
+
scale: z.number().positive().describe("Positive time multiplier for operation 'scale_time', applied to each keyframe's distance from the origin (`start_frame`, else the first keyframe). Must be greater than 0; invalid values are rejected without changing keyframes. 0.5 = twice as fast, 2 = twice as slow. Defaults to 1 (a silent no-op), so always pass it for that operation. Ignored by every other operation.").optional(),
|
|
2022
|
+
start_frame: z.number().describe("Window start. For 'scale_time' this is the ORIGIN the scaling pivots around (keyframes at this frame do not move). For 'reverse' and 'distribute_evenly' it is the start of the frame window. Defaults to the property's FIRST existing keyframe. Ignored by 'shift_frames' and 'set_easing'.").optional(),
|
|
2023
|
+
end_frame: z.number().describe("Window end for 'reverse' (the mirror is `start + end - frame`) and 'distribute_evenly' (the last keyframe lands here). Defaults to the property's LAST existing keyframe. Ignored by 'shift_frames', 'scale_time' and 'set_easing'.").optional(),
|
|
2024
|
+
easing: retimeEasingInputSchema.describe("Easing for operation 'set_easing' (required by it, ignored by every other operation). 'overshoot-soft'/'overshoot-hard' = springy/bouncy; 'snappy-out' = snappy; 'smooth-in-out' = smooth. Applied to the property's keyframes in place, values and frames untouched.\n\nAccepts EITHER the bare preset name (`'gentle-out'`) OR the same object form `apply_keyframes` takes (`{ type: 'PRESET', name: 'gentle-out' }`, `{ type: 'LINEAR' }`, `{ type: 'CUBIC_BEZIER', x1, y1, x2, y2 }`) — carry whichever shape you already have, no translation needed.").optional()
|
|
2054
2025
|
}).strict()
|
|
2055
2026
|
},
|
|
2056
2027
|
delete_layer: {
|
|
@@ -2058,21 +2029,21 @@ const allClientTools = {
|
|
|
2058
2029
|
inputSchema: z.object({ layer_id: z.string() }).strict()
|
|
2059
2030
|
},
|
|
2060
2031
|
create_mask: {
|
|
2061
|
-
description: "Add a mask to a layer. Masks clip the layer's visible content to an arbitrary shape — use for reveal wipes ('reveal from left'), clipping images/Lotties to a shape ('mask this image to a circle'), and any 'show only the part inside' request.\n\nFor the common shapes, pass one of the shorthands:\n\n- `shape: \"rectangle\"` + `rect: { x, y, width, height }` — axis-aligned rectangle in LAYER-local coordinates.\n- `shape: \"ellipse\"` + `ellipse: { cx, cy, width, height }` — center-defined ellipse, width/height being the bounding box. Plugin synthesizes a 4-point cubic bezier approximation (standard kappa constant).\n- `shape: \"path\"` + `path: { points: [{vertex:{x,y}, inTan:{x,y}, outTan:{x,y}}...], closed: boolean }` — raw PathData for complex shapes.\n\nCoords are LAYER-LOCAL — a rect at (0, 0, 100, 100) covers the top-left 100×100 of the layer's content, regardless of where the layer sits in the scene.\n\nOptional `mode` defaults to 'add' (show only inside the mask). Pass 'subtract' to cut a hole. Optional `opacity` (0–100) controls mask strength.
|
|
2032
|
+
description: "Add a mask to a layer. Masks clip the layer's visible content to an arbitrary shape — use for reveal wipes ('reveal from left'), clipping images/Lotties to a shape ('mask this image to a circle'), and any 'show only the part inside' request.\n\nFor the common shapes, pass one of the shorthands:\n\n- `shape: \"rectangle\"` + `rect: { x, y, width, height }` — axis-aligned rectangle in LAYER-local coordinates.\n- `shape: \"ellipse\"` + `ellipse: { cx, cy, width, height }` — center-defined ellipse, width/height being the bounding box. Plugin synthesizes a 4-point cubic bezier approximation (standard kappa constant).\n- `shape: \"path\"` + `path: { points: [{vertex:{x,y}, inTan:{x,y}, outTan:{x,y}}...], closed: boolean }` — raw PathData for complex shapes.\n\nCoords are LAYER-LOCAL — a rect at (0, 0, 100, 100) covers the top-left 100×100 of the layer's content, regardless of where the layer sits in the scene.\n\nOptional `mode` defaults to 'add' (show only inside the mask). Pass 'subtract' to cut a hole. Optional `opacity` (0–100) controls mask opacity; it is not an enable/disable strength. For an ADD mask, opacity 0 makes the masked layer fully transparent and 100 shows the content inside the outline. For a SUBTRACT mask, opacity 0 cuts nothing and 100 fully cuts inside the outline. Use `remove_mask` to restore unmasked rendering.\n\n**RESPONSE — this is the ONLY source of a mask id:** returns `{ layer_id, mask_id, shape, mode, opacity }`. `read_scene` does NOT list masks or mask ids, so if you need to address a specific mask later, keep `mask_id` from this response. Everything that takes a mask id also has a first-mask fallback, so a lost id is recoverable on single-mask layers: `update_mask` / `remove_mask` act on the FIRST mask when `mask_id` is omitted, and `animate_property` targets the first mask when both `target_id` and `target_index` are omitted.\n\nAnimate mask opacity/path with `animate_property({ layer_id, property: 'mask_opacity' | 'mask_path', ... })` — pass `target_id: mask_id` (or `target_index`) only when the layer has MORE THAN ONE mask; otherwise omit both and it uses the first one. `apply_keyframes` targets layer properties only and cannot reach masks. Note `mask_path` keyframe values are whole PathData objects (`{ points: [...], closed }`), not a width — build the full path for each keyframe.\n\nCommon recipes:\n- 'reveal from left': rectangle mask at (0, 0, 0, layerH), then animate `mask_path` from a zero-width rect to a full-width one with `animate_property`.\n- 'circle clip': ellipse mask, cx/cy = layer center, width/height = desired diameter.\n- 'oval-vignette': ellipse mask slightly inset from layer bounds, opacity 0 → 100 animated with `animate_property` on `mask_opacity`.",
|
|
2062
2033
|
inputSchema: createMaskInputSchema
|
|
2063
2034
|
},
|
|
2064
2035
|
update_mask: {
|
|
2065
|
-
description: "Update an existing mask path, opacity, or mode. Use after create_mask when the reveal shape needs tuning without recreating the mask.",
|
|
2036
|
+
description: "Update an existing mask path, opacity, or mode. Use after create_mask when the reveal shape needs tuning without recreating the mask. Only the fields you pass change; omitted fields are left alone.\n\nReturns `{ layer_id, mask_id, updated: { path, opacity, mode } }` — each `updated` flag reports whether that field was part of this call.",
|
|
2066
2037
|
inputSchema: z.object({
|
|
2067
|
-
layer_id: z.string(),
|
|
2068
|
-
mask_id: z.string().optional(),
|
|
2069
|
-
path: pathDataInputSchema.optional(),
|
|
2070
|
-
opacity: z.number().min(0).max(100).optional(),
|
|
2071
|
-
mode: z.enum(["add", "subtract"]).optional()
|
|
2038
|
+
layer_id: z.string().describe("Layer that owns the mask."),
|
|
2039
|
+
mask_id: z.string().describe("Mask id from the `create_mask` response. Omit to update the layer's FIRST mask — `read_scene` does not list mask ids, so on a single-mask layer omitting this is the normal path. Errors with \"mask not found\" if the id is not on this layer.").optional(),
|
|
2040
|
+
path: pathDataInputSchema.describe("Replacement mask outline, in LAYER-LOCAL coordinates.").optional(),
|
|
2041
|
+
opacity: z.number().min(0).max(100).describe("Mask opacity. For an ADD mask, 100 shows inside the outline and 0 makes the masked layer fully transparent. For a SUBTRACT mask, 100 fully cuts inside and 0 cuts nothing. Use `remove_mask` to restore unmasked rendering. Omit to leave unchanged.").optional(),
|
|
2042
|
+
mode: z.enum(["add", "subtract"]).describe("'add' = show ONLY the content inside the mask outline (the normal clip/reveal). 'subtract' = cut a HOLE — hide the content inside the outline and show everything outside it. Omit to leave the current mode unchanged.").optional()
|
|
2072
2043
|
}).strict()
|
|
2073
2044
|
},
|
|
2074
2045
|
remove_mask: {
|
|
2075
|
-
description: "Remove one mask from a layer. Pass mask_id when known; otherwise removes the first mask only.",
|
|
2046
|
+
description: "Remove one mask from a layer. Pass mask_id when known; otherwise removes the first mask only. Returns `{ layer_id, mask_id, removed: true }`.",
|
|
2076
2047
|
inputSchema: z.object({
|
|
2077
2048
|
layer_id: z.string(),
|
|
2078
2049
|
mask_id: z.string().optional()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lottiefiles/creator-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "MCP server for LottieFiles Creator",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"tsx": "4.21.0",
|
|
32
32
|
"typescript": "5.8.3",
|
|
33
33
|
"typescript-eslint": "8.29.0",
|
|
34
|
-
"vitest": "4.1.
|
|
35
|
-
"@lottiefiles/
|
|
36
|
-
"@lottiefiles/
|
|
34
|
+
"vitest": "4.1.10",
|
|
35
|
+
"@lottiefiles/creator-mcp-shared": "0.1.0",
|
|
36
|
+
"@lottiefiles/motion-copilot-contract": "0.2.1"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "tsdown",
|