Events

Events are what runs your scripts (e.g. app.onUpdate is called every tick)

  • - on<Event> functions run in the bubble phase
  • - capture<Event> functions run in the capture phase
  • - All events bind this to the block that has the handler
  • - If you have multiple handlers for the same event, name them onUpdate, onUpdate2, onUpdate3, etc.

function onUpdate()

Run every tick

  • - this Block: Target block

function onStart(user, ...args)

Run when the app is started

  • - user Block: User who started it
  • - args ...any (optional): Optional "command line" parameters

function onStop(user)

Run when the app is stopped

  • - user Block: User who stopped it

function onAddUser(user)

Run when a user joins

  • - Use to e.g. spawn a player character in a game
  • - user Block: User that joined

function onRemoveUser(user)

Run when a user leaves

  • - user Block: User that left

function onMouseMove(user, x, y, hit)

Run when the pointer moves (if over this block)

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - hit Block: Block under the pointer

function onMouseDown(user, x, y, hit)

Run when the pointer is pressed (if over this block)

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - hit Block: Block under the pointer

function onMouseUp(user, x, y, hit)

Run when the pointer is released (if over this block)

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - hit Block: Block under the pointer

function onClick(user, x, y, hit)

Run when the pointer is pressed and released on this block

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - hit Block: Block under the pointer

function onMouseOver(user, x, y, hit)

Run when the pointer enters this block

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - hit Block: Block under the pointer

function onMouseOut(user, x, y, hit)

Run when the pointer leaves this block

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - hit Block: Block under the pointer

function onMouseWheel(user, deltaX, deltaY, hit)

Run when the mouse wheel scrolls (if over this block)

  • - Return true to stop bubbling
  • - user Block
  • - deltaX number
  • - deltaY number
  • - hit Block: Block under the pointer

function onKeyDown(user, key, repeat)

Run when a key is pressed (if this block has keyboard focus)

  • - user Block
  • - key string
  • - repeat boolean (optional): true for a held key's repeats

function onKeyUp(user, key)

Run when a key is released (if this block has keyboard focus)

  • - user Block
  • - key string

function onCopy(user)

Run when the user copies/cuts to clipboard (if this block has keyboard focus)

  • - user Block
  • - returns string|undefined: Text to place on the clipboard (or nothing to leave the clipboard alone, e.g. if no selection to copy)

function onText(user, text)

Run when text is pasted, or typed on the on-screen keyboard (if this block has keyboard focus)

  • - user Block
  • - text string: The pasted text (or an empty string if cut)

function onDragBegin(user, x, y, hit, data)

Run when a pointer drag starts on this block

  • - Return true to accept the drag and receive more drag events
  • - user Block
  • - x number: World x-coordinate where the drag started
  • - y number: World y-coordinate where the drag started
  • - hit Block: Block where the drag started
  • - data Block: Shared drag data

function onDragMove(user, x, y, hit, data)

Run while an accepted drag moves this block

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - hit Block: Block under the pointer
  • - data Block: Shared drag data

function onDropMove(user, x, y, hit, data)

Run on the current drop target while an accepted drag moves

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - hit Block: Block under the pointer
  • - data Block: Shared drag data

function onDragOver(user, x, y, newHit, data)

Run on the dragged block when the pointer enters a new drop target

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - newHit Block: New block under the pointer
  • - data Block: Shared drag data

function onDropOver(user, x, y, newHit, data)

Run on the new drop target when the pointer enters it during a drag

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - newHit Block: New block under the pointer
  • - data Block: Shared drag data

function onDragOut(user, x, y, oldHit, data)

Run on the dragged block when the pointer leaves a drop target

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - oldHit Block: Previous block under the pointer
  • - data Block: Shared drag data

function onDropOut(user, x, y, oldHit, data)

Run on the old drop target when the pointer leaves it during a drag

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - oldHit Block: Previous block under the pointer
  • - data Block: Shared drag data

function onDragEnd(user, x, y, hit, data)

Run on the dragged block when an accepted drag ends

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - hit Block: Block under the pointer
  • - data Block: Shared drag data

function onDrop(user, x, y, hit, data)

Run on the final drop target when an accepted drag ends

  • - Return true to stop bubbling
  • - user Block
  • - x number: World x-coordinate
  • - y number: World y-coordinate
  • - hit Block: Block under the pointer
  • - data Block: Shared drag data

function onResize(oldWidth, oldHeight)

Run after this block is resized (e.g. the app window changed)

  • - Use e.g. to update the layout of child blocks
  • - oldWidth number
  • - oldHeight number

function onLoad()

Run after the OS loads a project snapshot


Events.send(type, block, ...args)

Send an event to block

  • - type string: E.g. "onResize"
  • - block Block: The target block
  • - args ...any: The event's arguments

Events.broadcast(type, root, ...args)

Send an event to root and every enabled block inside it

  • - type string: E.g. "onUpdate"
  • - root Block: E.g. an app
  • - args ...any: The event's arguments

Block

State is stored in a single tree of Block nodes

  • - Each block has a set of key-value properties
  • - You can put any property you want on a block, like speed: 3, and use it from a script with this.speed
  • - The following are built-in properties:

block.x: number = 0

Horizontal position (in the parent)


block.y: number = 0

Vertical position (down is positive)


block.z: number = 0

Depth (higher is in front)


block.scaleX: number = 1

Horizontal scale factor


block.scaleY: number = 1

Vertical scale factor


block.scaleZ: number = 1

Depth scale factor


block.angleX: number = 0

Rotation around the X-axis (in degrees)


block.angleY: number = 0

Rotation around the Y-axis (in degrees)


block.angleZ: number = 0

Rotation around the Z-axis (in degrees)


block.shape: string|function

What to draw (if anything)

  • - Typically a path to a shape function, like "/shapes/circle"
  • - Or a path to a block (will batch its whole tree)
  • - Or a function returning a Mesh

block.clip: boolean = false

Clip children to this block's bounds


block.visible: boolean = true

Set false to hide the block (and ignore clicks)


block.enabled: boolean = true

Set false to disable clicks, keys, and onUpdate


new Block(properties)

Create a new block

  • - properties object (optional): Add these key: value pairs to the new block
  • - returns Block

block.clone(properties)

Deep-copy this block

  • - properties object (optional): Add these key: value pairs to the clone (or use undefined as the value to *remove* a property)
  • - returns Block

Block.fromJSON(json)

Deserialize from JSON

  • - json *
  • - returns *: Objects become blocks, @-strings become functions, others pass through

block.toJSON()

Serialize block to JSON

  • - returns object

block.parent

Return block's parent (or null)

  • - returns Block

block[Symbol.iterator]()

Iterate over this block's properties as [key, value] pairs

  • - returns IterableIterator<[string, any]>

block.has(key)

Check whether this block has a property named key

  • - key string
  • - returns boolean

block.getSize()

Count this block's properties

  • - returns number

block.keys()

Iterate over this block's property keys

  • - returns IterableIterator<string>

block.values()

Iterate over this block's property values

  • - returns IterableIterator<any>

block.watch(...keys)

Convenience function for subscribing to all keys in this block

  • - A no-op outside a shape function
  • - keys ...string

block.getKey()

Get this block's key in its parent

  • - returns string|null

block.getUniqueKey(key)

If "key" already exists, return "key2", etc.

  • - key string
  • - returns string

block.renameKey(oldKey, newKey)

Rename an existing property (preserving its index)

  • - oldKey string
  • - newKey string

block.getIndex()

Get this block's index in its parent

  • - returns number

block.insert(key, value, index)

Insert a property at index (an existing key moves there)

  • - key string
  • - value *
  • - index number (optional)

block.getPath()

Get a path reference to this block

  • - returns string: E.g. "/apps/pong/icon"

block.contains(block)

Check if block is a descendant of this block (or *is* this block)

  • - block Block
  • - returns boolean

block.localToWorld(x, y, z)

Convert this block's local coordinates to world space

  • - x number (optional)
  • - y number (optional)
  • - z number (optional)
  • - returns [number, number, number]

block.worldToLocal(x, y, z)

Convert world coordinates to this block's local space

  • - x number
  • - y number
  • - z number (optional)
  • - returns [number, number, number]

block.find(predicate)

Find all blocks satisfying predicate (including this)

  • - predicate function(Block): boolean
  • - returns Block[]

block.getBounds()

Return block's shape bounding box (in local space)

  • - returns [number, number, number, number]: [minX, minY, maxX, maxY]

block.getBoundsTree()

Like getBounds but expanded to include all children

  • - A turned child adds the box around its turned box: it holds all of it, but can be bigger than it (getBoundsTreeTransformed is exact)
  • - returns [number, number, number, number]: [minX, minY, maxX, maxY]

block.getBoundsTreeTransformed()

Like getBoundsTree but rotation/3D transforms applied

  • - Every shape's own corners go through the whole chain of transforms: boxing each level in turn would inflate the result, as a box around a turned box is bigger than one around what it holds
  • - returns [number, number, number, number]: [minX, minY, maxX, maxY]

Block.get(path)

Get the value stored at path

  • - Safe to pass an undefined path (so you can do Block.get(block.path) without having to check if block *has* a path)
  • - path string (optional): A path like "/path/to/value"
  • - returns *: The value (or undefined)

Block.isPath(value)

Check if value is a path (like "/path/to/value")

  • - value *
  • - returns boolean

Block.script(path)

Get the callable function stored at path

  • - path string
  • - returns Function

Block.set(path, value)

Set the value at path

  • - path string
  • - value *

Block.clone(value)

Create a deep copy of value (if it's a block)

  • - returns *

Block.rewritePaths(oldPath, newPath, block)

Replace oldPath by newPath in every path

  • - E.g. after moving a value, so any references to it update
  • - oldPath string
  • - newPath string
  • - block Block (optional): Subtree to update (defaults to OS.state)

OS

Main module that boots the OS


OS.state: Block

Root of the multiplayer-synced state tree


OS.projectID: string

Loaded project (e.g. "17/1")


OS.userID: number

Local user ID (differs per client, use with care to not desync!)


OS.time: number

Tick counter


OS.users: Block

All connected users, e.g. for (const user of OS.users.values())


OS.lockView(win, crop, margin, tween)

Lock the local view to a block: pan/zoom to fit it on screen (tweened), follow it every frame, and suspend view gestures (pinch-zoom) and the touch input delay, so e.g. games get full-speed touch input

  • - The view is a *local* camera, so this never touches shared state — but that also means scripts (which run on every client) must gate the call to the acting user: if (user === OS.getUserByID(OS.userID)) ...
  • - win Block: Block to fit: the maximize button (and Build's play button, for touch users) fits the whole window, the kiosk shell fits the bare app
  • - crop boolean (optional): Crop the render to the fitted block: the rest of the world is scissored away to flat wallpaper, and presses out there are dropped. The maximize button's focus mode, also the kiosk shell's whole world
  • - margin number (optional): Extra ring of world-units to fit around win's rect. Outlines ride *outside* a block's rect, so windows pass their outline width to keep the frame line on screen
  • - tween boolean (optional): Animate the camera to the fit. Pass false when there's no prior view to depart from: the kiosk shell locks before its first frame and should just open at the right zoom

OS.unlockView(win)

Undo lockView: restore the view (tweened) to where it was before locking

  • - win Block (optional): Only unlock if locked to this window (pass it when cleaning up after a specific window; omit to unlock unconditionally)

OS.renderImage(block, size, background)

Render a block (with its children) to a PNG image

  • - The image covers the block's bounds, scaled so its longest side is size pixels -- e.g. await OS.renderImage('/apps/snek/icon') makes a square 1024px share preview from an app's icon
  • - Renders offscreen with the same meshes and shader as the screen, and never touches shared state, so it's safe to call from scripts anytime
  • - block Block|string: Block (or path to one), e.g. an app icon
  • - size number (optional): Longest image side in pixels
  • - background string (optional): Backdrop color (a /colors name or "#RRGGBB"), or leave undefined for a transparent backdrop
  • - returns Promise<Blob>: PNG image data

OS.share(user, params, patch)

Publish the current state as a shared game (the server's /share/ API)

  • - Safe to call from synced commands: only the sharing user's own client uploads (a fetch is a side effect), all others return immediately
  • - Rollback replays just repeat the same idempotent POST
  • - user Block: The user who's sharing
  • - params Object: Publish parameters (publishID, name, image, width, height, controls): name, size, and controls are stored in the game's registry row, where the host page's handheld chrome reads them; image is a block (or path, e.g. the app's icon) rendered into the share link's preview image
  • - patch (json: Object) => void (optional): Publish-time edits to the snapshot copy (Build hands the app to the kiosk shell and garbage-collects the rest; the live desktop stays untouched)

OS.getUserByID(id)

Find the user with a given ID

  • - id number|string
  • - returns Block

OS.getUserByColor(color)

Find the user with a given color

  • - Users are seated in Settings.USER_COLORS order as they join: alone, you're "yellow", the next player "pink", then "purple" and "cyan"
  • - A user who disconnects keeps their color for a minute, so a phone lock doesn't reshuffle anyone
  • - color string
  • - returns Block: The user block (or undefined)

OS.didChange(path, deep)

Did the value at path change this tick?

  • - For editors showing a *copy* of shared data (Build's canvas, a file list): poll this in onUpdate and reload when true. Every client runs the same commands each tick, so they all reload at the same tick
  • - True if a key along the path was written (the value was replaced or deleted, or a holder swapped out), or the block at path had a key written
  • - deep also counts writes to any block inside that block's subtree
  • - "This tick" is every write since the previous tick's onUpdate finished, e.g. the tick's commands and onUpdate handlers that ran before the caller
  • - example: if (OS.didChange(this.path, true)) this.reload();
  • - path string: Absolute path, e.g. "/apps/pong"
  • - deep boolean (optional): Also count writes inside the subtree
  • - returns boolean

Settings

Shared settings


Settings.USER_COLORS

Each user.color is one of these "seats", dealt by the server on join

  • - Each user gets a unique color, which you can use e.g. to color their player sprite, to tell them apart
  • - Seats fill in this order: alone, you're always yellow
  • - If there are more connected users than colors, some will be reused
  • - A user who disconnects keeps their seat for a minute, so a phone lock or a resync gets the same color back
  • - Every name must exist in the default palette (Color._HSL): a seat is sent by name to kernels of every version, which resolve it through their snapshot's /colors

Color

Indexed palette

  • - Used globally, for a consistent look
  • - Also installed in the state tree at /colors, as name: '#RRGGBB' pairs, so users can customize them. Blocks reference colors by name (color: 'yellow'), so editing an entry recolors everything that uses it
  • - Key order is the picker's display order

Color.isColor(value)

Check whether value is a color: a /colors palette name

  • - value *
  • - returns boolean

Color.getRGB(name)

Look up a palette name's RGB

  • - Anyone can edit or delete /colors/cyan, so dangling names are a steady state: they come out loud magenta (plus a deduped chat error), same medicine as missing shapes
  • - A shape being built gets subscribed to the entry (see Block.watch), so editing it rebuilds exactly the shapes using it
  • - name string: Palette name
  • - returns number[]: [r, g, b] in [0,1]

Color.hslToHex(h, s, l)

Convert HSL to a '#RRGGBB' hex color

  • - h number: Hue in [0,360]
  • - s number: Saturation in [0,100]
  • - l number: Lightness in [0,100]
  • - returns string

Color.hexToHSL(hex)

Convert a '#RRGGBB' hex color to HSL

  • - hex string
  • - returns [number, number, number]: h in [0,360], s and l in [0,100]

Sound

Play SFXR style sound effects


Sound.play(sound, volume, pitch, loopID)

Play a sound effect

  • - sound string|Block: Path to a sound effect (e.g. "/sounds/coin") or a SFXR definition
  • - volume number (optional): Volume multiplier
  • - pitch number (optional): Speed multiplier
  • - loopID string (optional): Keep the same loop alive by calling play every tick

Mouse

Browser mouse event listeners


Mouse.dropMoves: boolean

Drop pointermove commands at the source

  • - Scripts set this when a game reads no pointer movement (e.g. from Controls.detect): a thumb resting on glass jitters out a move per frame, and every client would pay a broadcast and a rollback-replay for each

Mouse.retarget()

Re-aim every pointer at what's under it now

  • - For scripts that change what's under a still pointer: a view rebuilt, content scrolling by, a window opening. Hover then follows, and onMouseOut/onMouseOver fire as if the pointer had moved
  • - Not for a lifted finger (no mouse.hit), nor mid-drag, where the next move retargets the drop anyway
  • - example: Mouse.retarget()

Mouse.getBlockAt(block, x, y, ignore, sleeping)

Find the block at x,y (if any)

  • - block Block: Start search from this block
  • - x number: Coordinate to check (relative to block)
  • - y number: Coordinate to check (relative to block)
  • - ignore Block (optional): Block to skip (e.g. the dragged block)
  • - sleeping boolean (optional, default false): Hit enabled: false blocks too: an editor picks what's drawn, not what's clickable (a button's label)
  • - returns Block: Hit block (block itself if none of its descendants)

Keyboard

Browser keyboard event listeners


Keyboard.summon(user)

Show the native on-screen keyboard (on the user's own device only)

  • - Phones and tablets summon their built-in keyboard; computers show nothing, physical typing just keeps working
  • - What the keyboard produces is sent as regular key/text commands to the user's focused block, so typing stays deterministic for everyone
  • - Call this from the text target's press handler (ui/input does, for a press on its text), not from wherever it takes focus: focus also moves to a text field's scroll bar, or back from a toolbar button, and neither is a wish to type
  • - The keyboard rises when the finger lifts. Deferring to the tap's own pointer-up keeps the focus change inside a user gesture (required to show the keyboard), while letting a second finger cancel it into a pinch-zoom (a buffered press never executes then, see Mouse)
  • - example: Keyboard.summon(user)
  • - user Block: Who gets the keyboard (no-op for everyone but the local user: each device keeps its keyboard to itself)

Keyboard.dismiss(user)

Put away the native on-screen keyboard (if summoned) when the tap completes: call this wherever the focus moves

  • - A no-op with the focus back on the text target that summoned it: a toolbar button's press focuses its window, but its click hands the focus back to the editor, and a keyboard that was up stays up
  • - Asked for by a focus that has no use for it, so if the same tap moves the focus on, the request lapses and the new focus decides anew
  • - example: Keyboard.dismiss(user)
  • - user Block: Whose device to dismiss (no-op for everyone but the local user)

Keyboard.copy(user)

Copy as if by Cmd+C, for devices without one: the focused block's onCopy to the device's clipboard (the local user's only: each device keeps its clipboard to itself)

  • - Call this from a button's click handler: the browser only writes inside a tap, which the click's command runs inside of (see Mouse)
  • - example: Keyboard.copy(user)
  • - user Block: Whose device (no-op for everyone but the local user)

Keyboard.paste(user)

Paste for devices without a Cmd+V: opens the chat with a /paste command typed, for the user to paste the clipboard after. Sent, it types the text into the focused block (onText), see Agent._submit

  • - The user pastes, never a script: no page lets scripts read the clipboard (no clipboard-read), so what's on it stays the user's
  • - Nothing on a published game's page, which has no chat input
  • - example: Keyboard.paste(user)
  • - user Block: Whose device (no-op for everyone but the local user)

Chat

Message history dropdown (for logs, chat, and AI assistant)

  • - Logs will automatically show and hide the Chat dropdown
  • - Click or drag the tab just below the header to pull the Chat view down
  • - When opened manually it'll include the input where you can ask questions to the AI assistant

Chat.log(...args)

Add a system message to the chat

  • - args ...any (optional): One or more values to print

Chat.message(user, text)

Show a chat message from user

  • - user Block: Sender
  • - text string: Message text

Chat.error(label, error)

Report an exception: a deduped red chat entry carrying the stack trace (selectable and copyable even on browsers without a devtools console), plus a one-time console.error

  • - The entry is local chrome, so it's fine that browser engines word messages and stacks differently; never feed it back into world state!
  • - label string: Origin, e.g. "/ball/onUpdate" or "render"
  • - error Error: The exception

Mesh

Renderable WebGL mesh with a VBO


new Mesh(vertices)
  • - vertices number[]: [x, y, z, r, g, b, x, y, z, r, g, b, ...]

Font

WebGL text rendering helpers


Font.getWidth(text, size)

Measure a text's width: its longest line's character cells

  • - text string
  • - size number (optional): Font size

Font.getHeight(text, size)

Measure a text's height: its line boxes (size each, LINE_HEIGHT apart)

  • - Not the glyph ink, so a descender doesn't make one line taller than the next
  • - text string
  • - size number (optional): Font size

Font.getVertices(text, size, color, alignX, alignY, outline, outlineWidth)

Get vertices for a text string

  • - text string
  • - size number: Font size
  • - color string|null (optional): Fill color (palette name), or null for no fill
  • - alignX number: 0.0 = left, 0.5 = center, 1.0 = right
  • - alignY number: 0.0 = top, 0.5 = middle, 1.0 = bottom
  • - outline string|null (optional): Outline color (palette name)
  • - outlineWidth number (optional): Outline width

Code

Eval user scripts into JavaScript function objects

  • - Functions are stored as actual callable function objects, interned by source: identical code is always the identical object, so no-op change detection, autosave comparison, and === all work by identity
  • - Snapshots serialize them (via the toJSON stamped on each function) as "@" + source strings (source may start with a JSDoc comment), and Block.fromJSON interns them back

Code.compile(source)

Get the canonical function object for JavaScript source

  • - Invalid source returns a NOP function that still carries the source, so half-typed code in an editor round-trips losslessly until it parses
  • - source string: E.g. "function onUpdate() {}"
  • - returns Function

Code.lex(source)

Lex JavaScript-ish source into source-preserving tokens

  • - Total: unknown characters become unknown tokens instead of throwing, so half-typed or non-JS text still lexes (editors and the assistant colorize best-effort)
  • - source string
  • - returns {type: string, value: string, start: number, end: number}[]

Geometry

Geometry helpers


Geometry.intersectSegmentSegment(x1, y1, x2, y2, x3, y3, x4, y4)

Intersect two line segments

  • - x1 number: First line start X
  • - y1 number: First line start Y
  • - x2 number: First line end X
  • - y2 number: First line end Y
  • - x3 number: Second line start X
  • - y3 number: Second line start Y
  • - x4 number: Second line end X
  • - y4 number: Second line end Y
  • - returns {x: number, y: number, nx: number, ny: number, dx: number, dy: number, time: number} | null

Geometry.intersectSegmentAABB(x1, y1, x2, y2, boxX, boxY, boxWidth, boxHeight)

Intersect a line segment and an axis-aligned bounding box (i.e. "raycast")

  • - x1 number: Line start X
  • - y1 number: Line start Y
  • - x2 number: Line end X
  • - y2 number: Line end Y
  • - boxX number: Box center X
  • - boxY number: Box center Y
  • - boxWidth number: Box width
  • - boxHeight number: Box height
  • - returns {x: number, y: number, normalX: number, normalY: number, deltaX: number, deltaY: number, time: number} | null

Geometry.intersectCircleCircle(x1, y1, radius1, x2, y2, radius2)

Intersect two circles

  • - x1 number: Circle center X
  • - y1 number: Circle center Y
  • - radius1 number: Circle radius
  • - x2 number: Circle center X
  • - y2 number: Circle center Y
  • - radius2 number: Circle radius
  • - returns {x: number, y: number, nx: number, ny: number, dx: number, dy: number, depth: number} | null

Geometry.intersectCircleSegment(cx, cy, radius, x1, y1, x2, y2)

Intersect a circle and a line segment

  • - cx number: Circle center X
  • - cy number: Circle center Y
  • - radius number: Circle radius
  • - x1 number: Segment start X
  • - y1 number: Segment start Y
  • - x2 number: Segment end X
  • - y2 number: Segment end Y
  • - returns {x: number, y: number, nx: number, ny: number, dx: number, dy: number, depth: number} | null

Geometry.intersectCircleAABB(circleX, circleY, circleRadius, boxX, boxY, boxWidth, boxHeight)

Intersect a circle and an axis-aligned bounding box

  • - circleX number: Circle center X
  • - circleY number: Circle center Y
  • - circleRadius number: Circle radius
  • - boxX number: Box center X
  • - boxY number: Box center Y
  • - boxWidth number: Box width
  • - boxHeight number: Box height
  • - returns {x: number, y: number, nx: number, ny: number, dx: number, dy: number, depth: number} | null

Geometry.fill(points)

Triangulate a closed contour with ear clipping

  • - points number[][]: Closed contour points in clockwise screen-space order
  • - returns number[][]: Triangle vertices, with every 3 points forming one triangle

Geometry.outline(inner, outer)

Triangulate the strip between two matching contours

  • - inner number[][]: Inner contour points
  • - outer number[][]: Outer contour points
  • - returns number[][]: Triangle vertices for the strip mesh

Geometry.offset(points, distance)

Offset a closed contour by a fixed distance

  • - points number[][]: Closed contour points in clockwise screen-space order
  • - distance number: Offset distance; positive moves outward, negative inward
  • - returns number[][]: Offset contour points

Geometry.vertices(points, color, z)

Pack 2D points into mesh vertices with a shared color and Z

  • - points number[][]: 2D points
  • - color string: Palette name
  • - z number (optional): Z coordinate
  • - returns number[]: Packed vertex data as x, y, z, r, g, b per point

Geometry.contourVertices(points, color, outline, outlineWidth)

Build packed mesh vertices for a closed contour's fill and/or outline

  • - points number[][]: Closed contour points in clockwise screen-space order
  • - color string | null (optional): Fill color (palette name), or null for no fill
  • - outline string | null (optional): Outline color (palette name), or null for no outline
  • - outlineWidth number (optional): Outline width
  • - returns number[]: Packed vertex data

Geometry.bounds(points)

Compute axis-aligned bounds for a point set

  • - points number[][]: 2D points
  • - returns number[][]: Bounding corners as [[minX, minY], [maxX, maxY]]

Geometry.spatialIndex(items, cellSize)

Build or reuse a uniform-grid spatial index for blocks with bounds

  • - Bounds may be explicit minX/minY/maxX/maxY, or an AABB x/y/width/height
  • - items Block: Block whose children should be indexed
  • - cellSize number (optional): World-space grid cell size
  • - returns { queryCircle(x: number, y: number, radius: number, out?: Block[]): Block[], querySegment(x1: number, y1: number, x2: number, y2: number, out?: Block[]): Block[], queryAABB(minX: number, minY: number, maxX: number, maxY: number, out?: Block[]): Block[], }

Documentation

Searchable index of API documentation

  • - Built entries come from the generated api-documentation.js: the kernel, whose JSDoc the minified build drops, and the desktop's building blocks (Input, Box, Desktop...), whose module banners never enter the tree. With them, the modules in curated order, each with its members
  • - Tree entries are read live, so they include what users write themselves and follow every edit: a function whose source opens with a JSDoc. Named like a built entry, a live one replaces it
  • - Tree entries are named the way scripts reach them: Grid.create for /scripts/grid/create, through the globals aliasing a block (one entry per alias when several do); input.setValue for a method of a type, the one its module's @typedef names (built into API_TYPES) or its own @this {Input} does; the bare path when neither applies. A script whose JSDoc opens a type (@typedef {Shape} Text on /shapes/text) documents the blocks it serves: it's that type's entry, found by its path too. @ignore leaves a function out, as in the source files
  • - search reports on the index and the state tree in one text, the AI assistant's search tool and the Documentation app alike

Documentation.get(name)

Look up one name

  • - name string: E.g. "Util.clamp", "onUpdate", or "Grid.create"
  • - returns string|undefined: Its JSDoc and signature

Documentation.index()

Every documented name, with its documentation: JSDoc and signature for a function, text and member list for a module

  • - returns Map<string, string>

Documentation.search(query, excludePaths)

Report what matches query, in the documentation and the state tree: text for a reader who then searches a name or gets a path

  • - Entries and tree hits rank alike (see _rank): matching more of the query's words comes first, then matching them in the name (a tree hit's parent/key), then by how much of the name they match, then by mentions
  • - A name (onMouseDown, Block.get) gets its full entry, and a module its members too, one line each: the signature, then the summary of its documentation as a comment. So does the one name matching every word, when there is exactly one. The other matches follow as such lines, with a count of the ones left out. An empty query lists the modules, by importance
  • - Tree hits read like grep: the path, then what matched there — a key's value, a matching string, or a script's matching lines with their numbers
  • - query string: A name, or words to look for
  • - excludePaths string[] (optional): Subtrees to leave out of the tree hits
  • - returns string

Random

Deterministic random number generator


Random.number(min, max)
  • - min number (optional)
  • - max number (optional)
  • - returns number: A number in [min, max)

Random.integer(min, max)
  • - min number (optional)
  • - max number
  • - returns number: An integer in [min, max]

Util

Utility functions


Util.sinRad(radians)

Deterministic version of Math.sin


Util.sin(degrees)

Deterministic version of Math.sin (for angles in degrees)


Util.cosRad(radians)

Deterministic version of Math.cos


Util.cos(degrees)

Deterministic version of Math.cos (for angles in degrees)


Util.atan2Rad(y, x)

Deterministic version of Math.atan2


Util.atan2(y, x)

Deterministic version of Math.atan2 (for angles in degrees)


Util.pow(base, exponent)

Deterministic version of Math.pow


Util.hypot(x, y)

Deterministic version of Math.hypot


Util.exp(x)

Deterministic version of Math.exp


Util.PI

Deterministic version of Math.PI


Util.max(...numbers)

Deterministic version of Math.max


Util.min(...numbers)

Deterministic version of Math.min


Util.abs(x)

Deterministic version of Math.abs


Util.floor(x)

Deterministic version of Math.floor


Util.ceil(x)

Deterministic version of Math.ceil


Util.round(x)

Deterministic version of Math.round


Util.sqrt(x)

Deterministic version of Math.sqrt


Util.mod(n, m)

Compute n modulo m

  • - Like n % m but gracefully handles negative numbers (e.g. mod(-1, 5) returns 4)
  • - n number
  • - m number
  • - returns number

Util.radians(degrees)

Convert degrees to radians

  • - degrees number
  • - returns number: Radians

Util.degrees(radians)

Convert radians to degrees

  • - radians number
  • - returns number: Degrees

Util.roundTo(number, step)

Round a number to the nearest step

  • - number number
  • - step number
  • - returns number: Rounded number

Util.clamp(number, min, max)

Clamp a number between min and max

  • - number number
  • - min number
  • - max number
  • - returns number: Clamped number

Util.length(x, y)

Calculate the length of a vector

  • - x number
  • - y number
  • - returns number: Length of the vector

Util.distance(x1, y1, x2, y2)

Calculate the distance between two points

  • - x1 number
  • - y1 number
  • - x2 number
  • - y2 number
  • - returns number: Distance between the two points

Util.splitPath(path, all)

Split a path into prefix and key

  • - path string: A path like "/data/icons/play"
  • - all boolean (optional): Split into *all* possible parts
  • - returns string[]: A prefix/key pair like ["/data/icons", "play"] (or ["/", "system", "images", "app"] if all is true)

Util.joinPath(...parts)

Join parts into one path

  • - parts ...string: Path parts like "/data/icons", "play"
  • - returns string: A full path like "/data/icons/play"

Util.copyToClipboard(text)

Copy text to clipboard

  • - text string
  • - returns Promise<boolean>

Util.isName(str)

Check if str is an allowed display name like "First Last3" or "My Game"

  • - The one rule for user, project, and shared game names alike
  • - Not ridiculously long
  • - Safe to display in HTML (no <, no XSS injection)
  • - All characters can be displayed by our WebGL font (no emojis!)
  • - No attention-calling punctuation like "---( Magni )---"
  • - No zero-length or space-only strings (an invisible name is no name)
  • - str string
  • - returns boolean

Util.hash(str, seed)

Hash str into a string of exactly 6 chars, like "k3v9p2"

  • - cyrb53 (https://stackoverflow.com/a/52171480) mixing, ~30 bits -- plenty when uniqueness only needs to hold within one scope, like an author's publish IDs
  • - The first char is always a letter, so hash IDs can never look like a number (e.g. publish IDs are distinct from numeric project IDs by design, no namespace prefix needed)
  • - Deterministic (Math.imul and bit ops only), so multiplayer clients running the same command always agree on the result
  • - str string
  • - seed number (optional)
  • - returns string

Util.defineGlobal(name, descriptor)

Define a global variable (or ignore if already defined)

  • - name string
  • - descriptor PropertyDescriptor

Shape

A shape is any block whose shape names a script in /shapes

  • - E.g. shape: '/shapes/circle'. The script builds the block's mesh from the properties below, which every shape script reads, plus its own (Circle's segments, Text's alignX), and rebuilds it when one changes
  • - A shape is bounds first, drawing second: without a color (or an outline) it draws nothing, and is a hit area, a collider, or the bare frame of a graphic (shapes on a root shape, which colored is also their background)

shape.width: number = 40

Width in pixels (text and obj size themselves instead)


shape.height: number = 40

Height in pixels


shape.color: string

Fill: a palette name

  • - None by default: a colorless shape draws nothing (obj excepted, see Obj)
  • - An outline on a shape *without* a color stands alone: a ring

shape.outline: string

Outline color (none by default)


shape.outlineWidth: number = 2

Outline thickness, drawn outward

Panel

shape: '/shapes/panel' draws a width x height rectangle hanging from its top-left corner at the origin (windows, buttons)

Rectangle

shape: '/shapes/rectangle' draws a width x height rectangle centered on the origin

Circle

shape: '/shapes/circle' draws a disc filling width x height, centered on the origin


circle.segments: number

Triangle count

  • - 3 is a triangle, 6 a hexagon (default: enough to look round at its size)

Star

shape: '/shapes/star' draws a star filling width x height, centered on the origin


star.points: number = 5

Number of points


star.innerScale: number = 0.5

Inner radius over the outer

  • - 0.3 with 4 points is a sparkle

Triangle

shape: '/shapes/triangle' draws a width x height triangle centered on the origin, apex up

Wedge

shape: '/shapes/wedge' draws a width x height right triangle centered on the origin, its right angle at the bottom left

Text

shape: '/shapes/text' draws a string, sized to fit it

  • - color fills the glyphs and outline rings them, like any shape; width and height are ignored
  • - alignX and alignY place the text around the block's origin: at 0.5 it's centered there, at 0 it hangs right and down from it

text.text: string = 'Text'

The string to draw (\n breaks lines)


text.size: number = 16

Font size in pixels


text.alignX: number = 0.5

Horizontal alignment, 0 (left) to 1 (right): 0.5 centers it


text.alignY: number = 0.5

Vertical alignment, 0 (top) to 1 (bottom): 0.5 centers it

Obj

shape: '/shapes/obj' draws a /meshes model, at the model's own size

  • - The model's o names pick its colors: fill takes the block's color (white by default), outline its outline, and any palette name itself

obj.obj: string

Path to the model, e.g. /meshes/plus

Apps

The app definitions

  • - Shell.startApp(user, Apps.pong) runs a clone of one in a window, a shortcut's path names one (/apps/pong), and a published project runs one full screen

Inventory

Curated set of blocks in /inventory shown by the Build app

Scripts

Functions shared by reference, in modules under /scripts

  • - A block points at one with Block.script('/scripts/input/setValue'), so a template and all its clones run the same function, and an edit in the Code app reaches them all
  • - A module is a block of functions; the desktop aliases a few as globals: Box, Controls

Scripts.parseOBJ(obj)

Parse an OBJ file

  • - obj string: The OBJ to parse, e.g. exported from Blender with:
    • - Export as "Wavefront (.obj)"
    • - Model in screen coords (X+ right, Y+ down)
    • - No UV Coordinates
    • - No normals
    • - Triangulated Mesh
    • - No materials
  • - returns [number[][], Map<string, number[][]>]: Array of [x,y,z] vertices, and a Map of object names to their [a,b,c] (vertex index) triangles

Scripts.window.dock(owner)

Dock this window to owner: from now on the two (and whatever is docked to either) move, rise, and close as one group, and only the group's root keeps its maximize and close buttons. This window shows the undock button in their place

  • - A title bar drag that lands a window against another docks on its own (see onDragEndTitleBar); Build docks its palettes explicitly
  • - The app hears onDock(owner), and onUndock(owner) on the way out: the Inspector is its Explorer's for as long as it's docked to it
  • - owner Block: The window to dock to

Scripts.window.undock()

Undock this window (and whatever is docked to it), getting its maximize and close buttons back: popped a cascade step down and right, so it clearly left, and no longer sits in a docked position

Controls

Standard game controls


Controls.holding(user, control)

Check whether this user is holding a game control

  • - user Block
  • - control string (optional): "left" or "right" (or undefined for any tap/key)
  • - returns boolean: True while the control is held

Controls.pressed(user, control)

Check whether this user pressed a game control this tick

  • - user Block
  • - control string (optional): "left" or "right" (or undefined for any tap/key)
  • - returns boolean: True on the first tick the control is pressed

Controls.joystick(user, side, radius = 48)

Read a virtual joystick as a direction vector

  • - user Block
  • - side string (optional): "left" or "right" screen half and arrow-keys/WASD (or undefined for either)
  • - radius number (optional): Max drag distance in pixels
  • - returns number[]: [x, y] from -1 to +1

Button

Push button: dips down while pressed

  • - Edit its onClick to make it do something (the starter just logs)

button.label: Block

The text shape (set its text, or replace it with a shape of your own for an icon button)

Slider

Slider: drag the thumb (or click the track) to pick a value, or step it with the buttons at the ends


slider.value: number

The current value


slider.min: number

Value at the start of the track


slider.max: number

Value at the end of the track


slider.step: number

Round value to a multiple of this (from min)

  • - Also how far the buttons step (else a tenth of the range)

slider.axis: string = 'x'

'y' for a vertical slider


slider.thumb: Block

The handle (which you can style)


slider.decrementButton: Block

The button at the min end


slider.incrementButton: Block

The button at the max end


function onChange(slider)

Event sent when setValue changes the value


slider.setValue(value)

Set the value (rounded to step and clamped to min..max), sending onChange if it changed

  • - To set it silently, assign value and call layout instead
  • - value number
  • - returns number: The value actually set

slider.layout()

Update layout after changing value, width, or height directly

Scroll

Scroll view: wheel the content around (with bounce), or drag its bars

  • - No drag-to-scroll, on purpose: a swipe a pixel off the background would drag a block off to who-knows-where instead, so fingers get the bars
  • - The bounce is loosely based on https://github.com/zynga/scroller

scroll.content: Block

Put your blocks in here, size it (width and height) to fit them, then call updateBounds

  • - Its x and y are the scroll position (set them with scrollTo)

scroll.minX: number = 0

How far negative content.x scrolls (its overflow past the view), and minY likewise: 0 is the start


scroll.hideScrollBarX: boolean

Scroll horizontally without a bottom bar (e.g. a single-line text input)


scroll.scrollBarX: Block

The bottom slider (which you can style), once the content has overflowed the view


scroll.scrollBarY: Block

The right slider, likewise


scroll.updateBounds()

Update the scroll range after resizing the view or its content


scroll.scrollTo(x, y)

Scroll to put content at (x, y): 0 shows its start, minX and minY its end (clamped to that range), and any bounce stops

  • - x number
  • - y number

scroll.push(axis, delta)

Scroll by delta along axis ("X" or "Y"): freely within the range, and past an edge against a rubber band that stiffens the further out it is

  • - The wheel and a bounce's momentum both push through it
  • - onUpdate springs the stretch back
  • - axis string
  • - delta number

Input

Text input widget


input.value: string

The text


input.multiline: boolean

Enter inserts a newline (else it sends onSubmit)


input.placeholder: string

Hint shown if there's no text


input.prefix: string

Fixed text drawn before the value, on its first line: not part of it, so neither editable nor selectable (a path's folder, a prompt)


input.formatText: function(string): string

Custom text formatting (e.g. syntax highlighting)


input.readOnly: boolean

Text to select and copy, not edit: no caret or on-screen keyboard, and only setValue changes it (the Documentation app's results)


input.caretColor: string

The text cursor's color (else the text's)


input.selectionColor: string = 'lightGray'

Behind selected text


input.caretIndex: number = 0

Where the text cursor is (move it with setCursor)


input.anchorIndex: number

The selection's other end, while text is selected


input.undoStack: Block

The edits to undo, while there are any (redoStack likewise)


input.scroll: Block

The text is always drawn in a scroll view (in case it's too long to fit)

  • - scroll.content.text is the text shape (which you can style)

function onChange(input)

Event sent after every edit (except setValue)


function onSubmit(user, input)

Event sent on Enter (unless multiline)


function onCancel(user, input)

Event sent on Escape


input.setValue(value, caretIndex)

Replace the text

  • - value string: The new text
  • - caretIndex number (optional): Move the text cursor here

input.onResize()

Fit the text area to the input's width and height

  • - Call it after changing them from a script

input.setCursor(index, keepAnchor)

Move the text cursor (scrolling it into view)

  • - index number: New position
  • - keepAnchor boolean (optional): Keep and extend the current selection

input.undo()

Undo the last edit


input.redo()

Redo the last undone edit


input.insertText(text)

Insert text at the current cursor position

  • - text string

input.deleteText(beforeCursor)

Delete the character at the cursor (or the selected text)

  • - beforeCursor boolean (optional): Backspace instead of Delete

Scoreboard

Scoreboard: a text shape listing each user's name and points, best first

  • - Style it like any text (size, alignX, outline...): the lines take the users' colors
  • - The text follows scores and the users: it's re-listed every tick

scoreboard.scores: Block

Points by user ID (which you can edit directly)


scoreboard.add(user, points)

Add points to a user's score

  • - user Block: Scoring user (nothing happens if they've left)
  • - points number: Points to add (negative to subtract)

scoreboard.clear()

Clear all scores, e.g. for a new round (departed users' points too)

Box

Box any key-value property to make it a drag-and-drop thumbnail + label

  • - Every app in the entire OS (including the desktop) uses boxes as the common data transfer format, so you can drag a box from anywhere and drop it on anything.

Drag data contract (data is the shared drag Block)

  • - dragType: 'box'
  • - key: Property key the value was dragged from (dropping inserts key: value)
  • - value: The dragged value (a clone, for blocks)
  • - source: Data path the value came from (if any) — a move or trash deletes it in onDragEnd, and watchers rebuild any views
  • - drag: The clone (also Shell.dragClone). It follows the cursor, so it receives the drag events, and hit testing ignores it
  • - locked: The source keeps its value (a shelf with its pen unlit, see the Explorer): a drop that would move it copies instead
  • - dropAction: The target's claim, 'move' (a shelf taking a box, both ends on screen). Without one a drop is a copy (Build's canvas stamping a prefab). Shift makes any drop a copy, and Alt a link (see getAction)
  • - dropParent/dropIndex: Where the drop will insert (no index: last)
  • - Shell.dropPreview: Marks the drop target, as a ring around a box it would land inside (see updateDragClone), or styled by whoever shows it (a caret in a shelf's gap, a square on a desktop slot)

Shell

/shell: the editing shell, which runs apps in windows next to the tools that build them, on the desktop: the folder /desktop shown as free-floating boxes (see desktop), so a link to an app is a launcher, and anything dropped on the desktop is parked there

  • - The kernel only ever talks to /system/shell, which is this block on the editing desktop and /kiosk in a published app; both answer to Shell, so the same app runs under either

Shell.startApp(user, app, ...args)

Start an app

  • - example: Shell.startApp(user, Apps.pong)
  • - user Block: User who's starting the app
  • - app Block: App definition or detached app copy
  • - args ...any (optional): "command line arguments" to pass to the started app
  • - returns Block: The started window

Shell.stopApp(user, win)

Stop an app

  • - user Block|null: User who's stopping the app, or null if the app stops itself
  • - win Block: The app's window

Shell.open(user, path, app)

Open the value at path with the app that handles it, like a file: a sound in Sound, a color in Color, a script in Code, a shape or an app in Build, a folder in a new Explorer (see getOpener)

  • - The *editing* half of "open with": a desktop icon runs an app instead
  • - example: Shell.open(user, '/graphics/ball')
  • - user Block: Who's opening it
  • - path string: Data path of the value
  • - app Block (optional): Open with this app (a definition, e.g. a desktop icon's) instead, if it can
  • - returns Block|undefined: The window, or undefined if nothing opens the value (a number, a plain string -- or app can't)

Shell.setFocus(user, block)

Move a user's keyboard focus

  • - Focus raises: the focused window comes to the front
  • - The title bar lights up while anyone's focus is inside the window
  • - Works in a published app too, where there is no window to raise
  • - example: Shell.setFocus(user, myInput)
  • - user Block: Whose focus to move
  • - block Block (optional): Block to receive the user's key events (omit to just clear focus)

Shell.restore(user)

Restore (un-maximize) whatever window user has maximized: the flip side of the title bar's maximize button

  • - user Block

Shell.getApp(block)

Get the running app a block belongs to

  • - undefined outside any window: shape functions render blocks anywhere, including inventory shelf copies
  • - For tools: an app reaches itself through this.parent, which also works published
  • - block Block: A block inside the app
  • - returns Block|undefined

Shell.getProcesses(app)

Find every window running app

  • - Instances are clones with no link back to their app, so name stands in for its identity
  • - app Block: App definition
  • - returns Block[]: The windows

Shell.newKey(prefix)

A key no window has had, for a new one: prefix numbered by a count that never goes back

  • - For getProcessKey hooks that open a fresh window per launch. Not Windows.getUniqueKey, which hands a closed window's key to the next one: a path kept to a window that's gone (Build's Properties palette, an Explorer's Inspector) then stays dead, as any path to something deleted does, instead of quietly naming a stranger
  • - Apps with one shared window key it by name instead, so that a relaunch *is* the running window (see startApp)
  • - example: return win?.getKey() ?? Windows.newKey('code')
  • - prefix string
  • - returns string: E.g. 'code7'

Shell.trash(path)

Move the value at path into the Trash (/desktop/trash, a folder like any other: browse it to take things back, or drag them out), references following (a trashed graphic still in use keeps working until the Trash is emptied)

  • - Already in the Trash (or the Trash itself): deleted for good
  • - A folder whatever it holds, by its path (see Box.getType): it keeps the keys it's given, shape and icon included
  • - example: Shell.trash('/graphics/ball')
  • - path string

Shell.rename(path, name)

Rename the value at path to name, as a key code can type ("My circle" becomes "myCircle"), references following (see Block.rewritePaths)

  • - What's left that code still can't type (a built-in like "parent", a leading digit like "2ndPlayer") is refused with a hint, and an empty or unchanged name changes nothing, like a file manager
  • - example: Shell.rename('/graphics/ball', 'Blue ball') // '/graphics/blueBall'
  • - path string
  • - name string
  • - returns string: The path after the rename (path itself if nothing changed)