@pinepaper.studio/mcp-server 1.6.33 → 1.6.35
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/README.md +102 -1
- package/dist/build-info.json +2 -2
- package/dist/character/perform.d.ts +14 -0
- package/dist/character/perform.d.ts.map +1 -1
- package/dist/character/perform.js +29 -3
- package/dist/character/perform.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +1 -1
- package/dist/ontology/upstream-vocabulary.js +1 -1
- package/dist/tools/batch-macros.d.ts +20 -1
- package/dist/tools/batch-macros.d.ts.map +1 -1
- package/dist/tools/batch-macros.js +146 -7
- package/dist/tools/batch-macros.js.map +1 -1
- package/dist/tools/compact-descriptions.js +1 -1
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +87 -68
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +4 -4
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +13 -4
- package/dist/tools/engine-lists.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +1 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +8 -6
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/beats.d.ts +1 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +24 -10
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +8 -1
- package/dist/tools/lane.d.ts.map +1 -1
- package/dist/tools/lane.js +54 -13
- package/dist/tools/lane.js.map +1 -1
- package/dist/tools/minimal-descriptions.js +1 -1
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.js +2 -2
- package/dist/tools/stick-vocabulary.d.ts +1 -1
- package/dist/tools/stick-vocabulary.js +1 -1
- package/dist/tools/toolkits.d.ts +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +7 -2
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +32 -0
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +298 -69
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +74 -30
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +13 -8
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ import { TOOL_TAGS } from './toolkits.js';
|
|
|
14
14
|
import { STICK_GAITS, STICK_POSES, STICK_SEQUENCES, STICK_EXPRESSIONS, STICK_PROPS } from './stick-vocabulary.js';
|
|
15
15
|
import { AgentBatchOperationTypeSchema, GeneratorNameSchema, ItemTypeSchema } from '../types/schemas.js';
|
|
16
16
|
import { expressionGrammarText } from './expression-grammar.js';
|
|
17
|
-
import { DESIGN_MEDIA, DESIGN_MEDIA_APPLY, FILTER_TYPES, FILTER_DOCS, LINEAGE_KINDS, STYLED_SCENE_STYLES, STYLED_SCENE_STYLE_INFO, PATTERN_KINDS, ANIMATION_TYPES, ANIMATION_WAVEFORMS, ANIMATION_ORIGINS } from './engine-lists.js';
|
|
17
|
+
import { ENGINE_EASING_NAMES, DESIGN_MEDIA, DESIGN_MEDIA_APPLY, FILTER_TYPES, FILTER_DOCS, LINEAGE_KINDS, STYLED_SCENE_STYLES, STYLED_SCENE_STYLE_INFO, PATTERN_KINDS, ANIMATION_TYPES, ANIMATION_WAVEFORMS, ANIMATION_ORIGINS } from './engine-lists.js';
|
|
18
18
|
import { EffectTypeSchema, AgentExportFormatSchema, RelationTypeSchema, SimpleAnimationTypeSchema, EasingSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, KEYFRAME_EASING_JSON_SCHEMA, GEOMETRY_OPERATIONS, THREAD_STITCHES, } from '../types/schemas.js';
|
|
19
19
|
/**
|
|
20
20
|
* Vocabulary lists derived from Zod schemas so tool descriptions stay in
|
|
@@ -74,10 +74,10 @@ export const AI_AGENT_GUIDE = ("⚠️ You are connected to PinePaper Studio via
|
|
|
74
74
|
"aper_export_widget_html` exports the scene as a self-contained HTML page, and `pinepaper_export_widget` as an embeddable widget. Asked for a web page or an embed, build the scene with tools and export it — that is the intended path, not a workaround.\n- CODE IS a supported input. `pinepaper_execute_custom_code` runs Paper.js against the live canvas, and `pinepaper_execute_generator` runs a named generator. Reach for them when no tool fits; they are part of the surface, not an escape from it.\n- The rule above is about SUBSTITUTION — writing a CSS animation instead of calling a tool. It is not a rule against the formats themselves, and a model that reads it as one will refuse work this serv" +
|
|
75
75
|
"er can do.\nThe generated code is shown in tool responses and can be copied into PinePaper's Code Console.\n\n⚠️ 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.\n\n" +
|
|
76
76
|
String(CHOOSE_THE_DOOR) +
|
|
77
|
-
"\n\nTHE AGENT LANE — when the job is a scene of shapes, text, keyframes and relations exported to a file (a data animation, a title card, a motion test), and no craft tool above is wanted: PINEPAPER_TOOLKIT=lane (or pinepaper_set_toolkit {toolkit: \"lane\"}) serves
|
|
78
|
-
" surface for characters, styled scenes, maps, diagrams and sound.\n\nWORKFLOW — Create → Validate → Iterate:\n\n1. pinepaper_agent_start_job (clearCanvas: true, canvasPreset)\n2. The craft tool(s) above that fit the brief, each called once.\n Then pinepaper_agent_batch_execute — ONE call for the glue:\n set_canvas_size → set_background → create items → keyframe_animate / animate → relation → apply_mask → apply_effect → play_timeline\n3. pinepaper_agent_end_job — returns screenshot\n4. Show screenshot to user for validation\n5. If user wants changes → modify
|
|
79
|
-
"
|
|
80
|
-
"
|
|
77
|
+
"\n\nTHE AGENT LANE — when the job is a scene of shapes, text, keyframes and relations exported to a file (a data animation, a title card, a motion test), and no craft tool above is wanted: PINEPAPER_TOOLKIT=lane (or pinepaper_set_toolkit {toolkit: \"lane\"}) serves eight tools — pinepaper_agent_batch_execute, pinepaper_keyframe_animate, pinepaper_pattern, pinepaper_agent_export, pinepaper_lint_scene, pinepaper_event (timed and clicked events), pinepaper_clear_canvas (start over), pinepaper_register_custom_relation (a motion of your own) — whose descriptions carry the rules that otherwise cost a failed build (the keyframe shape, an item's words and look inside properties, position is the ce" +
|
|
78
|
+
"ntre). The whole contract is under 10k tokens. PINEPAPER_TOOLKIT=starter serves its first five, for a first scene. Use the full surface for characters, styled scenes, maps, diagrams and sound.\n\nWORKFLOW — Create → Validate → Iterate:\n\n1. pinepaper_agent_start_job (clearCanvas: true, canvasPreset)\n2. The craft tool(s) above that fit the brief, each called once.\n Then pinepaper_agent_batch_execute — ONE call for the glue:\n set_canvas_size → set_background → create items → keyframe_animate / animate → relation → apply_mask → apply_effect → play_timeline\n3. pinepaper_agent_end_job — returns screenshot\n4. Show screenshot to user for validation\n5. If user wants changes → modify s" +
|
|
79
|
+
"pecific items or start a new job\n\n⚠️ CRITICAL — Every operation is LIVE on the canvas:\nEvery create, animate, relation, or generator call IMMEDIATELY executes on the connected browser canvas.\nCalling the same operation twice creates DUPLICATE elements. There is no \"preview\" — it's all live.\nIf you restart the pipeline or re-call batch_execute, you DOUBLE every item on the canvas.\nTo fix mistakes: use modify/delete operations, or start a NEW job with clearCanvas: true.\n\nRULES:\n- NEVER restart the pipeline — one start_job, one batch_execute, one end_job; the craft tools go between them\n- Primitive operations go in the ONE batch_execute — canvas, background, items, animation, mask" +
|
|
80
|
+
"s, effects, playback\n- EXCEPTION: Diagrams (flowcharts, UML) use pinepaper_create_diagram_shape + pinepaper_connect, NOT batch_execute\n- 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\n- Use \"$0\", \"$1\" to reference the items a batch makes, in op order (create and group ops are numbered)\n\n─── BATCH OPERATIONS (" +
|
|
81
81
|
String(AgentBatchOperationTypeSchema.options.length) +
|
|
82
82
|
" types, in order) ───\n\nCANVAS: set_canvas_size (width/height or preset) → set_background (color) → execute_generator (procedural art)\nITEMS: create (itemType, position, properties) → modify (itemId, properties) → delete (itemId) → group (itemIds → one entity)\nANIMATE: animate (loop presets) → keyframe_animate (timed reveals) → relation (behavioral links)\nEFFECTS: apply_mask (reveal animations) → apply_effect (sparkle/blast/bubbles/fireflies/trail...)\nPLAY: play_timeline (start playback)\n\n─── GENERATORS (procedural backdrops — under a scene, not as one) ───\n\nGenerators fill the canvas with procedural art: richer than a flat colour as a backdrop, but not a composition by them" +
|
|
83
83
|
"selves.\nChoose based on mood:\n Dreamy/soft: drawBokeh, drawGradientMesh, drawOrganicFlow\n Techy/modern: drawCircuit, drawGrid, drawWindField\n Nature/warm: drawSunsetScene, drawSunburst, drawWaves\n Abstract: drawGeometricAbstract, drawFluidFlow, drawNoiseTexture\n Decorative: drawPattern, drawStackedCircles\n\nGenerators paint the canvas WITHOUT fully registering what they paint — by\ndesign, so a large procedural field does not cost memory per path. How much is\nregistered varies by generator: drawSunsetScene registers 6 of its 27 objects,\ndrawSunburst 1 of 18, drawPattern none of 15. So get_items, modify and the\nontology see a partial view or nothing at all after one r" +
|
|
@@ -437,11 +437,11 @@ IMPORTANT NOTES:
|
|
|
437
437
|
"the engine's own. Three further parameters are each\nread by six of these eight shapes, and THE THREE SIXES ARE NOT THE SAME SIX —\na shape missing from a list ignores that parameter silently, so check before\nyou pass it:\n cornerRadius — speech-bubble, speech-bubble-pointed, comment-box,\n callout-box, double-bubble, quote-bubble\n tailDirection (bottom-left | bottom-right | bottom | top-left | top-right |\n top | left | right) — speech-bubble, speech-bubble-square,\n speech-bubble-pointed, thought-bubble, comment-box, callout-box\n tailSize (0-1, default 0.35) — speech-bubble, speech-bubble-square,\n speech-bubble-pointed, comment-box, callout-box, double-bubble\n- speech" +
|
|
438
438
|
"-bubble: Classic round speech bubble with triangular tail.\n- speech-bubble-square: Rectangular speech bubble with sharp corners and\n pointed tail.\n- speech-bubble-pointed: Tooltip-style bubble with arrow pointer (tail defaults\n to \"bottom\").\n- thought-bubble: Cloud-like thought bubble with trailing dots.\n- comment-box: Modern chat message bubble with high corner radius.\n- callout-box: Annotation callout with narrow arrow (tail defaults to \"left\").\n- double-bubble: Two overlapping bubbles for conversation (properties:\n secondaryColor).\n- quote-bubble: Stylized bubble with quotation marks.\n\nPATH FOR CUSTOM SHAPES:\nUse itemType: \"path\" with either:\n- segments: Array of " +
|
|
439
439
|
"[x,y] points for polygons, e.g. [[100,100], [200,50], [300,100]]\n- pathData: SVG path string for curves, e.g. \"M 100 100 C 150 50, 250 50, 300 100 Z\"\n\nADVANCED STYLING (NEW):\nAll shapes support these additional properties:\n- No fill (stroke-only outlines, line art): fillColor: null, 'transparent' or 'none', with a strokeColor and strokeWidth.\n- fillOpacity (0..1): the solid fill's own alpha, apart from opacity (which fades the stroke too) — on text, fillOpacity: 0.3 with a strokeColor is a see-through letter with a solid outline. Not applied to a gradient fill. The result's fillOpacity says whether the studio applied it (an older studio ignores the key and says so).\n- Gradients: c" +
|
|
440
|
-
"olor can be an object: {type: \"linear\"|\"radial\", stops: [{color: \"#fff\", offset: 0}, {color: \"#000\", offset: 1}], origin: [x,y], destination: [x,y]}. Without origin / destination a radial gradient is centred on the item's own bounds and reaches its right edge, wherever anchor put it; a linear one
|
|
441
|
-
"
|
|
442
|
-
"
|
|
443
|
-
"
|
|
444
|
-
"
|
|
440
|
+
"olor can be an object: {type: \"linear\"|\"radial\", stops: [{color: \"#fff\", offset: 0}, {color: \"#000\", offset: 1}], origin: [x,y], destination: [x,y]}. Without origin / destination a radial gradient is centred on the item's own bounds and reaches its right edge, wherever anchor put it; a linear one runs top to bottom across the item's bounds. Pass origin and destination (canvas px) for any other direction.\n- Shadows: shadowColor, shadowBlur (pixels), shadowOffset: [x, y]\n- Blend modes: blendMode: \"multiply\"|\"screen\"|\"overlay\"|\"darken\"|\"lighten\"|etc.\n- Opacity: opacity (0-1)\n\nEXAMPLES:\n- \"Create red text saying HELLO\" → itemType: \"text\", properties: {content: \"HEL" +
|
|
441
|
+
"LO\", color: \"#ef4444\", fontSize: 48}\n- \"Add a blue circle\" → itemType: \"circle\", properties: {radius: 50, color: \"#3b82f6\"}\n- \"Create a 5-pointed gold star\" → itemType: \"star\", properties: {radius1: 60, radius2: 30, points: 5, color: \"#fbbf24\"}\n- \"Draw a heart\" → itemType: \"path\", properties: {pathData: \"M 300 350 C 300 300, 250 250, 200 250 C 150 250, 100 300, 100 350 C 100 450, 300 550, 300 550 C 300 550, 500 450, 500 350 C 500 300, 450 250, 400 250 C 350 250, 300 300, 300 350 Z\", fillColor: \"#ef4444\"}\n- \"Circle with radial gradient\" → itemType: \"circle\", properties: {radius: 50, color: {type: \"radial\", stops: [{color: \"#ff6b6b\", offset: 0}, {color: \"#" +
|
|
442
|
+
"c0392b\", offset: 1}]}}\n- \"Rectangle with shadow\" → itemType: \"rectangle\", properties: {width: 100, height: 50, color: \"#3b82f6\", shadowColor: \"rgba(0,0,0,0.5)\", shadowBlur: 10, shadowOffset: [5, 5]}\n\nITEM PROVENANCE:\nItems on the canvas can originate from multiple sources — all become first-class items with itemIds:\n- pinepaper_create_item: Direct shape creation (this tool)\n- pinepaper_import_svg: SVG file import → becomes a full item\n- pinepaper_import_image: Raster image import → becomes a full item\n- pinepaper_import_asset: Pre-made asset import → becomes a full item\nAll imported items participate in the relation and animation systems just like natively created items." +
|
|
443
|
+
"\n\nRENDER-TIME SURFACES — 'shader' and 'field':\nThese are not shapes. A shape is built as an item and drawn from its geometry; a\nsurface has no geometry — it is evaluated, per pixel or per sample, as each frame\nis drawn. Use them for things a vector cannot be: an open sea, a drifting field\nof marks, a lit material.\n- shader: pick one of water | liquid_metal | heatmap | gem_smoke, give it width\n and height, and set behind:true to place it UNDER the vector art — which is\n what scenery wants, a sea the boats float ON rather than a sheen over them.\n- field: xExpr/yExpr/count draw many marks from one expression, re-evaluated\n every frame against t. rExpr and aExpr vary size and alp" +
|
|
444
|
+
"ha per mark.\nBoth draw fully in a cloud render; on a local canvas they stand in as a flat\nplate of their own colour so the layout still reads.\n\nLIFETIMES — bornAt and ttl, on any item:\nThe only way to build a piece that CUTS between shots. Without them every item\nis on screen for the whole render, which is what makes a long piece read as one\ncrowded frame instead of a sequence.\n\nWORKFLOW TIP:\nAfter creating items, use pinepaper_add_relation to animate them (e.g., orbits, follows, attached_to).\nFor glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal stripes, use pinepaper_create_diagonal_stripes."
|
|
445
445
|
|
|
446
446
|
|
|
447
447
|
|
|
@@ -1200,7 +1200,7 @@ Use pinepaper_clear_canvas instead of deleting items one by one.`,
|
|
|
1200
1200
|
opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Opacity, 0 to 1. The same as pinepaper_modify_item after the import.' },
|
|
1201
1201
|
blendMode: { type: 'string', description: "How it composites over what is below: 'multiply', 'screen' and the other pinepaper_modify_item blend modes." },
|
|
1202
1202
|
scale: { type: 'number', exclusiveMinimum: 0, description: 'Multiplies the size maxWidth / maxHeight / maxEdge gave it.' },
|
|
1203
|
-
loop: { anyOf: [{ type: 'boolean' }, { type: 'integer', minimum: 1 }, { type: 'string', enum: ['forever'] }], description: "An animated GIF or
|
|
1203
|
+
loop: { anyOf: [{ type: 'boolean' }, { type: 'integer', minimum: 1 }, { type: 'string', enum: ['forever'] }], description: "An animated GIF, WebP or APNG: true or 'forever' loops, false plays once and holds, n plays n times. Left out, the file's own loop count. Change it later with pinepaper_modify_item's loop, and its start time with imageStart. An APNG plays too, except a remote file named .png, which stays a still." },
|
|
1204
1204
|
url: {
|
|
1205
1205
|
type: 'string',
|
|
1206
1206
|
description: 'URL of the image to import',
|
|
@@ -1688,10 +1688,10 @@ EXAMPLE:
|
|
|
1688
1688
|
openWorldHint: false,
|
|
1689
1689
|
},
|
|
1690
1690
|
description: ("Manage the pp:event channel — the backbone of EVENT-DRIVEN SCENE CHAINS (FxTool S11). An event is a fireable channel; relations with an event as their source run when it fires. This is how you author a LONG scene as a graph of timed beats instead of a keyframe track.\n\nACTIONS:\n- create: make an event. { name (required), payloadType?, x?, y? } → returns { eventId }. Use the eventId as the sourceId of on_event_* relations (pinepaper_add_relation).\n- pulse: fire an event now. { eventId (required), payload? }. Kicks off a chain.\n\nTYPICAL SCENE CHAIN:\n1. pinepaper_event create → e0, e1, e2 (one per beat).\n2. Chain them: add_relation on_event_fire_after from e0→e1 (params.delay, timeline" +
|
|
1691
|
-
":'canvas'), e1→e2, …\n3. Give each beat reactions: on_event_add_relation (spawn an orbit/path), on_event_set_color, on_event_set_property, on_event_set_visibility (all via pinepaper_add_relation, sourceId=the event).\n4. pinepaper_event pulse e0 to start. Canvas-timed fires are scrub/loop/replay-stable.\n\nRULES (contraptions): a rule runs when an event fires: { action: 'add_contraption', sourceEvent, actions, condition?, thenTrigger?, elseTrigger?, contraptionId? }. sourceEvent is
|
|
1692
|
-
"
|
|
1693
|
-
"
|
|
1694
|
-
"
|
|
1691
|
+
":'canvas'), e1→e2, …\n3. Give each beat reactions: on_event_add_relation (spawn an orbit/path), on_event_set_color, on_event_set_property, on_event_set_visibility (all via pinepaper_add_relation, sourceId=the event).\n4. pinepaper_event pulse e0 to start. Canvas-timed fires are scrub/loop/replay-stable.\n\nRULES (contraptions): a rule runs when an event fires: { action: 'add_contraption', sourceEvent, actions, condition?, thenTrigger?, elseTrigger?, contraptionId? }. sourceEvent is the eventId that create returned (e.g. item_3; its timer and pulse fire that id, NOT the event's name), any other channel name you pulse yourself, or 'state:<key>' when that variable changes. actions: setVariabl" +
|
|
1692
|
+
"e { key, value? }, incrementVariable { key, by? }, toggleVariable { key }, modifyItem { targetId, properties: { visible, opacity, fillColor, strokeColor, content, position } }, toggleItemVisible { targetId }, animateItem { targetId, animation?, speed? }, playSound { preset?, note?, gain?, duration? }, playTimeline { duration?, loop? }. list_contraptions shows each rule's fireCount, which is how to tell a LIVE pulse ran it (a pulse runs its rules on the next frame); an export puts fireCount back with everything else, so after an export check the frames, not the count. An event's NAME given as sourceEvent is resolved to its id on studios that do so (the reply's sourceEventResolvedTo); elsewh" +
|
|
1693
|
+
"ere the reply warns. remove_contraption takes contraptionId.\n- In an export, what a rule changes is put back afterwards, so the next export starts from the same scene. animateItem cannot be put back, and the export warns. A change from a pulse outside an export stays.\n- An export is silent to playSound and refuses playTimeline, since it already steps the timeline. For sound in a video, use on_event_play_sound (above).\n- pinepaper_clear_canvas removes rules, and so does clear_contraptions. They are saved with the scene.\n\nVARIABLES: set_variable { key, value }, get_variable { key, fallback? }, increment_variable { key, by? }, each with an optional scope. They live in the page's storage," +
|
|
1694
|
+
" so they survive pinepaper_clear_canvas: set the ones a scene reads when it starts. Nothing private belongs in them.\n\nclear_variables { scope? } removes every variable in a scope, and no rule fires for it.\n\nBOUND TEXT: bind_text { itemId, template: \"Score: {score}\" } keeps a text item showing the variables it names.\n\nA studio without the contraption engine refuses these actions, naming the method it lacks.\n\nRETURNS: create → { success, action, eventId }; pulse → { success, action, eventId }; add_contraption → { success, action, contraptionId, warning? }."
|
|
1695
1695
|
|
|
1696
1696
|
|
|
1697
1697
|
|
|
@@ -1728,7 +1728,7 @@ EXAMPLE:
|
|
|
1728
1728
|
description: "Optional value forwarded to listeners — action 'pulse'",
|
|
1729
1729
|
},
|
|
1730
1730
|
contraptionId: { type: 'string', description: "The rule's id: optional for add_contraption, required for remove_contraption" },
|
|
1731
|
-
sourceEvent: { type: 'string', description: "add_contraption: what runs the rule:
|
|
1731
|
+
sourceEvent: { type: 'string', description: "add_contraption: what runs the rule: the eventId create returned (its timer and pulse fire the id, not the event's name), any other channel you pulse yourself, or 'state:<key>' when that variable changes" },
|
|
1732
1732
|
condition: {
|
|
1733
1733
|
type: 'object',
|
|
1734
1734
|
description: 'add_contraption: the rule runs its actions only when this holds',
|
|
@@ -2943,8 +2943,8 @@ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
|
|
|
2943
2943
|
},
|
|
2944
2944
|
description: ("The formats other tools read and write — Lottie, dotLottie, GLB, BVH, PNG sequence.\n\npinepaper_agent_export covers the PLATFORM formats: png, svg, mp4, webm, gif, pdf, sized for Instagram or YouTube. These are the interchange ones — a Lottie an app plays, a GLB a 3D tool opens, a BVH a rig imports, a PNG sequence an editor ingests. Import is here too, because a format you can only write is half a bridge.\n\nThree of these refuse quietly in the engine: exportGLB with no perspective objects and exportBVH with no rigging system both warn to the console and return nothing, and the production build strips the console. Over MCP there is no console to read, so the precondition is checked BEFORE" +
|
|
2945
2945
|
" the call and named — \"no perspective objects to export — create one with createObject3D first\" rather than an empty result and a guess.\n\nEXPORTS ARE FILES. Each export is written to a file in the export directory and the result returns { format, filePath, bytes } instead of the data — the bytes are not inlined. An export that produced nothing is refused by name.\n\nEXPORT:\n- export_lottie: { options?: { fps?, duration? (s), includeRelations? } } → a .json Lottie file\n- export_dotlottie: { options?: { fps?, duration? (s), id?, loop?, autoplay? } } → a .lottie file (the zipped Lottie plus manifest)\n- export_glb: { } → a .glb file of the PERSPECTIVE 3D objects (createObject3D). Refuse" +
|
|
2946
|
-
"d when there are none. It does not export world3d meshes, which are a separate renderer.\n- export_bvh: { skeletonId (required), options?: { fps? } } → a .bvh file of that skeleton's motion. Refused when nothing is rigged.\n- export_png_sequence: { options?: { duration (s), fps, width?, height?, transparent? } } → a .zip of one PNG per frame. PASS duration and fps: the studio does not size the sequence to the scene, and the result's defaultsUsed says when its own default length was used
|
|
2947
|
-
" import fails with the importer's errors. data is parsed as JSON, so a URL or a file path does not work; read the file and pass its contents. There is no .lottie, GLB or BVH import here: a glTF goes into a 3D world with the world3d tool's import_gltf, a BVH onto a rig with the rigging tool's import_bvh / retarget_bvh."
|
|
2946
|
+
"d when there are none. It does not export world3d meshes, which are a separate renderer.\n- export_bvh: { skeletonId (required), options?: { fps? } } → a .bvh file of that skeleton's motion. Refused when nothing is rigged.\n- export_png_sequence: { options?: { duration (s), fps, width?, height?, transparent? } } → a .zip of one PNG per frame. PASS duration and fps: the studio does not size the sequence to the scene, and the result's defaultsUsed says when its own default length was used. To encode the frames into an MP4 that repeats, use your own FFmpeg (the server does not provide it): ffmpeg -framerate <fps> -i frame_%05d.png -c:v libx264 -pix_fmt yuv420p -crf 18 -threads 1 out.mp4\n\nIM" +
|
|
2947
|
+
"PORT:\n- import_lottie: { data (the Lottie JSON — an object or a JSON string), options?: { fitToCanvas?, autoPlay?, centerOnCanvas?, scale? } } → { itemId, itemIds, items, duration, bounds }; a failed import fails with the importer's errors. data is parsed as JSON, so a URL or a file path does not work; read the file and pass its contents. There is no .lottie, GLB or BVH import here: a glTF goes into a 3D world with the world3d tool's import_gltf, a BVH onto a rig with the rigging tool's import_bvh / retarget_bvh."
|
|
2948
2948
|
|
|
2949
2949
|
|
|
2950
2950
|
|
|
@@ -3539,26 +3539,30 @@ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
|
|
|
3539
3539
|
"o (centre - pen tip) so a hand image's TIP, not its centre, rides the stroke. closed (loop back to the start; default from the path item), phase (0–1 start point), easing (linear | easeIn | easeOut | easeInOut | sine | bounce | pingpong), signal (deterministic, frame-rate-invariant — prefer it for export).\n- The item travels at CONSTANT SPEED along the route: the fraction of the duration elapsed is the fraction of the route's LENGTH covered, not of the equation's parameter or of the point count. Where the curve is long it moves the same speed as where it is short; easing reshapes that progress. For a route timed by the parameter instead, key the position with pinepaper_keyframe_animate.\n" +
|
|
3540
3540
|
"- params.equation is in math coordinates with y UP (flipY: true by default), placed at cx, cy: on screen the curve is mirrored about cy. Write flipY: false to author it in screen coordinates (y down).\n- With no path from any of those it is refused, not accepted as a still item.\n\nUSE WHEN:\n- \"moon orbits earth\" → relationType: orbits\n- \"label follows player\" → relationType: follows\n- \"hat attached to character\" → relationType: attached_to\n- \"keep satellite 200px from station\" → relationType: maintains_distance\n- \"arrow points at target\" → relationType: points_at\n- \"reflection mirrors original\" → relationType: mirrors\n- \"background moves with parallax\" → relationType:" +
|
|
3541
3541
|
" parallax\n- \"player stays in arena\" → relationType: bounds_to\n- \"M is the midpoint of A and B\" → relationType: is_midpoint_of (params.other: B)\n- \"O is the circumcenter of the triangle\" → relationType: is_circumcenter_of\n- \"reveal each step in turn\" → relationType: construction_reveal (params.revealAt, fadeIn)\n- \"bar height driven by the slider's x\" / \"tint the dot by the planet's position\" → relationType: driven_by\n- \"make it bob with sin(t)\" / \"property follows a math expression of time\" → relationType: time_expression\n- Creating physics-based or behavioral animations\n- Building live geometric constructions (GeoGebra-style — dragging an anchor updates the dependen" +
|
|
3542
|
-
"t)\n\nRELATION TYPES:\n- orbits: Circular motion around target (params: radius, speed, direction, phaseDegrees). speed is REVOLUTIONS PER SECOND: 0.5 is one turn every 2 s, 0.1 one turn every 10 s (not radians, not a period). Use phaseDegrees. 'phase' is the one parameter in the whole relation vocabulary measured in RADIANS — the documented exception to \"angles are DEGREES\", kept only for scenes that already set it. phaseDegrees wins when both are given.\n- follows: Move toward target with smoothing (params: offset, smoothing, delay)
|
|
3543
|
-
"
|
|
3544
|
-
"
|
|
3545
|
-
"it follows; the sound's stereo position follows the item across the frame, in the preview and in exported audio. params: axis (x | y, default x), spread (0..1: pan at the frame edge, default 0.85), reference (camera | canvas, default camera), distanceGain (true also quietens it away from the centre), smoothing (ms, default 80).\n- connects_to: Diagram edge from source
|
|
3546
|
-
" each end belongs to; the diagram system is what moves a connector's ends when an item moves, and it does so only for connectors made with pinepaper_connect. On a plain line (create itemType 'line') they record the structure and move nothing: for a connector that follows the items, make it with pinepaper_connect.\n- expresses: Play a facial expression over a root's
|
|
3547
|
-
"to blink.\n- part_of: Semantic membership with a named role (params: role, offset, structural). structural: true records the part WITHOUT driving its position — the right choice when the parent is a group that already transforms its children, since driving it too would fight the group.\n-
|
|
3548
|
-
"
|
|
3549
|
-
"
|
|
3550
|
-
"
|
|
3551
|
-
"
|
|
3552
|
-
"
|
|
3553
|
-
"
|
|
3554
|
-
"
|
|
3555
|
-
"
|
|
3556
|
-
"
|
|
3557
|
-
" the
|
|
3558
|
-
"
|
|
3559
|
-
"
|
|
3560
|
-
"
|
|
3561
|
-
"
|
|
3542
|
+
"t)\n\nRELATION TYPES:\n- orbits: Circular motion around target (params: radius, speed, direction, phaseDegrees). speed is REVOLUTIONS PER SECOND: 0.5 is one turn every 2 s, 0.1 one turn every 10 s (not radians, not a period). Use phaseDegrees. 'phase' is the one parameter in the whole relation vocabulary measured in RADIANS — the documented exception to \"angles are DEGREES\", kept only for scenes that already set it. phaseDegrees wins when both are given.\n- follows: Move toward target with smoothing (params: offset, smoothing, delay, startOnTarget). It starts where the follower was made and closes in; startOnTarget: true puts it on the target at t=0.\n- attached_to: Fixed offset from tar" +
|
|
3543
|
+
"get (params: offset, inherit_rotation)\n- maintains_distance: Stay fixed distance from target (params: distance, strength)\n- points_at: Rotate to face target (params: offset_angle, smoothing)\n- mirrors: Mirror position across axis (params: axis, center)\n- parallax: Move relative by depth (params: depth, origin)\n- bounds_to: Stay within bounds (params: padding, bounce)\n- anchored_in_world: Pin the item to a 3D world position so it tracks the world camera.\n- spring_follow: Follow with spring dynamics rather than smoothing — it overshoots and settles. stiffness and damping shape it, and a heavier mass is slower and overshoots more. Some studio builds ignore mass when stiffness and dampi" +
|
|
3544
|
+
"ng are both at or below 1: check the motion if mass matters.\n- repels: Move away from the target inside a radius and ease back when it leaves (params: force, maxDistance, minDistance, returnSpeed). Crowd avoidance, and reacting to a cursor without being dragged by it.\n- wiggle: Continuous procedural noise on the item. Richer than a sine: it does not repeat visibly.\n- syncs_with: Run the source's animation on the TARGET's timeline (params: offset, speedRatio) — they share a clock and stay in step even when the target's timing changes, which merely starting together does not give you.\n- triggers_animation: On a condition (params: trigger, delay, atTime) reveal a hidden target and pulse a" +
|
|
3545
|
+
"n event. It does not re-time the target's keyframes: to start B's track when A ends, use then / after.\n- tours / synced_to_audio: Move through a sequence of targets; bind playback to an audio track.\n- localizes_sound: a sound pans with an item. sourceId is the sound item (pinepaper_sound create's itemId), targetId the item it follows; the sound's stereo position follows the item across the frame, in the preview and in exported audio. params: axis (x | y, default x), spread (0..1: pan at the frame edge, default 0.85), reference (camera | canvas, default camera), distanceGain (true also quietens it away from the centre), smoothing (ms, default 80).\n- connects_to: Diagram edge from source " +
|
|
3546
|
+
"to target (params: via = the connector's item id, sourcePort / targetPort = auto | top | bottom | left | right, label). A RECORD of the connection: it draws nothing itself.\n- attached_to_tail / head_points_to: source = the connector, target = the item; params: port (auto | top | bottom | left | right). They record which port each end belongs to; the diagram system is what moves a connector's ends when an item moves, and it does so only for connectors made with pinepaper_connect. On a plain line (create itemType 'line') they record the structure and move nothing: for a connector that follows the items, make it with pinepaper_connect.\n- expresses: Play a facial expression over a root's par" +
|
|
3547
|
+
"t_of children (params: expression — blink|smile|surprise…, interval, duration, easing). No interval means sustained. It walks part_of from the ROOT, so parts with roles but no parent are INERT: the relation applies to nothing and reports success, which is what makes an unparented pair of eyes look finished while being unable to blink.\n- part_of: Semantic membership with a named role (params: role, offset, structural). structural: true records the part WITHOUT driving its position — the right choice when the parent is a group that already transforms its children, since driving it too would fight the group.\n- then / after: re-time a follower's whole track to start when the source's ends (t" +
|
|
3548
|
+
"hen: target after source; after: source after target), params delay, offset. Extending the source re-times the follower. A loop (a → b → a) is refused.\n- on_complete / at_beat: when the source's track ends (on_complete) or at a beat (at_beat: beat, bpm, or at in seconds, from the source's start) they pulse params.event (default: the target's id). A reaction hears an event by its id, so params.event is an event id; a name exactly one event holds is bound as that event's id (eventResolvedTo in the reply) and reveal the target if it is hidden (visible: false). They do NOT re-time the target's keyframes: for \"B starts when A ends\" use then. Bind reactions to the pulsed event for anything el" +
|
|
3549
|
+
"se.\n- phase_wave / ring_forming / flock: declared ONCE over a set: params.members { ids | names | tag } or a group as the source. Each member's phase, ring slot or boid comes from its rank in that set, so one relation per item gives every member slot 0. A batch relation op with a name pattern (sourceId \"disc-*\") binds them once over the matches. ring_forming centres on params.center or the target. To limit flock in time, params.window { start, end } works on every studio; a studio that names delay / duration in its params takes those too (an unread one is named in the reply).\n- soundtrack_cue: a sound at a beat or at the source's start / end (params sound, anchor, beat, bpm, offset, ga" +
|
|
3550
|
+
"in). What an export plays depends on the studio build: check the exported audio, and use on_event_play_sound on a timed event for a sound you need exactly.\n- camera_tracks / camera_follows: bind on the tracked item with a null target ('camera' as the source is refused). Whether they move the EXPORTED frame depends on the studio build: check the frames, and for a camera move you need in a video use camera_animates or pinepaper_camera_director.\n- staggered_with: Delay an item's animation by its place in a sequence (params: index, stagger, effect). For a SHAPED stagger — outward from the centre, converging from the edges, across a grid — also pass count (required: without it there is no \"e" +
|
|
3551
|
+
"nd\" to measure distance from), plus from (start|end|center|edges|random), amount (total seconds for the whole run, overrides stagger), grid (\"rows,cols\" or \"auto\"), axis (x|y) and distributeEase. Plain index x stagger still works and is taken first. To stagger items directly rather than through a relation, use pinepaper_stagger.\n\nINTERACTIVE RELATIONS — the state machine lives in the graph.\nAn event channel (pinepaper_event) is the hub: something FIRES it, and reactions bound to it run. Nothing here is a callback; it is all queryable graph, so it scrubs, exports and survives a reload.\n\n- Input triggers (item → event): on_click_fire, on_pointer_enter_fire, on_pointer_exit_fire, on" +
|
|
3552
|
+
"_key_fire (params: key, modifiers, global, preventDefault). { key: 'Enter' } makes Enter activate a button exactly as a click does. The chord is matched EXACTLY in both directions — { key: 'Enter' } does NOT fire on Ctrl+Enter, and { key: 's' } does not steal the browser's Ctrl+S; ask for a modifier with modifiers: { ctrl: true }. Listens only while the source item has focus unless global: true, so it will not take keys from the rest of the page.\n- Event reactions (event → item). Each reads its own params:\n · on_event_set_property { property, value } / _set_color { color, which } / _set_data { property, value } (into item.data) — write a value.\n · on_event_set_visibility { visible } —" +
|
|
3553
|
+
" true shows, false hides (value is not read, and is refused).\n · on_event_set_property_from_template — write a value built from a '{token}' TEMPLATE read off item.data, so the text can quote live state (\"Score: {score}\") instead of a literal fixed when you authored it.\n · on_event_increment (params: property, by) / on_event_toggle — counters and flip-flops.\n · on_event_fire_after (params: delay, timeline) — chain one beat to the next.\n · on_event_fire_if (params: predicate, event, else?) — the only reaction that TESTS graph state rather than mutating it. When the source fires it evaluates params.predicate and pulses the EVENT params.event if it holds (params.else if not); its tar" +
|
|
3554
|
+
"get is not fired, so the visible change comes from a reaction on that second event. params.predicate is an ExpressionIR object, not a string: { op:'const', value } · { op:'cmp', cmp: eq|neq|gt|gte|lt|lte, a, b } · { op:'logic', logic: and|or|not, args:[…] } · { op:'graph', read: data|active|visible, item, property } (data reads item.data[property]). A string predicate, or no event, is refused.\n · on_event_add_relation / on_event_remove_relation — the scene rewires itself.\n · 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 " +
|
|
3555
|
+
"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.\n · 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 t" +
|
|
3556
|
+
"wo widgets on a page do not collide. restores_from is the read half and fires ONCE at load.\n- 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.\n- Exclusive state: exclusive_group makes at most one member active — tabs, steppers, quiz cards. MEMBERSHIP is pairwise: bind exclusive_group from one member to each other member with the same relationOptions.groupName (default '_default'); nothing else adds an item to the group. Being active has NO LOOK OF ITS OWN: give each member on_enter_set_property / o" +
|
|
3557
|
+
"n_exit_set_property (say opacity 1 / 0.3), with the member as both source and target, and those reactions draw the change, in exports too. A current studio starts the group with its first member entered (relationOptions.initial: '<itemId>' picks another, 'none' starts with none), firing its on_enter reactions at t=0; an older studio starts with none entered, so every member shows its un-entered look until the first step. on_event_set_active activates its target. on_event_step_active moves the active member by relationOptions.delta (default +1) for next / previous arrows, wrapping unless wrap: false; a current studio runs it with no target, an older one needs a targetId (any item, e.g. one " +
|
|
3558
|
+
"member) and is refused without one, and its relationOptions.items only sets the step order of items already in the group. menubar_group is the same grouping WITHOUT the mutex.\n\nThere are 80 relation types and this list names the families rather than every member. For the live catalogue — what exists in THIS build, with definitions — call pinepaper_query_capabilities { kind: 'relation' }, which reads the engine's registry rather than a list written down here.\n\nRELATION COMPATIBILITY:\n- Universal relations (position-based) work with ALL item types including imported SVGs/images:\n orbits, follows, attached_to, maintains_distance, points_at, mirrors, parallax, bounds_to\n- morphs_to has" +
|
|
3559
|
+
" type-specific behavior:\n Path→Path: point interpolation\n Circle→Circle: radius interpolation\n Text→Text: glyph by glyph; shared glyphs travel, the rest cross-dissolve. params.mode: 'auto' (default), 'glyphs' (the same), 'dissolve' or 'particles' (the whole item cross-fades). Any other mode is refused.\n Cross-type: particle denoising transition\n A source with its own keyframes: on a channel its track keys, the target takes over at the keyed pose and the morph follows the track; an axis the track does not key travels to the target and stays there. A target that keys a channel keeps its own. A track that ended before the morph began sets no pose.\n- group_morphs_to: pair-by-index m" +
|
|
3560
|
+
"orph between two paper.Groups (any two — graph vertices+edges, letter collages, dashboard clusters). Path.Line children deform via endpoints; other children translate; excess children fade. Params: duration, hold, loop, easing (linear|easeIn|easeOut|easeInOut), deformLines.\n- moves_along_path: self-relation (targetId=null); item is driven along a path. Source the path EITHER from params.path (array of {x,y} or [x,y]) OR from params.equation (S10 B5) — a math curve the item TRAVERSES, sampled deterministically on the Expression IR: params.equation = { kind: function|parametric|fourier|preset, expr | xExpr/yExpr | harmonics | preset, min, max, samples, scale, cx, cy, flipY, warp } (same equ" +
|
|
3561
|
+
"ation contract as pinepaper_equation_path, but origin is cx/cy, default 0). y is MATH-UP by default (flipY: true): on screen the curve is mirrored about cy, so a curve written in screen coordinates (y down) needs flipY: false. Params: path OR equation, speed, closed, phase, easing (linear|easeIn|easeOut|easeInOut|sine|bounce|pingpong).\n\nGEOMETRIC CONSTRUCTION CONSTRAINTS (Layer 2) — the source item is RE-DERIVED every frame from its anchor item(s), so dragging an anchor updates the dependent live (GeoGebra-style), and the construction persists as relation graph data (edit it by editing relations, not by re-running code). Anchor A is the relation target; extra anchors ride in params:\n- i" +
|
|
3562
|
+
"s_midpoint_of: source = midpoint(target, params.other). params.other = id of the second endpoint (B).\n- lies_on_line: source held on the line target→params.other at fraction params.t (0=A, 1=B; default 0.5). params.other (B), params.t.\n- is_centroid_of: source = centroid(target, ...params.others). params.others = array of the remaining point ids.\n- is_circumcenter_of: source = circumcenter(target, params.other1, params.other2); holds position when the three anchors are collinear. params.other1 (B), params.other2 (C).\n- concentric_with: source stays centered on the target (shared center).\n- construction_reveal: self-relation (targetId=null); opacity 0→1 starting at params.revealAt (tim" +
|
|
3563
|
+
"eline seconds) over params.fadeIn seconds — loop- and scrub-correct. Pair with pinepaper_construction_sequence to reveal a figure one step at a time.\n\nPROCEDURAL / DETERMINISTIC BINDING (Expression IR — S10 G1) — bind a property to another property or to a math function of time. Pass params.signal:true to route through the pure, seed/frame-deterministic signal interpreter (replay-stable — same time → identical output); omit it for the per-frame compute fallback.\n- driven_by: source's sourceProperty = target's targetProperty * multiplier + offset (optionally clamped). sourceProperty ∈ x|y|rotation|opacity|scale|strokeWidth|fillColor|strokeColor; targetProperty ∈ x|y|rotation|opacity|scal" +
|
|
3564
|
+
"e. For fillColor/strokeColor the (clamped 0..1) driven value interpolates colorFrom→colorTo — so a relation drives COLOR. NEEDS a target (the property source). params: sourceProperty, targetProperty, multiplier, offset, clamp {min,max}, colorFrom, colorTo, signal. e.g. tint a dot by a planet's x: sourceProperty 'fillColor', targetProperty 'x', colorFrom '#0055ff', colorTo '#ff3300', signal true.\n- time_expression: self-relation (targetId=null); the source's params.property (x, y, rotation, opacity or scale) is driven by a formula of t (scene time) and v (params.baseValue). params: property (default 'y'), expression (e.g. 'sin(t*2)*50 + v'), baseValue, signal (true also evaluates it as a p" +
|
|
3565
|
+
"ure f(t) via the Expression IR). The formula grammar: " +
|
|
3562
3566
|
String(expressionGrammarText()) +
|
|
3563
3567
|
". A name outside it, or a wrong argument count, is refused by name: it would parse to nothing and the item would not move.\n\nEVENT-DRIVEN SCENE CHAINS (S11) — build a LONG scene as a graph of timed events, not a keyframe track. First make events with pinepaper_event (action:'create' → eventId). The relations below have an EVENT as their sourceId; firing the event runs the reaction. Chain beats over animation time with on_event_fire_after (timeline:'canvas'), have beats mutate the scene with on_event_add_relation, then kick it off with pinepaper_event action:'pulse'. Because canvas-timed fires are on the animation clock, the whole chain is scrub-, loop-, and replay-stable.\n- on_event_fire" +
|
|
3564
3568
|
"_after: sourceId=event, targetId=the NEXT event. When source fires, pulse target after params.delay ms. params.timeline: 'canvas' (animation clock — use for scene chains) | 'wall' (real setTimeout). This is the chaining primitive.\n- on_event_fire_if: sourceId=event, targetId=any item (it is not fired). params.event = the event to pulse when params.predicate holds, params.else = the one when it does not. A branch is two relations: A —on_event_fire_if { predicate, event: B }→ any item; B —on_event_set_color→ lamp. Example predicate, \"score is at least 3\": { op:'cmp', cmp:'gte', a:{ op:'graph', read:'data', item:'item_3', property:'score' }, b:{ op:'const', value:3 } }.\n- on_event_add_rel" +
|
|
@@ -3566,8 +3570,10 @@ String(expressionGrammarText()) +
|
|
|
3566
3570
|
"ourceId=event, targetId=item. params { visible } (default true; false hides). params.value is not read: it is refused.\n\nSTRUCTURAL LAYOUT (S12) — express STATIC COMPOSITION as relations instead of hardcoded x/y. Placement is computed from the TARGET'S BOUNDS and re-derived every frame, so moving or resizing the target moves the dependent with it, and the layout survives as editable graph data. Reach for these whenever the intent is \"on\", \"under\", \"next to\", \"inside\", or \"lined up with\" — a create_item with literal coordinates cannot express that intent and silently breaks when the target moves. All six need a target; all accept params.signal (deterministic IR port) and params.w" +
|
|
3567
3571
|
"indow.\n- on_top_of: source's BOTTOM edge rests on the target's TOP edge — stacking (a cocktail on a bar). params { gap (px between edges, default 0), align: 'left'|'center'(default)|'right', overhang (extra signed x-offset) }.\n- below: mirror of on_top_of — source's TOP edge rests on the target's BOTTOM edge. params { gap, align, overhang }.\n- beside: source flanks the target horizontally. params { side: 'left'|'right' (default 'right'), gap, align: 'top'|'center'(default)|'bottom' }.\n- inside: source is placed within the target's bounds at a 9-way anchor. params { anchor: 'center'(default)|'top-left'|'top'|'top-right'|'left'|'right'|'bottom-left'|'bottom'|'bottom-right', padding (inse" +
|
|
3568
3572
|
"t from the target edge) }. Contrast bounds_to, which CLAMPS a moving item; inside PLACES it.\n- centered_on: source center = target center + offset. params { offsetX, offsetY } — concentric rings at zero offset.\n- aligned_with: matches the target's center on ONE axis, leaving the other free (a partial write — use it to column-align a label while it still moves vertically). params { axis: 'x'|'y' — REQUIRED, the relation is inert without it, offset }.\n\nCURSOR / MOUSE AS TARGET — a relation can target the live pointer instead of an item by passing the reserved targetId 'cursor' (aliases 'mouse' / 'pointer'); collaborator cursors are 'cursor:<peerId>'. e.g. make an item flee the mouse: rel" +
|
|
3569
|
-
"ationType 'repels', targetId 'cursor', params { returnSpeed: 0.08 }. The cursor is a virtual target (no canvas item) and its interactions are inspectable in FxTool's \"Mouse / Cursor\" registry section.\n\nTEMPORAL VALIDITY WINDOWS (temporal-model v1) — ANY relation may carry params.window = { start, end?, repeat? } (scene seconds) to gate WHEN it is active. The engine only applies the relation within [start, end) and feeds it window-relative local time (so an orbit/wave starts its phase at the window open, not at scene 0). repeat ∈ once (default) | loop | pingpong. Omit end for open-ended. Windowless relations are always active.
|
|
3570
|
-
" does not
|
|
3573
|
+
"ationType 'repels', targetId 'cursor', params { returnSpeed: 0.08 }. The cursor is a virtual target (no canvas item) and its interactions are inspectable in FxTool's \"Mouse / Cursor\" registry section.\n\nTEMPORAL VALIDITY WINDOWS (temporal-model v1) — ANY relation may carry params.window = { start, end?, repeat? } (scene seconds) to gate WHEN it is active. The engine only applies the relation within [start, end) and feeds it window-relative local time (so an orbit/wave starts its phase at the window open, not at scene 0). repeat ∈ once (default) | loop | pingpong. Omit end for open-ended. Windowless relations are always active. A window gates on_event_* reactions too: outside it the even" +
|
|
3574
|
+
"t does not trigger the reaction. e.g. params: { ...relationParams, window: { start: 2, end: 5 } } makes the relation live only from 2s to 5s.\n\nVERIFYING ANIMATION WORKS:\nAfter adding a relation, verify the animation is running:\n1. Use pinepaper_browser_screenshot to see the canvas\n2. Wait 1-2 seconds and take another screenshot - items should have moved\n3. Use pinepaper_query_relations to confirm the relation was added\n\nRelations are COMPOSITIONAL - an item can have multiple relations that work together!"
|
|
3575
|
+
|
|
3576
|
+
|
|
3571
3577
|
|
|
3572
3578
|
|
|
3573
3579
|
|
|
@@ -3834,12 +3840,12 @@ USE WHEN:
|
|
|
3834
3840
|
},
|
|
3835
3841
|
description: ("SECURITY SWITCH: needs a server started with PINEPAPER_ALLOW_CODE=1 (off by default; only the person running the server can turn it on). Otherwise it is refused and nothing runs.\n\nExecute arbitrary JavaScript code in the PinePaper context.\n\nUSE WHEN:\n- Advanced Paper.js operations not covered by other tools\n- Complex mathematical calculations\n- Custom item registration\n- Accessing PinePaper APIs directly\n- Prototyping new features\n- A scene whose repetition is the point: dozens of items built in a loop. This is the cheapest route by an order of magnitude — a 63-item animated scene is ~4k characters here against ~147k as keyframe tool calls — because the loop collapses the repetit" +
|
|
3836
3842
|
"ion instead of restating it per item. Synchronous build work is bounded by the governor's 4s deadline, which such a scene fits easily.\n\nENGINE SEMANTICS THAT BITE RAW-JS AUTHORS. The tool schemas protect tool callers from these; writing JS opts out of that protection, and each of these fails SILENTLY:\n- app.create() returns a Paper.js item whose .id is a NUMBER. The id every tool and relation wants is the STRING at item.data.registryId ('item_1'). Passing the number to addAnimation throws \"Cannot create property 'data' on number\".\n- app.create() with an unknown type returns undefined and creates nothing — 'rect' is not 'rectangle'. The schema catches that for tool callers; a loop doe" +
|
|
3837
|
-
"s not.\n- app.addAnimation(id, keyframeArray, options) takes a real ARRAY. A JSON string installs nothing, silently, and the scene exports frozen. It also returns without error on an id that does not resolve.\n- Keyframes live at item.data.keyframes. app.keyframeItems is not the live store, so probing it reports false negatives.\n- transformOrigin is IGNORED by the keyframe engine: rotation always pivots on the item centre
|
|
3838
|
-
"
|
|
3839
|
-
"
|
|
3840
|
-
"
|
|
3841
|
-
"
|
|
3842
|
-
"
|
|
3843
|
+
"s not.\n- app.addAnimation(id, keyframeArray, options) takes a real ARRAY. A JSON string installs nothing, silently, and the scene exports frozen. It also returns without error on an id that does not resolve.\n- Keyframes live at item.data.keyframes. app.keyframeItems is not the live store, so probing it reports false negatives.\n- transformOrigin is IGNORED by the keyframe engine: rotation always pivots on the item's bounding-box centre (the same point as its position). On an asymmetric shape (triangle, star, text) that is not the shape's centroid, so it swings slightly off the visual middle; a full turn returns it home. For true joint pivots use pinepaper_rigging bones, or keyframe compu" +
|
|
3844
|
+
"ted x/y per joint.\n- Bone angles are RADIANS in the engine and relative to the parent, though pinepaper_rigging converts degrees for you — that conversion is the tool's, not the engine's.\n\nWHAT THE CHEAP ROUTE COSTS, so the choice is deliberate: items built and animated in raw JS carry NO behavioral record. No graph edge, nothing SMIL or widget export can lift, nothing the ontology can read — the scene renders and cannot describe itself. Relations are the canonical behavior surface here, so for motion that is a function of time prefer pinepaper_add_relation with 'time_expression' (still ~5.7x cheaper than keyframes), and for a whole group moving together staggered_with or wave_through. " +
|
|
3845
|
+
"Reach for a loop when the geometry is procedural, not merely to avoid the typing.\n\nAVAILABLE GLOBALS:\n- app: PinePaper application instance\n - app.create(type, params): Create items\n - app.executeGenerator(name, params): Run a generator — including a CUSTOM one you defined as app.<name> = function(params, options) { ...draw with paper... }. That is the whole draw contract: no registration step is required\n - app.registerItem(item, type, properties): THE DECISION THAT MATTERS when you draw your own. It returns a registry id and makes that item a first-class object: the USER can click it, select it and change its colour, and you can address it afterwards with modify_item, animate it" +
|
|
3846
|
+
", keyframe it, point a relation at it, or read it back through get_items and the ontology. An UNregistered path is pixels — it renders and exports identically, and nothing can ever touch it again without redrawing the scene. So register what someone might want to change (the kettle, the title, the sun) and leave the bulk unregistered (a thousand snow specks, a hatch field), which is exactly why generator output is only partly registered: it is a memory decision, and when you write the generator it is YOUR decision\n - app.addAnimation(itemId, keyframes, options): Attach keyframe TRACKS programmatically — the other half of a procedural scene, and what makes the loop route cheap: the keyfra" +
|
|
3847
|
+
"mes are generated, not typed. Accepts an id or the item itself; options.timeUnits ('seconds'|'ms') overrides the ms auto-detect, which long-form timelines need\n - app.animate(item, params): Attach a LOOP animation (pulse, bounce, wobble, …) — the other kind of motion, and not what addAnimation does\n - app.addRelation(sourceId, targetId, type, params): Add relations\n - app.registerRelationRule(name, definition): Register custom relations\n - app.itemRegistry: Access all registered items\n - app.relationRegistry: Access relation definitions\n- paper: Full Paper.js library\n - paper.Path, paper.Shape, paper.PointText, etc.\n - paper.view: Current view/canvas\n - paper.project: Acti" +
|
|
3848
|
+
"ve project\n\nEXAMPLE - Boolean operation:\ncode: `\nconst outer = new paper.Path.Circle({ center: [400, 300], radius: 80 });\nconst inner = new paper.Path.Circle({ center: [400, 300], radius: 40 });\nconst ring = outer.subtract(inner);\nring.fillColor = '#8b5cf6';\nconst id = app.registerItem(ring, 'ring', { source: 'mcp' });\nouter.remove();\ninner.remove();\nreturn { success: true, itemId: id };\n`"
|
|
3843
3849
|
|
|
3844
3850
|
|
|
3845
3851
|
|
|
@@ -4082,13 +4088,17 @@ String(EASING_LINES) +
|
|
|
4082
4088
|
enum: ['replace', 'merge'],
|
|
4083
4089
|
description: "The studio's spelling of append, accepted so its keyframe_track_replaced warning (\"Pass { mode: 'merge' }\") can be followed as written: 'merge' is append: true; 'replace' is the default. append and mode that disagree are refused as a contradiction.",
|
|
4084
4090
|
},
|
|
4091
|
+
keys: {
|
|
4092
|
+
type: 'object',
|
|
4093
|
+
description: 'The keyframes by column, instead of keyframes, and far cheaper to send for a track of many keys: {"time":[0,1,2],"x":[100,400,700],"y":[300,null,500],"easing":"easeOut"}. One list per property, each as long as time; a null means that key does not set that property; easing is one name, or a list with one per key. Evenly spaced keys take "time":{"step":0.033333,"count":180} (from defaults to 0). Passing both keys and keyframes is refused.',
|
|
4094
|
+
},
|
|
4085
4095
|
easingAt: {
|
|
4086
4096
|
type: 'string',
|
|
4087
4097
|
enum: ['arrival', 'departure'],
|
|
4088
4098
|
description: "Which segment a key's easing shapes. 'arrival' (default): the move ARRIVING at the key, so the first key's easing has no effect. 'departure': the move LEAVING the key, so an ease placed where a motion starts applies there. Current studio; an older one reads arrival and the result says so.",
|
|
4089
4099
|
},
|
|
4090
4100
|
},
|
|
4091
|
-
required: ['itemId'
|
|
4101
|
+
required: ['itemId'],
|
|
4092
4102
|
},
|
|
4093
4103
|
},
|
|
4094
4104
|
{
|
|
@@ -6334,7 +6344,7 @@ ACTIONS:
|
|
|
6334
6344
|
},
|
|
6335
6345
|
description: `Export current scene as a PineWidget (pp:PinePaper ontology JSON v0.4.3).
|
|
6336
6346
|
|
|
6337
|
-
Returns portable scene data with items, relations, animations, masks, and interactions serialized in the pp:PinePaper semantic format. Includes embed code snippet for external sites.
|
|
6347
|
+
Returns portable scene data with items, relations, animations, masks, and interactions serialized in the pp:PinePaper semantic format. Includes embed code snippet for external sites. Contraption rules, bound text, layout constraints and relation windows run in the widget; a rule with a custom action function is left out, with a warning (widget-contraption-custom).
|
|
6338
6348
|
|
|
6339
6349
|
Params: download (bool), filename, includeInteractions (bool, default true), minify (bool)`,
|
|
6340
6350
|
inputSchema: {
|
|
@@ -8139,7 +8149,7 @@ String(STYLED_SCENE_STYLES.map((k) => `${k} (${STYLED_SCENE_STYLE_INFO[k]})`).jo
|
|
|
8139
8149
|
itemId: { type: 'string' },
|
|
8140
8150
|
states: { type: 'array', items: { type: 'object', properties: { at: { type: 'number' }, duration: { type: 'number' }, to: { type: 'string' }, shape: { type: 'object', additionalProperties: true }, hideTarget: { type: 'boolean' } }, required: ['at'] } },
|
|
8141
8151
|
hideTarget: { type: 'boolean', description: 'Default for every state; false keeps the reference items visible (default true).' },
|
|
8142
|
-
easing: { type: 'string', enum: [
|
|
8152
|
+
easing: { type: 'string', enum: [...ENGINE_EASING_NAMES], description: 'Default easeInOut; back and elastic easings work too.' },
|
|
8143
8153
|
camera: { description: 'true, or {fill, easing}', oneOf: [{ type: 'boolean' }, { type: 'object', additionalProperties: true }] },
|
|
8144
8154
|
sound: { description: 'true, or a catalogue sound name', oneOf: [{ type: 'boolean' }, { type: 'string' }] },
|
|
8145
8155
|
},
|
|
@@ -8150,11 +8160,11 @@ String(STYLED_SCENE_STYLES.map((k) => `${k} (${STYLED_SCENE_STYLE_INFO[k]})`).jo
|
|
|
8150
8160
|
name: 'pinepaper_emitter',
|
|
8151
8161
|
annotations: { title: 'Particle Emitter', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
8152
8162
|
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.\n\nACTIONS:\n- create: { spec } → itemId\n- set: { itemId, spec } — merge a partial spec onto the emitter's.\n\nSPEC:\n- mode: 'burst' (default) — bursts: [{ t, x, y, count, …per-burst overrides }]: a pop of particles at scene time t.\n- mode: 'trail' (or give target) — target (an item id), rate (per second), emitWindows [[t0, t1], …] (when it sheds), window [t0, " +
|
|
8153
|
-
"t1] (when the WHOLE trail shows: it switches on and off at those times, with what was shed before t0 already there), offset ([dx, dy] or {x, y}): particles shed from a moving item. rate, start and end are trail settings: a burst emitter does not read them.\n- look and physics (per emitter or per burst): shape ('circle' | 'rect' | 'dash' | 'star' | 'confetti', or an array; size is a circle's diameter, a rect's or confetti's width (height is half of it), and a star's extent is 0.8 of it; a dash is a thin line drawn forward from the particle along its velocity
|
|
8154
|
-
"velocities[i % length], so give one per particle — the reply warns when count and the list differ), gravity (px/s², +down), drag (slows the launch velocity only: what gravity adds is not damped, so a falling particle has no terminal velocity), friction, lifetime, fadeIn / fadeOut, spin, opacity, jitter, jitterGrow, seed.\nUNITS (a number, or a [min, max] range picked per particle where it says range):\n- count: particles per burst, at most 10000 on the current studio (2000 on older studios) — a larger count is drawn at the cap and the reply warns with the number drawn. For more, add a second burst at the same t.\n
|
|
8155
|
-
" px/s². lifetime: seconds (range, > 0). size: px (range).\n- gravity, drag, friction, opacity, sizeEnd and jitter also take a range on the current studio, sampled per particle; an older studio draws the midpoint and warns emitter-range-not-read.\n- fadeIn / fadeOut: SECONDS at the start / end of each particle's life. fadeOut is capped at that particle's lifetime (a 0.5 s life cannot fade for 2 s; it fades over its whole life instead), and the current studio warns when it caps one. Omitted (or fade: 'linear'), the particle fades linearly over its WHOLE life; fade: 'none' turns both off.\n- spin: DEGREES PER SECOND,
|
|
8156
|
-
"
|
|
8157
|
-
"
|
|
8163
|
+
"t1] (when the WHOLE trail shows: it switches on and off at those times, with what was shed before t0 already there), offset ([dx, dy] or {x, y}): particles shed from a moving item. rate, start and end are trail settings: a burst emitter does not read them.\n- look and physics (per emitter or per burst): shape ('circle' | 'rect' | 'dash' | 'star' | 'confetti', or an array; size is a circle's diameter, a rect's or confetti's width (height is half of it), and a star's extent is 0.8 of it; a dash is a thin line drawn forward from the particle along its velocity; confetti tumbles, with a seeded tilt and a darker back, and a rect does not), palette [colours], size / sizes / sizeEnd, speed + angl" +
|
|
8164
|
+
"e ranges or velocities ([[vx, vy], …] px/s, or [{x, y}, …]; particle i takes velocities[i % length], so give one per particle — the reply warns when count and the list differ), gravity (px/s², +down), drag (slows the launch velocity only: what gravity adds is not damped, so a falling particle has no terminal velocity), friction, lifetime, fadeIn / fadeOut, spin, opacity, jitter, jitterGrow, seed.\nUNITS (a number, or a [min, max] range picked per particle where it says range):\n- count: particles per burst, at most 10000 on the current studio (2000 on older studios) — a larger count is drawn at the cap and the reply warns with the number drawn. For more, add a second burst at the same t.\n" +
|
|
8165
|
+
"- speed: px/s (range). angle: degrees, 0 = right, 90 = down (range). gravity: px/s². lifetime: seconds (range, > 0). size: px (range).\n- gravity, drag, friction, opacity, sizeEnd and jitter also take a range on the current studio, sampled per particle; an older studio draws the midpoint and warns emitter-range-not-read.\n- fadeIn / fadeOut: SECONDS at the start / end of each particle's life. fadeOut is capped at that particle's lifetime (a 0.5 s life cannot fade for 2 s; it fades over its whole life instead), and the current studio warns when it caps one. Omitted (or fade: 'linear'), the particle fades linearly over its WHOLE life; fade: 'none' turns both off.\n- spin: DEGREES PER SECOND," +
|
|
8166
|
+
" positive = clockwise on screen (range; spinAlternate flips every other particle). It turns rect, confetti, star and dash particles; a circle has no orientation, so spin on circles shows nothing (the current studio warns when every particle is a circle).\n- jitter: PIXELS — on the current studio each particle is displaced up to ±jitter px on each axis from its path, at full size from birth; jitterGrow: true keeps the older ramp (zero at birth, the full ±jitter at the end of life). Older studios (before jitterGrow) displace up to ±jitter/2, growing from 0 at birth. jitterHz: how many times a second that offset is re-rolled (0, the default, keeps one offset per particle).\nAn unknown shape, " +
|
|
8167
|
+
"mode or bad range is refused with the engine's reason — never defaulted.\n\nEXAMPLE — confetti at 2 s:\n{ 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 } }"
|
|
8158
8168
|
|
|
8159
8169
|
|
|
8160
8170
|
|
|
@@ -8829,10 +8839,14 @@ String(KEYFRAME_EASINGS_LIST) +
|
|
|
8829
8839
|
" by name; the result has a receipt per tween.\n {type: \"timeline\", defaults: {ease: \"power2.out\"}, tweens: [{to: \"bar-*\", from: {scaleY: 0}, vars: {scaleY: 1}, duration: 0.8, at: 0.5, stagger: 0.15}, {to: \"ring\", vars: {rotation: 480}, duration: 8, ease: \"none\", at: 0}]}\n relation — Behavioral link: {sourceId, targetId, relationType, relationOptions}\n relationType: orbits, follows, attached_to, points_at, mirrors, parallax, wave_through, morphs_to, group_morphs_to, moves_along_path (+ 9 more in schema)\n\n MOVE vs ANIMATE vs PATH (pick by intent): instant reposition → modify {x,y}. Animated A→B move → keyframe_animate two keyframes [{time:0,{x,y}},{time:T,{x',y'}}]. Fol" +
|
|
8830
8840
|
"low a multi-point path the user lists → relation moves_along_path {params:{path:[{x,y},…]}}.\n\nEFFECTS:\n apply_mask — Reveal effect: {itemId, maskPreset, maskOptions}\n maskPreset: wipeLeft, wipeRight, wipeUp, wipeDown, iris, irisOut, star, heart, curtainHorizontal, curtainVertical, cinematic, diagonalWipe, revealUp, revealDown\n apply_effect — Visual effect: {itemId, effectType, effectParams}\n effectType: " +
|
|
8831
8841
|
String(EFFECT_TYPES_LIST) +
|
|
8832
|
-
"\n\nPLAYBACK:\n play_timeline — {action: \"play\"|\"stop\"|\"seek\", duration, loop}\n\
|
|
8833
|
-
"
|
|
8834
|
-
"
|
|
8835
|
-
"
|
|
8842
|
+
"\n\nPLAYBACK:\n play_timeline — {action: \"play\"|\"stop\"|\"seek\", duration, loop}\n\nCOMPACT FORMS (they run as the long forms do, and cost a third as much to send on a dense scene):\n create_many — {itemType, name, properties, items}: one create per item. properties is what they share; an item is [x, y] or {x, y, …what differs}. name is a prefix (name-0, name-1 …) unless an item names itself. Slow past a few hundred (2000 took about a minute).\n keys on keyframe_animate — {\"time\":[0,1,2],\"x\":[100,400,700],\"y\":[300,null,500],\"easing\":\"easeOut\"} in place of keyframes: one list per property, each as long as time; a null means that key does not set that property; easing is a n" +
|
|
8843
|
+
"ame or a list with one per key. Evenly spaced keys: \"time\":{\"step\":0.033333,\"count\":180}.\n\nVARIABLE REFERENCES: \"$0\", \"$1\" etc. reference the items this batch makes, in op order: create and group ops each take the next number (a ring / grid / orbit / create_many that makes items takes one per item); no other op does. [create, create, group, …] makes the group \"$2\". Place the ops that make an item before any op that references it.\n\nANIMATION NOTES:\n- keyframe_animate position accepts [x, y] or { x, y } (normalized). fillColor/strokeColor accept rgba() for alpha. The create \"opacity\" and \"blendMode\" props are applied to the item.\n- For MP4/GIF export the keyframe timeli" +
|
|
8844
|
+
"ne is rendered once across the duration; author motion to fill the clip and use loop:false. \n\nEXAMPLE — Animated sky scene with timed reveals:\n{operations: [\n {type: \"set_canvas_size\", preset: \"full-hd-1080p\"},\n {type: \"execute_generator\", generatorName: \"drawGradientMesh\", generatorParams: {colors: [\"#1e3a5f\", \"#87CEEB\", \"#fbbf24\"], bgColor: \"#0c1445\", blobCount: 4}},\n {type: \"create\", itemType: \"circle\", position: {x: 1600, y: 150}, properties: {radius: 80, color: \"#fbbf24\", shadowColor: \"#fbbf24\", shadowBlur: 40}},\n {type: \"create\", itemType: \"ellipse\", position: {x: 400, y: 180}, properties: {radiusX: 100, radiusY: 35, color: \"#ffffff\", opacity:" +
|
|
8845
|
+
" 0.8}},\n {type: \"create\", itemType: \"ellipse\", position: {x: 900, y: 120}, properties: {radiusX: 120, radiusY: 40, color: \"#ffffff\", opacity: 0.7}},\n {type: \"create\", itemType: \"text\", position: {x: 960, y: 500}, properties: {content: \"Dream Big\", fontSize: 96, color: \"#ffffff\", fontWeight: \"bold\"}},\n {type: \"animate\", itemId: \"$0\", animationType: \"pulse\", animationOptions: {speed: 0.3}},\n {type: \"animate\", itemId: \"$1\", animationType: \"slideLeftRight\", animationOptions: {speed: 0.2}},\n {type: \"animate\", itemId: \"$2\", animationType: \"slide\", animationOptions: {speed: 0.15, direction: \"left\"}},\n {type: \"keyframe_animate\", itemId: \"$3\", key" +
|
|
8846
|
+
"frames: [{time: 0, properties: {opacity: 0, scale: 0.8}}, {time: 2, properties: {opacity: 1, scale: 1}, easing: \"easeOutBack\"}], duration: 3},\n {type: \"apply_mask\", itemId: \"$3\", maskPreset: \"wipeLeft\"},\n {type: \"play_timeline\", action: \"play\", duration: 5, loop: true}\n]}"
|
|
8847
|
+
|
|
8848
|
+
|
|
8849
|
+
|
|
8836
8850
|
|
|
8837
8851
|
|
|
8838
8852
|
|
|
@@ -8906,7 +8920,7 @@ String(EFFECT_TYPES_LIST) +
|
|
|
8906
8920
|
properties: {
|
|
8907
8921
|
type: {
|
|
8908
8922
|
type: 'string',
|
|
8909
|
-
enum: ['set_canvas_size', 'set_background', 'execute_generator', 'create', 'modify', 'delete', 'group', 'animate', 'keyframe_animate', 'relation', 'apply_mask', 'apply_effect', 'play_timeline', 'ring', 'grid', 'orbit', 'stagger', 'patch', 'timeline'],
|
|
8923
|
+
enum: ['set_canvas_size', 'set_background', 'execute_generator', 'create', 'modify', 'delete', 'group', 'animate', 'keyframe_animate', 'relation', 'apply_mask', 'apply_effect', 'play_timeline', 'ring', 'grid', 'orbit', 'stagger', 'patch', 'create_many', 'timeline'],
|
|
8910
8924
|
description: 'Operation type. timeline compiles a GSAP-shaped timeline onto keyframe tracks. ring / grid make many items in one op (each a create, so each fills a $N slot in order), orbit binds an orbits relation (making the orbiting item when itemType is given), stagger plays the same keyframes on itemIds one after another, patch changes the item with that name (refused when several items hold it).',
|
|
8911
8925
|
},
|
|
8912
8926
|
// Canvas setup
|
|
@@ -8957,8 +8971,10 @@ String(EFFECT_TYPES_LIST) +
|
|
|
8957
8971
|
easing: { type: 'string' },
|
|
8958
8972
|
},
|
|
8959
8973
|
},
|
|
8960
|
-
description: 'For keyframe_animate: [{time, properties, easing}]',
|
|
8974
|
+
description: 'For keyframe_animate: [{time, properties, easing}], or send keys instead',
|
|
8961
8975
|
},
|
|
8976
|
+
keys: { type: 'object', description: 'For keyframe_animate, ring, grid and stagger: the keyframes by column instead of keyframes: {"time":[0,1],"x":[100,400],"easing":"easeOut"}. One list per property, each as long as time; a null means that key does not set that property. time may be {step, count, from?} for evenly spaced keys.' },
|
|
8977
|
+
items: { type: 'array', description: 'For create_many: each item, as [x, y] or {x, y, …what differs from properties}' },
|
|
8962
8978
|
duration: { type: 'number', description: 'Duration in seconds for keyframe_animate or play_timeline' },
|
|
8963
8979
|
loop: { type: 'boolean', description: 'Loop for keyframe_animate or play_timeline' },
|
|
8964
8980
|
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." },
|
|
@@ -9169,7 +9185,7 @@ String(AgentExportFormatSchema.options.join(', ')) +
|
|
|
9169
9185
|
loop: { anyOf: [{ type: 'boolean' }, { type: 'integer', minimum: 0, maximum: 1000 }], description: 'gif / apng: true = loop forever, false / 0 / 1 = play once, n = play n times (email clients often want a few plays, not forever). Refused on video formats — see SEAMLESS LOOPS.' },
|
|
9170
9186
|
broadcast: { type: 'boolean', description: 'mp4 only: broadcast-safe — BT.709, limited range (16-235), tagged bt709, constant bitrate with an 8 Mbps floor at 720p+ (4 below). result.video reports what the encoder did.' },
|
|
9171
9187
|
frames: { type: 'integer', minimum: 1, maximum: 36000, description: 'Animated formats: exactly this many frames instead of duration. A duration makes ceil(duration x fps) frames (7.91667 s at 24 fps is 191, not 190), so use frames for frame-exact segments.' },
|
|
9172
|
-
deterministic: { type: 'boolean', description: 'mp4 / webm: identical encoder input for the same scene, seed and settings (the receipt\'s frameHashes / frameDigest), and a container identical apart from the encoded samples. WebM is byte-identical. An MP4 is byte-identical only when result.video reports bitExact: true (same machine and browser build); a software H.264 MP4 at 640x480 and above is not, and warns determinism_not_pinned. Compare MP4s by frameDigest, not sha256.' },
|
|
9188
|
+
deterministic: { type: 'boolean', description: 'mp4 / webm, on by default (false opts out): identical encoder input for the same scene, seed and settings (the receipt\'s frameHashes / frameDigest), and a container identical apart from the encoded samples. WebM is byte-identical. An MP4 is byte-identical only when result.video reports bitExact: true (same machine and browser build); a software H.264 MP4 at 640x480 and above is not, and warns determinism_not_pinned. Compare MP4s by frameDigest, not sha256.' },
|
|
9173
9189
|
motionBlur: {
|
|
9174
9190
|
type: 'object',
|
|
9175
9191
|
description: 'mp4 / webm: real motion blur. Each frame is rendered `subframes` times across the shutter and averaged, so fast movement smears instead of strobing. Costs subframes× the render time: run estimateOnly first (it reports renders and renderCost). The result carries motionBlur (what the exporter applied).',
|
|
@@ -9187,7 +9203,7 @@ String(AgentExportFormatSchema.options.join(', ')) +
|
|
|
9187
9203
|
time: { type: 'number', minimum: 0, description: 'Stills only (png / jpg / webp / svg / pdf): render at this time in seconds; the playhead is put back afterwards. Without it a still is whatever frame the playhead is on, so repeated runs can differ.' },
|
|
9188
9204
|
pdf: {
|
|
9189
9205
|
type: 'object',
|
|
9190
|
-
description: "pdf only: print options. paperFormat 'a4' | 'a3' | 'letter' | … | 'custom' (canvas size, default); orientation; bleed in mm (> 0 includes the bleed area); trimMarks adds crop marks; dpi 72–600.
|
|
9206
|
+
description: "pdf only: print options. paperFormat 'a4' | 'a3' | 'letter' | … | 'custom' (canvas size, default); orientation; bleed in mm (> 0 includes the bleed area); trimMarks adds crop marks; dpi 72–600. With paperFormat 'custom' the page is the canvas plus bleed; on a named size the trim is that sheet and the page is the sheet plus 2 x bleed. trimMarks adds an 8 mm margin per side for the marks where the studio draws them; a studio that draws none on a named size says so in the result's warnings, and the result's print record (where present) says what the page carries. The canvas's physical size comes from its preset's DPI: start the job with canvasPreset 'print-a4' / 'print-letter' / 'print-a4-landscape' / 'print-letter-landscape' (300 dpi) for print; a hand-sized canvas is read at 96 dpi.",
|
|
9191
9207
|
properties: {
|
|
9192
9208
|
paperFormat: { type: 'string' },
|
|
9193
9209
|
orientation: { type: 'string', enum: ['portrait', 'landscape'] },
|
|
@@ -9200,7 +9216,7 @@ String(AgentExportFormatSchema.options.join(', ')) +
|
|
|
9200
9216
|
},
|
|
9201
9217
|
region: {
|
|
9202
9218
|
type: 'object',
|
|
9203
|
-
description: 'png / jpg / webp only: export just this part of the canvas (canvas coordinates, x/y = TOP-LEFT) — carousel slices, crops. Output is the region\'s own size unless outputWidth/outputHeight say otherwise; a different aspect is
|
|
9219
|
+
description: 'png / jpg / webp only: export just this part of the canvas (canvas coordinates, x/y = TOP-LEFT) — carousel slices, crops. Output is the region\'s own size unless outputWidth/outputHeight say otherwise; a different aspect is cropped about the region\'s centre (the reply\'s croppedTo says to what), never stretched and never filled from outside the region. SMALL ASSETS (emotes, badges, favicons): design at 2-4x on a canvas of at least 100 px, then export the whole board as a region with the small outputWidth/outputHeight — e.g. a 112x112 canvas, region {x:0, y:0, width:112, height:112, outputWidth:28, outputHeight:28}.',
|
|
9204
9220
|
properties: {
|
|
9205
9221
|
x: { type: 'number' }, y: { type: 'number' },
|
|
9206
9222
|
width: { type: 'number', exclusiveMinimum: 0 }, height: { type: 'number', exclusiveMinimum: 0 },
|
|
@@ -9708,8 +9724,10 @@ USE WHEN:
|
|
|
9708
9724
|
openWorldHint: false,
|
|
9709
9725
|
},
|
|
9710
9726
|
description: ("Get detailed guidance for PinePaper tools on demand.\n\nUSE WHEN: You need full parameter docs, examples, or workflow guidance — especially in minimal verbosity mode where tool descriptions are short summaries.\n\nPREFER RESOURCES WHERE POSSIBLE: The full AI Agent Guide is also available via the MCP resources API at pinepaper://docs/agent-guide — clients that support resources should pull it that way rather than calling this tool. Per-domain references live at pinepaper://docs/{generators,relations,masks,diagrams,maps,effects-and-filters,fonts,custom-paths,paperjs-in-pinepaper,templates,customization,getting-started}. Use this tool when the client doesn't support resources or when you want" +
|
|
9711
|
-
" a specific tool's verbose description.\n\nTHE AGENT LANE: for a scene built from shapes, text, keyframes and relations and exported to a file,
|
|
9712
|
-
" masks, camera, generators, effects, filters, scene, template, query, export, diagram, map, font, letter_collage, triggers, quiz, performance, custom_code, p5, register, guide"
|
|
9727
|
+
" a specific tool's verbose description.\n\nTHE AGENT LANE: for a scene built from shapes, text, keyframes and relations and exported to a file, an eight-tool lane carries the whole contract in its descriptions (no guide, no lessons file): start the server with PINEPAPER_TOOLKIT=lane, or pinepaper_set_toolkit {toolkit: \"lane\"}.\n\nTHE STARTER: toolkit \"starter\" is the lane's first five tools, the fewest that make a first scene: the batch (create, with relations as ops), keyframes, patterns, export and lint. Events, starting over and custom relations need the lane.\n\nMODES:\n- No args → returns the full AI Agent Guide (same content as pinepaper://docs/agent-guide)\n- tool: \"pinepaper_a" +
|
|
9728
|
+
"gent_batch_execute\" → returns that tool's full verbose description\n- category: \"animation\" → returns all tools in that category with full descriptions\n\nCATEGORIES: agent, browser, canvas, core, import, assets, batch, relations, animation, masks, camera, generators, effects, filters, scene, template, query, export, diagram, map, font, letter_collage, triggers, quiz, performance, custom_code, p5, register, guide"
|
|
9729
|
+
|
|
9730
|
+
|
|
9713
9731
|
|
|
9714
9732
|
|
|
9715
9733
|
|
|
@@ -9749,8 +9767,9 @@ USE WHEN:
|
|
|
9749
9767
|
},
|
|
9750
9768
|
description: ("Load more tools, or switch the toolkit profile and/or verbosity, at runtime. The tools/list updates immediately.\n\nThe list starts with the CORE tools (the craft tools, the agent workflow, canvas and query). Everything else loads on demand:\n- load: [\"map\"] adds a group; load: [\"<a tool's name>\"] adds one tool. Loaded tools stay for the session, and the tools they name come with them.\n- Groups: " +
|
|
9751
9769
|
String(Object.keys(TOOL_TAGS).join(', ')) +
|
|
9752
|
-
".\n\nUSE WHEN:\n- You need a tool that is not listed (maps, diagrams, fonts, physics, ontology…) → load its group or its name\n- You want everything at once → toolkit: 'full' (or 'agent', which leaves out three diagnostics)\n- You want more/less detail in tool descriptions → switch verbosity\n\nPROFILES:\n- core: the craft tools and what they name, the agent workflow, canvas and query — default\n- full: Every tool, no filtering\n- agent: Every tool except the diagnostics (get_performance_metrics, diagnostic_report) and register_item\n- diagram: Diagram/flowchart tools\n- map: Map/choropleth tools\n- font: Font creation tools\n- minimal: Bare essentials\n-
|
|
9753
|
-
" (batch_execute, keyframe_animate, pattern, agent_export, lint_scene, event, clear_canvas) whose descriptions carry everything needed to build and export a shape/text/keyframe scene; set it for a scripted build that needs nothing else. It lists no way back: set it last.\n\nVERBOSITY:\n- verbose: Full descriptions\n- compact: Shorter descriptions for 13 largest tools — default\n- minimal: 1-line summaries (use pinepaper_tool_guide for details)"
|
|
9770
|
+
".\n\nUSE WHEN:\n- You need a tool that is not listed (maps, diagrams, fonts, physics, ontology…) → load its group or its name\n- You want everything at once → toolkit: 'full' (or 'agent', which leaves out three diagnostics)\n- You want more/less detail in tool descriptions → switch verbosity\n\nPROFILES:\n- core: the craft tools and what they name, the agent workflow, canvas and query — default\n- full: Every tool, no filtering\n- agent: Every tool except the diagnostics (get_performance_metrics, diagnostic_report) and register_item\n- diagram: Diagram/flowchart tools\n- map: Map/choropleth tools\n- font: Font creation tools\n- minimal: Bare essentials\n- starter: the lane's first five too" +
|
|
9771
|
+
"ls (batch_execute, keyframe_animate, pattern, agent_export, lint_scene), for a first scene\n- lane: the agent lane — eight tools (batch_execute, keyframe_animate, pattern, agent_export, lint_scene, event, clear_canvas, register_custom_relation) whose descriptions carry everything needed to build and export a shape/text/keyframe scene; set it for a scripted build that needs nothing else. It lists no way back: set it last.\n\nVERBOSITY:\n- verbose: Full descriptions\n- compact: Shorter descriptions for 13 largest tools — default\n- minimal: 1-line summaries (use pinepaper_tool_guide for details)"
|
|
9772
|
+
|
|
9754
9773
|
|
|
9755
9774
|
|
|
9756
9775
|
|
|
@@ -9778,7 +9797,7 @@ String(Object.keys(TOOL_TAGS).join(', ')) +
|
|
|
9778
9797
|
toolkit: {
|
|
9779
9798
|
type: 'string',
|
|
9780
9799
|
description: 'Toolkit profile: core (default), full, agent, diagram, map, font, minimal, lane',
|
|
9781
|
-
enum: ['core', 'full', 'agent', 'diagram', 'map', 'font', 'minimal', 'lane', 'edit'],
|
|
9800
|
+
enum: ['core', 'full', 'agent', 'diagram', 'map', 'font', 'minimal', 'lane', 'edit', 'starter'],
|
|
9782
9801
|
},
|
|
9783
9802
|
load: {
|
|
9784
9803
|
type: 'array',
|