@pinepaper.studio/mcp-server 1.6.12 → 1.6.14

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 (100) 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 +341 -4
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/agent/export-manager.d.ts.map +1 -1
  11. package/dist/agent/export-manager.js +9 -7
  12. package/dist/agent/export-manager.js.map +1 -1
  13. package/dist/assets/repositories/iconify.d.ts +0 -5
  14. package/dist/assets/repositories/iconify.d.ts.map +1 -1
  15. package/dist/assets/repositories/iconify.js +17 -5
  16. package/dist/assets/repositories/iconify.js.map +1 -1
  17. package/dist/browser/puppeteer-controller.d.ts +16 -0
  18. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  19. package/dist/browser/puppeteer-controller.js +32 -2
  20. package/dist/browser/puppeteer-controller.js.map +1 -1
  21. package/dist/build-info.json +5 -0
  22. package/dist/design/design-systems.d.ts +27 -2
  23. package/dist/design/design-systems.d.ts.map +1 -1
  24. package/dist/design/design-systems.js +20 -0
  25. package/dist/design/design-systems.js.map +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/ontology/upstream-vocabulary.d.ts +23 -0
  30. package/dist/ontology/upstream-vocabulary.d.ts.map +1 -0
  31. package/dist/ontology/upstream-vocabulary.js +1172 -0
  32. package/dist/ontology/upstream-vocabulary.js.map +1 -0
  33. package/dist/ontology/vocabulary.d.ts.map +1 -1
  34. package/dist/ontology/vocabulary.js +19 -0
  35. package/dist/ontology/vocabulary.js.map +1 -1
  36. package/dist/tools/definitions.d.ts.map +1 -1
  37. package/dist/tools/definitions.js +299 -75
  38. package/dist/tools/definitions.js.map +1 -1
  39. package/dist/tools/engine-methods.d.ts.map +1 -1
  40. package/dist/tools/engine-methods.js +28 -2
  41. package/dist/tools/engine-methods.js.map +1 -1
  42. package/dist/tools/engine-surface.d.ts +1 -1
  43. package/dist/tools/engine-surface.d.ts.map +1 -1
  44. package/dist/tools/engine-surface.js +18 -12
  45. package/dist/tools/engine-surface.js.map +1 -1
  46. package/dist/tools/handlers/camera.d.ts.map +1 -1
  47. package/dist/tools/handlers/camera.js +17 -2
  48. package/dist/tools/handlers/camera.js.map +1 -1
  49. package/dist/tools/handlers/font.d.ts.map +1 -1
  50. package/dist/tools/handlers/font.js +48 -3
  51. package/dist/tools/handlers/font.js.map +1 -1
  52. package/dist/tools/handlers/maps.d.ts.map +1 -1
  53. package/dist/tools/handlers/maps.js +16 -0
  54. package/dist/tools/handlers/maps.js.map +1 -1
  55. package/dist/tools/handlers.d.ts +24 -3
  56. package/dist/tools/handlers.d.ts.map +1 -1
  57. package/dist/tools/handlers.js +645 -27
  58. package/dist/tools/handlers.js.map +1 -1
  59. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  60. package/dist/tools/minimal-descriptions.js +2 -0
  61. package/dist/tools/minimal-descriptions.js.map +1 -1
  62. package/dist/tools/shape-params.d.ts +13 -2
  63. package/dist/tools/shape-params.d.ts.map +1 -1
  64. package/dist/tools/shape-params.js +80 -2
  65. package/dist/tools/shape-params.js.map +1 -1
  66. package/dist/tools/stick-vocabulary.d.ts +1 -1
  67. package/dist/tools/stick-vocabulary.js +1 -1
  68. package/dist/tools/toolkits.d.ts.map +1 -1
  69. package/dist/tools/toolkits.js +12 -1
  70. package/dist/tools/toolkits.js.map +1 -1
  71. package/dist/types/code-generator.d.ts +27 -1
  72. package/dist/types/code-generator.d.ts.map +1 -1
  73. package/dist/types/code-generator.js +2871 -316
  74. package/dist/types/code-generator.js.map +1 -1
  75. package/dist/types/schemas.d.ts +1595 -271
  76. package/dist/types/schemas.d.ts.map +1 -1
  77. package/dist/types/schemas.js +269 -31
  78. package/dist/types/schemas.js.map +1 -1
  79. package/dist/utils/a11y.d.ts +67 -0
  80. package/dist/utils/a11y.d.ts.map +1 -0
  81. package/dist/utils/a11y.js +117 -0
  82. package/dist/utils/a11y.js.map +1 -0
  83. package/dist/utils/ad-package.d.ts +44 -0
  84. package/dist/utils/ad-package.d.ts.map +1 -0
  85. package/dist/utils/ad-package.js +121 -0
  86. package/dist/utils/ad-package.js.map +1 -0
  87. package/dist/utils/scc.d.ts +41 -0
  88. package/dist/utils/scc.d.ts.map +1 -0
  89. package/dist/utils/scc.js +185 -0
  90. package/dist/utils/scc.js.map +1 -0
  91. package/dist/utils/zip.d.ts +7 -0
  92. package/dist/utils/zip.d.ts.map +1 -0
  93. package/dist/utils/zip.js +82 -0
  94. package/dist/utils/zip.js.map +1 -0
  95. package/dist/version.d.ts +3 -0
  96. package/dist/version.d.ts.map +1 -1
  97. package/dist/version.js +31 -1
  98. package/dist/version.js.map +1 -1
  99. package/manifest.json +9 -1
  100. package/package.json +6 -4
@@ -11,7 +11,7 @@ import { COMPACT_DESCRIPTIONS } from './compact-descriptions.js';
11
11
  import { MINIMAL_DESCRIPTIONS } from './minimal-descriptions.js';
12
12
  import { STICK_GAITS, STICK_POSES, STICK_SEQUENCES, STICK_EXPRESSIONS, STICK_PROPS } from './stick-vocabulary.js';
13
13
  import { GeneratorNameSchema, ItemTypeSchema } from '../types/schemas.js';
14
- import { EffectTypeSchema, SimpleAnimationTypeSchema, GEOMETRY_OPERATIONS, THREAD_STITCHES, } from '../types/schemas.js';
14
+ import { EffectTypeSchema, SimpleAnimationTypeSchema, EasingSchema, GEOMETRY_OPERATIONS, THREAD_STITCHES, } from '../types/schemas.js';
15
15
  /**
16
16
  * Vocabulary lists derived from Zod schemas so tool descriptions stay in
17
17
  * sync automatically. The description-schema-parity test
@@ -21,6 +21,10 @@ import { EffectTypeSchema, SimpleAnimationTypeSchema, GEOMETRY_OPERATIONS, THREA
21
21
  */
22
22
  const EFFECT_TYPES_LIST = EffectTypeSchema.options.join(', ');
23
23
  const SIMPLE_ANIMATION_TYPES_LIST = SimpleAnimationTypeSchema.options.join(', ');
24
+ // The easings a keyframe accepts. The batch description once listed
25
+ // easeInCubic / easeOutCubic / easeOutBounce, which keyframe_animate rejects
26
+ // (round 8 DD, 1.68) — derived now, so it cannot say what the schema refuses.
27
+ const KEYFRAME_EASINGS_LIST = EasingSchema.options.join(', ');
24
28
  // =============================================================================
25
29
  // AI AGENT GUIDE (exported for pinepaper_tool_guide)
26
30
  // =============================================================================
@@ -257,12 +261,12 @@ COMMON PRESETS:
257
261
  - hd-landscape: 1920x1080
258
262
  - hd-portrait: 1080x1920
259
263
 
260
- Max canvas size: 4096x4096. For wedding invitations, event cards, or detailed designs, use at least 1080x1080 or larger.`,
264
+ Max canvas size: 8192x8192 (a 5400x1350 carousel panorama fits). For wedding invitations, event cards, or detailed designs, use at least 1080x1080 or larger.`,
261
265
  inputSchema: {
262
266
  type: 'object',
263
267
  properties: {
264
- width: { type: 'number', description: 'Canvas width in pixels (100-4096)' },
265
- height: { type: 'number', description: 'Canvas height in pixels (100-4096)' },
268
+ width: { type: 'number', description: 'Canvas width in pixels (100-8192)' },
269
+ height: { type: 'number', description: 'Canvas height in pixels (100-8192)' },
266
270
  preset: {
267
271
  type: 'string',
268
272
  description: 'Optional preset name',
@@ -338,10 +342,19 @@ USE WHEN:
338
342
 
339
343
  IMPORTANT NOTES:
340
344
  - This reloads the entire page, which resets everything
341
- - The welcome template will NOT appear after refresh (only shows for first-time visitors)
342
- - After refresh, the canvas will be completely empty
343
345
  - All unsaved work will be lost
344
- - You may need to wait a moment after refresh before executing other commands`,
346
+ - You may need to wait a moment after refresh before executing other commands
347
+
348
+ WHAT YOU ACTUALLY GET, which is not always an empty canvas:
349
+ - The template picker and a "Restore your previous work?" dialog can both
350
+ appear after a reload — observed in agent mode. A screenshot taken straight
351
+ afterwards shows those, not your scene, so do not read one as evidence that
352
+ the canvas is broken or that your items failed to draw.
353
+ - Dismiss or clear whatever appears before judging the result, and prefer
354
+ pinepaper_agent_reset when you want a clean canvas rather than a clean PAGE:
355
+ it skips the reload and the dialogs entirely.
356
+ - This description used to promise the welcome template would not appear and
357
+ that the canvas would be empty. Neither is reliable, so neither is claimed.`,
345
358
  inputSchema: {
346
359
  type: 'object',
347
360
  properties: {},
@@ -391,6 +404,10 @@ its own size. anchor: 'top-left' (also top-right, bottom-left, bottom-right,
391
404
  center; origin is an alias for the parameter) fixes that without you computing
392
405
  bounds you cannot know until the item exists. It is applied after size and
393
406
  rotation, so it uses the item's real box.
407
+ PIXEL-EXACT SLOTS (a spec'd 615x345 end-screen box, a UI frame): use anchor
408
+ 'top-left' with WHOLE-NUMBER x, y, width and height. A centred box with an odd
409
+ width or height has its edges on half pixels, which renders 1 px short with a
410
+ soft anti-aliased edge (measured: 615x345 centred came out 614x344).
394
411
 
395
412
  EVERY type below also takes a label, which wraps the item in a labelled group —
396
413
  you do not need a second create call and a hand-computed position to caption a
@@ -404,7 +421,12 @@ Labelling a text item gives you TWO text items in the group — the body and the
404
421
  label. That is what you want for a caption; it is a surprise if you expected
405
422
  the label to replace the content.
406
423
 
407
- - text: Text content with font styling (properties: content, fontSize, color, fontFamily)
424
+ - text: Text content with font styling (properties: content, fontSize, color, fontFamily, justification, fontWeight, fontStyle, leading, lineHeight)
425
+ 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.
426
+ 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.
427
+ 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 studio applied it; a studio without RTL support lays text out left-to-right and says so. justification: 'right' is alignment, not direction.
428
+ 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 says whether the studio applied them.
429
+ 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.
408
430
  - circle: Circular shape (properties: radius, color, strokeColor, strokeWidth)
409
431
  - star: Star shape (properties: radius1, radius2, points, color)
410
432
  radius1 is the OUTER radius and is what sizes the star; radius or
@@ -441,6 +463,7 @@ the label to replace the content.
441
463
  positioned it. Offset the points yourself, or use sides + radius + position.
442
464
  - ellipse: Oval shape (properties: width, height, color) — defaults to 100x60
443
465
  - path: Custom path with segments or SVG data (properties: segments, pathData, strokeColor, fillColor, closed, smooth)
466
+ 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.
444
467
  - line: Line between two points (properties: from, to, strokeColor, strokeWidth)
445
468
  - arc: Curved arc through three points (properties: from, through, to, strokeColor)
446
469
  - pentagon: Regular 5-sided polygon (properties: radius, color)
@@ -487,6 +510,7 @@ Use itemType: "path" with either:
487
510
 
488
511
  ADVANCED STYLING (NEW):
489
512
  All shapes support these additional properties:
513
+ - No fill (stroke-only outlines, line art): fillColor: null, 'transparent' or 'none', with a strokeColor and strokeWidth.
490
514
  - Gradients: color can be an object: {type: "linear"|"radial", stops: [{color: "#fff", offset: 0}, {color: "#000", offset: 1}], origin: [x,y], destination: [x,y]}
491
515
  - Shadows: shadowColor, shadowBlur (pixels), shadowOffset: [x, y]
492
516
  - Blend modes: blendMode: "multiply"|"screen"|"overlay"|"darken"|"lighten"|etc.
@@ -565,6 +589,11 @@ LIFETIME — on ANY item, and the only way to build a piece that CUTS between sh
565
589
  - ttl: seconds it lives for. Omit for "until the end".
566
590
  Without these every item is on screen for the whole render, which is what makes
567
591
  a long piece read as one crowded frame instead of a sequence of shots.
592
+ They set the item's VISIBILITY for [bornAt, bornAt + ttl) — not its opacity —
593
+ so they compose with any keyframes, opacity fades included. Re-timing with
594
+ pinepaper_modify_item {bornAt, ttl} replaces the previous lifetime — pass both.
595
+ The result's lifetime field says whether it was applied; it is runtime-only
596
+ until the studio reads bornAt / ttl itself, so re-time a reopened scene.
568
597
 
569
598
  SHADER (itemType 'shader') — a lit surface, drawn per pixel:
570
599
  - shader: 'water' (open sea, lit swell) | 'liquid_metal' (chrome) | 'heatmap' (thermal halo) | 'gem_smoke' (smoke) | 'electric_arc' (branching discharge, stepped flicker) | 'vortex' (hollow bright RING, sheared by differential rotation) | 'rain_veil' | 'caustics' | 'ink_bleed'
@@ -587,6 +616,11 @@ thousands of authored items.
587
616
  - Example — a rolling swell: xExpr "(i % 120) * 8", yExpr "400 + sin((i % 120) * 0.4 - t * 2) * 12", count 3600`,
588
617
  additionalProperties: true,
589
618
  },
619
+ anchor: {
620
+ type: 'string',
621
+ enum: ['center', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
622
+ description: "Which point of the item `position` names (default center). Same as properties.anchor — for a pixel-exact slot use 'top-left' with whole numbers.",
623
+ },
590
624
  data: {
591
625
  type: 'object',
592
626
  description: 'Item data flags for selection and behavior control',
@@ -659,6 +693,7 @@ MODIFIABLE PROPERTIES:
659
693
  - content: Text content
660
694
  - opacity: Transparency (0-1)
661
695
  - rotation: Rotation in degrees
696
+ - skewX / skewY: shear in degrees about the item's centre; matrix: [a, b, c, d, tx, ty] affine. For mockups (a box face, a screen at an angle). APPLIED to the current shape, so calling again compounds — the result's affine says so.
662
697
  - scale: Size multiplier
663
698
  - shadowColor: Shadow color
664
699
  - shadowBlur: Shadow blur radius
@@ -957,6 +992,8 @@ USE WHEN:
957
992
  - Adding logos or vector graphics
958
993
  - User mentions SVG files or vector graphics
959
994
 
995
+ CHECK LINE-ART IMPORTS: fill="none" and stroke-dasharray have been measured to be lost on import — outline icons come in filled, dashed strokes come in solid. For line art, check the result with a screenshot; to fix it, set fillColor: null (and dashArray) on the imported paths with pinepaper_modify_item.
996
+
960
997
  ⚠️ RECOMMENDED FOR COMPLEX CHARACTERS:
961
998
  Instead of drawing "a witch" or "a cat" with many path operations, use SVG import:
962
999
  1. Describe what you need and use a simple placeholder SVG
@@ -1120,6 +1157,11 @@ EXAMPLES:
1120
1157
  enum: ['circle', 'rounded', 'hexagon', 'star'],
1121
1158
  description: 'Optional mask shape to apply to the image',
1122
1159
  },
1160
+ smoothing: {
1161
+ type: 'string',
1162
+ enum: ['off', 'low', 'medium', 'high'],
1163
+ description: "Resampling when the image is scaled. 'off' = nearest-neighbour, hard pixel edges — use it for pixel art. Default 'low'. Change it later with pinepaper_modify_item {smoothing}.",
1164
+ },
1123
1165
  },
1124
1166
  required: ['url'],
1125
1167
  },
@@ -1530,7 +1572,7 @@ warp (optional): chained parametric warp(s) — displace each sampled point by {
1530
1572
 
1531
1573
  RETURNS: { success, kind, itemId } — itemId is the created path. Bad expressions / empty ranges return { success:false, error }.
1532
1574
 
1533
- To make an item TRAVEL along an equation curve (instead of drawing it), use pinepaper_add_relation relationType moves_along_path with params.equation instead.
1575
+ To make an item TRAVEL along an equation curve (instead of drawing it), use pinepaper_add_relation relationType moves_along_path with params.equation — but note that equation is read ONLY on the relation's signal path. An option that leaves that path takes the equation with it and the item simply does not move: easing 'bounce' is piecewise and falls back, so bounce + equation is a static item reporting success. Use a signal-friendly easing (linear, easeIn, easeOut, easeInOut, sine, pingpong) with an equation, or supply params.path instead.
1534
1576
 
1535
1577
  EXAMPLES:
1536
1578
  - Sine wave: { kind: "function", expr: "sin(x/40) * 60", min: -300, max: 300, style: { strokeColor: "#4f46e5" } }
@@ -1555,7 +1597,7 @@ EXAMPLES:
1555
1597
  y: { type: 'number', description: 'Canvas origin y (default: canvas center)' },
1556
1598
  flipY: { type: 'boolean', description: 'Math y-up → screen y-down (default true)' },
1557
1599
  style: { type: 'object', description: 'Path style forwarded to create (strokeColor, strokeWidth, fillColor, …)' },
1558
- solveOde: { type: 'object', description: "Integrate an ODE instead of plotting a closed form, and return the SOLUTION rather than drawing it: { equations, initialState, tEnd?, dt?, method? ('rk4' default, or 'euler' — faster and visibly wrong on anything stiff) }. The trajectory comes back as data, to inspect, feed to a path, or drive keyframes with." },
1600
+ solveOde: { type: 'object', description: "Integrate an ODE instead of plotting a closed form, and return the SOLUTION rather than drawing it: { equations, initialState, tEnd?, dt?, method? ('rk4' default, or 'euler' — faster and visibly wrong on anything stiff) }. STATE VARIABLES ARE POSITIONAL: each expression is written in terms of t, y0, y1, y2 ... where yN is the Nth entry of initialState. There is no x or y, so a Lorenz system is ['10*(y1-y0)', 'y0*(28-y2)-y1', 'y0*y1-(8/3)*y2'] with initialState [1,1,1] — writing it as '10*(y-x)' fails with \"Undefined symbol y\". The trajectory comes back as data, to inspect, feed to a path, or drive keyframes with." },
1559
1601
  warp: {
1560
1602
  anyOf: [{ type: 'object' }, { type: 'array', items: { type: 'object' } }],
1561
1603
  description: 'Chained parametric warp(s): { dx, dy } expressions of x, y, t — one object or an array',
@@ -1687,6 +1729,75 @@ EXAMPLES:
1687
1729
  required: ['action'],
1688
1730
  },
1689
1731
  },
1732
+ {
1733
+ name: 'pinepaper_accessibility_check',
1734
+ annotations: {
1735
+ title: 'Accessibility Check',
1736
+ readOnlyHint: true,
1737
+ destructiveHint: false,
1738
+ idempotentHint: true,
1739
+ openWorldHint: false,
1740
+ },
1741
+ description: `Check the live scene for two accessibility failures that stop ads and broadcast spots: low text contrast and flashing.
1742
+
1743
+ - contrast: WCAG 1.4.3 AA — 4.5:1, or 3:1 for large text (24 px, or 18.66 px bold). Each text's fill against the solid fill of the topmost item under it, or the page background. Text over a photo or gradient is skipped and listed.
1744
+ - flash: WCAG 2.3.1 general flash — samples the timeline (duration, fps) and fails any region of the frame with more than 3 flashes in a second. It seeks deterministically, so playback is not disturbed.
1745
+
1746
+ Both are heuristic screens and say so: not a certified photosensitive-epilepsy (PSE) test, and red flashes are not checked. Needs a live studio for verdicts; in code-only mode it returns the measuring code.
1747
+
1748
+ EXAMPLE: { checks: ["contrast", "flash"], duration: 6, fps: 20 }`,
1749
+ inputSchema: {
1750
+ type: 'object',
1751
+ properties: {
1752
+ checks: { type: 'array', items: { type: 'string', enum: ['contrast', 'flash'] }, description: 'Which checks (default both).' },
1753
+ duration: { type: 'number', exclusiveMinimum: 0, maximum: 120, description: 'flash: seconds of the timeline to sample (default 5).' },
1754
+ fps: { type: 'integer', minimum: 10, maximum: 60, description: 'flash: samples per second (default 20).' },
1755
+ },
1756
+ },
1757
+ },
1758
+ {
1759
+ name: 'pinepaper_render_batch',
1760
+ annotations: {
1761
+ title: 'Render Batch',
1762
+ readOnlyHint: false,
1763
+ destructiveHint: false,
1764
+ idempotentHint: false,
1765
+ openWorldHint: false,
1766
+ },
1767
+ description: `Render one creative per row of data from the current scene: for each row, apply its changes (modify_item per item, and / or a template's params), then export with the shared export options. For variants, translations and dynamic creative (DCO).
1768
+
1769
+ Each row runs through the same tools you would call yourself, so every check applies per row: unread properties, text fit, export fidelity. A row that fails is reported by index and the batch continues.
1770
+
1771
+ ROWS DO NOT RESET: row 2 starts from the scene row 1 left. Give every row every field that varies (all headlines, all prices), not only what differs from the row before. Pair with text fit so longer copy still fits its box, and read each row's fidelity for text_overflow.
1772
+
1773
+ Needs a live studio (it renders); in code-only mode it is refused. Max 100 rows; estimateOnly checks the rows first.
1774
+
1775
+ EXAMPLE:
1776
+ { rows: [
1777
+ { id: "en", changes: { headline: { content: "Summer Sale" }, price: { content: "$19" } } },
1778
+ { id: "de", changes: { headline: { content: "Sommerschlussverkauf" }, price: { content: "19 €" } } } ],
1779
+ export: { format: "png", platform: "instagram" } }`,
1780
+ inputSchema: {
1781
+ type: 'object',
1782
+ properties: {
1783
+ rows: {
1784
+ type: 'array', minItems: 1, maxItems: 100,
1785
+ description: 'One entry per creative.',
1786
+ items: {
1787
+ type: 'object',
1788
+ properties: {
1789
+ id: { type: 'string', description: 'A name for the row, echoed in its result.' },
1790
+ changes: { type: 'object', additionalProperties: { type: 'object' }, description: 'itemId → properties (as modify_item takes them).' },
1791
+ template: { type: 'object', properties: { templateId: { type: 'string' }, params: { type: 'object' } }, required: ['templateId', 'params'], description: 'template_params apply for this row.' },
1792
+ },
1793
+ },
1794
+ },
1795
+ export: { type: 'object', description: 'pinepaper_agent_export options for every row (format, platform, quality, duration, ad, …).' },
1796
+ estimateOnly: { type: 'boolean', description: 'Validate and return the plan without rendering.' },
1797
+ },
1798
+ required: ['rows', 'export'],
1799
+ },
1800
+ },
1690
1801
  {
1691
1802
  name: 'pinepaper_camera_director',
1692
1803
  annotations: {
@@ -1933,7 +2044,7 @@ EXAMPLE: { times: [0, 0.5, 1, 1.5, 2], seed: 42 }`,
1933
2044
 
1934
2045
  ACTIONS:
1935
2046
  - upload_video: { url (required), position?, scale?, timeOffset? (timeline start, s), clipInPoint?, clipOutPoint? (trim, s) } → { id, duration, width, height, name }
1936
- - upload_audio: { url (required), volume? (0–1, default 1), loop? (default true), muted?, timeOffset? } → { id, duration, name }
2047
+ - upload_audio: { url (required), volume? (0–1, default 1), loop?, muted?, timeOffset? } → { id, registryId, duration, name }. Pass loop EXPLICITLY — loop:true for a music bed, loop:false for a voice-over or sting; when omitted, the studio's own default applies and differs between builds.
1937
2048
  - list: → all media [{ id, kind, duration, … }]
1938
2049
  - remove: { id } → removed boolean
1939
2050
  - set_playback_rate: { id, rate (0.25–4) } → applies to video or audio
@@ -1971,7 +2082,7 @@ EXAMPLE: { action: 'upload_video', url: 'https://…/clip.mp4', scale: 0.5, time
1971
2082
  strength: { type: 'number', description: 'apply_track_matte: 0–1.' },
1972
2083
  hideMatte: { type: 'boolean', description: 'apply_track_matte: hide the matte item after applying.' },
1973
2084
  live: { type: 'boolean', description: 'apply_track_matte: keep re-cutting as the matte moves/animates — the kinetic mask reveal. Omitted = bake once, destructively.' },
1974
- url: { type: 'string', description: 'Media URL — required for upload_video / upload_audio.' },
2085
+ url: { type: 'string', description: "Required for upload_video / upload_audio: an http(s) URL, a data: URL, or a PATH TO A LOCAL FILE. Remote URLs and local files are fetched/read by the server (the page may not fetch third-party hosts), so stock music and SFX links work; the HTTP status or content-type is named if one does not. Up to 48 MB." },
1975
2086
  id: { type: 'string', description: 'Media id — required for remove / set_playback_rate / set_clip.' },
1976
2087
  rate: { type: 'number', description: 'Playback rate 0.25–4 — set_playback_rate.' },
1977
2088
  inPoint: { type: 'number', description: 'Clip in-point (media-time s) — set_clip.' },
@@ -1982,7 +2093,7 @@ EXAMPLE: { action: 'upload_video', url: 'https://…/clip.mp4', scale: 0.5, time
1982
2093
  clipInPoint: { type: 'number', description: 'Trim in-point (s) — upload_video.' },
1983
2094
  clipOutPoint: { type: 'number', description: 'Trim out-point (s) — upload_video.' },
1984
2095
  volume: { type: 'number', description: 'Volume 0–1 (default 1) — upload_audio.' },
1985
- loop: { type: 'boolean', description: 'Loop (default true) — upload_audio.' },
2096
+ loop: { type: 'boolean', description: 'upload_audio: true = loop (music beds), false = play once (voice-over, stings). Pass it explicitly; the default when omitted is the studio\'s and differs between builds.' },
1986
2097
  muted: { type: 'boolean', description: 'Start muted — upload_audio.' },
1987
2098
  },
1988
2099
  required: ['action'],
@@ -2004,11 +2115,24 @@ ACTIONS:
2004
2115
  - set_font_axes: { itemId, axes: { weight?, width?, slant? } } — STANDARD variable-font axes only (Canvas 2D has no font-variation-settings, so custom foundry axes are unreachable — platform limit, stated, not silent). Returns {applied, rejected}: CHECK rejected — an axis silently ignored is the failure this surface exists to prevent. All three are ordinary animatable properties: keyframe fontWeight and type breathes between weights.
2005
2116
  - list_styles: {} — styles + palettes + axes, for pickers. Call this first; styleKey strings come from here.
2006
2117
 
2118
+ OVER PHOTOGRAPHS, PASS A PALETTE — NEVER THE DEFAULT. Measured on a real
2119
+ render of captions over photographs: highlighter, sticker, caption and badge
2120
+ read correctly at the default palette. neon needs one on a dark photo. chrome,
2121
+ cutout and glitch are off-brief at the default palette over any photograph —
2122
+ they are designed against flat backgrounds and the default palette assumes one.
2123
+ A style that is hard to read still reports success, so this is a judgement the
2124
+ tool cannot make for you.
2125
+
2126
+ MEASURE THE WIDTH BEFORE COMMITTING TO A SIZE. glitch at 80px in a monospace
2127
+ face overflows a 1920 canvas at 36 characters. Layered styles draw offset
2128
+ copies, so the group is WIDER than the text — use 'wrap' first, or shorten the
2129
+ line, rather than discovering it in an export.
2130
+
2007
2131
  EXAMPLE: { action: 'apply_style', itemId: 'title_1', styleKey: 'arcade', fontFamily: 'suggested' }`,
2008
2132
  inputSchema: {
2009
2133
  type: 'object',
2010
2134
  properties: {
2011
- action: { type: 'string', enum: ['apply_style', 'set_font_axes', 'list_styles', 'cursive', 'wrap', 'unwrap', 'to_collage'], description: "Text style operation. 'cursive' draws text as STROKED handwriting — a path, not a glyph, so draw-on animation and outline_stroke both apply. 'wrap'/'unwrap' break a text item to a width, reversibly. 'to_collage' converts an EXISTING text item into a letter collage in place." },
2135
+ action: { type: 'string', enum: ['apply_style', 'set_font_axes', 'list_styles', 'list', 'cursive', 'wrap', 'unwrap', 'to_collage'], description: "Text style operation. 'list' is accepted as an alias for 'list_styles', because pinepaper_text_effect spells the same action 'list' and an agent moving between them guesses wrong. 'cursive' draws text as STROKED handwriting — a path, not a glyph, so draw-on animation and outline_stroke both apply. 'wrap'/'unwrap' break a text item to a width, reversibly. 'to_collage' converts an EXISTING text item into a letter collage in place." },
2012
2136
  text: { type: 'string', description: 'cursive: the words to write. to_collage: the text to build from, if it differs from the item.' },
2013
2137
  maxWidth: { type: 'number', description: 'wrap: the width to break at, in canvas units. unwrap restores the original, so this is not destructive.' },
2014
2138
  cursiveOptions: { type: 'object', description: 'cursive: { x?, y?, scale?, strokeColor?, strokeWidth? }.' },
@@ -2278,9 +2402,9 @@ RECIPE — assemble-from-pieces: shatter with keepSource, keyframe each tile fro
2278
2402
 
2279
2403
  Input is the output of a single-image layer decomposer (See-through — one flat anime drawing in, ~20 occlusion-inpainted layers out — or any PSD-style layer dump with the same manifest shape). The decomposition model does NOT run here: it needs a GPU and minutes per image; run it where it fits and import the folder.
2280
2404
 
2281
- Layer tags map onto the exact role tokens the expresses presets read (eye_left, pupil_right, mouth…), so pinepaper_animate/expresses blink-smile-talk work on the imported character immediately.
2405
+ Layer tags map onto the exact role tokens the expresses presets read (eye_left, pupil_right, mouth…), so blink / smile / talk work on the imported character immediately — through an 'expresses' RELATION (pinepaper_add_relation relationType expresses), which is the only thing that drives them. pinepaper_animate applies ambient motion (bounce, pulse, wobble) and cannot blink: asking it to is a silent no-op.
2282
2406
 
2283
- Params: { info (the decomposer manifest: tags, xyxy rects, depth_median draw order, frame_size [h,w]), images ({tag: PNG data URL or CORS-reachable https URL} — data URLs are the reliable path; ~20 layers of base64 is a big call, use URLs when you can), position?, scale?, name? }
2407
+ Params: { info — the decomposer manifest, whose parts live under info.parts (or info.tag2pinfo) as an OBJECT KEYED BY TAG, not a layers[] array: { eye_left: { xyxy: [x0,y0,x1,y1], depth_median: <draw order> }, … } plus frame_size [h,w]. An array here imports nothing and reports no parts. images ({tag: PNG data URL or CORS-reachable https URL} — data URLs are the reliable path; ~20 layers of base64 is a big call, use URLs when you can), position?, scale?, name? }
2284
2408
  Returns: { groupId, parts, roles, rolesWired, warnings }
2285
2409
 
2286
2410
  CHECK rolesWired: a character importing with 0 roles renders perfectly and silently refuses to animate — that number is the difference between a picture and a puppet.`,
@@ -2464,7 +2588,7 @@ Three of these refuse quietly in the engine: exportGLB with no perspective objec
2464
2588
  type: 'object',
2465
2589
  properties: {
2466
2590
  action: { type: 'string', enum: ['export_lottie', 'export_dotlottie', 'import_lottie', 'export_glb', 'export_bvh', 'export_png_sequence'], description: 'Which interchange operation.' },
2467
- options: { type: 'object', description: 'Format options passed through to the exporter: frame range, dimensions, fps, naming.' },
2591
+ options: { type: 'object', description: "Format options passed through to the exporter. export_png_sequence: { duration (seconds), fps, width, height, transparent (keep alpha; default false fills the background) } — PASS duration and fps: the studio does not size the sequence to the scene, and without them it writes its own default length, which does not follow the scene (runs have measured 45 and 90 frames). The result's defaultsUsed says when that happened." },
2468
2592
  data: { anyOf: [{ type: 'string' }, { type: 'object' }], description: 'import_lottie: the Lottie JSON — object, JSON string, or URL.' },
2469
2593
  skeletonId: { type: 'string', description: "export_bvh: which skeleton to write. A BVH is one skeleton's motion." },
2470
2594
  },
@@ -2496,7 +2620,8 @@ render_soundtrack mixes every placed sound to a WAV and writes it to a file. It
2496
2620
  inputSchema: {
2497
2621
  type: 'object',
2498
2622
  properties: {
2499
- action: { type: 'string', enum: ['list_instruments', 'list_percussion', 'list_sfx', 'play_tone', 'play_chord', 'chord_frequencies', 'play_percussion', 'play_sfx', 'play_spec', 'from_text', 'play_from_text', 'create', 'timbre_from_path', 'set_placement', 'remove', 'stop_all', 'define_instrument', 'define_percussion', 'define_sfx', 'render_soundtrack'], description: 'Which sound operation.' },
2623
+ action: { type: 'string', enum: ['list_instruments', 'list_percussion', 'list_sfx', 'play_tone', 'play_chord', 'chord_frequencies', 'play_percussion', 'play_sfx', 'play_spec', 'from_text', 'play_from_text', 'create', 'sequence', 'timbre_from_path', 'set_placement', 'remove', 'stop_all', 'define_instrument', 'define_percussion', 'define_sfx', 'render_soundtrack'], description: 'Which sound operation.' },
2624
+ cues: { type: 'array', description: "sequence: every cue in ONE call — [{t, spec|preset, note?, duration?, visual?}]. Each is created and placed at its own t, so a music bed is one call rather than one per cue. Failures come back by index in failed[], because a bed where three of a hundred cues did not sound is neither a success nor a failure.", items: { type: 'object' } },
2500
2625
  partials: { type: 'array', items: { type: 'object' }, description: 'define_*: [{ h, amp }] — the harmonic table that IS the timbre. Required for define_instrument.' },
2501
2626
  envelope: { type: 'object', description: 'define_*: { attack, decay, sustain, release } — seconds, except sustain which is a level 0-1.' },
2502
2627
  gain: { type: 'number', description: 'define_*: output level 0-1 (0.8 instrument, 0.85 percussion, 0.65 sfx).' },
@@ -2628,7 +2753,7 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2628
2753
  camera: { type: 'object', description: "set_camera: { mode: follow|fixed|orbit, target?, radius?, speed?, eye?, lookAt? }." },
2629
2754
  object: { type: 'object', description: 'add_object: { x, z, height?, color?, y? }. metalness/roughness are accepted and IGNORED here — a plain object is drawn by the prop shader, which declares no such uniform. They render on the mesh path only.' },
2630
2755
  pathId: { type: 'string', description: 'extrude_path / lathe_path: the canvas path to turn into geometry. The path itself is unchanged.' },
2631
- mesh: { type: 'object', description: 'extrude_path / lathe_path options: { id?, depth?, unitsPerPixel?, caps?, segments?, arc?, flatness?, x?, y?, z?, rotY?, rotYDegrees?, scale?, doubleSided? }. arc is DEGREES; rotY is RADIANS (fed straight to cos/sin in the shader) — pass rotYDegrees to write degrees. These names are the ones the engine records as the mesh provenance, so a captured meshProvenance.opts spreads straight back in.' },
2756
+ mesh: { type: 'object', description: 'extrude_path / lathe_path options: { id?, depth?, unitsPerPixel?, caps?, segments?, arc?, flatness?, x?, y?, z?, rotY?, rotYDegrees?, scale?, doubleSided?, color?, metalness?, roughness? }. color is hex or [r,g,b] (default light grey — without it the mesh rendered black); it becomes the shader\'s diffuse uniform. arc is DEGREES; rotY is RADIANS (fed straight to cos/sin in the shader) — pass rotYDegrees to write degrees. These names are the ones the engine records as the mesh provenance, so a captured meshProvenance.opts spreads straight back in.' },
2632
2757
  meshId: { type: 'string', description: 'remove_mesh: the mesh id.' },
2633
2758
  light: { type: 'object', description: 'add_light / set_light: { id?, x?, y?, z?, color?, intensity?, range? }. At most 8 point lights; the ninth is refused by name because the shader array is fixed-size.' },
2634
2759
  lightId: { type: 'string', description: 'set_light / remove_light: the light id.' },
@@ -2929,6 +3054,11 @@ PARAMS: { eps? (bounds-match tolerance px, default 4), cap? (max suggestions, de
2929
3054
  },
2930
3055
  description: `Create a behavior relationship between two items. Relations are the PRIMARY way to add animation in PinePaper - they describe HOW items should behave relative to each other.
2931
3056
 
3057
+ MOVES_ALONG_PATH — ride a path (a hand tracing a stroke, a car on a road):
3058
+ - sourceId: the item that moves. The path comes from ONE of: targetId (or params.pathId) naming a PATH ITEM, sampled into points for you; params.path as points [[x, y], …]; or params.equation.
3059
+ - params.duration: seconds for one traversal (or pass speed directly, 1 ≈ 150 px/s). params.delay: seconds on the timeline before it starts; until then the item waits at the path start. params.offset: [dx, dy] added to the point on the path — set it to (centre - pen tip) so a hand image's TIP, not its centre, rides the stroke. closed (loop back to the start; default from the path item), phase (0–1 start point), easing (linear | easeIn | easeOut | easeInOut | sine | bounce | pingpong), signal (deterministic, frame-rate-invariant — prefer it for export).
3060
+ - With no path from any of those it is refused, not accepted as a still item.
3061
+
2932
3062
  USE WHEN:
2933
3063
  - "moon orbits earth" → relationType: orbits
2934
3064
  - "label follows player" → relationType: follows
@@ -2994,7 +3124,7 @@ RELATION COMPATIBILITY:
2994
3124
  Text→Text: font blend
2995
3125
  Cross-type: particle denoising transition
2996
3126
  - group_morphs_to: pair-by-index 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.
2997
- - moves_along_path: self-relation (targetId=null); item is driven along a path. Source the path EITHER from params.path (array of {x,y} or [x,y]) OR from params.equation (S10 B5) — a math curve the item TRAVERSES, sampled deterministically on the Expression IR: params.equation = { kind: function|parametric|fourier|preset, expr | xExpr/yExpr | harmonics | preset, min, max, samples, scale, cx, cy, flipY, warp } (same equation contract as pinepaper_equation_path, but origin is cx/cy, default 0). Params: path OR equation, speed, closed, phase, easing (linear|easeIn|easeOut|easeInOut|sine|bounce|pingpong).
3127
+ - moves_along_path: self-relation (targetId=null); item is driven along a path. Source the path EITHER from params.path (array of {x,y} or [x,y]) OR from params.equation (S10 B5) — a math curve the item TRAVERSES, sampled deterministically on the Expression IR: params.equation = { kind: function|parametric|fourier|preset, expr | xExpr/yExpr | harmonics | preset, min, max, samples, scale, cx, cy, flipY, warp } (same equation contract as pinepaper_equation_path, but origin is cx/cy, default 0). READ ONLY ON THE SIGNAL PATH: easing 'bounce' is piecewise and leaves it, which silently discards the equation and leaves the item where it started. Params: path OR equation, speed, closed, phase, easing (linear|easeIn|easeOut|easeInOut|sine|bounce|pingpong).
2998
3128
 
2999
3129
  GEOMETRIC 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:
3000
3130
  - is_midpoint_of: source = midpoint(target, params.other). params.other = id of the second endpoint (B).
@@ -3372,9 +3502,13 @@ ANIMATABLE PROPERTIES:
3372
3502
  - opacity: 0-1
3373
3503
  - fillColor: Color string
3374
3504
  - strokeColor: Color string
3505
+ - strokeWidth, width, height, radius, shadowBlur, shadowColor: Numbers / colours
3375
3506
  - fontSize: Number
3507
+ - 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.
3508
+ The authoritative list for a given item is pinepaper_get_animatable_properties — ask it rather than guessing.
3376
3509
 
3377
- EASING OPTIONS:
3510
+ EASING — IT SHAPES THE SEGMENT ARRIVING AT THE KEYFRAME IT IS ON.
3511
+ The move from keyframe A to keyframe B is eased by B's easing, not A's. Easing on the FIRST keyframe does nothing (no segment arrives at it) — a Ken Burns with easing only on its first key plays linear. Put the easing on the key you are moving TO.
3378
3512
  - linear: Constant speed
3379
3513
  - easeIn: Slow start
3380
3514
  - easeOut: Slow end
@@ -3410,6 +3544,7 @@ EXAMPLE — Reveal at 2s, skip first second of keyframe data, end at 4s of keyfr
3410
3544
  easing: {
3411
3545
  type: 'string',
3412
3546
  enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce', 'elastic'],
3547
+ description: 'Shapes the segment ARRIVING at this keyframe (from the previous one). Ignored on the first keyframe.',
3413
3548
  },
3414
3549
  },
3415
3550
  required: ['time', 'properties'],
@@ -3617,7 +3752,9 @@ USE WHEN:
3617
3752
  - Creating complex reveal sequences
3618
3753
  - Building custom transitions
3619
3754
 
3620
- Requires maskType and keyframes array.`,
3755
+ Requires maskType and keyframes array.
3756
+
3757
+ COORDINATES: a 'rectangle' mask's x / y are its TOP-LEFT corner (measured: {x:540, y:620, width:800, height:800} reads back as bounds [540, 620, 800, 800]) — unlike items, which are placed by their centre. Convert before masking an item you placed by centre.`,
3621
3758
  inputSchema: {
3622
3759
  type: 'object',
3623
3760
  properties: {
@@ -3754,7 +3891,6 @@ USE WHEN:
3754
3891
  - Building presentation zoom sequences
3755
3892
  - Making dramatic reveals with camera motion
3756
3893
  - Tour animations showing different parts of canvas
3757
- - 3D perspective effects (pitch/yaw tilt via equirectangular projection)
3758
3894
 
3759
3895
  The camera follows a keyframe sequence. For an orbit-style camera (revolving
3760
3896
  around a subject), add 4-8 keyframes around the pivot using pathMode: "arc"
@@ -3769,8 +3905,7 @@ KEYFRAME PROPERTIES:
3769
3905
  'item-id' itemRegistry lookup → item bounds center (tracks the item as it moves)
3770
3906
  { item: 'id', offset: [dx, dy] } item center + offset
3771
3907
  - center: [x, y] — legacy alias, honored only when focus is absent
3772
- - pitch: 3D tilt in degrees (0=flat, positive=tilt forward). Uses equirectangular projection
3773
- - yaw: 3D rotation in degrees. Uses equirectangular projection
3908
+ - pitch / yaw: 3D tilt in degrees. Rendered by studio builds that support camera tilt; on others they are accepted with NO visible effect — the result's tiltRenders says which this studio is, and a note appears when a tilt would be invisible.
3774
3909
  - easing: Timing function (linear, easeIn, easeOut, easeInOut, bounce, elastic)
3775
3910
 
3776
3911
  CURVED INTER-KEYFRAME PATHS (optional; default is linear):
@@ -3822,8 +3957,8 @@ EXAMPLE (curved arc between waypoints):
3822
3957
  items: { type: 'number' },
3823
3958
  description: 'Legacy view center [x, y] — honored only when focus is absent',
3824
3959
  },
3825
- pitch: { type: 'number', description: '3D tilt in degrees (0=flat, positive=forward tilt)' },
3826
- yaw: { type: 'number', description: '3D rotation in degrees' },
3960
+ pitch: { type: 'number', description: '3D tilt in degrees — rendered where the studio supports camera tilt (result.tiltRenders).' },
3961
+ yaw: { type: 'number', description: '3D rotation in degrees — rendered where the studio supports camera tilt (result.tiltRenders).' },
3827
3962
  easing: {
3828
3963
  type: 'string',
3829
3964
  enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce', 'elastic'],
@@ -3860,12 +3995,12 @@ EXAMPLE (curved arc between waypoints):
3860
3995
  },
3861
3996
  },
3862
3997
  },
3863
- duration: { type: 'number', description: 'Total animation duration in seconds' },
3998
+ duration: { type: 'number', description: 'Total animation duration in seconds. Optional: omitted, the camera runs to its last keyframe.' },
3864
3999
  loop: { type: 'boolean', description: 'Loop the animation' },
3865
4000
  delay: { type: 'number', description: 'Delay before animation starts' },
3866
4001
  fov: { type: 'number', description: 'Field of view in degrees for 3D perspective (default: 60)' },
3867
4002
  },
3868
- required: ['keyframes', 'duration'],
4003
+ required: ['keyframes'],
3869
4004
  },
3870
4005
  },
3871
4006
  {
@@ -4004,8 +4139,8 @@ SUPPORTED ANIMATIONS: bounce, breathe, fade, glow, jelly, path, pulse, rotate, s
4004
4139
  },
4005
4140
  itemType: {
4006
4141
  type: 'string',
4007
- enum: ['text', 'circle', 'star', 'rectangle', 'triangle', 'polygon', 'ellipse', 'path', 'line', 'arc'],
4008
- description: 'Type of item to create',
4142
+ enum: [...ItemTypeSchema.options],
4143
+ description: 'Type of item to create. Every type the engine has — this used to advertise ten while the tool hand-built four and turned the rest into a small circle.',
4009
4144
  },
4010
4145
  position: {
4011
4146
  type: 'object',
@@ -4293,7 +4428,7 @@ EXAMPLE:
4293
4428
  },
4294
4429
  description: `Shape a delay across many items. What makes a stagger read as designed rather than as a queue is where it STARTS and how it SPREADS — a grid lighting up outward from the centre, a row converging from both edges.
4295
4430
 
4296
- Delays are written to the channel the engine and the SMIL exporter already read, so a staggered scene scrubs, exports and restores. Nothing here is playback-only.
4431
+ Delays go where each kind of animation reads them: loop presets take animationDelay, and an item that already has a KEYFRAME track has that track's start offset (timeOffset) shifted by its delay — so apply the stagger after the keyframes, and re-applying replaces the shift rather than adding to it. The result's shiftedKeyframeTracks says how many tracks moved.
4297
4432
 
4298
4433
  ACTIONS:
4299
4434
  - apply: write the delays onto real items, in the order you list them (row-major for a grid).
@@ -4760,7 +4895,7 @@ Both actions return a WCAG contrast audit, so you can see failing pairs before s
4760
4895
  ACTIONS:
4761
4896
  - define: Turn items into a component master. Params: itemIds[], name
4762
4897
  - list: All defined components
4763
- - instantiate: Place an instance. Params: componentId, position, overrides
4898
+ - instantiate: Place an instance. Params: componentId, position, overrides → { instanceId, parts: [{key, type, content?}], orphaned }. overrides is { <componentKey>: { <prop>: value } }, e.g. { "ck_1": { "content": "50% OFF" }, "ck_2": { "fillColor": "#e11d48" } } — keys come from list (each component's parts) or from a previous instantiate.
4764
4899
  - set_override: Change ONE part of ONE instance without detaching it. Params: instanceId, componentKey, prop, value
4765
4900
  - sync: Push the master's current state to every instance (overrides are preserved). Params: componentId
4766
4901
  - update_from_instance: Promote an instance's edits back into the master. Params: instanceId
@@ -4788,7 +4923,7 @@ Overrides are the point: an instance can differ from its master and still receiv
4788
4923
  anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }, { type: 'object' }, { type: 'array' }],
4789
4924
  description: 'Override value',
4790
4925
  },
4791
- overrides: { type: 'object', description: 'Overrides applied at instantiate time' },
4926
+ overrides: { type: 'object', description: 'instantiate: { <componentKey>: { <prop>: value } } — keys are listed in parts by list and instantiate. A key the component does not have is reported in orphaned, not applied silently.' },
4792
4927
  },
4793
4928
  required: ['action'],
4794
4929
  },
@@ -4933,7 +5068,7 @@ node survives none of them.`,
4933
5068
  type: 'object',
4934
5069
  properties: {
4935
5070
  action: { type: 'string', enum: ['analyze', 'animate_to_beat'] },
4936
- source: { type: 'string', description: 'Asset id, data URL, or URL' },
5071
+ source: { type: 'string', description: 'Asset id, data URL, URL — or an uploaded clip\'s id / registryId from pinepaper_media upload_audio / upload_video.' },
4937
5072
  itemId: { type: 'string' },
4938
5073
  beats: { type: 'array', items: { type: 'number' }, description: 'Beat times (s) you already have' },
4939
5074
  grid: { type: 'boolean', description: 'Even pulse from the tempo instead of raw onsets' },
@@ -5023,26 +5158,28 @@ ACTIONS:
5023
5158
 
5024
5159
  Available filters (GPU raster set):
5025
5160
  - Color: grayscale, sepia, brightness, contrast, saturation, invert, posterize (levels), hsl (hue/saturation/lightness), colorTint (color, intensity, blendMode), colorMatrix (matrix), duotone (shadow, highlight, mix), paletteMap (swatches[], amount, preserveShading — recolor to a fixed palette)
5026
- - Stylize: vignette (intensity, radius), edgeDetect, halftoneDots (size, angle), halftoneCMYK (size), dither (levels), grain (amount, seed), scanlines (intensity, period), chromaticAberration (amount), blur
5161
+ - Stylize: vignette (intensity, radius), edgeDetect, halftoneDots (size, angle), halftoneCMYK (size), dither (levels), grain (amount, seed), scanlines (intensity, period), chromaticAberration (amount), blur (radius)
5027
5162
  - Light (additive): bloom (amount, threshold, radius), glow, halation (warm-fringed bloom; color), lightShafts (amount, threshold, decay, density, x, y, color — volumetric rays from a point)
5028
5163
  - Keying: lumaKey (threshold, softness, invert)
5029
5164
  - SECOND-INPUT (params.map = another item's id, resolved to its pixels): displace (amount, dispersion, map, mapChannel — the map's brightness pushes pixels), refract (displace with per-channel dispersion — glass), trackMatte (channel, invert, strength — prefer pinepaper_media apply_track_matte for the live version), datamosh (amount, block, map? — self-moshes without a map)
5030
5165
 
5031
5166
  RECIPE — grunge poster: chain [grain, scanlines, vignette]. Glass header: refract with map = a gradient item.
5167
+ HARD-EDGED LOOKS NEED SMOOTHING OFF: posterize, dither, halftone and pixel looks are exact at 1:1, but a raster that is then scaled, skewed or rotated is RESAMPLED with smoothing, which blends the flat levels back into a gradient (measured: posterize then skew → 256 tones again). Set smoothing 'off' on the image (pinepaper_import_image, or pinepaper_modify_item {smoothing: 'off'}) to keep the levels.
5168
+ CHAIN ORDER MATTERS: each filter runs on the previous one's output. duotone then halftoneDots comes out grey (the ink colour is lost); halftoneDots then duotone keeps it. If a chain loses its colour, move the colour-mapping filter (duotone, paletteMap, colorTint) to the end.
5032
5169
  (Scene-wide filters — sharpen/emboss/noise/vintage etc. — are a different surface: use pinepaper_add_filter.)`,
5033
5170
  inputSchema: {
5034
5171
  type: 'object',
5035
5172
  properties: {
5036
5173
  action: {
5037
5174
  type: 'string',
5038
- enum: ['apply', 'chain'],
5175
+ enum: ['apply', 'chain', 'analyze_palette', 'recolor_palette'],
5039
5176
  description: 'Filter action',
5040
5177
  },
5041
5178
  itemId: { type: 'string', description: 'Target raster item ID' },
5042
5179
  filterName: { type: 'string', description: 'Filter name (for apply)' },
5043
5180
  params: {
5044
5181
  type: 'object',
5045
- description: 'Filter parameters (e.g. {amount: 5} for blur)',
5182
+ description: "Filter parameters, named per filter in the list above. Blur takes {radius: 5} — a misspelled parameter is ignored rather than refused, so check the name. For blur only, 'amount' is accepted as an alias for radius because this example used to say so; every other filter that lists amount reads amount.",
5046
5183
  },
5047
5184
  filters: {
5048
5185
  type: 'array',
@@ -5054,8 +5191,18 @@ RECIPE — grunge poster: chain [grain, scanlines, vignette]. Glass header: refr
5054
5191
  },
5055
5192
  required: ['name'],
5056
5193
  },
5057
- description: 'Filter chain (for chain action)',
5194
+ description: 'Filter chain (for chain action). Order matters — see CHAIN ORDER above.',
5195
+ },
5196
+ maxSwatches: { type: 'integer', minimum: 1, maximum: 32, description: 'analyze_palette: swatch count cap (default 8).' },
5197
+ mapping: {
5198
+ oneOf: [
5199
+ { type: 'array', items: { type: 'object', properties: { from: { type: 'string' }, to: { type: 'string' } }, required: ['from', 'to'] } },
5200
+ { type: 'object', additionalProperties: { type: 'string' } },
5201
+ ],
5202
+ description: "recolor_palette: {from,to}[] or {'#old':'#new'} — colours swap, shading survives.",
5058
5203
  },
5204
+ amount: { type: 'number', minimum: 0, maximum: 1, description: 'recolor_palette: blend 0–1. (A filter\'s own amount goes in params.)' },
5205
+ preserveShading: { type: 'number', minimum: 0, maximum: 1, description: 'recolor_palette: how much of the original luminance detail survives (default 1).' },
5059
5206
  },
5060
5207
  required: ['action', 'itemId'],
5061
5208
  },
@@ -5069,20 +5216,28 @@ RECIPE — grunge poster: chain [grain, scanlines, vignette]. Glass header: refr
5069
5216
  idempotentHint: false,
5070
5217
  openWorldHint: false,
5071
5218
  },
5072
- description: `Freeform lasso selection for image items.
5219
+ description: `Cut a region out of an image along a polygon — the subject cutout behind 2.5D parallax, stickers and collage.
5073
5220
 
5074
5221
  ACTIONS:
5075
- - activate: Start lasso mode on an image item. Params: itemId
5076
- - apply: Confirm and apply the lasso selection`,
5222
+ - cut: { itemId, points: [[x, y], …] (canvas coordinates, at least 3 vertices, in order around the subject), keepOriginal? (default true) } → { cutoutId, originalId, originalKept }. The cutout is a NEW image item, placed exactly over the region it came from, transparent outside the polygon. keepOriginal:false consumes the source image (the studio's own lasso deletes it); the default keeps it, so you can layer the cutout over the photo.
5223
+ - activate / apply: the interactive, mouse-driven lasso. Refused over MCP — use cut.
5224
+
5225
+ Vertices closer than 12 px to the FIRST vertex are skipped (the lasso reads a click there as "close the shape"), so trace with points at least that far from where you started.`,
5077
5226
  inputSchema: {
5078
5227
  type: 'object',
5079
5228
  properties: {
5080
5229
  action: {
5081
5230
  type: 'string',
5082
- enum: ['activate', 'apply'],
5083
- description: 'Lasso action',
5231
+ enum: ['cut', 'activate', 'apply'],
5232
+ description: 'Lasso action — cut is the one that works over MCP',
5233
+ },
5234
+ itemId: { type: 'string', description: 'The image item to cut from' },
5235
+ points: {
5236
+ type: 'array',
5237
+ description: 'cut: polygon vertices in canvas coordinates, [x, y] or {x, y}, at least 3',
5238
+ items: { anyOf: [{ type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 }, { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, required: ['x', 'y'] }] },
5084
5239
  },
5085
- itemId: { type: 'string', description: 'Target image item ID (for activate)' },
5240
+ keepOriginal: { type: 'boolean', description: 'cut: keep the source image (default true). false = the source is consumed.' },
5086
5241
  },
5087
5242
  required: ['action'],
5088
5243
  },
@@ -5514,6 +5669,9 @@ Params: title (page title), download (bool)`,
5514
5669
  type: 'object',
5515
5670
  properties: {
5516
5671
  title: { type: 'string', description: 'HTML page title (default: PinePaper Widget)' },
5672
+ lang: { type: 'string', description: 'BCP 47 language tag for the page (default en), e.g. "ar", "hi", "zh-Hans".' },
5673
+ dir: { type: 'string', enum: ['ltr', 'rtl', 'auto'], description: 'Text direction for the page — "rtl" for Arabic / Hebrew.' },
5674
+ alt: { type: 'string', description: 'Text alternative for the animation: the canvas gets role="img" and this as its aria-label, and the page a meta description. Screen readers otherwise announce nothing.' },
5517
5675
  download: { type: 'boolean', description: 'Trigger browser download (default: false)' },
5518
5676
  },
5519
5677
  },
@@ -6419,6 +6577,7 @@ ACTIONS:
6419
6577
 
6420
6578
  ACTIONS:
6421
6579
  - enable — { rotation?: [lambda,phi,gamma], momentum?, showOcean? } switch to globe mode
6580
+ - disable — {} leave globe mode: stops spin, drag and rotation (reload the map for a flat projection)
6422
6581
  - rotate_to — { lon, lat, duration? } versor-slerp the globe to face a coordinate
6423
6582
  - spin — { speed?, axis?: "longitude"|"latitude", duration? } continuous auto-rotation
6424
6583
  - world_tour — { regions?: string[] | coords?: [lon,lat][], dwell?, travel?, tilt?, loop?, easing?, highlightColors?: string[], showLabels?, labelColor? } the headline: the globe swings to each stop, highlighting it (requires regions OR coords)
@@ -6428,7 +6587,7 @@ ACTIONS:
6428
6587
  inputSchema: {
6429
6588
  type: 'object',
6430
6589
  properties: {
6431
- action: { type: 'string', enum: ['enable', 'rotate_to', 'spin', 'world_tour', 'stop_tour', 'pin_item', 'tour_item'], description: 'Globe action' },
6590
+ action: { type: 'string', enum: ['enable', 'disable', 'rotate_to', 'spin', 'world_tour', 'stop_tour', 'pin_item', 'tour_item'], description: 'Globe action' },
6432
6591
  rotation: { type: 'array', items: { type: 'number' }, description: 'Initial rotation [lambda, phi, gamma] (enable)' },
6433
6592
  momentum: { type: 'boolean', description: 'Drag-spin inertia (enable)' },
6434
6593
  showOcean: { type: 'boolean', description: 'Render ocean sphere (enable)' },
@@ -6472,7 +6631,8 @@ ACTIONS:
6472
6631
  Most of this tool is glyph authoring — drawing a typeface. If you just want to know what font families are available to set on a text item, that is ONE action:
6473
6632
 
6474
6633
  - list_available — { category?, loadedOnly? } every family the studio can render, with whether its file is loaded yet
6475
- - check — { name, text? } is this family really there, and can it draw THIS string? Pass the text: a font with four glyphs is installed and will still render most characters in the fallback face, and that second answer is the one that decides the pixels. Do NOT rely on document.fonts.check() — it returns true for a family that does not exist.
6634
+ - load — { name } load a family NOW: the studio's catalogue first, then Google Fonts (e.g. Monoton). Returns available + via. Text tools also load on first use, so this is for checking up front.
6635
+ - check — { name, text? } is this family really there, and can it draw THIS string? "available: false" means NOT LOADED YET — the result says whether it is loadable; use load. Pass the text: a font with four glyphs is installed and will still render most characters in the fallback face, and that second answer is the one that decides the pixels. Do NOT rely on document.fonts.check() — it returns true for a family that does not exist.
6476
6636
  - fallbacks — {} which text items on the canvas are silently drawing in something other than the family they asked for
6477
6637
 
6478
6638
  ACTIONS (authoring):
@@ -6498,7 +6658,7 @@ ACTIONS (authoring):
6498
6658
  action: {
6499
6659
  type: 'string',
6500
6660
  enum: [
6501
- 'list_available', 'check', 'fallbacks',
6661
+ 'list_available', 'check', 'load', 'fallbacks',
6502
6662
  'show_studio', 'set_name', 'get_required_chars', 'get_status',
6503
6663
  'create_glyph', 'create_space', 'remove_glyph', 'set_metrics',
6504
6664
  'export', 'load_into_document', 'export_data', 'import_data',
@@ -6509,7 +6669,8 @@ ACTIONS (authoring):
6509
6669
  text: { type: 'string', description: 'check: the string you intend to draw. Without it you learn only whether the family is installed, not whether it has glyphs for your characters.' },
6510
6670
  category: { type: 'string', description: 'list_available: narrow to one category (e.g. display, serif, mono).' },
6511
6671
  loadedOnly: { type: 'boolean', description: 'list_available: only families whose font file has already loaded. Unloaded ones still render once the studio fetches them.' },
6512
- name: { type: 'string', description: 'Font name (set_name)' },
6672
+ name: { type: 'string', description: 'Font family (check, load) or the font being built (set_name). family / fontFamily are accepted for it.' },
6673
+ family: { type: 'string', description: 'Alias of name, for check / load.' },
6513
6674
  set: { type: 'string', description: 'Character set name (get_required_chars; default "minimum")' },
6514
6675
  character: { type: 'string', description: 'Single-character glyph identifier (create_glyph, remove_glyph)' },
6515
6676
  pathId: { type: 'string', description: 'Item id of the path source (create_glyph, remove_overlap, correct_direction, cleanup_path)' },
@@ -6772,7 +6933,7 @@ New — Geometric:
6772
6933
  - drawGeometricAbstract: Mixed translucent shapes (colors, shapeCount, blendMode, rotation)
6773
6934
 
6774
6935
  New — Particles:
6775
- - drawWindField: Directional wind particles with noise turbulence (particleCount, colors, direction, turbulence, trailLength, speed)
6936
+ - drawWindField: Directional wind particles with noise turbulence (particleCount, colors, windDirection (degrees), windSpeed, turbulence, trailLength, trailWidth, opacity, bgColor; direction / speed are accepted for windDirection / windSpeed)
6776
6937
 
6777
6938
  New — Textures:
6778
6939
  - drawNoiseTexture: Perlin/grain/stipple noise (noiseType: perlin|grain|stipple, colors, scale, density, animated)
@@ -6795,7 +6956,7 @@ S6 Procedural (seeded, OKLCH palettes, curated presets, optional Motion). All ta
6795
6956
  GPU / GLSL math-art (transpiled to a fragment shader, 60fps; all take renderScale 0.25–1, transparentBg, speed):
6796
6957
  - drawFormulaArt: 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).
6797
6958
  - 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).
6798
- - drawShaderArt: paste your own GLSL fragment shader (fragmentSource; uniforms u_time, u_resolution, u_color1/2/3) or pick a preset.
6959
+ - 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 outColor' — 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.
6799
6960
  - drawYeganehMountains: GLSL port of Hamid Naderi Yeganeh's "Mountains" (mountainCount, timeOffset, skyTop, skyBottom, ridgeColor, valleyColor).
6800
6961
 
6801
6962
  OKLCH seeded backgrounds (palette + seed → reproducible):
@@ -6825,14 +6986,19 @@ COMMON PARAMS (all generators):
6825
6986
  },
6826
6987
  region: {
6827
6988
  type: 'object',
6828
- description: 'Optional sub-region {x, y, width, height} (canvas coords) — generator draws into a clipped, translated sub-group instead of full-canvas. Region runs imply preserve (won\'t wipe the canvas) and re-running the same generator replaces its prior region; different generators\' regions coexist. Invalid/zero-size regions fall back to full-canvas.',
6989
+ description: 'Optional sub-region {x, y, width, height} (canvas coords) — generator draws into a clipped, translated sub-group instead of full-canvas. Region runs imply preserve (won\'t wipe the canvas). An instance is the generator plus its box, or an explicit region.id: re-running with the same box (or id) replaces it, and the same generator in a different box adds another — so one generator several times, or several generators side by side, is regions. Older studios keep one region per generator (a second box replaces the first). Invalid/zero-size regions fall back to full-canvas. shape clips the region to a rounded rect, ellipse, circle, polygon or star, or to an existing path ({itemId}: x/y/width/height may then be omitted and default to its bounds). A studio without region shapes says so in the result (regionShapeApplied: false). On older studios, some generators also paint a page-wide background when confined to a region.',
6829
6990
  properties: {
6830
6991
  x: { type: 'number', description: 'Region top-left X in canvas coords' },
6831
6992
  y: { type: 'number', description: 'Region top-left Y in canvas coords' },
6832
6993
  width: { type: 'number', minimum: 1, description: 'Region width in pixels' },
6833
6994
  height: { type: 'number', minimum: 1, description: 'Region height in pixels' },
6995
+ shape: { anyOf: [{ type: 'string', enum: ['rect', 'roundedRect', 'ellipse', 'circle', 'polygon', 'star'] }, { type: 'object', properties: { itemId: { type: 'string' } }, required: ['itemId'] }], description: "Clip shape (default 'rect'), or {itemId} to clip to that path's geometry." },
6996
+ radius: { type: 'number', minimum: 0, description: 'roundedRect corner radius (default 20% of the shorter side).' },
6997
+ sides: { type: 'integer', minimum: 3, maximum: 64, description: 'polygon sides (default 6).' },
6998
+ points: { type: 'integer', minimum: 3, maximum: 64, description: 'star points (default 5).' },
6999
+ innerRatio: { type: 'number', minimum: 0.05, maximum: 0.95, description: 'star inner radius / outer (default 0.5).' },
7000
+ id: { type: 'string', minLength: 1, maxLength: 64, description: 'Names this region instance, so re-running under the same id replaces it even if its box moved.' },
6834
7001
  },
6835
- required: ['x', 'y', 'width', 'height'],
6836
7002
  },
6837
7003
  },
6838
7004
  required: ['generatorName'],
@@ -6879,6 +7045,15 @@ USE WHEN:
6879
7045
  - Celebration effects (confetti)
6880
7046
  - Enhancing visual impact (ripple, glow, electric)
6881
7047
 
7048
+ SIZE IS A MULTIPLIER, NOT PIXELS — and this is the one that bites hardest.
7049
+ Every effect derives its scale from the ITEM'S OWN BOUNDS, and 'size' multiplies
7050
+ that. glow computes a radius from max(width, height) and pulses it to
7051
+ '1 + (size - 1)', so size: 30 on a modest item asks for a disc thousands of
7052
+ pixels across, gets one, and reports success. Typical values are 1.5 to 3 —
7053
+ glow's own default is 1.5. Pass a pixel count here and you will be wrong by
7054
+ about two orders of magnitude. A recent studio warns about it
7055
+ (kind: 'glow-size-unit'); an older one does not.
7056
+
6882
7057
  EFFECTS (15 particle + 7 shader auras):
6883
7058
  - sparkle: Glitter/sparkle particles (color, speed, size)
6884
7059
  - blast: Explosion burst effect (color, radius, count)
@@ -6955,13 +7130,14 @@ AVAILABLE FILTERS:
6955
7130
  - blur: Gaussian blur effect (params: radius 0-20)
6956
7131
  - brightness: Adjust brightness (params: value -100 to 100)
6957
7132
  - contrast: Adjust contrast (params: value -100 to 100)
6958
- - saturate: Adjust color saturation (params: value -100 to 100)
7133
+ - saturation: Adjust color saturation (params: value -100 to 100). 'saturate' is accepted as the same filter.
6959
7134
  - invert: Invert colors (params: intensity 0-1)
6960
7135
  - noise: Add film grain (params: intensity 0-100, monochrome: true/false)
6961
7136
  - vignette: Darken edges (params: intensity 0-1, radius 0-1)
6962
7137
  - vintage: Preset combining sepia, vignette, noise
6963
7138
  - colorOverlay: Add color tint (params: color, intensity, blendMode)
6964
7139
  - sharpen: Increase sharpness (params: intensity 0-100)
7140
+ - emboss: 3D emboss (params: intensity 0-1)
6965
7141
  - posterize: Reduce color levels (params: levels 2-32)
6966
7142
 
6967
7143
  EXAMPLES:
@@ -6976,7 +7152,7 @@ Filters can be stacked - call multiple times to combine effects.`,
6976
7152
  properties: {
6977
7153
  filterType: {
6978
7154
  type: 'string',
6979
- enum: ['grayscale', 'sepia', 'blur', 'brightness', 'contrast', 'saturate', 'invert', 'noise', 'vignette', 'vintage', 'colorOverlay', 'sharpen', 'posterize'],
7155
+ enum: ['grayscale', 'sepia', 'blur', 'brightness', 'contrast', 'saturation', 'invert', 'noise', 'vignette', 'vintage', 'colorOverlay', 'sharpen', 'emboss', 'posterize', 'saturate'],
6980
7156
  description: 'Type of filter to apply',
6981
7157
  },
6982
7158
  params: {
@@ -7408,7 +7584,7 @@ WORKFLOW: start_job → batch_execute (everything in ONE call) → end_job (scre
7408
7584
 
7409
7585
  RETURNS: canvasSize {width, height} — use this to position items within bounds. Also returns ontology: a pp: triples + item summary snapshot of the (typically empty) canvas. Reuse this baseline across the same agent turn instead of calling pinepaper_get_canvas_ontology unless you've mutated the canvas. When clearCanvas is false, the ontology captures whatever's already on the canvas.
7410
7586
 
7411
- CANVAS PRESETS: instagram (1080x1080), instagram-story (1080x1920), tiktok (1080x1920), youtube (1920x1080), youtube-thumbnail (1280x720), twitter (1200x675), linkedin (1200x627), web, print-a4, print-letter`,
7587
+ CANVAS PRESETS: instagram (1080x1080), instagram-story (1080x1920), tiktok (1080x1920), youtube (1920x1080), youtube-thumbnail (1280x720), twitter (1200x675), linkedin (1200x627), web, print-a4, print-letter, print-a4-landscape, print-letter-landscape (print presets carry 300 dpi, so a PDF comes out at paper size)`,
7412
7588
  inputSchema: {
7413
7589
  type: 'object',
7414
7590
  properties: {
@@ -7422,12 +7598,12 @@ CANVAS PRESETS: instagram (1080x1080), instagram-story (1080x1920), tiktok (1080
7422
7598
  },
7423
7599
  screenshotPolicy: {
7424
7600
  type: 'string',
7425
- enum: ['on_complete', 'on_error', 'never', 'on_request'],
7426
- description: 'When to take screenshots (default: on_complete)',
7601
+ enum: ['on_complete', 'on_error', 'none', 'on_request', 'never'],
7602
+ description: "When to take screenshots (default: on_complete). 'never' is accepted as 'none'.",
7427
7603
  },
7428
7604
  canvasPreset: {
7429
7605
  type: 'string',
7430
- enum: ['instagram', 'instagram-story', 'tiktok', 'youtube', 'youtube-thumbnail', 'twitter', 'linkedin', 'web', 'print-a4', 'print-letter'],
7606
+ enum: ['instagram', 'instagram-story', 'tiktok', 'youtube', 'youtube-thumbnail', 'twitter', 'linkedin', 'web', 'print-a4', 'print-letter', 'print-a4-landscape', 'print-letter-landscape'],
7431
7607
  description: 'Set canvas size to platform preset',
7432
7608
  },
7433
7609
  clearCanvas: {
@@ -7534,7 +7710,7 @@ EXAMPLES:
7534
7710
  properties: {
7535
7711
  canvasPreset: {
7536
7712
  type: 'string',
7537
- enum: ['instagram', 'instagram-story', 'tiktok', 'youtube', 'youtube-thumbnail', 'twitter', 'linkedin', 'web', 'print-a4', 'print-letter'],
7713
+ enum: ['instagram', 'instagram-story', 'tiktok', 'youtube', 'youtube-thumbnail', 'twitter', 'linkedin', 'web', 'print-a4', 'print-letter', 'print-a4-landscape', 'print-letter-landscape'],
7538
7714
  description: 'Set canvas size to platform preset after reset',
7539
7715
  },
7540
7716
  backgroundColor: {
@@ -7563,7 +7739,7 @@ NOTE: For diagrams (flowcharts, UML), use pinepaper_create_diagram_shape + pinep
7563
7739
 
7564
7740
  ⚠️ EVERY operation executes LIVE on the canvas. Calling this twice DOUBLES all items. Call ONCE per pipeline.
7565
7741
 
7566
- ⚠️ SIZING: All positions/sizes MUST fit within the canvas. start_job returns canvasSize. Keep items within 5%-95% of canvas width/height. Max canvas: 4096x4096.
7742
+ ⚠️ SIZING: All positions/sizes MUST fit within the canvas. start_job returns canvasSize. Keep items within 5%-95% of canvas width/height. Max canvas: 8192x8192.
7567
7743
 
7568
7744
  ONTOLOGY PREFLIGHT: Before code generation, this tool validates itemType, relationType, effectType, and generatorName against the canonical PinePaper vocabulary. A typo (e.g. "circel") returns a structured VALIDATION_ERROR with the operation index, the offending field, the canonical valid list, and a suggested fix — fix and retry rather than guessing. Pass skipValidation: true to bypass when intentionally using experimental vocabulary.
7569
7745
 
@@ -7599,8 +7775,8 @@ ANIMATION:
7599
7775
  animate — Loop animation: {itemId, animationType, animationOptions}
7600
7776
  animationType: ${SIMPLE_ANIMATION_TYPES_LIST}
7601
7777
  keyframe_animate — Timed animation: {itemId, keyframes: [{time, properties, easing}], duration, loop}
7602
- Animatable properties: opacity, scale, scaleX, scaleY, x, y, rotation, fillColor, strokeColor, fontSize
7603
- Easing: easeInOut, easeIn, easeOut, easeInCubic, easeOutCubic, easeOutBounce, linear
7778
+ Animatable properties: opacity, scale, scaleX, scaleY, x, y, rotation, fillColor, strokeColor, strokeWidth, width, height, radius, fontSize, trimStart/trimEnd/trimOffset (path draw-on)
7779
+ Easing (on the keyframe being moved TO): ${KEYFRAME_EASINGS_LIST}
7604
7780
  relation — Behavioral link: {sourceId, targetId, relationType, relationOptions}
7605
7781
  relationType: orbits, follows, attached_to, points_at, mirrors, parallax, wave_through, morphs_to, group_morphs_to, moves_along_path (+ 9 more in schema)
7606
7782
 
@@ -7650,8 +7826,8 @@ EXAMPLE — Animated sky scene with timed reveals:
7650
7826
  description: 'Operation type',
7651
7827
  },
7652
7828
  // Canvas setup
7653
- width: { type: 'number', description: 'Canvas width for set_canvas_size (100-4096)' },
7654
- height: { type: 'number', description: 'Canvas height for set_canvas_size (100-4096)' },
7829
+ width: { type: 'number', description: 'Canvas width for set_canvas_size (100-8192)' },
7830
+ height: { type: 'number', description: 'Canvas height for set_canvas_size (100-8192)' },
7655
7831
  preset: { type: 'string', description: 'Canvas preset for set_canvas_size' },
7656
7832
  backgroundColor: { type: 'string', description: 'For set_background: hex color' },
7657
7833
  generatorName: {
@@ -7803,7 +7979,10 @@ Held exports last until released, or until a LATER export needs the space — an
7803
7979
 
7804
7980
  WHAT quality ACTUALLY SETS, because it bundles three things and only one of them is "quality": draft = 15fps / 72dpi / compression 0.6, standard = 30fps / 150dpi / 0.85, high = 60fps / 300dpi / 0.95. So asking for high DOUBLES the frame count and the render time against standard. Override either half directly - fps for frame rate, scale for resolution - and leave quality to mean compression.
7805
7981
 
7806
- DOES THIS SURVIVE EXPORT? It is a PATH fact, not a per-feature one, so the rule is short and does not go stale as capabilities land: IF IT TICKS INSIDE THE ENGINE'S UPDATE LOOP, IT EXPORTS. Loop animations, relations, keyframes, generators and camera moves all do - measured frame-by-frame, not assumed. What does NOT survive is anything driven by the wall clock outside that loop, or anything on the realtime recorder path.
7982
+ DOES THIS SURVIVE EXPORT? It is a PATH fact, not a per-feature one, so the rule is short and does not go stale as capabilities land: IF IT TICKS INSIDE THE ENGINE'S UPDATE LOOP, IT EXPORTS. Loop presets (pinepaper_animate), relations, keyframes, generators and camera moves all do - measured frame-by-frame, not assumed. What does NOT survive is anything driven by the wall clock outside that loop, or anything on the realtime recorder path.
7983
+ ONE MEASURED EXCEPTION TO CHECK: keyframe_animate's loop:true has been measured playing ONCE in an export and then holding the last keyframe. Before exporting past one cycle, sample a frame in the second cycle; if it holds, write the keyframes out for each cycle, or use a loop preset.
7984
+
7985
+ SEAMLESS LOOPS: there is no loop switch on export. Key the animation to the SAME state at t = 0 and at t = duration, and export exactly that duration — the export renders t = 0 up to one frame before duration, so no frame is doubled at the wrap. A GIF's play count is the loop parameter.
7807
7986
 
7808
7987
  If exported frames look frozen, check the SAMPLING before believing it: a loop animation at animationSpeed 1 has a one-second period, so two frames a whole second apart are identical by design. Compare frames that are not a whole number of periods apart.
7809
7988
 
@@ -7817,6 +7996,8 @@ PLATFORMS & OPTIMAL FORMATS:
7817
7996
  | twitter | 1200x675 | PNG | GIF |
7818
7997
  | web | flexible | SVG | SVG |
7819
7998
 
7999
+ A preset's dimensions assume the canvas has the same aspect. When it does not, the canvas aspect is KEPT (never stretched) and its short edge takes the preset's short edge — a 4:5 canvas on instagram exports 1080x1350 — and the result carries platformFit saying so.
8000
+
7820
8001
  FORMATS: svg, png, gif, mp4, webm, pdf.
7821
8002
  QUALITY: draft, standard, high.
7822
8003
  FRAMING: canvas (default, full canvas) | camera (camera_animates first-keyframe viewport — video formats only, requires a keyframe-mode walkthrough on the canvas; camera animation still drives motion within the fixed output frame).
@@ -7831,19 +8012,21 @@ EXAMPLES:
7831
8012
  - {platform: "youtube", format: "mp4", framing: "camera"} — render only what the camera frames during the walkthrough
7832
8013
  - {platform: "youtube", format: "mp4", duration: 30, estimateOnly: true} — how big would that be, and what would it lose? (renders nothing)
7833
8014
 
7834
- Every export answers the second question too: where this scene loses something to the format you chose, the result carries "fidelity" naming it. Its ABSENCE means this scene has nothing that format would drop — not that the format is lossless.`,
8015
+ Every export answers the second question too: the result carries "fidelity", which says whether the check could run at all (available), what this scene loses to the format you chose (warnings), and what that verdict was derived from (checked). An empty warnings list with a note means this SCENE loses nothing to this format — not that the format is lossless.
8016
+
8017
+ VERIFY MOTION BEFORE YOU RENDER. An export takes seconds to minutes and shows you the result only at the end, by which time a frozen animation has cost you the whole render. pinepaper_capture_frames samples the timeline and reports allIdentical plus per-frame hashes, so a scene that is not moving is one cheap call away — it is the fastest way to catch a frozen chart, an animation whose tail never fires, or a curve that was never drawn. Run it before a long export, not after.`,
7835
8018
  inputSchema: {
7836
8019
  type: 'object',
7837
8020
  properties: {
7838
8021
  platform: {
7839
8022
  type: 'string',
7840
- enum: ['auto', 'instagram', 'instagram-story', 'tiktok', 'youtube', 'youtube-thumbnail', 'twitter', 'linkedin', 'web', 'print-a4', 'print-letter'],
8023
+ enum: ['auto', 'instagram', 'instagram-story', 'tiktok', 'youtube', 'youtube-thumbnail', 'twitter', 'linkedin', 'web', 'print-a4', 'print-letter', 'print-a4-landscape', 'print-letter-landscape'],
7841
8024
  description: "Target platform, which sets the output DIMENSIONS. 'auto' (the default) renders the canvas at its own size — use it when the canvas is already the size you want, otherwise a preset REPLACES your dimensions. Every other value forces that platform's frame regardless of the canvas.",
7842
8025
  },
7843
8026
  format: {
7844
8027
  type: 'string',
7845
- enum: ['svg', 'png', 'gif', 'mp4', 'webm', 'pdf', 'wav'],
7846
- description: "Override format (auto-detected if not specified). 'wav' exports the SOUNDTRACK ON ITS OWN, with no frames rendered — no platform preset resolves to it, so it must be asked for by name, and platform dimensions, framing and quality do not apply.",
8028
+ enum: ['svg', 'png', 'gif', 'apng', 'mp4', 'webm', 'pdf', 'jpg', 'webp', 'srt', 'vtt', 'wav', 'scc', 'html5-ad', 'playable'],
8029
+ description: "Override format (auto-detected if not specified). 'scc' is broadcast CEA-608 captions (CC1, pop-on, 29.97 drop-frame; two rows of 32 characters, Latin set) from the same timed text as srt / vtt — the result names captions cut, characters dropped and captions that had to go on late. 'html5-ad' is a display-network HTML5 ad (Google Ads, DV360 / CM360, most DSPs): a zip with index.html carrying the ad.size meta and a clickTag click target (the whole ad, or ad.ctaItemId's box), plus a backup PNG saved beside it; the result checks the zip against ad.maxBytes (default 150000) and lists any external requests. 'playable' is an MRAID playable: one HTML file loading mraid.js, whose CTA (ad.ctaItemId, required) opens ad.clickUrl (required) through mraid.open. Both are built from the standalone widget page at the canvas size. 'apng' is animation with FULL alpha (stepped PNG frames, saved as .png; transparent unless transparent: false) — the answer for an animated overlay or sticker, since webm here cannot carry alpha and gif's transparency is 1-bit. 'jpg' / 'webp' are stills for a byte budget (ad specs): the png render re-encoded at the quality tier's compression — draft 0.6, standard 0.85, high 0.95 — so lower quality means a smaller file; 'srt' / 'vtt' write a caption file from every text item that has a lifetime (bornAt / ttl) — one cue each, in start order; duration closes a caption with no ttl. jpg has no transparency: it is flattened onto the scene's background colour, or white when none is set. 'wav' exports the SOUNDTRACK ON ITS OWN, with no frames rendered — no platform preset resolves to it, so it must be asked for by name, and platform dimensions, framing and quality do not apply.",
7847
8030
  },
7848
8031
  sampleRate: {
7849
8032
  type: 'number',
@@ -7854,22 +8037,62 @@ Every export answers the second question too: where this scene loses something t
7854
8037
  enum: [16, 32],
7855
8038
  description: 'wav only: 16 (default) or 32-bit float. Rejected for any other format.',
7856
8039
  },
8040
+ ad: {
8041
+ type: 'object',
8042
+ description: 'html5-ad / playable options.',
8043
+ properties: {
8044
+ clickUrl: { type: 'string', description: 'html5-ad: the clickTag value for testing (networks replace it at serve time). playable: the store / landing URL the CTA opens (required).' },
8045
+ ctaItemId: { type: 'string', description: 'The item whose bounds are the click target. Optional for html5-ad (default: whole ad); required for playable. Static: where the item is at export time.' },
8046
+ maxBytes: { type: 'integer', minimum: 1, description: 'Upload size budget (zip for html5-ad, HTML for playable). html5-ad default 150000 (Google Ads display); set your network figure.' },
8047
+ },
8048
+ },
8049
+ transparent: { type: 'boolean', description: 'apng only: keep the alpha channel (the default); false fills the background colour.' },
8050
+ loop: { anyOf: [{ type: 'boolean' }, { type: 'integer', minimum: 0, maximum: 1000 }], description: 'gif / apng: true = loop forever, false / 0 / 1 = play once, n = play n times (email clients often want a few plays, not forever). Refused on video formats — see SEAMLESS LOOPS.' },
8051
+ broadcast: { type: 'boolean', description: 'mp4 only: broadcast-safe — BT.709, limited range (16-235), tagged bt709, constant bitrate with an 8 Mbps floor at 720p+ (4 below). result.video reports what the encoder did.' },
8052
+ broadcastHeadroom: { type: 'integer', minimum: 0, maximum: 40, description: 'mp4 with broadcast only, default 12: luma codes kept clear at both ends, so encoder ringing stays legal (12 measured clean on hard edges; 4 = ~100x fewer out-of-range samples, 12 = ~1000x; never zero). Fidelity raises luma_out_of_range with the legaliser command when any remain.' },
8053
+ 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.' },
8054
+ 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.' },
8055
+ bitrateMode: { type: 'string', enum: ['constant', 'variable'], description: "mp4 / webm: 'constant' (the default with broadcast) or 'variable'." },
8056
+ 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.' },
8057
+ 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.' },
8058
+ pdf: {
8059
+ type: 'object',
8060
+ description: "pdf only: print options. paperFormat 'a4' | 'a3' | 'letter' | … | 'custom' (canvas size, default); orientation; bleed in mm (> 0 includes the bleed area); trimMarks adds crop marks; dpi 72–600. Bleed and trim marks take effect with paperFormat 'custom' (the page is the canvas plus bleed) — on a named size the page stays that size and they are dropped, which the result's warnings say. The canvas's physical size comes from its preset's DPI: start the job with canvasPreset 'print-a4' / 'print-letter' / 'print-a4-landscape' / 'print-letter-landscape' (300 dpi) for print; a hand-sized canvas is read at 96 dpi.",
8061
+ properties: {
8062
+ paperFormat: { type: 'string' },
8063
+ orientation: { type: 'string', enum: ['portrait', 'landscape'] },
8064
+ bleed: { type: 'number', minimum: 0, maximum: 20 },
8065
+ trimMarks: { type: 'boolean' },
8066
+ searchableText: { type: 'boolean', description: 'Default true where the studio supports it: an invisible text layer over the artwork, so the PDF is selectable, searchable and screen-readable. Lines no font in the PDF can carry (older studios have only a built-in Latin font) are drawn but left out, and fidelity says how many and which fonts could not be embedded. Multi-page (pages) exports get it per page on studios that support it. false omits it. result.pdf reports linesWritten / linesSkipped.' },
8067
+ dpi: { type: 'integer', minimum: 72, maximum: 600 },
8068
+ pages: { anyOf: [{ type: 'string', enum: ['scenes'] }, { type: 'array', items: { anyOf: [{ type: 'string' }, { type: 'object', properties: { sceneId: { type: 'string' }, width: { type: 'number' }, height: { type: 'number' } }, required: ['sceneId', 'width', 'height'] }] }, minItems: 1 }], description: "Multi-page PDF: 'scenes' = one page per saved scene (the scenes tool's save action) in timeline order, or an ordered list of scene ids. Saved scenes do NOT record their canvas size, so every page uses the current canvas size unless you give it: {sceneId, width, height} (pixels) per page. Pages are embedded as JPEG at the quality tier's compression." },
8069
+ },
8070
+ },
8071
+ region: {
8072
+ type: 'object',
8073
+ description: 'png / jpg / webp only: export just this part of the canvas (canvas coordinates, x/y = TOP-LEFT) — carousel slices, crops. Output is the region\'s own size unless outputWidth/outputHeight say otherwise; a different aspect is covered, not stretched. SMALL ASSETS (emotes, badges, favicons): design at 2-4x on a canvas of at least 100 px, then export the whole board as a region with the small outputWidth/outputHeight — e.g. a 112x112 canvas, region {x:0, y:0, width:112, height:112, outputWidth:28, outputHeight:28}.',
8074
+ properties: {
8075
+ x: { type: 'number' }, y: { type: 'number' },
8076
+ width: { type: 'number', exclusiveMinimum: 0 }, height: { type: 'number', exclusiveMinimum: 0 },
8077
+ outputWidth: { type: 'integer', minimum: 1, maximum: 8192 }, outputHeight: { type: 'integer', minimum: 1, maximum: 8192 },
8078
+ excludeForeign: { type: 'boolean', description: 'Default true: an item reaching into the region but centred outside it (a neighbouring card overflow) is left out of this image; result.excludedItems names them. false renders everything that touches the region.' },
8079
+ },
8080
+ required: ['x', 'y', 'width', 'height'],
8081
+ },
7857
8082
  quality: {
7858
8083
  type: 'string',
7859
8084
  enum: ['draft', 'standard', 'high'],
7860
8085
  description: 'Compression level (default: standard). It ALSO carries a default frame rate — draft 15, standard 30, high 60 — so "high" doubles the frame count and the render time against "standard" unless you set fps yourself. Set fps and scale to control those halves directly and leave quality meaning compression.',
7861
8086
  },
7862
8087
  fps: {
7863
- type: 'integer',
7864
- minimum: 1,
7865
- maximum: 120,
7866
- description: 'Video only: frames per second, overriding whatever `quality` implies. File size is bitrate x duration and does not move with fps, but the picture gets fewer bits per frame at a higher one.',
8088
+ anyOf: [{ type: 'number', minimum: 1, maximum: 120 }, { type: 'string', pattern: '^[0-9]+/[0-9]+$' }],
8089
+ description: 'Video only: frames per second, overriding whatever `quality` implies. Integers, NTSC rates (23.976, 29.97, 59.94 — snapped to the exact 24000/1001, 30000/1001, 60000/1001, which CTV / OTT specs require) or a rational string "30000/1001". At an NTSC rate a duration of N seconds is round(N x fps) frames, so 6 s at 29.97 is 180 frames = 6.006 s — the result says when that overruns the duration you asked for. File size is bitrate x duration and does not move with fps.',
7867
8090
  },
7868
8091
  scale: {
7869
8092
  type: 'number',
7870
8093
  minimum: 0.1,
7871
8094
  maximum: 1,
7872
- description: 'Video only: render at this fraction of the platform preset dimensions (0.1-1). The engine derives its encode target from RESOLUTION, so this is the size control — there is no bitrate to set, and halving the frame roughly quarters the pixels and the file. It is also the preview knob: scale 0.5 with quality "draft" is the fast look-check before committing to a full render. Rounded to even dimensions, which H.264 requires.',
8095
+ description: 'Video only: render at this fraction of the platform preset dimensions (0.1-1). The engine derives its encode target from RESOLUTION unless bitrate is given, so this is the size control; halving the frame roughly quarters the pixels and the file. It is also the preview knob: scale 0.5 with quality "draft" is the fast look-check before committing to a full render. Rounded to even dimensions, which H.264 requires.',
7873
8096
  },
7874
8097
  framing: {
7875
8098
  type: 'string',
@@ -8441,6 +8664,7 @@ DATA FORMAT: Array of objects, e.g. [{category: "A", value: 10}, {category: "B",
8441
8664
 
8442
8665
  OPTIONS (vary by chart type):
8443
8666
  - Common: title, width, height, x, y, colors, animation, axisColor, labelColor, labelFontSize
8667
+ x / y are the chart's CENTRE (measured: x 1000, y 300 at 800x600 reads back as bounds [600, -25, 800, 625]) — pass the centre of the rect you want it in, not its top-left.
8444
8668
  - Bar: barWidth, barGap, xLabel, yLabel
8445
8669
  orient: 'horizontal' flips bars to grow rightward from a left Y-axis — best for long category names
8446
8670
  labelArrangement: 'auto'|'horizontal'|'slanted'|'vertical'|'truncate'|'wrap'