@pinepaper.studio/mcp-server 1.6.19 → 1.6.21

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 (113) hide show
  1. package/README.de.md +1 -1
  2. package/README.es.md +1 -1
  3. package/README.fr.md +1 -1
  4. package/README.hi.md +1 -1
  5. package/README.ja.md +1 -1
  6. package/README.ko.md +1 -1
  7. package/README.md +161 -5
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/build-info.json +2 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +7 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  15. package/dist/ontology/upstream-vocabulary.js +3 -3
  16. package/dist/ontology/upstream-vocabulary.js.map +1 -1
  17. package/dist/ontology/vocabulary.d.ts.map +1 -1
  18. package/dist/ontology/vocabulary.js +9 -1
  19. package/dist/ontology/vocabulary.js.map +1 -1
  20. package/dist/tools/choreograph.d.ts +64 -0
  21. package/dist/tools/choreograph.d.ts.map +1 -0
  22. package/dist/tools/choreograph.js +346 -0
  23. package/dist/tools/choreograph.js.map +1 -0
  24. package/dist/tools/compact-descriptions.js +1 -1
  25. package/dist/tools/craft-tools.d.ts +55 -0
  26. package/dist/tools/craft-tools.d.ts.map +1 -0
  27. package/dist/tools/craft-tools.js +139 -0
  28. package/dist/tools/craft-tools.js.map +1 -0
  29. package/dist/tools/definitions.d.ts +10 -1
  30. package/dist/tools/definitions.d.ts.map +1 -1
  31. package/dist/tools/definitions.js +401 -32
  32. package/dist/tools/definitions.js.map +1 -1
  33. package/dist/tools/effect-check.d.ts +26 -0
  34. package/dist/tools/effect-check.d.ts.map +1 -0
  35. package/dist/tools/effect-check.js +129 -0
  36. package/dist/tools/effect-check.js.map +1 -0
  37. package/dist/tools/effect-class.d.ts +31 -0
  38. package/dist/tools/effect-class.d.ts.map +1 -0
  39. package/dist/tools/effect-class.js +176 -0
  40. package/dist/tools/effect-class.js.map +1 -0
  41. package/dist/tools/engine-lists.d.ts +6 -6
  42. package/dist/tools/engine-lists.d.ts.map +1 -1
  43. package/dist/tools/engine-lists.js +11 -9
  44. package/dist/tools/engine-lists.js.map +1 -1
  45. package/dist/tools/engine-methods.d.ts.map +1 -1
  46. package/dist/tools/engine-methods.js +12 -0
  47. package/dist/tools/engine-methods.js.map +1 -1
  48. package/dist/tools/engine-surface.d.ts +1 -1
  49. package/dist/tools/engine-surface.d.ts.map +1 -1
  50. package/dist/tools/engine-surface.js +221 -10
  51. package/dist/tools/engine-surface.js.map +1 -1
  52. package/dist/tools/handlers/export.d.ts.map +1 -1
  53. package/dist/tools/handlers/export.js +8 -1
  54. package/dist/tools/handlers/export.js.map +1 -1
  55. package/dist/tools/handlers/generate.d.ts +28 -28
  56. package/dist/tools/handlers.d.ts +23 -0
  57. package/dist/tools/handlers.d.ts.map +1 -1
  58. package/dist/tools/handlers.js +301 -6
  59. package/dist/tools/handlers.js.map +1 -1
  60. package/dist/tools/index.d.ts +2 -2
  61. package/dist/tools/index.d.ts.map +1 -1
  62. package/dist/tools/index.js +2 -2
  63. package/dist/tools/index.js.map +1 -1
  64. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  65. package/dist/tools/minimal-descriptions.js +16 -3
  66. package/dist/tools/minimal-descriptions.js.map +1 -1
  67. package/dist/tools/shape-params.d.ts +2 -2
  68. package/dist/tools/shape-params.d.ts.map +1 -1
  69. package/dist/tools/shape-params.js +25 -2
  70. package/dist/tools/shape-params.js.map +1 -1
  71. package/dist/tools/stick-vocabulary.d.ts +1 -1
  72. package/dist/tools/stick-vocabulary.js +1 -1
  73. package/dist/tools/toolkits.d.ts.map +1 -1
  74. package/dist/tools/toolkits.js +15 -0
  75. package/dist/tools/toolkits.js.map +1 -1
  76. package/dist/types/code-generator.d.ts +44 -1
  77. package/dist/types/code-generator.d.ts.map +1 -1
  78. package/dist/types/code-generator.js +1140 -128
  79. package/dist/types/code-generator.js.map +1 -1
  80. package/dist/types/schemas.d.ts +3091 -584
  81. package/dist/types/schemas.d.ts.map +1 -1
  82. package/dist/types/schemas.js +308 -6
  83. package/dist/types/schemas.js.map +1 -1
  84. package/dist/vendor/design/design-style.d.ts +2 -2
  85. package/dist/vendor/design/design-style.d.ts.map +1 -1
  86. package/dist/vendor/design/design-style.js +7 -2
  87. package/dist/vendor/design/design-style.js.map +1 -1
  88. package/dist/vendor/design/subdivision-tool-schema.d.ts +16 -0
  89. package/dist/vendor/design/subdivision-tool-schema.d.ts.map +1 -0
  90. package/dist/vendor/design/subdivision-tool-schema.js +104 -0
  91. package/dist/vendor/design/subdivision-tool-schema.js.map +1 -0
  92. package/dist/vendor/design/textile-tool-schema.d.ts +14 -0
  93. package/dist/vendor/design/textile-tool-schema.d.ts.map +1 -0
  94. package/dist/vendor/design/textile-tool-schema.js +195 -0
  95. package/dist/vendor/design/textile-tool-schema.js.map +1 -0
  96. package/dist/vendor/design/ui-kit-runtime.d.ts +49 -0
  97. package/dist/vendor/design/ui-kit-runtime.d.ts.map +1 -0
  98. package/dist/vendor/design/ui-kit-runtime.js +761 -0
  99. package/dist/vendor/design/ui-kit-runtime.js.map +1 -0
  100. package/dist/vendor/design/ui-kit-tool-schema.d.ts +280 -0
  101. package/dist/vendor/design/ui-kit-tool-schema.d.ts.map +1 -0
  102. package/dist/vendor/design/ui-kit-tool-schema.js +209 -0
  103. package/dist/vendor/design/ui-kit-tool-schema.js.map +1 -0
  104. package/manifest.json +53 -1
  105. package/package.json +5 -4
  106. package/dist/assets/repositories/openclipart.d.ts +0 -41
  107. package/dist/assets/repositories/openclipart.d.ts.map +0 -1
  108. package/dist/assets/repositories/openclipart.js +0 -139
  109. package/dist/assets/repositories/openclipart.js.map +0 -1
  110. package/dist/assets/repositories/svgrepo.d.ts +0 -61
  111. package/dist/assets/repositories/svgrepo.d.ts.map +0 -1
  112. package/dist/assets/repositories/svgrepo.js +0 -276
  113. package/dist/assets/repositories/svgrepo.js.map +0 -1
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { COMPACT_DESCRIPTIONS } from './compact-descriptions.js';
11
11
  import { MINIMAL_DESCRIPTIONS } from './minimal-descriptions.js';
12
+ import { craftToolDefinitions } from './craft-tools.js';
12
13
  import { STICK_GAITS, STICK_POSES, STICK_SEQUENCES, STICK_EXPRESSIONS, STICK_PROPS } from './stick-vocabulary.js';
13
14
  import { AgentBatchOperationTypeSchema, GeneratorNameSchema, ItemTypeSchema } from '../types/schemas.js';
14
15
  import { DESIGN_MEDIA, DESIGN_MEDIA_APPLY, FILTER_TYPES, FILTER_DOCS, LINEAGE_KINDS, STYLED_SCENE_STYLES, STYLED_SCENE_STYLE_INFO } from './engine-lists.js';
@@ -27,9 +28,44 @@ const SIMPLE_ANIMATION_TYPES_LIST = SimpleAnimationTypeSchema.options.join(', ')
27
28
  // (round 8 DD, 1.68) — derived now, so it cannot say what the schema refuses.
28
29
  const KEYFRAME_EASINGS_LIST = EasingSchema.options.join(', ');
29
30
  const EASING_LINES = KEYFRAME_EASINGS.map((e) => `- ${e}: ${EASING_DESCRIPTIONS[e]}`).join('\n');
30
- // =============================================================================
31
- // AI AGENT GUIDE (exported for pinepaper_tool_guide)
32
- // =============================================================================
31
+ const DOORS = [
32
+ { when: 'A scene or landscape drawn in an art style (cut paper, ink, watercolour, dither, flow, ASCII)', doors: [{ tool: 'pinepaper_styled_scene', text: 'pinepaper_styled_scene' }] },
33
+ { when: 'Make THIS — an item, a character, the whole scene — look like a style (8-bit, blueprint, ink sketch, flat modern, print halftone, paper cutout…)', doors: [{ tool: 'pinepaper_look', text: 'pinepaper_look (describe first: the engine grades each look native / faithful / impression)' }] },
34
+ { when: 'A story, several beats, a narrative from prose — a video or film in N parts, or a click-through widget', doors: [{ tool: 'pinepaper_story', text: "pinepaper_story (from_text; nav: 'scenes' for a VIDEO — a scene chain that cuts — 'panels' for a widget)" }, { tool: 'pinepaper_scene_playback', text: 'scene chains (pinepaper_scene_playback: cutSound, play)' }] },
35
+ { when: 'An ORIGINAL character from a description (a hero with a cape, a round robot, a cat in glasses), or a cast of variants', doors: [{ tool: 'pinepaper_original_character', text: 'pinepaper_original_character (describe first, then create)' }] },
36
+ { when: 'A character that acts (expressions, poses, walks)', doors: [{ tool: 'pinepaper_character', text: 'pinepaper_character' }, { tool: 'pinepaper_stick', text: 'pinepaper_stick' }, { tool: 'pinepaper_rigging', text: 'pinepaper_rigging' }, { tool: 'pinepaper_deform', text: 'squash and stretch via pinepaper_deform' }] },
37
+ { when: 'A number that counts up (a KPI roll-up, a year counter, a timer)', doors: [{ tool: 'pinepaper_create_item', text: 'create_item text with counter: exact on every frame, steady width; year counters: counter {from: -30000, to: 2026, era: true}' }] },
38
+ { when: 'Any item that moves with intent (pops in, hops, bounces, rolls, flies, peeks, lands)', doors: [{ tool: 'pinepaper_choreograph', text: 'pinepaper_choreograph: beats, not hand-timed keyframes' }] },
39
+ { when: 'One element that becomes the next thing, and the next, with no cuts (a pill → a field → a dot → a card)', doors: [{ tool: 'pinepaper_morph_sequence', text: 'pinepaper_morph_sequence' }] },
40
+ { when: 'A title or caption with craft', doors: [{ tool: 'pinepaper_text_style', text: 'pinepaper_text_style (display styles; cursive with animate writes itself on)' }, { tool: 'pinepaper_text_effect', text: 'pinepaper_text_effect' }] },
41
+ { when: 'A poster or layout in an aesthetic, or a collage', doors: [{ tool: 'pinepaper_design_system', text: 'pinepaper_design_system (compose)' }, { tool: 'pinepaper_compose', text: 'a collage: pinepaper_compose' }] },
42
+ { when: 'A painted, stitched, hatched, inked or cut-paper look on shapes or photos', doors: [{ tool: 'pinepaper_design_medium', text: 'pinepaper_design_medium' }, { tool: 'pinepaper_image_filter', text: 'photos also pinepaper_image_filter (watercolor, painterly)' }] },
43
+ { when: 'A brand', doors: [{ tool: 'pinepaper_brand_kit', text: 'pinepaper_brand_kit set (first): every item made afterwards inherits it' }] },
44
+ { when: 'Sound and music', doors: [{ tool: 'pinepaper_sound', text: 'pinepaper_sound' }, { tool: 'pinepaper_audio_beats', text: 'pinepaper_audio_beats' }, { tool: 'pinepaper_beat_cuts', text: 'pinepaper_beat_cuts' }] },
45
+ { when: 'Particles that must survive save, export and the cloud (confetti bursts, a trail), or custom per-frame drawing', doors: [{ tool: 'pinepaper_emitter', text: 'pinepaper_emitter' }, { tool: 'pinepaper_render_hook', text: 'custom drawing: pinepaper_render_hook' }] },
46
+ { when: 'A camera move through the piece, or 3D', doors: [{ tool: 'pinepaper_camera_director', text: 'pinepaper_camera_director' }, { tool: 'pinepaper_world3d', text: '3D: pinepaper_world3d' }, { tool: 'pinepaper_relight', text: 'pinepaper_relight' }] },
47
+ ];
48
+ /**
49
+ * The door block for a host's tool list (D72; cloud request from a3). A host
50
+ * that offers only some tools passes their names: a row keeps only the doors
51
+ * that host has, and a row with none is dropped — so the model is never told
52
+ * to "use pinepaper_X" for a tool it cannot see. With no list, every door.
53
+ */
54
+ export function buildDoorBlock(available) {
55
+ const has = available ? new Set(available) : null;
56
+ const rows = DOORS.map((d) => {
57
+ const doors = d.doors.filter((x) => !has || !x.tool || has.has(x.tool));
58
+ return doors.length ? `- ${d.when} → ${doors.map((x) => x.text).join('; ')}` : null;
59
+ }).filter(Boolean);
60
+ return [
61
+ 'CHOOSE THE HIGHEST-LEVEL TOOL FIRST. Primitive shapes over a generator backdrop with a pulse is the stock result users reject; reach for it only when nothing below fits the brief.',
62
+ ...rows,
63
+ 'pinepaper_agent_batch_execute then does the glue in ONE call: extra items, keyframes, relations, masks, effects, playback.',
64
+ 'If no tool reaches what the brief asks (for example, redrawing a subject in another art style), tell the user what is missing instead of handing over a lesser result as done.',
65
+ ].join('\n');
66
+ }
67
+ /** The full door block (every tool), for the guide and a local server. */
68
+ export const CHOOSE_THE_DOOR = buildDoorBlock();
33
69
  export const AI_AGENT_GUIDE = `⚠️ You are connected to PinePaper Studio via MCP tools. ALWAYS use these tools to create visual content.
34
70
  NEVER create standalone HTML pages, React components, or web apps as a substitute. Do NOT use frontend design skills or CSS-only animations instead of PinePaper.
35
71
  PinePaper tools ARE the implementation — they generate Paper.js/JavaScript code that runs on the canvas and produce Canvas 2D, SVG, MP4, GIF, PNG exports.
@@ -43,11 +79,14 @@ The generated code is shown in tool responses and can be copied into PinePaper's
43
79
 
44
80
  ⚠️ KNOWN CONFLICT: The "frontend-design" plugin instructs agents to generate HTML/CSS pages for visual content. This conflicts with PinePaper. If you keep generating HTML instead of calling PinePaper tools, the user should disable the frontend-design plugin.
45
81
 
82
+ ${CHOOSE_THE_DOOR}
83
+
46
84
  WORKFLOW — Create → Validate → Iterate:
47
85
 
48
86
  1. pinepaper_agent_start_job (clearCanvas: true, canvasPreset)
49
- 2. pinepaper_agent_batch_execute — ONE call with ALL operations:
50
- set_canvas_size → set_background / execute_generator → create items → animate / keyframe_animate → relation → apply_mask → apply_effect → play_timeline
87
+ 2. The craft tool(s) above that fit the brief, each called once.
88
+ Then pinepaper_agent_batch_execute — ONE call for the glue:
89
+ set_canvas_size → set_background → create items → keyframe_animate / animate → relation → apply_mask → apply_effect → play_timeline
51
90
  3. pinepaper_agent_end_job — returns screenshot
52
91
  4. Show screenshot to user for validation
53
92
  5. If user wants changes → modify specific items or start a new job
@@ -59,13 +98,13 @@ If you restart the pipeline or re-call batch_execute, you DOUBLE every item on t
59
98
  To fix mistakes: use modify/delete operations, or start a NEW job with clearCanvas: true.
60
99
 
61
100
  RULES:
62
- - NEVER restart the pipeline — one start_job, one batch_execute, one end_job
63
- - ALL operations go in batch_execute — canvas, background, items, animation, masks, effects, playback
101
+ - NEVER restart the pipeline — one start_job, one batch_execute, one end_job; the craft tools go between them
102
+ - Primitive operations go in the ONE batch_execute — canvas, background, items, animation, masks, effects, playback
64
103
  - EXCEPTION: Diagrams (flowcharts, UML) use pinepaper_create_diagram_shape + pinepaper_connect, NOT batch_execute
65
- - Use generators for rich backgrounds — the built-in templates are limited, generators create much better visuals
104
+ - A generator is a BACKDROP: under a composed scene it adds texture; on its own it is the stock look. Prefer a styled scene, a drawn world or a real photo for the ground of a piece
66
105
  - Use "$0", "$1" to reference items by creation order within the batch
67
106
 
68
- ─── BATCH OPERATIONS (12 types, in order) ───
107
+ ─── BATCH OPERATIONS (${AgentBatchOperationTypeSchema.options.length} types, in order) ───
69
108
 
70
109
  CANVAS: set_canvas_size (width/height or preset) → set_background (color) → execute_generator (procedural art)
71
110
  ITEMS: create (itemType, position, properties) → modify (itemId, properties) → delete (itemId) → group (itemIds → one entity)
@@ -73,9 +112,9 @@ ANIMATE: animate (loop presets) → keyframe_animate (timed reveals) → relatio
73
112
  EFFECTS: apply_mask (reveal animations) → apply_effect (sparkle/blast/bubbles/fireflies/trail...)
74
113
  PLAY: play_timeline (start playback)
75
114
 
76
- ─── GENERATORS (prefer these for backgrounds) ───
115
+ ─── GENERATORS (procedural backdrops — under a scene, not as one) ───
77
116
 
78
- Generators fill the canvas with procedural art — much richer than solid colors.
117
+ Generators fill the canvas with procedural art: richer than a flat colour as a backdrop, but not a composition by themselves.
79
118
  Choose based on mood:
80
119
  Dreamy/soft: drawBokeh, drawGradientMesh, drawOrganicFlow
81
120
  Techy/modern: drawCircuit, drawGrid, drawWindField
@@ -105,7 +144,7 @@ Shapes: circle, rectangle, star, ellipse, triangle, polygon, line, arc, path, pe
105
144
  Surfaces (render-time, evaluated per pixel — not shapes): shader, field
106
145
  shader: a lit surface — water, liquid_metal, heatmap, gem_smoke. Set behind:true to put it UNDER the art, which is what scenery wants.
107
146
  field: one expression, many marks — xExpr/yExpr/count, re-evaluated every frame against t.
108
- Both draw fully in a cloud render; locally they stand in as a flat plate.
147
+ Both draw in the studio (live, stills, every export) and in a cloud render.
109
148
  Text: text (content, fontSize, fontFamily, color, fontWeight)
110
149
  Dynamic text: set contentType on text items → clock (live time), timer (elapsed), countdown (from N seconds), stopwatch (pauseable)
111
150
  All items: opacity, shadowColor, shadowBlur, blendMode, strokeColor, strokeWidth
@@ -556,10 +595,13 @@ For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal
556
595
  inputSchema: {
557
596
  type: 'object',
558
597
  properties: {
598
+ screenSpace: { type: 'boolean', description: 'HUD: true keeps the item fixed in the FRAME under camera moves; its x/y, keyframes and relations are then frame coordinates. Exports (mp4, webm, gif, png, pdf) honour it; the live preview still tilts it; SVG and Lottie ignore it (agent_export warns). A studio without it refuses.' },
599
+ counter: { description: "TEXT only: a number that counts on the scene clock, exact on every exported frame. {from, to, start? (s), duration? (s, 1), easing? (engine name or [x1,y1,x2,y2]), decimals?, prefix?, suffix?, locale?, grouping?, padStart?, steps?, tabular? (true: steady width)}; null removes it. A year counter: {from: -30000, to: 2026, era: true} reads '30,000 BCE' … '2026 CE' (eraNegative / eraPositive relabel; no year 0); a KPI: {from: 1.67, to: 5.58, suffix: '%'}. A studio without counters refuses.", oneOf: [{ type: 'object', additionalProperties: true }, { type: 'null' }] },
600
+ stepTiming: { description: "Animate 'on twos': N or {every: N, baseFps?} (every N frames of a 30 fps base unless baseFps), {fps}, or null to turn off. A stepped group steps its children; follows, spring_follow, auras, rigging and GPU generators are not stepped. A studio without it refuses.", oneOf: [{ type: 'integer', minimum: 1 }, { type: 'object', properties: { every: { type: 'integer', minimum: 1 }, baseFps: { type: 'number' }, fps: { type: 'number' } } }, { type: 'null' }] },
559
601
  itemType: {
560
602
  type: 'string',
561
603
  enum: ['text', 'circle', 'star', 'rectangle', 'triangle', 'polygon', 'ellipse', 'path', 'line', 'arc', 'pentagon', 'hexagon', 'diamond', 'arrow', 'heart', 'disk', 'circle-outline', 'arrow-right', 'speech-bubble', 'speech-bubble-square', 'speech-bubble-pointed', 'thought-bubble', 'double-bubble', 'quote-bubble', 'comment-box', 'callout-box', 'shader', 'field'],
562
- description: "Type of item to create. 'shader' and 'field' are RENDER-TIME SURFACES drawn per pixel by the cloud renderer — locally they stand in as a flat plate. See the shader/field properties below.",
604
+ description: "Type of item to create. 'shader' and 'field' are RENDER-TIME SURFACES drawn per pixel, in the studio and in a cloud render. See the shader/field properties below.",
563
605
  },
564
606
  position: {
565
607
  oneOf: [
@@ -586,14 +628,15 @@ For glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal
586
628
 
587
629
  LIFETIME — on ANY item, and the only way to build a piece that CUTS between shots:
588
630
  - bornAt: seconds. The item does not exist before this instant.
589
- - ttl: seconds it lives for. Omit for "until the end".
631
+ - ttl: seconds it lives for. Omit for "until the end". A numeric lifetime is
632
+ read as ttl, except on an emitter, where lifetime is each particle's life.
590
633
  Without these every item is on screen for the whole render, which is what makes
591
634
  a long piece read as one crowded frame instead of a sequence of shots.
592
635
  They set the item's VISIBILITY for [bornAt, bornAt + ttl) — not its opacity —
593
636
  so they compose with any keyframes, opacity fades included. Re-timing with
594
637
  pinepaper_modify_item {bornAt, ttl} replaces the previous lifetime — pass both.
595
- The result's lifetime field says whether it was applied; it is runtime-only
596
- until the studio reads bornAt / ttl itself, so re-time a reopened scene.
638
+ The result's lifetime field says whether it was applied, and persists: true
639
+ when it is saved with the scene; without that, re-time a reopened scene.
597
640
 
598
641
  SHADER (itemType 'shader') — a lit surface, drawn per pixel:
599
642
  - shader: 'water' (open sea, lit swell) | 'liquid_metal' (chrome) | 'heatmap' (thermal halo) | 'gem_smoke' (smoke) | 'electric_arc' (branching discharge, stepped flicker) | 'vortex' (hollow bright RING, sheared by differential rotation) | 'rain_veil' | 'caustics' | 'ink_bleed'
@@ -714,6 +757,9 @@ Use the collage group's id for palette/collageStyle; use a child letter's id for
714
757
  inputSchema: {
715
758
  type: 'object',
716
759
  properties: {
760
+ screenSpace: { type: 'boolean', description: 'HUD: true keeps the item fixed in the FRAME under camera moves; its x/y, keyframes and relations are then frame coordinates. Exports (mp4, webm, gif, png, pdf) honour it; the live preview still tilts it; SVG and Lottie ignore it (agent_export warns). A studio without it refuses.' },
761
+ counter: { description: "TEXT only: a number that counts on the scene clock, exact on every exported frame. {from, to, start? (s), duration? (s, 1), easing? (engine name or [x1,y1,x2,y2]), decimals?, prefix?, suffix?, locale?, grouping?, padStart?, steps?, tabular? (true: steady width)}; null removes it. A year counter: {from: -30000, to: 2026, era: true} reads '30,000 BCE' … '2026 CE' (eraNegative / eraPositive relabel; no year 0); a KPI: {from: 1.67, to: 5.58, suffix: '%'}. A studio without counters refuses.", oneOf: [{ type: 'object', additionalProperties: true }, { type: 'null' }] },
762
+ stepTiming: { description: "Animate 'on twos': N or {every: N, baseFps?} (every N frames of a 30 fps base unless baseFps), {fps}, or null to turn off. A stepped group steps its children; follows, spring_follow, auras, rigging and GPU generators are not stepped. A studio without it refuses.", oneOf: [{ type: 'integer', minimum: 1 }, { type: 'object', properties: { every: { type: 'integer', minimum: 1 }, baseFps: { type: 'number' }, fps: { type: 'number' } } }, { type: 'null' }] },
717
763
  itemId: {
718
764
  type: 'string',
719
765
  description: 'Registry ID of the item (e.g., "item_1")',
@@ -1073,7 +1119,7 @@ EXAMPLE (sequenceDiagram):
1073
1119
  "mermaidText": "sequenceDiagram\\n Alice->>Bob: Hello\\n Bob-->>Alice: Hi back\\n Note right of Bob: Thinking..."
1074
1120
  }
1075
1121
 
1076
- Returns { success, nodeCount, edgeCount, nodes, edges, errors }. The created shapes participate in the relation/animation systems just like natively created items — you can grab their ids from \`nodes\` and feed them into pinepaper_add_relation, pinepaper_animate, etc.
1122
+ Returns { success, nodeCount, edgeCount, nodeItemIds, nodeIdMap?, edgeIds?, nodes, edges, errors }. nodeItemIds are the CANVAS ids of the shapes (item_N): pass THOSE to pinepaper_auto_layout, pinepaper_animate, pinepaper_add_relation. The mermaid ids (A, B…) in edges[].from/to name nodes in the source text and resolve to nothing on the canvas; nodeIdMap (where the studio returns it) maps one to the other, and edgeIds are the connectors' canvas ids.
1077
1123
 
1078
1124
  NOTE: Import-only — there is no MCP equivalent of FxTool's exportMermaid. Mermaid lacks animation primitives, so a round-trip would silently drop animations.`,
1079
1125
  inputSchema: {
@@ -1635,6 +1681,7 @@ RETURNS: create → { success, action, eventId }; pulse → { success, action, e
1635
1681
  inputSchema: {
1636
1682
  type: 'object',
1637
1683
  properties: {
1684
+ at: { description: 'create: fire this event at these scene times (seconds) — a timed trigger that runs on the timeline and in video exports. The widget player does not run timed triggers.', oneOf: [{ type: 'number' }, { type: 'array', items: { type: 'number' } }] },
1638
1685
  action: { type: 'string', enum: ['create', 'pulse'], description: "'create' a pp:event · 'pulse' it to fire" },
1639
1686
  name: { type: 'string', description: "Event name — required for action 'create'" },
1640
1687
  payloadType: { type: 'string', description: "Payload type label (default 'Pulse') — action 'create'" },
@@ -2355,7 +2402,7 @@ READ THIS BEFORE PROMISING A MEDIUM. Naming a medium does not create its marks.
2355
2402
  Call resolve before telling a user you will render in a medium. "yes, but as an impression" is the true answer for half of them, and a flat approximation misrepresents a medium rather than approximating it.
2356
2403
 
2357
2404
  ACTIONS:
2358
- - apply: { itemId, medium, options? } — render an item in ANY medium the engine can make: ${Object.keys(DESIGN_MEDIA_APPLY).join(', ')}. One engine entry point; a medium this studio cannot apply is refused with the reason: pass it on. ink / charcoal (where the studio applies them) restyle a path, text, or group of them IN PLACE — same itemId, keyframes and relations kept — with a boiling hand-drawn line; raster and video exports boil, SVG/Lottie get the colour and width only. Their options: color, amplitude (px), wavelength (px), fps (boil rate, 12), seed, boil (default true), width (px, paths), keepColor. watercolor options: color, brush, water 0..1, opacity, edge 0..1 (the dark rim), angle, bleed, seed. oil (stylised): color, brush, length, density, toneRange, relief, angle, seed. cutPaper (vector: survives SVG/PDF/video): color, facet, jitter, shadow, shadowColor, shadowOpacity, shadowOffset [dx, dy], seed. The result replaces the source in the stacking order and hides it; applying again replaces; one undo step.
2405
+ - apply: { itemId, medium, options? } — on a PHOTO (an image item), watercolor and oil run as the watercolor / painterly image filters and report fidelity 'stylised' (an image filter, not paint simulated over geometry); other media are refused for photos, with the reason. On shapes — render an item in ANY medium the engine can make: ${Object.keys(DESIGN_MEDIA_APPLY).join(', ')}. One engine entry point; a medium this studio cannot apply is refused with the reason: pass it on. ink / charcoal (where the studio applies them) restyle a path, text, or group of them IN PLACE — same itemId, keyframes and relations kept — with a boiling hand-drawn line; raster and video exports boil, SVG/Lottie get the colour and width only. Their options: color, amplitude (px), wavelength (px), fps (boil rate, 12), seed, boil (default true), width (px, paths), keepColor. watercolor options: color, brush, water 0..1, opacity, edge 0..1 (the dark rim), angle, bleed, seed. oil (stylised): color, brush, length, density, toneRange, relief, angle, seed. cutPaper (vector: survives SVG/PDF/video): color, facet, jitter, shadow, shadowColor, shadowOpacity, shadowOffset [dx, dy], seed. The result replaces the source in the stacking order and hides it; applying again replaces; one undo step.
2359
2406
  - list_media: every medium with fidelity, definition and limitation. Read it rather than assuming a fixed set — the catalogue grows (hatch and watercolor are recent additions).
2360
2407
  - resolve: { medium } → { ok, fidelity, reason, markMaker }. ok:false with a reason is a real answer — pass it on rather than trying anyway.
2361
2408
  - list_stitches: the thread stitches with their params.
@@ -2771,7 +2818,9 @@ The geometry is vendored from mcp-cloud, which makes this a three-repo artifact:
2771
2818
  idempotentHint: false,
2772
2819
  openWorldHint: false,
2773
2820
  },
2774
- description: `A piece of prose becomes a scene.
2821
+ description: `FORM — nav: 'scenes' is the VIDEO story: one scene per part on a scene chain that cuts in time (export as MP4); each part's length follows its role (hook 2.5 s, stat 3 s, section 3.5 s, quote 3 s, exit 3 s; 0.4 s transitions), and the reply carries the cut times. nav: 'panels' (the default) is the WIDGET story: tabs a viewer clicks through (export_widget_html). A video brief needs 'scenes'.
2822
+
2823
+ A piece of prose becomes a scene.
2775
2824
 
2776
2825
  - distill: reduce an article to its story beats and draw NOTHING. This is the half worth having on its own — a caller can read what the distiller made of the text, edit the beats, and only then assemble. Discovering the reading by looking at a finished scene is the expensive way round.
2777
2826
  - from_text: distill and assemble in one call, when the reading does not need checking.
@@ -2780,6 +2829,7 @@ The geometry is vendored from mcp-cloud, which makes this a three-repo artifact:
2780
2829
  inputSchema: {
2781
2830
  type: 'object',
2782
2831
  properties: {
2832
+ nav: { type: 'string', enum: ['panels', 'scenes'], description: "'scenes' = the video form (a scene chain that cuts); 'panels' = the widget form (default)" },
2783
2833
  action: { type: 'string', enum: ['distill', 'from_text', 'apply_spec', 'plan_book'], description: 'Which story operation.' },
2784
2834
  text: { type: 'string', description: 'distill / from_text: the prose.' },
2785
2835
  spec: { type: 'object', description: 'apply_spec: the story spec, usually a distilled one you edited.' },
@@ -3440,6 +3490,7 @@ An event channel (pinepaper_event) is the hub: something FIRES it, and reactions
3440
3490
  · on_event_fire_after (params: delay, timeline) — chain one beat to the next.
3441
3491
  · on_event_fire_if — the only reaction that TESTS graph state rather than mutating it; params.predicate is an ExpressionIR program.
3442
3492
  · on_event_add_relation / on_event_remove_relation — the scene rewires itself.
3493
+ · on_event_play_sound (source = the EVENT, target null; params: sound, gain?, note?) — play a sound when the event fires: a catalogue name (effect, drum or instrument; aliases accepted) or a define_sfx spec; gain is a multiplier capped at full scale; note is for instruments (default C5). An unknown sound is refused with the catalogue. Wire a click sound as on_click_fire (button → event) + on_event_play_sound (event → null). For a sound at a TIME, pinepaper_event create { at: seconds | [seconds] } fires the event on the timeline (and in video exports); a scene chain's cutSound puts one on every cut. LIMITS: the exported widget player does not run timed triggers, and a browser may block sound until the viewer's first click or key.
3443
3494
  · on_event_store_set / on_event_store_increment / restores_from — persistent state. The target is a store node whose id is the storage SCOPE, so two stores in a scene and two widgets on a page do not collide. restores_from is the read half and fires ONCE at load.
3444
3495
  - Proximity (pointer over an item): on_enter_* / on_exit_* mirror the on_event_* reactions above (set_color, set_property, set_property_from_template, set_visibility, set_data, increment, toggle) — hover states without a single line of script.
3445
3496
  - Exclusive state: exclusive_group makes at most one member active (on_event_set_active is how you activate one — tabs, steppers, quiz cards). menubar_group is the same grouping WITHOUT the mutex.
@@ -3509,7 +3560,7 @@ Relations are COMPOSITIONAL - an item can have multiple relations that work toge
3509
3560
  },
3510
3561
  relationType: {
3511
3562
  type: 'string',
3512
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', 'attracts', 'wiggle', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'construction_reveal', 'triggers_animation', 'syncs_with', 'tours', 'synced_to_audio', 'expresses', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'driven_by', 'time_expression', 'on_click_fire', 'on_pointer_enter_fire', 'on_pointer_exit_fire', 'on_key_fire', 'on_event_fire_after', 'on_event_fire_if', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_property_from_template', 'on_event_set_visibility', 'on_event_set_active', 'on_event_set_data', 'on_event_increment', 'on_event_toggle', 'on_event_store_set', 'on_event_store_increment', 'on_enter_set_color', 'on_enter_set_property', 'on_enter_set_property_from_template', 'on_enter_set_visibility', 'on_enter_set_data', 'on_enter_increment', 'on_enter_toggle', 'on_exit_set_color', 'on_exit_set_property', 'on_exit_set_property_from_template', 'on_exit_set_visibility', 'on_exit_set_data', 'on_exit_increment', 'on_exit_toggle', 'exclusive_group', 'menubar_group', 'restores_from', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with', 'part_of', 'connects_to', 'attached_to_tail', 'head_points_to'],
3563
+ enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', 'attracts', 'wiggle', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'construction_reveal', 'triggers_animation', 'syncs_with', 'tours', 'synced_to_audio', 'expresses', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'driven_by', 'time_expression', 'on_click_fire', 'on_pointer_enter_fire', 'on_pointer_exit_fire', 'on_key_fire', 'on_event_fire_after', 'on_event_fire_if', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_property_from_template', 'on_event_set_visibility', 'on_event_set_active', 'on_event_set_data', 'on_event_increment', 'on_event_toggle', 'on_event_store_set', 'on_event_store_increment', 'on_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', 'on_enter_set_color', 'on_enter_set_property', 'on_enter_set_property_from_template', 'on_enter_set_visibility', 'on_enter_set_data', 'on_enter_increment', 'on_enter_toggle', 'on_exit_set_color', 'on_exit_set_property', 'on_exit_set_property_from_template', 'on_exit_set_visibility', 'on_exit_set_data', 'on_exit_increment', 'on_exit_toggle', 'exclusive_group', 'menubar_group', 'restores_from', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with', 'part_of', 'connects_to', 'attached_to_tail', 'head_points_to'],
3513
3564
  description: 'Type of relationship. A name registered with pinepaper_register_custom_relation in this session is accepted too.',
3514
3565
  },
3515
3566
  params: {
@@ -3552,7 +3603,7 @@ USE WHEN:
3552
3603
  targetId: { type: 'string', description: 'Target item ID' },
3553
3604
  relationType: {
3554
3605
  type: 'string',
3555
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', 'attracts', 'wiggle', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'construction_reveal', 'triggers_animation', 'syncs_with', 'tours', 'synced_to_audio', 'expresses', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'driven_by', 'time_expression', 'on_click_fire', 'on_pointer_enter_fire', 'on_pointer_exit_fire', 'on_key_fire', 'on_event_fire_after', 'on_event_fire_if', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_property_from_template', 'on_event_set_visibility', 'on_event_set_active', 'on_event_set_data', 'on_event_increment', 'on_event_toggle', 'on_event_store_set', 'on_event_store_increment', 'on_enter_set_color', 'on_enter_set_property', 'on_enter_set_property_from_template', 'on_enter_set_visibility', 'on_enter_set_data', 'on_enter_increment', 'on_enter_toggle', 'on_exit_set_color', 'on_exit_set_property', 'on_exit_set_property_from_template', 'on_exit_set_visibility', 'on_exit_set_data', 'on_exit_increment', 'on_exit_toggle', 'exclusive_group', 'menubar_group', 'restores_from', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with', 'part_of', 'connects_to', 'attached_to_tail', 'head_points_to'],
3606
+ enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', 'attracts', 'wiggle', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'construction_reveal', 'triggers_animation', 'syncs_with', 'tours', 'synced_to_audio', 'expresses', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'driven_by', 'time_expression', 'on_click_fire', 'on_pointer_enter_fire', 'on_pointer_exit_fire', 'on_key_fire', 'on_event_fire_after', 'on_event_fire_if', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_property_from_template', 'on_event_set_visibility', 'on_event_set_active', 'on_event_set_data', 'on_event_increment', 'on_event_toggle', 'on_event_store_set', 'on_event_store_increment', 'on_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', 'on_enter_set_color', 'on_enter_set_property', 'on_enter_set_property_from_template', 'on_enter_set_visibility', 'on_enter_set_data', 'on_enter_increment', 'on_enter_toggle', 'on_exit_set_color', 'on_exit_set_property', 'on_exit_set_property_from_template', 'on_exit_set_visibility', 'on_exit_set_data', 'on_exit_increment', 'on_exit_toggle', 'exclusive_group', 'menubar_group', 'restores_from', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with', 'part_of', 'connects_to', 'attached_to_tail', 'head_points_to'],
3556
3607
  description: 'Specific relation type to remove (optional - removes all if not specified)',
3557
3608
  },
3558
3609
  },
@@ -3580,7 +3631,7 @@ USE WHEN:
3580
3631
  itemId: { type: 'string', description: 'Item to query relations for' },
3581
3632
  relationType: {
3582
3633
  type: 'string',
3583
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', 'attracts', 'wiggle', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'construction_reveal', 'triggers_animation', 'syncs_with', 'tours', 'synced_to_audio', 'expresses', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'driven_by', 'time_expression', 'on_click_fire', 'on_pointer_enter_fire', 'on_pointer_exit_fire', 'on_key_fire', 'on_event_fire_after', 'on_event_fire_if', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_property_from_template', 'on_event_set_visibility', 'on_event_set_active', 'on_event_set_data', 'on_event_increment', 'on_event_toggle', 'on_event_store_set', 'on_event_store_increment', 'on_enter_set_color', 'on_enter_set_property', 'on_enter_set_property_from_template', 'on_enter_set_visibility', 'on_enter_set_data', 'on_enter_increment', 'on_enter_toggle', 'on_exit_set_color', 'on_exit_set_property', 'on_exit_set_property_from_template', 'on_exit_set_visibility', 'on_exit_set_data', 'on_exit_increment', 'on_exit_toggle', 'exclusive_group', 'menubar_group', 'restores_from', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with', 'part_of', 'connects_to', 'attached_to_tail', 'head_points_to'],
3634
+ enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', 'attracts', 'wiggle', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'construction_reveal', 'triggers_animation', 'syncs_with', 'tours', 'synced_to_audio', 'expresses', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'driven_by', 'time_expression', 'on_click_fire', 'on_pointer_enter_fire', 'on_pointer_exit_fire', 'on_key_fire', 'on_event_fire_after', 'on_event_fire_if', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_property_from_template', 'on_event_set_visibility', 'on_event_set_active', 'on_event_set_data', 'on_event_increment', 'on_event_toggle', 'on_event_store_set', 'on_event_store_increment', 'on_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', 'on_enter_set_color', 'on_enter_set_property', 'on_enter_set_property_from_template', 'on_enter_set_visibility', 'on_enter_set_data', 'on_enter_increment', 'on_enter_toggle', 'on_exit_set_color', 'on_exit_set_property', 'on_exit_set_property_from_template', 'on_exit_set_visibility', 'on_exit_set_data', 'on_exit_increment', 'on_exit_toggle', 'exclusive_group', 'menubar_group', 'restores_from', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with', 'part_of', 'connects_to', 'attached_to_tail', 'head_points_to'],
3584
3635
  description: 'Filter by relation type (optional)',
3585
3636
  },
3586
3637
  direction: {
@@ -3844,6 +3895,10 @@ EASING — IT SHAPES THE SEGMENT ARRIVING AT THE KEYFRAME IT IS ON.
3844
3895
  The move from keyframe A to keyframe B is eased by B's easing, not A's. Easing on the FIRST keyframe does nothing (no segment arrives at it) — a Ken Burns with easing only on its first key plays linear. Put the easing on the key you are moving TO.
3845
3896
  ${EASING_LINES}
3846
3897
  - or a custom cubic-bezier [x1, y1, x2, y2] — x in 0..1, y may exceed 0..1 for overshoot. keyframe_animate and camera_animate only.
3898
+ COUNTERS: a text item with a counter (create_item / modify_item counter) also animates the numeric property counterValue, drawn through the counter's format — keyframe it to shape a count by hand (hold on a number, then race). Its keys override the counter's own from / to.
3899
+
3900
+ HOLD KEYS (where the studio has them): interpolation: 'hold' on a key holds ITS value until the next key, then cuts — no in-between. Easing 'hold' / 'step' on the ARRIVING key does the same. A studio without hold keys refuses rather than interpolate; there, two keys a frame apart make the cut. For a whole layer 'on twos', set stepTiming on the item (create_item / modify_item).
3901
+
3847
3902
  Every easing renders exactly in video, GIF and APNG. Lottie and CSS get the closest single Bézier: springPlayful loses its wobble, and the in-out quart/quint/expo/circ curves are within 2–4 %. SMIL is exact for springs and those four.
3848
3903
 
3849
3904
  CLIP WINDOW (optional — Premiere/Canva-style clip behavior):
@@ -4504,7 +4559,7 @@ SUPPORTED ANIMATIONS: bounce, breathe, fade, glow, jelly, path, pulse, rotate, s
4504
4559
  target: { type: 'string', description: 'Name of target item' },
4505
4560
  type: {
4506
4561
  type: 'string',
4507
- enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', 'attracts', 'wiggle', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'construction_reveal', 'triggers_animation', 'syncs_with', 'tours', 'synced_to_audio', 'expresses', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'driven_by', 'time_expression', 'on_click_fire', 'on_pointer_enter_fire', 'on_pointer_exit_fire', 'on_key_fire', 'on_event_fire_after', 'on_event_fire_if', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_property_from_template', 'on_event_set_visibility', 'on_event_set_active', 'on_event_set_data', 'on_event_increment', 'on_event_toggle', 'on_event_store_set', 'on_event_store_increment', 'on_enter_set_color', 'on_enter_set_property', 'on_enter_set_property_from_template', 'on_enter_set_visibility', 'on_enter_set_data', 'on_enter_increment', 'on_enter_toggle', 'on_exit_set_color', 'on_exit_set_property', 'on_exit_set_property_from_template', 'on_exit_set_visibility', 'on_exit_set_data', 'on_exit_increment', 'on_exit_toggle', 'exclusive_group', 'menubar_group', 'restores_from', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with', 'part_of', 'connects_to', 'attached_to_tail', 'head_points_to'],
4562
+ enum: ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to', 'anchored_in_world', 'spring_follow', 'repels', 'attracts', 'wiggle', 'animates', 'grows_from', 'staggered_with', 'indicates', 'circumscribes', 'wave_through', 'morphs_to', 'group_morphs_to', 'moves_along_path', 'construction_reveal', 'triggers_animation', 'syncs_with', 'tours', 'synced_to_audio', 'expresses', 'camera_follows', 'camera_animates', 'is_midpoint_of', 'lies_on_line', 'is_centroid_of', 'is_circumcenter_of', 'concentric_with', 'driven_by', 'time_expression', 'on_click_fire', 'on_pointer_enter_fire', 'on_pointer_exit_fire', 'on_key_fire', 'on_event_fire_after', 'on_event_fire_if', 'on_event_add_relation', 'on_event_remove_relation', 'on_event_set_color', 'on_event_set_property', 'on_event_set_property_from_template', 'on_event_set_visibility', 'on_event_set_active', 'on_event_set_data', 'on_event_increment', 'on_event_toggle', 'on_event_store_set', 'on_event_store_increment', 'on_event_play_sound', 'looks_at', 'reacts_to', 'drag_to_turn', 'on_enter_set_color', 'on_enter_set_property', 'on_enter_set_property_from_template', 'on_enter_set_visibility', 'on_enter_set_data', 'on_enter_increment', 'on_enter_toggle', 'on_exit_set_color', 'on_exit_set_property', 'on_exit_set_property_from_template', 'on_exit_set_visibility', 'on_exit_set_data', 'on_exit_increment', 'on_exit_toggle', 'exclusive_group', 'menubar_group', 'restores_from', 'on_top_of', 'below', 'beside', 'inside', 'centered_on', 'aligned_with', 'part_of', 'connects_to', 'attached_to_tail', 'head_points_to'],
4508
4563
  },
4509
4564
  params: {
4510
4565
  type: 'object',
@@ -4637,6 +4692,7 @@ EXAMPLE — Slideshow:
4637
4692
  inputSchema: {
4638
4693
  type: 'object',
4639
4694
  properties: {
4695
+ cutSound: { description: "create_chain: a sound at every cut — a catalogue name, or {sound, gain?, offset? (s)}. The scene_cut event also fires at each cut.", oneOf: [{ type: 'string' }, { type: 'object', additionalProperties: true }] },
4640
4696
  action: {
4641
4697
  type: 'string',
4642
4698
  enum: ['create_chain', 'play', 'pause', 'resume', 'stop', 'toggle_loop', 'jump'],
@@ -5185,6 +5241,8 @@ ACTIONS:
5185
5241
  - plan: Report what WOULD change (count, per-role breakdown, WCAG contrast audit) WITHOUT touching
5186
5242
  the canvas. Call this first on an unfamiliar scene; a whole-scene recolour is hard to eyeball afterwards.
5187
5243
  - apply: Commit it. A history snapshot is taken FIRST, so one undo puts everything back.
5244
+ - set: { kit } — apply it AND make it the ACTIVE brand for the rest of the piece: every shape created afterwards (create_item, batch create, agent_batch_execute) takes colors.primary, and every text colors.text with the heading font from 32 px up (body below), wherever the call names none. Pass noBrand: true in an item's properties to opt it out. Use set at the start of a multi-beat piece so beat 5 matches beat 1. It lives with the studio session: set it again after a reload.
5245
+ - get: the active brand (or null). clear: stop inheriting (what is on the canvas keeps its colours).
5188
5246
  - from_url: { url, screenshots? } — read the product's REAL brand from its own site before animating anything: screenshots down the page, the logo (header image or SVG, else the site icon), and a proposed kit from the colours and fonts the site's CSS computes (buttons and links weigh most). Saved to files; the result lists them. Read in an isolated browser context: the studio is not touched. Review the kit against the screenshots, then plan / apply it. Use the brand you are given, never an invented one.
5189
5247
 
5190
5248
  KIT SHAPE — only name and colors.primary are required:
@@ -5200,7 +5258,7 @@ Both actions return a WCAG contrast audit, so you can see failing pairs before s
5200
5258
  inputSchema: {
5201
5259
  type: 'object',
5202
5260
  properties: {
5203
- action: { type: 'string', enum: ['plan', 'apply', 'from_url'], description: 'plan (dry run), apply, or from_url (read a site into a proposed kit)' },
5261
+ action: { type: 'string', enum: ['plan', 'apply', 'from_url', 'set', 'get', 'clear'], description: 'plan (dry run), apply (once), set (apply + every new item inherits it), get, clear, or from_url (read a site into a proposed kit)' },
5204
5262
  url: { type: 'string', description: "from_url: the product's site, http(s)." },
5205
5263
  screenshots: { type: 'integer', minimum: 1, maximum: 6, description: 'from_url: screenshots down the page (default 3).' },
5206
5264
  kit: {
@@ -5497,7 +5555,7 @@ ACTIONS:
5497
5555
  idempotentHint: false,
5498
5556
  openWorldHint: false,
5499
5557
  },
5500
- description: `Apply GPU-accelerated image filters to raster items.
5558
+ description: `Apply image filters to raster items: a GPU set, plus painted watercolor / painterly looks for photos.
5501
5559
 
5502
5560
  ACTIONS:
5503
5561
  - apply: Apply a single filter. Params: itemId, filterName, params
@@ -5510,6 +5568,8 @@ Available filters (GPU raster set):
5510
5568
  - Stylize: vignette (intensity, radius), edgeDetect, halftoneDots (size, angle), halftoneCMYK (size), dither (levels), grain (amount, seed), scanlines (intensity, period), chromaticAberration (amount), blur (radius)
5511
5569
  - Light (additive): bloom (amount, threshold, radius), glow, halation (warm-fringed bloom; color), lightShafts (amount, threshold, decay, density, x, y, color — volumetric rays from a point)
5512
5570
  - Keying: lumaKey (threshold, softness, invert)
5571
+ - Pixel art: pixelate (size or cells across, levels, dither, palette [colours] — e.g. a PICO-8 set), where the studio has it
5572
+ - Painted (CPU, STILL images only, where the studio has them): watercolor (strength, seed, radius, edges, bleed, granulation, paper, washes — layered washes, pigment rims, paper grain), painterly (strength, seed, radius, stroke, texture, edges, vivid — brush strokes along the form). About 1 s per 1080p image; the same seed paints the same picture; values are clamped and unknown params are warned about. A video is refused (they cannot run per frame): apply to a still or a captured frame. A studio without them refuses with its list of filters.
5513
5573
  - SECOND-INPUT (params.map = another item's id, resolved to its pixels): displace (amount, dispersion, map, mapChannel — the map's brightness pushes pixels), refract (displace with per-channel dispersion — glass), trackMatte (channel, invert, strength — prefer pinepaper_media apply_track_matte for the live version), datamosh (amount, block, map? — self-moshes without a map)
5514
5574
 
5515
5575
  RECIPE — grunge poster: chain [grain, scanlines, vignette]. Glass header: refract with map = a gradient item.
@@ -5869,6 +5929,109 @@ PHASES: sin, blink, linear, pingpong, once, elastic, heartbeat, stepped`,
5869
5929
  required: ['action'],
5870
5930
  },
5871
5931
  },
5932
+ {
5933
+ name: 'pinepaper_hand_drawn',
5934
+ annotations: {
5935
+ title: 'Hand-Drawn Line',
5936
+ readOnlyHint: false,
5937
+ destructiveHint: false,
5938
+ idempotentHint: false,
5939
+ openWorldHint: false,
5940
+ },
5941
+ description: `Give a path a hand-drawn look: a rough, bowed outline (sketchy), or a stroke that draws itself on (reveal).
5942
+
5943
+ USE WHEN:
5944
+ - "give it a hand-drawn / sketchy / rough outline"
5945
+ - "draw the line on like ink" (reveal)
5946
+
5947
+ STYLES:
5948
+ - 'sketchy' (default): one-time seeded jitter of the path's points and curve handles. Saved with the scene; exports as drawn.
5949
+ - 'reveal' (alias 'ink_reveal'): keyframes the item's stroke trimEnd 0 -> 1 over duration seconds, merged with any keyframes it already has. Needs itemId.
5950
+ - 'wobble', 'stop_motion': refused, nothing changed. A line that boils frame to frame in exports is pinepaper_design_medium medium 'ink'; the refusal returns that call, with fps as the boil rate.
5951
+
5952
+ PARAMETERS:
5953
+ - itemId: target item registry id (sketchy falls back to the selection)
5954
+ - style / mode: 'sketchy' | 'reveal' | 'ink_reveal' | 'wobble' | 'stop_motion'
5955
+ - roughness: point jitter in px (default 2)
5956
+ - bowing: curve handle jitter factor (default 1)
5957
+ - seed: same seed, same jitter (default 1)
5958
+ - duration: reveal seconds (default 2)
5959
+ - fps / posterizeFps: boil rate passed on in the wobble/stop_motion refusal (default 12)`,
5960
+ inputSchema: {
5961
+ type: 'object',
5962
+ properties: {
5963
+ itemId: { type: 'string', description: 'Target item registry ID (or current selection if omitted)' },
5964
+ style: {
5965
+ type: 'string',
5966
+ enum: ['sketchy', 'reveal', 'ink_reveal', 'wobble', 'stop_motion'],
5967
+ description: "Hand-drawn style (default: sketchy). wobble and stop_motion are refused with the pinepaper_design_medium call that does it",
5968
+ },
5969
+ mode: {
5970
+ type: 'string',
5971
+ enum: ['sketchy', 'reveal', 'ink_reveal', 'wobble', 'stop_motion'],
5972
+ description: 'Alias for style',
5973
+ },
5974
+ roughness: { type: 'number', description: 'Noise magnitude in pixels (default: 2)' },
5975
+ bowing: { type: 'number', description: 'Line bowing curve handle factor (default: 1)' },
5976
+ fps: { type: 'number', description: 'wobble/stop_motion: the boil rate handed to pinepaper_design_medium (default: 12)' },
5977
+ posterizeFps: { type: 'number', description: 'Alias for fps' },
5978
+ seed: { type: 'number', description: 'Random seed for deterministic vertex displacement (default: 1)' },
5979
+ duration: { type: 'number', description: 'reveal: seconds to draw the stroke on (default: 2)' },
5980
+ },
5981
+ },
5982
+ },
5983
+ {
5984
+ name: 'pinepaper_pen_figure',
5985
+ annotations: {
5986
+ title: 'Pen Figure',
5987
+ readOnlyHint: false,
5988
+ destructiveHint: false,
5989
+ idempotentHint: false,
5990
+ openWorldHint: false,
5991
+ },
5992
+ description: `Draw a mathematical figure with a pen: the tip travels the figure and the ink appears behind it, in the editor and in every export.
5993
+
5994
+ USE WHEN:
5995
+ - "draw a Lorenz attractor / Lissajous / Hilbert curve being drawn"
5996
+ - "a pen plotting a curve", "a harmonograph drawing itself"
5997
+
5998
+ The figures are the studio's catalogue of declarative maths (parametric and polar curves, ODE attractors, iterated maps, L-systems). action 'list' returns it: id, title, kind. A figure is fitted into its box by one uniform scale, so it is the same figure on any canvas size.
5999
+
6000
+ ACTIONS:
6001
+ - create (default): { figure, box?, margin?, duration?, delay?, loop?, pen?, strokeColor?, penColor?, strokeWidth? } → { inkId, penId, title, strokes }. The ink is a group, one child per pen stroke; the pen is a circle joined to it by the draws_figure relation. pen: false draws the ink complete, with no pen and no animation. An unknown figure is refused with the catalogue's ids.
6002
+ - list: the catalogue.
6003
+
6004
+ To draw a static curve from your own equation, use pinepaper_equation_path.
6005
+
6006
+ EXAMPLES:
6007
+ - { figure: 'lorenz', duration: 8 }
6008
+ - { figure: 'hilbert', box: { x: 100, y: 100, width: 600, height: 600 }, strokeColor: '#0f766e' }
6009
+ - { figure: 'harmonograph', pen: false }`,
6010
+ inputSchema: {
6011
+ type: 'object',
6012
+ properties: {
6013
+ action: { type: 'string', enum: ['create', 'list'], description: "'create' (default) draws a figure; 'list' returns the catalogue." },
6014
+ figure: {
6015
+ anyOf: [{ type: 'string' }, { type: 'object' }],
6016
+ description: "create: a catalogue id from action 'list' (e.g. 'lorenz', 'lissajous', 'hilbert', 'harmonograph'), or an inline figure spec object.",
6017
+ },
6018
+ box: {
6019
+ type: 'object',
6020
+ properties: { x: { type: 'number' }, y: { type: 'number' }, width: { type: 'number' }, height: { type: 'number' } },
6021
+ required: ['x', 'y', 'width', 'height'],
6022
+ description: 'Where to draw, in canvas px (default: the whole canvas).',
6023
+ },
6024
+ margin: { type: 'number', description: 'Fraction of the box left clear (default 0.06).' },
6025
+ duration: { type: 'number', description: 'Seconds the pen takes for the whole figure, pen-up travel included (default 6).' },
6026
+ delay: { type: 'number', description: 'Seconds before the pen starts (default 0).' },
6027
+ loop: { type: 'boolean', description: 'Draw the figure again after it finishes (default false: it stays drawn).' },
6028
+ pen: { type: 'boolean', description: 'false: the ink only, fully drawn, no animation (default true).' },
6029
+ strokeColor: { type: 'string', description: "Ink colour (default '#1e293b')." },
6030
+ penColor: { type: 'string', description: "Pen tip colour (default '#e11d48')." },
6031
+ strokeWidth: { type: 'number', description: 'Ink width in px (default 2).' },
6032
+ },
6033
+ },
6034
+ },
5872
6035
  {
5873
6036
  name: 'pinepaper_sprite_sheet',
5874
6037
  annotations: {
@@ -6506,7 +6669,7 @@ EXAMPLES:
6506
6669
  - Grid of icons: {layoutType: "grid", options: {columns: 4, cellWidth: 120}}
6507
6670
 
6508
6671
  PARTIAL LAYOUT:
6509
- Pass itemIds array to only layout specific items, leaving others in place.`,
6672
+ Pass itemIds array to only layout specific items, leaving others in place. They are CANVAS ids (item_N) — after pinepaper_import_mermaid, use its nodeItemIds, not the mermaid ids (A, B…), which lay out nothing.`,
6510
6673
  inputSchema: {
6511
6674
  type: 'object',
6512
6675
  properties: {
@@ -6518,7 +6681,7 @@ Pass itemIds array to only layout specific items, leaving others in place.`,
6518
6681
  itemIds: {
6519
6682
  type: 'array',
6520
6683
  items: { type: 'string' },
6521
- description: 'Specific items to layout (default: all diagram items)',
6684
+ description: 'Canvas item ids (item_N) to layout (default: all diagram items). After import_mermaid, its nodeItemIds.',
6522
6685
  },
6523
6686
  options: {
6524
6687
  type: 'object',
@@ -7396,6 +7559,8 @@ USE WHEN:
7396
7559
  },
7397
7560
  description: `Apply a visual effect to an item.
7398
7561
 
7562
+ EFFECT OR EMITTER: apply_effect is a live look on an item. For particles a saved scene, a template, the cloud render and every export must reproduce frame for frame — confetti at 2 s, a trail behind a moving item — use pinepaper_emitter: its particles are a pure function of its spec and the scene time.
7563
+
7399
7564
  USE WHEN:
7400
7565
  - Adding sparkle/glitter effects
7401
7566
  - Creating burst/explosion, fire, smoke effects
@@ -7708,6 +7873,205 @@ EXAMPLE: { style: 'ink', duration: 6 }`,
7708
7873
  },
7709
7874
  },
7710
7875
  },
7876
+ {
7877
+ name: 'pinepaper_look',
7878
+ annotations: { title: 'Look', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
7879
+ description: `Restyle what is already drawn — one item, a group and everything in it, or the WHOLE SCENE — as a named look: palette remapped in place (colour keyframes too), strokes, type, image treatment, texture, background and a framing device. Ids, keyframes and relations are kept; it survives a project round trip; remove restores it pixel for pixel. This is "the same subject, in style X".
7880
+
7881
+ ACTIONS:
7882
+ - list: every look with its FIDELITY — the engine's own grade: native (the medium itself), faithful (close, its approximations named), impression (an evocation, not the real style).
7883
+ - describe: { look } — the recipe, its fidelity, what it approximates, and the engine call per part. Read it before promising a look.
7884
+ - apply: { look, target? ('scene' default | an itemId), scope? ('item' | 'subtree' | 'scene') } → what it restyled, and the fidelity. An impression comes with a warning — present it as an approximation, not the finished style; a faithful look lists what it approximates.
7885
+ - remove: { target } — restore what was there. get: { target } — the look on it now.
7886
+ - grid: { looks?, cols?, cellWidth?, t? } — a contact sheet of the scene in several looks, to choose from: the PNG is the reply's \`data\` (a data URL, as agent_export returns it), with cells [{ id, name, fidelity }].
7887
+
7888
+ look is an id from list, or a look spec. An unknown look is refused with the known ones.
7889
+ Looks act on what is THERE: they restyle a drawing, they do not redraw a subject from scratch in another medium.`,
7890
+ inputSchema: {
7891
+ type: 'object',
7892
+ properties: {
7893
+ action: { type: 'string', enum: ['list', 'describe', 'apply', 'remove', 'get', 'grid'] },
7894
+ look: { description: 'a look id, or a look spec', oneOf: [{ type: 'string' }, { type: 'object', additionalProperties: true }] },
7895
+ target: { type: 'string', description: "an itemId, or 'scene' (default)" },
7896
+ scope: { type: 'string', enum: ['item', 'subtree', 'scene'] },
7897
+ looks: { type: 'array', items: { type: 'string' } },
7898
+ cols: { type: 'integer' }, cellWidth: { type: 'number' }, t: { type: 'number' },
7899
+ },
7900
+ required: ['action'],
7901
+ },
7902
+ },
7903
+ {
7904
+ name: 'pinepaper_original_character',
7905
+ annotations: { title: 'Original Character', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
7906
+ description: `Make an ORIGINAL character from a base rig and a declarative variant — no drawing code, and nothing anyone else owns. Every variant keeps its base's parts and rig, so expressions, walks, poses and pinepaper_choreograph (which drives its root) all work on it.
7907
+
7908
+ ACTIONS:
7909
+ - bases: the bases (person, robot, cat, bird, blob) with their roles.
7910
+ - describe: { base? } — every legal palette key, feature value, accessory (and its slot), proportion range and look. READ IT FIRST: a value it does not list is refused by name and nothing is drawn.
7911
+ - random: { base, seed, fixed? } — a distinct, valid variant; the same seed gives the same one. fixed is shared by every member (a look, proportions) while each draws its own signature — a cast of 12 is twelve seeds with one fixed.
7912
+ - create: { base, variant?: { palette, features, look, seed, name }, at: { x, y }, height? (320) | scale?, rig? (true) } → itemId (the root), rigId, parts { role: id }. y is the FEET.
7913
+
7914
+ FEATURES: hair, eyes, eyeColor, nose, mouth, ears, brows, accessories (12 slots, one per slot), proportions { headScale, bodyScale, limbLength }.
7915
+ LOOKS on a rig: flat, ink, charcoal, paper-cutout, pixel. watercolor / oil / hatch are refused with the reason.
7916
+
7917
+ INTERACTION (pinepaper_add_relation, source = the character's itemId):
7918
+ - looks_at → an item id, or target 'cursor': the face follows it.
7919
+ - reacts_to → an event or another character; params reaction (hop | jolt | shake | nod | an expression), after, duration, amount.
7920
+ - drag_to_turn (no target): dragging turns the head; params max, sensitivity, turn (keyframeable), spring.
7921
+ - a sound on the reaction: on_event_play_sound on the same event.
7922
+
7923
+ EXAMPLE: { action: 'create', base: 'person', at: { x: 480, y: 900 }, variant: { palette: { skin: '#e8b48a', hair: '#2b1d16', outfit: '#3a7bd5', accent: '#f2c14e' }, features: { hair: 'bun', eyes: 'almond', mouth: 'smile', accessories: ['cape'] }, look: 'ink', name: 'Mira' } }`,
7924
+ inputSchema: {
7925
+ type: 'object',
7926
+ properties: {
7927
+ action: { type: 'string', enum: ['bases', 'describe', 'random', 'create'] },
7928
+ base: { type: 'string' }, variant: { type: 'object', additionalProperties: true },
7929
+ seed: { type: 'integer' }, fixed: { type: 'object', additionalProperties: true },
7930
+ at: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } } },
7931
+ height: { type: 'number' }, scale: { type: 'number' }, rig: { type: 'boolean' },
7932
+ },
7933
+ required: ['action'],
7934
+ },
7935
+ },
7936
+ {
7937
+ name: 'pinepaper_morph_sequence',
7938
+ annotations: { title: 'Morph Sequence', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
7939
+ description: `ONE element through many states, with no cuts: each state grows out of the last (a pill becomes an input field becomes a dot becomes a card). The one-take UI-launch idiom, from one call.
7940
+
7941
+ { itemId, states: [{ at, duration? (0.8), to: itemId | shape: { itemType, position, properties? } }], easing?, camera?, sound? }
7942
+ - Each state is the shape the element morphs INTO, starting at \`at\` seconds. Name an existing item (to) or describe one (shape); either way the reference is hidden and the element keeps its own id throughout. States run one after another; an overlap is refused.
7943
+ - easing: linear | easeIn | easeOut | easeInOut (default easeInOut) — the four the morph supports.
7944
+ - camera: true (or { fill, easing }) keys the camera on the SAME timing: it holds while a state rests and moves with each morph, framing the new state to \`fill\` of the frame (default 0.45).
7945
+ - sound: true puts a whoosh at each morph start (or name any catalogue sound).
7946
+
7947
+ Shapes morph by kind: path → path point by point, circle → circle by radius, text → text by font, across kinds by a particle transition. Colours and size morph too.
7948
+
7949
+ RETURNS: states [{ at, duration, targetId }], end (seconds), camera { keyframes }, sounds.
7950
+
7951
+ EXAMPLE — pill → field → dot:
7952
+ { itemId: "item_1", camera: true, sound: true, states: [
7953
+ { at: 1, shape: { itemType: "rectangle", position: { x: 960, y: 540 }, properties: { width: 640, height: 88, cornerRadius: 44, color: "#ffffff" } } },
7954
+ { at: 3, shape: { itemType: "circle", position: { x: 960, y: 540 }, properties: { radius: 18, color: "#111111" } } } ] }`,
7955
+ inputSchema: {
7956
+ type: 'object',
7957
+ properties: {
7958
+ itemId: { type: 'string' },
7959
+ states: { type: 'array', items: { type: 'object', properties: { at: { type: 'number' }, duration: { type: 'number' }, to: { type: 'string' }, shape: { type: 'object', additionalProperties: true } }, required: ['at'] } },
7960
+ easing: { type: 'string', enum: ['linear', 'easeIn', 'easeOut', 'easeInOut'] },
7961
+ camera: { description: 'true, or {fill, easing}', oneOf: [{ type: 'boolean' }, { type: 'object', additionalProperties: true }] },
7962
+ sound: { description: 'true, or a catalogue sound name', oneOf: [{ type: 'boolean' }, { type: 'string' }] },
7963
+ },
7964
+ required: ['itemId', 'states'],
7965
+ },
7966
+ },
7967
+ {
7968
+ name: 'pinepaper_emitter',
7969
+ annotations: { title: 'Particle Emitter', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
7970
+ description: `Particles as an ITEM whose every frame is a pure function of its spec and the scene time — so the preview, capture_frames, every export, a saved project, a template and the cloud render all draw the same particles, scrubbing backwards included. The serialisable, cloud-safe choice; pinepaper_apply_effect is a live look on an item.
7971
+
7972
+ ACTIONS:
7973
+ - create: { spec } → itemId
7974
+ - set: { itemId, spec } — merge a partial spec onto the emitter's.
7975
+
7976
+ SPEC:
7977
+ - mode: 'burst' (default) — bursts: [{ t, x, y, count, …per-burst overrides }]: a pop of particles at scene time t.
7978
+ - mode: 'trail' (or give target) — target (an item id), rate (per second), emitWindows [[t0, t1], …], window, offset: particles shed from a moving item.
7979
+ - look and physics (per emitter or per burst): shape ('circle' | 'rect' | 'dash' | 'star' | 'confetti', or an array), palette [colours], size / sizes / sizeEnd, speed + angle ranges or velocities, gravity (px/s², +down), drag, friction, lifetime, fadeIn / fadeOut, spin, opacity, jitter, seed.
7980
+ An unknown shape, mode or bad range is refused with the engine's reason — never defaulted.
7981
+
7982
+ EXAMPLE — confetti at 2 s:
7983
+ { action: 'create', spec: { bursts: [{ t: 2, x: 960, y: 400, count: 80 }], shape: 'confetti', palette: ['#f43f5e', '#facc15', '#22d3ee'], speed: [300, 700], angle: [-150, -30], gravity: 900, lifetime: 2.5, seed: 7 } }`,
7984
+ inputSchema: {
7985
+ type: 'object',
7986
+ properties: {
7987
+ action: { type: 'string', enum: ['create', 'set'] },
7988
+ itemId: { type: 'string', description: 'set: the emitter' },
7989
+ spec: { type: 'object', additionalProperties: true, description: 'create: the whole spec; set: the keys to change' },
7990
+ },
7991
+ required: ['action', 'spec'],
7992
+ },
7993
+ },
7994
+ {
7995
+ name: 'pinepaper_render_hook',
7996
+ annotations: { title: 'Render Hook', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
7997
+ description: `Custom per-frame drawing that a saved scene CARRIES: SOURCE TEXT, compiled and run under the governor's loop guards with seeded Math, saved in the project document, the template and undo, and re-installed on load. (A raw function added with code is not saved — pinepaper_export_scene { full: true } names any such as unserialized.)
7998
+
7999
+ ACTIONS:
8000
+ - register: { id, source, layer? ('above' | 'below'), deterministic? (true), seed? } — source evaluates to (ctx, t, info) => {}: ctx in canvas coordinates, t the scene time in seconds, info { width, height, frame, seed }. 'above' draws over every item, 'below' under them. deterministic reseeds Math.random from (seed, frame) each call, so a frame always draws the same.
8001
+ - unregister: { id } list: the saved hooks.
8002
+
8003
+ Reach for it when no tool draws the look (a scanline overlay, a custom texture pass). For particles, pinepaper_emitter is data rather than code.
8004
+
8005
+ EXAMPLE: { action: 'register', id: 'scanlines', layer: 'above', source: "(ctx, t, info) => { ctx.globalAlpha = 0.08; for (let y = (t * 40) % 4; y < info.height; y += 4) ctx.fillRect(0, y, info.width, 1); }" }`,
8006
+ inputSchema: {
8007
+ type: 'object',
8008
+ properties: {
8009
+ action: { type: 'string', enum: ['register', 'unregister', 'list'] },
8010
+ id: { type: 'string' }, source: { type: 'string' },
8011
+ layer: { type: 'string', enum: ['above', 'below'] },
8012
+ deterministic: { type: 'boolean' }, seed: { type: 'integer' },
8013
+ },
8014
+ required: ['action'],
8015
+ },
8016
+ },
8017
+ {
8018
+ name: 'pinepaper_choreograph',
8019
+ annotations: {
8020
+ title: 'Choreograph an Actor',
8021
+ readOnlyHint: false,
8022
+ destructiveHint: false,
8023
+ idempotentHint: false,
8024
+ openWorldHint: false,
8025
+ },
8026
+ description: `Make any item ACT: a list of beats compiled to one keyframe track with anticipation, squash and stretch, contact and settle — the timing a hand animator would key, from one call.
8027
+
8028
+ BEATS run one after another: [{ at, verb, ...params }]. Positions are the actor's CENTRE in canvas px; a squash keeps its BOTTOM on the ground.
8029
+ - pop: scales up from nothing with an overshoot (an entrance in place).
8030
+ - drop { to? }: falls and lands with a squash. Already in the air (after a fly), it falls from where it is; on the ground or below the landing point, it comes in from above the frame.
8031
+ - hop { to?, height? }: crouch, launch stretched, arc, land squashed, settle. Without to it hops in place.
8032
+ - bounce { times? (3), height?, decay? (0.6), to? }: bounces that lose height; with to it travels while bouncing.
8033
+ - roll { to }: rolls without slipping — it turns by the distance over its radius (right = clockwise).
8034
+ - fly { to, arc? }: an arc through the air, nose up on the rise and down on the fall.
8035
+ - peek { edge (left|right|top|bottom), amount? (0.5), hold? (1) }: slides partly in from a frame edge, waits, ducks back out.
8036
+ - shake { intensity? }: a startled shiver. squash { amount? }: an in-place squash and recover (a reaction, a landing hit).
8037
+ - enter { from (edge), to? }: slides in from an edge with an overshoot. exit { edge }: a small anticipation, then away off that edge.
8038
+ Each verb has a default duration; pass duration to change it.
8039
+
8040
+ WHAT IT RETURNS: keyframes written, the duration, and where the actor ends (and whether that is on screen). Beats that overlap, an unknown verb, or roll / fly without a destination are refused by name.
8041
+
8042
+ ACTORS: a shape, a group, an imported SVG character, a photo cut-out — anything with one transform. Its own scale and rotation are kept (a beat squashes relative to them). A figure placed by pinepaper_character is several parts with their own tracks and no single transform, so choreograph its parts' group only if you grouped a STATIC figure; otherwise direct it with character beats.
8043
+
8044
+ SOUND: sound: true puts a sound on every moment the motion makes — the pop, each launch, every landing (a bounce's landings get quieter as the bounces shrink), a shake — placed on the timeline and baked into the video. sound: { contact: 'kick', whoosh: false, gain: 0.6 } picks or silences them by catalogue name (SFX beep, pop, wind, whoosh, zap; drums kick, snare, tom, hat, clap; or your own from define_sfx). The reply lists the sounds placed. A sound due before 0.06 s is placed at 0.06 s (movedFrom says so): an MP4 encoder swallows the first ~50 ms of audio.
8045
+
8046
+ MIX WITH: pinepaper_character / text expressions on top; pinepaper_apply_effect (confetti, shockwave) at a landing; pinepaper_sound at the same times. Call choreograph BEFORE keyframing the actor's other properties (colour, fill): the beat keys carry position, scale, rotation and opacity only, and the result warns if the actor already animates something else.
8047
+
8048
+ EXAMPLE — a ball comes to life and crosses the frame:
8049
+ { itemId: "item_1", beats: [ { at: 0, verb: "pop" }, { at: 0.6, verb: "bounce", times: 3, to: [700, 820] }, { at: 2.4, verb: "roll", to: [1200, 820] }, { at: 3.5, verb: "fly", to: [1600, 300] } ] }`,
8050
+ inputSchema: {
8051
+ type: 'object',
8052
+ properties: {
8053
+ itemId: { type: 'string', description: 'The actor (any item with one transform).' },
8054
+ sound: { description: "true, or {pop?, whoosh?, contact?, shake? (catalogue name or false), gain? 0..1}: a sound on every pop, launch, landing and shake.", oneOf: [{ type: 'boolean' }, { type: 'object', additionalProperties: true }] },
8055
+ beats: {
8056
+ type: 'array',
8057
+ description: 'Beats in time order: [{at, verb, to?, duration?, height?, times?, decay?, arc?, edge?, from?, amount?, hold?, intensity?}].',
8058
+ items: {
8059
+ type: 'object',
8060
+ properties: {
8061
+ at: { type: 'number' },
8062
+ verb: { type: 'string', enum: ['pop', 'drop', 'hop', 'bounce', 'roll', 'fly', 'peek', 'shake', 'squash', 'enter', 'exit'] },
8063
+ to: { description: '[x, y] or {x, y}: the actor centre at the end of the beat' },
8064
+ duration: { type: 'number' }, height: { type: 'number' }, times: { type: 'integer' }, decay: { type: 'number' },
8065
+ arc: { type: 'number' }, edge: { type: 'string', enum: ['left', 'right', 'top', 'bottom'] }, from: { type: 'string', enum: ['left', 'right', 'top', 'bottom'] },
8066
+ amount: { type: 'number' }, hold: { type: 'number' }, intensity: { type: 'number' },
8067
+ },
8068
+ required: ['at', 'verb'],
8069
+ },
8070
+ },
8071
+ },
8072
+ required: ['itemId', 'beats'],
8073
+ },
8074
+ },
7711
8075
  {
7712
8076
  name: 'pinepaper_relight',
7713
8077
  annotations: {
@@ -8104,7 +8468,7 @@ PRIVACY: No screenshots, no tool call arguments, no user content — only struct
8104
8468
 
8105
8469
  ⚠️ Call ONCE per pipeline. NEVER restart — creates duplicates.
8106
8470
 
8107
- WORKFLOW: start_job → batch_execute (everything in ONE call) → end_job (screenshot) → show user → iterate if needed.
8471
+ WORKFLOW: start_job → the craft tool(s) that fit the brief (styled_scene, story, character, text_style, compose, design_medium…) → batch_execute for the primitive glue, in ONE call → end_job (screenshot) → show user → iterate if needed. Primitive shapes over a generator backdrop is the stock look; use it only when no higher-level tool fits.
8108
8472
 
8109
8473
  RETURNS: canvasSize {width, height} — use this to position items within bounds. Without a canvasPreset, an unbounded canvas is fixed at that size, so stills and video both export it (the result says so in note). Also returns ontology: a pp: triples + item summary snapshot of the (typically empty) canvas. Reuse this baseline across the same agent turn instead of calling pinepaper_get_canvas_ontology unless you've mutated the canvas. When clearCanvas is false, the ontology captures whatever's already on the canvas.
8110
8474
 
@@ -8257,7 +8621,7 @@ EXAMPLES:
8257
8621
  idempotentHint: false,
8258
8622
  openWorldHint: false,
8259
8623
  },
8260
- description: `Execute ALL operations in a single call for building animated scenes — canvas setup, backgrounds, items, animations, masks, effects, and playback.
8624
+ description: `Execute the primitive operations of a scene in a single call — canvas setup, backgrounds, items, animations, masks, effects, and playback. It is the GLUE: a styled scene, a character, a story or a crafted title has its own tool (styled_scene, character, story, text_style), called first.
8261
8625
 
8262
8626
  NOTE: For diagrams (flowcharts, UML), use pinepaper_create_diagram_shape + pinepaper_connect instead.
8263
8627
 
@@ -8268,7 +8632,7 @@ NOTE: For diagrams (flowcharts, UML), use pinepaper_create_diagram_shape + pinep
8268
8632
  ONTOLOGY PREFLIGHT: Before code generation, this tool validates itemType, relationType, effectType, and generatorName against the canonical PinePaper vocabulary. A typo (e.g. "circel") returns a structured VALIDATION_ERROR with the operation index, the offending field, the canonical valid list, and a suggested fix — fix and retry rather than guessing. Pass skipValidation: true to bypass when intentionally using experimental vocabulary.
8269
8633
 
8270
8634
  WORKFLOW:
8271
- 1. pinepaper_agent_start_job → 2. pinepaper_agent_batch_execute (everything) → 3. pinepaper_agent_end_job (screenshot for validation)
8635
+ 1. pinepaper_agent_start_job → 2. the craft tools that fit the brief (styled_scene, story, character, text_style, compose…) → 3. pinepaper_agent_batch_execute (the primitive glue, in one call) → 4. pinepaper_agent_end_job (screenshot for validation)
8272
8636
  After validation: user reviews screenshot → feedback → modify/recreate as needed.
8273
8637
 
8274
8638
  OPERATION TYPES (${AgentBatchOperationTypeSchema.options.length}) — use in this order:
@@ -8298,7 +8662,7 @@ ITEMS:
8298
8662
  ANIMATION:
8299
8663
  animate — Loop animation: {itemId, animationType, animationOptions}
8300
8664
  animationType: ${SIMPLE_ANIMATION_TYPES_LIST}
8301
- keyframe_animate — Timed animation: {itemId, keyframes: [{time, properties, easing}], duration, loop}
8665
+ keyframe_animate — Timed animation: {itemId, keyframes: [{time, properties, easing}], duration, loop, append}. Replaces the item's track unless append: true; two ops on one item in a batch are merged into one.
8302
8666
  Animatable properties: opacity, scale, scaleX, scaleY, x, y, rotation, fillColor, strokeColor, strokeWidth, width, height, radius, fontSize, trimStart/trimEnd/trimOffset (path draw-on)
8303
8667
  Easing (on the keyframe being moved TO): ${KEYFRAME_EASINGS_LIST}
8304
8668
  relation — Behavioral link: {sourceId, targetId, relationType, relationOptions}
@@ -8354,6 +8718,7 @@ EXAMPLE — Animated sky scene with timed reveals:
8354
8718
  height: { type: 'number', description: 'Canvas height for set_canvas_size (100-16384)' },
8355
8719
  preset: { type: 'string', description: 'Canvas preset for set_canvas_size' },
8356
8720
  backgroundColor: { type: 'string', description: 'For set_background: hex color' },
8721
+ color: { type: 'string', description: 'For set_background: same as backgroundColor. For create: same as fillColor.' },
8357
8722
  generatorName: {
8358
8723
  type: 'string',
8359
8724
  enum: [...GeneratorNameSchema.options],
@@ -8400,6 +8765,7 @@ EXAMPLE — Animated sky scene with timed reveals:
8400
8765
  },
8401
8766
  duration: { type: 'number', description: 'Duration in seconds for keyframe_animate or play_timeline' },
8402
8767
  loop: { type: 'boolean', description: 'Loop for keyframe_animate or play_timeline' },
8768
+ append: { type: 'boolean', description: "For keyframe_animate: add these keys to the item's existing track instead of replacing it (a key at the same time merges, the new values win). Without it the op replaces the track; the op's result says which happened." },
8403
8769
  // Relations
8404
8770
  sourceId: { type: 'string', description: 'For relation: source item ID or $N' },
8405
8771
  targetId: { type: 'string', description: 'For relation: target item ID or $N' },
@@ -9403,8 +9769,11 @@ ACTIONS:
9403
9769
  },
9404
9770
  },
9405
9771
  // ---------------------------------------------------------------------------
9406
- // STITCHCRAFT PROCEDURAL EMBROIDERY & THREAD
9772
+ // STITCHCRAFT PROCEDURAL EMBROIDERY & THREAD, SUBDIVISION, UI KIT
9773
+ // The cloud's craft tools, shared: schemas vendored from mcp-cloud, local
9774
+ // words and behaviour in craft-tools.ts.
9407
9775
  // ---------------------------------------------------------------------------
9776
+ ...craftToolDefinitions(),
9408
9777
  ];
9409
9778
  /**
9410
9779
  * Get a tool definition by name