Events
Events are what makes your app "tick". E.g. the "Update" event is sent every frame, and by adding an onUpdate function you can do stuff like moving a game object.
- on<EventName> functions are called in the bubble phase
- capture<EventName> functions are called in the capture phase
- All event handlers bind this to the block that has the handler function
function onUpdate()
Event handler that runs every frame
- Every block inside a running app receives it automatically, parent-first. Set enabled: false on a block to sleep its whole subtree (templates, editor copies)
- Runs once for everyone: there is no user parameter. Poll per-user input by looping for (const user of OS.users.values())
- Multiplayer games typically spawn one actor per user in onAddUser, and each actor's own onUpdate then updates it
- this Block: Target block
function onStart(user, ...args)
Run when this (app) block is started
- user Block: User who started it
- args ...any: Optional "command line" parameters
function onStop(user)
Run when this (app) block is stopped
- user Block: User who stopped it
function onAddUser(user)
Run when a user joins this app
- Typically spawns the user's actor, which onUpdate then updates
- user Block: User that joined
function onRemoveUser(user)
Run when a user leaves this app
- user Block: User that left
function onMouseMove(user, x, y, hit)
Run when the pointer moves (if over this block)
- Return true to stop propagation
- 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 propagation
- 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 propagation
- 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 propagation
- 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 propagation
- 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 propagation
- 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 wheel scrolls (if over this block)
- Return true to stop propagation
- user Block
- deltaX number
- deltaY number
- hit Block: Block under the pointer
function onKeyDown(user, key)
Run when a key is pressed (if this block has keyboard focus)
- user Block
- key string
function onKeyUp(user, key)
Run when a key is released (if this block has keyboard focus)
- user Block
- key string
function onFocus(user)
Run when a user's keyboard focus moves into this (app) block's window
- This window now receives that user's key events
- user Block: User whose focus arrived
function onBlur(user)
Run when a user's keyboard focus moves out of this (app) block's window
- Also sent when the window closes or the user disconnects
- Another user may still have focus here: check OS.users before treating the window as unfocused
- user Block: User whose focus left
function onCopy(user)
Run when the user triggered copy/cut-to-clipboard (if this block has keyboard focus)
- user Block
- returns string|void: Text to place on the clipboard
function onText(user, text)
Run when text is pasted (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 propagation
- 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 propagation
- 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 propagation
- 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 propagation
- 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 propagation
- 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 propagation
- 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 propagation
- 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 propagation
- 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 (app) block's window is resized
- oldWidth number
- oldHeight number
function onLoad()
Run after a project snapshot loads
Block
State is stored in a single tree of Block nodes
- Each block has a set of key-value properties
- Multiplayer rollback bookkeeping is automatically handled by a Proxy
new Block(properties)
Create a new block
- properties object: Add these key: value pairs to the new block
block.clone(properties)
Deep-copy this block
- properties object: Add these key: value pairs to the clone (or use undefined as the value to *remove* a property)
- returns Block
block[Symbol.iterator]()
Iterate over this block's own properties as [key, value] pairs
- returns IterableIterator<[string, any]>
block.has(key)
Check whether this block has an own property named key
- key string
- returns boolean
block.getSize()
Count this block's own properties
- returns number
block.keys()
Iterate over this block's own property keys
- returns IterableIterator<string>
block.values()
Iterate over this block's own property values
- returns IterableIterator<any>
block.getKey()
Get this block's key in its parent
- returns string
block.getUniqueKey(key)
If "key" already exists, return "key2", etc.
- key string
- returns string
block.renameKey(oldKey, newKey)
Rename an existing property while preserving its position
- References elsewhere are *not* touched; editors that want renaming to keep them working call Block.rewritePaths afterwards
- oldKey string
- newKey string
block.getIndex()
Get this block's index in its parent
- returns number
block.insert(key, value, index)
Insert a new property at index
- key string
- value *
- index number
block.getPath()
Get a path reference to the block
- A block can only be in one place in the state tree, with a unique parent
- Hit-testing uniquely identifies the block
- Events trickle/bubble along the unique path
- But you can store *references*, which look up the block when accessed
- This allows reusing "assets" like functions and meshes, which don't need hit-testing and events
- returns string: E.g. "/desktop/windows/draw/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
- y number
- z number
- returns [number, number]
block.worldToLocal(x, y)
Convert world coordinates to this block's local space
- x number
- y number
- returns [number, number]
block.find(predicate)
Find all blocks satisfying predicate (including this)
- predicate function(Block): boolean
- returns Block[]
block.getBounds()
Return bounding box (in local space)
- returns [number, number, number, number]: [minX, minY, maxX, maxY]
block.getBoundsTree()
Return bounding box, expanded to include all children
- Fast 2D UI case (x/y/scaleX/scaleY)
- returns [number, number, number, number]: [minX, minY, maxX, maxY]
block.getBoundsTreeTransformed()
Return bounding box, expanded to include all children
- Fully transformed case (rotation/3D)
- returns [number, number, number, number]: [minX, minY, maxX, maxY]
Block.fromJSON(json)
Static
Block.get(path)
Get the value stored at path: one dereference, no more
- What you stored is what you get: if the value is itself a path (a link), you get that path *string* — resolve it with another Block.get if that's what you meant. Links are never followed implicitly, neither mid-path nor at the final key
- Safe to pass an undefined path (so you can do Block.get(block.path) without having to check if block *has* a path), and safe to pass a path whose blocks don't exist (returns undefined)
- Values that are not a path pass through unchanged, so you can resolve properties that hold *either* a path or a plain value: Block.get(data.drag)
- path string: A path like "/documents/note1"
- returns *
Block.isPath(value)
Check if value is a path (like "/path/to/value")
- value *
- returns boolean
Block.script(path)
Get a callable reference to the function stored at path
- path string: E.g. "/scripts/bounce/onUpdate"
- returns Function
Block.set(path, value)
Set the value path points to
- path string
- value *
Block.clone(value)
Create a deep copy of value (if it's a block)
- returns *
Block.rewritePaths(oldPath, newPath, block)
Update every reference to oldPath (or inside it) to point at newPath
- E.g. after renaming or moving a value, so "/graphics/ball" strings and Block.script values keep pointing at the same thing
- oldPath string
- newPath string
- block Block: Subtree to update (defaults to the whole tree)
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
Sound
Sound effects
Sound.play(sound, volume, pitch, loopID)
Play a sound effect
- sound Block|string: SFXR definition, or a path to one
- volume number: Volume multiplier
- pitch number: Speed multiplier
- loopID string: Keep the same loop alive by calling play every tick
Chat
The one message log
- Everything anyone says lands here in one chronological list: chat, toasts, script errors, prompts, and assistant replies
- It's local chrome: a private HTML transcript that never enters lockstep, with native text selection, copy/paste, and code rendering for free
- One surface, two moods: the drop-down shows the full history (opened by clicking the Ask input where assistant.js installs one), and new entries *peek* it — the same drop-down dropped just far enough to show the newest entry, sliding shut on a timer unless the user pins it open
- Rollback-safe: entries emitted from *inside* lockstep execution (chat, toasts, errors) set replay: true, so _rollback drops them and the replay re-emits them; local entries (prompts and replies) survive
Chat.add(entry)
Append an entry, peek the drop-down at it, and re-render
- quiet entries skip the peek: the assistant's prompt/reply land while the asker already has the drop-down open in front of them
- returns Object: The entry (so a pending reply can be mutated later)
Chat.message(user, text)
Show a chat message from user
- user Block (optional): Sender (or none, to speak as the System)
- text string: Message text
Chat.error(label, error, replay)
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
- replay boolean (optional): Set when thrown from inside lockstep execution, so a rollback drops the entry and the replay re-emits it
Chat.toast(...args)
Show a temporary text under the page header (it also stays in the chat)
- args ...any (optional): Information to show
OS
OS.state: Block
Block tree with multiplayer-synced state
OS.userID: number
Local user ID (differs per client, use with care to not desync!)
OS.time: number
Tick counter
OS.projectID: string
Loaded project (e.g. "17/1")
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 both fit the whole window, the kiosk page 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 used by the kiosk page's auto-start
- 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 page 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 or a block inside it (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('/graphics/snekIcon') 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.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 assigned "yellow", "pink", "purple", or "cyan" when they connect
- color string
- returns Block: The user block (or undefined)
Desktop
Desktop.startApp(user, app, ...args)
Start an app
- example: Desktop.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
Desktop.stopApp(user, win)
Stop an app
- user Block: User who's stopping the app
- win Block: The app's window
Desktop.setFocus(user, block)
Move a user's keyboard focus, sending onBlur/onFocus to the windows (and their apps) that lost/gained it
- example: Desktop.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)
Desktop.restore(user)
Restore (un-maximize) whatever window user has maximized: the flip side of the title bar's maximize button
- user Block
Settings
Shared settings
Settings.USER_COLORS
Each user.color is selected from this set of colors
- Each user gets a unique color, which you can use e.g. to color their player sprite, to tell them apart
- If there are more connected users than colors, some will be reused
- If a user disconnects, they might get a different color when reconnected
Color
Color
- The palette lives in the state tree at /colors, as name: '#RRGGBB' pairs — edit an entry, and everything referencing its name repaints
- Blocks reference colors by palette name (color: 'yellow') or literal hex (color: '#BADA55')
Color.isColor(value)
Check whether value is a color: a /colors palette name, or a literal like "#BADA55"
- value *
- returns boolean
Color.resolve(value)
Resolve a color value to '#RRGGBB' hex
- A palette name looks up /colors; a "#..." literal passes through
- Anyone can edit or delete /colors/cyan, so dangling names are a steady state: they resolve to loud magenta (plus a deduped chat error), same medicine as missing meshes
- value string (optional): Palette name or hex literal (null stays null, so "no fill" passes through shape code unharmed)
- returns string
Color.getRGB(value)
Resolve a color value to RGB
- value string: Palette name or '#RRGGBB' hex
- returns number[]: Shared [r, g, b] in [0,1] — don't mutate!
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]
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 or '#RRGGBB' hex
- z number (optional, default 0): 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: Fill color (palette name or hex), or null for no fill
- outline string | null: Outline color (palette name or hex), or null for no outline
- outlineWidth number (optional, default Settings.BORDER): 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, default 64): 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[], }
Mesh
new Mesh(vertices)
- vertices number[]: [x, y, z, r, g, b, x, y, z, r, g, b, ...]
Mesh.getCached(hash)
Get a cached mesh
- hash string: Unique content signature for the mesh
- returns Mesh
Mesh.setCached(hash, mesh)
Cache a mesh
- hash string: Unique content signature for the mesh
- mesh Mesh: Mesh to cache
Code
Code
- 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}[]
Font
Font.getVertices(text, size, color, alignX, alignY, outline, outlineWidth)
Get vertices for a text string
- alignX number: 0.0 = left, 0.5 = center, 1.0 = right
- alignY number: 0.0 = top, 0.5 = middle, 1.0 = bottom
Agent
Browser-side agent helpers
Agent.getContext(user, excludePaths)
Build context for the current agent request
- A manifest of threads to pull on, not dumps: everything included implies "this matters" (Grice), and only the model can match threads against the prompt. So blocks stay behind get_block, and only the focused editor's source is inlined
- user Block: The asking user; their focus picks the edited value
- excludePaths string[] (optional): Subtrees to hide from the agent, e.g. ephemeral UI state
- returns string
Watch
Block change watchers
- Caches and apps subscribe to the Blocks they depend on.
- When a Block mutates, subscribers get marked dirty.
- Records are local cache state, never lockstep state: they don't survive snapshot loads, and rollback undo dirties them at client-specific times. So dirtiness fires *spuriously*, and a handler that writes shared state in response must be an idempotent recompute from the watched state (skip the write when nothing changed), and must re-arm its watches.
- Idempotent includes *key order*: deleting and re-adding keys appends them at the end, so seed any keys set after the loop at creation, so a first build orders keys like every later rebuild (see /scripts/grid)
Watch.block(owner, name, block)
Watch a single block for direct property changes.
- owner Block: Owner of the watch record
- name string: Identifier for this watch
- block Block: Block to watch
Watch.tree(owner, name, root)
Watch every block in a subtree.
- owner Block: Owner of the watch record
- name string: Identifier for this watch
- root Block: Root of the subtree to watch
Watch.path(owner, name, path)
Watch a path for direct value changes or replacement anywhere along the path.
- owner Block: Owner of the watch record
- name string: Identifier for this watch
- path string: Absolute path to watch
Watch.isDirty(owner, name)
Check whether a named watch has been dirtied.
- A watch that doesn't exist is dirty: unknown means recompute (matching _record's _dirty: true initial state). This is what refreshes a just-joined client, whose snapshot-loaded tree has no watches yet.
- owner Block: Owner of the watch record
- name string: Identifier for this watch
- returns boolean
Watch.clear(owner, name)
Remove a named watch.
- owner Block: Owner of the watch record
- name string: Identifier for this watch
Watch.clearAll(owner)
Remove all watches owned by owner.
- owner Block: Owner of the watch records
Box
Box.fromValue(value, key, path)
Box value, ready to drag
- value *
- key string (optional): Property key to show after the value
- path string (optional): Data path of the value (for thumbnails)
- returns Block: box
box.toValue()
Unbox the original value
- this Box
- returns *: value
Scripts
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.add(user, points)
Add points to a user's score
- user Block: Scoring user
- points number: Points to add (negative to subtract)
Scripts.clear()
Clear all scores, e.g. for a new round (departed users' points too)
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.getBlockAt(block, x, y, ignore, canHitSelf)
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): Optional block to skip (e.g. the dragged block)
- returns Block|null: Hit block (or null)
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 a click/mouse-down handler: 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
- 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)
- Like summon, applied when the tap completes, so a pinch-zoom starting on empty desktop doesn't yank the keyboard away mid-edit
- example: Keyboard.dismiss(user)
- user Block: Whose device to dismiss (no-op for everyone but the local user)
Random
Deterministic random number generator
Random.number(min, max)
- min number (optional, default 0)
- max number (optional, default 1)
- returns number: A number in [min, max)
Random.integer(min, max)
- min number
- 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