@pinepaper.studio/mcp-server 1.6.35 → 1.6.37
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.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.hi.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +139 -7
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/agent/types.d.ts +12 -12
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +7 -1
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +2 -2
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +141 -91
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/effect-class.d.ts.map +1 -1
- package/dist/tools/effect-class.js +4 -0
- package/dist/tools/effect-class.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +30 -3
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +346 -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 +4 -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 +29 -7
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/export-file-receipt.d.ts +33 -0
- package/dist/tools/export-file-receipt.d.ts.map +1 -1
- package/dist/tools/export-file-receipt.js +49 -0
- package/dist/tools/export-file-receipt.js.map +1 -1
- package/dist/tools/frozen-output.d.ts +33 -0
- package/dist/tools/frozen-output.d.ts.map +1 -0
- package/dist/tools/frozen-output.js +73 -0
- package/dist/tools/frozen-output.js.map +1 -0
- package/dist/tools/handlers.d.ts +15 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +141 -15
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +1 -0
- 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.map +1 -1
- package/dist/tools/toolkits.js +2 -1
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +18 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +462 -198
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +112 -270
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +46 -145
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/asset-paths.js +1 -1
- package/dist/utils/asset-paths.js.map +1 -1
- package/manifest.json +5 -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 { 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';
|
|
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, ENGINE_EFFECT_TYPES } 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
|
|
@@ -44,6 +44,7 @@ const DOORS = [
|
|
|
44
44
|
{ 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)' }] },
|
|
45
45
|
{ when: 'A brand', doors: [{ tool: 'pinepaper_brand_kit', text: 'pinepaper_brand_kit set (first): every item made afterwards inherits it' }] },
|
|
46
46
|
{ 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' }] },
|
|
47
|
+
{ when: 'Thousands to a million similar marks (a crowd, swarm, starfield, rain, sand, a confetti field)', doors: [{ tool: 'pinepaper_instances', text: 'pinepaper_instances (rules first): one item, not one per mark' }] },
|
|
47
48
|
{ 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' }] },
|
|
48
49
|
{ 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' }] },
|
|
49
50
|
];
|
|
@@ -424,25 +425,25 @@ IMPORTANT NOTES:
|
|
|
424
425
|
"rking feature does nothing.\n- DO NOT take screenshots after every create operation. Trust the API response - if it returns success, the item was created. Take ONE screenshot at the end to verify the final result.\n\n⚠️ COMPLEX CHARACTERS/ILLUSTRATIONS:\nFor requests like \"draw a witch\", \"draw a cat face\", \"draw a person\", or any detailed characters:\n- These are VERY time-consuming to draw with paths (10-20+ operations per character)\n- Use pinepaper_import_svg instead - import pre-made SVG graphics\n- Or compose from simple shapes (circle for head, triangle for hat, etc.)\n- DO NOT attempt pixel-level character drawing with paths\n\nITEM TYPES:\n\nEVERY type below also takes an anc" +
|
|
425
426
|
"hor, which says WHICH CORNER your x/y meant.\nPosition is the bounding-box CENTRE by default. Anything that computed a layout\nbox — a design system, or any CSS-shaped mental model — authored the TOP-LEFT\ninstead, and passing those coordinates straight in displaces every item by half\nits own size. anchor: 'top-left' (also top-right, bottom-left, bottom-right,\ncenter; origin is an alias for the parameter) fixes that without you computing\nbounds you cannot know until the item exists. It is applied after size and\nrotation, so it uses the item's real box.\nPIXEL-EXACT SLOTS (a spec'd 615x345 end-screen box, a UI frame): use anchor\n'top-left' with WHOLE-NUMBER x, y, width and height. A ce" +
|
|
426
427
|
"ntred box with an odd\nwidth or height has its edges on half pixels, which renders 1 px short with a\nsoft anti-aliased edge (measured: 615x345 centred came out 614x344).\n\nEVERY type below also takes a label, which wraps the item in a labelled group —\nyou do not need a second create call and a hand-computed position to caption a\nshape. A string is the short form; the object form places and styles it:\nlabel: { content: 'Start', position: 'top', offset: [0, 10], fontSize: 14,\nfontFamily: 'Inter, sans-serif', color: '#ffffff', backgroundColor: null }.\nposition takes center (default), top, bottom, left, right or a corner. Use\nlabel.position for placement: it works here today, and where" +
|
|
427
|
-
" the sibling\nlabelPosition spelling is also accepted, label.position wins.\nLabelling a text item gives you TWO text items in the group — the body and the\nlabel. That is what you want for a caption; it is a surprise if you expected\nthe label to replace the content.\n\n- text: Text content with font styling (properties: content, fontSize, color, fontFamily, justification, fontWeight, fontStyle, leading, lineHeight, letterSpacing, wordSpacing, textTransform, textDecoration)\n fontWeight: 400 / 700 / 'bold'.
|
|
428
|
-
" in px / em; it animates), textTransform ('uppercase' / 'lowercase' / 'capitalize' / 'none'), textDecoration ('underline' / 'line-through' / 'overline', combinable as 'underline overline'; 'none' clears) and textAlign: 'justify' (every line but the last stretched at its spaces) are drawn, on create, modify and keyframes; the result's typography reads back what the item holds. An older studio does not draw them, and its result says so (typography.applied: false; keyframes name them in
|
|
429
|
-
"
|
|
430
|
-
"studio applied it; a studio without RTL support lays text out left-to-right and says so. justification: 'right' is alignment, not direction.\n tabularFigures: true (or fontVariantNumeric: 'tabular-nums', or fontFeatures containing 'tnum') — fixed-width digits, so a counter, price or timer does not jitter as it changes. The result's tabularFigures.applied is measured on the current studio: true only when the layout moves a digit's advance; false with a reason (font-digits-already-equal: the font
|
|
431
|
-
"outside' — the stroke sits entirely outside the letters (drawn under the fill at twice strokeWidth), so a thick caption / meme outline keeps the letterform; 'center' (default) is the ordinary stroke. The result's strokePosition says whether the studio applied it.\n- circle: Circular shape (properties: radius, color, strokeColor, strokeWidth)\n- star: Star shape (properties: radius1, radius2, points, color)\n radius1 is the OUTER radius and is what sizes the star; radius or\n width/height size
|
|
432
|
-
"sets that\n same waist directly and needs no radii at all. points defaults to 5.\n A star is always radially symmetric — given width and height it uses the\n smaller — and the size you give is the radius BOX, the circle the tips\n touch. The visible bounds are never larger than that box and usually\n smaller — they fill it only when points is a multiple of 4.\n- rectangle: Rectangle (properties: width, height, color, cornerRadius)\n- triangle: Triangular shape (properties: width, height,
|
|
433
|
-
"
|
|
434
|
-
" with N sides (properties: sides, radius, color)\n points additionally accepts an explicit VERTEX LIST — [[x, y], ...] or\n [{x, y}, ...], three or more, in canvas coordinates — which builds that\n exact, possibly IRREGULAR polygon and ignores sides. Reach for this before\n path when the outline is straight-edged. A plain number in points is not a\n vertex list and falls through to sides (default 6).\n In the vertex-list form the coordinates are ABSOLUTE canvas coordinates and\n position
|
|
435
|
-
"color) — defaults to 100x60\n- path: Custom path with segments or SVG data (properties: segments, pathData, strokeColor, fillColor, closed, smooth)\n color on a path: a CLOSED path (closed:true, or pathData ending in Z) is FILLED with it; an OPEN path is STROKED with it. Pass fillColor / strokeColor to say exactly which.\n- line: Line between two points (properties: from, to, strokeColor, strokeWidth)\n- arc: Curved arc through three points (properties: from, through, to, strokeColor)\n-
|
|
436
|
-
"
|
|
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-
|
|
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
|
|
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
|
|
440
|
-
"
|
|
441
|
-
"
|
|
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
|
|
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
|
|
444
|
-
"
|
|
445
|
-
|
|
428
|
+
" the sibling\nlabelPosition spelling is also accepted, label.position wins.\nLabelling a text item gives you TWO text items in the group — the body and the\nlabel. That is what you want for a caption; it is a surprise if you expected\nthe label to replace the content.\n\n- text: Text content with font styling (properties: content, fontSize, color, fontFamily, justification, fontWeight, fontStyle, leading, lineHeight, letterSpacing, wordSpacing, textTransform, textDecoration)\n fontWeight: 400 / 700 / 'bold'. A weight the font has no face for draws as the nearest face (900 as 700 on most system fonts); where the studio measures it, governor.engineWarnings carries font_weight_no_face { weig" +
|
|
429
|
+
"ht, drawsAs }. fontStyle: 'italic'. leading: line spacing in px; lineHeight: a multiple of fontSize (<= 4) or px. Multi-line content splits on \n.\n Current studio: wordSpacing (px, or a CSS length in px / em; it animates), textTransform ('uppercase' / 'lowercase' / 'capitalize' / 'none'), textDecoration ('underline' / 'line-through' / 'overline', combinable as 'underline overline'; 'none' clears) and textAlign: 'justify' (every line but the last stretched at its spaces) are drawn, on create, modify and keyframes; the result's typography reads back what the item holds. An older studio does not draw them, and its result says so (typography.applied: false; keyframes name them in ignoredProp" +
|
|
430
|
+
"erties).\n fit: {maxWidth, maxHeight?, minFontSize? (8), maxFontSize? (current), wrap? (true), hold? ('top' | 'center' | …)} — auto-size the text into a box, keeping its anchored edge. The box is remembered: changing content later refits (shrinks, or grows back to maxFontSize); modify_item {fit: {...}} sets a new box; {fit: null} stops fitting. The result's textFit says {fontSize, lines, fits}; fits:false means it overflows even at minFontSize. Studios without auto-fit say so in textFit. Every export's fidelity then lists text_overflow for any text past its box or off the frame — the check to run before a batch of variants.\n direction: 'auto' (default: the first strong letter decides, s" +
|
|
431
|
+
"o Arabic and Hebrew need nothing) | 'ltr' | 'rtl' — the paragraph's base direction, which places neutrals like the final period (aliases textDirection, dir). The result's direction says whether the studio applied it; a studio without RTL support lays text out left-to-right and says so. justification: 'right' is alignment, not direction.\n tabularFigures: true (or fontVariantNumeric: 'tabular-nums', or fontFeatures containing 'tnum') — fixed-width digits, so a counter, price or timer does not jitter as it changes. The result's tabularFigures.applied is measured on the current studio: true only when the layout moves a digit's advance; false with a reason (font-digits-already-equal: the font" +
|
|
432
|
+
"'s digits are one width already, so nothing changes and steadyWidth is true; rtl; not-requested); 'unverified' on an older studio that records the request but cannot measure it.\n strokePosition: 'outside' — the stroke sits entirely outside the letters (drawn under the fill at twice strokeWidth), so a thick caption / meme outline keeps the letterform; 'center' (default) is the ordinary stroke. The result's strokePosition says whether the studio applied it.\n- circle: Circular shape (properties: radius, color, strokeColor, strokeWidth)\n- star: Star shape (properties: radius1, radius2, points, color)\n radius1 is the OUTER radius and is what sizes the star; radius or\n width/height size " +
|
|
433
|
+
"it equally well. radius2 is read ONLY as a ratio to\n radius1 (inner/outer, default 0.4), so radius2 passed WITHOUT radius1 is\n ignored completely and you get the default waist. innerRadiusRatio sets that\n same waist directly and needs no radii at all. points defaults to 5.\n A star is always radially symmetric — given width and height it uses the\n smaller — and the size you give is the radius BOX, the circle the tips\n touch. The visible bounds are never larger than that box and usually\n smaller — they fill it only when points is a multiple of 4.\n- rectangle: Rectangle (properties: width, height, color, cornerRadius)\n- triangle: Triangular shape (properties: width, height, co" +
|
|
434
|
+
"lor, angles, kind)\n Defaults to a 100x86 isosceles. For any OTHER triangle, do not hand-build it\n from a path — name it: kind: 'right' | 'equilateral' | 'isosceles' |\n 'obtuse' | 'acute' | 'scalene', or give exact interior angles in degrees,\n angles: [30, 60] (the third is inferred) or [30, 60, 90]. Angles that nearly\n sum to 180 are normalized, so rounding is safe. With angles or kind the\n triangle scales UNIFORMLY into the width x height box — the angles are fixed\n and win, so one axis comes out smaller than requested. That is the only way\n to keep a 'right' triangle actually right.\n An angle set the engine cannot use — fewer than two entries, or any angle\n outside 0." +
|
|
435
|
+
"5-179.5 degrees — falls back to the default isosceles SILENTLY,\n with no error. If you asked for a right triangle and got an isosceles one,\n the angles were rejected.\n- polygon: Regular polygon with N sides (properties: sides, radius, color)\n points additionally accepts an explicit VERTEX LIST — [[x, y], ...] or\n [{x, y}, ...], three or more, in canvas coordinates — which builds that\n exact, possibly IRREGULAR polygon and ignores sides. Reach for this before\n path when the outline is straight-edged. A plain number in points is not a\n vertex list and falls through to sides (default 6).\n In the vertex-list form the coordinates are ABSOLUTE canvas coordinates and\n position " +
|
|
436
|
+
"is ignored — the polygon lands where its points say, not where you\n positioned it. Offset the points yourself, or use sides + radius + position.\n- ellipse: Oval shape (properties: width, height, color) — defaults to 100x60\n- path: Custom path with segments or SVG data (properties: segments, pathData, strokeColor, fillColor, closed, smooth)\n color on a path: a CLOSED path (closed:true, or pathData ending in Z) is FILLED with it; an OPEN path is STROKED with it. Pass fillColor / strokeColor to say exactly which.\n- line: Line between two points (properties: from, to, strokeColor, strokeWidth)\n- arc: Curved arc through three points (properties: from, through, to, strokeColor)\n- pentag" +
|
|
437
|
+
"on: Regular 5-sided polygon (properties: radius, color)\n- hexagon: Regular 6-sided polygon (properties: radius, color)\n- diamond: 4-sided diamond/rhombus (properties: radius, color)\n- arrow: Right-pointing block arrow (properties: width, height, color)\n- heart: Heart shape (properties: width, height, color) — defaults to 80x80\n- disk: Filled disk — the region inside a circle (properties: radius, color)\n- circle-outline: The circle CURVE — stroked, no interior (properties: radius,\n strokeColor, strokeWidth)\n- arrow-right: Alias for arrow — the same definition under the name the shape\n grids use. Identical output and the same width, height and color; both are\n listed because bot" +
|
|
438
|
+
"h are accepted.\n\nSPEECH AND ANNOTATION SHAPES — the engine draws these; use one instead of\nhand-building an outline from a path. All take width, height and color, and the\ndescriptions below are 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-" +
|
|
439
|
+
"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-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 b" +
|
|
440
|
+
"ubbles 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 [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" +
|
|
441
|
+
" 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: color 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: b" +
|
|
442
|
+
"lendMode: \"multiply\"|\"screen\"|\"overlay\"|\"darken\"|\"lighten\"|etc.\n- Opacity: opacity (0-1)\n\nEXAMPLES:\n- \"Create red text saying HELLO\" → itemType: \"text\", properties: {content: \"HELLO\", 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, 3" +
|
|
443
|
+
"00 350 Z\", fillColor: \"#ef4444\"}\n- \"Circle with radial gradient\" → itemType: \"circle\", properties: {radius: 50, color: {type: \"radial\", stops: [{color: \"#ff6b6b\", offset: 0}, {color: \"#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" +
|
|
444
|
+
" → 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.\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 wan" +
|
|
445
|
+
"ts, 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 alpha 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, attach" +
|
|
446
|
+
"ed_to).\nFor glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal stripes, use pinepaper_create_diagonal_stripes."
|
|
446
447
|
|
|
447
448
|
|
|
448
449
|
|
|
@@ -1163,8 +1164,8 @@ Use pinepaper_clear_canvas instead of deleting items one by one.`,
|
|
|
1163
1164
|
openWorldHint: true,
|
|
1164
1165
|
},
|
|
1165
1166
|
description: ("Import a raster image onto the canvas from an http(s) URL, a data: URL, or a LOCAL FILE PATH (the server reads the file). Returns an itemId for the imported image.\n\nUSE WHEN:\n- User wants to add a photo, PNG, JPG, or other raster image\n- Importing images from URLs\n- Adding backgrounds or reference images\n- User provides an image URL\n\nIMPORTANT - IMPORTED IMAGES ARE FULL ITEMS:\nThe imported image becomes a first-class item in PinePaper's registry. It gets an itemId and\ncan participate in the full relation and animation system — you can orbit it, attach things to it,\napply masks, animate it with keyframes, etc., just like any natively created shape.\n\nOPTIONAL MASK:\nApply a mask" +
|
|
1166
|
-
" shape during import to clip the image:\n- circle: Circular crop\n- rounded: Rounded rectangle crop\n- hexagon: Hexagonal crop\n- star: Star-shaped crop\n\nSIZE CONSTRAINTS:\nUse maxWidth/maxHeight to constrain the image dimensions while preserving aspect ratio.\n- maxEdge: the longest edge the studio STORES the image at (px), or 'native' to keep every pixel. The result says when it was stored smaller.\n- nativeSize: true places the image at its source footprint even if it was stored smaller.\n- smoothing: 'off' (nearest-neighbour, for pixel art) | 'low' (default) | 'medium' | 'high'. Chromium draws 'low', 'medium' and 'high' the same; the visible choice is 'off' or not
|
|
1167
|
-
"
|
|
1167
|
+
" shape during import to clip the image:\n- circle: Circular crop\n- rounded: Rounded rectangle crop\n- hexagon: Hexagonal crop\n- star: Star-shaped crop\n\nSIZE CONSTRAINTS:\nUse maxWidth/maxHeight to constrain the image dimensions while preserving aspect ratio.\n- maxEdge: the longest edge the studio STORES the image at (px), or 'native' to keep every pixel. The result says when it was stored smaller.\n- nativeSize: true places the image at its source footprint even if it was stored smaller.\n- smoothing: 'off' (nearest-neighbour, for pixel art) | 'low' (default) | 'medium' | 'high'. Chromium draws 'low', 'medium' and 'high' the same; the visible choice is 'off' or not. Asking for 'medium" +
|
|
1168
|
+
"' or 'high' is applied, and the reply says so in smoothingNote.\n- shadowColor / shadowBlur / shadowOffset: a drop shadow on the placed image, the same keys pinepaper_modify_item takes.\n- name: a name for the image, so a batch patch or any tool that takes an item can address it by name instead of its id.\n\nEXAMPLES:\n- Import photo: url: \"https://example.com/photo.jpg\"\n- Import with mask: url: \"https://example.com/avatar.png\", mask: \"circle\"\n- Import at position: url: \"https://example.com/bg.jpg\", position: {x: 400, y: 300}, maxWidth: 800"
|
|
1168
1169
|
|
|
1169
1170
|
|
|
1170
1171
|
|
|
@@ -2487,12 +2488,13 @@ EXAMPLE: { itemId: "circle_3", action: "front" }`,
|
|
|
2487
2488
|
"\n- absent — no vertex-level expression; the tool REFUSES with the reason instead of producing flat shapes in its colours (encaustic)\n\nCall 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.\n\nACTIONS:\n- 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: " +
|
|
2488
2489
|
String(Object.keys(DESIGN_MEDIA_APPLY).join(', ')) +
|
|
2489
2490
|
". 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/vide" +
|
|
2490
|
-
"o): 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.\n-
|
|
2491
|
-
"
|
|
2492
|
-
"the fix. A studio without the SDF brush refuses with the reason.\n\nTHE DIRECTION FIELD IS THE WHOLE THING. Stitches that all lie the same way are hatching; stitches that follow the form are needlepainting. Use { kind: 'radial', cx, cy } for anything that radiates (a flower, an eye), { kind: 'spine', spine: [...] } to run them along a midrib or feather shaft, { kind: 'constant', angle } when you actually
|
|
2493
|
-
"
|
|
2494
|
-
"
|
|
2495
|
-
"
|
|
2491
|
+
"o): 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.\n- ink colour: unless options.color or options.keepColor is given, ink marks are drawn in the brush default, a near-black (#16141c), whatever the item's own colour. On a dark background they disappear: pass options.color (a light ink) or options.keepColor: true (the item's own colour).\n- 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).\n- resolve: { medium } → { ok, fide" +
|
|
2492
|
+
"lity, reason, markMaker }. ok:false with a reason is a real answer — pass it on rather than trying anyway.\n- list_stitches: the thread stitches with their params.\n- apply_thread: render an item in thread. Its own silhouette is the region and its own fill is the thread colour, so an existing drawing becomes stitched rather than needing to be redrawn. The source is HIDDEN, not destroyed.\n- apply_hatch: rule an item with hatching. Same deal — silhouette is the region, source is hidden, re-hatching replaces.\n- list_flow_fields: the 7 fields that bend a hatch line.\n- list_hatch_options: the hatch defaults.\n- sdf_stroke: { points, width? (8), softness? (0.2), color?, opacity?, resolution? " +
|
|
2493
|
+
"(2), name? } → itemId — a smooth, anti-aliased brush stroke along points ({x, y, pressure?} or [x, y, pressure?], canvas px; pressure 0..1 thins the line there), made as an image item (registered, one undo step). Runs on the CPU; a stroke too large for its bitmap budget is refused with the fix. A studio without the SDF brush refuses with the reason.\n\nTHE DIRECTION FIELD IS THE WHOLE THING. Stitches that all lie the same way are hatching; stitches that follow the form are needlepainting. Use { kind: 'radial', cx, cy } for anything that radiates (a flower, an eye), { kind: 'spine', spine: [...] } to run them along a midrib or feather shaft, { kind: 'constant', angle } when you actually wan" +
|
|
2494
|
+
"t flat hatch.\n\nSTITCH CHOICE: longAndShort (default) is the shading fill — the length variance IS the blend. satin spans the shape edge to edge in one sheet, which is right for a petal or a letter stroke and wrong for a round shape. seed is texture, not direction. stem is an OUTLINE mark and leaves the interior bare — that is the stitch, not a bug.\n\nAn unknown stitch is refused rather than defaulted, and re-stitching an item REPLACES its previous stitching instead of stacking.\n\nHATCHING STATES VALUE THROUGH LINE DENSITY, not colour. The same shape at distance 6 and at distance 3 reads as light and dark with nothing else changed — that is the whole medium, and it is why distance is th" +
|
|
2495
|
+
"e parameter to reach for rather than a lighter ink. gradient makes the density fall off across the shape, which is a shaded ramp rather than a flat tone.\n\nThe straight ruling is what a printer makes; flowField is what makes it read as DRAWN. hand is the small correlated wander of a hand-drawn line, waves for water/hair/contours, spiral for wood grain around a knot, columns for rake marks. continuous joins the whole set into one serpentine path — a pen that never leaves the paper, and one item instead of many.\n\nHatching needs a CLOSED outline. Text has none: convert it to glyph paths (pinepaper_text_style) and hatch the result. A group is hatched child by child, up to 40 paths; past tha" +
|
|
2496
|
+
"t the result says so rather than quietly stopping.\n\nNote that a thread field and a hatch flowField are different things and are named differently for it: 'field' steers stitches around a form, 'flowField' bends a ruling line the way a hand drifts.\n\nEXAMPLE — a hatched shadow side, drawn rather than printed:\n{ \"action\": \"apply_hatch\", \"itemId\": \"item_3\", \"distance\": 3, \"angle\": 30, \"flowField\": \"hand\", \"rand\": 0.2 }\n\nEXAMPLE — a stitched leaf:\n{ \"action\": \"apply_thread\", \"itemId\": \"item_3\", \"stitch\": \"longAndShort\",\n \"field\": { \"kind\": \"spine\", \"spine\": [{\"x\":400,\"y\":200},{\"x\":400,\"y\":520}] },\n \"stitchLen\": 26, \"variance\": 0.45 }"
|
|
2497
|
+
|
|
2496
2498
|
|
|
2497
2499
|
|
|
2498
2500
|
|
|
@@ -2821,7 +2823,7 @@ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
|
|
|
2821
2823
|
title: { type: 'string', description: 'compose: the headline most styles build around.' },
|
|
2822
2824
|
subtitle: { type: 'string', description: 'compose: the supporting line.' },
|
|
2823
2825
|
body: { type: 'string', description: 'compose: body copy, where the style has room for it.' },
|
|
2824
|
-
width: { type: 'number', description: 'compose: canvas width. compose
|
|
2826
|
+
width: { type: 'number', description: 'compose: canvas width. Without width and height, compose lays out at the CURRENT canvas size, so your canvas is kept. compose sets the canvas to the size it laid out for: most styles honour the size; a few lay out at their own fixed size, and the result warns when the size was not used. canvasResized says when the canvas changed. With draw: false and no size, the scene comes back at the style\'s own default size.' },
|
|
2825
2827
|
height: { type: 'number', description: 'compose: canvas height.' },
|
|
2826
2828
|
variant: { type: 'string', description: "compose: a style-specific variant, e.g. art deco's 'emerald'." },
|
|
2827
2829
|
draw: { type: 'boolean', description: 'compose: draw it (default true). false returns the scene and ops as data and draws nothing.' },
|
|
@@ -2854,7 +2856,7 @@ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
|
|
|
2854
2856
|
action: { type: 'string', enum: ['figure', 'set'], description: "'figure' or 'set'." },
|
|
2855
2857
|
id: { type: 'string', description: 'Prefix for the created items.' },
|
|
2856
2858
|
kind: { type: 'string', description: 'figure: which figure. set: room | tabletop | chair | table | counter | door | shelf | window (an unknown one is refused with the current list).' },
|
|
2857
|
-
at: { type: 'object', description:
|
|
2859
|
+
at: { type: 'object', description: "{ x?, y? } — where it goes. For a figure, y is where its FEET stand, so at.y = a room's groundY stands it on the floor (travel points' y likewise); a figure in pose 'sit' is placed by its seat, so at = a chair's anchor seats it. The reply's anchor says which rule this studio used: 'feet', or 'hip' on an older studio that put the hip at y." },
|
|
2858
2860
|
scale: { type: 'number', description: 'Size multiplier.' },
|
|
2859
2861
|
facing: { type: 'string', enum: ['left', 'right'], description: "set: which way a CHAIR faces. A figure has none and passing it here is refused — a figure faces the way it travels." },
|
|
2860
2862
|
pose: { type: 'string', enum: [...STICK_POSES], description: 'figure: ONE pose, held.' },
|
|
@@ -2978,14 +2980,14 @@ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
|
|
|
2978
2980
|
idempotentHint: false,
|
|
2979
2981
|
openWorldHint: false,
|
|
2980
2982
|
},
|
|
2981
|
-
description: ("Synthesis — and the drawing that is the same object.\n\nThe engine carries a full audio graph, and none of it was reachable. pinepaper_audio_beats ANALYSES audio that already exists; this MAKES audio.\n\n- Catalogues: list_instruments, list_percussion, list_sfx. Call one before naming a drum or effect — the names are the engine's.\n- Play: play_tone ('A4'), play_chord (root + kind: major, minor, maj7, dim…), play_percussion, play_sfx (beep, pop,
|
|
2982
|
-
"
|
|
2983
|
-
"
|
|
2984
|
-
"Rhodes' and the response returns canonicalName 'rhodes' — play THAT.\n\nARGUMENTS BY ACTION. A sound spec is { note? ('A4') | hz? | chord? + root?, instrument? (a catalogue name), waveform? ('sine' | 'square' | 'sawtooth' | 'triangle'), partials? ([{ h, amp }]), envelope? ({ attack, decay, sustain, release }), gain? (0..1), pan? (-1..1), duration? (seconds) }.\n\nCATALOGUES:\n- list_instruments / list_percussion / list_sfx: {} → the engine's names, built-in and defined.\n\nPLAY — each returns { played }. When the studio returns no voice (it may have no audio output) the call fails and says so; placed sounds and render_soundtrack need no audio output:\n- play_tone: { note ('A4
|
|
2985
|
-
"options? } — options is merged into the spec, so { duration, gain, pan, instrument, envelope, waveform } all work, and options.hz overrides the note. A top-level hz is NOT play_tone's: it belongs to define_percussion / define_sfx, and play_tone without note is refused.\n- play_chord: { root ('C4'), chord? ('major' default; 'minor', 'maj7', 'dim' …), options? }\n- play_percussion / play_sfx: { name (from the catalogue), options? }\n- play_spec: { spec, options? } — options is merged into the spec.\n- play_from_text: { text, options? } — options is merged into the resolved spec.\n\nREAD WITHOUT PLAYING:\n- chord_frequencies: { root, chord?, options? } → { frequencies } (Hz)\n-
|
|
2986
|
-
"
|
|
2987
|
-
"}.\n- set_placement: { itemId, placement: { startTime?, duration? } } (seconds; no other keys are read) → { placed }\n- remove: { itemId } → { removed } · stop_all: {} → { stopped } — stops everything sounding now; placed sounds stay placed.\n\nDEFINE (name is normalised to [a-z][a-z0-9_-]; play the returned canonicalName):\n- define_instrument: { name, partials (required, ≥1), envelope?, gain? (0.8), aliases? } → { canonicalName, requestedName, normalised, listed }\n- define_percussion / define_sfx: { name, partials? or noise? (0..1; one of the two is required), noiseFreq? (Hz), hz? (base Hz), pitch? ({ from (Hz), tau (s, ≤5) }), envelope?, gain?, aliases? }\n\nRENDER:\n-
|
|
2988
|
-
"
|
|
2983
|
+
description: ("Synthesis — and the drawing that is the same object.\n\nThe engine carries a full audio graph, and none of it was reachable. pinepaper_audio_beats ANALYSES audio that already exists; this MAKES audio.\n\n- Catalogues: list_instruments, list_percussion, list_sfx. Call one before naming a drum or effect — the names are the engine's.\n- Play: play_tone ('A4'), play_chord (root + kind: major, minor, maj7, dim…), play_percussion, play_sfx (beep, pop, whoosh, zap, and more: list_sfx), play_spec, play_from_text.\n- Read without playing: chord_frequencies (the Hz a chord resolves to), from_text (a plain-language description — \"a soft warm bell on A4\" — resolved to a spec you can inspect or edit " +
|
|
2984
|
+
"before committing), timbre_from_path.\n\nTHE PART WORTH KNOWING: sound and drawing are the same object here, both ways.\n- create draws a sound AS a waveform path on the canvas. Edit the path and the timbre changes with it — it is not a picture of the sound, it is the sound.\n- timbre_from_path reads ANY drawn path back as harmonic content. A curve someone drew by hand becomes the partials of a note.\n\nA path with no length cannot be a timbre, and the engine answers a flat single-partial timbre for anything that is not one — a real answer and a refusal look identical — so the item is checked before the call and a non-path is refused by name.\n\nTHE CATALOGUE IS A STARTING SET, NOT THE LIM" +
|
|
2985
|
+
"IT. Six instruments, five drums and six effects are what the engine ships with; that is not a claim about how many instruments exist. define_instrument, define_percussion and define_sfx register a new one at runtime — a rhodes, a koto, a taiko, a specific synth patch — and it is first-class from then on: it appears in the list_* catalogues and plays by name like any built-in.\n- A melodic instrument needs a partial table, because one with no partials renders silence. Percussion and SFX may be pure noise instead, because a hat and a whoosh are.\n- For a drum, pass pitch: { from, tau }. The glide is what makes a kick a kick; without it you get a beep.\n- Names are normalised to [a-z][a-z0-9_" +
|
|
2986
|
+
"-]. Register 'Rhodes' and the response returns canonicalName 'rhodes' — play THAT.\n\nARGUMENTS BY ACTION. A sound spec is { note? ('A4') | hz? | chord? + root?, instrument? (a catalogue name), waveform? ('sine' | 'square' | 'sawtooth' | 'triangle'), partials? ([{ h, amp }]), envelope? ({ attack, decay, sustain, release }), gain? (0..1), pan? (-1..1), duration? (seconds) }.\n\nCATALOGUES:\n- list_instruments / list_percussion / list_sfx: {} → the engine's names, built-in and defined.\n\nPLAY — each returns { played }. When the studio returns no voice (it may have no audio output) the call fails and says so; placed sounds and render_soundtrack need no audio output:\n- play_tone: { note ('A4" +
|
|
2987
|
+
"', required), options? } — options is merged into the spec, so { duration, gain, pan, instrument, envelope, waveform } all work, and options.hz overrides the note. A top-level hz is NOT play_tone's: it belongs to define_percussion / define_sfx, and play_tone without note is refused.\n- play_chord: { root ('C4'), chord? ('major' default; 'minor', 'maj7', 'dim' …), options? }\n- play_percussion / play_sfx: { name (from the catalogue), options? }\n- play_spec: { spec, options? } — options is merged into the spec.\n- play_from_text: { text, options? } — options is merged into the resolved spec.\n\nREAD WITHOUT PLAYING:\n- chord_frequencies: { root, chord?, options? } → { frequencies } (Hz)\n- " +
|
|
2988
|
+
"from_text: { text ('a soft warm bell on A4') } → { spec } to inspect or edit, then play_spec or create\n- timbre_from_path: { itemId (a path with length), samples? (256) } → { timbre: { partials } }\n\nON THE CANVAS — a placed sound is a timeline item:\n- create: { spec?, visual?: { width? (220), height? (64), position? ({ x, y }), color?, startTime? (s), duration? (s) } } → { itemId, placement? } — startTime / duration place it in the same call.\n- sequence: { cues: [{ t (s, required), spec? | preset? (an instrument name) + note?, duration?, visual? }] } → { placed, itemIds, cues, failed? } — a whole bed in ONE call; failures come back by index. preset is shorthand for spec: { instrument:" +
|
|
2989
|
+
" preset, note }.\n- set_placement: { itemId, placement: { startTime?, duration? } } (seconds; no other keys are read) → { placed }\n- remove: { itemId } → { removed } · stop_all: {} → { stopped } — stops everything sounding now; placed sounds stay placed.\n\nDEFINE (name is normalised to [a-z][a-z0-9_-]; play the returned canonicalName):\n- define_instrument: { name, partials (required, ≥1), envelope?, gain? (0.8), aliases? } → { canonicalName, requestedName, normalised, listed }\n- define_percussion / define_sfx: { name, partials? or noise? (0..1; one of the two is required), noiseFreq? (Hz), hz? (base Hz), pitch? ({ from (Hz), tau (s, ≤5) }), envelope?, gain?, aliases? }\n\nRENDER:\n- re" +
|
|
2990
|
+
"nder_soundtrack: { duration? (s, ≤600), sampleRate? (48000), bitDepth? (16 | 32) } → a WAV FILE; the result names filePath, fileSize, placed and any dropped. Refused by name when no sound is placed.\n\nrender_soundtrack mixes every placed sound to a WAV and writes it to a file. It runs offline, with no Web Audio and no playback, so it works in a headless studio. An empty scene is refused by name rather than producing a silent file, and any sound that could not be mixed is reported as dropped — a soundtrack missing a track otherwise looks exactly like a complete one."
|
|
2989
2991
|
|
|
2990
2992
|
|
|
2991
2993
|
|
|
@@ -3054,7 +3056,7 @@ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
|
|
|
3054
3056
|
itemId: { type: 'string', description: 'timbre_from_path: the path whose shape becomes the timbre. set_placement / remove: the sound item.' },
|
|
3055
3057
|
samples: { type: 'number', description: 'timbre_from_path: points sampled along the path (default 256).' },
|
|
3056
3058
|
placement: { type: 'object', description: 'set_placement: where and when the sound sits on the timeline.' },
|
|
3057
|
-
visual: { type: 'object', description: 'create: { width?, height?, position?, color? } for the waveform path.' },
|
|
3059
|
+
visual: { anyOf: [{ type: 'object' }, { type: 'boolean', const: false }], description: 'create: { width?, height?, position?, color?, startTime?, duration? } for the waveform path. false = no drawing options: the waveform chip is an editor affordance that no export draws, so there is nothing to turn off.' },
|
|
3058
3060
|
},
|
|
3059
3061
|
required: ['action'],
|
|
3060
3062
|
},
|
|
@@ -3263,7 +3265,9 @@ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
|
|
|
3263
3265
|
},
|
|
3264
3266
|
description: ("Place a CHARACTER from the design graph and DIRECT it — no geometry, no bones, no poses.\n\nThe knowledge of how a figure is built and how it moves lives in the graph; this asks for it by name and states WHAT happens. The alternative is thirty-odd exactly-right calls — create the skeleton, add each bone with the right parent and angle, create each shape, attach each to the right bone, then author poses — where one mistake anywhere leaves a broken figure.\n\n { concept: \"pp:Character\", at: { x: 470, y: 250 }, height: 300,\n beats: [ { at: 1.0, channel: \"blink\" },\n { at: 1.8, channel: \"say\", until: 4.2 },\n { at: 5.0, channel: \"headTurn\", value: -0.8 } ] " +
|
|
3265
3267
|
"}\n\nCHANNELS ARE DECLARED BY THE CONCEPT, not by you. Ask pinepaper_query_ontology for a concept to see the ones it has. Four kinds exist:\n- transform: one part deformed with an attack, a hold and a release — a blink is the eyes flattening in scaleY and easing back. This is what gives a gesture a MIDDLE; a swap between two drawings has only two ends and reads as flashing.\n- cycle: a repeating throw that runs for as long as you ask — a pigeon's head bob, a tail flick. Amplitude is a fraction of the part, so it means the same at any size.\n- shift: every part moved together, for a turn a flat drawing cannot rotate into.\n- alternate / speak: swaps between drawn states, for shapes that gen" +
|
|
3266
|
-
"uinely differ rather than deform.\n\nBEATS ARE INSTANTS OR SPANS. `at` is when it happens; `until` is how long for a channel that occupies time. `value` is how far, for channels that take a magnitude.\n\nWHAT YOU GET BACK: the items and keyframe tracks the beats compile to. They are ordinary scene items afterwards — modify, animate and relate them like anything else.\n\
|
|
3268
|
+
"uinely differ rather than deform.\n\nBEATS ARE INSTANTS OR SPANS. `at` is when it happens; `until` is how long for a channel that occupies time. `value` is how far, for channels that take a magnitude.\n\nWHAT YOU GET BACK: the items and keyframe tracks the beats compile to. They are ordinary scene items afterwards — modify, animate and relate them like anything else.\n\nWALKING, POSES, ACTIONS: a concept character moves through its channels and has no skeleton, so a walk, a pose or a rigging call acts on nothing (and the reply says so). For a figure that walks, poses and acts, make it with pinepaper_original_character, which is rigged.\n\nNOT LIP SYNC. `say` opens and closes a mouth for th" +
|
|
3269
|
+
"e length of a line. Matching phonemes needs the word timings a voice track carries; pass `timings` if you have them and each entry drives one mouth opening."
|
|
3270
|
+
|
|
3267
3271
|
|
|
3268
3272
|
|
|
3269
3273
|
|
|
@@ -3545,24 +3549,25 @@ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
|
|
|
3545
3549
|
"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
3550
|
"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
3551
|
"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).
|
|
3549
|
-
"
|
|
3550
|
-
"
|
|
3551
|
-
"
|
|
3552
|
-
"
|
|
3553
|
-
"
|
|
3554
|
-
"
|
|
3555
|
-
"
|
|
3556
|
-
"
|
|
3557
|
-
"
|
|
3558
|
-
"
|
|
3559
|
-
"
|
|
3560
|
-
"
|
|
3561
|
-
"
|
|
3562
|
-
"
|
|
3563
|
-
"
|
|
3564
|
-
"
|
|
3565
|
-
"
|
|
3552
|
+
"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). The reveal is only for a target that STARTS hidden: one that starts shown is never theirs to show, so a later hide stands, including " +
|
|
3553
|
+
"one from a reaction to their own pulse. 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 else.\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 p" +
|
|
3554
|
+
"arams 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, gain). WHEN IT FIRES: with no anchor it is on beat 1 at 120 bpm, 0.5 s after the source starts, and offset is ADDED to that (offset 0.5 fires at 1.0 s). For a set time use anchor: 'at' with at: seconds on the source's track clock (plus offset); anchor: 'start' or 'complete' fire at the source's start or end (plus offset). 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 track" +
|
|
3555
|
+
"ed 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 \"end\" 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 i" +
|
|
3556
|
+
"ndex 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_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 " +
|
|
3557
|
+
"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 } — 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 (\"S" +
|
|
3558
|
+
"core: {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 target 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, " +
|
|
3559
|
+
"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 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 → nul" +
|
|
3560
|
+
"l). 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 two 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_pr" +
|
|
3561
|
+
"operty, 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 / on_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 (rela" +
|
|
3562
|
+
"tionOptions.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 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 a" +
|
|
3563
|
+
"nd 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 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), 'g" +
|
|
3564
|
+
"lyphs' (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. Opacity keyed on the source is carried through the settle like position, and is written every frame while the relation exists, so a manual opacity change on the target is overridden until the relation is removed.\n- group_morphs_to: pair-by-in" +
|
|
3565
|
+
"dex morph 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 } (sam" +
|
|
3566
|
+
"e equation 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:" +
|
|
3567
|
+
"\n- is_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" +
|
|
3568
|
+
" (timeline 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" +
|
|
3569
|
+
"|scale. 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 a" +
|
|
3570
|
+
"s a pure f(t) via the Expression IR). The formula grammar: " +
|
|
3566
3571
|
String(expressionGrammarText()) +
|
|
3567
3572
|
". 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" +
|
|
3568
3573
|
"_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" +
|
|
@@ -3656,7 +3661,6 @@ String(expressionGrammarText()) +
|
|
|
3656
3661
|
|
|
3657
3662
|
|
|
3658
3663
|
|
|
3659
|
-
|
|
3660
3664
|
|
|
3661
3665
|
|
|
3662
3666
|
),
|
|
@@ -3986,9 +3990,9 @@ USE WHEN:
|
|
|
3986
3990
|
"O behavioral record — no graph edge, nothing SMIL or widget export can lift, nothing the ontology can read. Scene building fits the governor's 4s synchronous deadline easily (it is the injected loop guards, not the 10s async-tail budget, that bites a build loop).\n\n- TEXT THAT CHANGES OVER TIME -> 'content' IS keyframeable, and it is DISCRETE: it holds the current word until the next keyframe and does not tween. One thing worth knowing before you build a cascade out of it. A cascade of N words is N keyframes you have to write out, which is the reason to prefer the call below rather than any limitation. For the common case prefer pinepaper_execute_custom_code with app.textSequence(itemOrId" +
|
|
3987
3991
|
", words, { interval }): ONE item cycling its words in one call, against one item per word with opacity tracks faking the cut. It returns { ok: false, reason } on an id it cannot resolve.\n\nKeyframes are for motion that is AUTHORED, not computed. A track of hundreds of entries produced from a formula is the signal that one of the three above is the right route.\n\nANIMATABLE PROPERTIES:\n- position: [x, y] array (a { x, y } object is also accepted and normalized)\n- x, y: Individual coordinates\n- scale: Uniform scale\n- scaleX, scaleY: Axis scaling\n- rotation: Degrees. Keys interpolate as plain numbers, with no short way round: 350 to 10 sweeps back through 180. Key 350 to 370 to cross z" +
|
|
3988
3992
|
"ero.\n- opacity: 0-1\n- fillColor: Color string\n- strokeColor: Color string\n- strokeWidth, width, height, radius, shadowBlur, shadowColor: Numbers / colours\n- fontSize: Number\n- skewX, skewY (current studio): degrees, clamped to ±89; they combine with rotation and scale. An older studio stores them without drawing and the result names them in ignoredProperties; there, a shear is a transform matrix track.\n- TEXT (current studio): fontStyle ('normal' / 'italic' / 'oblique', discrete — switches at the next key), textAlign ('left' / 'center' / 'right', discrete; the anchor stays put), lineHeight (px, or a multiple of fontSize up to 5), letterSpacing (px, or a CSS length such as '0.1em'), " +
|
|
3989
|
-
"wordSpacing (px or a CSS length), textTransform and textDecoration (discrete). An older studio stores these keys without drawing them; the result names any it did not animate (ignoredProperties). fontWeight keys STEP: a font with fixed weights switches from one weight to the next once, partway through the move, and does not ramp. Key fontSize or scale for a smooth change
|
|
3990
|
-
"
|
|
3991
|
-
" they mark either way.\n" +
|
|
3993
|
+
"wordSpacing (px or a CSS length), textTransform and textDecoration (discrete). An older studio stores these keys without drawing them; the result names any it did not animate (ignoredProperties). fontWeight keys STEP: a font with fixed weights switches from one weight to the next once, partway through the move, and does not ramp (a variable font ramps). Key fontSize or scale for a smooth change. Where the studio measures it, the reply's governor.engineWarnings says so: font_weight_snap { from, to, at, atTime } (at: the weight where the face changes; null when both ends draw one face).\n- STROKE DRAW-ON (paths): trimStart, trimEnd (0-1 of the path's length), trimOffset (shifts the visible w" +
|
|
3994
|
+
"indow along it), dashArray. A draw-on is trimEnd 0 -> 1.\nThe authoritative list for a given item is pinepaper_get_animatable_properties — ask it rather than guessing.\n\nEASING — BY DEFAULT IT SHAPES THE SEGMENT ARRIVING AT THE KEYFRAME IT IS ON. It belongs to the key, not to one property: 'step' on a key holds every property in that key, so keep a stepped property in its own call from one that moves.\nThe move from keyframe A to keyframe B is eased by B's easing, not A's. So by default the FIRST keyframe's easing has no effect (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, or pass easingAt: 'departure'" +
|
|
3995
|
+
" for the other reading: each key's easing shapes the move LEAVING it, so the first key's easing eases the first move (current studio; an older one reads arrival and the result says so). 'hold' / 'step' stay on the key they mark either way.\n" +
|
|
3992
3996
|
String(EASING_LINES) +
|
|
3993
3997
|
"\n- 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.\nCOUNTERS: 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.\n\nHOLD 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 " +
|
|
3994
3998
|
"the cut. For a whole layer 'on twos', set stepTiming on the item (create_item / modify_item). A DISCRETE change (a bar turning red at 2 s, a label switching colour) needs this too: without hold / step every key ramps from the one before it, so the colour fades across the whole gap instead of switching. This applies to batch keyframe ops the same way.\n\nEvery 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.\n\nCLIP WINDOW (optional — Premiere/Canva-style clip behavior):\n- timeOffset: Shift the whole clip in canv" +
|
|
@@ -5719,7 +5723,7 @@ this tool, so a control cannot exist that an agent cannot set.`,
|
|
|
5719
5723
|
description: `Undo/redo and history state inspection.
|
|
5720
5724
|
|
|
5721
5725
|
ACTIONS:
|
|
5722
|
-
- undo: Undo last canvas action
|
|
5726
|
+
- undo: Undo last canvas action. Creates and animations made in quick succession can share one step, so one undo may take back more than one item or track; check pinepaper_get_items after an undo, or delete_item / remove the single thing instead. On a very large scene the studio may stop taking undo steps (and stop saving the session) rather than run out of memory; the reply's receipts say so (history_snapshot_skipped, session_not_saved), and undo then cannot go back past that point.
|
|
5723
5727
|
- redo: Redo last undone action
|
|
5724
5728
|
- get_state: Get history stack state (canUndo, canRedo, count)`,
|
|
5725
5729
|
inputSchema: {
|
|
@@ -7608,15 +7612,16 @@ EXAMPLES:
|
|
|
7608
7612
|
"iberately, because the enum is what makes the built-ins discoverable and typo-proof. The two routes above are how you run your own.\n\nGenerator output is only PARTLY registered — drawSunsetScene registers 6 of its 27 objects, drawPattern none of 15 — so pinepaper_get_items and ontology queries see a partial view or nothing. The engine records the invocation instead: re-invoke with new params to \"edit\".\n\nWHEN YOU WRITE THE GENERATOR, THAT IS YOUR CHOICE TO MAKE, and it is the important one. app.registerItem(item, type, properties) returns an id and makes the item selectable by the USER and addressable by you — colour changes, modify_item, keyframes, relations, ontology. Unregistered ou" +
|
|
7609
7613
|
"tput is pixels: identical on screen and in the export, and untouchable afterwards without redrawing. Register the handful anyone might want to edit; leave the dense decorative bulk out, which is the memory reason the built-ins do the same.\n\nUSE WHEN:\n- \"add a sunburst background\", \"bokeh effect\", \"gradient mesh\"\n- \"create wave pattern\", \"wind field\", \"fluid flow\"\n- \"grid background\", \"geometric abstract\", \"noise texture\"\n- Creating dynamic procedural backgrounds\n\nGENERATORS (highlights; all " +
|
|
7610
7614
|
String(GeneratorNameSchema.options.length) +
|
|
7611
|
-
" are in the generatorName enum):\n
|
|
7612
|
-
"
|
|
7613
|
-
"
|
|
7614
|
-
"
|
|
7615
|
-
"
|
|
7616
|
-
"|
|
|
7617
|
-
"
|
|
7618
|
-
"
|
|
7619
|
-
"
|
|
7615
|
+
" are in the generatorName enum):\n- FOREGROUND (product and story content, drawn as editable items rather than a backdrop): drawChatThread, drawPhoneFrame, drawBrowserMockup, drawTerminalTyping, drawMetricCard, drawFunnel, drawRoadmapTimeline, drawComparisonSplit, drawBeatCaptions. The output is editable by default: a foreground generator (the studio's ui, infographics and captions categories) runs with params.interactive: true unless you pass false, so each text and card is an item, and pinepaper_group ungroup on the composition (pinepaper_get_items lists it, with the generator name as its type) releases it into separate items. The reply says interactive: true when the default was applied" +
|
|
7616
|
+
". params.interactive: false gives one flat drawing.\n\nClassic:\n- drawSunburst: Radial rays from center (rayCount, colors, bgColor, opacity, rayGap, gradientRays)\n- drawSunsetScene: Animated sunset with clouds (sunColor, skyColors, cloudCount, skyOpacity, starCount, starColor, reflectionEnabled, reflectionOpacity)\n- drawGrid: Lines/dots/squares (gridType, spacing, lineColor, opacity, gap, randomRotation, colorMode: checkerboard|gradient|random|rows|columns, strokeColor, strokeWidth)\n- drawStackedCircles: Overlapping circles (count, colors, distribution: random|poisson|golden, opacityMin, opacityMax, blendMode, strokeWidth, strokeColor, sizeGradient, animationType: pulse|float|none)\n- " +
|
|
7617
|
+
"drawCircuit: Tech circuit board (lineColor, nodeColor, density, traceOpacity, nodeOpacity, diagonalPaths, chipDensity: auto|none|low|medium|high)\n- drawWaves: Layered wave pattern (waveCount, colors, amplitude, frequency, opacity, fill, fillOpacity, amplitudeVariation, blendMode)\n- drawPattern: Geometric shapes in orbit (patternType, size, color, opacity, blendMode, layers: 1-5, layerScaleDecay)\n\nNew — Effects:\n- drawBokeh: Soft-focus circles (count, colors, minRadius, maxRadius, shadowBlur, distribution: random|poisson, driftAnimation)\n\nNew — Organic:\n- drawGradientMesh: Radial gradient blobs with screen blending (colors, blobCount, blendMode, drift)\n- drawFluidFlow: Sinusoidal s" +
|
|
7618
|
+
"tream paths with depth (streamCount, colors, depthLayers, speed)\n- drawOrganicFlow: Aurora/silk layers with fill-to-bottom (layerCount, colors, blendMode, fillToBottom)\n\nNew — Geometric:\n- drawGeometricAbstract: Mixed translucent shapes (colors, shapeCount, blendMode, rotation)\n\nNew — Particles:\n- drawWindField: Directional wind particles with noise turbulence (particleCount, colors, windDirection (degrees), windSpeed, turbulence, trailLength, trailWidth, opacity, bgColor; direction / speed are accepted for windDirection / windSpeed)\n\nNew — Textures:\n- drawNoiseTexture: Perlin/grain/stipple noise (noiseType: perlin|grain|stipple, colors, scale, density, animated)\n\nNew — Tech:\n" +
|
|
7619
|
+
"- drawGlobeWireframe: Vercel-style wireframe globe (bgColor, wireColor, glowColor, highlightColor, meridianCount, latitudeCount, globeRadius, nodeCount, nodeSize, animationEnabled, animationSpeed, strokeWidth, gridOpacity)\n\nPineMath — Math/Science:\n- drawFunctionPlot: Plot y=f(x) (expression, xMin, xMax, yMin, yMax, color, lineWidth, showAxes, showGrid, steps, animate: none|scroll-right|scroll-left|oscillate|bounce|wipe|custom)\n- drawParametricCurve: Plot (x(t),y(t)) Lissajous/spirals/roses (xExpression, yExpression, tMin, tMax, color, steps, scale, animate: none|linear|slow|fast|wave|bounce, trailFade)\n- drawSimulation: Dynamic systems with ODE solvers (preset: pendulum|springMass|lo" +
|
|
7620
|
+
"renz|doublePendulum|vanderpol, solver: euler|rk4|rk45, timeScale, showTrail, trailLength, trailColor, objectColor)\n- drawSpectrumAnalyzer: FFT signal visualization (waveform: sine|square|sawtooth|triangle|noise|composite, frequency, amplitude, sampleRate, window: none|hann|hamming|blackman, waveColor, spectrumColor)\n- draw3DSurface: 3D surfaces from equations (preset: torus|sphere|wave|klein|mobius|custom, xExpr, yExpr, zExpr, uSteps, vSteps, scale, rotateSpeed)\n\nS6 Procedural (seeded, OKLCH palettes, curated presets, optional Motion). All take: palette (ocean|neon|ember|slate|dusk|pastel|mint|lagoon…), colorMode (gradient|random|mono), bgColor, seed (same seed → identical output), ani" +
|
|
7621
|
+
"mation (none|driftX|driftY|rotate|pulse), animationSpeed:\n- drawTruchet: Truchet tiles — interlocking arcs/diagonals (tileSize, lineWidth, variant: arcs|diagonals, lineColor). Presets: Ocean Maze, Neon Circuit, Ember Weave, Slate Diagonals.\n- drawHalftone: halftone dot field (toneMode: radial|noise|linear, dotSpacing, maxDotRadius, contrast, dotColor, opacity). Presets: Sunburst Dots, Noise Field, Pop Ramp, Soft Mono.\n- drawRibbons: flowing Bézier ribbons (ribbonCount, ribbonWidth, amplitude, waviness, opacity). Presets: Deep Current, Aurora, Silk, Mint Flow.\n\nGPU / GLSL math-art (transpiled to a fragment shader, 60fps; all take renderScale 0.25–1, transparentBg, speed):\n- drawFormul" +
|
|
7622
|
+
"aArt: type a math expression in x,y,t → scalar mapped through a 3-stop palette (formula, domain, color1/color2/color3). Try sin(x*5+t)*cos(y*3).\n- drawParametricCollection: N circles or line segments indexed by k, expressions in k,N,t — Yeganeh-style (mode: circles|lines, count, xExpression/yExpression/radiusExpression or x1..y2Expression, scale, palette, bgColor).\n- drawShaderArt: a GLSL fragment shader. PASSING fragmentSource ALONE DOES NOTHING — preset defaults to 'wavefield' and wins, so your shader is ignored and a preset renders instead, reporting success. Pass preset: 'custom' to use fragmentSource verbatim. The shader is GLSL ES 3.00: inputs 'in vec2 v_uv', output 'out vec4 outCo" +
|
|
7623
|
+
"lor' — NOT gl_FragColor, and there is no u_uv. Uniforms: u_time, u_resolution (vec2), u_color1/2/3 (vec3). Other presets: wavefield, gradient_glow, noise_clouds, voronoi, simplex_noise, perlin, pulsing_border. renderScale 0.25-1 (default 0.5) trades resolution for speed.\n- drawYeganehMountains: GLSL port of Hamid Naderi Yeganeh's \"Mountains\" (mountainCount, timeOffset, skyTop, skyBottom, ridgeColor, valleyColor).\n\nOKLCH seeded backgrounds (palette + seed → reproducible):\n- drawBlobs: soft organic blobs (smoothed jittered circles). Dreamy/soft backgrounds.\n- drawLowPoly: triangulated low-poly facets (gradient-filled). Modern/geometric.\n- drawPeaks: stacked zig-zag mountain ridgeline" +
|
|
7624
|
+
" bands.\n- drawStackedWaves: layered cubic-bézier wave bands — hero/section-divider background.\n- drawScatter: scattered field of small shapes (circles/triangles/squares), palette-colored.\n- draw3DParametricCurve: plot a 3D curve (x(t),y(t),z(t)) projected to canvas. Try Helix: sin(t),cos(t),t/4.\n\nCOMMON PARAMS (all generators):\n- bgColor: Background color (\"none\"/\"transparent\" for no background)\n- interactive: Make generated items selectable/draggable (default false)\n- clipToCanvas: Clip output to canvas bounds (default true, set false for overflow)"
|
|
7620
7625
|
|
|
7621
7626
|
|
|
7622
7627
|
|
|
@@ -7753,9 +7758,13 @@ USE WHEN:
|
|
|
7753
7758
|
String(EffectTypeSchema.options.length) +
|
|
7754
7759
|
": particles, then the shader auras from heatmap on):\n- sparkle: Glitter/sparkle particles (color, speed, size)\n- blast: Explosion burst effect (color, radius, count)\n- smoke: Rising smoke plumes (color, speed, size, drift, height, growthRate)\n- fire: Animated fire/flames (color, colors, speed, size, height, turbulence)\n- rain: Falling rain drops (color, count, speed, angle, spread)\n- snow: Falling snowflakes (color, count, speed, spread, particleLife)\n- confetti: Celebration confetti (colors, count, radius, gravity, size, spread, interval, repeat)\n- ripple: Expanding ring ripples (color, speed, maxRadius, ringCount, strokeWidth)\n- glow: Pulsing glow aura (color drives the shadow h" +
|
|
7755
7760
|
"alo; speed, intensity, size; optional fill — interior tint decoupled from color; fillOpacity 0..1 multiplies intensity opacity)\n- electric: Lightning bolts (color, speed, boltCount, length, flickerRate)\n- bubbles: Rising transparent bubbles (color drives the rim; count, speed, size, spread; optional fill — interior tint, default null = outlined-only; fillOpacity 0..1, default 0.25 when fill is set)\n- dust: Ambient drifting motes (color, count, speed, size, spread)\n- fireflies: Glowing wandering dots (color, count, speed, size, spread)\n- shockwave: Expanding concentric rings — burst type (speed, maxRadius, count, thickness)\n- trail: Fading afterimages following movement (speed, count," +
|
|
7756
|
-
" size, fadeRate)\n
|
|
7757
|
-
"\n-
|
|
7758
|
-
"
|
|
7761
|
+
" size, fadeRate)\n- aurora: Shimmering northern-lights curtain ribbons undulating around the item (color, speed, count, size, spread)\n- orbit_swirl: Small glowing orbs circling the item on spiralling elliptical orbits (color, speed, count, size, maxRadius). The particle swirl; vortex is the shader aura\n- glitch: RGB chromatic-aberration slices jittering across the item bounds (color, speed, count, size)\n- petals: Blossom petals tumbling and swaying across the item (color, speed, count, size, spread)\n\nSHADER AURAS (WebGL2, silhouette-clipped — same tool, routed to the aura system):\n- heatmap: Thermal shimmer aura (palette 0=Hot 1=Cool 2=Violet)\n- liquid_metal: Flowing metallic sheen " +
|
|
7762
|
+
"(palette 0=Chrome 1=Gold 2=Copper)\n- gem_smoke: Crystalline smoke wisps\n- electric_arc: Branching discharge filaments with a hot core and inverse-square bloom. Stepped flicker, not a sine — a stroke is a train of return strokes, which reads far more electric than a smooth pulse.\n- vortex: The bright band is a RING, not a disc — condensation tracks the steepest pressure gradient, so the core is hollow, and differential rotation shears the field rather than spinning one picture round.\n- rain_veil: Drifting sheets of falling rain across the silhouette.\n- caustics: Refracted light patterns, as through moving water.\n- ink_bleed: Ink wicking outward into the surface, as on absorbent paper." +
|
|
7763
|
+
" Routable since the engine began publishing every aura name and warming the subsystem before routing; it was defined-but-unreachable before that, which is why it arrived last.\nAura params: { intensity, radius, palette, mode ('inside'|'outside'|'overlay'), tint, tintStrength, offsetX, offsetY, channels: { a: {speed, curve}, b: {speed, curve} } } — curves: linear, sawtooth, sine, triangle, pulse, ease, bounce, noise.\n\nEXAMPLE — bubbles with magenta interior over a blue rim:\n{ effectType: \"bubbles\", params: { color: \"#93c5fd\", fill: \"#ff00aa\", fillOpacity: 0.4 } }\nEXAMPLE — gold liquid-metal aura outside a logo:\n{ effectType: \"liquid_metal\", params: { palette: 1, mode: \"outside" +
|
|
7764
|
+
"\", intensity: 0.8 } }"
|
|
7765
|
+
|
|
7766
|
+
|
|
7767
|
+
|
|
7759
7768
|
|
|
7760
7769
|
|
|
7761
7770
|
|
|
@@ -7806,7 +7815,7 @@ String(EffectTypeSchema.options.length) +
|
|
|
7806
7815
|
itemId: { type: 'string', description: 'Registry ID of the item' },
|
|
7807
7816
|
effectType: {
|
|
7808
7817
|
type: 'string',
|
|
7809
|
-
enum: [
|
|
7818
|
+
enum: [...ENGINE_EFFECT_TYPES],
|
|
7810
7819
|
description: 'Type of effect',
|
|
7811
7820
|
},
|
|
7812
7821
|
params: {
|
|
@@ -8159,12 +8168,13 @@ String(STYLED_SCENE_STYLES.map((k) => `${k} (${STYLED_SCENE_STYLE_INFO[k]})`).jo
|
|
|
8159
8168
|
{
|
|
8160
8169
|
name: 'pinepaper_emitter',
|
|
8161
8170
|
annotations: { title: 'Particle Emitter', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
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 } —
|
|
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
|
|
8164
|
-
"
|
|
8165
|
-
"
|
|
8166
|
-
"
|
|
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: ['#
|
|
8171
|
+
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 } — merges the spec's top-level keys onto the emitter's. An array it names replaces the old array whole: set with bursts: [one] leaves one burst. To add a burst, pass all of them; the reply's burstsReplaced gives the counts before and after.\n\nSPEC:\n- mode: 'burst' (default) — bursts: [{ t, x, y, co" +
|
|
8172
|
+
"unt, …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, 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 " +
|
|
8173
|
+
"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 + angle 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- spawn: where par" +
|
|
8174
|
+
"ticles start, as a box [width, height] in px on the canvas axes, centred on the burst point (one number is a square). It is not a range and does not turn with angle: spawn [0, 200] is a vertical line of start points whatever the angle. Default [0, 0], a single point.\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- 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 th" +
|
|
8175
|
+
"e 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, 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" +
|
|
8176
|
+
" 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, 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: ['#f43f" +
|
|
8177
|
+
"5e', '#facc15', '#22d3ee'], speed: [300, 700], angle: [-150, -30], gravity: 900, lifetime: 2.5, seed: 7 } }"
|
|
8168
8178
|
|
|
8169
8179
|
|
|
8170
8180
|
|
|
@@ -8190,6 +8200,46 @@ String(STYLED_SCENE_STYLES.map((k) => `${k} (${STYLED_SCENE_STYLE_INFO[k]})`).jo
|
|
|
8190
8200
|
required: ['action', 'spec'],
|
|
8191
8201
|
},
|
|
8192
8202
|
},
|
|
8203
|
+
{
|
|
8204
|
+
name: 'pinepaper_instances',
|
|
8205
|
+
annotations: {
|
|
8206
|
+
title: 'Instances (swarms)',
|
|
8207
|
+
readOnlyHint: false,
|
|
8208
|
+
destructiveHint: false,
|
|
8209
|
+
idempotentHint: false,
|
|
8210
|
+
openWorldHint: false,
|
|
8211
|
+
},
|
|
8212
|
+
description: ("Thousands to a million similar marks as ONE item: a crowd, a swarm, a starfield, rain, sand, a confetti field. Reach for it past about 5,000 similar marks; ordinary items cost memory and time per item, and this costs one item. For bursts that are born and die, use pinepaper_emitter.\n\nACTIONS:\n- rules: the motion rules (with their params and determinism), layouts and shapes this studio has. Read it before create; the lists are the studio's, not a copy.\n- create: { count, seed?, palette? (up to 16), size? (px or [min, max]), shape?, colorMode? ('random' | 'gradient'), opacity?, x?, y?, init?: { layout, width?, height?, radius?, thickness?, length?, angle? }, motion?: { rule, ...params } " +
|
|
8213
|
+
"} → { itemId, count, drawn, subsampled, path, rule, determinism }.\n- modify: { itemId, ...the fields to change } — motion and init merge field by field.\n- describe: { itemId } → its spec, count, drawn, path and determinism.\n\nWHAT THE REPLY TELLS YOU: drawn is how many marks are actually drawn. When the studio draws a subset (a cap for its drawing path), drawn is less than count and subsampled is true: the same marks every frame, and the receipt says so. determinism.scope 'everywhere' means every mark is in the same place at the same time on any machine, so two exports give the same frames (compare their frameDigest). Whether the encoded file is byte-identical is the encoder's business," +
|
|
8214
|
+
" not the swarm's: see the export's bitExact. A rule whose scope is narrower says so in rules and in the export receipt.\n\nA rule, layout or shape the studio does not have is refused by name with its list."
|
|
8215
|
+
|
|
8216
|
+
|
|
8217
|
+
|
|
8218
|
+
|
|
8219
|
+
|
|
8220
|
+
|
|
8221
|
+
|
|
8222
|
+
),
|
|
8223
|
+
inputSchema: {
|
|
8224
|
+
type: 'object',
|
|
8225
|
+
properties: {
|
|
8226
|
+
action: { type: 'string', enum: ['create', 'modify', 'describe', 'rules'], description: "'create' a swarm · 'modify' its spec · 'describe' it · 'rules' what this studio offers" },
|
|
8227
|
+
itemId: { type: 'string', description: 'modify / describe: the swarm item.' },
|
|
8228
|
+
x: { type: 'number' }, y: { type: 'number' },
|
|
8229
|
+
count: { type: 'integer', minimum: 1, maximum: 1000000, description: 'How many marks (up to 1,000,000).' },
|
|
8230
|
+
seed: { type: 'integer', description: 'The same seed is the same swarm.' },
|
|
8231
|
+
palette: { type: 'array', items: { type: 'string' }, maxItems: 16, description: 'Up to 16 colours.' },
|
|
8232
|
+
size: { anyOf: [{ type: 'number' }, { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 }], description: 'Mark size in px (0.25-64), or [min, max].' },
|
|
8233
|
+
shape: { type: 'string', description: "e.g. 'dot' or 'square'; rules lists them." },
|
|
8234
|
+
colorMode: { type: 'string', enum: ['random', 'gradient'] },
|
|
8235
|
+
opacity: { type: 'number', minimum: 0, maximum: 1 },
|
|
8236
|
+
init: { type: 'object', additionalProperties: true, description: 'Where the marks start: { layout, width, height, radius, thickness, length, angle }. Layouts come from rules.' },
|
|
8237
|
+
motion: { type: 'object', additionalProperties: true, description: 'How they move: { rule, ...params }. Rules and their params come from rules.' },
|
|
8238
|
+
name: { type: 'string' },
|
|
8239
|
+
},
|
|
8240
|
+
required: ['action'],
|
|
8241
|
+
},
|
|
8242
|
+
},
|
|
8193
8243
|
{
|
|
8194
8244
|
name: 'pinepaper_render_hook',
|
|
8195
8245
|
annotations: { title: 'Render Hook (register needs PINEPAPER_ALLOW_CODE)', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
@@ -8227,8 +8277,8 @@ String(STYLED_SCENE_STYLES.map((k) => `${k} (${STYLED_SCENE_STYLE_INFO[k]})`).jo
|
|
|
8227
8277
|
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.\n\nBEATS run one after another: [{ at, verb, ...params }]. Positions are the actor's CENTRE in canvas px; a squash keeps its BOTTOM on the ground.\n- pop: scales up from nothing with an overshoot (an entrance in place).\n- 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.\n- hop { to?, height? }: crouch, launch stretched, arc, land squashed, settle. Without to it hops in place.\n- bou" +
|
|
8228
8278
|
"nce { times? (3), height?, decay? (0.6), to? }: bounces that lose height; with to it travels while bouncing.\n- roll { to }: rolls without slipping — it turns by the distance over its radius (right = clockwise).\n- fly { to, arc? }: an arc through the air, nose up on the rise and down on the fall.\n- peek { edge (left|right|top|bottom), amount? (0.5), hold? (1) }: slides partly in from a frame edge, waits, ducks back out.\n- shake { intensity? }: a startled shiver. squash { amount? }: an in-place squash and recover (a reaction, a landing hit).\n- enter { from (edge), to? }: slides in from an edge with an overshoot. exit { edge }: a small anticipation, then away off that edge.\nEach ver" +
|
|
8229
8279
|
"b has a default duration; pass duration to change it.\n\nWHAT 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.\n\nACTORS: 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.\n\nSOUND: sound: true puts a sound on every moment" +
|
|
8230
|
-
" 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,
|
|
8231
|
-
"
|
|
8280
|
+
" 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 such as beep, pop, whoosh, zap; drums such as kick, snare, clap, tabla-na, djembe-tone; or your own from define_sfx; pinepaper_sound list_percussion and list_sfx name them all). 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.\n\nMIX WITH: pinepaper_character / text expressions on top; pinepaper_apply_effect (confetti, sh" +
|
|
8281
|
+
"ockwave) 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.\n\nEXAMPLE — a ball comes to life and crosses the frame:\n{ 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] } ] }"
|
|
8232
8282
|
|
|
8233
8283
|
|
|
8234
8284
|
|
|
@@ -8839,11 +8889,11 @@ String(KEYFRAME_EASINGS_LIST) +
|
|
|
8839
8889
|
" 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" +
|
|
8840
8890
|
"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: " +
|
|
8841
8891
|
String(EFFECT_TYPES_LIST) +
|
|
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.
|
|
8843
|
-
"
|
|
8844
|
-
"
|
|
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: \"
|
|
8846
|
-
"
|
|
8892
|
+
"\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. A batch saves and draws once, so thousands are fast (2,000 in under a second).\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 t" +
|
|
8893
|
+
"hat property; easing is a name 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" +
|
|
8894
|
+
" export the keyframe timeline 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, c" +
|
|
8895
|
+
"olor: \"#ffffff\", opacity: 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_ani" +
|
|
8896
|
+
"mate\", itemId: \"$3\", keyframes: [{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
8897
|
|
|
8848
8898
|
|
|
8849
8899
|
|
|
@@ -9194,11 +9244,11 @@ String(AgentExportFormatSchema.options.join(', ')) +
|
|
|
9194
9244
|
shutter: { type: 'number', minimum: 0, maximum: 1, description: 'Fraction of the frame interval the shutter is open (default 0.5).' },
|
|
9195
9245
|
},
|
|
9196
9246
|
},
|
|
9197
|
-
broadcastHeadroom: { type: 'integer', minimum: 0, maximum: 40, description: 'mp4 with broadcast only, default 12: luma codes kept clear at both ends,
|
|
9247
|
+
broadcastHeadroom: { type: 'integer', minimum: 0, maximum: 40, description: 'mp4 with broadcast only, default 12: luma codes kept clear at both ends, which cuts encoder ringing outside 16-235 (4 = ~100x fewer out-of-range samples, 12 = ~1000x; not zero on every picture, and more headroom narrows the luma range of the picture). Fidelity raises luma_out_of_range with the legaliser command when any remain.' },
|
|
9198
9248
|
bitrate: { type: 'integer', minimum: 100000, maximum: 200000000, description: 'mp4 / webm: target bits/s, replacing the quality-derived one. The browser encoder treats it as a CEILING; result.video.achievedBitrate is what it produced.' },
|
|
9199
9249
|
minBitrate: { type: 'integer', minimum: 100000, maximum: 200000000, description: 'mp4 / webm: a floor; a miss is a fidelity warning naming the re-encode a delivery spec needs.' },
|
|
9200
9250
|
bitrateMode: { type: 'string', enum: ['constant', 'variable'], description: "mp4 / webm: 'constant' (the default with broadcast) or 'variable'." },
|
|
9201
|
-
audioCodec: { type: 'string', enum: ['any', 'aac'], description: "mp4 only: 'aac' refuses the export (code audio_codec_unavailable, no file) where the soundtrack could only be Opus — Chromium on Linux has no AAC encoder; macOS and Windows do. 'any' (default) muxes Opus there with the audio_codec_opus warning (whatsappSafe: false). Silent MP4s are AAC everywhere." },
|
|
9251
|
+
audioCodec: { type: 'string', enum: ['any', 'aac'], description: "mp4 only: 'aac' refuses the export (code audio_codec_unavailable, no file) where the soundtrack could only be Opus — Chromium on Linux has no AAC encoder; macOS and Windows do. 'any' (default) muxes Opus there with the audio_codec_opus warning (whatsappSafe: false). Silent MP4s are AAC everywhere. Without it: where the browser could only make Opus (Chromium on Linux), the Claude Desktop extension re-encodes the audio to AAC (video copied; result.audioDelivered, and file.sha256 is the delivered file); without ffmpeg, a social platform (instagram, tiktok, twitter, linkedin) is refused by name and anything else keeps the Opus file with a note." },
|
|
9202
9252
|
maxBytes: { type: 'integer', minimum: 1, description: 'gif only: size budget in bytes (email: 1000000). Over it, the GIF is re-encoded at a smaller frame size, at most twice; result.budget reports each attempt.' },
|
|
9203
9253
|
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.' },
|
|
9204
9254
|
pdf: {
|