AC Piece (Disk) Style Guide #
How we write interactive pieces on aesthetic.computer.
Philosophy #
One .mjs file per piece. Each piece lives in system/public/aesthetic.computer/disks/. A piece is a self-contained program that exports lifecycle functions. The system calls them at the right time -- you just fill in the blanks.
Pieces are resolution-adaptive, input-agnostic (mouse, touch, gamepad), and run inside a sandboxed worker. They range from single-screen toys (sprinkles.mjs: 18 lines) to full apps (keep.mjs: 3600+ lines). Keep it as simple as the idea demands.
File Structure #
system/public/aesthetic.computer/disks/
mypiece.mjs <- your piece
system/public/aesthetic.computer/lib/
num.mjs <- math utilities (rand, lerp, clamp, etc.)
geo.mjs <- geometry (Box, Grid, Circle, Line)
note-colors.mjs <- music note color mapping
gamepad-mappings.mjs <- controller button/axis mapping
...
system/public/aesthetic.computer/dep/
... <- third-party dependencies
One file is the norm. Import from ../lib/ or ../dep/ when truly needed (notepat imports note-colors, 1v1 imports gamepad-mappings). Don't create utility files for single-piece use.
Lifecycle Functions #
Export any subset of these. All are optional. All receive a single api object to destructure.
// ๐ฅพ Boot (Runs once when piece loads)
function boot({ screen, num, resolution, hud, cursor, store }) {}
// ๐งฎ Sim (Runs at 120fps for logic/physics, frame-independent)
function sim({ num, screen, simCount }) {}
// ๐จ Paint (Runs once per display refresh for rendering)
function paint({ wipe, ink, screen, pen, write, paste }) {}
// ๐ช Act (Runs on user interaction)
function act({ event: e, needsPaint, sound }) {}
// ๐ฅ Beat (Runs once per metronomic BPM)
function beat({ sound }) {}
// ๐ Leave (Runs once before piece unloads -- cleanup, save state)
function leave({ store }) {}
// ๐ฐ Meta (Returns metadata for documentation/search)
function meta() {
return { title: "Name", desc: "What it does." };
}
export { boot, sim, paint, act, meta };
Execution order: boot -> (sim at 120fps, paint at display rate, act on input, beat on BPM) -> leave
The API Object #
Every lifecycle function receives a single object. Destructure only what you need.
Drawing #
| API | Usage |
|---|---|
wipe(r, g, b) or wipe("color") |
Clear screen |
ink(r, g, b, a) |
Set color, returns chainable draw object |
ink().plot(x, y) |
Draw single pixel |
ink().line(x1, y1, x2, y2) |
Draw line |
ink().box(x, y, w, h) |
Draw filled box |
ink().box(x, y, w, h, "outline") |
Draw box outline |
ink().circle(x, y, r, filled?) |
Draw circle |
ink().poly(points) |
Draw polygon |
ink().write(text, {x, y}) |
Draw text |
paste(bitmap, x, y, scale?) |
Blit image/painting |
stamp(bitmap, cx, cy) |
Center-paste image |
Chaining: ink(255, 0, 0).box(0, 0, 10, 10) -- set color and draw in one call.
Named colors: ink("red"), ink("blue"), ink("cyan"), ink("lime"), etc.
Screen & Pixels #
function paint({ screen }) {
const { width, height, pixels } = screen;
// pixels is a Uint8ClampedArray (RGBA, 4 bytes per pixel)
// Index: (y * width + x) * 4
}
Use screen.pixels for per-pixel work. Calling ink().plot() in a loop over thousands of pixels is slow. Write directly to the pixel buffer instead:
// Fast: direct pixel buffer
const pi = (y * width + x) << 2;
pixels[pi] = r;
pixels[pi + 1] = g;
pixels[pi + 2] = b;
pixels[pi + 3] = a;
// Slow: function call per pixel (fine for < 100 points)
ink(r, g, b, a).plot(x, y);
Input #
function act({ event: e }) {
if (e.is("touch")) { /* finger/mouse down at e.x, e.y */ }
if (e.is("draw")) { /* finger/mouse moved while down */ }
if (e.is("lift")) { /* finger/mouse released */ }
if (e.is("keyboard")) { /* key pressed: e.key */ }
}
// Continuous pointer position (available in paint/sim too):
function paint({ pen }) {
if (pen) { /* pen.x, pen.y, pen.pressure (0-1), pen.device */ }
}
Math (num) #
function boot({ num }) {
num.rand() // Random float 0-1
num.randInt(n) // Random int 0-n (inclusive)
num.randIntRange(lo, hi) // Random int lo-hi (inclusive)
num.randIntArr(n, count) // Array of random ints
num.randInd(arr) // Random index into array
num.clamp(val, min, max)
num.lerp(a, b, t)
num.dist(x1, y1, x2, y2)
num.radians(degrees)
num.degrees(radians)
num.map(val, inLo, inHi, outLo, outHi)
}
Helpers #
function sim({ help: { repeat, choose } }) {
repeat(10, (i) => { /* runs 10 times */ });
choose("red", "blue", "green") // random pick
}
Resolution #
Set a custom virtual resolution in boot. The system scales the canvas to fill the viewport.
function boot({ resolution, screen }) {
resolution(128, 128); // Low-res pixel art
resolution(512, 512); // Fixed square
resolution(screen.width / 2, screen.height / 2); // Half-res
// No call = native screen resolution (default)
}
Note: After calling resolution(), screen.width/screen.height in subsequent frames will reflect the new size.
Cursor #
function boot({ cursor }) {
cursor("none"); // Hide system cursor
cursor("native"); // Show default system cursor
// Default: AC custom cursor
}
HUD / Label #
The corner label shows the piece name by default.
function boot({ hud }) {
hud.label("mypiece"); // Custom label text
hud.label("mypiece 42"); // Label with state info
hud.label(); // Hide label entirely
}
Navigation #
function act({ event: e, jump }) {
if (e.key === "Enter") jump("other-piece");
// jump("piece:param1:param2") -- with parameters
// jump("piece", true) -- ahistorical (no back button)
}
Persistence #
async function boot({ store }) {
const saved = await store.retrieve("key", "local:db");
if (saved) state = saved;
}
function leave({ store }) {
store["key"] = state;
store.persist("key", "local:db");
}
Sound #
function act({ sound }) {
sound.synth({
type: "sine", // "sine", "triangle", "square", "sawtooth"
tone: 440, // Hz
duration: 0.5, // seconds
attack: 0.1,
decay: 0.9,
volume: 0.5 // 0-1
});
}
Images & Network #
async function boot({ net: { preload }, get }) {
// Preload an image asset
const { img } = await preload("https://assets.aesthetic.computer/rain/guy.png");
// Get another user's painting
const { img: painting } = await get.painting("slug").by("handle");
}
Painting Buffers #
Create off-screen canvases for layered rendering:
let buf;
function boot({ painting, screen }) {
buf = painting(screen.width, screen.height, (api) => api.wipe(0));
}
function paint({ page, paste, ink, screen }) {
page(buf); // Switch render target to buffer
ink("red").circle(50, 50, 10); // Draw into buffer
page(screen); // Switch back to screen
paste(buf); // Composite buffer onto screen
}
UI Buttons #
let btn;
function boot({ ui: { Button } }) {
btn = new Button({ x: 10, y: 10, w: 60, h: 20 });
}
function paint({ ink }) {
ink("lime").box(btn.box);
ink("black").write("Go", { x: btn.box.x + 4, y: btn.box.y + 4 });
}
function act({ event: e, jump }) {
btn.act(e, () => jump("target"));
}
System Modes #
Control how the piece integrates with the AC shell by exporting a system string.
// Default: standard piece with prompt HUD
// (no export needed)
// Hide the prompt HUD entirely (fullscreen experiences)
export const system = "noprompt";
// Inherit the nopaint raster editor system (drawing tools)
export const system = "nopaint";
// Nopaint + auto-save painting on leave
export const system = "nopaint:bake-on-leave";
// Prompt/chat mode (LLM-powered character pieces)
export const system = "prompt:character";
// 3D world mode
export const system = "world";
When to use noprompt #
Use it for fullscreen simulations, games, visualizations -- anything where the typing prompt would get in the way. The ant simulation, metaballs, games, etc.
When to use nopaint #
Use it when building a drawing tool that needs a persistent canvas (rect, line, spray, smear, word, icon). The nopaint system provides:
system.painting-- the persistent canvas with.pixels(Uint8ClampedArray)system.nopaint.buffer-- overlay layer for previewssystem.nopaint.undo-- undo historysystem.nopaint.zoomLevel/system.nopaint.translation-- pan/zoom statesystem.nopaint.updateBrush()-- transform pointer coordssystem.nopaint.present()-- render painting + overlay to screen
Parameters #
Pieces receive URL parameters. For /mypiece:hello:42:
function boot({ params }) {
params[0] // "hello"
params[1] // "42"
}
Colon params (after ~) for configuration: /mypiece~thick~aa
function boot({ colon }) {
colon[0] // "thick"
colon[1] // "aa"
}
Performance Guide #
Pixel-heavy rendering #
For simulations, fractals, or effects that touch every pixel:
- Write to
screen.pixelsdirectly instead ofink().plot()per pixel - Use bitwise ops for integer conversion:
x | 0instead ofMath.floor(x),i << 2instead ofi * 4 - Use typed arrays (Float32Array, Uint8ClampedArray) for grids/buffers
- Combined wipe + draw -- skip
wipe()and fill pixels in your own loop
// Good: single pass, no function calls per pixel
function paint({ screen }) {
const { width, height, pixels } = screen;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pi = (y * width + x) << 2;
pixels[pi] = r; pixels[pi+1] = g; pixels[pi+2] = b; pixels[pi+3] = 255;
}
}
}
Object-based rendering #
For pieces with a manageable number of entities (< 1000):
ink().plot(),ink().circle(),ink().box()are finewipe()then draw everything is the standard pattern- Use
needsPaint()inact()to trigger repaints only when needed
Simulation #
sim()runs at 120fps -- keep it cheap- Heavy math belongs in
sim(), notpaint() - Pre-compute what you can in
boot()
Conventions #
Naming #
Piece names are short, evocative, lowercase: ant, rain, tone, notepat, spray, wand. The name becomes the URL slug: aesthetic.computer/ant.
File Header #
// PieceName, YYYY.M.DD
// One-line description.
TODO Regions #
/* #region ๐ TODO
- [] Incomplete task.
- [x] Completed task.
#endregion */
Section Comments #
Use emoji markers for section clarity (optional, common in larger pieces):
// ๐ฅพ Boot
function boot() {}
// ๐งฎ Sim
function sim() {}
// ๐จ Paint
function paint() {}
// ๐ช Act
function act() {}
// ๐ Library
function helper() {}
State #
Top-level let variables. No state management library.
let ants = [];
let score = 0;
let gridW, gridH;
Exports #
Named exports at the bottom. System mode first if present.
export const system = "noprompt";
export { boot, sim, paint, act, meta };
Library Section #
Helper functions go at the bottom, after the exports:
export { boot, sim, paint, act, meta };
// ๐ Library
function gridIndex(x, y) { ... }
function sense(ant, trail, offset) { ... }
Minimal Piece Template #
// MyPiece, 2026.2.10
// What it does in one sentence.
function boot({ screen }) {
// Initialize state.
}
function paint({ wipe, ink, screen }) {
wipe(0);
ink("white").write("hello", { x: 6, y: 6 });
}
function act({ event: e }) {
if (e.is("touch")) {
// Respond to input.
}
}
function meta() {
return { title: "MyPiece", desc: "What it does." };
}
export { boot, paint, act, meta };
Fullscreen Simulation Template #
// MySim, 2026.2.10
// Pixel-level simulation with direct buffer access.
let state;
function boot({ screen }) {
state = new Float32Array(screen.width * screen.height);
}
function sim({ screen }) {
// Update state at 120fps.
}
function paint({ screen }) {
const { width, height, pixels } = screen;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pi = (y * width + x) << 2;
const v = state[y * width + x];
pixels[pi] = v; pixels[pi+1] = v; pixels[pi+2] = v; pixels[pi+3] = 255;
}
}
}
function act({ event: e }) {
if (e.is("touch")) { /* interact */ }
}
function meta() {
return { title: "MySim", desc: "A simulation." };
}
export const system = "noprompt";
export { boot, sim, paint, act, meta };
Reference Implementations #
| Category | Piece | Lines | Notes |
|---|---|---|---|
| Minimal | sprinkles.mjs | ~32 | Random pixel plotting, no boot |
| Simulation | rain.mjs | ~93 | Particles, preloaded images, sim+paint |
| Pixel buffer | metaballs.mjs | ~200 | Direct screen.pixels, per-pixel math |
| Drawing tool | spray.mjs | ~90 | nopaint system, painting buffers |
| Game | 1v1.mjs | ~2000 | Multiplayer, gamepad, 3D rendering |
| Music | notepat.mjs | ~4000 | Touch UI, sound synthesis, responsive layout |
| Complex app | keep.mjs | ~3600 | Multi-mode app, persistence, blockchain |
What We Don't Use #
- Frameworks or build tools inside pieces
- TypeScript (pieces are plain .mjs)
- External CSS or HTML (everything is code-drawn)
requestAnimationFrame(the system handles the render loop)documentorwindow(pieces run in a worker sandbox)Math.random()(usenum.rand()for consistency)