Skip to main content
Glama

Pixelbolt

Pixel art and sprite animation that your AI agent can make, check and fix. Pixelbolt turns an editable JSON recipe into sprites, tiles, icons, effects and short animations, and exports them as PNG sprite sheets, texture atlases, animated PNGs, GIFs and frames for a video editor. Agents drive it through a local MCP server or the command line; you can open, play and edit the same recipe in a local browser studio.

Try it in your browser: render, export and share recipes with nothing installed.

Poke the pixels: blow on a live pixel-art campfire, pour honey and slime, then save a GIF or share a link that replays exactly what you did.

Effect guides: fire, explosions, smoke and steam, water, slime and honey, and magic, each with recipes that open in the playground.

English · 简体中文 · 日本語

Tests License: MIT Node.js: 20+

A library drawn with Pixelbolt: books circle the chandelier, a lectern book turns its page and a grey tabby sleeps on a shelf

A campfire simulated by Pixelbolt's fluid compiler: flames lick up from the logs, embers drift up and smoke curls away An explosion simulated by Pixelbolt: a white flash grows into an orange fireball that rises, cools and curls into smoke

Fire, smoke, steam, explosions, magic, water, honey and slime come from a real fluid simulation that runs on your own CPU in well under a second and bakes every frame into palette pixels you can edit, with seamless loops (fluid effects).

  • Who it's for: game developers, game-jam teams and web or video makers who want an agent such as Claude, Codex or Cursor to draw and revise their pixel art, and people building agents that need exact, repeatable images.

  • Use it when you want small 2D game assets that stay editable as text: the agent reads every pixel back, fixes one pixel or one frame without redrawing the rest, and the same recipe renders the same image every time.

  • Don't use it for painted or photographic images, large animations (an animation frame is at most 256 × 256 pixels, and a project at most 256 frames and 4,194,304 pixels in all; a single still, such as a 1920 × 1080 backdrop or mural, may be up to 4096 pixels a side within that total), generating art with an image model, or editing GIFs and Aseprite files (it imports 8-bit RGB/RGBA PNG only).

  • Compared with aseprite-mcp, pixel-mcp, pixscii, pixelart-mcp, pixelforge-mcp and SpriteCook, with Pixelbolt's measured runs, the others read from their code and docs, and when to choose each one instead: Pixel art and sprite MCP servers for AI agents: 7 compared (checked 17 October 2026). For fire, smoke, explosion, water and spell effects, with Pixel FX Designer, SpriteMancer, Fluid FX, Pixel Composer, PHOSPHOR, BlastFX and Pixel VFX Forge: Pixel FX Designer alternatives: 8 tools compared (checked 19 October 2026; Pixel VFX Forge added 27 October).

Try it. You need Node.js 20 or newer; there is nothing else to install, no API key and no network service:

git clone https://github.com/skulitom/Pixelbolt.git
cd Pixelbolt
node bin/pixelbolt.js preview --open

Give it to your agent. For Claude Code, one line installs the pixel art skill, with nothing to clone and no MCP server (Pixelbolt 0.9.2 or later):

npx -y pixelbolt@latest skill install

It writes ~/.claude/skills/pixelbolt-pixel-art/ (--project writes the current folder's .claude/skills/ instead, --dir any skills folder), runs every command it names through npx -y pixelbolt@<that version>, and never replaces a copy you have changed unless you add --force. For Pixelbolt's MCP tools too, install the Claude Code plugin instead: it brings the server and the same skill together. In Claude Desktop, download pixelbolt.mcpb from the latest release and open it (or install it from file under Settings → Extensions), choose a workspace folder for recipes and artwork, then ask the agent to call pixel_help. For Codex, Cursor and other MCP clients, see Connect an agent through MCP. Pixelbolt is in the official MCP Registry as io.github.skulitom/pixelbolt.

Pixelbolt was called PixelForge until 0.8.0. The pixelforge command still works as an alias, and recipe files keep their pixelforge-* format tags, so existing files and agent setups keep working.

Gallery · Fluid effects · Quick start · Agent setup · MCP server · Portable builds · Recipe reference · Contributing · Sibling tools

How it works

Write a JSON recipe with palettes, text grids, reusable symbols and drawing operations. Get transparent PNGs, sprite sheets, animated PNGs, GIFs for sharing, numbered frames for video editors, and the metadata and player needed to use them. Inspect and edit the recipe in a local browser studio.

Zero runtime dependencies. No build step, API key, image model, network service or native graphics library. Requires Node.js 20 or newer. Works from a checkout on Windows, macOS and Linux.

Related MCP server: Aseprite MCP Server

Figures in motion

Author limbs as bones, bodies as masses, and faces or props as grids. Relative joints and two-bone reach keep attachments together; idle, walk, run, jump, attack and hurt templates expand into editable poses. Review planted feet, ground contact and loop joins in the CLI, MCP or studio. The output uses the existing sprite player.

node bin/pixelbolt.js preview character.poses.json
node bin/pixelbolt.js compile character.poses.json --expand --out character-edit.poses.json
node bin/pixelbolt.js inspect character.poses.json --motion run --speed 48

Pose and motion reference

Baked wave effects

Looping interference and portal interiors combine waves before colouring them, leaving cancellation gaps between bright bands. Two editable presets bake to ordinary sprite frames and appear in the studio's sample list. The game only plays the exported animation.

Paired cyan waves and a violet portal in a Tidewatch game scene

node bin/pixelbolt.js preview examples/portal.wave.json --open
node bin/pixelbolt.js compile examples/portal.wave.json --out output/portal.json
node bin/pixelbolt.js render output/portal.json --out output/portal-assets

Wave controls and limits · Scene comparison and review. Loops only; masks shape the artwork without simulating reflection or collision.

An AI agent drew these with Pixelbolt. Its scripts write the recipes, particle sources and scenes; Pixelbolt renders every sprite to a transparent sheet with an atlas, plays each 240×135 room as a scene and writes the GIFs. They are shown at 3×.

An observatory: a porthole onto a ringed planet, a telescope, an armillary with a blooming crystal and a cat asleep on the window seat

A workshop: a half-built brass automaton, a gear clock, glass coils that arc and a clockwork mouse

Eight potions, each popping its cork: green, red, blue, purple, orange, frost, shadow and rainbow

Four bell jars: a sprouting bloom, glowcap mushrooms, a snapping flytrap and a succulent

Three clockwork familiars: a brass owl, a koi in its bowl and a crystal moth over a lantern

Two more rooms and how these were made

Fluid effects

A pixelforge-fluid source runs a small, deterministic fluid simulation and compiles it into an ordinary recipe of palette grids: gases with buoyancy, swirling vorticity, cooling and dissipation, and liquids as FLIP particles on the same grid, thin as water, thick as honey (a stream that never breaks, a mound that slowly sinks) or firm as slime (a blob that squashes, springs back and holds its shape). Look layers map density, temperature, speed and depth onto your palette ramps through ordered dither, so no colour outside the palette can appear, and looping effects close their seam. Each preset below compiles in well under a second on a laptop CPU, with no GPU and no model calls. Shown at 4×.

A campfire: flames lick up from crossed logs, embers drift up and smoke curls away A torch: a small flame flickers on a wooden handle with a metal cup Steam puffs rhythmically from a pipe, rises, curls and thins A magic swirl: two glowing sources orbit and leave purple mana that spirals

An explosion: a white flash grows into an orange fireball that rises, cools and curls into smoke while sparks fly A cartoon smoke puff, outlined and lit from the upper left, billows out in lobes and thins away

A waterfall: water pours off a mossy ledge in a foaming arc, splashes into a basin and spills over its lip A drop falls from a stalactite and splashes into a small pool A green potion bubbles in a round flask: the surface heaves and bubbles rise and pop A drop falls into a pool, throws up a crown and spray, and the waves settle

Glossy honey runs off a dipper into a jar as one unbroken rope that folds into a mound on the calm surface A blob of green slime drops, squashes flat, springs up tall, wobbles and settles into a dome that holds its shape

Obstacles can be smooth, so thick liquids slide down drawn slopes, or move and vanish on cue; dust can be thrown; a liquid can burst in mid-air or change on a frame you choose:

Wet snow slides off a house's roof as one slab, pours over the eave and settles in a drift against the wall A plank dam vanishes and the water behind it floods out as a wave, rolls over a rock and splashes off the far wall A stone drops onto the ground and throws a wave of dust out both ways that billows, slows and thins away A ball of green slime bursts in mid-air into chunks that fly out, land and wobble into mounds Stiff honey clings under a wooden spoon, then thins, stretches, lets go and drops into a bowl of honey

node bin/pixelbolt.js compile examples/campfire.fluid.json --out output/campfire.json
node bin/pixelbolt.js gif output/campfire.json --scale 4 --out output/campfire.gif

The seventeen presets are in examples/ (*.fluid.json); agents compile them with MCP pixel_compile and read the format with pixel_help topic fluid. In the studio, Fluid lab (/fluid.html) runs the same compiler in your browser: pick a preset, move a few sliders and download the GIF, sprite sheet, recipe or edited source. The fluid effects guide covers emitters, sparks, obstacles, looks, loops and thick fluids (viscosity, yield stress, adhesion). In a web page, createLiveFluid runs the same simulation live, a frame at a time, and takes pushes, puffs and emitter boosts from the pointer (live fluids).

Start

git clone https://github.com/skulitom/Pixelbolt.git
cd Pixelbolt
node bin/pixelbolt.js preview

Open http://127.0.0.1:4747, or add --open to have your default browser opened for you. If that port is in use, a free one is taken and printed instead; --port N insists on one port and fails when it is busy. preview hero.pixel.json starts with your own recipe. Pick the forest spirit, campfire or coin; edit the JSON; inspect the animation or sprite sheet; download the asset ZIP. The studio includes playback speed, frame selection, whole-number zoom (the scroll wheel zooms at the pointer, dragging pans, and + − 0 and F zoom, fit and go full screen), a pixel grid, onion skinning, palette inspection, source save/open, a GIF download of the current animation, numbered frames for a video editor, recovery of unsaved edits and live error messages. All assets and fonts are local.

Pixelbolt's local studio showing the forest spirit sprite, animation frames, editable JSON recipe and palette

Or generate an asset without opening a browser:

node bin/pixelbolt.js init hero.pixel.json
node bin/pixelbolt.js validate hero.pixel.json
node bin/pixelbolt.js inspect hero.pixel.json --out hero-frames.png
node bin/pixelbolt.js render hero.pixel.json --out output/hero

inspect saves a contact sheet of every frame and lists each frame's timing. Add --grid to read frames back as palette-key text; --animation, --frames (names or patterns such as 'flash-*') and --region narrow the view, and --step 2 --offset 1 keeps every second cell from the second, so a strobe's A and B frames can be read apart. validate hero.pixel.json --fps 30 also lists the durations that are not whole video frames at that rate and what a video sequence would do to each animation. patch hero.pixel.json --changes fix.json previews targeted edits and reports every pixel they change; add --out to save the new recipe. gif hero.pixel.json --out hero.gif saves one animation as a GIF for sharing. Command results except help and the preview server are JSON. Errors go to stderr and exit with code 1. Use - instead of a filename to read JSON from stdin. Existing output files are protected; add --force to replace them. A forced export over an earlier export of the same recipe or sequence also removes the frames it no longer has and lists them as removed; a sprite export removes only frame and animation PNGs its own earlier atlas lists and no other atlas in the folder does, so another sprite's frames and PNGs you add there aren't deleted. No command writes over a file it reads, even with --force and through a link or another spelling of its path; patch may save over the recipe it edits. No export writes over its own recipe or the files it references. You can optionally run npm link for the pixelbolt command. No npm install is needed.

pixelbolt share <recipe.json|-> prints a playground link; render and scene accept --embed-recipe (recipes up to 1 MiB, so every PNG still opens in Pillow), recovered with import image.png --embedded --out recipe.json.

Both carry the resolved recipe text and engine version: the link in its URL fragment, which browsers do not send to a server, and the PNG in a compressed recipe chunk.

Diorama: a 3/4 dungeon room

An original 3/4 top-down dungeon room in the style of procedurally generated "floating diorama" games: 8-pixel tiles, wall caps with front faces, a sunken pool fed by a waterfall, moss, props, torches and a hero whose strike breaks a pot. The room is one recipe whose tilemap operation places walls, faces, cliffs, water and worn bricks by neighbour rules; moss is seeded value noise on the floor colour; shadows and torchlight move colours along palette ramps, so the scene keeps its 31 colours lit or re-themed.

node bin/pixelbolt.js preview showcase/diorama/art/room.scene.json

The diorama by torchlight: torches and candles flicker, and the room flashes where the hero's strike breaks a pot

By night, each torch and candle light follows its placement and flickers on its own seed, and a light at the pot flashes on the strike cue: moving scene lights, still in exactly the palette's colours.

Study details and images · Feasibility report: what the style asked of Pixelbolt

Art quality workflow

The art workflow covers silhouette, value and native-size review, animation-aware onion skins, masked regional corrections, authored parts and attachments, guarded rebuild overlays, scenes, PNG interchange and aligned normal/emissive passes. Passing checks are not a verdict on the art: look at every frame, at native size and in playback.

node bin/pixelbolt.js inspect examples/forest-spirit.json --animation idle --view onion --native --diagnostics
node bin/pixelbolt.js preview examples/forest-spirit-lit.scene.json

For agents

Use Pixelbolt to create or revise pixel sprites, tiles, icons, effects and short animations from editable JSON. Choose the CLI when you have shell access or the Model Context Protocol (MCP) server when your client supports tools. Both use the same renderer and work locally.

Entry point

Purpose

llms.txt

Compact documentation index with direct links for agents

Pixel art skill

Reusable authoring and visual inspection workflow; npx -y pixelbolt@latest skill install installs it on its own

Authoring guide

Recipe fields, drawing operations and limits

JSON Schema

Machine-readable recipe structure

Examples

Complete forest spirit, campfire, coin and shrine recipes, a caption with pixel text, a rotated sword swing, a particle effects source, four spell effects (slash hit, fire explosion, lightning strike, healing circle), five cel-shaded water effects and seventeen fluid presets

AGENTS.md

Instructions for agents contributing to the toolkit

Start with the authoring guide or MCP's pixel_help. Write a recipe, validate it, inspect every frame with pixel_inspect or pixelbolt inspect, revise with targeted patches, then render to a fresh output directory. Validation checks the format; image inspection checks the art.

A tiny animation

{
  "version": 1,
  "name": "slime",
  "width": 8,
  "height": 8,
  "palette": { "g": "#72b58d", "e": "#20383f" },
  "frames": [
    {
      "name": "rest",
      "duration": 180,
      "ops": [{ "op": "grid", "x": 1, "y": 3,
        "rows": [".gggg.", "ggeegg", "gggggg", ".gggg."] }]
    },
    { "name": "up", "from": "rest", "translate": [0, -1], "duration": 180 }
  ],
  "animations": { "bounce": { "frames": ["rest", "up"] } }
}

Each text-grid character selects a palette color; . and space leave pixels untouched. Frame inheritance makes small changes cheap to describe. Rendering is deterministic and uses integer pixel coordinates.

Export bundle

File

Use

name.png

RGBA PNG sprite sheet, with configurable columns, padding and integer scale, or packed by visible bounds with sheet.trim

name.atlas.json

TexturePacker-style JSON hash: rectangles, trim offsets, source sizes, pivots/anchors, named points, frame durations, named animations and Aseprite-style frame tags

name.tres

Godot 4 SpriteFrames for AnimatedSprite2D: one atlas texture per frame (trimmed frames keep their place on the canvas) and every animation with its loop and exact frame times; keep it next to name.png

frames/*.png

Each frame as a transparent PNG

animations/*.png

APNG for each animation, retaining transparency, frame timing and loop behavior

name.css

CSS animation classes; supports multi-row sheets and unequal frame durations

player.js

Small, dependency-free Canvas player with play, pause, resume and named animations, plus loadSpriteSheet, frameAt and drawFrame for games

name.pixel.json

Editable source recipe

preview.html

Standalone preview you can open directly in a browser

APNG files use the .png extension intentionally. Recipes can share a palette file ("palette": { "$ref": "palette.json" }), recolor keys per frame for palette cycling or per layer for a flash that leaves the rest of the frame alone, copy rectangles from template symbols or earlier frames (with exact colour swaps), outline layers (outside, inside or both, with direction masks for rim lights and drop shadows), dither between colours with canvas-anchored Bayer, custom, seeded non-periodic noise or clumping value-noise patterns, shade along palette ramps (ramps and the shade operation) without leaving the palette, draw whole character maps with the tilemap operation (variants, weights and neighbour rules), and grow detail such as moss or cracks with seeded rewrite rules. They compile 47-tile blob or 16-tile cardinal autotile sets, optionally with shape variants, with pixelbolt autotile; see the authoring guide and art workflow.

Several of these ideas come from Pixel Composer's node set, adapted to text recipes. Patches can propose De-Corner/De-Stray cleanup, and inspection diagnostics count per frame what it would change. Pose sources rotate parts by any angle into editable symbols, using RotSprite-style resampling that adds no colours, and tween eased in-betweens. Scene trajectories ease. A pixelforge-fx source compiles seeded particle emitters and spell shapes (slashes, shockwave rings, beams, lightning, magic circles, impact stars, fireballs, cel-shaded water streams and drops) into an ordinary recipe with pixelbolt compile, a pixelforge-fluid source does the same with a fluid simulation, and a pixelforge-normals source derives a lighting normal pass from any recipe: an editable recipe aligned frame for frame, for scene lighting and engine normal maps (the spirit's, lit in a small night scene; details). See the shrine, the swing, the effects and a healing circle. Lossless non-interlaced 8-bit RGB/RGBA PNG import is supported, optionally with unscaled atlas timing metadata; see interchange limits. GIF import, native Aseprite files and automatic quantization remain outside the current scope. Sources stay editable JSON.

GIF for sharing

APNG keeps full alpha and exact timing, but many chat, forum and store pages only animate GIFs and cannot scale pixels crisply. pixelbolt gif writes one animation as a GIF at the frames' own size; --scale (1-16) enlarges it by a whole number for pages that would show a small sprite tiny:

node bin/pixelbolt.js gif hero.pixel.json --out hero.gif
node bin/pixelbolt.js gif hero.pixel.json --animation blink --scale 8 --background "#17191d" --out hero-blink.gif

Colours are exact: nothing is quantized or dithered, and a frame that needs more than 256 colours is refused with an error instead of being approximated. GIF has no partial transparency, so pixels below 50% alpha become transparent and the rest opaque; --background (a palette name or opaque hex colour) blends every pixel onto one colour instead, which keeps soft shadows. Frame times are rounded to GIF's 10 ms steps with the remainder carried to the next frame, and raised to 20 ms where shorter. Transparent pixels stay clear in every viewer, including ffmpeg and Pillow, which fill a cleared frame with the background colour unless it names a transparent index (the notes report the rare frame with 256 colours of its own that has none to name). The JSON result's notes say which of these happened. The studio's GIF button downloads the current animation at the current zoom, and MCP pixel_render takes "gif": {} (the frames' own size) or "gif": { "scale": 8 }. Every render also reports a check of what it wrote; give pixel_render an expect (or render an --expect) with the size, frames, colours, background or loop the user asked for, and the check names each file that misses it and how to fix it. The export bundle itself is unchanged.

pixelbolt gif-frames makes a GIF from a folder of numbered PNG frames, such as a sequence folder or frames a video or effects tool wrote, by the same rules:

node bin/pixelbolt.js gif-frames shots/prism --fps 25 --out prism.gif
node bin/pixelbolt.js gif-frames shots/prism --fps 30 --loops 3 --background "#101828" --prefix prism_ --out prism-3x.gif

Files are read in number order (prism_0001.png, prism_0002.png …); gaps in the numbering, other PNGs in the folder and a folder holding two sequences (choose one with --prefix) are reported, never guessed around. Each frame keeps its exact colours: one with more than 256 is refused by name, and when the frames use different colours, each frame carries a colour table of its own, so a loop of 48 frames at 1080 × 1080 with about 120 colours per frame and 4,345 in all decodes back pixel for pixel, where a palette generator would shift some colours. Frame k starts at k × 1000 / fps ms rounded to GIF's centiseconds: 25 fps is 40 ms per frame, 30 fps alternates 30 and 40 ms and keeps the loop's length, and above 50 fps delays are raised to 20 ms, which the notes report with the new length. --loops is the number of plays, 0 (the default) for ever. Without transparency, each frame after the first stores only the rectangle that changed. The PNGs must be 8-bit RGB or RGBA and the same size; the loop may hold 134,217,728 pixels (115 frames at 1080 × 1080).

Pixel text

The text operation draws titles, captions, labels and counters in a built-in pixel font: printable ASCII, the accented letters of western European languages (À–ÿ, Œ œ Š š Ž ž Ÿ) and common symbols such as € £ © — … “ ” „ « », with capitals 7 pixels tall, proportional letters and tabular digits. The accent of a capital sits in the two rows above y.

{ "op": "text", "x": 120, "y": 8, "text": "GAME OVER\nscore 12480", "color": "ink", "align": "center" }

align says whether x is the left edge, the centre or the right edge; spacing, lineHeight, scale and rotation adjust it, and outline after it gives a readable edge. Text that runs off the canvas is reported like any other clipped drawing, and a character the font lacks is an error that names it. examples/caption.json is a lower third that opens and types its title; node bin/pixelbolt.js sequence examples/caption.json --fps 30 --size 1080p --align bottom --offset 0,-64 --out shots/caption turns it into 81 frames that sit across the bottom of a 1080p picture.

Frames for a video editor

Video editors do not animate APNG, and GIF loses alpha and exact timing. pixelbolt sequence writes what editors do read: one numbered PNG per video frame, at a constant frame rate, on a canvas of the video's size, with the alpha kept.

node bin/pixelbolt.js sequence hero.pixel.json --fps 30 --size 1080p --out shots/hero
node bin/pixelbolt.js sequence examples/effects.fx.json --animation sparks --fps 29.97 --size 4k --scale 16 --align bottom --offset 0,-200 --out shots/sparks
node bin/pixelbolt.js sequence room.scene.json --fps 24 --size 1080p --fit cover --out shots/room
  • Timing. Each video frame shows the pose that is active when the frame starts. --fps takes 24, 25, 30, 50, 60, the NTSC rates 23.976, 29.97 and 59.94 (kept exact as 24000/1001, 30000/1001 and 60000/1001, so long clips do not drift), or a fraction. --loops n or --seconds s sets the length; --step 2 animates on twos. A looping animation keeps cycling; one that plays once holds its last pose.

  • Size. --size is 720p, 1080p, 1440p, 4k, vertical (1080×1920), square or WIDTHxHEIGHT. The sprite is enlarged by a whole number, never resampled: the largest that fits, or --scale up to 256. --align (top-left … center … bottom-right) and --offset x,y place it. --fit cover takes the smallest whole number that covers the canvas instead and crops what hangs over; --align then chooses the part that stays, and the result's placement.visible says which columns and rows of the source that is. Without --size the canvas is the sprite at --scale. --background blends onto an opaque colour instead of keeping alpha.

  • Sources. A recipe (--animation picks one), a pose, autotile or effects source, which is compiled on the way in, or a scene, which is rendered moment by moment as a shot; moments that look the same share one PNG. A scene of 192×108 or 240×135 pixels fills 1080p exactly at scale 10 or 8. Any other scene is centred with margins in its own background colour, so the shot is still opaque from edge to edge, or fills the canvas with --fit cover.

  • What you get. A new folder with name_0001.png onwards, sequence.json (frame rate as a fraction, frame count, size, placement, loop facts and the timing of every pose) and a README.txt that says how to import them. The result's notes say what the frame grid did: which poses changed length, which are shorter than a video frame and never shown, and whether the clip ends exactly where the loop does, with the number of loops that would.

  • One video file. Pixelbolt does not encode video. The folder's README and the JSON result give the exact ffmpeg command for a ProRes 4444 .mov with alpha, and, when there is a background, for an H.264 .mp4 in lossless 4:4:4. H.264 usually stores colour once for each 2 × 2 pixels (4:2:0), which smears every pixel edge whatever the scale once a player interpolates it; 4:4:4 keeps every colour, in a file no larger for pixel art, but QuickTime, Safari and Windows' own players may not play it, so on an even canvas h264Compatible gives a 4:2:0 file as well. With ffmpeg 7 the ProRes command keeps the alpha exactly and the colours within 2 of 255, and the H.264 command the colours within 1; scripts/verify-exports.py repeats those checks when ffmpeg is installed.

In the editor, import the files as an image sequence and set the clip's frame rate (the files carry none), then leave the clip at 100% scale: scaling there would blur the pixels. A sequence holds at most 18,000 frames and a canvas at most 4096 pixels on a side. MCP pixel_render and pixel_scene take "sequence": { "fps": 30, "size": "1080p" } with the same options.

The studio's Frames button downloads the same files for the animation on screen as one ZIP. Its dialog also shows the pixelbolt sequence command and the MCP sequence argument for the chosen frame rate and size, so an agent can repeat the export without the browser.

Large stills

A recipe with a single frame may be larger than 256 × 256: up to 4096 pixels a side and 4,194,304 pixels in all. A 1920 × 1080 or 2560 × 1440 backdrop, wallpaper or mural is then one canvas, drawn at one screen pixel per art pixel, and every drawing operation sees all of it. On such a canvas, rectangles, ellipses and dither or shade regions may be up to twice its side instead of 512.

Everything else keeps its limits. A recipe with two or more frames, a scene and its assets, pose, effect, wave and fluid sources, and import stay at 256 pixels a side, and the 67-million-pixel drawing budget is unchanged: fill, replace and outline each pass over the whole canvas, about 2 million of that budget at 1920 × 1080. To show bigger pixels instead, enlarge a small canvas with sheet.scale or sequence --size, as before. The studio opens a large still fitted to its window, scaled down smoothly so dither reads as the tone it makes; zoom in with the scroll wheel and drag to look around at 1× and up.

Connect an agent through MCP

Claude Code plugin. One command installs the MCP server and the pixel art skill, with nothing to clone (Claude Code 2.1.292 or later):

claude plugin install pixelbolt --marketplace skulitom/Pixelbolt

Inside a session, /plugin install pixelbolt --marketplace skulitom/Pixelbolt does the same from Claude Code 2.1.275. On older versions, add the marketplace first with claude plugin marketplace add skulitom/Pixelbolt, then run claude plugin install pixelbolt@pixelbolt. The plugin runs its own copy of bin/pixelbolt.js with your Node.js (20 or later), in the project folder you open in Claude Code: file references resolve inside that folder and exports go to its output/ folder. Restart Claude Code after installing.

Other MCP clients. Clone the repository first, then add the server to your agent's MCP configuration. Replace /absolute/path/to/Pixelbolt with your checkout's absolute path; on Windows use forward slashes, for example C:/DEV/Pixelbolt:

{
  "mcpServers": {
    "pixelbolt": {
      "command": "node",
      "args": [
        "/absolute/path/to/Pixelbolt/bin/pixelbolt.js",
        "mcp",
        "--out",
        "/absolute/path/to/Pixelbolt/output"
      ]
    }
  }
}

For clients that use TOML:

[mcp_servers.pixelbolt]
command = "node"
args = ["/absolute/path/to/Pixelbolt/bin/pixelbolt.js", "mcp", "--out", "/absolute/path/to/Pixelbolt/output"]

The eight tools are:

  • pixel_help: a short quick start (the recipe format, operations, tools and workflow, about 2,000 tokens); topic returns the full authoring guide (guide), the art workflow (workflow), the recipe schema (schema), a complete sample (example), or the poses, scenes, autotile, fx, fluid or normals schemas.

  • pixel_validate: validate { "project": ... } and save its recipe revision without exporting assets. Reports every clipped location; "fps": 30 adds frameGrid, the durations off that video frame grid and what a sequence would do to each animation.

  • pixel_inspect: contact sheets, exact regional grids, silhouette/grayscale/onion views, 3×3 tile repeats with seam evidence, native size, named layer isolation, saved-reference comparisons, advisory diagnostics, motion review with contacts and explicit pose metadata, and perspective guides (a horizon, vanishing points, an isometric grid). frames takes */? patterns, step/offset keep every nth cell, and optional bounded samples expose omissions. Saves its recipe revision.

  • pixel_patch: apply targeted set, insert, remove and paint edits. For example, { "paint": "frames[blink]", "value": [{ "x": 9, "y": 7, "color": "k" }] } corrects a pixel in final canvas coordinates after all layers; transparent erases it. Returns every frame whose pixels changed (exact pixels for small edits) and a before/after PNG. The source stays unchanged and successful edits get a new revision.

  • pixel_render: render { "project": ... }, return a PNG contact sheet of every frame with cell names/timing plus the output folder, its key files and frame/animation counts (listFiles: true lists everything). Add "animation": "idle" to preview a sequence in playback order. The exported APNGs and HTML preview play the animation. Add "gif": {} (or { "scale": 8, "background": "#17191d" }) to also write every animation as a GIF in gifs/, with notes on any alpha or timing change, and "sequence": { "fps": 30, "size": "1080p" } to write one animation as numbered PNG frames for a video editor in sequence/. Every call writes a fresh folder inside the configured output directory.

  • pixel_compile: compile a pixelforge-poses source (including bones, masses, relative joints, reach targets and editable motion templates), a pixelforge-autotile template, a pixelforge-fx source (particles and spell shapes), a pixelforge-fluid source (simulated smoke, fire, explosions, magic and water) or a pixelforge-normals source (a normal pass derived from a recipe or revision, with flat faces by colour for boxes and walls) into a recipe revision, with a contact sheet and optional metadata.

  • pixel_scene: render a pixelforge-scene manifest (tilemaps with autotile legends and neighbour rules, named cues, sequences, trajectories, attachments, ground sorting, shade placements, ramp or multiplied lighting from point lights and a sun, with cast shadows, palette themes, haze for far planes, a panning camera with parallax planes) at any time, with a depth view of what is drawn in front of what, export it, or write it as a shot of numbered PNG frames with sequence; assets may be revisions.

  • pixel_import: import a native-resolution PNG, given as base64 or a file inside the server root, as a lossless recipe revision.

pixel_render optionally accepts "share": true for a playground link (or a link-limit error without failing the render) and "embedRecipe": true for recipes inside exported PNGs.

pixel_render also writes files at the names the user asked for: "saveAs": { "sheet": "assets/hero.png", "gif": "assets/hero.gif" } (keys sheet, atlas, godot for the Godot .tres, recipe, gif, apng or any exported PNG, GIF or JSON file; paths inside the server root, keeping the file type, never under a hidden name or in the home folder's AppData or Library). A saved atlas or .tres names the saved sheet (or the export folder's), and a .tres inside a Godot project needs its sheet inside it too. It never replaces an existing file unless "overwrite": true, and then a JSON file only if it is a recipe, atlas or sequence, and a .tres only if it is a SpriteFrames; it never writes over the palette and asset files the recipe references, and checks every path before writing anything.

pixel_patch also supports compact grid, masked move, regional recolor and proposed cleanup of doubled corners and stray pixels, with explicit frame or inherited scope, and applies saved correction overlays (overlay). Large pixel_render previews are sampled with total/shown/omitted metadata instead of blocking a valid export; exported animations remain complete. Palette and scene file references resolve inside the server's --root (default: its working directory); requests may be up to 16 MiB, and an oversized request is answered with an error without stopping the server. Each tool call runs in a worker thread and is stopped if it takes longer than 50 seconds (--time-limit <seconds> changes this) or runs out of memory; the call is answered with an error that says so, and the server keeps serving. A sequence from one call may come to at most 512 MB; pixelbolt sequence writes longer shots.

Send a recipe once. Each successful recipe-tool response includes a revision id that the other tools accept in place of project, so later calls, including patches, need not resend the recipe. Validate, inspect, patch and render save immutable snapshots in <MCP --out directory>/.revisions/; they survive restarts when you use the same directory, even before an asset export. Prefer an absolute --out path. Earlier revisions remain undo points. Older rendered revisions can be recovered from saved bundle recipes. Render also saves the recipe beside the assets.

The server implements newline-delimited stdio MCP with initialization, version negotiation, ping and tool discovery/calls. It supports protocol versions 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25. It uses no HTTP transport or external services. See the MCP stdio specification.

An agent can also use the CLI directly. The reusable agent skill and authoring reference explain the complete format; npx -y pixelbolt@latest skill install gives Claude Code the skill with its guides, schemas and examples and no server. schema.json supports editor completion and structural validation; the rasterizer additionally validates references, matching row widths and resource limits.

Use in a web app

Serve the exported folder with your application:

<canvas id="sprite"></canvas>
<script type="module">
  import { SpritePlayer } from './assets/player.js';
  const player = await SpritePlayer.load(
    document.querySelector('#sprite'),
    './assets/forest-spirit.atlas.json',
    { scale: 4 }
  );
  player.play('idle');
  // player.pause(); player.resume(); player.destroy();
</script>

Or use CSS alone:

<link rel="stylesheet" href="assets/forest-spirit.css">
<span class="pf-forest-spirit pf-forest-spirit-idle" role="img" aria-label="Forest spirit"></span>

CSS honors reduced-motion preferences. The Canvas API leaves autoplay decisions to the application. Atlas paths in SpritePlayer.load resolve relative to the atlas URL.

A game drawing many sprites into one canvas can use the same module's helpers. drawFrame places a frame by its anchor (when the recipe declares one), restores trimmed frames to their original offset and snaps fractional positions to whole pixels, so a sprite moving at sub-pixel speeds keeps hard edges:

<script type="module">
  import { loadSpriteSheet, frameAt, drawFrame } from './assets/player.js';
  const hero = await loadSpriteSheet('./assets/hero.atlas.json'), context = canvas.getContext('2d');
  requestAnimationFrame(function draw(time) {
    context.clearRect(0, 0, canvas.width, canvas.height);
    drawFrame(context, hero, frameAt(hero.atlas, 'walk', time), 64, 96, { scale: 3 }); // (64, 96) is where the anchor lands
    requestAnimationFrame(draw);
  });
</script>

For Godot 4, put name.png and name.tres in one folder of the project and use the .tres as an AnimatedSprite2D's Sprite Frames. For another engine, use the atlas rectangles or individual PNGs. For regular-grid importers, set sheet.padding to 0 and use the exported frame dimensions. Phaser accepts the JSON-hash frame layout via load.atlas; named animation sequences are in the extra animations field. See game integration notes.

JavaScript API

import { renderProject, inspectProject, patchRecipe, compareProjects, createBundle, writeBundle, compileEffects, compileFluid, createLiveFluid, animationGIF, folderGIF, frameGrid } from './src/index.js';

const project = renderProject(recipe); // RGBA buffers, durations, warnings
const view = inspectProject(project, { grid: true }); // contact sheet RGBA, palette-key grids
const { recipe: next, edits } = patchRecipe(recipe, [{ set: 'frames[idle].duration', value: 120 }]);
const report = compareProjects(project, renderProject(next)); // changed pixels, timing, before/after RGBA
const bundle = await createBundle(recipe);
await writeBundle(bundle, './output/my-sprite');
const { recipe: effects } = compileEffects(fxSource); // seeded particles and spell shapes baked into an ordinary recipe
const { recipe: fire, metadata } = compileFluid(fluidSource); // a fluid simulation baked into palette grids
const { rgba, keys } = createLiveFluid(fluidSource).push({ x: 8, y: 20, vx: 2 }).step().render(); // the same simulation, live
const gif = animationGIF(project, 'idle', { scale: 8 }); // { data, width, height, notes, ... }
const loop = folderGIF('shots/prism', { fps: 25 }); // GIF from numbered PNG frames; framesGIF takes RGBA frames
const grid = frameGrid(project, 30); // durations off a 30 fps video frame grid

src/core.js, src/craft.js, src/font.js, src/patch.js, src/fx.js, src/fluid.js and src/gif.js are browser-compatible and have no Node imports. The Node-only exporter uses the standard library for compression and file output. The studio shares the same renderer as the CLI and MCP server.

Develop and verify

npm test
npm run demo

Tests cover pixels, alpha blending, inheritance, transformations, flood fill, text and the font, dither (including value noise), palette ramps and shading, outlines, rewrite rules, cleanup, rotation, tweens, particle effects, fluid simulation (golden recipes per preset, palette-only output, loop seams), tilemaps and rule tiles, scene cues, attachments and sorting, schema alignment, packing, timing, PNG/APNG structure, GIF structure and decoding, video frame timing and placement, inspection sheets and grids, patching and comparison, revisions, overwrite protection, CLI stdin/errors, MCP calls, the Canvas runtime, the local server and the portable Windows build. node scripts/qa-studio.mjs walks the studio in a real browser (an installed Edge or Chrome, headless, with a throwaway profile; Node.js 22+): every sample, live editing, a broken recipe, the preview controls, Save, Open, Export and GIF downloads landing on disk, a recipe that links a shared palette file, draft recovery, keyboard access, narrow and wide windows, and that no request leaves the computer. node scripts/verify-godot.mjs loads exported SpriteFrames in a headless Godot 4, when one is installed, and checks every frame's region, trim margin, pixels and timing and each animation's playback. Optional independent checks use Pillow and Python's ZIP reader: python scripts/verify-exports.py after npm run demo; it also decodes GIFs (including ones made from PNG folders) and an image sequence and compares every pixel, delay and loop flag, and runs the sequence's ffmpeg command when ffmpeg is installed.

The studio binds to 127.0.0.1, serves an explicit asset allowlist, rejects foreign Host/Origin headers, and never writes through its HTTP API. Unsaved edits are kept as a draft in the browser's own storage for the studio's address, one per tab, and offered back after a reload or, once that tab is closed, on the next visit; nothing about them is sent to the server, and Save JSON remains the way to keep a recipe. Browser storage is per address, so a draft made on one port is not seen on another. Projects are limited to 256×256 pixels, 256 frames, 4,194,304 source pixels, 16,777,216 atlas pixels and bounded drawing/export work. This is designed for small sprites, effects and tiles.

Pixelbolt Studio: portable builds

scripts/build-studio.mjs packs the toolkit, the official Node.js 24 LTS runtime and a few small launcher scripts into one archive per system, for people who have no Node.js, git or administrator rights: a ZIP for 64-bit Windows, and a .tar.gz that keeps execute permissions for Linux (x64 and ARM64) and macOS (Apple silicon and Intel). Nothing is installed, and deleting the folder removes everything.

Studio

Command line

Connect an agent

Windows

double-click Pixelbolt Studio.cmd, or drag a recipe onto it

pixelbolt.cmd

Connect your agent.cmd

macOS

double-click Pixelbolt Studio.command, or ./pixelbolt-studio [recipe]

./pixelbolt

Connect your agent.command or ./connect-your-agent

Linux

./pixelbolt-studio [recipe]

./pixelbolt

./connect-your-agent

The studio launcher is pixelbolt preview --open: it opens the default browser on loopback port 4747, or on any free port when that one is taken (the usual port is what lets the studio's draft recovery find its draft again). The connect helper prints MCP settings for Claude Code, Claude Desktop, Codex and Cursor with that folder's paths, plus a one-line claude mcp add and codex mcp add command written for that system's shells; the helper itself never edits a configuration file.

node scripts/build-studio.mjs                       # this machine's own target, into dist/
node scripts/build-studio.mjs --target all          # win-x64, linux-x64, linux-arm64, darwin-arm64, darwin-x64
node scripts/build-studio.mjs --target all --check  # rebuild and compare with the archives in dist/
node scripts/verify-studio.mjs dist/PixelboltStudio-<version>-<target>.zip   # or .tar.gz

Any machine can build any target; each archive comes with its .sha256 and a per-file manifest. An archive holds exactly the files npm pack ships (under app/), the runtime, and the files for its system from packaging: no demos, tests or development scripts. The runtimes are pinned in the build script by version and SHA-256, downloaded from nodejs.org on the first build (36 to 58 MB each, cached in dist/.cache; --node-archive <file> uses a copy you already have) and included unmodified; a mismatch stops the build. BUILD-INFO.txt inside names the Pixelbolt version, the source commit, the target and the runtime's hashes. A build refuses uncommitted changes to shipped files unless --allow-dirty marks it as a development build, and a release build takes every shipped file from the commit itself rather than the working copy. One commit gives the same file list and per-file hashes anywhere. Where a runtime is pinned for the building machine the packing runs under that runtime, and the archives then come out byte-identical whichever system builds them: every CI runner (Windows, Linux on x64 and ARM64, and macOS) builds all five and prints their hashes.

verify-studio.mjs extracts an archive with the system's own tool into a folder whose path has a space and a non-ASCII character, and runs it with no Node.js or git on PATH: the launcher (several copies, loopback only, every studio route, foreign Host and Origin refused, export, a recipe handed to it), the commands exactly as START HERE.txt gives them in two shells, the MCP server over stdio, the helper's output and its one-line commands, execute permissions and line endings, and that no process is left behind. It needs the system the archive is for; elsewhere it checks the contents only and says so. The Tests workflow builds, rebuilds and verifies each archive on a runner of its own system (the Intel Mac build through Rosetta on an Apple silicon runner), then runs the test suite on the bundled runtime and walks the extracted build's studio in the runner's browser. On the macOS runner the command line also runs with the download quarantine mark set. No runner has a person at the screen: the prompts Windows and macOS show for downloaded files, a double-click and a real browser are not exercised there.

Contributing

Bug reports, recipe examples, documentation improvements and focused pull requests are welcome. Read CONTRIBUTING.md and AGENTS.md before changing the toolkit. Report reproducible bugs through GitHub Issues.

Sibling tools

Pixelbolt is one of five free, MIT-licensed tools for agents by the same author. Each runs locally and works as an MCP server:

  • Anode: Windows desktop automation in a hidden background session, so an agent can click, type and take screenshots without taking over your screen.

  • Cutbolt: a video editing engine with exact timelines, captions and rendering.

  • Inkbolt: vector and raster image editing for diagrams and illustrations, with SVG import and SVG or PDF export.

  • Nestbolt: 2D nesting of parts on sheets for laser, CNC, plasma and waterjet cutting, also in your browser.

License

MIT licensed. Included artwork is original and covered by the same license.

Available Tools

8 tools
pixel_compileB
Idempotent

Build a character animation from parts (with idle, walk, run, jump, attack and hurt templates, squash and tweens), a terrain tileset, particle effects (sparks, embers, dust) and spell shapes (slashes, shockwave rings, beams, lightning, magic circles, impact stars, fireballs, cel water streams and drops), simulated smoke, fire, explosions, steam, magic swirls, water (splashes, waterfalls, drips, bubbling potions) and thick liquids (honey, slime, mud), coherent interference and portal loops, or a lighting normal map. Compiles the source into an ordinary recipe revision and returns a contact sheet; metadata: true adds pose joints, contacts and timing, the autotile mask table, particle counts, fluid figures, wave counts or normal directions.

NameRequiredDescriptionDefault
sourceYesA pixelforge-poses, pixelforge-autotile, pixelforge-fx, pixelforge-fluid, pixelforge-wave or pixelforge-normals document (see pixel_help topics). A normals source's recipe may be inline, a file under the server root or {"revision": id}.
metadataNo

TDQS

B3.1/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare idempotent=true, destructive=false and readOnly=false, so the bar is lower. The description still adds real value: it discloses the output artifact (a contact sheet), that compilation persists an 'ordinary recipe revision', and precisely what the metadata flag surfaces (pose joints, contacts, timing, autotile mask table, particle counts, fluid figures, wave counts, normal directions). It stops short of failure modes or cost, but the additions are substantive.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single sprawling run-on sentence whose operative clause ('Compiles the source...') is buried past a long comma-separated catalogue of particles, spells and fluids. The enumeration is not front-loaded and much of it duplicates what the source schema already implies about document families. It is under-structured rather than merely long.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema and a nested object parameter, the description does supply the return shape (contact sheet plus optional metadata) and points to pixel_help for document formats, which keeps it viable. But for a compile step it says nothing about validation behavior, when it fails, or whether output is rendered or just a sheet, leaving an agent uncertain about sequencing with pixel_validate and pixel_render.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

At 50% schema coverage the description must compensate, and it does for the undocumented boolean: it enumerates exactly what metadata=true returns for each source type. The source parameter is already well covered by the schema (document families, inline/file/revision forms), so the description need not repeat it. Minor gap: no per-source-type constraints on what a valid source must contain.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The actual action ('compiles the source into an ordinary recipe revision and returns a contact sheet') appears only after four lines of asset-type enumeration, so the verb+resource is real but buried. It never distinguishes itself from siblings like pixel_validate, pixel_inspect or pixel_render, and the opening 'Build a character animation...' line frames it as a content generator rather than a compiler. An agent can eventually infer the purpose, but the noisy feature dump dilutes it.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no statement of when to use this versus pixel_validate (check first?), pixel_inspect (what would it return?), or pixel_render (render output vs compiled revision). The only routing hint is a parenthetical 'see pixel_help topics' inside the schema, which points at documentation, not at a sibling tool choice. No prerequisites or exclusions are given.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

pixel_helpA
Read-onlyIdempotent

Start here before drawing pixel art, sprites, tiles, icons, effects or animations. Returns a short quick start: the recipe format, the operations, the tools and the workflow. Pass a topic only when you need more: guide (the full authoring reference and every tool option), workflow (art workflow: poses, scenes, particles, fluids, lighting), schema (the recipe JSON Schema), example (a complete recipe), or poses, scenes, autotile, fx, fluid, wave or normals for those source formats' schemas.

NameRequiredDescriptionDefault
topicNo

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, and non-destructive behavior, so safety is covered. The description adds useful behavioral detail the annotations do not: the default response is short, and per-topic responses are larger, deeper resources. It does not specify the response format (text vs. structured), which is the remaining gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, both front-loaded: the first states purpose and timing, the second states the escalation path and enumerates topics. No filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single optional enum parameter with no output schema, the description covers purpose, timing, and the meaning of every enum value. The only missing piece is what the returned content looks like (text guide vs. JSON), which an agent would benefit from knowing before invoking.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, and the enum has no per-value documentation, so the description must carry that load. It does: it explains guide, workflow, schema, example, and groups the source-format schemas (poses, scenes, autotile, fx, fluid, wave, normals). The grouped values are only implied rather than individually described, which keeps this just short of a 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (returns a quick start) plus concrete resources (recipe format, operations, tools, workflow), and names the exact topics available. It is clearly distinguishable from all siblings (validate, inspect, patch, render, compile, scene, import), which are action tools rather than documentation tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

"Start here before drawing pixel art..." gives an explicit entry-point instruction, and "Pass a topic only when you need more" gives an explicit condition for using the optional parameter versus the default. No alternatives or exclusions are needed here because this is the entry tool by design.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

pixel_importA
Idempotent

Turn existing pixel art or a sprite sheet PNG into an editable recipe. Import a native-resolution 8-bit RGB/RGBA PNG (optionally sliced by an unscaled atlas) as an editable recipe without changing any pixel. Pass data (base64 PNG) or path (inside the server root). Saves the recipe as a revision.

NameRequiredDescriptionDefault
dataNoBase64 PNG bytes.
nameNo
pathNoPNG file path relative to the server root.
atlasNo

TDQS

A3.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations already declare non-read-only, idempotent, non-destructive, and closed-world behavior. The description adds meaningful context beyond that: it preserves pixels exactly ('without changing any pixel') and saves the result as a revision.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded and reasonably sized, with the core import action stated first. There is minor redundancy in repeating 'editable recipe', but no wasted structure.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a four-parameter import tool with a nested atlas object and no output schema, the description is adequate for understanding the main operation. It remains incomplete about the atlas structure, the name parameter, and what the saved revision result looks like.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 50%. The description clarifies that either base64 data or a server-relative path may be passed and that the atlas is optional and unscaled, but it does not explain the nested atlas object or the name parameter, and the data/path details largely repeat the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb and resource: turning an existing PNG or sprite sheet into an editable recipe. It clearly states the import-and-convert outcome, though it does not explicitly differentiate the tool from siblings such as pixel_render or pixel_compile.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by 'Turn existing pixel art or a sprite sheet PNG into an editable recipe' and by the native-resolution 8-bit RGB/RGBA constraint. However, it does not explicitly say when to choose this over related tools or describe exclusions and prerequisites.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

pixel_inspectA
Idempotent

Look at pixel art, a sprite or an animation frame by frame to check how it looks, without exporting. Returns a PNG contact sheet of every frame (or the chosen frames, or one animation in playback order) with each cell's name and duration. grid: true adds palette-key text grids for exact pixels, region zooms in, and view picks a tile, onion or motion review. Use this while iterating; call pixel_render to export.

NameRequiredDescriptionDefault
gridNoAlso return palette-key text grids with x/y rulers (at most 16,384 pixels).
stepNoKeep every step-th cell from offset; step 2 shows the A frames of an A/B strobe.
viewNoonion and motion need animation; motion reports foot slide, ground error and loop joins (source or metadata adds contacts); tile repeats each frame 3x3.
scaleNoContact sheet scale. Default: the largest that fits about 1024 px.
speedNoMotion view: pixels per second.
framesNoFrame names to show, in order; * and ? match, as in "flash-*". Default: every frame.
groundNoMotion view: ground row.
guidesNoPerspective guides: {horizon: row, vanish: [[x, y], …up to 3], iso: {tile: [32, 16], at: [x, y]}}.
layersNoShow only these named layers.
nativeNoAlso return a 1x contact sheet.
offsetNo
regionNoCrop every frame to this canvas rectangle.
sourceNoA pixelforge-poses source instead of project/revision, for motion review with its contacts.
projectNoA version-1 Pixelbolt recipe: {"version": 1, "name", "width", "height", "palette", "symbols"?, "frames": [{"name", "duration"?, "from"?, "ops"?, "layers"?}], "animations"?}; see pixel_help.
maxCellsNoShow at most this many evenly spaced cells; step and offset pick positions instead.
metadataNoPose metadata from pixel_compile (metadata: true), for motion review of a compiled revision.
revisionNoRevision id from an earlier response, sent instead of project.
animationNoShow one animation in playback order instead of frames.
referenceNoRevision to compare against: adds a before/after image and the changed pixels.
backgroundNochecker (default), transparent, a palette name or a hex color.
diagnosticsNoAdvisory evidence: duplicate and empty frames, stray pixels, palette, loop joins, timing, cleanup counts.

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description discloses key behavior beyond annotations: it returns a PNG contact sheet, includes frame names and durations, supports grid and region outputs, and changes behavior by view. It does not reconcile the non-read-only annotation for an inspection tool, but it also does not explicitly claim read-only behavior, so there is no direct contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three tight sentences with the core action and output front-loaded. Each sentence contributes useful context without waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex 21-parameter tool with no output schema, the description covers the main output format, frame/animation selection, grid and region behavior, and the iteration versus export distinction. It omits details on several optional parameters, but the high schema coverage compensates for most of that.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 95%, so the schema already documents nearly every parameter in detail. The description highlights a few parameters such as grid, region, and view, but largely restates schema-provided semantics rather than adding substantial new meaning.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: 'Look at pixel art, a sprite or an animation frame by frame to check how it looks.' It also distinguishes the inspection use from export by naming pixel_render as the export path.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives a clear usage context ('Use this while iterating') and an explicit alternative for exporting ('call pixel_render to export'). It does not spell out when-not-to-use cases beyond that export distinction, so it is strong but not exhaustive.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

pixel_patchA
Idempotent

Fix or tweak pixel art (one pixel, a colour, a frame or its timing) without redrawing it. Pass revision or project plus changes applied in order, such as {"set": "frames[blink].duration", "value": 120} or {"paint": "frames[blink]", "value": [{"x": 9, "y": 7, "color": "k"}]}; other paths look like palette.k or frames[blink].layers[body].ops[2].x2. The patch is atomic and must validate. Returns the new revision, every frame whose pixels changed and a before/after PNG.

NameRequiredDescriptionDefault
changesNoEdits applied in order. Paths match error paths, such as frames[3].duration; lists also accept [name] and [-] to append.
overlayNoA saved pixelforge-overlay to apply instead of changes.
projectNoA version-1 Pixelbolt recipe: {"version": 1, "name", "width", "height", "palette", "symbols"?, "frames": [{"name", "duration"?, "from"?, "ops"?, "layers"?}], "animations"?}; see pixel_help.
revisionNoRevision id from an earlier response, sent instead of project.

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Adds substantial traits beyond the annotations: the patch is atomic, must validate, and returns the new revision plus every frame whose pixels changed and a before/after PNG. That is real behavioral context (failure mode, idempotency backing, return payload) rather than restatement of the hints.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded purpose sentence, then concrete examples, then the contract sentence about atomicity and returns. Dense but every clause carries information; the inline JSON examples cost some readability but buy real clarity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 4-parameter, nested-object mutation tool with no output schema, the description covers the edit grammar, atomicity, and return values, which is most of what an agent needs. The gap is 'overlay' being completely unaddressed and the revision-vs-project exclusivity being implied rather than stated.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema coverage the baseline is 3, but the description earns an extra point by showing the change-object DSL in use ({"set": "frames[blink].duration", "value": 120}) and the path grammar (palette.k, frames[blink].layers[body].ops[2].x2), which clarifies ordering and addressing intent beyond the per-key schema blurbs. It omits any mention of the overlay parameter's role.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (fix/tweak) on a specific resource (pixel art: one pixel, a colour, a frame or its timing) and draws the boundary against the sibling that redraws ('without redrawing it'). An agent can distinguish this from pixel_compile or pixel_scene without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives the prerequisite shape ('Pass revision or project plus changes applied in order') and the operative condition of when it applies, i.e. small edits that must not trigger a redraw. It never names an alternative sibling (pixel_compile) or an explicit when-not, so it stops short of a full routing rule.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

pixel_renderA
Destructive

Export pixel art as game-ready files: sprite sheet, sprites, animations, GIFs or frames for a video. Writes a PNG sheet, atlas JSON, Godot 4 SpriteFrames, frame PNGs, APNGs, CSS and a Canvas player to a new folder in the export directory, and returns the key files, a check of what was delivered and a contact sheet. Pass expect with what the user asked for, then fix every problem the check lists before handing the files on. Pass saveAs to also write files where the user asked.

NameRequiredDescriptionDefault
gifNoAlso write each animation as a GIF in gifs/ ({} for defaults): exact colours, at most 256 per frame.
shareNoAlso return a playground link that reopens this recipe in a browser.
expectNoWhat the user asked for: frame size, frames per animation, at most colors, a transparent background, loop, GIF scale. The reply's check lists each miss and the call that fixes it.
saveAsNoAlso write files where the user asked, relative to the server root: {"sheet"|"atlas"|"godot"|"recipe"|"gif"|"apng" or a listed file such as "gifs/walk.gif": "assets/hero.png"}. A GIF here implies gif: {}.
projectNoA version-1 Pixelbolt recipe: {"version": 1, "name", "width", "height", "palette", "symbols"?, "frames": [{"name", "duration"?, "from"?, "ops"?, "layers"?}], "animations"?}; see pixel_help.
revisionNoRevision id from an earlier response, sent instead of project.
sequenceNoAlso write numbered PNG frames with alpha for a video editor into sequence/, with the ffmpeg command for a .mov.
animationNoPreview this animation in playback order; every animation is exported either way.
listFilesNoList every exported file, not only the key ones.
overwriteNoLet saveAs replace existing files. Default: refuse.
embedRecipeNoEmbed the recipe in the exported PNGs so they reopen in the playground.

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare destructiveHint=true and idempotentHint=false, and the description earns credit by specifying that output goes to a new folder in the export directory (limiting overwrite risk) and that it returns key files, a delivery check, and a contact sheet. The overwrite-refusal default is only in the schema, not repeated here, and no rate/permission context is given, keeping it at 4.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the deliverable list, then the expect/check/fix workflow, then the saveAs note — a sensible order with no filler sentences. Slightly dense with enumerations, but every clause conveys an output type or an action the agent must take.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With 11 parameters, nested objects, and no output schema, the description compensates by naming what the call returns (key files, delivery check, contact sheet) and how to respond to the check. Combined with 100% schema coverage and populated annotations, an agent has enough to call this correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and the nested gif, sequence, expect, and saveAs descriptions already carry rich semantics, so the baseline is 3. The prose mainly restates expect and saveAs and adds the workflow instruction to act on the check output, but contributes no new syntax or format detail for the remaining parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('Export') and resource ('pixel art as game-ready files') and enumerates the concrete artifacts produced: sprite sheet, atlas JSON, Godot 4 SpriteFrames, frame PNGs, APNGs, CSS, Canvas player. That enumeration also distinguishes it from siblings like pixel_compile or pixel_scene, which produce intermediate representations rather than deliverables.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear workflow context: pass expect with what the user asked for, then fix every problem the check lists before handing files on, and pass saveAs to also write where the user asked. It does not name an explicit alternative tool or a when-not-to-use condition, but the sequencing guidance is concrete enough to guide invocation.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

pixel_sceneA

Compose a pixel art scene or room from sprites, tilemaps and lighting. Renders a pixelforge-scene manifest (placements, autotiled tilemaps, cues, motion, ground sorting, camera and parallax, haze, lights and shadows; pixel_help topic scenes) at a time in milliseconds and returns the image, placements, lights and warnings. Assets may be inline recipes, files under the server root or {"revision": id}. view: depth shows who covers whom; export: true writes a replayable scene bundle.

NameRequiredDescriptionDefault
litNo
timeNo
viewNo
scaleNo
sceneYes
exportNo
guidesNoPerspective guides: {horizon: row, vanish: [[x, y], …up to 3], iso: {tile: [32, 16], at: [x, y]}}.
densityNo
sequenceNoAlso write numbered PNG frames for a video editor: pixel_render's sequence options, without animation and step.

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare destructiveHint=false and idempotentHint=false, so the safety bar is partly covered; the description earns credit beyond that by disclosing side effects annotations don't capture — export:true writes a replayable scene bundle, sequence writes numbered PNG frames, timing is reported in milliseconds, and warnings are returned alongside the image. It stops short of saying where bundles/frames are written or what failures look like.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, front-loaded with the verb and resource, with the parenthetical enumerating exactly what a manifest may contain. It is dense but nearly every clause carries signal; the 'pixel_help topic scenes' aside is the one item that could be trimmed or moved.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a high-complexity tool with nested objects, no output schema and low schema coverage, the description does well on the scene manifest and outputs (image, placements, lights, warnings) but leaves three parameters and the practical workflow (how a revision-based asset resolves, what a scene bundle contains) undocumented.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 9 parameters and only 22% schema description coverage, the description must compensate — and it does for scene (enumerating placements, autotiling, cues, motion, sorting, parallax, lights), view: depth ('shows who covers whom'), export, and time units. But lit, scale and density get no explanation anywhere, leaving a real gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource — 'Compose a pixel art scene or room from sprites, tilemaps and lighting' — and then names the actual artifact produced (a pixelforge-scene manifest with placements, tilemaps, camera, haze, lights). It also implicitly separates itself from pixel_render by referencing 'pixel_render's sequence options', so an agent can place it in the sibling set without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied rather than stated: the description tells you what inputs are acceptable (inline recipes, files under the server root, or {"revision": id}) and points to pixel_help topic scenes, but never says when to choose this over pixel_render, pixel_compile or pixel_validate, nor any precondition or exclusion.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

pixel_validateA
Idempotent

Check a pixel art recipe for mistakes before inspecting or exporting it. Returns its revision id, size, frame timing, animations and clipping, without images or files.

NameRequiredDescriptionDefault
fpsNoAlso check timing against a video frame rate (24, 25, "29.97", 30, 60 or a fraction): frameGrid lists durations off the grid.
projectNoA version-1 Pixelbolt recipe: {"version": 1, "name", "width", "height", "palette", "symbols"?, "frames": [{"name", "duration"?, "from"?, "ops"?, "layers"?}], "animations"?}; see pixel_help.
revisionNoRevision id from an earlier response, sent instead of project.

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no output schema, the description carries the useful load of disclosing what comes back: revision id, size, frame timing, animations and clipping, and that no images or files are returned. It does not reconcile the non-read-only annotation (readOnlyHint=false) with what appears to be a check operation, leaving the persistence side-effect undefined.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two tight sentences with the purpose front-loaded and the return-surface summary second. Nothing is redundant and no sentence could be dropped without losing information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-required-parameter tool with a nested recipe object and no output schema, the description covers purpose, workflow placement and return contents adequately. The remaining gap is behavioral: it never says whether validating persists or mutates anything, which matters given readOnlyHint=false.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, with detailed descriptions for fps, the nested project recipe and the revision id alternative, so the schema does the heavy lifting. The description adds only an indirect hint ('its revision id') linking the revision parameter to a prior response; the fps grid-checking behavior lives entirely in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb and resource ('Check a pixel art recipe for mistakes') and scopes it to a pre-inspection/pre-export gate, which separates it from siblings like pixel_render and pixel_compile. It stops short of naming those siblings directly, so the differentiation is suggestive rather than explicit.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Before inspecting or exporting it' states the point in the workflow where this tool applies, giving the agent a clear usage context. It does not name the alternative tools or call out when validation can or should be skipped, so it is clear but not exhaustive.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 7 tool updatesv0.9.2
    • Changedpixel_compile1 field changed
      • changedInput schema / properties / source / description
        Previous value: -"A pixelforge-poses, pixelforge-autotile, pixelforge-fx, pixelforge-fluid or pixelforge-normals document (see pixel_help topics). A normals source's recipe may be inline, a file under the server root or {\"revision\": id}."New value: +"A pixelforge-poses, pixelforge-autotile, pixelforge-fx, pixelforge-fluid, pixelforge-wave or pixelforge-normals document (see pixel_help topics). A normals source's recipe may be inline, a file under the server root or {\"revision\": id}."
    • Changedpixel_help1 field changed
      • changedInput schema / properties / topic / enum
        Previous value: -[
        -  "guide",
        -  "workflow",
        -  "schema",
        -  "example",
        -  "poses",
        -  "scenes",
        -  "autotile",
        -  "fx",
        -  "fluid",
        -  "normals"
        -]New value: +[
        +  "guide",
        +  "workflow",
        +  "schema",
        +  "example",
        +  "poses",
        +  "scenes",
        +  "autotile",
        +  "fx",
        +  "fluid",
        +  "wave",
        +  "normals"
        +]
    • Changedpixel_inspect24 fields changed
      • changedInput schema / properties / background / description
        Previous value: -"Cell background: checker (default), transparent, a palette name or a hex color."New value: +"checker (default), transparent, a palette name or a hex color."
      • changedInput schema / properties / diagnostics / description
        Previous value: -"Advisory duplicate, empty, isolated-pixel, palette and loop-boundary evidence, original animation timing, and per-frame counts of pixels a cleanup change would alter (corners, strays)."New value: +"Advisory evidence: duplicate and empty frames, stray pixels, palette, loop joins, timing, cleanup counts."
      • changedInput schema / properties / frames / description
        Previous value: -"Frame names to show, in this order. * (any characters) and ? (one character) in a name select every matching frame in project order, such as \"flash-*\". Default: every frame."New value: +"Frame names to show, in order; * and ? match, as in \"flash-*\". Default: every frame."
      • changedInput schema / properties / grid / description
        Previous value: -"Also return each frame as palette-key text rows with x/y rulers. At most 16,384 pixels in total."New value: +"Also return palette-key text grids with x/y rulers (at most 16,384 pixels)."
      • changedInput schema / properties / ground / description
        Previous value: -"Motion view: ground pixel row; overrides metadata.rig.ground."New value: +"Motion view: ground row."
      • addedInput schema / properties / guides
        Added value: +{
        +  "description": "Perspective guides: {horizon: row, vanish: [[x, y], …up to 3], iso: {tile: [32, 16], at: [x, y]}}.",
        +  "type": "object"
        +}
      • changedInput schema / properties / layers / description
        Previous value: -"Isolate named layers, preserving inheritance/transforms/visibility. Omits frame ops, background and final canvas corrections, which do not belong to a named layer."New value: +"Show only these named layers."
      • changedInput schema / properties / maxCells / description
        Previous value: -"Bound the preview with evenly spaced samples; returns omitted count and original positions. Does not alter exported animation. Evenly spaced samples can alias with a repeating pattern; use step and offset to choose positions."New value: +"Show at most this many evenly spaced cells; step and offset pick positions instead."
      • changedInput schema / properties / metadata / description
        Previous value: -"Pose metadata returned by pixel_compile with metadata:true. Needed with a compiled project/revision for contacts and joint angles; a source supplies its own."New value: +"Pose metadata from pixel_compile (metadata: true), for motion review of a compiled revision."
      • changedInput schema / properties / native / description
        Previous value: -"Also return a native-size contact sheet beside the enlarged view."New value: +"Also return a 1x contact sheet."
      • removedInput schema / properties / offset / description
        Removed value: -"First position the step keeps, counting from 0. Default 0; offset 1 with step 2 shows the B frames."
      • changedInput schema / properties / project / description
        Previous value: -"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}. pixel_help explains it; its topic schema is the full JSON Schema. Mistakes come back with their exact path."New value: +"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}; see pixel_help."
      • changedInput schema / properties / reference / description
        Previous value: -"Saved revision to compare against at the same origin. Adds a before/after image and changed-pixel report."New value: +"Revision to compare against: adds a before/after image and the changed pixels."
      • changedInput schema / properties / region / description
        Previous value: -"Crop every frame to this canvas rectangle. Smaller regions are shown larger."New value: +"Crop every frame to this canvas rectangle."
      • changedInput schema / properties / region / properties / h / maximum
        Previous value: -256New value: +4096
      • changedInput schema / properties / region / properties / w / maximum
        Previous value: -256New value: +4096
      • changedInput schema / properties / region / properties / x / maximum
        Previous value: -255New value: +4095
      • changedInput schema / properties / region / properties / y / maximum
        Previous value: -255New value: +4095
      • changedInput schema / properties / revision / description
        Previous value: -"Revision id from an earlier Pixelbolt response. Send it instead of project to reuse that recipe, including after restarting with the same MCP --out directory."New value: +"Revision id from an earlier response, sent instead of project."
      • changedInput schema / properties / scale / description
        Previous value: -"Contact sheet scale. Default: the largest that fits about 1024px."New value: +"Contact sheet scale. Default: the largest that fits about 1024 px."
      • changedInput schema / properties / source / description
        Previous value: -"A pixelforge-poses source, instead of project/revision; compiles it with contact and joint metadata for motion review."New value: +"A pixelforge-poses source instead of project/revision, for motion review with its contacts."
      • changedInput schema / properties / speed / description
        Previous value: -"Motion view: horizontal speed in pixels per second."New value: +"Motion view: pixels per second."
      • changedInput schema / properties / step / description
        Previous value: -"Keep every step-th cell of the selection (frames or animation positions), starting at offset: step 2 shows only the A frames of an A/B strobe. Applied before maxCells."New value: +"Keep every step-th cell from offset; step 2 shows the A frames of an A/B strobe."
      • changedInput schema / properties / view / description
        Previous value: -"Onion uses pink previous/cyan next poses in playback order. Motion requires animation and reports slide, signed ground error, joint seam and pose spread, with native and enlarged trajectory/timing strips. Pass source or compile metadata to measure contacts. Tile repeats each frame 3x3."New value: +"onion and motion need animation; motion reports foot slide, ground error and loop joins (source or metadata adds contacts); tile repeats each frame 3x3."
    • Changedpixel_patch10 fields changed
      • changedInput schema / properties / changes / items / properties / cleanup / description
        Previous value: -"Frame path. Value: {corners?, strays?, x?, y?, w?, h?, mask?, colors?}. Proposes fixes as canvas corrections: corners removes doubled L-shaped steps in one-pixel lines; strays gives a pixel the colour all eight neighbours share (noise, pinholes, isolated specks). Check the report and keep deliberate corners, eyes and sparks. Inspection diagnostics count both per frame."New value: +"Frame path; value: {corners?, strays?, x?, y?, w?, h?, mask?, colors?}. Proposes fixes for doubled corners and stray pixels; check the report."
      • changedInput schema / properties / changes / items / properties / grid / description
        Previous value: -"Frame path. Value: {x,y,rows,erase?,mask?}. Dots/spaces preserve; explicit erase character clears; mask x selects and dot preserves."New value: +"Frame path; value: {x, y, rows, erase?, mask?}, dots keep pixels."
      • changedInput schema / properties / changes / items / properties / move / description
        Previous value: -"Frame path. Value: {x,y,w,h,dx,dy,mask?}. Moves exact pixels including corrections; erases selected source. Destination must fit."New value: +"Frame path; value: {x, y, w, h, dx, dy, mask?}."
      • changedInput schema / properties / changes / items / properties / paint / description
        Previous value: -"Frame path, such as frames[blink]. Replaces pixels in final canvas coordinates, after all layers, preserving source operations. Use transparent to erase."New value: +"Frame path such as frames[blink]; value: [{x, y, color}] canvas pixels after all layers (transparent erases)."
      • changedInput schema / properties / changes / items / properties / recolor / description
        Previous value: -"Frame path. Value: {x,y,w,h,from,to,mask?}. Replaces only matching selected colors."New value: +"Frame path; value: {x, y, w, h, from, to, mask?}."
      • changedInput schema / properties / changes / items / properties / scope / description
        Previous value: -"Canvas edits: inherited is default. frame preserves other poses by recording compensating canvas corrections, reported in edits.protected."New value: +"Canvas edits: inherited (default), or frame to leave frames that inherit it unchanged."
      • removedInput schema / properties / changes / items / properties / value / description
        Removed value: -"JSON value for set or insert; for paint, a list of {x, y, color} pixels inside the canvas (palette names or hex colors, exact RGBA replacement)."
      • changedInput schema / properties / overlay / description
        Previous value: -"Apply a saved pixelforge-overlay instead of changes. Canvas-only overlays reapply when the frames they touch are unchanged (reported as rebased)."New value: +"A saved pixelforge-overlay to apply instead of changes."
      • changedInput schema / properties / project / description
        Previous value: -"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}. pixel_help explains it; its topic schema is the full JSON Schema. Mistakes come back with their exact path."New value: +"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}; see pixel_help."
      • changedInput schema / properties / revision / description
        Previous value: -"Revision id from an earlier Pixelbolt response. Send it instead of project to reuse that recipe, including after restarting with the same MCP --out directory."New value: +"Revision id from an earlier response, sent instead of project."
    • Changedpixel_render21 fields changed
      • changedInput schema / properties / animation / description
        Previous value: -"Preview this animation in playback order. Default: preview every frame in project order. All animations are always exported."New value: +"Preview this animation in playback order; every animation is exported either way."
      • changedInput schema / properties / expect / description
        Previous value: -"What the user asked for: pixel size of each frame, frames, colours, background, loop, GIF scale. The reply's check lists every file that misses it and the call that fixes it."New value: +"What the user asked for: frame size, frames per animation, at most colors, a transparent background, loop, GIF scale. The reply's check lists each miss and the call that fixes it."
      • removedInput schema / properties / expect / properties / colors / description
        Removed value: -"At most."
      • removedInput schema / properties / expect / properties / frames / description
        Removed value: -"Per animation."
      • removedInput schema / properties / expect / properties / transparent / description
        Removed value: -"Background."
      • changedInput schema / properties / gif / description
        Previous value: -"Also write every animation as a GIF in gifs/ for sharing ({} for defaults). Exact colours, at most 256 per frame, never quantized; notes report alpha and timing changes."New value: +"Also write each animation as a GIF in gifs/ ({} for defaults): exact colours, at most 256 per frame."
      • changedInput schema / properties / gif / properties / background / description
        Previous value: -"Palette name or opaque hex colour to blend onto. Default: 1-bit transparency."New value: +"Palette name or opaque hex to blend onto. Default: 1-bit transparency."
      • changedInput schema / properties / gif / properties / scale / description
        Previous value: -"Default: 1, the frames' own size. Enlarge (4 or 8) only for posting where a small sprite would look tiny."New value: +"Default 1, the frames' own size; enlarge only for posting."
      • changedInput schema / properties / listFiles / description
        Previous value: -"List every exported file. Default: key files plus frame/animation counts."New value: +"List every exported file, not only the key ones."
      • changedInput schema / properties / project / description
        Previous value: -"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}. pixel_help explains it; its topic schema is the full JSON Schema. Mistakes come back with their exact path."New value: +"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}; see pixel_help."
      • changedInput schema / properties / revision / description
        Previous value: -"Revision id from an earlier Pixelbolt response. Send it instead of project to reuse that recipe, including after restarting with the same MCP --out directory."New value: +"Revision id from an earlier response, sent instead of project."
      • changedInput schema / properties / saveAs / description
        Previous value: -"Also write files where the user asked, relative to the server root: keys sheet, atlas, recipe, gif or apng (one animation), or a listed file such as gifs/walk.gif; values such as \"assets/hero.png\". A GIF here implies gif: {}."New value: +"Also write files where the user asked, relative to the server root: {\"sheet\"|\"atlas\"|\"godot\"|\"recipe\"|\"gif\"|\"apng\" or a listed file such as \"gifs/walk.gif\": \"assets/hero.png\"}. A GIF here implies gif: {}."
      • changedInput schema / properties / sequence / description
        Previous value: -"Also write a numbered PNG sequence for a video editor into sequence/: one PNG per video frame with alpha, plus sequence.json and a README with the ffmpeg command for a .mov. The result reports timing changes and whether a loop closes on the frame grid."New value: +"Also write numbered PNG frames with alpha for a video editor into sequence/, with the ffmpeg command for a .mov."
      • changedInput schema / properties / sequence / properties / background / description
        Previous value: -"Palette name or opaque hex colour. Default: transparent."New value: +"Palette name or opaque hex. Default: transparent."
      • removedInput schema / properties / sequence / properties / fit / description
        Removed value: -"contain (default): the whole sprite fits the canvas. cover: the smallest scale that covers the canvas; the overhang is cropped."
      • changedInput schema / properties / sequence / properties / fps / description
        Previous value: -"Frame rate: 24, 25, 30, 60, or \"23.976\", \"29.97\", \"59.94\" (exact n000/1001), or a fraction such as \"30000/1001\"."New value: +"24, 25, 30, 60, \"23.976\", \"29.97\", \"59.94\" or a fraction such as \"30000/1001\"."
      • changedInput schema / properties / sequence / properties / offset / description
        Previous value: -"[x, y] canvas pixels added to the aligned position."New value: +"[x, y] added to the aligned position."
      • removedInput schema / properties / sequence / properties / scale / description
        Removed value: -"Whole-number enlargement. Default: the largest that fits the canvas, or 1 without a size."
      • changedInput schema / properties / sequence / properties / seconds / description
        Previous value: -"Length of the clip instead of loops."New value: +"Clip length instead of loops."
      • changedInput schema / properties / sequence / properties / size / description
        Previous value: -"Canvas: 720p, 1080p, 1440p, 4k, vertical, square or WIDTHxHEIGHT. Default: the sprite at scale."New value: +"720p, 1080p, 1440p, 4k, vertical, square or WIDTHxHEIGHT. Default: the sprite at scale."
      • changedInput schema / properties / sequence / properties / step / description
        Previous value: -"Hold each sampled frame for this many video frames; 2 animates on twos."New value: +"Video frames per sampled frame; 2 animates on twos."
    • Changedpixel_scene5 fields changed
      • addedInput schema / properties / guides
        Added value: +{
        +  "description": "Perspective guides: {horizon: row, vanish: [[x, y], …up to 3], iso: {tile: [32, 16], at: [x, y]}}.",
        +  "type": "object"
        +}
      • removedInput schema / properties / sequence / additionalProperties
        Removed value: -false
      • changedInput schema / properties / sequence / description
        Previous value: -"Also write a numbered PNG sequence for a video editor into sequence/: one PNG per video frame with alpha, plus sequence.json and a README with the ffmpeg command for a .mov. The result reports timing changes and whether a loop closes on the frame grid."New value: +"Also write numbered PNG frames for a video editor: pixel_render's sequence options, without animation and step."
      • removedInput schema / properties / sequence / properties
        Removed value: -{
        -  "align": {
        -    "enum": [
        -      "top-left",
        -      "top",
        -      "top-right",
        -      "left",
        -      "center",
        -      "right",
        -      "bottom-left",
        -      "bottom",
        -      "bottom-right"
        -    ]
        -  },
        -  "background": {
        -    "description": "Palette name or opaque hex colour. Default: transparent.",
        -    "type": "string"
        -  },
        -  "fit": {
        -    "description": "contain (default): the whole sprite fits the canvas. cover: the smallest scale that covers the canvas; the overhang is cropped.",
        -    "enum": [
        -      "contain",
        -      "cover"
        -    ]
        -  },
        -  "fps": {
        -    "description": "Frame rate: 24, 25, 30, 60, or \"23.976\", \"29.97\", \"59.94\" (exact n000/1001), or a fraction such as \"30000/1001\".",
        -    "type": [
        -      "number",
        -      "string"
        -    ]
        -  },
        -  "loops": {
        -    "maximum": 1000,
        -    "minimum": 1,
        -    "type": "integer"
        -  },
        -  "offset": {
        -    "description": "[x, y] canvas pixels added to the aligned position.",
        -    "items": {
        -      "type": "integer"
        -    },
        -    "maxItems": 2,
        -    "minItems": 2,
        -    "type": "array"
        -  },
        -  "scale": {
        -    "description": "Whole-number enlargement. Default: the largest that fits the canvas, or 1 without a size.",
        -    "maximum": 256,
        -    "minimum": 1,
        -    "type": "integer"
        -  },
        -  "seconds": {
        -    "description": "Length of the clip instead of loops.",
        -    "exclusiveMinimum": 0,
        -    "maximum": 3600,
        -    "type": "number"
        -  },
        -  "size": {
        -    "description": "Canvas: 720p, 1080p, 1440p, 4k, vertical, square or WIDTHxHEIGHT. Default: the sprite at scale.",
        -    "type": "string"
        -  }
        -}
      • changedInput schema / properties / view / enum
        Previous value: -[
        -  "color",
        -  "grayscale"
        -]New value: +[
        +  "color",
        +  "grayscale",
        +  "depth"
        +]
    • Changedpixel_validate3 fields changed
      • changedInput schema / properties / fps / description
        Previous value: -"Also check timing against a video frame rate (24, 25, 29.97, 30, 60 or a fraction): frameGrid lists frame durations that are not whole video frames, positions a sequence would lengthen, shorten or never show, and whether each loop closes on the grid."New value: +"Also check timing against a video frame rate (24, 25, \"29.97\", 30, 60 or a fraction): frameGrid lists durations off the grid."
      • changedInput schema / properties / project / description
        Previous value: -"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}. pixel_help explains it; its topic schema is the full JSON Schema. Mistakes come back with their exact path."New value: +"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}; see pixel_help."
      • changedInput schema / properties / revision / description
        Previous value: -"Revision id from an earlier Pixelbolt response. Send it instead of project to reuse that recipe, including after restarting with the same MCP --out directory."New value: +"Revision id from an earlier response, sent instead of project."
  2. 5 tool updatesv0.9.1
    • Changedpixel_help1 field changed
      • changedInput schema / properties / topic / enum
        Previous value: -[
        -  "poses",
        -  "scenes",
        -  "autotile",
        -  "fx",
        -  "fluid",
        -  "normals"
        -]New value: +[
        +  "guide",
        +  "workflow",
        +  "schema",
        +  "example",
        +  "poses",
        +  "scenes",
        +  "autotile",
        +  "fx",
        +  "fluid",
        +  "normals"
        +]
    • Changedpixel_inspect12 fields changed
      • removedInput schema / $defs
        Removed value: -{
        -  "frame": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "anchor": {
        -        "description": "[x, y] in unscaled canvas pixels; the top-left corner of that pixel.",
        -        "items": {
        -          "maximum": 4096,
        -          "minimum": -4096,
        -          "type": "integer"
        -        },
        -        "maxItems": 2,
        -        "minItems": 2,
        -        "type": "array"
        -      },
        -      "duration": {
        -        "description": "Frame duration in milliseconds; default 100.",
        -        "maximum": 60000,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "flipX": {
        -        "type": "boolean"
        -      },
        -      "flipY": {
        -        "type": "boolean"
        -      },
        -      "from": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "layers": {
        -        "items": {
        -          "$ref": "#/$defs/layer"
        -        },
        -        "maxItems": 64,
        -        "type": "array"
        -      },
        -      "name": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "ops": {
        -        "items": {
        -          "$ref": "#/$defs/op"
        -        },
        -        "maxItems": 2048,
        -        "type": "array"
        -      },
        -      "palette": {
        -        "additionalProperties": {
        -          "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -          "type": "string"
        -        },
        -        "description": "Recolor project palette keys for this frame only (palette cycling). Keys must exist in the project palette.",
        -        "maxProperties": 256,
        -        "type": "object"
        -      },
        -      "pixels": {
        -        "description": "Final canvas-coordinate RGBA replacements after ops and layers. Coordinates must be inside this canvas. Later entries win; transparent erases.",
        -        "items": {
        -          "$ref": "#/$defs/pixel"
        -        },
        -        "maxItems": 65536,
        -        "type": "array"
        -      },
        -      "points": {
        -        "additionalProperties": {
        -          "description": "[x, y] in unscaled canvas pixels; the top-left corner of that pixel.",
        -          "items": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "maxItems": 2,
        -          "minItems": 2,
        -          "type": "array"
        -        },
        -        "description": "Named frame positions (hands, hit points, glyph advance), exported in the atlas in exported pixels.",
        -        "maxProperties": 64,
        -        "propertyNames": {
        -          "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -          "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -          "type": "string"
        -        },
        -        "type": "object"
        -      },
        -      "translate": {
        -        "items": {
        -          "maximum": 4096,
        -          "minimum": -4096,
        -          "type": "integer"
        -        },
        -        "maxItems": 2,
        -        "minItems": 2,
        -        "type": "array"
        -      },
        -      "wrap": {
        -        "description": "Translation wraps around the canvas instead of clipping (scrolling tiles).",
        -        "type": "boolean"
        -      }
        -    },
        -    "required": [
        -      "name"
        -    ],
        -    "type": "object"
        -  },
        -  "layer": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "name": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "opacity": {
        -        "maximum": 1,
        -        "minimum": 0,
        -        "type": "number"
        -      },
        -      "ops": {
        -        "items": {
        -          "$ref": "#/$defs/op"
        -        },
        -        "maxItems": 2048,
        -        "type": "array"
        -      },
        -      "palette": {
        -        "additionalProperties": {
        -          "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -          "type": "string"
        -        },
        -        "description": "Recolor project palette keys for this layer's drawing only, on top of the frame palette: a flash or cycle on one layer leaves other drawing that shares the key alone. Keys must exist in the project palette; values are colours or project palette names.",
        -        "maxProperties": 256,
        -        "type": "object"
        -      },
        -      "visible": {
        -        "type": "boolean"
        -      },
        -      "x": {
        -        "maximum": 4096,
        -        "minimum": -4096,
        -        "type": "integer"
        -      },
        -      "y": {
        -        "maximum": 4096,
        -        "minimum": -4096,
        -        "type": "integer"
        -      }
        -    },
        -    "required": [],
        -    "type": "object"
        -  },
        -  "op": {
        -    "oneOf": [
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "pixel"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "filled": {
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "rect"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "filled": {
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "ellipse"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "line"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "x2": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y2": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "x2",
        -          "y2",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "clear"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "fill"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "from": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "replace"
        -          },
        -          "to": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "from",
        -          "to"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "grid"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "rows": {
        -            "$ref": "#/$defs/rows"
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "rows"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "stamp"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "symbol"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw text in the built-in pixel font. y is the top of the capitals; a line is 8 rows: capitals 7 tall, lowercase 5, descenders 1 below, and the accent of a capital reaches 2 rows above y. Glyphs are 1 to 5 pixels wide (© ® — Œ œ are 7), digits always 5, a space 3. One line of n average characters is about 6n pixels wide.",
        -        "properties": {
        -          "align": {
        -            "description": "What x is: the left edge of the text (default), its centre or its right edge. Lines align the same way.",
        -            "enum": [
        -              "left",
        -              "center",
        -              "right"
        -            ]
        -          },
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "lineHeight": {
        -            "description": "Pixels from the top of one line to the top of the next; default 10.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "text"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "spacing": {
        -            "description": "Pixels between glyphs; default 1.",
        -            "maximum": 16,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "text": {
        -            "description": "Printable ASCII, western European accented letters and common symbols; \\n starts a new line (at most 64).",
        -            "maxLength": 512,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "text",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Copy a rectangle of a symbol (palette characters) or an earlier frame (exact RGBA).",
        -        "oneOf": [
        -          {
        -            "required": [
        -              "from"
        -            ]
        -          },
        -          {
        -            "required": [
        -              "symbol"
        -            ]
        -          }
        -        ],
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "from": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "h": {
        -            "maximum": 256,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "copy"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Symbol copies: recolor grid characters (one character each) → palette name or colour. Frame copies: swap exact colours, named by project palette key or hex (#RGB to #RRGGBBAA), for a palette name or colour.",
        -            "maxProperties": 256,
        -            "propertyNames": {
        -              "maxLength": 64,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "sx": {
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "sy": {
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "w": {
        -            "maximum": 256,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw one autotile tile from a template symbol two tiles wide and three tall: the four quarters the mask selects (see the autotile template format). The autotile compiler emits one per frame, so editing the template redraws every tile.",
        -        "properties": {
        -          "mask": {
        -            "description": "Neighbour mask. Blob: N=1, NE=2, E=4, SE=8, S=16, SW=32, W=64, NW=128, reduced so a diagonal counts only with both of its sides. Cardinal: N=1, E=2, S=4, W=8.",
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "mode": {
        -            "description": "blob (default) or cardinal.",
        -            "enum": [
        -              "blob",
        -              "cardinal"
        -            ]
        -          },
        -          "op": {
        -            "const": "autotile"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "symbol",
        -          "mask"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Outline every visible pixel in this buffer (frame or layer): outside rings, inside edge rings or both, in chosen directions.",
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "diagonal": {
        -            "type": "boolean"
        -          },
        -          "directions": {
        -            "description": "3×3 grid centred on a visible pixel, with a dot in the centre: x marks the sides that get the line, e.g. [\"...\", \"...\", \"..x\"] for a drop shadow or [\".x.\", \"...\", \"...\"] for a top rim. Replaces diagonal.",
        -            "items": {
        -              "pattern": "^[x.]{3}$",
        -              "type": "string"
        -            },
        -            "maxItems": 3,
        -            "minItems": 3,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "outline"
        -          },
        -          "position": {
        -            "description": "outside (default) surrounds the visible pixels; inside recolors their edge pixels; middle splits the width, the odd pixel outside.",
        -            "enum": [
        -              "outside",
        -              "inside",
        -              "middle"
        -            ]
        -          },
        -          "width": {
        -            "description": "Rings of pixels; default 1.",
        -            "maximum": 8,
        -            "minimum": 1,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "anyOf": [
        -          {
        -            "required": [
        -              "color"
        -            ]
        -          },
        -          {
        -            "required": [
        -              "erase"
        -            ]
        -          }
        -        ],
        -        "description": "Ordered dither between the current pixels and color: draws (or erases) where a canvas-anchored threshold pattern is below the density (default 0.5). x/y default to 0 and w/h reach the canvas edge.",
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "density": {
        -            "description": "Share of pixels changed, 0–1, or [from, to] ramping along direction.",
        -            "oneOf": [
        -              {
        -                "maximum": 1,
        -                "minimum": 0,
        -                "type": "number"
        -              },
        -              {
        -                "items": {
        -                  "maximum": 1,
        -                  "minimum": 0,
        -                  "type": "number"
        -                },
        -                "maxItems": 2,
        -                "minItems": 2,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "direction": {
        -            "description": "Ramp direction for a [from, to] density; radial runs from the centre to the edge.",
        -            "enum": [
        -              "down",
        -              "up",
        -              "right",
        -              "left",
        -              "radial"
        -            ]
        -          },
        -          "erase": {
        -            "description": "Clear the pattern pixels instead of drawing a color.",
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "offset": {
        -            "description": "Shift the pattern, for example to crawl it between frames.",
        -            "items": {
        -              "maximum": 4096,
        -              "minimum": -4096,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "dither"
        -          },
        -          "over": {
        -            "description": "Only change pixels whose exact current color is listed (transparent included).",
        -            "oneOf": [
        -              {
        -                "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                "type": "string"
        -              },
        -              {
        -                "items": {
        -                  "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                  "type": "string"
        -                },
        -                "maxItems": 64,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "pattern": {
        -            "description": "bayer4 by default. noise is seeded white noise with no repeating grid; value is seeded smooth noise that forms clumps about scale pixels across. Patterns follow canvas coordinates, so neighbouring areas line up.",
        -            "oneOf": [
        -              {
        -                "enum": [
        -                  "bayer2",
        -                  "bayer4",
        -                  "bayer8",
        -                  "noise",
        -                  "value"
        -                ]
        -              },
        -              {
        -                "description": "Rows of ranks from 0 to cells − 1; low ranks turn on first, and a highest rank n gives n + 1 levels, so [[0, 1], [1, 0]] is a checkerboard at 0.5.",
        -                "items": {
        -                  "items": {
        -                    "maximum": 255,
        -                    "minimum": 0,
        -                    "type": "integer"
        -                  },
        -                  "maxItems": 16,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "maxItems": 16,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "scale": {
        -            "description": "Clump size of the value pattern in pixels; default 4.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "seed": {
        -            "description": "Orders the noise or value pattern (default 0): the same seed keeps every pixel's threshold, so a rising density grows in a fixed order; another seed reshuffles it.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Move every pixel of the region along its colour's ramp (see ramps), so shadows and light keep the palette on any surface. Colours on no ramp stay. density (default 1) and pattern give a dithered edge. x/y default to 0 and w/h reach the canvas edge.",
        -        "properties": {
        -          "density": {
        -            "description": "Share of pixels changed, 0–1, or [from, to] ramping along direction.",
        -            "oneOf": [
        -              {
        -                "maximum": 1,
        -                "minimum": 0,
        -                "type": "number"
        -              },
        -              {
        -                "items": {
        -                  "maximum": 1,
        -                  "minimum": 0,
        -                  "type": "number"
        -                },
        -                "maxItems": 2,
        -                "minItems": 2,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "direction": {
        -            "description": "Ramp direction for a [from, to] density; radial runs from the centre to the edge.",
        -            "enum": [
        -              "down",
        -              "up",
        -              "right",
        -              "left",
        -              "radial"
        -            ]
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "offset": {
        -            "description": "Shift the pattern, for example to crawl it between frames.",
        -            "items": {
        -              "maximum": 4096,
        -              "minimum": -4096,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "shade"
        -          },
        -          "over": {
        -            "description": "Only change pixels whose exact current color is listed (transparent included).",
        -            "oneOf": [
        -              {
        -                "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                "type": "string"
        -              },
        -              {
        -                "items": {
        -                  "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                  "type": "string"
        -                },
        -                "maxItems": 64,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "pattern": {
        -            "description": "bayer4 by default. noise is seeded white noise with no repeating grid; value is seeded smooth noise that forms clumps about scale pixels across. Patterns follow canvas coordinates, so neighbouring areas line up.",
        -            "oneOf": [
        -              {
        -                "enum": [
        -                  "bayer2",
        -                  "bayer4",
        -                  "bayer8",
        -                  "noise",
        -                  "value"
        -                ]
        -              },
        -              {
        -                "description": "Rows of ranks from 0 to cells − 1; low ranks turn on first, and a highest rank n gives n + 1 levels, so [[0, 1], [1, 0]] is a checkerboard at 0.5.",
        -                "items": {
        -                  "items": {
        -                    "maximum": 255,
        -                    "minimum": 0,
        -                    "type": "integer"
        -                  },
        -                  "maxItems": 16,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "maxItems": 16,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "scale": {
        -            "description": "Clump size of the value pattern in pixels; default 4.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "seed": {
        -            "description": "Orders the noise or value pattern (default 0): the same seed keeps every pixel's threshold, so a rising density grows in a fixed order; another seed reshuffles it.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "shape": {
        -            "description": "rect (default) or the ellipse inside the region.",
        -            "enum": [
        -              "rect",
        -              "ellipse"
        -            ]
        -          },
        -          "steps": {
        -            "description": "Steps along each colour's ramp: negative darkens (default -1), positive lightens. Clamped at the ramp's ends; not 0.",
        -            "maximum": 8,
        -            "minimum": -8,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw a character map from this recipe's symbols and autotile templates, with variants, weights and neighbour rules, so later operations see the composed result (dither over its floor, rewrite rules at its corners).",
        -        "properties": {
        -          "classes": {
        -            "additionalProperties": {
        -              "maxLength": 64,
        -              "minLength": 1,
        -              "type": "string"
        -            },
        -            "description": "Characters that stand for several map characters in rule patterns, e.g. {\"#\": \"WV\"}; include the empty character to match empty cells too.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "empty": {
        -            "description": "A character, not a legend key, that stands for empty cells in rule patterns.",
        -            "maxLength": 1,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "legend": {
        -            "additionalProperties": {
        -              "oneOf": [
        -                {
        -                  "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                  "type": "string"
        -                },
        -                {
        -                  "additionalProperties": false,
        -                  "properties": {
        -                    "autotile": {
        -                      "description": "Pick by neighbour mask: 47-value blob or 16-value cardinal.",
        -                      "enum": [
        -                        "blob",
        -                        "cardinal"
        -                      ]
        -                    },
        -                    "match": {
        -                      "description": "Characters counted as the same terrain for the mask; default: this character.",
        -                      "minLength": 1,
        -                      "type": "string"
        -                    },
        -                    "symbol": {
        -                      "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                      "type": "string"
        -                    },
        -                    "symbols": {
        -                      "description": "Variants of symbol, picked by cell position (seeded when the tilemap has a seed).",
        -                      "items": {
        -                        "minLength": 1,
        -                        "type": "string"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    },
        -                    "template": {
        -                      "description": "An autotile template symbol (two tiles wide, three tall) drawn with the cell's neighbour mask; needs autotile.",
        -                      "type": "string"
        -                    },
        -                    "templates": {
        -                      "description": "Variants of template, picked by cell position (seeded when the tilemap has a seed).",
        -                      "items": {
        -                        "minLength": 1,
        -                        "type": "string"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    },
        -                    "weights": {
        -                      "description": "One whole-number weight per variant name; heavier names are picked more often.",
        -                      "items": {
        -                        "maximum": 1000,
        -                        "minimum": 1,
        -                        "type": "integer"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    }
        -                  },
        -                  "required": [],
        -                  "type": "object"
        -                },
        -                {
        -                  "additionalProperties": false,
        -                  "properties": {
        -                    "rules": {
        -                      "description": "Every rule that matches draws, in order, until one with stop draws.",
        -                      "items": {
        -                        "additionalProperties": false,
        -                        "properties": {
        -                          "autotile": {
        -                            "description": "Pick by neighbour mask: 47-value blob or 16-value cardinal.",
        -                            "enum": [
        -                              "blob",
        -                              "cardinal"
        -                            ]
        -                          },
        -                          "chance": {
        -                            "description": "Seeded probability that a matching rule draws.",
        -                            "maximum": 1,
        -                            "minimum": 0,
        -                            "type": "number"
        -                          },
        -                          "match": {
        -                            "description": "Characters counted as the same terrain for the mask; default: this character.",
        -                            "minLength": 1,
        -                            "type": "string"
        -                          },
        -                          "offset": {
        -                            "description": "[columns, rows] to draw into another cell; it draws over that cell's own tiles.",
        -                            "items": {
        -                              "maximum": 8,
        -                              "minimum": -8,
        -                              "type": "integer"
        -                            },
        -                            "maxItems": 2,
        -                            "minItems": 2,
        -                            "type": "array"
        -                          },
        -                          "stop": {
        -                            "description": "When this rule draws, skip the rest of the cell's rules.",
        -                            "type": "boolean"
        -                          },
        -                          "symbol": {
        -                            "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                            "type": "string"
        -                          },
        -                          "symbols": {
        -                            "description": "Variants of symbol, picked by cell position (seeded when the tilemap has a seed).",
        -                            "items": {
        -                              "minLength": 1,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "template": {
        -                            "description": "An autotile template symbol (two tiles wide, three tall) drawn with the cell's neighbour mask; needs autotile.",
        -                            "type": "string"
        -                          },
        -                          "templates": {
        -                            "description": "Variants of template, picked by cell position (seeded when the tilemap has a seed).",
        -                            "items": {
        -                              "minLength": 1,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "weights": {
        -                            "description": "One whole-number weight per variant name; heavier names are picked more often.",
        -                            "items": {
        -                              "maximum": 1000,
        -                              "minimum": 1,
        -                              "type": "integer"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "where": {
        -                            "description": "3×3 characters centred on the cell. A dot matches anything, the tilemap's empty character matches empty cells (and cells beyond the map), a class matches its members, any other character matches itself.",
        -                            "items": {
        -                              "maxLength": 3,
        -                              "minLength": 3,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 3,
        -                            "minItems": 3,
        -                            "type": "array"
        -                          }
        -                        },
        -                        "required": [],
        -                        "type": "object"
        -                      },
        -                      "maxItems": 32,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    }
        -                  },
        -                  "required": [
        -                    "rules"
        -                  ],
        -                  "type": "object"
        -                },
        -                {
        -                  "description": "Context cell: never drawn, but it counts for other entries' match and for rules.",
        -                  "type": "null"
        -                }
        -              ]
        -            },
        -            "description": "Map characters to what they draw. Dot and space are empty cells.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "op": {
        -            "const": "tilemap"
        -          },
        -          "outside": {
        -            "description": "Cells beyond the map: empty (default), or matching every mask and rule pattern.",
        -            "enum": [
        -              "empty",
        -              "match"
        -            ]
        -          },
        -          "rows": {
        -            "items": {
        -              "maxLength": 256,
        -              "minLength": 1,
        -              "type": "string"
        -            },
        -            "maxItems": 256,
        -            "minItems": 1,
        -            "type": "array"
        -          },
        -          "seed": {
        -            "description": "Reorders seeded variant picks and rule chances; default 0 keeps position-based picks.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "tile": {
        -            "description": "Cell size [width, height] in pixels.",
        -            "items": {
        -              "maximum": 256,
        -              "minimum": 1,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "tile",
        -          "rows",
        -          "legend"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Markov-style rewrite rules on this buffer: each small match grid found on exact colors is replaced, in a seeded order, without overlapping rewrites. The region defaults to the whole canvas.",
        -        "properties": {
        -          "chance": {
        -            "description": "Seeded probability that each match is applied.",
        -            "maximum": 1,
        -            "minimum": 0,
        -            "type": "number"
        -          },
        -          "empty": {
        -            "description": "A non-palette character meaning a transparent pixel in match and erasure in replace.",
        -            "maxLength": 1,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "limit": {
        -            "description": "Most replacements per rule per step.",
        -            "maximum": 65536,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "mirror": {
        -            "description": "Also match the rules mirrored horizontally.",
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "rewrite"
        -          },
        -          "rotate": {
        -            "description": "Also match the rules turned 90°, 180° and 270°.",
        -            "type": "boolean"
        -          },
        -          "rules": {
        -            "items": {
        -              "additionalProperties": false,
        -              "properties": {
        -                "match": {
        -                  "description": "Equal-width rows of palette keys; dot or space matches anything (match) or keeps the pixel (replace); the empty character stands for transparent.",
        -                  "items": {
        -                    "maxLength": 8,
        -                    "minLength": 1,
        -                    "type": "string"
        -                  },
        -                  "maxItems": 8,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "replace": {
        -                  "description": "Equal-width rows of palette keys; dot or space matches anything (match) or keeps the pixel (replace); the empty character stands for transparent.",
        -                  "items": {
        -                    "maxLength": 8,
        -                    "minLength": 1,
        -                    "type": "string"
        -                  },
        -                  "maxItems": 8,
        -                  "minItems": 1,
        -                  "type": "array"
        -                }
        -              },
        -              "required": [
        -                "match",
        -                "replace"
        -              ],
        -              "type": "object"
        -            },
        -            "maxItems": 16,
        -            "minItems": 1,
        -            "type": "array"
        -          },
        -          "seed": {
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "steps": {
        -            "description": "Passes over every rule; stops early when a pass changes nothing.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "rules"
        -        ],
        -        "type": "object"
        -      }
        -    ]
        -  },
        -  "pixel": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "color": {
        -        "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -        "type": "string"
        -      },
        -      "x": {
        -        "maximum": 255,
        -        "minimum": 0,
        -        "type": "integer"
        -      },
        -      "y": {
        -        "maximum": 255,
        -        "minimum": 0,
        -        "type": "integer"
        -      }
        -    },
        -    "required": [
        -      "x",
        -      "y",
        -      "color"
        -    ],
        -    "type": "object"
        -  },
        -  "rows": {
        -    "description": "Equal-width text rows. Every character is a palette key; dot and space skip a pixel.",
        -    "items": {
        -      "maxLength": 256,
        -      "minLength": 1,
        -      "type": "string"
        -    },
        -    "maxItems": 256,
        -    "minItems": 1,
        -    "type": "array"
        -  }
        -}
      • addedInput schema / properties / ground
        Added value: +{
        +  "description": "Motion view: ground pixel row; overrides metadata.rig.ground.",
        +  "maximum": 255,
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedInput schema / properties / metadata
        Added value: +{
        +  "description": "Pose metadata returned by pixel_compile with metadata:true. Needed with a compiled project/revision for contacts and joint angles; a source supplies its own.",
        +  "type": "object"
        +}
      • removedInput schema / properties / project / additionalProperties
        Removed value: -false
      • addedInput schema / properties / project / description
        Added value: +"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}. pixel_help explains it; its topic schema is the full JSON Schema. Mistakes come back with their exact path."
      • removedInput schema / properties / project / properties
        Removed value: -{
        -  "$schema": {
        -    "type": "string"
        -  },
        -  "anchor": {
        -    "description": "Default pivot for every frame, exported to the atlas as anchor (exported pixels) and pivot (anchor / source size).",
        -    "items": {
        -      "maximum": 4096,
        -      "minimum": -4096,
        -      "type": "integer"
        -    },
        -    "maxItems": 2,
        -    "minItems": 2,
        -    "type": "array"
        -  },
        -  "animations": {
        -    "additionalProperties": {
        -      "additionalProperties": false,
        -      "properties": {
        -        "direction": {
        -          "enum": [
        -            "forward",
        -            "reverse",
        -            "pingpong"
        -          ]
        -        },
        -        "frames": {
        -          "items": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "maxItems": 1024,
        -          "minItems": 1,
        -          "type": "array"
        -        },
        -        "loop": {
        -          "type": "boolean"
        -        }
        -      },
        -      "required": [
        -        "frames"
        -      ],
        -      "type": "object"
        -    },
        -    "description": "ZIP32 bundle budget: animation count + frame count + 7 support files must not exceed 65,535.",
        -    "maxProperties": 65527,
        -    "minProperties": 1,
        -    "propertyNames": {
        -      "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -      "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -      "type": "string"
        -    },
        -    "type": "object"
        -  },
        -  "background": {
        -    "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -    "type": "string"
        -  },
        -  "frames": {
        -    "items": {
        -      "$ref": "#/$defs/frame"
        -    },
        -    "maxItems": 256,
        -    "minItems": 1,
        -    "type": "array"
        -  },
        -  "height": {
        -    "maximum": 256,
        -    "minimum": 1,
        -    "type": "integer"
        -  },
        -  "name": {
        -    "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -    "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -    "type": "string"
        -  },
        -  "palette": {
        -    "additionalProperties": {
        -      "pattern": "^(transparent|#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8}))$",
        -      "type": "string"
        -    },
        -    "maxProperties": 257,
        -    "properties": {
        -      "$ref": {
        -        "description": "Shared palette file, resolved by the CLI and MCP relative to this recipe. Local entries add to or override it.",
        -        "type": "string"
        -      }
        -    },
        -    "type": "object"
        -  },
        -  "ramps": {
        -    "description": "Palette keys from dark to light, one list per material. shade and scene ramp lighting move a colour along its ramp instead of inventing one. A key belongs to one ramp. A shared palette file ({\"palette\": {...}, \"ramps\": [...]}) can supply them.",
        -    "items": {
        -      "items": {
        -        "maxLength": 64,
        -        "minLength": 1,
        -        "type": "string"
        -      },
        -      "maxItems": 32,
        -      "minItems": 2,
        -      "type": "array"
        -    },
        -    "maxItems": 64,
        -    "type": "array"
        -  },
        -  "sheet": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "columns": {
        -        "maximum": 256,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "padding": {
        -        "maximum": 16,
        -        "minimum": 0,
        -        "type": "integer"
        -      },
        -      "scale": {
        -        "maximum": 16,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "trim": {
        -        "description": "Pack each frame by its visible bounds; the atlas reports trimmed rectangles with spriteSourceSize. Ignores columns.",
        -        "type": "boolean"
        -      }
        -    },
        -    "required": [],
        -    "type": "object"
        -  },
        -  "symbols": {
        -    "additionalProperties": {
        -      "$ref": "#/$defs/rows"
        -    },
        -    "propertyNames": {
        -      "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -      "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -      "type": "string"
        -    },
        -    "type": "object"
        -  },
        -  "version": {
        -    "const": 1
        -  },
        -  "width": {
        -    "maximum": 256,
        -    "minimum": 1,
        -    "type": "integer"
        -  }
        -}
      • removedInput schema / properties / project / required
        Removed value: -[
        -  "version",
        -  "name",
        -  "width",
        -  "height",
        -  "frames"
        -]
      • removedInput schema / properties / project / title
        Removed value: -"Pixelbolt project"
      • addedInput schema / properties / source
        Added value: +{
        +  "description": "A pixelforge-poses source, instead of project/revision; compiles it with contact and joint metadata for motion review.",
        +  "type": "object"
        +}
      • addedInput schema / properties / speed
        Added value: +{
        +  "description": "Motion view: horizontal speed in pixels per second.",
        +  "maximum": 4096,
        +  "minimum": -4096,
        +  "type": "number"
        +}
      • changedInput schema / properties / view / description
        Previous value: -"Onion uses pink previous/cyan next poses in animation playback order (requires animation). Tile repeats each frame 3x3 and reports doubled edges and wrap steps."New value: +"Onion uses pink previous/cyan next poses in playback order. Motion requires animation and reports slide, signed ground error, joint seam and pose spread, with native and enlarged trajectory/timing strips. Pass source or compile metadata to measure contacts. Tile repeats each frame 3x3."
      • changedInput schema / properties / view / enum
        Previous value: -[
        -  "color",
        -  "silhouette",
        -  "grayscale",
        -  "onion",
        -  "tile"
        -]New value: +[
        +  "color",
        +  "silhouette",
        +  "grayscale",
        +  "onion",
        +  "tile",
        +  "motion"
        +]
    • Changedpixel_patch6 fields changed
      • removedInput schema / $defs
        Removed value: -{
        -  "frame": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "anchor": {
        -        "description": "[x, y] in unscaled canvas pixels; the top-left corner of that pixel.",
        -        "items": {
        -          "maximum": 4096,
        -          "minimum": -4096,
        -          "type": "integer"
        -        },
        -        "maxItems": 2,
        -        "minItems": 2,
        -        "type": "array"
        -      },
        -      "duration": {
        -        "description": "Frame duration in milliseconds; default 100.",
        -        "maximum": 60000,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "flipX": {
        -        "type": "boolean"
        -      },
        -      "flipY": {
        -        "type": "boolean"
        -      },
        -      "from": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "layers": {
        -        "items": {
        -          "$ref": "#/$defs/layer"
        -        },
        -        "maxItems": 64,
        -        "type": "array"
        -      },
        -      "name": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "ops": {
        -        "items": {
        -          "$ref": "#/$defs/op"
        -        },
        -        "maxItems": 2048,
        -        "type": "array"
        -      },
        -      "palette": {
        -        "additionalProperties": {
        -          "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -          "type": "string"
        -        },
        -        "description": "Recolor project palette keys for this frame only (palette cycling). Keys must exist in the project palette.",
        -        "maxProperties": 256,
        -        "type": "object"
        -      },
        -      "pixels": {
        -        "description": "Final canvas-coordinate RGBA replacements after ops and layers. Coordinates must be inside this canvas. Later entries win; transparent erases.",
        -        "items": {
        -          "$ref": "#/$defs/pixel"
        -        },
        -        "maxItems": 65536,
        -        "type": "array"
        -      },
        -      "points": {
        -        "additionalProperties": {
        -          "description": "[x, y] in unscaled canvas pixels; the top-left corner of that pixel.",
        -          "items": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "maxItems": 2,
        -          "minItems": 2,
        -          "type": "array"
        -        },
        -        "description": "Named frame positions (hands, hit points, glyph advance), exported in the atlas in exported pixels.",
        -        "maxProperties": 64,
        -        "propertyNames": {
        -          "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -          "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -          "type": "string"
        -        },
        -        "type": "object"
        -      },
        -      "translate": {
        -        "items": {
        -          "maximum": 4096,
        -          "minimum": -4096,
        -          "type": "integer"
        -        },
        -        "maxItems": 2,
        -        "minItems": 2,
        -        "type": "array"
        -      },
        -      "wrap": {
        -        "description": "Translation wraps around the canvas instead of clipping (scrolling tiles).",
        -        "type": "boolean"
        -      }
        -    },
        -    "required": [
        -      "name"
        -    ],
        -    "type": "object"
        -  },
        -  "layer": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "name": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "opacity": {
        -        "maximum": 1,
        -        "minimum": 0,
        -        "type": "number"
        -      },
        -      "ops": {
        -        "items": {
        -          "$ref": "#/$defs/op"
        -        },
        -        "maxItems": 2048,
        -        "type": "array"
        -      },
        -      "palette": {
        -        "additionalProperties": {
        -          "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -          "type": "string"
        -        },
        -        "description": "Recolor project palette keys for this layer's drawing only, on top of the frame palette: a flash or cycle on one layer leaves other drawing that shares the key alone. Keys must exist in the project palette; values are colours or project palette names.",
        -        "maxProperties": 256,
        -        "type": "object"
        -      },
        -      "visible": {
        -        "type": "boolean"
        -      },
        -      "x": {
        -        "maximum": 4096,
        -        "minimum": -4096,
        -        "type": "integer"
        -      },
        -      "y": {
        -        "maximum": 4096,
        -        "minimum": -4096,
        -        "type": "integer"
        -      }
        -    },
        -    "required": [],
        -    "type": "object"
        -  },
        -  "op": {
        -    "oneOf": [
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "pixel"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "filled": {
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "rect"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "filled": {
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "ellipse"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "line"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "x2": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y2": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "x2",
        -          "y2",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "clear"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "fill"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "from": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "replace"
        -          },
        -          "to": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "from",
        -          "to"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "grid"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "rows": {
        -            "$ref": "#/$defs/rows"
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "rows"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "stamp"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "symbol"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw text in the built-in pixel font. y is the top of the capitals; a line is 8 rows: capitals 7 tall, lowercase 5, descenders 1 below, and the accent of a capital reaches 2 rows above y. Glyphs are 1 to 5 pixels wide (© ® — Œ œ are 7), digits always 5, a space 3. One line of n average characters is about 6n pixels wide.",
        -        "properties": {
        -          "align": {
        -            "description": "What x is: the left edge of the text (default), its centre or its right edge. Lines align the same way.",
        -            "enum": [
        -              "left",
        -              "center",
        -              "right"
        -            ]
        -          },
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "lineHeight": {
        -            "description": "Pixels from the top of one line to the top of the next; default 10.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "text"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "spacing": {
        -            "description": "Pixels between glyphs; default 1.",
        -            "maximum": 16,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "text": {
        -            "description": "Printable ASCII, western European accented letters and common symbols; \\n starts a new line (at most 64).",
        -            "maxLength": 512,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "text",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Copy a rectangle of a symbol (palette characters) or an earlier frame (exact RGBA).",
        -        "oneOf": [
        -          {
        -            "required": [
        -              "from"
        -            ]
        -          },
        -          {
        -            "required": [
        -              "symbol"
        -            ]
        -          }
        -        ],
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "from": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "h": {
        -            "maximum": 256,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "copy"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Symbol copies: recolor grid characters (one character each) → palette name or colour. Frame copies: swap exact colours, named by project palette key or hex (#RGB to #RRGGBBAA), for a palette name or colour.",
        -            "maxProperties": 256,
        -            "propertyNames": {
        -              "maxLength": 64,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "sx": {
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "sy": {
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "w": {
        -            "maximum": 256,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw one autotile tile from a template symbol two tiles wide and three tall: the four quarters the mask selects (see the autotile template format). The autotile compiler emits one per frame, so editing the template redraws every tile.",
        -        "properties": {
        -          "mask": {
        -            "description": "Neighbour mask. Blob: N=1, NE=2, E=4, SE=8, S=16, SW=32, W=64, NW=128, reduced so a diagonal counts only with both of its sides. Cardinal: N=1, E=2, S=4, W=8.",
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "mode": {
        -            "description": "blob (default) or cardinal.",
        -            "enum": [
        -              "blob",
        -              "cardinal"
        -            ]
        -          },
        -          "op": {
        -            "const": "autotile"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "symbol",
        -          "mask"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Outline every visible pixel in this buffer (frame or layer): outside rings, inside edge rings or both, in chosen directions.",
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "diagonal": {
        -            "type": "boolean"
        -          },
        -          "directions": {
        -            "description": "3×3 grid centred on a visible pixel, with a dot in the centre: x marks the sides that get the line, e.g. [\"...\", \"...\", \"..x\"] for a drop shadow or [\".x.\", \"...\", \"...\"] for a top rim. Replaces diagonal.",
        -            "items": {
        -              "pattern": "^[x.]{3}$",
        -              "type": "string"
        -            },
        -            "maxItems": 3,
        -            "minItems": 3,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "outline"
        -          },
        -          "position": {
        -            "description": "outside (default) surrounds the visible pixels; inside recolors their edge pixels; middle splits the width, the odd pixel outside.",
        -            "enum": [
        -              "outside",
        -              "inside",
        -              "middle"
        -            ]
        -          },
        -          "width": {
        -            "description": "Rings of pixels; default 1.",
        -            "maximum": 8,
        -            "minimum": 1,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "anyOf": [
        -          {
        -            "required": [
        -              "color"
        -            ]
        -          },
        -          {
        -            "required": [
        -              "erase"
        -            ]
        -          }
        -        ],
        -        "description": "Ordered dither between the current pixels and color: draws (or erases) where a canvas-anchored threshold pattern is below the density (default 0.5). x/y default to 0 and w/h reach the canvas edge.",
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "density": {
        -            "description": "Share of pixels changed, 0–1, or [from, to] ramping along direction.",
        -            "oneOf": [
        -              {
        -                "maximum": 1,
        -                "minimum": 0,
        -                "type": "number"
        -              },
        -              {
        -                "items": {
        -                  "maximum": 1,
        -                  "minimum": 0,
        -                  "type": "number"
        -                },
        -                "maxItems": 2,
        -                "minItems": 2,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "direction": {
        -            "description": "Ramp direction for a [from, to] density; radial runs from the centre to the edge.",
        -            "enum": [
        -              "down",
        -              "up",
        -              "right",
        -              "left",
        -              "radial"
        -            ]
        -          },
        -          "erase": {
        -            "description": "Clear the pattern pixels instead of drawing a color.",
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "offset": {
        -            "description": "Shift the pattern, for example to crawl it between frames.",
        -            "items": {
        -              "maximum": 4096,
        -              "minimum": -4096,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "dither"
        -          },
        -          "over": {
        -            "description": "Only change pixels whose exact current color is listed (transparent included).",
        -            "oneOf": [
        -              {
        -                "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                "type": "string"
        -              },
        -              {
        -                "items": {
        -                  "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                  "type": "string"
        -                },
        -                "maxItems": 64,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "pattern": {
        -            "description": "bayer4 by default. noise is seeded white noise with no repeating grid; value is seeded smooth noise that forms clumps about scale pixels across. Patterns follow canvas coordinates, so neighbouring areas line up.",
        -            "oneOf": [
        -              {
        -                "enum": [
        -                  "bayer2",
        -                  "bayer4",
        -                  "bayer8",
        -                  "noise",
        -                  "value"
        -                ]
        -              },
        -              {
        -                "description": "Rows of ranks from 0 to cells − 1; low ranks turn on first, and a highest rank n gives n + 1 levels, so [[0, 1], [1, 0]] is a checkerboard at 0.5.",
        -                "items": {
        -                  "items": {
        -                    "maximum": 255,
        -                    "minimum": 0,
        -                    "type": "integer"
        -                  },
        -                  "maxItems": 16,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "maxItems": 16,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "scale": {
        -            "description": "Clump size of the value pattern in pixels; default 4.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "seed": {
        -            "description": "Orders the noise or value pattern (default 0): the same seed keeps every pixel's threshold, so a rising density grows in a fixed order; another seed reshuffles it.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Move every pixel of the region along its colour's ramp (see ramps), so shadows and light keep the palette on any surface. Colours on no ramp stay. density (default 1) and pattern give a dithered edge. x/y default to 0 and w/h reach the canvas edge.",
        -        "properties": {
        -          "density": {
        -            "description": "Share of pixels changed, 0–1, or [from, to] ramping along direction.",
        -            "oneOf": [
        -              {
        -                "maximum": 1,
        -                "minimum": 0,
        -                "type": "number"
        -              },
        -              {
        -                "items": {
        -                  "maximum": 1,
        -                  "minimum": 0,
        -                  "type": "number"
        -                },
        -                "maxItems": 2,
        -                "minItems": 2,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "direction": {
        -            "description": "Ramp direction for a [from, to] density; radial runs from the centre to the edge.",
        -            "enum": [
        -              "down",
        -              "up",
        -              "right",
        -              "left",
        -              "radial"
        -            ]
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "offset": {
        -            "description": "Shift the pattern, for example to crawl it between frames.",
        -            "items": {
        -              "maximum": 4096,
        -              "minimum": -4096,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "shade"
        -          },
        -          "over": {
        -            "description": "Only change pixels whose exact current color is listed (transparent included).",
        -            "oneOf": [
        -              {
        -                "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                "type": "string"
        -              },
        -              {
        -                "items": {
        -                  "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                  "type": "string"
        -                },
        -                "maxItems": 64,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "pattern": {
        -            "description": "bayer4 by default. noise is seeded white noise with no repeating grid; value is seeded smooth noise that forms clumps about scale pixels across. Patterns follow canvas coordinates, so neighbouring areas line up.",
        -            "oneOf": [
        -              {
        -                "enum": [
        -                  "bayer2",
        -                  "bayer4",
        -                  "bayer8",
        -                  "noise",
        -                  "value"
        -                ]
        -              },
        -              {
        -                "description": "Rows of ranks from 0 to cells − 1; low ranks turn on first, and a highest rank n gives n + 1 levels, so [[0, 1], [1, 0]] is a checkerboard at 0.5.",
        -                "items": {
        -                  "items": {
        -                    "maximum": 255,
        -                    "minimum": 0,
        -                    "type": "integer"
        -                  },
        -                  "maxItems": 16,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "maxItems": 16,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "scale": {
        -            "description": "Clump size of the value pattern in pixels; default 4.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "seed": {
        -            "description": "Orders the noise or value pattern (default 0): the same seed keeps every pixel's threshold, so a rising density grows in a fixed order; another seed reshuffles it.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "shape": {
        -            "description": "rect (default) or the ellipse inside the region.",
        -            "enum": [
        -              "rect",
        -              "ellipse"
        -            ]
        -          },
        -          "steps": {
        -            "description": "Steps along each colour's ramp: negative darkens (default -1), positive lightens. Clamped at the ramp's ends; not 0.",
        -            "maximum": 8,
        -            "minimum": -8,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw a character map from this recipe's symbols and autotile templates, with variants, weights and neighbour rules, so later operations see the composed result (dither over its floor, rewrite rules at its corners).",
        -        "properties": {
        -          "classes": {
        -            "additionalProperties": {
        -              "maxLength": 64,
        -              "minLength": 1,
        -              "type": "string"
        -            },
        -            "description": "Characters that stand for several map characters in rule patterns, e.g. {\"#\": \"WV\"}; include the empty character to match empty cells too.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "empty": {
        -            "description": "A character, not a legend key, that stands for empty cells in rule patterns.",
        -            "maxLength": 1,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "legend": {
        -            "additionalProperties": {
        -              "oneOf": [
        -                {
        -                  "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                  "type": "string"
        -                },
        -                {
        -                  "additionalProperties": false,
        -                  "properties": {
        -                    "autotile": {
        -                      "description": "Pick by neighbour mask: 47-value blob or 16-value cardinal.",
        -                      "enum": [
        -                        "blob",
        -                        "cardinal"
        -                      ]
        -                    },
        -                    "match": {
        -                      "description": "Characters counted as the same terrain for the mask; default: this character.",
        -                      "minLength": 1,
        -                      "type": "string"
        -                    },
        -                    "symbol": {
        -                      "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                      "type": "string"
        -                    },
        -                    "symbols": {
        -                      "description": "Variants of symbol, picked by cell position (seeded when the tilemap has a seed).",
        -                      "items": {
        -                        "minLength": 1,
        -                        "type": "string"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    },
        -                    "template": {
        -                      "description": "An autotile template symbol (two tiles wide, three tall) drawn with the cell's neighbour mask; needs autotile.",
        -                      "type": "string"
        -                    },
        -                    "templates": {
        -                      "description": "Variants of template, picked by cell position (seeded when the tilemap has a seed).",
        -                      "items": {
        -                        "minLength": 1,
        -                        "type": "string"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    },
        -                    "weights": {
        -                      "description": "One whole-number weight per variant name; heavier names are picked more often.",
        -                      "items": {
        -                        "maximum": 1000,
        -                        "minimum": 1,
        -                        "type": "integer"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    }
        -                  },
        -                  "required": [],
        -                  "type": "object"
        -                },
        -                {
        -                  "additionalProperties": false,
        -                  "properties": {
        -                    "rules": {
        -                      "description": "Every rule that matches draws, in order, until one with stop draws.",
        -                      "items": {
        -                        "additionalProperties": false,
        -                        "properties": {
        -                          "autotile": {
        -                            "description": "Pick by neighbour mask: 47-value blob or 16-value cardinal.",
        -                            "enum": [
        -                              "blob",
        -                              "cardinal"
        -                            ]
        -                          },
        -                          "chance": {
        -                            "description": "Seeded probability that a matching rule draws.",
        -                            "maximum": 1,
        -                            "minimum": 0,
        -                            "type": "number"
        -                          },
        -                          "match": {
        -                            "description": "Characters counted as the same terrain for the mask; default: this character.",
        -                            "minLength": 1,
        -                            "type": "string"
        -                          },
        -                          "offset": {
        -                            "description": "[columns, rows] to draw into another cell; it draws over that cell's own tiles.",
        -                            "items": {
        -                              "maximum": 8,
        -                              "minimum": -8,
        -                              "type": "integer"
        -                            },
        -                            "maxItems": 2,
        -                            "minItems": 2,
        -                            "type": "array"
        -                          },
        -                          "stop": {
        -                            "description": "When this rule draws, skip the rest of the cell's rules.",
        -                            "type": "boolean"
        -                          },
        -                          "symbol": {
        -                            "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                            "type": "string"
        -                          },
        -                          "symbols": {
        -                            "description": "Variants of symbol, picked by cell position (seeded when the tilemap has a seed).",
        -                            "items": {
        -                              "minLength": 1,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "template": {
        -                            "description": "An autotile template symbol (two tiles wide, three tall) drawn with the cell's neighbour mask; needs autotile.",
        -                            "type": "string"
        -                          },
        -                          "templates": {
        -                            "description": "Variants of template, picked by cell position (seeded when the tilemap has a seed).",
        -                            "items": {
        -                              "minLength": 1,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "weights": {
        -                            "description": "One whole-number weight per variant name; heavier names are picked more often.",
        -                            "items": {
        -                              "maximum": 1000,
        -                              "minimum": 1,
        -                              "type": "integer"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "where": {
        -                            "description": "3×3 characters centred on the cell. A dot matches anything, the tilemap's empty character matches empty cells (and cells beyond the map), a class matches its members, any other character matches itself.",
        -                            "items": {
        -                              "maxLength": 3,
        -                              "minLength": 3,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 3,
        -                            "minItems": 3,
        -                            "type": "array"
        -                          }
        -                        },
        -                        "required": [],
        -                        "type": "object"
        -                      },
        -                      "maxItems": 32,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    }
        -                  },
        -                  "required": [
        -                    "rules"
        -                  ],
        -                  "type": "object"
        -                },
        -                {
        -                  "description": "Context cell: never drawn, but it counts for other entries' match and for rules.",
        -                  "type": "null"
        -                }
        -              ]
        -            },
        -            "description": "Map characters to what they draw. Dot and space are empty cells.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "op": {
        -            "const": "tilemap"
        -          },
        -          "outside": {
        -            "description": "Cells beyond the map: empty (default), or matching every mask and rule pattern.",
        -            "enum": [
        -              "empty",
        -              "match"
        -            ]
        -          },
        -          "rows": {
        -            "items": {
        -              "maxLength": 256,
        -              "minLength": 1,
        -              "type": "string"
        -            },
        -            "maxItems": 256,
        -            "minItems": 1,
        -            "type": "array"
        -          },
        -          "seed": {
        -            "description": "Reorders seeded variant picks and rule chances; default 0 keeps position-based picks.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "tile": {
        -            "description": "Cell size [width, height] in pixels.",
        -            "items": {
        -              "maximum": 256,
        -              "minimum": 1,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "tile",
        -          "rows",
        -          "legend"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Markov-style rewrite rules on this buffer: each small match grid found on exact colors is replaced, in a seeded order, without overlapping rewrites. The region defaults to the whole canvas.",
        -        "properties": {
        -          "chance": {
        -            "description": "Seeded probability that each match is applied.",
        -            "maximum": 1,
        -            "minimum": 0,
        -            "type": "number"
        -          },
        -          "empty": {
        -            "description": "A non-palette character meaning a transparent pixel in match and erasure in replace.",
        -            "maxLength": 1,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "limit": {
        -            "description": "Most replacements per rule per step.",
        -            "maximum": 65536,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "mirror": {
        -            "description": "Also match the rules mirrored horizontally.",
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "rewrite"
        -          },
        -          "rotate": {
        -            "description": "Also match the rules turned 90°, 180° and 270°.",
        -            "type": "boolean"
        -          },
        -          "rules": {
        -            "items": {
        -              "additionalProperties": false,
        -              "properties": {
        -                "match": {
        -                  "description": "Equal-width rows of palette keys; dot or space matches anything (match) or keeps the pixel (replace); the empty character stands for transparent.",
        -                  "items": {
        -                    "maxLength": 8,
        -                    "minLength": 1,
        -                    "type": "string"
        -                  },
        -                  "maxItems": 8,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "replace": {
        -                  "description": "Equal-width rows of palette keys; dot or space matches anything (match) or keeps the pixel (replace); the empty character stands for transparent.",
        -                  "items": {
        -                    "maxLength": 8,
        -                    "minLength": 1,
        -                    "type": "string"
        -                  },
        -                  "maxItems": 8,
        -                  "minItems": 1,
        -                  "type": "array"
        -                }
        -              },
        -              "required": [
        -                "match",
        -                "replace"
        -              ],
        -              "type": "object"
        -            },
        -            "maxItems": 16,
        -            "minItems": 1,
        -            "type": "array"
        -          },
        -          "seed": {
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "steps": {
        -            "description": "Passes over every rule; stops early when a pass changes nothing.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "rules"
        -        ],
        -        "type": "object"
        -      }
        -    ]
        -  },
        -  "pixel": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "color": {
        -        "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -        "type": "string"
        -      },
        -      "x": {
        -        "maximum": 255,
        -        "minimum": 0,
        -        "type": "integer"
        -      },
        -      "y": {
        -        "maximum": 255,
        -        "minimum": 0,
        -        "type": "integer"
        -      }
        -    },
        -    "required": [
        -      "x",
        -      "y",
        -      "color"
        -    ],
        -    "type": "object"
        -  },
        -  "rows": {
        -    "description": "Equal-width text rows. Every character is a palette key; dot and space skip a pixel.",
        -    "items": {
        -      "maxLength": 256,
        -      "minLength": 1,
        -      "type": "string"
        -    },
        -    "maxItems": 256,
        -    "minItems": 1,
        -    "type": "array"
        -  }
        -}
      • removedInput schema / properties / project / additionalProperties
        Removed value: -false
      • addedInput schema / properties / project / description
        Added value: +"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}. pixel_help explains it; its topic schema is the full JSON Schema. Mistakes come back with their exact path."
      • removedInput schema / properties / project / properties
        Removed value: -{
        -  "$schema": {
        -    "type": "string"
        -  },
        -  "anchor": {
        -    "description": "Default pivot for every frame, exported to the atlas as anchor (exported pixels) and pivot (anchor / source size).",
        -    "items": {
        -      "maximum": 4096,
        -      "minimum": -4096,
        -      "type": "integer"
        -    },
        -    "maxItems": 2,
        -    "minItems": 2,
        -    "type": "array"
        -  },
        -  "animations": {
        -    "additionalProperties": {
        -      "additionalProperties": false,
        -      "properties": {
        -        "direction": {
        -          "enum": [
        -            "forward",
        -            "reverse",
        -            "pingpong"
        -          ]
        -        },
        -        "frames": {
        -          "items": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "maxItems": 1024,
        -          "minItems": 1,
        -          "type": "array"
        -        },
        -        "loop": {
        -          "type": "boolean"
        -        }
        -      },
        -      "required": [
        -        "frames"
        -      ],
        -      "type": "object"
        -    },
        -    "description": "ZIP32 bundle budget: animation count + frame count + 7 support files must not exceed 65,535.",
        -    "maxProperties": 65527,
        -    "minProperties": 1,
        -    "propertyNames": {
        -      "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -      "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -      "type": "string"
        -    },
        -    "type": "object"
        -  },
        -  "background": {
        -    "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -    "type": "string"
        -  },
        -  "frames": {
        -    "items": {
        -      "$ref": "#/$defs/frame"
        -    },
        -    "maxItems": 256,
        -    "minItems": 1,
        -    "type": "array"
        -  },
        -  "height": {
        -    "maximum": 256,
        -    "minimum": 1,
        -    "type": "integer"
        -  },
        -  "name": {
        -    "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -    "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -    "type": "string"
        -  },
        -  "palette": {
        -    "additionalProperties": {
        -      "pattern": "^(transparent|#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8}))$",
        -      "type": "string"
        -    },
        -    "maxProperties": 257,
        -    "properties": {
        -      "$ref": {
        -        "description": "Shared palette file, resolved by the CLI and MCP relative to this recipe. Local entries add to or override it.",
        -        "type": "string"
        -      }
        -    },
        -    "type": "object"
        -  },
        -  "ramps": {
        -    "description": "Palette keys from dark to light, one list per material. shade and scene ramp lighting move a colour along its ramp instead of inventing one. A key belongs to one ramp. A shared palette file ({\"palette\": {...}, \"ramps\": [...]}) can supply them.",
        -    "items": {
        -      "items": {
        -        "maxLength": 64,
        -        "minLength": 1,
        -        "type": "string"
        -      },
        -      "maxItems": 32,
        -      "minItems": 2,
        -      "type": "array"
        -    },
        -    "maxItems": 64,
        -    "type": "array"
        -  },
        -  "sheet": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "columns": {
        -        "maximum": 256,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "padding": {
        -        "maximum": 16,
        -        "minimum": 0,
        -        "type": "integer"
        -      },
        -      "scale": {
        -        "maximum": 16,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "trim": {
        -        "description": "Pack each frame by its visible bounds; the atlas reports trimmed rectangles with spriteSourceSize. Ignores columns.",
        -        "type": "boolean"
        -      }
        -    },
        -    "required": [],
        -    "type": "object"
        -  },
        -  "symbols": {
        -    "additionalProperties": {
        -      "$ref": "#/$defs/rows"
        -    },
        -    "propertyNames": {
        -      "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -      "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -      "type": "string"
        -    },
        -    "type": "object"
        -  },
        -  "version": {
        -    "const": 1
        -  },
        -  "width": {
        -    "maximum": 256,
        -    "minimum": 1,
        -    "type": "integer"
        -  }
        -}
      • removedInput schema / properties / project / required
        Removed value: -[
        -  "version",
        -  "name",
        -  "width",
        -  "height",
        -  "frames"
        -]
      • removedInput schema / properties / project / title
        Removed value: -"Pixelbolt project"
    • Changedpixel_render12 fields changed
      • removedInput schema / $defs
        Removed value: -{
        -  "frame": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "anchor": {
        -        "description": "[x, y] in unscaled canvas pixels; the top-left corner of that pixel.",
        -        "items": {
        -          "maximum": 4096,
        -          "minimum": -4096,
        -          "type": "integer"
        -        },
        -        "maxItems": 2,
        -        "minItems": 2,
        -        "type": "array"
        -      },
        -      "duration": {
        -        "description": "Frame duration in milliseconds; default 100.",
        -        "maximum": 60000,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "flipX": {
        -        "type": "boolean"
        -      },
        -      "flipY": {
        -        "type": "boolean"
        -      },
        -      "from": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "layers": {
        -        "items": {
        -          "$ref": "#/$defs/layer"
        -        },
        -        "maxItems": 64,
        -        "type": "array"
        -      },
        -      "name": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "ops": {
        -        "items": {
        -          "$ref": "#/$defs/op"
        -        },
        -        "maxItems": 2048,
        -        "type": "array"
        -      },
        -      "palette": {
        -        "additionalProperties": {
        -          "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -          "type": "string"
        -        },
        -        "description": "Recolor project palette keys for this frame only (palette cycling). Keys must exist in the project palette.",
        -        "maxProperties": 256,
        -        "type": "object"
        -      },
        -      "pixels": {
        -        "description": "Final canvas-coordinate RGBA replacements after ops and layers. Coordinates must be inside this canvas. Later entries win; transparent erases.",
        -        "items": {
        -          "$ref": "#/$defs/pixel"
        -        },
        -        "maxItems": 65536,
        -        "type": "array"
        -      },
        -      "points": {
        -        "additionalProperties": {
        -          "description": "[x, y] in unscaled canvas pixels; the top-left corner of that pixel.",
        -          "items": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "maxItems": 2,
        -          "minItems": 2,
        -          "type": "array"
        -        },
        -        "description": "Named frame positions (hands, hit points, glyph advance), exported in the atlas in exported pixels.",
        -        "maxProperties": 64,
        -        "propertyNames": {
        -          "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -          "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -          "type": "string"
        -        },
        -        "type": "object"
        -      },
        -      "translate": {
        -        "items": {
        -          "maximum": 4096,
        -          "minimum": -4096,
        -          "type": "integer"
        -        },
        -        "maxItems": 2,
        -        "minItems": 2,
        -        "type": "array"
        -      },
        -      "wrap": {
        -        "description": "Translation wraps around the canvas instead of clipping (scrolling tiles).",
        -        "type": "boolean"
        -      }
        -    },
        -    "required": [
        -      "name"
        -    ],
        -    "type": "object"
        -  },
        -  "layer": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "name": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "opacity": {
        -        "maximum": 1,
        -        "minimum": 0,
        -        "type": "number"
        -      },
        -      "ops": {
        -        "items": {
        -          "$ref": "#/$defs/op"
        -        },
        -        "maxItems": 2048,
        -        "type": "array"
        -      },
        -      "palette": {
        -        "additionalProperties": {
        -          "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -          "type": "string"
        -        },
        -        "description": "Recolor project palette keys for this layer's drawing only, on top of the frame palette: a flash or cycle on one layer leaves other drawing that shares the key alone. Keys must exist in the project palette; values are colours or project palette names.",
        -        "maxProperties": 256,
        -        "type": "object"
        -      },
        -      "visible": {
        -        "type": "boolean"
        -      },
        -      "x": {
        -        "maximum": 4096,
        -        "minimum": -4096,
        -        "type": "integer"
        -      },
        -      "y": {
        -        "maximum": 4096,
        -        "minimum": -4096,
        -        "type": "integer"
        -      }
        -    },
        -    "required": [],
        -    "type": "object"
        -  },
        -  "op": {
        -    "oneOf": [
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "pixel"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "filled": {
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "rect"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "filled": {
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "ellipse"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "line"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "x2": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y2": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "x2",
        -          "y2",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "clear"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "fill"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "from": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "replace"
        -          },
        -          "to": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "from",
        -          "to"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "grid"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "rows": {
        -            "$ref": "#/$defs/rows"
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "rows"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "stamp"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "symbol"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw text in the built-in pixel font. y is the top of the capitals; a line is 8 rows: capitals 7 tall, lowercase 5, descenders 1 below, and the accent of a capital reaches 2 rows above y. Glyphs are 1 to 5 pixels wide (© ® — Œ œ are 7), digits always 5, a space 3. One line of n average characters is about 6n pixels wide.",
        -        "properties": {
        -          "align": {
        -            "description": "What x is: the left edge of the text (default), its centre or its right edge. Lines align the same way.",
        -            "enum": [
        -              "left",
        -              "center",
        -              "right"
        -            ]
        -          },
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "lineHeight": {
        -            "description": "Pixels from the top of one line to the top of the next; default 10.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "text"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "spacing": {
        -            "description": "Pixels between glyphs; default 1.",
        -            "maximum": 16,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "text": {
        -            "description": "Printable ASCII, western European accented letters and common symbols; \\n starts a new line (at most 64).",
        -            "maxLength": 512,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "text",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Copy a rectangle of a symbol (palette characters) or an earlier frame (exact RGBA).",
        -        "oneOf": [
        -          {
        -            "required": [
        -              "from"
        -            ]
        -          },
        -          {
        -            "required": [
        -              "symbol"
        -            ]
        -          }
        -        ],
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "from": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "h": {
        -            "maximum": 256,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "copy"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Symbol copies: recolor grid characters (one character each) → palette name or colour. Frame copies: swap exact colours, named by project palette key or hex (#RGB to #RRGGBBAA), for a palette name or colour.",
        -            "maxProperties": 256,
        -            "propertyNames": {
        -              "maxLength": 64,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "sx": {
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "sy": {
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "w": {
        -            "maximum": 256,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw one autotile tile from a template symbol two tiles wide and three tall: the four quarters the mask selects (see the autotile template format). The autotile compiler emits one per frame, so editing the template redraws every tile.",
        -        "properties": {
        -          "mask": {
        -            "description": "Neighbour mask. Blob: N=1, NE=2, E=4, SE=8, S=16, SW=32, W=64, NW=128, reduced so a diagonal counts only with both of its sides. Cardinal: N=1, E=2, S=4, W=8.",
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "mode": {
        -            "description": "blob (default) or cardinal.",
        -            "enum": [
        -              "blob",
        -              "cardinal"
        -            ]
        -          },
        -          "op": {
        -            "const": "autotile"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "symbol",
        -          "mask"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Outline every visible pixel in this buffer (frame or layer): outside rings, inside edge rings or both, in chosen directions.",
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "diagonal": {
        -            "type": "boolean"
        -          },
        -          "directions": {
        -            "description": "3×3 grid centred on a visible pixel, with a dot in the centre: x marks the sides that get the line, e.g. [\"...\", \"...\", \"..x\"] for a drop shadow or [\".x.\", \"...\", \"...\"] for a top rim. Replaces diagonal.",
        -            "items": {
        -              "pattern": "^[x.]{3}$",
        -              "type": "string"
        -            },
        -            "maxItems": 3,
        -            "minItems": 3,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "outline"
        -          },
        -          "position": {
        -            "description": "outside (default) surrounds the visible pixels; inside recolors their edge pixels; middle splits the width, the odd pixel outside.",
        -            "enum": [
        -              "outside",
        -              "inside",
        -              "middle"
        -            ]
        -          },
        -          "width": {
        -            "description": "Rings of pixels; default 1.",
        -            "maximum": 8,
        -            "minimum": 1,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "anyOf": [
        -          {
        -            "required": [
        -              "color"
        -            ]
        -          },
        -          {
        -            "required": [
        -              "erase"
        -            ]
        -          }
        -        ],
        -        "description": "Ordered dither between the current pixels and color: draws (or erases) where a canvas-anchored threshold pattern is below the density (default 0.5). x/y default to 0 and w/h reach the canvas edge.",
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "density": {
        -            "description": "Share of pixels changed, 0–1, or [from, to] ramping along direction.",
        -            "oneOf": [
        -              {
        -                "maximum": 1,
        -                "minimum": 0,
        -                "type": "number"
        -              },
        -              {
        -                "items": {
        -                  "maximum": 1,
        -                  "minimum": 0,
        -                  "type": "number"
        -                },
        -                "maxItems": 2,
        -                "minItems": 2,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "direction": {
        -            "description": "Ramp direction for a [from, to] density; radial runs from the centre to the edge.",
        -            "enum": [
        -              "down",
        -              "up",
        -              "right",
        -              "left",
        -              "radial"
        -            ]
        -          },
        -          "erase": {
        -            "description": "Clear the pattern pixels instead of drawing a color.",
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "offset": {
        -            "description": "Shift the pattern, for example to crawl it between frames.",
        -            "items": {
        -              "maximum": 4096,
        -              "minimum": -4096,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "dither"
        -          },
        -          "over": {
        -            "description": "Only change pixels whose exact current color is listed (transparent included).",
        -            "oneOf": [
        -              {
        -                "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                "type": "string"
        -              },
        -              {
        -                "items": {
        -                  "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                  "type": "string"
        -                },
        -                "maxItems": 64,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "pattern": {
        -            "description": "bayer4 by default. noise is seeded white noise with no repeating grid; value is seeded smooth noise that forms clumps about scale pixels across. Patterns follow canvas coordinates, so neighbouring areas line up.",
        -            "oneOf": [
        -              {
        -                "enum": [
        -                  "bayer2",
        -                  "bayer4",
        -                  "bayer8",
        -                  "noise",
        -                  "value"
        -                ]
        -              },
        -              {
        -                "description": "Rows of ranks from 0 to cells − 1; low ranks turn on first, and a highest rank n gives n + 1 levels, so [[0, 1], [1, 0]] is a checkerboard at 0.5.",
        -                "items": {
        -                  "items": {
        -                    "maximum": 255,
        -                    "minimum": 0,
        -                    "type": "integer"
        -                  },
        -                  "maxItems": 16,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "maxItems": 16,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "scale": {
        -            "description": "Clump size of the value pattern in pixels; default 4.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "seed": {
        -            "description": "Orders the noise or value pattern (default 0): the same seed keeps every pixel's threshold, so a rising density grows in a fixed order; another seed reshuffles it.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Move every pixel of the region along its colour's ramp (see ramps), so shadows and light keep the palette on any surface. Colours on no ramp stay. density (default 1) and pattern give a dithered edge. x/y default to 0 and w/h reach the canvas edge.",
        -        "properties": {
        -          "density": {
        -            "description": "Share of pixels changed, 0–1, or [from, to] ramping along direction.",
        -            "oneOf": [
        -              {
        -                "maximum": 1,
        -                "minimum": 0,
        -                "type": "number"
        -              },
        -              {
        -                "items": {
        -                  "maximum": 1,
        -                  "minimum": 0,
        -                  "type": "number"
        -                },
        -                "maxItems": 2,
        -                "minItems": 2,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "direction": {
        -            "description": "Ramp direction for a [from, to] density; radial runs from the centre to the edge.",
        -            "enum": [
        -              "down",
        -              "up",
        -              "right",
        -              "left",
        -              "radial"
        -            ]
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "offset": {
        -            "description": "Shift the pattern, for example to crawl it between frames.",
        -            "items": {
        -              "maximum": 4096,
        -              "minimum": -4096,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "shade"
        -          },
        -          "over": {
        -            "description": "Only change pixels whose exact current color is listed (transparent included).",
        -            "oneOf": [
        -              {
        -                "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                "type": "string"
        -              },
        -              {
        -                "items": {
        -                  "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                  "type": "string"
        -                },
        -                "maxItems": 64,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "pattern": {
        -            "description": "bayer4 by default. noise is seeded white noise with no repeating grid; value is seeded smooth noise that forms clumps about scale pixels across. Patterns follow canvas coordinates, so neighbouring areas line up.",
        -            "oneOf": [
        -              {
        -                "enum": [
        -                  "bayer2",
        -                  "bayer4",
        -                  "bayer8",
        -                  "noise",
        -                  "value"
        -                ]
        -              },
        -              {
        -                "description": "Rows of ranks from 0 to cells − 1; low ranks turn on first, and a highest rank n gives n + 1 levels, so [[0, 1], [1, 0]] is a checkerboard at 0.5.",
        -                "items": {
        -                  "items": {
        -                    "maximum": 255,
        -                    "minimum": 0,
        -                    "type": "integer"
        -                  },
        -                  "maxItems": 16,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "maxItems": 16,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "scale": {
        -            "description": "Clump size of the value pattern in pixels; default 4.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "seed": {
        -            "description": "Orders the noise or value pattern (default 0): the same seed keeps every pixel's threshold, so a rising density grows in a fixed order; another seed reshuffles it.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "shape": {
        -            "description": "rect (default) or the ellipse inside the region.",
        -            "enum": [
        -              "rect",
        -              "ellipse"
        -            ]
        -          },
        -          "steps": {
        -            "description": "Steps along each colour's ramp: negative darkens (default -1), positive lightens. Clamped at the ramp's ends; not 0.",
        -            "maximum": 8,
        -            "minimum": -8,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw a character map from this recipe's symbols and autotile templates, with variants, weights and neighbour rules, so later operations see the composed result (dither over its floor, rewrite rules at its corners).",
        -        "properties": {
        -          "classes": {
        -            "additionalProperties": {
        -              "maxLength": 64,
        -              "minLength": 1,
        -              "type": "string"
        -            },
        -            "description": "Characters that stand for several map characters in rule patterns, e.g. {\"#\": \"WV\"}; include the empty character to match empty cells too.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "empty": {
        -            "description": "A character, not a legend key, that stands for empty cells in rule patterns.",
        -            "maxLength": 1,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "legend": {
        -            "additionalProperties": {
        -              "oneOf": [
        -                {
        -                  "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                  "type": "string"
        -                },
        -                {
        -                  "additionalProperties": false,
        -                  "properties": {
        -                    "autotile": {
        -                      "description": "Pick by neighbour mask: 47-value blob or 16-value cardinal.",
        -                      "enum": [
        -                        "blob",
        -                        "cardinal"
        -                      ]
        -                    },
        -                    "match": {
        -                      "description": "Characters counted as the same terrain for the mask; default: this character.",
        -                      "minLength": 1,
        -                      "type": "string"
        -                    },
        -                    "symbol": {
        -                      "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                      "type": "string"
        -                    },
        -                    "symbols": {
        -                      "description": "Variants of symbol, picked by cell position (seeded when the tilemap has a seed).",
        -                      "items": {
        -                        "minLength": 1,
        -                        "type": "string"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    },
        -                    "template": {
        -                      "description": "An autotile template symbol (two tiles wide, three tall) drawn with the cell's neighbour mask; needs autotile.",
        -                      "type": "string"
        -                    },
        -                    "templates": {
        -                      "description": "Variants of template, picked by cell position (seeded when the tilemap has a seed).",
        -                      "items": {
        -                        "minLength": 1,
        -                        "type": "string"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    },
        -                    "weights": {
        -                      "description": "One whole-number weight per variant name; heavier names are picked more often.",
        -                      "items": {
        -                        "maximum": 1000,
        -                        "minimum": 1,
        -                        "type": "integer"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    }
        -                  },
        -                  "required": [],
        -                  "type": "object"
        -                },
        -                {
        -                  "additionalProperties": false,
        -                  "properties": {
        -                    "rules": {
        -                      "description": "Every rule that matches draws, in order, until one with stop draws.",
        -                      "items": {
        -                        "additionalProperties": false,
        -                        "properties": {
        -                          "autotile": {
        -                            "description": "Pick by neighbour mask: 47-value blob or 16-value cardinal.",
        -                            "enum": [
        -                              "blob",
        -                              "cardinal"
        -                            ]
        -                          },
        -                          "chance": {
        -                            "description": "Seeded probability that a matching rule draws.",
        -                            "maximum": 1,
        -                            "minimum": 0,
        -                            "type": "number"
        -                          },
        -                          "match": {
        -                            "description": "Characters counted as the same terrain for the mask; default: this character.",
        -                            "minLength": 1,
        -                            "type": "string"
        -                          },
        -                          "offset": {
        -                            "description": "[columns, rows] to draw into another cell; it draws over that cell's own tiles.",
        -                            "items": {
        -                              "maximum": 8,
        -                              "minimum": -8,
        -                              "type": "integer"
        -                            },
        -                            "maxItems": 2,
        -                            "minItems": 2,
        -                            "type": "array"
        -                          },
        -                          "stop": {
        -                            "description": "When this rule draws, skip the rest of the cell's rules.",
        -                            "type": "boolean"
        -                          },
        -                          "symbol": {
        -                            "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                            "type": "string"
        -                          },
        -                          "symbols": {
        -                            "description": "Variants of symbol, picked by cell position (seeded when the tilemap has a seed).",
        -                            "items": {
        -                              "minLength": 1,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "template": {
        -                            "description": "An autotile template symbol (two tiles wide, three tall) drawn with the cell's neighbour mask; needs autotile.",
        -                            "type": "string"
        -                          },
        -                          "templates": {
        -                            "description": "Variants of template, picked by cell position (seeded when the tilemap has a seed).",
        -                            "items": {
        -                              "minLength": 1,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "weights": {
        -                            "description": "One whole-number weight per variant name; heavier names are picked more often.",
        -                            "items": {
        -                              "maximum": 1000,
        -                              "minimum": 1,
        -                              "type": "integer"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "where": {
        -                            "description": "3×3 characters centred on the cell. A dot matches anything, the tilemap's empty character matches empty cells (and cells beyond the map), a class matches its members, any other character matches itself.",
        -                            "items": {
        -                              "maxLength": 3,
        -                              "minLength": 3,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 3,
        -                            "minItems": 3,
        -                            "type": "array"
        -                          }
        -                        },
        -                        "required": [],
        -                        "type": "object"
        -                      },
        -                      "maxItems": 32,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    }
        -                  },
        -                  "required": [
        -                    "rules"
        -                  ],
        -                  "type": "object"
        -                },
        -                {
        -                  "description": "Context cell: never drawn, but it counts for other entries' match and for rules.",
        -                  "type": "null"
        -                }
        -              ]
        -            },
        -            "description": "Map characters to what they draw. Dot and space are empty cells.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "op": {
        -            "const": "tilemap"
        -          },
        -          "outside": {
        -            "description": "Cells beyond the map: empty (default), or matching every mask and rule pattern.",
        -            "enum": [
        -              "empty",
        -              "match"
        -            ]
        -          },
        -          "rows": {
        -            "items": {
        -              "maxLength": 256,
        -              "minLength": 1,
        -              "type": "string"
        -            },
        -            "maxItems": 256,
        -            "minItems": 1,
        -            "type": "array"
        -          },
        -          "seed": {
        -            "description": "Reorders seeded variant picks and rule chances; default 0 keeps position-based picks.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "tile": {
        -            "description": "Cell size [width, height] in pixels.",
        -            "items": {
        -              "maximum": 256,
        -              "minimum": 1,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "tile",
        -          "rows",
        -          "legend"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Markov-style rewrite rules on this buffer: each small match grid found on exact colors is replaced, in a seeded order, without overlapping rewrites. The region defaults to the whole canvas.",
        -        "properties": {
        -          "chance": {
        -            "description": "Seeded probability that each match is applied.",
        -            "maximum": 1,
        -            "minimum": 0,
        -            "type": "number"
        -          },
        -          "empty": {
        -            "description": "A non-palette character meaning a transparent pixel in match and erasure in replace.",
        -            "maxLength": 1,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "limit": {
        -            "description": "Most replacements per rule per step.",
        -            "maximum": 65536,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "mirror": {
        -            "description": "Also match the rules mirrored horizontally.",
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "rewrite"
        -          },
        -          "rotate": {
        -            "description": "Also match the rules turned 90°, 180° and 270°.",
        -            "type": "boolean"
        -          },
        -          "rules": {
        -            "items": {
        -              "additionalProperties": false,
        -              "properties": {
        -                "match": {
        -                  "description": "Equal-width rows of palette keys; dot or space matches anything (match) or keeps the pixel (replace); the empty character stands for transparent.",
        -                  "items": {
        -                    "maxLength": 8,
        -                    "minLength": 1,
        -                    "type": "string"
        -                  },
        -                  "maxItems": 8,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "replace": {
        -                  "description": "Equal-width rows of palette keys; dot or space matches anything (match) or keeps the pixel (replace); the empty character stands for transparent.",
        -                  "items": {
        -                    "maxLength": 8,
        -                    "minLength": 1,
        -                    "type": "string"
        -                  },
        -                  "maxItems": 8,
        -                  "minItems": 1,
        -                  "type": "array"
        -                }
        -              },
        -              "required": [
        -                "match",
        -                "replace"
        -              ],
        -              "type": "object"
        -            },
        -            "maxItems": 16,
        -            "minItems": 1,
        -            "type": "array"
        -          },
        -          "seed": {
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "steps": {
        -            "description": "Passes over every rule; stops early when a pass changes nothing.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "rules"
        -        ],
        -        "type": "object"
        -      }
        -    ]
        -  },
        -  "pixel": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "color": {
        -        "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -        "type": "string"
        -      },
        -      "x": {
        -        "maximum": 255,
        -        "minimum": 0,
        -        "type": "integer"
        -      },
        -      "y": {
        -        "maximum": 255,
        -        "minimum": 0,
        -        "type": "integer"
        -      }
        -    },
        -    "required": [
        -      "x",
        -      "y",
        -      "color"
        -    ],
        -    "type": "object"
        -  },
        -  "rows": {
        -    "description": "Equal-width text rows. Every character is a palette key; dot and space skip a pixel.",
        -    "items": {
        -      "maxLength": 256,
        -      "minLength": 1,
        -      "type": "string"
        -    },
        -    "maxItems": 256,
        -    "minItems": 1,
        -    "type": "array"
        -  }
        -}
      • addedInput schema / properties / embedRecipe
        Added value: +{
        +  "description": "Embed the recipe in the exported PNGs so they reopen in the playground.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / expect
        Added value: +{
        +  "additionalProperties": false,
        +  "description": "What the user asked for: pixel size of each frame, frames, colours, background, loop, GIF scale. The reply's check lists every file that misses it and the call that fixes it.",
        +  "properties": {
        +    "colors": {
        +      "description": "At most.",
        +      "minimum": 1,
        +      "type": "integer"
        +    },
        +    "frames": {
        +      "description": "Per animation.",
        +      "minimum": 1,
        +      "type": "integer"
        +    },
        +    "gifScale": {
        +      "maximum": 16,
        +      "minimum": 1,
        +      "type": "integer"
        +    },
        +    "height": {
        +      "minimum": 1,
        +      "type": "integer"
        +    },
        +    "loop": {
        +      "type": "boolean"
        +    },
        +    "transparent": {
        +      "description": "Background.",
        +      "type": "boolean"
        +    },
        +    "width": {
        +      "minimum": 1,
        +      "type": "integer"
        +    }
        +  },
        +  "type": "object"
        +}
      • changedInput schema / properties / gif / properties / scale / description
        Previous value: -"Default: the largest that keeps the longer side within 256 pixels."New value: +"Default: 1, the frames' own size. Enlarge (4 or 8) only for posting where a small sprite would look tiny."
      • addedInput schema / properties / overwrite
        Added value: +{
        +  "description": "Let saveAs replace existing files. Default: refuse.",
        +  "type": "boolean"
        +}
      • removedInput schema / properties / project / additionalProperties
        Removed value: -false
      • addedInput schema / properties / project / description
        Added value: +"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}. pixel_help explains it; its topic schema is the full JSON Schema. Mistakes come back with their exact path."
      • removedInput schema / properties / project / properties
        Removed value: -{
        -  "$schema": {
        -    "type": "string"
        -  },
        -  "anchor": {
        -    "description": "Default pivot for every frame, exported to the atlas as anchor (exported pixels) and pivot (anchor / source size).",
        -    "items": {
        -      "maximum": 4096,
        -      "minimum": -4096,
        -      "type": "integer"
        -    },
        -    "maxItems": 2,
        -    "minItems": 2,
        -    "type": "array"
        -  },
        -  "animations": {
        -    "additionalProperties": {
        -      "additionalProperties": false,
        -      "properties": {
        -        "direction": {
        -          "enum": [
        -            "forward",
        -            "reverse",
        -            "pingpong"
        -          ]
        -        },
        -        "frames": {
        -          "items": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "maxItems": 1024,
        -          "minItems": 1,
        -          "type": "array"
        -        },
        -        "loop": {
        -          "type": "boolean"
        -        }
        -      },
        -      "required": [
        -        "frames"
        -      ],
        -      "type": "object"
        -    },
        -    "description": "ZIP32 bundle budget: animation count + frame count + 7 support files must not exceed 65,535.",
        -    "maxProperties": 65527,
        -    "minProperties": 1,
        -    "propertyNames": {
        -      "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -      "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -      "type": "string"
        -    },
        -    "type": "object"
        -  },
        -  "background": {
        -    "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -    "type": "string"
        -  },
        -  "frames": {
        -    "items": {
        -      "$ref": "#/$defs/frame"
        -    },
        -    "maxItems": 256,
        -    "minItems": 1,
        -    "type": "array"
        -  },
        -  "height": {
        -    "maximum": 256,
        -    "minimum": 1,
        -    "type": "integer"
        -  },
        -  "name": {
        -    "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -    "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -    "type": "string"
        -  },
        -  "palette": {
        -    "additionalProperties": {
        -      "pattern": "^(transparent|#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8}))$",
        -      "type": "string"
        -    },
        -    "maxProperties": 257,
        -    "properties": {
        -      "$ref": {
        -        "description": "Shared palette file, resolved by the CLI and MCP relative to this recipe. Local entries add to or override it.",
        -        "type": "string"
        -      }
        -    },
        -    "type": "object"
        -  },
        -  "ramps": {
        -    "description": "Palette keys from dark to light, one list per material. shade and scene ramp lighting move a colour along its ramp instead of inventing one. A key belongs to one ramp. A shared palette file ({\"palette\": {...}, \"ramps\": [...]}) can supply them.",
        -    "items": {
        -      "items": {
        -        "maxLength": 64,
        -        "minLength": 1,
        -        "type": "string"
        -      },
        -      "maxItems": 32,
        -      "minItems": 2,
        -      "type": "array"
        -    },
        -    "maxItems": 64,
        -    "type": "array"
        -  },
        -  "sheet": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "columns": {
        -        "maximum": 256,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "padding": {
        -        "maximum": 16,
        -        "minimum": 0,
        -        "type": "integer"
        -      },
        -      "scale": {
        -        "maximum": 16,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "trim": {
        -        "description": "Pack each frame by its visible bounds; the atlas reports trimmed rectangles with spriteSourceSize. Ignores columns.",
        -        "type": "boolean"
        -      }
        -    },
        -    "required": [],
        -    "type": "object"
        -  },
        -  "symbols": {
        -    "additionalProperties": {
        -      "$ref": "#/$defs/rows"
        -    },
        -    "propertyNames": {
        -      "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -      "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -      "type": "string"
        -    },
        -    "type": "object"
        -  },
        -  "version": {
        -    "const": 1
        -  },
        -  "width": {
        -    "maximum": 256,
        -    "minimum": 1,
        -    "type": "integer"
        -  }
        -}
      • removedInput schema / properties / project / required
        Removed value: -[
        -  "version",
        -  "name",
        -  "width",
        -  "height",
        -  "frames"
        -]
      • removedInput schema / properties / project / title
        Removed value: -"Pixelbolt project"
      • addedInput schema / properties / saveAs
        Added value: +{
        +  "additionalProperties": {
        +    "type": "string"
        +  },
        +  "description": "Also write files where the user asked, relative to the server root: keys sheet, atlas, recipe, gif or apng (one animation), or a listed file such as gifs/walk.gif; values such as \"assets/hero.png\". A GIF here implies gif: {}.",
        +  "type": "object"
        +}
      • addedInput schema / properties / share
        Added value: +{
        +  "description": "Also return a playground link that reopens this recipe in a browser.",
        +  "type": "boolean"
        +}
    • Changedpixel_validate6 fields changed
      • removedInput schema / $defs
        Removed value: -{
        -  "frame": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "anchor": {
        -        "description": "[x, y] in unscaled canvas pixels; the top-left corner of that pixel.",
        -        "items": {
        -          "maximum": 4096,
        -          "minimum": -4096,
        -          "type": "integer"
        -        },
        -        "maxItems": 2,
        -        "minItems": 2,
        -        "type": "array"
        -      },
        -      "duration": {
        -        "description": "Frame duration in milliseconds; default 100.",
        -        "maximum": 60000,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "flipX": {
        -        "type": "boolean"
        -      },
        -      "flipY": {
        -        "type": "boolean"
        -      },
        -      "from": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "layers": {
        -        "items": {
        -          "$ref": "#/$defs/layer"
        -        },
        -        "maxItems": 64,
        -        "type": "array"
        -      },
        -      "name": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "ops": {
        -        "items": {
        -          "$ref": "#/$defs/op"
        -        },
        -        "maxItems": 2048,
        -        "type": "array"
        -      },
        -      "palette": {
        -        "additionalProperties": {
        -          "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -          "type": "string"
        -        },
        -        "description": "Recolor project palette keys for this frame only (palette cycling). Keys must exist in the project palette.",
        -        "maxProperties": 256,
        -        "type": "object"
        -      },
        -      "pixels": {
        -        "description": "Final canvas-coordinate RGBA replacements after ops and layers. Coordinates must be inside this canvas. Later entries win; transparent erases.",
        -        "items": {
        -          "$ref": "#/$defs/pixel"
        -        },
        -        "maxItems": 65536,
        -        "type": "array"
        -      },
        -      "points": {
        -        "additionalProperties": {
        -          "description": "[x, y] in unscaled canvas pixels; the top-left corner of that pixel.",
        -          "items": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "maxItems": 2,
        -          "minItems": 2,
        -          "type": "array"
        -        },
        -        "description": "Named frame positions (hands, hit points, glyph advance), exported in the atlas in exported pixels.",
        -        "maxProperties": 64,
        -        "propertyNames": {
        -          "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -          "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -          "type": "string"
        -        },
        -        "type": "object"
        -      },
        -      "translate": {
        -        "items": {
        -          "maximum": 4096,
        -          "minimum": -4096,
        -          "type": "integer"
        -        },
        -        "maxItems": 2,
        -        "minItems": 2,
        -        "type": "array"
        -      },
        -      "wrap": {
        -        "description": "Translation wraps around the canvas instead of clipping (scrolling tiles).",
        -        "type": "boolean"
        -      }
        -    },
        -    "required": [
        -      "name"
        -    ],
        -    "type": "object"
        -  },
        -  "layer": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "name": {
        -        "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -        "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -        "type": "string"
        -      },
        -      "opacity": {
        -        "maximum": 1,
        -        "minimum": 0,
        -        "type": "number"
        -      },
        -      "ops": {
        -        "items": {
        -          "$ref": "#/$defs/op"
        -        },
        -        "maxItems": 2048,
        -        "type": "array"
        -      },
        -      "palette": {
        -        "additionalProperties": {
        -          "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -          "type": "string"
        -        },
        -        "description": "Recolor project palette keys for this layer's drawing only, on top of the frame palette: a flash or cycle on one layer leaves other drawing that shares the key alone. Keys must exist in the project palette; values are colours or project palette names.",
        -        "maxProperties": 256,
        -        "type": "object"
        -      },
        -      "visible": {
        -        "type": "boolean"
        -      },
        -      "x": {
        -        "maximum": 4096,
        -        "minimum": -4096,
        -        "type": "integer"
        -      },
        -      "y": {
        -        "maximum": 4096,
        -        "minimum": -4096,
        -        "type": "integer"
        -      }
        -    },
        -    "required": [],
        -    "type": "object"
        -  },
        -  "op": {
        -    "oneOf": [
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "pixel"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "filled": {
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "rect"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "filled": {
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "ellipse"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "line"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "x2": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y2": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "x2",
        -          "y2",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "clear"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "w",
        -          "h"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "fill"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "from": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "op": {
        -            "const": "replace"
        -          },
        -          "to": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "from",
        -          "to"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "grid"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "rows": {
        -            "$ref": "#/$defs/rows"
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "rows"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "stamp"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "symbol"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw text in the built-in pixel font. y is the top of the capitals; a line is 8 rows: capitals 7 tall, lowercase 5, descenders 1 below, and the accent of a capital reaches 2 rows above y. Glyphs are 1 to 5 pixels wide (© ® — Œ œ are 7), digits always 5, a space 3. One line of n average characters is about 6n pixels wide.",
        -        "properties": {
        -          "align": {
        -            "description": "What x is: the left edge of the text (default), its centre or its right edge. Lines align the same way.",
        -            "enum": [
        -              "left",
        -              "center",
        -              "right"
        -            ]
        -          },
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "lineHeight": {
        -            "description": "Pixels from the top of one line to the top of the next; default 10.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "text"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "spacing": {
        -            "description": "Pixels between glyphs; default 1.",
        -            "maximum": 16,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "text": {
        -            "description": "Printable ASCII, western European accented letters and common symbols; \\n starts a new line (at most 64).",
        -            "maxLength": 512,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "text",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Copy a rectangle of a symbol (palette characters) or an earlier frame (exact RGBA).",
        -        "oneOf": [
        -          {
        -            "required": [
        -              "from"
        -            ]
        -          },
        -          {
        -            "required": [
        -              "symbol"
        -            ]
        -          }
        -        ],
        -        "properties": {
        -          "flipX": {
        -            "type": "boolean"
        -          },
        -          "flipY": {
        -            "type": "boolean"
        -          },
        -          "from": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "h": {
        -            "maximum": 256,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "op": {
        -            "const": "copy"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Symbol copies: recolor grid characters (one character each) → palette name or colour. Frame copies: swap exact colours, named by project palette key or hex (#RGB to #RRGGBBAA), for a palette name or colour.",
        -            "maxProperties": 256,
        -            "propertyNames": {
        -              "maxLength": 64,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "rotate": {
        -            "enum": [
        -              0,
        -              90,
        -              180,
        -              270
        -            ]
        -          },
        -          "scale": {
        -            "maximum": 16,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "sx": {
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "sy": {
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "w": {
        -            "maximum": 256,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw one autotile tile from a template symbol two tiles wide and three tall: the four quarters the mask selects (see the autotile template format). The autotile compiler emits one per frame, so editing the template redraws every tile.",
        -        "properties": {
        -          "mask": {
        -            "description": "Neighbour mask. Blob: N=1, NE=2, E=4, SE=8, S=16, SW=32, W=64, NW=128, reduced so a diagonal counts only with both of its sides. Cardinal: N=1, E=2, S=4, W=8.",
        -            "maximum": 255,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "mode": {
        -            "description": "blob (default) or cardinal.",
        -            "enum": [
        -              "blob",
        -              "cardinal"
        -            ]
        -          },
        -          "op": {
        -            "const": "autotile"
        -          },
        -          "remap": {
        -            "additionalProperties": {
        -              "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -              "type": "string"
        -            },
        -            "description": "Recolor grid characters for this operation only: character → palette name or colour.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "symbol": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "symbol",
        -          "mask"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Outline every visible pixel in this buffer (frame or layer): outside rings, inside edge rings or both, in chosen directions.",
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "diagonal": {
        -            "type": "boolean"
        -          },
        -          "directions": {
        -            "description": "3×3 grid centred on a visible pixel, with a dot in the centre: x marks the sides that get the line, e.g. [\"...\", \"...\", \"..x\"] for a drop shadow or [\".x.\", \"...\", \"...\"] for a top rim. Replaces diagonal.",
        -            "items": {
        -              "pattern": "^[x.]{3}$",
        -              "type": "string"
        -            },
        -            "maxItems": 3,
        -            "minItems": 3,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "outline"
        -          },
        -          "position": {
        -            "description": "outside (default) surrounds the visible pixels; inside recolors their edge pixels; middle splits the width, the odd pixel outside.",
        -            "enum": [
        -              "outside",
        -              "inside",
        -              "middle"
        -            ]
        -          },
        -          "width": {
        -            "description": "Rings of pixels; default 1.",
        -            "maximum": 8,
        -            "minimum": 1,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "color"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "anyOf": [
        -          {
        -            "required": [
        -              "color"
        -            ]
        -          },
        -          {
        -            "required": [
        -              "erase"
        -            ]
        -          }
        -        ],
        -        "description": "Ordered dither between the current pixels and color: draws (or erases) where a canvas-anchored threshold pattern is below the density (default 0.5). x/y default to 0 and w/h reach the canvas edge.",
        -        "properties": {
        -          "color": {
        -            "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -            "type": "string"
        -          },
        -          "density": {
        -            "description": "Share of pixels changed, 0–1, or [from, to] ramping along direction.",
        -            "oneOf": [
        -              {
        -                "maximum": 1,
        -                "minimum": 0,
        -                "type": "number"
        -              },
        -              {
        -                "items": {
        -                  "maximum": 1,
        -                  "minimum": 0,
        -                  "type": "number"
        -                },
        -                "maxItems": 2,
        -                "minItems": 2,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "direction": {
        -            "description": "Ramp direction for a [from, to] density; radial runs from the centre to the edge.",
        -            "enum": [
        -              "down",
        -              "up",
        -              "right",
        -              "left",
        -              "radial"
        -            ]
        -          },
        -          "erase": {
        -            "description": "Clear the pattern pixels instead of drawing a color.",
        -            "type": "boolean"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "offset": {
        -            "description": "Shift the pattern, for example to crawl it between frames.",
        -            "items": {
        -              "maximum": 4096,
        -              "minimum": -4096,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "dither"
        -          },
        -          "over": {
        -            "description": "Only change pixels whose exact current color is listed (transparent included).",
        -            "oneOf": [
        -              {
        -                "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                "type": "string"
        -              },
        -              {
        -                "items": {
        -                  "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                  "type": "string"
        -                },
        -                "maxItems": 64,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "pattern": {
        -            "description": "bayer4 by default. noise is seeded white noise with no repeating grid; value is seeded smooth noise that forms clumps about scale pixels across. Patterns follow canvas coordinates, so neighbouring areas line up.",
        -            "oneOf": [
        -              {
        -                "enum": [
        -                  "bayer2",
        -                  "bayer4",
        -                  "bayer8",
        -                  "noise",
        -                  "value"
        -                ]
        -              },
        -              {
        -                "description": "Rows of ranks from 0 to cells − 1; low ranks turn on first, and a highest rank n gives n + 1 levels, so [[0, 1], [1, 0]] is a checkerboard at 0.5.",
        -                "items": {
        -                  "items": {
        -                    "maximum": 255,
        -                    "minimum": 0,
        -                    "type": "integer"
        -                  },
        -                  "maxItems": 16,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "maxItems": 16,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "scale": {
        -            "description": "Clump size of the value pattern in pixels; default 4.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "seed": {
        -            "description": "Orders the noise or value pattern (default 0): the same seed keeps every pixel's threshold, so a rising density grows in a fixed order; another seed reshuffles it.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Move every pixel of the region along its colour's ramp (see ramps), so shadows and light keep the palette on any surface. Colours on no ramp stay. density (default 1) and pattern give a dithered edge. x/y default to 0 and w/h reach the canvas edge.",
        -        "properties": {
        -          "density": {
        -            "description": "Share of pixels changed, 0–1, or [from, to] ramping along direction.",
        -            "oneOf": [
        -              {
        -                "maximum": 1,
        -                "minimum": 0,
        -                "type": "number"
        -              },
        -              {
        -                "items": {
        -                  "maximum": 1,
        -                  "minimum": 0,
        -                  "type": "number"
        -                },
        -                "maxItems": 2,
        -                "minItems": 2,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "direction": {
        -            "description": "Ramp direction for a [from, to] density; radial runs from the centre to the edge.",
        -            "enum": [
        -              "down",
        -              "up",
        -              "right",
        -              "left",
        -              "radial"
        -            ]
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "offset": {
        -            "description": "Shift the pattern, for example to crawl it between frames.",
        -            "items": {
        -              "maximum": 4096,
        -              "minimum": -4096,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "op": {
        -            "const": "shade"
        -          },
        -          "over": {
        -            "description": "Only change pixels whose exact current color is listed (transparent included).",
        -            "oneOf": [
        -              {
        -                "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                "type": "string"
        -              },
        -              {
        -                "items": {
        -                  "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -                  "type": "string"
        -                },
        -                "maxItems": 64,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "pattern": {
        -            "description": "bayer4 by default. noise is seeded white noise with no repeating grid; value is seeded smooth noise that forms clumps about scale pixels across. Patterns follow canvas coordinates, so neighbouring areas line up.",
        -            "oneOf": [
        -              {
        -                "enum": [
        -                  "bayer2",
        -                  "bayer4",
        -                  "bayer8",
        -                  "noise",
        -                  "value"
        -                ]
        -              },
        -              {
        -                "description": "Rows of ranks from 0 to cells − 1; low ranks turn on first, and a highest rank n gives n + 1 levels, so [[0, 1], [1, 0]] is a checkerboard at 0.5.",
        -                "items": {
        -                  "items": {
        -                    "maximum": 255,
        -                    "minimum": 0,
        -                    "type": "integer"
        -                  },
        -                  "maxItems": 16,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "maxItems": 16,
        -                "minItems": 1,
        -                "type": "array"
        -              }
        -            ]
        -          },
        -          "scale": {
        -            "description": "Clump size of the value pattern in pixels; default 4.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "seed": {
        -            "description": "Orders the noise or value pattern (default 0): the same seed keeps every pixel's threshold, so a rising density grows in a fixed order; another seed reshuffles it.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "shape": {
        -            "description": "rect (default) or the ellipse inside the region.",
        -            "enum": [
        -              "rect",
        -              "ellipse"
        -            ]
        -          },
        -          "steps": {
        -            "description": "Steps along each colour's ramp: negative darkens (default -1), positive lightens. Clamped at the ramp's ends; not 0.",
        -            "maximum": 8,
        -            "minimum": -8,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Draw a character map from this recipe's symbols and autotile templates, with variants, weights and neighbour rules, so later operations see the composed result (dither over its floor, rewrite rules at its corners).",
        -        "properties": {
        -          "classes": {
        -            "additionalProperties": {
        -              "maxLength": 64,
        -              "minLength": 1,
        -              "type": "string"
        -            },
        -            "description": "Characters that stand for several map characters in rule patterns, e.g. {\"#\": \"WV\"}; include the empty character to match empty cells too.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "empty": {
        -            "description": "A character, not a legend key, that stands for empty cells in rule patterns.",
        -            "maxLength": 1,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "legend": {
        -            "additionalProperties": {
        -              "oneOf": [
        -                {
        -                  "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                  "type": "string"
        -                },
        -                {
        -                  "additionalProperties": false,
        -                  "properties": {
        -                    "autotile": {
        -                      "description": "Pick by neighbour mask: 47-value blob or 16-value cardinal.",
        -                      "enum": [
        -                        "blob",
        -                        "cardinal"
        -                      ]
        -                    },
        -                    "match": {
        -                      "description": "Characters counted as the same terrain for the mask; default: this character.",
        -                      "minLength": 1,
        -                      "type": "string"
        -                    },
        -                    "symbol": {
        -                      "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                      "type": "string"
        -                    },
        -                    "symbols": {
        -                      "description": "Variants of symbol, picked by cell position (seeded when the tilemap has a seed).",
        -                      "items": {
        -                        "minLength": 1,
        -                        "type": "string"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    },
        -                    "template": {
        -                      "description": "An autotile template symbol (two tiles wide, three tall) drawn with the cell's neighbour mask; needs autotile.",
        -                      "type": "string"
        -                    },
        -                    "templates": {
        -                      "description": "Variants of template, picked by cell position (seeded when the tilemap has a seed).",
        -                      "items": {
        -                        "minLength": 1,
        -                        "type": "string"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    },
        -                    "weights": {
        -                      "description": "One whole-number weight per variant name; heavier names are picked more often.",
        -                      "items": {
        -                        "maximum": 1000,
        -                        "minimum": 1,
        -                        "type": "integer"
        -                      },
        -                      "maxItems": 64,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    }
        -                  },
        -                  "required": [],
        -                  "type": "object"
        -                },
        -                {
        -                  "additionalProperties": false,
        -                  "properties": {
        -                    "rules": {
        -                      "description": "Every rule that matches draws, in order, until one with stop draws.",
        -                      "items": {
        -                        "additionalProperties": false,
        -                        "properties": {
        -                          "autotile": {
        -                            "description": "Pick by neighbour mask: 47-value blob or 16-value cardinal.",
        -                            "enum": [
        -                              "blob",
        -                              "cardinal"
        -                            ]
        -                          },
        -                          "chance": {
        -                            "description": "Seeded probability that a matching rule draws.",
        -                            "maximum": 1,
        -                            "minimum": 0,
        -                            "type": "number"
        -                          },
        -                          "match": {
        -                            "description": "Characters counted as the same terrain for the mask; default: this character.",
        -                            "minLength": 1,
        -                            "type": "string"
        -                          },
        -                          "offset": {
        -                            "description": "[columns, rows] to draw into another cell; it draws over that cell's own tiles.",
        -                            "items": {
        -                              "maximum": 8,
        -                              "minimum": -8,
        -                              "type": "integer"
        -                            },
        -                            "maxItems": 2,
        -                            "minItems": 2,
        -                            "type": "array"
        -                          },
        -                          "stop": {
        -                            "description": "When this rule draws, skip the rest of the cell's rules.",
        -                            "type": "boolean"
        -                          },
        -                          "symbol": {
        -                            "description": "A symbol drawn at the cell's top-left; with autotile, {mask} in its name becomes the neighbour mask.",
        -                            "type": "string"
        -                          },
        -                          "symbols": {
        -                            "description": "Variants of symbol, picked by cell position (seeded when the tilemap has a seed).",
        -                            "items": {
        -                              "minLength": 1,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "template": {
        -                            "description": "An autotile template symbol (two tiles wide, three tall) drawn with the cell's neighbour mask; needs autotile.",
        -                            "type": "string"
        -                          },
        -                          "templates": {
        -                            "description": "Variants of template, picked by cell position (seeded when the tilemap has a seed).",
        -                            "items": {
        -                              "minLength": 1,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "weights": {
        -                            "description": "One whole-number weight per variant name; heavier names are picked more often.",
        -                            "items": {
        -                              "maximum": 1000,
        -                              "minimum": 1,
        -                              "type": "integer"
        -                            },
        -                            "maxItems": 64,
        -                            "minItems": 1,
        -                            "type": "array"
        -                          },
        -                          "where": {
        -                            "description": "3×3 characters centred on the cell. A dot matches anything, the tilemap's empty character matches empty cells (and cells beyond the map), a class matches its members, any other character matches itself.",
        -                            "items": {
        -                              "maxLength": 3,
        -                              "minLength": 3,
        -                              "type": "string"
        -                            },
        -                            "maxItems": 3,
        -                            "minItems": 3,
        -                            "type": "array"
        -                          }
        -                        },
        -                        "required": [],
        -                        "type": "object"
        -                      },
        -                      "maxItems": 32,
        -                      "minItems": 1,
        -                      "type": "array"
        -                    }
        -                  },
        -                  "required": [
        -                    "rules"
        -                  ],
        -                  "type": "object"
        -                },
        -                {
        -                  "description": "Context cell: never drawn, but it counts for other entries' match and for rules.",
        -                  "type": "null"
        -                }
        -              ]
        -            },
        -            "description": "Map characters to what they draw. Dot and space are empty cells.",
        -            "propertyNames": {
        -              "maxLength": 1,
        -              "minLength": 1
        -            },
        -            "type": "object"
        -          },
        -          "op": {
        -            "const": "tilemap"
        -          },
        -          "outside": {
        -            "description": "Cells beyond the map: empty (default), or matching every mask and rule pattern.",
        -            "enum": [
        -              "empty",
        -              "match"
        -            ]
        -          },
        -          "rows": {
        -            "items": {
        -              "maxLength": 256,
        -              "minLength": 1,
        -              "type": "string"
        -            },
        -            "maxItems": 256,
        -            "minItems": 1,
        -            "type": "array"
        -          },
        -          "seed": {
        -            "description": "Reorders seeded variant picks and rule chances; default 0 keeps position-based picks.",
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "tile": {
        -            "description": "Cell size [width, height] in pixels.",
        -            "items": {
        -              "maximum": 256,
        -              "minimum": 1,
        -              "type": "integer"
        -            },
        -            "maxItems": 2,
        -            "minItems": 2,
        -            "type": "array"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "tile",
        -          "rows",
        -          "legend"
        -        ],
        -        "type": "object"
        -      },
        -      {
        -        "additionalProperties": false,
        -        "description": "Markov-style rewrite rules on this buffer: each small match grid found on exact colors is replaced, in a seeded order, without overlapping rewrites. The region defaults to the whole canvas.",
        -        "properties": {
        -          "chance": {
        -            "description": "Seeded probability that each match is applied.",
        -            "maximum": 1,
        -            "minimum": 0,
        -            "type": "number"
        -          },
        -          "empty": {
        -            "description": "A non-palette character meaning a transparent pixel in match and erasure in replace.",
        -            "maxLength": 1,
        -            "minLength": 1,
        -            "type": "string"
        -          },
        -          "h": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "limit": {
        -            "description": "Most replacements per rule per step.",
        -            "maximum": 65536,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "mirror": {
        -            "description": "Also match the rules mirrored horizontally.",
        -            "type": "boolean"
        -          },
        -          "op": {
        -            "const": "rewrite"
        -          },
        -          "rotate": {
        -            "description": "Also match the rules turned 90°, 180° and 270°.",
        -            "type": "boolean"
        -          },
        -          "rules": {
        -            "items": {
        -              "additionalProperties": false,
        -              "properties": {
        -                "match": {
        -                  "description": "Equal-width rows of palette keys; dot or space matches anything (match) or keeps the pixel (replace); the empty character stands for transparent.",
        -                  "items": {
        -                    "maxLength": 8,
        -                    "minLength": 1,
        -                    "type": "string"
        -                  },
        -                  "maxItems": 8,
        -                  "minItems": 1,
        -                  "type": "array"
        -                },
        -                "replace": {
        -                  "description": "Equal-width rows of palette keys; dot or space matches anything (match) or keeps the pixel (replace); the empty character stands for transparent.",
        -                  "items": {
        -                    "maxLength": 8,
        -                    "minLength": 1,
        -                    "type": "string"
        -                  },
        -                  "maxItems": 8,
        -                  "minItems": 1,
        -                  "type": "array"
        -                }
        -              },
        -              "required": [
        -                "match",
        -                "replace"
        -              ],
        -              "type": "object"
        -            },
        -            "maxItems": 16,
        -            "minItems": 1,
        -            "type": "array"
        -          },
        -          "seed": {
        -            "maximum": 2147483647,
        -            "minimum": 0,
        -            "type": "integer"
        -          },
        -          "steps": {
        -            "description": "Passes over every rule; stops early when a pass changes nothing.",
        -            "maximum": 64,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "w": {
        -            "maximum": 512,
        -            "minimum": 1,
        -            "type": "integer"
        -          },
        -          "x": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          },
        -          "y": {
        -            "maximum": 4096,
        -            "minimum": -4096,
        -            "type": "integer"
        -          }
        -        },
        -        "required": [
        -          "op",
        -          "rules"
        -        ],
        -        "type": "object"
        -      }
        -    ]
        -  },
        -  "pixel": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "color": {
        -        "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -        "type": "string"
        -      },
        -      "x": {
        -        "maximum": 255,
        -        "minimum": 0,
        -        "type": "integer"
        -      },
        -      "y": {
        -        "maximum": 255,
        -        "minimum": 0,
        -        "type": "integer"
        -      }
        -    },
        -    "required": [
        -      "x",
        -      "y",
        -      "color"
        -    ],
        -    "type": "object"
        -  },
        -  "rows": {
        -    "description": "Equal-width text rows. Every character is a palette key; dot and space skip a pixel.",
        -    "items": {
        -      "maxLength": 256,
        -      "minLength": 1,
        -      "type": "string"
        -    },
        -    "maxItems": 256,
        -    "minItems": 1,
        -    "type": "array"
        -  }
        -}
      • removedInput schema / properties / project / additionalProperties
        Removed value: -false
      • addedInput schema / properties / project / description
        Added value: +"A version-1 Pixelbolt recipe: {\"version\": 1, \"name\", \"width\", \"height\", \"palette\", \"symbols\"?, \"frames\": [{\"name\", \"duration\"?, \"from\"?, \"ops\"?, \"layers\"?}], \"animations\"?}. pixel_help explains it; its topic schema is the full JSON Schema. Mistakes come back with their exact path."
      • removedInput schema / properties / project / properties
        Removed value: -{
        -  "$schema": {
        -    "type": "string"
        -  },
        -  "anchor": {
        -    "description": "Default pivot for every frame, exported to the atlas as anchor (exported pixels) and pivot (anchor / source size).",
        -    "items": {
        -      "maximum": 4096,
        -      "minimum": -4096,
        -      "type": "integer"
        -    },
        -    "maxItems": 2,
        -    "minItems": 2,
        -    "type": "array"
        -  },
        -  "animations": {
        -    "additionalProperties": {
        -      "additionalProperties": false,
        -      "properties": {
        -        "direction": {
        -          "enum": [
        -            "forward",
        -            "reverse",
        -            "pingpong"
        -          ]
        -        },
        -        "frames": {
        -          "items": {
        -            "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -            "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -            "type": "string"
        -          },
        -          "maxItems": 1024,
        -          "minItems": 1,
        -          "type": "array"
        -        },
        -        "loop": {
        -          "type": "boolean"
        -        }
        -      },
        -      "required": [
        -        "frames"
        -      ],
        -      "type": "object"
        -    },
        -    "description": "ZIP32 bundle budget: animation count + frame count + 7 support files must not exceed 65,535.",
        -    "maxProperties": 65527,
        -    "minProperties": 1,
        -    "propertyNames": {
        -      "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -      "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -      "type": "string"
        -    },
        -    "type": "object"
        -  },
        -  "background": {
        -    "description": "Palette name, transparent, or #RGB/#RGBA/#RRGGBB/#RRGGBBAA.",
        -    "type": "string"
        -  },
        -  "frames": {
        -    "items": {
        -      "$ref": "#/$defs/frame"
        -    },
        -    "maxItems": 256,
        -    "minItems": 1,
        -    "type": "array"
        -  },
        -  "height": {
        -    "maximum": 256,
        -    "minimum": 1,
        -    "type": "integer"
        -  },
        -  "name": {
        -    "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -    "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -    "type": "string"
        -  },
        -  "palette": {
        -    "additionalProperties": {
        -      "pattern": "^(transparent|#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8}))$",
        -      "type": "string"
        -    },
        -    "maxProperties": 257,
        -    "properties": {
        -      "$ref": {
        -        "description": "Shared palette file, resolved by the CLI and MCP relative to this recipe. Local entries add to or override it.",
        -        "type": "string"
        -      }
        -    },
        -    "type": "object"
        -  },
        -  "ramps": {
        -    "description": "Palette keys from dark to light, one list per material. shade and scene ramp lighting move a colour along its ramp instead of inventing one. A key belongs to one ramp. A shared palette file ({\"palette\": {...}, \"ramps\": [...]}) can supply them.",
        -    "items": {
        -      "items": {
        -        "maxLength": 64,
        -        "minLength": 1,
        -        "type": "string"
        -      },
        -      "maxItems": 32,
        -      "minItems": 2,
        -      "type": "array"
        -    },
        -    "maxItems": 64,
        -    "type": "array"
        -  },
        -  "sheet": {
        -    "additionalProperties": false,
        -    "properties": {
        -      "columns": {
        -        "maximum": 256,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "padding": {
        -        "maximum": 16,
        -        "minimum": 0,
        -        "type": "integer"
        -      },
        -      "scale": {
        -        "maximum": 16,
        -        "minimum": 1,
        -        "type": "integer"
        -      },
        -      "trim": {
        -        "description": "Pack each frame by its visible bounds; the atlas reports trimmed rectangles with spriteSourceSize. Ignores columns.",
        -        "type": "boolean"
        -      }
        -    },
        -    "required": [],
        -    "type": "object"
        -  },
        -  "symbols": {
        -    "additionalProperties": {
        -      "$ref": "#/$defs/rows"
        -    },
        -    "propertyNames": {
        -      "description": "Portable name: do not use Windows device names (CON, PRN, AUX, NUL, COM1–9, LPT1–9). Frame/animation names must be unique ignoring case.",
        -      "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
        -      "type": "string"
        -    },
        -    "type": "object"
        -  },
        -  "version": {
        -    "const": 1
        -  },
        -  "width": {
        -    "maximum": 256,
        -    "minimum": 1,
        -    "type": "integer"
        -  }
        -}
      • removedInput schema / properties / project / required
        Removed value: -[
        -  "version",
        -  "name",
        -  "width",
        -  "height",
        -  "frames"
        -]
      • removedInput schema / properties / project / title
        Removed value: -"Pixelbolt project"
  3. 8 tool updatesv0.9.0
    • First observedpixel_compile
    • First observedpixel_help
    • First observedpixel_import
    • First observedpixel_inspect
    • First observedpixel_patch
    • First observedpixel_render
    • First observedpixel_scene
    • First observedpixel_validate

TDQS

A3.9/5.0

Scored across 8 tools

Disambiguation4/5

Each tool maps to a distinct stage of the pixel art pipeline (docs, validate, inspect, patch, render, compile, scene, import), and descriptions explicitly disambiguate the closest pairs (inspect for iterating vs render for export). Minor residual overlap: patch, compile and import all produce new revisions, and compile vs scene both compose content, but the guidance is clear enough to avoid misselection.

Naming Consistency5/5

Every tool uses the identical pixel_<verb> pattern with single, concrete verbs (help, validate, inspect, patch, render, compile, scene, import). Fully predictable and easy to scan.

Tool Count5/5

Eight tools is well within the ideal range and each earns its place by covering a distinct pipeline stage. Nothing looks redundant or padded.

Completeness4/5

The surface covers a full authoring lifecycle: learn the format, import existing art, build from sources, tweak, validate, preview, export, and compose scenes. Gaps are minor — revisions and projects are referenced as inputs but there is no tool to list, browse or delete them, and no plain create-from-scratch recipe entry point beyond help/compile.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI coding agents to create, draw, and refine Aseprite documents through deterministic canvas, layer, frame, tag, and palette tools, with pixel analyzers, style validation, and preview/diff visual feedback. It also supports spritesheet and animated GIF export, producing multi-layer game-ready assets.
    1
    MIT
  • A
    license
    B
    quality
    C
    maintenance
    Enables users to automate the creation, editing, animation, and export of pixel-art game assets through a multi-agent pipeline that derives style specs and blueprints, generates Lua build scripts, and drives the canvas editor to produce .aseprite files, spritesheets, atlases, GIFs, and previews.
    12
    91 npm
    3
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    A local stdio MCP server that lets agents describe, render, inspect, and preview procedural 2D pixel-art skill effects and baked sprite animations entirely offline. It reads supplied local PNG/JSON projects and writes transparent sprite sheets, playback JSON, and image previews to a chosen local directory.
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to create layered pixel-art sprites and animations, inspect pixels, and export images, spritesheets, GIFs, APNGs, and TrueType pixel fonts. Documents and edits stay local, accessible over stdio MCP or callable directly from a shell.
    22
    MIT