@pinepaper.studio/mcp-server 1.6.35 → 1.6.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.de.md +1 -1
  2. package/README.es.md +1 -1
  3. package/README.fr.md +1 -1
  4. package/README.hi.md +1 -1
  5. package/README.ja.md +1 -1
  6. package/README.ko.md +1 -1
  7. package/README.md +44 -6
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  11. package/dist/browser/puppeteer-controller.js +4 -0
  12. package/dist/browser/puppeteer-controller.js.map +1 -1
  13. package/dist/build-info.json +2 -2
  14. package/dist/ontology/upstream-vocabulary.d.ts +1 -1
  15. package/dist/ontology/upstream-vocabulary.js +1 -1
  16. package/dist/tools/definitions.d.ts.map +1 -1
  17. package/dist/tools/definitions.js +73 -32
  18. package/dist/tools/definitions.js.map +1 -1
  19. package/dist/tools/effect-class.d.ts.map +1 -1
  20. package/dist/tools/effect-class.js +4 -0
  21. package/dist/tools/effect-class.js.map +1 -1
  22. package/dist/tools/engine-lists.d.ts +2 -2
  23. package/dist/tools/engine-lists.js +2 -2
  24. package/dist/tools/engine-methods.d.ts.map +1 -1
  25. package/dist/tools/engine-methods.js +3 -0
  26. package/dist/tools/engine-methods.js.map +1 -1
  27. package/dist/tools/engine-surface.d.ts +1 -1
  28. package/dist/tools/engine-surface.d.ts.map +1 -1
  29. package/dist/tools/engine-surface.js +14 -5
  30. package/dist/tools/engine-surface.js.map +1 -1
  31. package/dist/tools/handlers.d.ts +7 -0
  32. package/dist/tools/handlers.d.ts.map +1 -1
  33. package/dist/tools/handlers.js +34 -1
  34. package/dist/tools/handlers.js.map +1 -1
  35. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  36. package/dist/tools/minimal-descriptions.js +1 -0
  37. package/dist/tools/minimal-descriptions.js.map +1 -1
  38. package/dist/tools/shape-params.d.ts +2 -2
  39. package/dist/tools/shape-params.js +2 -2
  40. package/dist/tools/stick-vocabulary.d.ts +1 -1
  41. package/dist/tools/stick-vocabulary.js +1 -1
  42. package/dist/tools/toolkits.d.ts.map +1 -1
  43. package/dist/tools/toolkits.js +2 -1
  44. package/dist/tools/toolkits.js.map +1 -1
  45. package/dist/types/code-generator.d.ts +7 -1
  46. package/dist/types/code-generator.d.ts.map +1 -1
  47. package/dist/types/code-generator.js +98 -11
  48. package/dist/types/code-generator.js.map +1 -1
  49. package/dist/types/schemas.d.ts +35 -3
  50. package/dist/types/schemas.d.ts.map +1 -1
  51. package/dist/types/schemas.js +31 -9
  52. package/dist/types/schemas.js.map +1 -1
  53. package/dist/utils/asset-paths.js +1 -1
  54. package/dist/utils/asset-paths.js.map +1 -1
  55. package/manifest.json +5 -1
  56. package/package.json +1 -1
@@ -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'. 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" +
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 ignoredProperties).\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 r" +
429
- "emembered: 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, so 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 " +
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'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: '" +
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 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 " +
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, color, 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' | 'sc" +
433
- "alene', 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.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" +
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 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, " +
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- pentagon: 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: Righ" +
436
- "t-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 both 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 " +
437
- "the engine's own. Three further parameters are each\nread by six of these eight shapes, and THE THREE SIXES ARE NOT THE SAME SIX —\na shape missing from a list ignores that parameter silently, so check before\nyou pass it:\n cornerRadius — speech-bubble, speech-bubble-pointed, comment-box,\n callout-box, double-bubble, quote-bubble\n tailDirection (bottom-left | bottom-right | bottom | top-left | top-right |\n top | left | right) — speech-bubble, speech-bubble-square,\n speech-bubble-pointed, thought-bubble, comment-box, callout-box\n tailSize (0-1, default 0.35) — speech-bubble, speech-bubble-square,\n speech-bubble-pointed, comment-box, callout-box, double-bubble\n- speech" +
438
- "-bubble: Classic round speech bubble with triangular tail.\n- speech-bubble-square: Rectangular speech bubble with sharp corners and\n pointed tail.\n- speech-bubble-pointed: Tooltip-style bubble with arrow pointer (tail defaults\n to \"bottom\").\n- thought-bubble: Cloud-like thought bubble with trailing dots.\n- comment-box: Modern chat message bubble with high corner radius.\n- callout-box: Annotation callout with narrow arrow (tail defaults to \"left\").\n- double-bubble: Two overlapping bubbles for conversation (properties:\n secondaryColor).\n- quote-bubble: Stylized bubble with quotation marks.\n\nPATH FOR CUSTOM SHAPES:\nUse itemType: \"path\" with either:\n- segments: Array of " +
439
- "[x,y] points for polygons, e.g. [[100,100], [200,50], [300,100]]\n- pathData: SVG path string for curves, e.g. \"M 100 100 C 150 50, 250 50, 300 100 Z\"\n\nADVANCED STYLING (NEW):\nAll shapes support these additional properties:\n- No fill (stroke-only outlines, line art): fillColor: null, 'transparent' or 'none', with a strokeColor and strokeWidth.\n- fillOpacity (0..1): the solid fill's own alpha, apart from opacity (which fades the stroke too) — on text, fillOpacity: 0.3 with a strokeColor is a see-through letter with a solid outline. Not applied to a gradient fill. The result's fillOpacity says whether the studio applied it (an older studio ignores the key and says so).\n- Gradients: c" +
440
- "olor can be an object: {type: \"linear\"|\"radial\", stops: [{color: \"#fff\", offset: 0}, {color: \"#000\", offset: 1}], origin: [x,y], destination: [x,y]}. Without origin / destination a radial gradient is centred on the item's own bounds and reaches its right edge, wherever anchor put it; a linear one runs top to bottom across the item's bounds. Pass origin and destination (canvas px) for any other direction.\n- Shadows: shadowColor, shadowBlur (pixels), shadowOffset: [x, y]\n- Blend modes: blendMode: \"multiply\"|\"screen\"|\"overlay\"|\"darken\"|\"lighten\"|etc.\n- Opacity: opacity (0-1)\n\nEXAMPLES:\n- \"Create red text saying HELLO\" → itemType: \"text\", properties: {content: \"HEL" +
441
- "LO\", color: \"#ef4444\", fontSize: 48}\n- \"Add a blue circle\" → itemType: \"circle\", properties: {radius: 50, color: \"#3b82f6\"}\n- \"Create a 5-pointed gold star\" → itemType: \"star\", properties: {radius1: 60, radius2: 30, points: 5, color: \"#fbbf24\"}\n- \"Draw a heart\" → itemType: \"path\", properties: {pathData: \"M 300 350 C 300 300, 250 250, 200 250 C 150 250, 100 300, 100 350 C 100 450, 300 550, 300 550 C 300 550, 500 450, 500 350 C 500 300, 450 250, 400 250 C 350 250, 300 300, 300 350 Z\", fillColor: \"#ef4444\"}\n- \"Circle with radial gradient\" → itemType: \"circle\", properties: {radius: 50, color: {type: \"radial\", stops: [{color: \"#ff6b6b\", offset: 0}, {color: \"#" +
442
- "c0392b\", offset: 1}]}}\n- \"Rectangle with shadow\" → itemType: \"rectangle\", properties: {width: 100, height: 50, color: \"#3b82f6\", shadowColor: \"rgba(0,0,0,0.5)\", shadowBlur: 10, shadowOffset: [5, 5]}\n\nITEM PROVENANCE:\nItems on the canvas can originate from multiple sources — all become first-class items with itemIds:\n- pinepaper_create_item: Direct shape creation (this tool)\n- pinepaper_import_svg: SVG file import → becomes a full item\n- pinepaper_import_image: Raster image import → becomes a full item\n- pinepaper_import_asset: Pre-made asset import → becomes a full item\nAll imported items participate in the relation and animation systems just like natively created items." +
443
- "\n\nRENDER-TIME SURFACES — 'shader' and 'field':\nThese are not shapes. A shape is built as an item and drawn from its geometry; a\nsurface has no geometry — it is evaluated, per pixel or per sample, as each frame\nis drawn. Use them for things a vector cannot be: an open sea, a drifting field\nof marks, a lit material.\n- shader: pick one of water | liquid_metal | heatmap | gem_smoke, give it width\n and height, and set behind:true to place it UNDER the vector art — which is\n what scenery wants, a sea the boats float ON rather than a sheen over them.\n- field: xExpr/yExpr/count draw many marks from one expression, re-evaluated\n every frame against t. rExpr and aExpr vary size and alp" +
444
- "ha per mark.\nBoth draw fully in a cloud render; on a local canvas they stand in as a flat\nplate of their own colour so the layout still reads.\n\nLIFETIMES — bornAt and ttl, on any item:\nThe only way to build a piece that CUTS between shots. Without them every item\nis on screen for the whole render, which is what makes a long piece read as one\ncrowded frame instead of a sequence.\n\nWORKFLOW TIP:\nAfter creating items, use pinepaper_add_relation to animate them (e.g., orbits, follows, attached_to).\nFor glossy 3D spheres, use pinepaper_create_glossy_sphere instead. For diagonal stripes, use pinepaper_create_diagonal_stripes."
445
-
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.\n- shadowColor / s" +
1167
- "hadowBlur / 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"
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
 
@@ -2854,7 +2855,7 @@ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
2854
2855
  action: { type: 'string', enum: ['figure', 'set'], description: "'figure' or 'set'." },
2855
2856
  id: { type: 'string', description: 'Prefix for the created items.' },
2856
2857
  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: '{ x?, y? } — where it goes.' },
2858
+ 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
2859
  scale: { type: 'number', description: 'Size multiplier.' },
2859
2860
  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
2861
  pose: { type: 'string', enum: [...STICK_POSES], description: 'figure: ONE pose, held.' },
@@ -3054,7 +3055,7 @@ EXAMPLE: { itemId: 'item_7', color: '#00b140', threshold: 60, smoothing: 12 }`,
3054
3055
  itemId: { type: 'string', description: 'timbre_from_path: the path whose shape becomes the timbre. set_placement / remove: the sound item.' },
3055
3056
  samples: { type: 'number', description: 'timbre_from_path: points sampled along the path (default 256).' },
3056
3057
  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.' },
3058
+ 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
3059
  },
3059
3060
  required: ['action'],
3060
3061
  },
@@ -3986,9 +3987,9 @@ USE WHEN:
3986
3987
  "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
3988
  ", 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
3989
  "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.\n- STROKE DRAW-ON (paths): trimStart, trimEnd (0-1 of the path's length), trimOffset (shifts the visible window 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 ARRI" +
3990
- "VING 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' 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" +
3991
- " they mark either way.\n" +
3990
+ "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" +
3991
+ "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'" +
3992
+ " 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
3993
  String(EASING_LINES) +
3993
3994
  "\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
3995
  "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 +5720,7 @@ this tool, so a control cannot exist that an agent cannot set.`,
5719
5720
  description: `Undo/redo and history state inspection.
5720
5721
 
5721
5722
  ACTIONS:
5722
- - undo: Undo last canvas action
5723
+ - 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
5724
  - redo: Redo last undone action
5724
5725
  - get_state: Get history stack state (canUndo, canRedo, count)`,
5725
5726
  inputSchema: {
@@ -8190,6 +8191,46 @@ String(STYLED_SCENE_STYLES.map((k) => `${k} (${STYLED_SCENE_STYLE_INFO[k]})`).jo
8190
8191
  required: ['action', 'spec'],
8191
8192
  },
8192
8193
  },
8194
+ {
8195
+ name: 'pinepaper_instances',
8196
+ annotations: {
8197
+ title: 'Instances (swarms)',
8198
+ readOnlyHint: false,
8199
+ destructiveHint: false,
8200
+ idempotentHint: false,
8201
+ openWorldHint: false,
8202
+ },
8203
+ 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 } " +
8204
+ "} → { 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 two exports anywhere give the same file; 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 lis" +
8205
+ "t."
8206
+
8207
+
8208
+
8209
+
8210
+
8211
+
8212
+
8213
+ ),
8214
+ inputSchema: {
8215
+ type: 'object',
8216
+ properties: {
8217
+ action: { type: 'string', enum: ['create', 'modify', 'describe', 'rules'], description: "'create' a swarm · 'modify' its spec · 'describe' it · 'rules' what this studio offers" },
8218
+ itemId: { type: 'string', description: 'modify / describe: the swarm item.' },
8219
+ x: { type: 'number' }, y: { type: 'number' },
8220
+ count: { type: 'integer', minimum: 1, maximum: 1000000, description: 'How many marks (up to 1,000,000).' },
8221
+ seed: { type: 'integer', description: 'The same seed is the same swarm.' },
8222
+ palette: { type: 'array', items: { type: 'string' }, maxItems: 16, description: 'Up to 16 colours.' },
8223
+ size: { anyOf: [{ type: 'number' }, { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 }], description: 'Mark size in px (0.25-64), or [min, max].' },
8224
+ shape: { type: 'string', description: "e.g. 'dot' or 'square'; rules lists them." },
8225
+ colorMode: { type: 'string', enum: ['random', 'gradient'] },
8226
+ opacity: { type: 'number', minimum: 0, maximum: 1 },
8227
+ init: { type: 'object', additionalProperties: true, description: 'Where the marks start: { layout, width, height, radius, thickness, length, angle }. Layouts come from rules.' },
8228
+ motion: { type: 'object', additionalProperties: true, description: 'How they move: { rule, ...params }. Rules and their params come from rules.' },
8229
+ name: { type: 'string' },
8230
+ },
8231
+ required: ['action'],
8232
+ },
8233
+ },
8193
8234
  {
8194
8235
  name: 'pinepaper_render_hook',
8195
8236
  annotations: { title: 'Render Hook (register needs PINEPAPER_ALLOW_CODE)', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
@@ -8839,11 +8880,11 @@ String(KEYFRAME_EASINGS_LIST) +
8839
8880
  " 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
8881
  "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
8882
  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. Slow past a few hundred (2000 took about a minute).\n keys on keyframe_animate — {\"time\":[0,1,2],\"x\":[100,400,700],\"y\":[300,null,500],\"easing\":\"easeOut\"} in place of keyframes: one list per property, each as long as time; a null means that key does not set that property; easing is a n" +
8843
- "ame or a list with one per key. Evenly spaced keys: \"time\":{\"step\":0.033333,\"count\":180}.\n\nVARIABLE REFERENCES: \"$0\", \"$1\" etc. reference the items this batch makes, in op order: create and group ops each take the next number (a ring / grid / orbit / create_many that makes items takes one per item); no other op does. [create, create, group, …] makes the group \"$2\". Place the ops that make an item before any op that references it.\n\nANIMATION NOTES:\n- keyframe_animate position accepts [x, y] or { x, y } (normalized). fillColor/strokeColor accept rgba() for alpha. The create \"opacity\" and \"blendMode\" props are applied to the item.\n- For MP4/GIF export the keyframe timeli" +
8844
- "ne is rendered once across the duration; author motion to fill the clip and use loop:false. \n\nEXAMPLE — Animated sky scene with timed reveals:\n{operations: [\n {type: \"set_canvas_size\", preset: \"full-hd-1080p\"},\n {type: \"execute_generator\", generatorName: \"drawGradientMesh\", generatorParams: {colors: [\"#1e3a5f\", \"#87CEEB\", \"#fbbf24\"], bgColor: \"#0c1445\", blobCount: 4}},\n {type: \"create\", itemType: \"circle\", position: {x: 1600, y: 150}, properties: {radius: 80, color: \"#fbbf24\", shadowColor: \"#fbbf24\", shadowBlur: 40}},\n {type: \"create\", itemType: \"ellipse\", position: {x: 400, y: 180}, properties: {radiusX: 100, radiusY: 35, color: \"#ffffff\", opacity:" +
8845
- " 0.8}},\n {type: \"create\", itemType: \"ellipse\", position: {x: 900, y: 120}, properties: {radiusX: 120, radiusY: 40, color: \"#ffffff\", opacity: 0.7}},\n {type: \"create\", itemType: \"text\", position: {x: 960, y: 500}, properties: {content: \"Dream Big\", fontSize: 96, color: \"#ffffff\", fontWeight: \"bold\"}},\n {type: \"animate\", itemId: \"$0\", animationType: \"pulse\", animationOptions: {speed: 0.3}},\n {type: \"animate\", itemId: \"$1\", animationType: \"slideLeftRight\", animationOptions: {speed: 0.2}},\n {type: \"animate\", itemId: \"$2\", animationType: \"slide\", animationOptions: {speed: 0.15, direction: \"left\"}},\n {type: \"keyframe_animate\", itemId: \"$3\", key" +
8846
- "frames: [{time: 0, properties: {opacity: 0, scale: 0.8}}, {time: 2, properties: {opacity: 1, scale: 1}, easing: \"easeOutBack\"}], duration: 3},\n {type: \"apply_mask\", itemId: \"$3\", maskPreset: \"wipeLeft\"},\n {type: \"play_timeline\", action: \"play\", duration: 5, loop: true}\n]}"
8883
+ "\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" +
8884
+ "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" +
8885
+ " 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" +
8886
+ "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" +
8887
+ "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
8888
 
8848
8889
 
8849
8890