Build Blueprint tools with your own agent
Already pay for Claude Code, Codex or Cursor? Connect it to Blueprint over MCP. Your agent writes the tool on your own subscription; Blueprint checks it, saves each version, and runs it in your Studio, where you tune it, export it, publish it and share it. No credits are used.
Connect in two minutes
Claude app (desktop or web): Settings › Connectors › Add custom connector, paste https://blueprint.page/api/mcp, click Connect, sign in to Blueprint and allow it. Nothing to copy.
Claude Code, Codex, Cursor:
- In Blueprint, open Settings › Connect your agent and create a token. It starts with
bp_and is shown once. - Add Blueprint to your agent with the line shown there. For Claude Code:
Codex and Cursor get their own snippet (claude mcp add --transport http blueprint https://blueprint.page/api/mcp --header "Authorization: Bearer bp_…"~/.codex/config.toml,~/.cursor/mcp.json). - Ask: “Build a risograph halftone tool in Blueprint.” The agent reads the format, submits the tool, fixes what Blueprint reports, and gives you the Studio link.
Included in
- Solo: 1 connection, 200 saved versions a day.
- Studio: 3 connections, 1,000 saved versions a day.
- Agency: 10 connections, 5,000 saved versions a day.
Revoke a token at any time in Settings. Blueprint never sees your agent's subscription; the token only lets it read and write tools in your account.
Tools your agent can use
get_format: the manual below, the JSON Schema of a spec, and five reference tools.list_projects,list_tools,get_tool: what is in your account, with every version.create_tool,edit_tool,save_version: create a tool, change it with small find/replace edits, or save a full rewrite (long code can be sent in parts withupload_code). Blueprint runs the Studio's checks and returns every problem; an accepted version becomes the current one.get_preview: a signed link that runs the version in a browser, valid for 24 hours, to check the result before you open the Studio.publish_tool,share_tool: only when you ask for it.
The tool format (format 1)
This is exactly what your agent reads. Tools saved in format 1 keep working when the format evolves.
Working through MCP
You are the user's own coding agent, connected to their Blueprint account. Blueprint generates nothing in this flow and charges no credits: you write the tool, Blueprint validates it, stores every accepted submission as a version, and runs it in the Studio, where the user tunes it, exports it (PNG, SVG, video), publishes it and shares it.
list_projectsandlist_toolsshow what exists.create_toolcreates a tool in a project (optionally with its first version in the same call). If the user has several projects and did not say which one, ask them; otherwise use the default project (default: truein list_projects: the one they worked in last). Always tell the user which project the tool is in, with the Studio link.- Write the tool as one JSON spec (format below), following every rule of this manual.
create_tool(new tool) orsave_version(full spec) submits it, withrequest: the user's request that led to this version, in their own words (a faithful one-line summary if it was long). Blueprint keeps it in the tool's history, so the user sees in their Studio what they asked and which version answered it; never leave it empty or invent one. Either a fullspec, oreditsapplied to the current version (code_edits: exact find/replace snippets, eachfindmust appear exactly once; plus any field that changes). Blueprint runs the same checks as its Studio: the schema, the coherence of controls and presets, the static analysis of the code (forbidden APIs, syntax,defineTool) and the fonts. A rejected submission returns the list of problems and saves nothing: fix them all and submit again. An accepted one becomes the tool's current version, labelled as made by your connection.- To change an existing tool, use
edit_tool: read the current code withget_tool, then send only exact find/replace snippets (code_edits) and the fields that change, withrequest. Never resend the whole code for a change: large calls are slow, and some clients give up before they reach Blueprint. Resend a full spec only for a new tool or a deep rewrite; if that code is longer than about 15,000 characters, send it in parts withupload_code(one call per part, in order), then callsave_versionwithcode_upload_id. - Validation is static: Blueprint does not run the code.
get_previewreturns a signed link to a page that runs the version in a browser (valid 24 hours). Give it to the user and ask them to check the render; if they report an error (the page shows it on the canvas), fix it and save a new version. Never claim the tool renders correctly before someone has looked at it. - Give the user the Studio link (
studio_url) so they can see and tune the tool. Never claim it works if you have not checked it. publish_toolandshare_toolonly when the user asks for it.- If a call fails without a message from Blueprint (a connection error on your side), do not assume it was too large: call
get_toolto see whether the version was saved, then retry with a smaller call (edit_toolorupload_code).
The spec (format 1)
{
"format": 1,
"name": "Riso Halftone",
"description": "One sentence, at most 300 characters.",
"renderer": "canvas2d",
"animated": false,
"sections": [
{ "label": "Grid", "controls": [
{ "type": "slider", "key": "cell", "label": "Cell size", "min": 4, "max": 40, "step": 1, "default": 12 },
{ "type": "select", "key": "shape", "label": "Shape", "options": [{ "value": "dot", "label": "Dot" }, { "value": "line", "label": "Line" }], "default": "dot" },
{ "type": "seed", "key": "seed", "label": "Seed", "default": 7 }
] }
],
"presets": [ { "name": "Coarse", "values": { "cell": 32, "shape": "line" } } ],
"code": "defineTool({ render(ctx, p) { /* … */ } });"
}name1–40 characters,descriptionat most 300,renderer"canvas2d" or "webgl2", 1 to 5 sections of at least one control, at most 6 presets,code20 to 60,000 characters. The JSON Schema is the resource blueprint://format/schema.- Keys are unique across all sections. A slider's default lies between min and max, with more than two steps. A select's default is one of its option values.
- Preset
valuesis an object of control key → value (numbers for sliders and seeds, booleans for toggles, option values for selects, "#rrggbb" for colors, a JSON string for type controls); only keys of controls that hold a value. A list of{ "key", "value" }pairs is accepted too. formatis optional (it defaults to 1). A spec in a format this server does not support is refused with the supported list.- Small formatting slips are fixed on the way in, as in the Studio ("22" for 22, "#fff" for "#ffffff", a slider default outside its range). Everything else is reported, never silently changed.
- The five reference tools are full specs, in the resources blueprint://examples/<slug>: riso-halftone, dither-studio, truchet-patterns, fluid-gradient, enamel-pin.
Limits
- Code: at most 60,000 characters per tool. One request: at most 400 KB. Keep each call's arguments under about 20,000 characters (edits, or code parts of 15,000 characters at most).
- Each call answers within 40 seconds, or returns an error saying so.
- No credits are used. Each account has a number of agent connections, accepted versions per day and calls per minute, set by its plan (Solo, Studio, Agency). A refusal says which limit was reached and when it resets.
How a tool works
A tool is exactly two things:
- A declarative list of controls, grouped in sections. The host app renders them as a consistent panel on the right side of the screen. You never write UI.
- A JavaScript render module that draws into a single canvas, using the parameter values the user picks.
The host owns everything else: the layout, the canvas size and aspect ratio, presets menu, exports (copy or download PNG, and SVG for tools drawn with paths, at 1×; MP4/WebM video: a fixed-duration render for animated tools and a live recording for every tool), undo, sharing, capturing the user's camera and microphone for tools that declare them, importing, playing and analysing sound files (audio controls), and playing the sounds your code asks for (kit.sound) at the visitor's volume, only after they click. Your job is to make the render module beautiful, fast, and expressive through well-chosen parameters.
The code contract
The code is a classic script (not a module: no import, no export). It must call defineTool exactly once:
defineTool({
animated: false, // true only if the image changes over time (uses ctx.time)
setup(ctx, p) { }, // optional, called once after load: compile shaders, allocate buffers
render(ctx, p) { }, // required, draws one complete frame
resize(ctx, p) { }, // optional, called when the canvas size changes
actions: { clear(ctx, p, rand) { } }, // optional, one function per button with action "code" (see Controls)
});Top-level helper functions and constants are allowed above the defineTool call.
ctx (provided by the runtime):
ctx.g: CanvasRenderingContext2D when renderer is "canvas2d", otherwise null. Its transform, alpha and composite mode are reset before each render.ctx.gl: WebGL2RenderingContext when renderer is "webgl2", otherwise null.ctx.canvas,ctx.width,ctx.height: canvas size in device pixels. The aspect ratio is chosen by the user, so never assume a square or a fixed size.ctx.px: device pixels per design pixel. It is 1–2 on screen and up to 8 during high-resolution exports. Multiply EVERY parameter expressed in pixels (cell size, stroke width, margin, font size, blur radius) byctx.px, so an export at ×4 looks identical to the preview, only sharper.ctx.time(seconds since load) andctx.frame: only meaningful when animated is true. Animated tools get a fixed-duration video render automatically (the host steps ctx.time frame by frame), and the host can record any tool live while the user draws or moves sliders: never build your own recorder, timeline or video export.ctx.state: an empty object you own, kept between frames. Use it to cache expensive work (fitted images, precomputed grids, compiled programs). Invalidate caches by comparing the inputs they depend on (for examplestate.src !== p.image || state.w !== w).ctx.pointer: { x, y, down, inside, pressed, released } in device pixels.pressed/releasedare true on the first render after the button went down / up (never missed, even for a click shorter than a frame): use them for one-shot actions (place a note, toggle a cell, trigger a sound). Only use the pointer if the request asks for interaction.ctx.download(data, filename): only when the user asks for an export format the Export menu does not already have. PNG and video are built in, and so is SVG for canvas2d tools drawn with paths (the host records the drawing calls): never add buttons for those. Only when the user asks for a bigger PNG, add a button with action "export" and key "png2x" or "png4x" (label "PNG 2×" / "PNG 4×"): the host renders it at that scale, with no action in the code. A WebGL tool, or one that reads or writes pixels, has no built-in SVG; it may get its own. Call it from the action of a button with action "export" (labeled "Export SVG", "Export JSON"…): the button appears in the Export menu next to the built-in formats, never in the controls. data is a string, a Blob, a canvas or akit.buffer(saved as PNG); the filename extension sets the type (svg, png, jpg, webp, gif, pdf, json, csv, txt, obj, stl, zip). Downloads only work right after a button click.
p: the current parameter values, keyed by each control's key. Sliders and seeds are numbers, toggles booleans, selects the chosen option value, colors "#rrggbb" strings, text controls strings, type controls a typography object for kit.type (see Controls). An image control is ALWAYS an image object, never null: when the user has not imported anything, it falls back to a grayscale noise field (kit.noiseImage('warp', 7)). A camera control is likewise ALWAYS an image object (the live webcam frame, refreshed about 30 times per second, already mirrored when the user wants it, at most 640 px on its long side), and a microphone control ALWAYS an audio object (see Controls); both have live: true while the user's device is on and live: false for the fallback. An audio control is ALWAYS a track object (see Controls), with loaded: false and a synthetic 120 bpm fallback until the user imports a sound.
The kit library (global kit)
Use it instead of reimplementing things. It is the only library available.
kit.math:TAU, clamp(v,a,b), lerp(a,b,t), map(v,a,b,c,d), fract(v), smoothstep(a,b,v).kit.rand(seed)→ function returning a deterministic float in [0,1). Also.range(a,b),.int(a,b)(inclusive),.pick(array),.chance(p),.gauss(). Create a fresh generator from the seed at the start of each render so the image is stable from frame to frame.kit.noise(seed)→ {simplex2(x,y),simplex3(x,y,z)(both in [-1,1]),fbm2(x,y,octaves=5),fbm3(x,y,z,octaves=5)(≈[-1,1]),ridged2(x,y,octaves=5)(≈[0,1]),worley2(x,y)(distance to the nearest feature point, ≈[0,1.2]) }. Creating it costs ~1 ms: create it once per render, never per pixel.kit.color:parse(hex) → [r,g,b](0–255),css(c, alpha?) → "rgb(…)"/"rgba(…)",hex(c),mix(a, b, t) → [r,g,b](perceptual OKLab blend; a and b can be hex strings or [r,g,b]),ramp([c0, c1, …], t) → [r,g,b],luminance(c) → [0,1],hsl(h°, s, l) → [r,g,b].kit.dither:threshold(x, y, kind) → [0,1)for ordered dithering, kind is "bayer2" | "bayer4" | "bayer8" | "ign" (interleaved gradient noise);bayer(n) → Float32Array(n*n);diffuse(values, w, h, "floyd" | "atkinson", levels=2) → Float32Array(error diffusion on values in [0,1], returns quantized values).deband(g, strength=1): removes 8-bit banding from smooth tones; call it as the very last step of render in canvas2d (see design rule 11).- Image objects (from image controls, or
kit.image(canvasOrBitmap)):{ width, height, source },lum(u,v) → [0,1]andrgb(u,v) → [r,g,b]with u,v normalized in [0,1] (nearest pixel, fast),pixels() → Uint8ClampedArray RGBA,drawCover(g, w, h)draws it cropped to fill a w×h area,fit(w, h, aspect?) → imagereturns a new w×h image of the area the canvas shows ("cover" crop at the canvas aspect ratio by default, or ataspect= width/height of the area those samples cover). So a grid of cols×rows cells over the whole canvas samplesp.image.fit(cols, rows)and stays in proportion even when cells are not square (ASCII characters, tall dots); for a sub-area, pass its aspect. Cache the result in ctx.state. kit.noiseImage(kind, seed) → image object: a full-contrast grayscale noise field, cached. kind is "warp" (domain-warped organic shapes), "ridged" (sharp ridges, marble-like veins), "cells" (Voronoi cells), "blobs" (large soft islands) or "flow" (wavy contour bands). Use it like an imported image (lum(u,v),fit(w,h)…).kit.type: typesetting with real fonts (HarfBuzz: kerning, ligatures, OpenType features, variable axes), for a type control's objectt = p.title(or a copy:{ ...p.title, size: p.title.size * 2 }).t.size,t.paragraph,t.indentare already in device pixels (× ctx.px);t.fontis a readyctx.fontstring;t.readyis false while the font loads (the tool is re-rendered when it arrives).- ↳
kit.type.draw(g, text, t, x, y, opts?) → layout: fills the text with the current fillStyle, applying every setting (tracking, word spacing, leading, case, decoration, features, axes; "\n" starts a paragraph). opts:{ width?, align?, maxLines?, valign? }— width (device px) wraps lines (justify needs it); without it, x is the left edge, the center or the right edge depending on align; y is the top of the block (valign "middle"/"bottom" to center or bottom-align it). maxLines ends with "…". - ↳
kit.type.layout(text, t, opts?) → { lines: [{ x, y (baseline), width, text, glyphs: [{ x, y, advance, char, d }] }], width, height, ascent, descent, lineHeight }: the same layout without drawing, positioned with opts.x / opts.y like draw (default 0, 0). A glyph's x, y are already absolute in that layout (its line's x and baseline are included): never add line.x or line.y to them, for per-glyph work (kinetic type, glyphs on a path, distortions): draw each glyph withkit.type.glyph(g, gl, t, x, y), which puts the glyph's origin (left edge, baseline) at x, y: in place withkit.type.glyph(g, gl, t, gl.x, gl.y), or moved withg.save(); g.translate(gl.x, gl.y); g.rotate(a); kit.type.glyph(g, gl, t, 0, 0); g.restore(). It fills the outline, or the character while the font is still loading (gl.dis empty then, so never drawgl.dyourself). - ↳
kit.type.path(text, t, x, y, opts?) → Path2Dof the whole block (outlines, clipping masks, strokes);kit.type.measure(text, t, opts?) → { width, height, ascent, descent };kit.type.metrics(t) → { ascender, descender, xHeight, capHeight, lineHeight }in px.
A single word or a short title that must fit a width: scale it (const k = maxW / kit.type.measure(text, t).width; const fit = { ...t, size: t.size * Math.min(1, k) }) instead of wrapping it, since wrapping breaks a word that is too long with a hyphen.
Text drawn with kit.type exports as real outlines in SVG. Never pick a font in code: the font is whatever the user chose in the type control.
kit.sound: makes sound. The host plays it (never before the user clicks, at their volume, limited to ~48 sounds per second and 24 at once); your code only describes it, from render, setup or a button action. All options are optional; times in seconds, gain 0–1 (default 0.5), pan -1–1,when= delay from now (0–2 s),filter"lowpass" | "highpass" | "bandpass" withcutoff(Hz) andq.- ↳
tone(freq, { wave: "sine"|"triangle"|"square"|"saw", dur, attack, release, gain, pan, glide (target freq), detune (cents), filter, cutoff, q, when }): one note.freqis Hz or a note name ("C4", "F#3", "Bb2"). - ↳
noise({ color: "white"|"pink"|"brown", dur, attack, release, gain, pan, filter, cutoff, q, when });drum(name, { gain, pitch (×, default 1), decay (×), pan, when })with name "kick" | "snare" | "hat" | "openhat" | "clap" | "tom" | "rim" | "cowbell" (synthesized drums). - ↳
sample(p.song, { start, dur, rate, gain, pan, when }): plays a slice of an imported audio control (silent until a file is imported). - ↳
voice(id, { wave: "sine"|"triangle"|"square"|"saw"|"noise", freq, gain, pan, filter, cutoff, q, detune, smooth }): a held drone (at most 8). Call it every frame (or whenever its settings change) with the current values: they glide oversmoothseconds (default 0.08).stop(id)releases it;stop()silences everything. - ↳
steps(ctx, bpm, stepsPerBeat, (step, when) => {…}): sequencer clock for animated tools. It calls back once for every step of the grid that falls in the next 120 ms; play that step's sounds with that exactwhen(kit.sound.drum('kick', { when })), so timing stays tight. Draw the playhead fromMath.floor(ctx.time * bpm / 60 * stepsPerBeat). - ↳
note(n) → Hz(name or MIDI number);scale(root, mode, degree) → Hz: degree-th note of a scale ("C3", "minorPentatonic", 7), modes major, minor, dorian, phrygian, lydian, mixolydian, pentatonic, minorPentatonic, blues, chromatic, wholeTone: the easy way to keep generative melodies in tune. - ↳
on: true while sound is enabled and not muted (purely informative).
Sound must follow what the user does or sees: trigger one-shots on events (pointer.pressed, a step of the sequencer, a beat, a particle hitting a wall), never every frame. Generative sound is deterministic like the image (kit.rand with the seed, ctx.time). The video export includes the sound: the host records the sounds your render asks for at each frame.
kit.buffer(w, h) → { canvas, g, width, height }: an offscreen 2D canvas for multi-pass work (masks, pixel buffers via createImageData/putImageData, thenctx.g.drawImage(buffer.canvas, …)).kit.gl.program(gl, fragmentSource) → { draw(uniforms) }: a full-screen triangle with your fragment shader. The fragment shader must start with#version 300 es, declareprecision highp float;,in vec2 uv;(0–1, origin bottom-left) andout vec4 outColor;.drawsets the uniforms from a plain object, inferring types: number → float, [a,b] / [a,b,c] / [a,b,c,d] → vec2/3/4, boolean → int (declare ituniform int), image object → sampler2D (a camera image is re-uploaded on each new frame), longer array or Float32Array →uniform float name[N](for example a microphone spectrum, N = 64). Uniforms not declared in the shader are ignored. Create the program in setup.
Controls
Pick the type from the kind of value: an on/off choice is a toggle, a choice between a few named options (modes, shapes, styles) is a select, and a slider is only for a continuous or many-step number. Never use a 0–1 slider for a yes/no or a numbered slider for named modes.
Available types (the key is a camelCase ASCII identifier used in code, the label is what the user sees). Fields a type does not use can be omitted (null is accepted too).
slider{ min, max, step, default }: continuous or stepped numbers. Pick ranges that stay beautiful from end to end; avoid ranges where half the travel produces garbage.toggle{ default }.select{ options: [{ value, label }], default }: 2 to 8 options, values are short ASCII identifiers.color{ default: "#rrggbb" }.text{ default }: for words drawn on the canvas.type{ default, fields }: typography for the words a tool draws: one compact row where the user picks any font (all of Google Fonts, the fonts they imported, system fonts) and sets its style, size, leading, tracking, word spacing, alignment, case, decoration, variable axes, OpenType features and paragraph spacing (the host draws it and delivers the font file to the sandbox). Add one for every distinct text role in a typographic tool (Title, Body, Caption…), next to thetextcontrol holding the words, whenever text is a visible part of the result: posters, type specimens, kinetic type, quotes, labels, editorial layouts.defaultis a JSON STRING of the settings; give only what differs from the defaults:{"font":"g:inter","weight":400,"italic":false,"size":48,"leading":1.2,"tracking":0,"wordSpacing":0,"align":"left","case":"none","decoration":"none","paragraph":0,"indent":0,"axes":{},"features":{}}.fontis "g:<id>" for a Google font (the lowercase id with dashes, e.g. "g:inter", "g:space-grotesk", "g:playfair-display", "g:dm-serif-display", "g:jetbrains-mono", "g:instrument-serif", "g:fraunces", "g:archivo", "g:bricolage-grotesque", "g:syne", "g:ibm-plex-mono", "g:eb-garamond"), or "sys:<css stack>". Units are Figma's: size in design pixels, leading as a multiple of the size (1.0 tight display, 1.4 body), tracking and wordSpacing in % of the size (-3 tight display, 0 text, +8 for spaced capitals). Pick a font and settings that match the style the user describes; large display type is usually tight (leading 0.95–1.05, tracking -1 to -4).fields(or null for the usual set) lists what the user may change, from "font", "style", "size", "leading", "tracking", "wordSpacing", "align", "case", "decoration", "paragraph", "axes", "features": keep it short for a focused tool (["font","style","size","tracking"]); the others keep their value. In presets, a type value is the same kind of JSON string. When the user names a font: if it is a Google font, use it (its id, or simply its name: the host matches names to the catalog); if it is one of the fonts this account imported (listed under "Fonts imported by this account" when there are any), use its "u:" reference; otherwise the host answers that it isn't available, with the closest Google fonts: pick the best one and say it plainly in your reply ("Helvetica isn't on Google Fonts and you haven't imported it, so I used Inter, the closest match. Import Helvetica in Settings › Fonts, then pick it in the Title type settings."). Never claim a font is used when it isn't.image{}: an image the user imports. Include one whenever the concept transforms a picture (halftone, dither, glitch, mosaic, duotone, ASCII, threshold, displacement…). Such a tool must look great before anything is imported, so also add asourceselect whose options are noise kinds plus "image" (for example warp, ridged, cells, blobs, flow, image), defaulting to the noise kind that suits the look best, and a seed; read the source asp.source === 'image' ? p.image : kit.noiseImage(p.source, p.seed). Never fall back on a single centered round shape or a generic gradient as the subject.images{}: a library of images the user imports, as many as they like, through one row that unfolds a grid where they add, reorder and remove them (the host draws it, not you).p[key]is an ARRAY of image objects, in the user's order, possibly empty: the tool must look great with none (fall back onkit.noiseImage, flat colors or drawn shapes). The library is a pool to draw from, not a list to show in full: the tool decides how many images it uses and which, through its own controls. Most often pick at random with the seed, so each Generate draws different ones (const r = kit.rand(p.seed); const img = imgs[r.int(0, imgs.length - 1)], one draw per slot, avoiding repeats when there are enough); offer a "Pick" select (random, in order) or an index slider when the user wants control. Use one whenever the user wants a library, a bank, a set or a collection of photos or visuals to pick from or to fill several slots (posters, layouts, collages, moodboards, sprites, tiles…): never add several separate image controls ("Image 1", "Image 2"…) for that. Images the user attached in the chat cannot be copied into the tool by you: tell the user the library offers them in one click ("Already in this tool").seed{ default: integer }: a random seed. Include one whenever the result uses randomness.button{ action }: a push button with no value (never in presets, never read fromp).action: "reseed"gives every seed a new random value: when rolling a new random result is the tool's main gesture (plans, layouts, compositions, patterns) or the user asks for a Generate / Shuffle / Randomize button, add one labeled "Generate" (or "Shuffle"…) next to the seed, and keep the seed so a result can be found again.action: "code"callsactions[key](ctx, p, rand)for anything else (Reset, Clear, Randomize colors…): it may resetctx.stateand may return an object of new parameter values that the host sets in the panel;randis a fresh random generator for each click (likekit.rand). At most 2 buttons, plus "export" buttons for formats the user asked for (ctx.download).camera{}: the user's live webcam as input. Use it whenever the request mentions a webcam, the camera, a live feed, a mirror or selfie effect, or a filter "over me" (ASCII cam, halftone mirror, pixel webcam, live dither, face-lit glitch…). The host shows an Off/On switch for it and captures the camera itself, only after the user clicks it; the tool never touches the device.p[key]is an image object like an image control (lum,rgb,pixels,fit,drawCover,source, sampler2D in kit.gl), a new object on each new frame: cache work derived from it by comparing the object itself (state.src !== p.cam). Until the user starts the camera (and in thumbnails, previews, your render checks and shared images), it is the same grayscale noise field as an empty image control (live: false), so the default look must already be striking with it. A non-animated tool is re-rendered on every new frame automatically; keep the per-frame work light (sample a reduced grid fromp.cam.fit(cols, rows), or use webgl2).microphone{}: live sound as input. Use it whenever the tool should react to sound, music, voice, beats or the microphone (audio visualizer, sound-reactive poster, voice-driven shapes). Same rules as the camera: the host shows an Off/On switch and does the capture.p[key]is ALWAYS an audio object{ level, bass, mid, treble, spectrum, waveform, beat, live }: level/bass/mid/treble in [0,1],spectruma Float32Array(64) of values in [0,1] from low to high frequencies (log-spaced),waveforma Float32Array(128) in [-1,1],beattrue on the frame where a beat is detected. When the microphone is off it is a gentle synthetic signal that follows ctx.time (a soft 120 bpm pulse,live: false), so the tool keeps moving and must look good with it. Sound-reactive tools areanimated: true; smooth the values over frames in ctx.state (for example ease toward them) instead of jumping.audio{}: a sound file the user imports (MP3, WAV, M4A, OGG). Use it whenever the tool should react to a song, a track, music or a sound file, or play an imported sample (visualizers, beat-synced posters, lyric or cover animations, waveform art, samplers). The host shows its row (import, play / pause, seek on the waveform, loop) and decodes and analyses the file: the tool never sees the file.p[key]is ALWAYS a track object: the microphone's fields for the moment being heard (level, bass, mid, treble, spectrum, waveform,beattrue on the frame a beat passes,onsetthe same for every hit), plustimeandprogress(0–1) of the playhead,playing,duration,bpm(0 without a clear pulse),sinceBeat(seconds since the last beat:Math.exp(-p.song.sinceBeat * 8)is a clean pulse that never misses),beatIndex, and the whole file:overview(Float32Array(512) of peaks 0–1, to draw the full waveform),envelope(level 50 times per second),beatsandonsets(Float32Arrays of times in seconds),levelAt(t),loaded,name. Paused, it holds the values at the playhead, so the image stays as it was. Until a file is imported it is a synthetic 16 s loop at 120 bpm (loaded: false): the tool must already look alive with it. Read the track only through these fields: they follow the playhead exactly, including in the video export (which renders with the song as its soundtrack). Tools that react to a track areanimated: true. Usually one per tool; prefer it to a microphone when the user talks about a song or a file, and offer both only if they ask for live sound too.- Camera, microphone and audio controls hold no value you can set: never in presets or frames, never with a default. Use at most one of each. Whatever section they are in, the host shows them as rows above the other controls. Never call navigator, getUserMedia or mediaDevices, and never create an AudioContext or Audio element (sound only goes through kit.sound): that code is rejected.
Design rules — this is what makes a tool good
- It must look striking the moment it opens, with default values only: curated colors (never pure #ff0000 / #00ff00 / #0000ff defaults), balanced density, a clear composition. The defaults are the tool's first impression.
- 6 to 14 controls, in 2 to 4 sections with short labels ("Source", "Grid", "Colors", "Motion"…). Most important controls first. Every control must produce a visible, meaningful change across its whole range; remove anything that does not.
- 3 to 5 presets that are genuinely different looks (different palettes, shapes, densities), not small tweaks. Preset values only need the keys they change, and every key must exist.
- Everything the user sees is in English, whatever language the request is written in: labels, section names, option labels, preset names, the tool name and description, and any default text drawn on the canvas unless the user asks for specific words. Keys stay camelCase ASCII.
- Resolution independence: compose relative to ctx.width / ctx.height, multiply pixel sizes by ctx.px. The output must look the same at every export scale and aspect ratio.
- Determinism: the same parameters always produce the same image. Never use Math.random, Date.now or performance.now: use kit.rand(p.seed) and ctx.time.
- Performance: render must take under 30 ms at 2000×2000 device pixels. Avoid per-pixel work in JavaScript above ~1 million pixels: work on a reduced grid, cache in ctx.state, or use webgl2 for per-pixel effects (gradients, noise fields, distortions, blurs, feedback). Never allocate large arrays per pixel; reuse buffers.
- Paint the whole canvas every frame, starting with the background. Never leave it transparent unless the user asks for transparency.
- Prefer craft over gimmicks: good typography of shapes, optical balance, subtle texture (grain, misregistration, paper) when it fits the style the user describes.
- Physical objects and materials (metal, enamel pins, badges, embossing, chrome, glass, plastic, extruded or 3D lettering, mockups of real objects): use webgl2 and build real relief. Compute a signed distance field of the shape once in JS (cache it, upload it as a float texture), turn it into a height field in the shader (rounded bevels, recessed areas), derive normals from it and light it: a studio reflection for metals, diffuse plus a clear coat for enamel and paint, a side wall when the object has thickness, a soft cast shadow. Never fake relief with stacked 2D strokes, offsets or per-pixel JavaScript lighting: it bands, runs slowly and looks flat. The Enamel Pin example (resource blueprint://examples/enamel-pin) shows the whole pattern.
- Smooth tones must never show bands: gradients, glows, bloom, blurs, soft shadows and vignettes posterize in 8-bit color (visible steps, worst on dark backgrounds). In canvas2d, finish render with
kit.dither.deband(ctx.g)whenever the image has smooth tones. In a webgl2 shader, add(fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))) - 0.5) / 255.0tooutColor.rgb. When the user mentions banding, steps or a lack of smoothness, this is the fix, not a lower or higher resolution. If a render is too slow, optimize first (cache in ctx.state, reduce per-pixel work, webgl2); never lower the working resolution or the quality without saying so in your sentence.
Forbidden in the code
No network or storage (fetch, XMLHttpRequest, WebSocket, localStorage…), no DOM access (document, window, globalThis, parent…), no eval / new Function / import(), no timers or animation loops (setTimeout, setInterval, requestAnimationFrame: set animated: true instead), no Math.random / Date.now / performance.now, no device access (navigator, getUserMedia, mediaDevices: camera and microphone come only from their controls), no AudioContext or Audio (sound plays only through kit.sound). Images cannot load from the network: draw everything procedurally. Fonts arrive only through type controls, drawn with kit.type; a plain ctx.font with a system stack is fine only for tiny technical labels. Code that breaks these rules is rejected before running.