Skip to content

Canvas and p5 scripts

The three.js script turns a scene layer into anything three can render. That is the right tool for objects, depth and a camera. It is the wrong tool for a chart that draws itself, a counter spinning up, a hand-drawn reveal or a field of particles behind a headline — those want a flat picture, and building them from 3D meshes is how small extruded type and boxes-with-labels creep into a piece.

So the same script layer has two more runtimes. A module’s first line says which:

// @runtime canvas — plain Canvas 2D
// @runtime p5 — a p5.js instance-mode sketch

Either draws the layer’s box as a transparent 2D image, at the composition’s frame rate and resolution, and hands it to Mixture like any other layer: masks, filters, blend modes and grain apply, the headline above it stays a real text layer, the product shot below it stays an image. Scrubbing, stills and export render the same pixels, because the module is asked for one frame at a time from scene time — never left to run on its own clock.

  • Canvas 2D for procedural drawing: data ribbons and charts that draw on, counters and ticker digits in the project font, tape and line diagrams, hand-drawn reveals, generative texture.
  • p5.js for the Processing vocabulary: noise flow fields, particle systems, flocking, organic generative backdrops. The sketch is the standard instance-mode shape; frameCount and millis() are driven from scene time, so a sketch written against p5’s own clock just works.
  • three.js when the beat needs objects, depth, a camera move, an instanced wall of cards, a product rig or custom GLSL.

Type, interface and product shots are never drawn inside a script. They stay real layers — a script can place or texture them (as its items), but it does not rebuild them.

A script can read the project’s data: a CSV or JSON dataset asset, the words of a transcript, or the beats detected in an audio track. The rows go through the same filter, sort and slice pipeline as repeated layers and arrive in the module ready to draw, re-timed through an edited clip when you point them at it. That is how a chart draws itself from the real numbers, a field pulses on every beat of the music bed, or a particle spawns on each spoken word.

Describe the motion and the agent picks the runtime, writes the module, creates it as a script asset and points a scene layer at it:

Behind the headline, a slow noise field of fine gold and teal trails that fades in over the first second and a half.

The revenue line should draw itself across the card over two seconds, with the dots landing as it passes and the total counting up in Switzer.

If a module fails, the agent sees the error in its own render check and fixes it. You can also upload a .js file as an asset yourself and pick it in the layer’s Form → Script menu; the panel shows which runtime the file declares.

Select the scene layer, set Form to Script, and choose the script asset. The layer’s box is the script’s canvas, so size and place the layer where the drawing should be. Parameters the module declares (a // @param header, the same grammar as custom shader effects) appear as controls and can be animated through the layer’s paramAnimations. Camera, lighting and 3D orientation have no effect on a flat script.

The same sandbox as every script: no session, no storage, no network. A canvas module imports nothing; a p5 sketch may import only p5. Loaders (loadImage, loadFont), loop(), timers, Math.random and the wall clock are refused when the asset is created, with the reason — they are what would make a frame differ between preview and export. A module that hangs is stopped and reported; it cannot take the editor with it.

The full contract, both worked examples and the bake pattern for stateful simulations live in the agent guide — the same text agents read: get_docs(topic: "canvas"), or the canvas topic in the agent docs. The short version:

// @runtime canvas
export default function build({ size, params, random, fonts, items }) {
return {
update({ ctx, time, params }) {
// ctx: a CanvasRenderingContext2D, 1 unit = 1 comp px, (0,0) top-left,
// cleared before every frame. Draw the whole frame for `time`.
},
};
}
// @runtime p5
export default function sketch(s, { size, fps, params, seed, items }) {
s.setup = () => { s.createCanvas(size.w, size.h); };
s.draw = () => {
const t = s.frameCount / fps; // scene time — set by Mixture before each frame
s.clear();
// draw
};
}

Two rules keep it a Mixture layer: draw from time (or frameCount) only, and bake anything stateful — a particle system is stepped once, in order, into a per-frame track that draw() reads, so any frame can be asked in any order and land on the same pixels.