Reference

p5.js sketches

The other kind of cartridge: an XP whose whole content is code you wrote, drawn by p5.js on its own canvas. It is still an XP — the store lists it, the battle wizard picks it, a room keeps it standing — but instead of a world of blueprints it carries JavaScript files, and instead of the script sandbox it runs in a sealed container with a real DOM, a real canvas and requestAnimationFrame. Ordinary p5: setup(), draw(), mouseX, keyIsDown — all of it works untouched.

What the platform adds is window.xp: the wrapper every sketch wakes up inside, which is how a hundred lines of p5 becomes a multiplayer game without a line of netcode.

What a sketch XP is

A document with a sketch block instead of a world. The sources live insidethe document as strings — there is no file upload and no second format — and they only ever run inside an iframe with an opaque origin and a strict content policy. That containment is why a sketch somebody else wrote is a sketch you can safely open: it cannot see your cookies, cannot reach the page around it, and cannot phone home — the only network it has is the platform's own art.

{
  "format": "xp/1",
  "id": "neon-pond",
  "name": "Neon Pond",
  "player": { "keys": [{ "key": "KeyE", "does": "boost" }] },
  "sketch": {
    "engine": "p5",
    "entry": "main.js",
    "stick": true,
    "files": { "pond.js": "…", "main.js": "…" }
  }
}

Up to sixteen files, half a megabyte across the project. Every file runs in the order written; entry runs last, so helpers exist by the time the main file does. Ship-along examples to read: neon-pond (2D, a flow with rounds), peep-beat (a rhythm game — lanes, pack art, a live scoreboard), conways-gambit (a two-player board game — one shared table, turns asked for over xp.send), and cube-yard(p5's WEBGL mode).

The project

Pick p5.js when creating a project and you get the project view instead of the 3D editor: the file list on the left, the code in the middle, the sketch running on the right with a console under it. Run rebuilds the preview from what you typed; Saverefuses, with the parser's own words, anything that would not open — the same bargain the level editor strikes. On a phone the three panes become tabs, so whichever one you are in has the whole screen.

Players & avatars

The roster is live and the avatar syncs itself: write your own position every frame, read everybody else's. Ten times a second yours goes out; theirs arrive smoothed, so movement reads as movement rather than teleporting.

xp.me · xp.playersWho you are; everybody here, you included.
xp.avatarYours: x, y, angle, and a free data object that travels with it — a score, a colour, a state.
player.avatarTheirs, already eased. Draw it; never write it.
player.imageA picture of their skin, for image() — a peep face over a dot.
xp.on('join' / 'leave', fn)Somebody arrived or went. Their held buttons are released for you.
function draw() {
  background(6, 2, 20)
  xp.avatar.x += xp.input.x * 4   // mine: written
  xp.players.forEach(function (p) {
    circle(p.avatar.x, p.avatar.y, 26)  // everybody's: read
    text(p.name, p.avatar.x, p.avatar.y - 24)
  })
}

A live scoreboard is one line, because avatar.data rides the same sync: xp.avatar.data.score += 1 on your machine is p.avatar.data.scoreon everybody's.

One input axis

xp.input is { x, y } in −1..1, clamped to the unit circle, +y down like a canvas. On a keyboard it is the arrows and WASD; on a phone it is the thumbstick, when the document set sketch.stick. A sketch written against it is playable on both without a line of device code — and player.inputgives you everybody else's axis too, synced with their avatar.

var pace = xp.pressed('boost') ? 7 : 3.5
xp.avatar.x = constrain(xp.avatar.x + xp.input.x * pace, 0, width)
xp.avatar.y = constrain(xp.avatar.y + xp.input.y * pace, 0, height)

Keys that become buttons

The document's player.keys — the same five-key vocabulary a level binds — arrive as named controls: from this keyboard, from an on-screen button on a phone, and from the wire for every other player. A press is a trigger everybody hears, which is what makes "glow while boosting" visible on every screen, not just your own.

xp.on('press' / 'release', fn)fn(name, player)— fires for every player's edges, yours included.
xp.pressed(name, player?)Held right now. Yours if no player is given.
xp.on('press', function (name, p) {
  if (name === 'boost') ripples.push({ x: p.avatar.x, y: p.avatar.y, age: 0 })
})

Shared objects — the ball rule

For a thing that is nobody's body — a ball, a puck, a crown — declare a shared object. Exactly one client moves it and everybody else watches it smoothed, which is the same election the 3D engine uses for its own balls: no owner messages, the lowest id starts as owner, and claim() takes it — touching it, catching it.

var ball

function setup() {
  ball = xp.object('ball', { x: 240, y: 200, dx: 0, dy: 0 })
}

function draw() {
  if (dist(xp.avatar.x, xp.avatar.y, ball.x, ball.y) < 36) {
    ball.claim()                        // it is yours now, on every screen
    ball.dx = (ball.x - xp.avatar.x) * 0.4
    ball.dy = (ball.y - xp.avatar.y) * 0.4
  }
  if (ball.mine) {                      // only the owner integrates
    ball.x += ball.dx
    ball.y += ball.dy
  }
  circle(ball.x, ball.y, 22)            // everybody draws
}

For everything else there is xp.send(data) and xp.on('message', fn)— fire-and-forget to everybody, capped at twenty a second and 8 kB, which is a game loop's worth of state and not a firehose.

The flow — rounds the platform runs

Give the document a flow and the platform runs it over your sketch: the strip above the canvas shows the round, the phase, the countdown and the phase's saysline, and a phase's allow really does silence the keys it takes away. One client drives, everybody follows, late joiners land in the right phase.

xp.phase{ name, round, left, over, says, allowed } — or null when the document has no flow.
xp.on('phase', fn)The run moved.
xp.emit('goal')Raise an event a flow step is listening for — a step written { "on": "goal", "go": "celebrate" } fires on it.
xp.matchWhat scheduled this, if anything did: started, timeLimit, scoreLimit from the battle wizard. Nulls mean you decide.

What of a flow a sketch cannot honour, it refuses honestly: steps with a whencondition never hold (they read a data block your code replaces), and a phase's does verbs fire at nothing — there are no entities in here to fire at.

Loading from packs

The shipped art is reachable from inside the container — and it is the only network the container has.

xp.load.image('peepz/bunny')A catalogue model&apos;s picture as a stable handle: check .ready, draw .image. (p5 2.x hands back a Promise and 1.x an image; the handle spares you caring which.)
xp.load.model('proto/Barrel_A')The model itself, for WEBGL mode: a handle whose .draw() feeds p5 the mesh with its base-colour texture once .ready. A prop, standing still — no skinning, no animation, no extensions.
xp.load.sound('hit').play()A player that cycles the sound&apos;s takes — five punches cycled read as a fight; one punch five times reads as a bug.
xp.tone(660, 0.12, 'square')A sound made rather than loaded — for the blip whose pitch is data: a streak, a countdown. Peep Beat&apos;s verdicts are these.
player.skinThe model id behind a player&apos;s look — hand it to xp.load.model and the actual peep stands in your sketch, as Cube Yard&apos;s players do.
xp.imageUrl · xp.soundUrlThe bare URLs, when you want them raw.
xp.file('glow.frag')A file the project carries rather than runs — .frag, .vert and .glsl are legal beside your .js — returned as the string createShader(vert, frag) wants.
xp.timeline{ seconds } when the document declared how long one pass of the sketch is — a composition can loop itself to it, and it is what a render of the sketch will run to.
var glow

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL)
  glow = createShader(xp.file('basic.vert'), xp.file('glow.frag'))
}

function draw() {
  shader(glow)
  glow.setUniform('t', millis() / 1000)
  rect(-width / 2, -height / 2, width, height)
}
var face, barrel

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL)
  face = xp.load.image('peepz/bunny')
  barrel = xp.load.model('proto/Barrel_A')
}

function draw() {
  if (face.ready) image(face.image, 100, 100, 60, 60)
  if (barrel.ready) {
    push()
    translate(0, 60, 0)
    scale(60)
    barrel.draw()   // textured, lit by your lights
    pop()
  }
  if (somethingLanded) xp.load.sound('thud').play()
}

Translation

The document's words block works here too: xp.t('Catch the ball')returns the reader's sentence, resolved for their language before your code ever runs. The same warning the script API carries: it differs per reader, so draw what it returns — never compare against it, never name a signal by it.

What a sketch cannot do

The container is the deal: no cookies, no storage of the page's, no network beyond the platform's own art, no reaching the page around it. Two honest gaps beyond that, both by design rather than by accident. The arbiter— the server-decided rules a board game's rolls and turns run through — is not exposed to sketches yet; what a sketch has is the elected-owner object above, which is the elected tier, not the serverone, so a sketch's facts are as honest as its players' machines. And p5 reads OBJ and STL, not the .glbour packs ship — so "the real 3D model" in a sketch is p5's own geometry (see cube-yard), with pack pictures for faces and floors.