EXPERIMENTAL / EVERYTHING BELOW IS A REAL RECORDING

Apps your AI can write,
running live.

Osvauld is an experiment in a different kind of software platform: small Lua apps over synchronized documents, rendered natively, written and changed by people and agents while they run. This is what it can do so far.

01 / APPS

An app is a Lua file.

The runtime turns a description of the interface into layout and vector pixels, every frame. The board you see is a few hundred lines of Lua. The cards live in a shared document, so it survives restarts and syncs between people with no extra code.

-- KANBAN
--
-- The entry point: it wires the parts together and owns the view. `require` resolves against
-- this app's own source doc, so these three names are `theme.lua`, `model.lua` and
-- `ui/widgets.lua` sitting next to this file in the uploaded folder.
--
-- The split runs along one line: **anything that touches pointer state stays here.** `drag`,
-- `placement` and `col_modal` are per-viewer UI state that handlers write and the view reads,
-- so every piece that reads them (`drop_slot`, `card_of`, `column_of`) lives in this file where
-- the read and the write are visible together. `ui/widgets.lua` holds the pieces that are pure
-- functions of their arguments, and `model.lua` holds the doc.
local C = require("theme")
local W = require("ui/widgets")
local model = require("model")

local board, update = model.board, model.update
-- One shared table, not a copy: `S.drag` is written by handlers inside model.lua and read here.
-- A `local drag` on either side could not do this — a local is file-scoped, so the assignment
-- would be invisible across the boundary.
local S = model.state

-- VIEW PIECES

-- Which gap the indicator belongs in: 1 = above the first card, #list+1 = below the last.
-- A column-background drop ("into") appends, which is the trailing gap — same answer, one path.
local function drop_slot(c, list)
  if not (S.drag and S.placement) then
    return nil
  end
  if S.placement.kind == "into" then
    return S.placement.col == c.id and #list + 1 or nil
  end
  if S.placement.kind ~= "card" or S.drag.id == S.placement.id then
    return nil
  end
  for i = 1, #list do
    if list[i].id == S.placement.id then
      return S.placement.before and i or i + 1
    end
  end
end

-- `ghost` builds the floating copy: same look, but no id and no handlers, because a duplicate
-- id would register a second drag target and collide in the state store.
local function card_of(c, ghost)
  local dragging = S.drag ~= nil and S.drag.kind == "card" and S.drag.id == c.id
  local t = ui.row({
    w_full = true,
    gap = 8,
    px = 10,
    py = 9,
    radius = 8,
    fill = ghost and C.card_hi or C.card,
    stroke = { 1, ghost and C.accent or C.line_soft },
    align_center = true,
    ui.col({ grow = true, ui.text({ c.text, color = C.text, font_size = 13 }) }),
  })
  if ghost then
    return t
  end
  t.id = c.id
  t.hover_fill = C.card_hi
  t.hover_stroke = { 1, C.line }
  t.opacity = dragging and 0.3 or 1.0
  t.on_drag = function(e)
    local phase, dx, dy, scale, ox, oy = e.phase, e.dx, e.dy, e.scale, e.origin_x, e.origin_y
    update({ kind = "drag", what = "card", id = c.id, phase = phase, x = ox, y = oy, scale = scale })
  end
  t.on_drop = function(e)
    local phase, x, y = e.phase, e.x, e.y
    update({ kind = "drop", id = c.id, phase = phase, x = x, y = y })
  end
  t[#t + 1] = W.icon_button("del-card:" .. c.id, "x", function()
    update({ kind = "delete", id = c.id })
  end, C.danger)
  return t
end

local function composer(c)
  local s = ui.state("draft:" .. c.id, { text = "", open = false })
  local close = function()
    s.open = false
  end
  local send = function()
    if s.text ~= "" then
      close()
      update({ kind = "add", col = c.id })
    end
  end
  local anchor = ui.button({
    id = "add-card:" .. c.id,
    w_full = true,
    h = 32,
    radius = 6,
    center = true,
    fill = C.accent,
    hover_fill = C.accent_hi,
    press_scale = 0.97,
    ui.text({ "Add card", no_wrap = true, color = "#ffffff", font_size = 13 }),
    on_click = function()
      s.open = true
    end,
  })
  if not s.open then
    return ui.col({ px = 10, py = 10, anchor })
  end
  local panel = ui.col({
    w = 280,
    gap = 8,
    pad = 10,
    radius = 8,
    fill = C.panel,
    stroke = { 1, C.line },
    ui.input({
      value = s.text,
      id = "draft:" .. c.id,
      autofocus = true,
      w_full = true,
      h = 34,
      px = 10,
      radius = 6,
      fill = C.sunken,
      stroke = { 1, C.line },
      color = C.text,
      font_size = 13,
      on_input = function(v)
        s.text = v
      end,
      on_enter = send,
    }),
    ui.button({
      id = "submit-card:" .. c.id,
      h = 32,
      radius = 6,
      center = true,
      fill = C.accent,
      hover_fill = C.accent_hi,
      press_scale = 0.97,
      ui.text({ "Create card", no_wrap = true, color = "#ffffff", font_size = 13 }),
      on_click = send,
    }),
  })
  return ui.col({
    px = 10,
    py = 10,
    ui.overlay({ id = "composer:" .. c.id, side = "top", align = "start", on_dismiss = close, anchor, panel }),
  })
end

-- What this column is *this frame*. Mid-drag the width comes from pointer state and the doc is not
-- touched; on release the model writes it once. Same shape as the card and column drags above —
-- nothing goes to the CRDT until the gesture finishes, so a drag is one op and not sixty.
local function width_of(c)
  if S.resize and S.resize.id == c.id then
    return S.resize.w
  end
  return c.w or C.col_w
end

local function column_of(c, list)
  local body = {}
  local slot = drop_slot(c, list)
  for i = 1, #list do
    body[#body + 1] = W.guide("g:" .. c.id .. ":" .. i, slot == i)
    body[#body + 1] = card_of(list[i])
  end
  body[#body + 1] = W.guide("g:" .. c.id .. ":tail", slot == #list + 1)
  if #list == 0 then
    body[#body + 1] = W.empty_slot(slot ~= nil)
  end

  local lifting = S.drag ~= nil and S.drag.kind == "col" and S.drag.id == c.id
  return ui.col({
    id = c.id,
    w = width_of(c),
    no_shrink = true,
    -- The clamp in `actions.resize` is what the drag obeys; these are what the *layout* obeys,
    -- and they are not the same guard. A width can arrive from a peer whose theme differs, or
    -- from a doc edited by hand, and neither went past the drag.
    min_w = C.col_w_min,
    max_w = C.col_w_max,
    radius = 12,
    fill = C.panel,
    stroke = { 1, C.line_soft },
    opacity = lifting and 0.35 or 1.0,
    on_drop = function(e)
      local phase, x, y = e.phase, e.x, e.y
      update({ kind = "drop_col", col = c.id, phase = phase, x = x })
    end,
    -- header doubles as the column's drag handle
    ui.row({
      id = "colhead:" .. c.id,
      gap = 8,
      px = 12,
      py = 11,
      align_center = true,
      hover_fill = C.line_soft,
      on_drag = function(e)
        local phase, dx, dy, scale, ox, oy = e.phase, e.dx, e.dy, e.scale, e.origin_x, e.origin_y
        update({ kind = "drag", what = "col", id = c.id, phase = phase, x = ox, y = oy, scale = scale })
      end,
      ui.text({ c.name, no_wrap = true, color = C.text, font_size = 14 }),
      W.badge(#list),
      ui.col({ grow = true }),
      W.icon_button("del-col:" .. c.id, "x", function()
        update({ kind = "delete_col", id = c.id })
      end, C.danger),
    }),
    ui.col({ h = 1, w_full = true, fill = C.line_soft }),
    -- cards
    ui.col({
      id = "cards:" .. c.id,
      scroll_y = true,
      grow = true,
      px = 10,
      py = 7,
      body,
    }),
    composer(c),
  })
end

local function modal()
  local m = ui.state("col_modal", { name = "" })
  local create = function()
    update({ kind = "add_col" })
  end
  local panel = ui.col({
    id = "col-modal",
    w = 360,
    gap = 14,
    pad = 20,
    radius = 14,
    fill = C.panel,
    stroke = { 1, C.line },
    fade_in = 120,
    on_click = function() end,
    ui.text({ "New column", no_wrap = true, color = C.text, font_size = 16 }),
    ui.input({
      value = m.name,
      id = "col_name",
      autofocus = true,
      w_full = true,
      h = 36,
      px = 10,
      radius = 6,
      fill = C.sunken,
      stroke = { 1, C.line },
      color = C.text,
      font_size = 13,
      on_input = function(v)
        m.name = v
      end,
      on_enter = create,
      on_esc = function()
        update({ kind = "close_col" })
      end,
    }),
    ui.row({
      gap = 8,
      ui.col({ grow = true }),
      ui.button({
        id = "cancel-col",
        h = 34,
        px = 14,
        radius = 6,
        center = true,
        hover_fill = C.line_soft,
        press_scale = 0.97,
        ui.text({ "Cancel", no_wrap = true, color = C.muted, font_size = 13 }),
        on_click = function()
          update({ kind = "close_col" })
        end,
      }),
      ui.button({
        id = "create-col",
        h = 34,
        px = 16,
        radius = 6,
        center = true,
        fill = C.accent,
        hover_fill = C.accent_hi,
        press_scale = 0.97,
        ui.text({ "Create", no_wrap = true, color = "#ffffff", font_size = 13 }),
        on_click = create,
      }),
    }),
  })
  return ui.col({
    absolute = true,
    top = 0,
    left = 0,
    right = 0,
    bottom = 0,
    id = "col-modal-backdrop",
    center = true,
    fill = "rgba(1,4,9,0.72)",
    on_click = function()
      update({ kind = "close_col" })
    end,
    panel,
  })
end

-- ROOT
return function()
  -- Bind the mirror once per frame. `view()` repatches it before calling this, so these two
  -- are current for the whole frame — and reading them once keeps every count and index below
  -- consistent with each other.
  local columns, cards = board.columns, board.cards
  local grouped = model.by_column()
  local col_list = {}
  local flying = nil

  local col_slot = nil
  if S.drag and S.placement and S.placement.kind == "col" and S.drag.id ~= S.placement.id then
    for i = 1, #columns do
      if columns[i].id == S.placement.id then
        col_slot = S.placement.before and i or i + 1
        break
      end
    end
  end

  for i = 1, #columns do
    local c = columns[i]
    col_list[#col_list + 1] = W.guide_v("gv:" .. i, col_slot == i)
    col_list[#col_list + 1] = column_of(c, grouped[c.id])
    -- The grip is a sibling of the column, not a child of it: it has to sit in the gutter
    -- *between* two columns, and a child clipped to its parent's rounded panel would be a
    -- three-point target hanging off the edge of a corner.
    col_list[#col_list + 1] = W.grip("grip:" .. c.id, S.resize ~= nil and S.resize.id == c.id, function(phase, x, y, dx)
      update({ kind = "resize", id = c.id, phase = phase, x = x, dx = dx })
    end)
    if S.drag and S.drag.kind == "col" and S.drag.id == c.id then
      flying = ui.col({
        w = 300,
        radius = 12,
        pad = 12,
        fill = C.panel,
        stroke = { 1, C.accent },
        ui.text({ c.name, no_wrap = true, color = C.text, font_size = 14 }),
      })
    end
  end
  col_list[#col_list + 1] = W.guide_v("gv:tail", col_slot == #columns + 1)

  if S.drag and S.drag.kind == "card" then
    for i = 1, #cards do
      if cards[i].id == S.drag.id then
        flying = card_of(cards[i], true)
        break
      end
    end
  end

  -- The ghost sits at the root, outside every scroll clip, in viewport coords: `x`/`y` are
  -- `pos - grab`, i.e. where the dragged element's top-left belongs right now.
  local ghost = flying
    and ui.col({
      absolute = true,
      left = S.drag.x,
      top = S.drag.y,
      w = 300,
      scale = S.drag.scale or 1,
      opacity = 0.9,
      flying,
    })

  return ui.col({
    full = true,
    fill = C.bg,
    -- top bar
    ui.row({
      gap = 10,
      px = 24,
      py = 14,
      align_center = true,
      ui.text({ "Board", no_wrap = true, color = C.text, font_size = 18 }),
      W.badge(#cards),
      ui.col({ grow = true }),
      ui.button({
        id = "open-col",
        h = 32,
        px = 14,
        radius = 8,
        center = true,
        fill = C.accent,
        hover_fill = C.accent_hi,
        press_scale = 0.97,
        ui.text({ "+  Column", no_wrap = true, color = "#ffffff", font_size = 13 }),
        on_click = function()
          update({ kind = "open_col" })
        end,
      }),
    }),
    ui.col({ h = 1, w_full = true, fill = C.line_soft }),
    -- board
    ui.col({
      id = "board_zoom",
      zoomable = true,
      grow = true,
      w_full = true,
      fill = C.bg,
      ui.row({
        id = "board",
        h_full = true,
        stretch = true,
        px = 13,
        py = 20,
        col_list,
      }),
    }),
    ghost or false,
    S.col_modal and modal(),
  })
end

02 / WORKSPACES

The workspace itself is configurable.

The home screen is a Lua file in your encrypted store, not fixed chrome. Change the theme in the built-in editor, or let an agent edit the layout over the bridge. A broken edit is refused and the workspace keeps running; Reset restores the original.

return {
  bg = "#10141d",
  panel = "#1a2130",
  hover = "#232e42",
  line = "#34415a",
  text = "#edf2ff",
  muted = "#9baac4",
  accent = "#83adff",
  kinds = {
    app = "#83adff", doc = "#86dbb1",
    table = "#eac280", canvas = "#c5a1f2",
  },
}

03 / SYNC

Two people, one document.

Persistence and collaboration come with the document, not with the app. This chat is a model file that opens a document and calls insert. Each person has their own identity and node, and the edits merge on reconnect.

-- CHAT
--
-- Channels on the left, messages or search results on the right. The search box is the point
-- of this app: it is the reference for `index.lua` + `search.query` (docs/design/search.md).
local C = require("theme")
local M = require("model")
local S = M.S

local function channel_button(ch)
  local on = ch.id == S.channel
  return ui.button({
    id = "ch:" .. ch.id,
    px = 10,
    py = 6,
    radius = 6,
    fill = on and C.card_hi or C.panel,
    hover_fill = C.card,
    ui.text({ "# " .. ch.name, color = on and C.text or C.muted, font_size = 13, no_wrap = true }),
    on_click = function()
      S.channel = ch.id
      S.editing = nil
    end,
  })
end

local function small_button(id, label, color, on_click)
  return ui.button({
    id = id,
    px = 6,
    py = 2,
    radius = 4,
    hover_fill = C.card_hi,
    no_shrink = true,
    ui.text({ label, color = color, font_size = 11, no_wrap = true }),
    on_click = on_click,
  })
end

local function message_row(m)
  return ui.row({
    id = "msg:" .. m.id,
    gap = 8,
    px = 10,
    py = 6,
    radius = 6,
    fill = S.editing == m.id and C.card_hi or C.card,
    align_center = true,
    ui.text({ m.author, color = C.accent_hi, font_size = 12, no_wrap = true, no_shrink = true }),
    ui.col({ grow = true, ui.text({ m.text, id = "text:" .. m.id, color = C.text, font_size = 13 }) }),
    small_button("edit:" .. m.id, "edit", C.muted, function()
      S.editing = m.id
    end),
    small_button("del:" .. m.id, "delete", C.danger, function()
      M.delete(m.id)
    end),
  })
end

local function hit_row(h)
  local cid = M.channel_of(h.doc)
  return ui.button({
    id = "hit:" .. h.id,
    gap = 8,
    px = 10,
    py = 6,
    radius = 6,
    fill = C.card,
    hover_fill = C.card_hi,
    ui.text({ "#" .. (cid or h.doc), color = C.hit, font_size = 12, no_wrap = true, no_shrink = true }),
    ui.text({ h.snippet or "", id = "snippet:" .. h.id, color = C.text, font_size = 13 }),
    on_click = function()
      if cid then
        S.channel = cid
      end
    end,
  })
end

local function body(query)
  local rows = {}
  if query ~= "" then
    local hits = search.query(query, { limit = 50 })
    for _, h in ipairs(hits) do
      rows[#rows + 1] = hit_row(h)
    end
    if #hits == 0 then
      rows[1] = ui.text({ "no results", id = "no-results", color = C.muted, font_size = 13 })
    end
    return ui.col({ id = "results", grow = true, scroll_y = true, gap = 6, pad = 12, rows })
  end
  local msgs = M.channel(S.channel).messages or {}
  for _, m in ipairs(msgs) do
    rows[#rows + 1] = message_row(m)
  end
  return ui.col({ id = "messages", grow = true, scroll_y = true, gap = 6, pad = 12, rows })
end

return function()
  local find = ui.state("search", { q = "" })
  local draft = ui.state("composer", { text = "" })
  local send = function()
    M.send(draft.text)
    draft.text = ""
  end

  local sidebar = {}
  for _, ch in ipairs(M.chat.channels or {}) do
    sidebar[#sidebar + 1] = channel_button(ch)
  end

  return ui.row({
    id = "chat",
    grow = true,
    stretch = true,
    fill = C.bg,
    ui.col({ w = 160, no_shrink = true, gap = 4, pad = 10, fill = C.panel, sidebar }),
    ui.col({
      grow = true,
      stretch = true,
      ui.row({
        pad = 10,
        gap = 8,
        ui.input({
          id = "search",
          value = find.q,
          grow = true,
          on_input = function(v)
            find.q = v
          end,
          on_esc = function()
            find.q = ""
          end,
        }),
      }),
      body(find.q),
      ui.row({
        pad = 10,
        gap = 8,
        ui.input({
          id = "composer",
          value = draft.text,
          grow = true,
          on_input = function(v)
            draft.text = v
          end,
          on_enter = send,
        }),
        ui.button({
          id = "send",
          px = 14,
          radius = 6,
          center = true,
          fill = C.accent,
          hover_fill = C.accent_hi,
          ui.text({ S.editing and "save" or "send", color = "#ffffff", font_size = 13, no_wrap = true }),
          on_click = send,
        }),
      }),
    }),
  })
end

04 / AGENTS

An agent writes and edits the app while it runs.

Agents drive the shell over a local bridge: read a source file, send exact edits, and the running app reloads. The agent changes code, not pixels.

-- Launch checklist. Tasks live in the "todo" document, so every reload keeps them.
local t = doc:open("todo")

if not t.tasks then
  t:set({ "tasks" }, doc.list({
    doc.map({ id = "t1", text = "Write the launch post", done = false, prio = "high" }),
    doc.map({ id = "t2", text = "Record the demo reel", done = true, prio = "high" }),
    doc.map({ id = "t3", text = "Fix the pricing page copy", done = false, prio = "med" }),
    doc.map({ id = "t4", text = "Reply to early testers", done = false, prio = "med" }),
    doc.map({ id = "t5", text = "Rotate the node keys", done = true, prio = "high" }),
    doc.map({ id = "t6", text = "Tidy the changelog", done = false, prio = "low" }),
  }))
end

local C = {
  bg = "#0d1117",
  card = "#1c2128",
  card_hi = "#22282f",
  sunken = "#0d1117",
  line = "#30363d",
  text = "#e6edf3",
  muted = "#8b949e",
  accent = "#2f81f7",
  accent_hi = "#4493f8",
}

local function row(task)
  return ui.button({
    id = "task:" .. task.id,
    w_full = true, h = 38, px = 14, gap = 12, radius = 10, align_center = true,
    fill = C.card, hover_fill = C.card_hi, press_scale = 0.99,
    ui.col({ w = 18, h = 18, radius = 6, fill = task.done and C.accent or C.sunken, stroke = { 1, task.done and C.accent or C.line } }),
    ui.text({ task.text, color = task.done and C.muted or C.text, font_size = 15, no_wrap = true }),
    on_click = function()
      t:set({ "tasks", task.id, "done" }, not task.done)
    end,
  })
end

return function()
  local rows = {}
  local done = 0
  for _, task in ipairs(t.tasks) do
    if task.done then done = done + 1 end
    table.insert(rows, row(task))
  end
  return ui.col({
    id = "todo",
    full = true, align_center = true, fill = C.bg, py = 22,
    ui.col({
      w = 640, gap = 14,
      ui.row({
        align_center = true,
        ui.col({
          gap = 4,
          ui.text({ "Launch checklist", color = C.text, font_size = 26, no_wrap = true }),
          ui.text({ done .. " of " .. #t.tasks .. " done", id = "summary", color = C.muted, font_size = 14, no_wrap = true }),
        }),
      }),
      ui.col({ gap = 6, rows }),
    }),
  })
end

05 / FILES

Assets are edited in place.

Workspace files open in apps that were themselves written in Lua, inside the same shell.

local C = require("theme")
local M = require("model")
local Stage = require("stage")
local Inspector = require("inspector")
local Handles = require("handles")

local selected = nil -- the selected part's id
local built = { key = nil, frame = nil } -- the canvas visual; rebuilt on a new selection or edit
local play = nil -- { name, drawing, clip } while a clip plays on the canvas; nil is edit mode
local seen_at = 1 -- the viewport's zoom, so selection marks keep their screen size
-- While a point dot is held: its shape `i`, the points moving (`{ j, k, from }`, the held one
-- first), and `d`, how far they have moved, in drawing units. While a part is held: `ids`, the
-- set of parts moving with it, and `d`.
local drag = nil

-- The canvas draws the asset scaled up by building a scaled copy, rather than scaling the
-- element: hits keep reporting part ids, and canvas units are drawing units times the zoom.
local function scaled_path(path, z)
  local out = {}
  for i, cmd in ipairs(path) do
    local c = { cmd[1] }
    for j = 2, #cmd do
      c[j] = cmd[j] * z
    end
    out[i] = c
  end
  return out
end

-- A colour stays as it is; a gradient's points and radii are in drawing space, so they scale
-- too. Angles and stops don't.
local function scaled_fill(fill, z)
  if type(fill) ~= "table" then
    return fill
  end
  local function pt(p)
    return p and { p[1] * z, p[2] * z }
  end
  local kind, g = next(fill)
  local out = { stops = g.stops, extend = g.extend }
  if kind == "linear" then
    out.from, out.to = pt(g.from), pt(g.to)
  elseif kind == "radial" then
    out.center, out.focus = pt(g.center), pt(g.focus)
    out.radius, out.focus_radius = g.radius * z, g.focus_radius and g.focus_radius * z
  else -- sweep
    out.center, out.from_angle, out.to_angle = pt(g.center), g.from_angle, g.to_angle
  end
  return { [kind] = out }
end

-- A lone length may be a var, `"@line"`: it stays a name here, and the vars scale instead.
local function len(v, z)
  return type(v) == "number" and v * z or v
end

local function scaled_shape(s, z)
  if s.shadow then
    local sh, r = s.shadow, s.shadow.rect
    local rect = { r[1] * z, r[2] * z, r[3] * z, r[4] * z }
    local radius = len(sh.radius or 0, z)
    return { shadow = { rect = rect, radius = radius, blur = len(sh.blur, z), color = sh.color } }
  end
  local stroke = s.stroke and { len(s.stroke[1], z), s.stroke[2] }
  return { path = scaled_path(s.path, z), fill = scaled_fill(s.fill, z), stroke = stroke }
end

-- The asset at canvas scale, with the `outline` part's outlines drawn again in the accent colour
-- and its path points on top as dots.
local function canvas_drawing(parts, z, outline)
  local out, held = {}, nil
  for i, p in ipairs(parts) do
    local shapes = {}
    for _, s in ipairs(p.shapes or {}) do
      table.insert(shapes, scaled_shape(s, z))
    end
    if p.id == outline then
      for _, s in ipairs(p.shapes or {}) do
        if s.path then -- a shadow has no outline to trace
          table.insert(shapes, { path = scaled_path(s.path, z), stroke = { 3 / seen_at, C.accent } })
        end
      end
    end
    out[i] = {
      id = p.id,
      parent = p.parent,
      pivot = { p.pivot[1] * z, p.pivot[2] * z },
      shapes = #shapes > 0 and shapes or nil,
    }
    if p.id == outline then
      held = p
    end
  end
  for _, h in ipairs(held and Handles.parts(held, z, seen_at) or {}) do
    table.insert(out, h)
  end
  local size = M.asset.size
  local vars = {} -- a number var is always a length
  for name, v in pairs(M.asset.vars or {}) do
    vars[name] = len(v, z)
  end
  return gfx.drawing({ size = { size[1] * z, size[2] * z }, vars = vars, parts = out })
end

local DIVS = { 1, 2, 4, 10, 20 } -- snap steps as divisions of a unit: 1/20 stays exact, 0.05 not

-- `v` on the finest step still a few pixels apart on screen: whole units at a glance, twentieths
-- up close.
local function snap(v)
  local div = 1
  for _, d in ipairs(DIVS) do
    if C.zoom * seen_at / d >= 4 then
      div = d
    end
  end
  return math.floor(v * div + 0.5) / div
end

-- `parts` with the held points where the drag has them, copying only what that changes.
local function dragged(parts)
  local out = table.clone(parts)
  for n, p in ipairs(parts) do
    if drag.ids then
      out[n] = drag.ids[p.id] and M.moved(p, drag.d) or p
    elseif p.id == selected then
      local part, shape = table.clone(p), table.clone(p.shapes[drag.i])
      shape.path = table.clone(shape.path)
      for _, m in ipairs(drag.moves) do -- an end and its control can share a command
        shape.path[m.j] = table.clone(shape.path[m.j])
      end
      for _, m in ipairs(drag.moves) do
        shape.path[m.j][2 * m.k] = m.from[1] + drag.d[1]
        shape.path[m.j][2 * m.k + 1] = m.from[2] + drag.d[2]
      end
      part.shapes = table.clone(p.shapes)
      part.shapes[drag.i] = shape
      out[n] = part
    end
  end
  return out
end

local function key(...)
  return { "views", M.view, "parts", ... }
end

-- Writes the held parts where the drag left them, and the sockets riding them. Paths go whole.
local function write_moved(parts)
  local d = drag.d
  for _, p in ipairs(parts) do
    if drag.ids[p.id] then
      local m = M.moved(p, d)
      M.asset:set(key(p.id, "pivot"), M.tag(m.pivot))
      for i, s in ipairs(m.shapes) do
        if s.path then
          M.asset:set(key(p.id, "shapes", i, "path"), M.tag(s.path))
        end
        if type(s.fill) == "table" then
          M.asset:set(key(p.id, "shapes", i, "fill"), M.tag(s.fill))
        end
        if s.shadow then
          M.asset:set(key(p.id, "shapes", i, "shadow", "rect"), M.tag(s.shadow.rect))
        end
      end
    end
  end
  for name, s in pairs(M.asset.sockets or {}) do
    if drag.ids[s.part] then
      M.asset:set({ "sockets", name, "at" }, M.tag({ s.at[1] + d[1], s.at[2] + d[2] }))
    end
  end
end

local function visual(parts)
  local live = drag and drag.d[1] .. "," .. drag.d[2] or ""
  local key = table.concat({ M.name, selected or "", Inspector.rev, seen_at, live }, ":")
  if built.key ~= key then
    built.key = key
    built.frame = canvas_drawing(drag and dragged(parts) or parts, C.zoom, selected):pose({})
  end
  return built.frame
end

-- A clip at canvas scale: moves are in drawing units, so x and y scale; turns and scales don't.
local function scaled_clip(clip, z)
  local tracks = {}
  for part, props in pairs(clip.tracks) do
    tracks[part] = {}
    for prop, keys in pairs(props) do
      local k = (prop == "x" or prop == "y") and z or 1
      local out = {}
      for i, key in ipairs(keys) do
        out[i] = { key[1], key[2] * k, key[3] }
      end
      tracks[part][prop] = out
    end
  end
  return { length = clip.length, loop = clip.loop, tracks = tracks }
end

-- Every press makes a new clip handle, which is what restarts a clip that is already playing.
local function start(name)
  play = {
    name = name,
    drawing = canvas_drawing(M.parts(), C.zoom, nil),
    clip = gfx.clip(scaled_clip(M.asset.clips[name], C.zoom)),
  }
end

local function chip(id, label, on, on_click)
  return ui.button({
    id = id,
    px = 10,
    py = 6,
    radius = 6,
    fill = on and C.row_selected or C.panel,
    hover_fill = on and C.row_selected or C.row_hover,
    on_click = on_click,
    ui.text({ label, color = on and C.accent or C.text, font_size = 13, no_wrap = true }),
  })
end

local function asset_tab(name)
  return chip("asset:" .. name, name, name == M.name, function()
    selected, play = nil, nil
    M.open(name)
  end)
end

-- "edit" shows the rest pose and selects parts; a clip name plays that clip in a world.
local function clip_bar()
  local names = {}
  for name in pairs(M.asset.clips or {}) do
    table.insert(names, name)
  end
  table.sort(names)
  local chips = { chip("mode:edit", "edit", play == nil, function()
    play = nil
  end) }
  for _, name in ipairs(names) do
    local on = play ~= nil and play.name == name
    table.insert(chips, chip("clip:" .. name, name, on, function()
      start(name)
    end))
  end
  return ui.row({ gap = 6, wrap = true, chips })
end

-- Edit mode is a frame, so a click names the part; play mode is a world, whose clicks would
-- only name the whole asset — hence two modes rather than one.
local function canvas(parts)
  if play then
    local size = M.asset.size
    return ui.world({
      id = "stage",
      width = size[1] * C.zoom,
      height = size[2] * C.zoom,
      { id = M.name, pos = { 0, 0 }, drawing = play.drawing, clip = play.clip },
    })
  end
  return ui.frame({
    id = "canvas",
    visual = visual(parts),
    -- A click on no part clears the selection; one on a point dot keeps it.
    on_click = function(e)
      if not (e.shape and e.shape:sub(1, 3) == "pt:") then
        selected = e.shape
      end
    end,
    -- A held dot or part follows the pointer on the canvas alone; letting go writes it. A
    -- part takes the parts under it along. A drag from empty canvas pans.
    drag_on_shapes = true,
    on_drag = function(e)
      if e.phase == "start" then
        if e.shape and e.shape:sub(1, 3) ~= "pt:" then
          selected = e.shape
          drag = { ids = M.family(parts, e.shape), d = { 0, 0 }, x0 = e.x, y0 = e.y }
        end
        local i, j, k = (e.shape or ""):match("^pt:(%d+):(%d+):(%d+)$")
        for _, p in ipairs(i and parts or {}) do
          if p.id == selected then
            i, j, k = tonumber(i), tonumber(j), tonumber(k)
            local path = p.shapes[i].path
            local moves = { { j = j, k = k } }
            for _, r in ipairs(Handles.riders(path, j, k)) do
              table.insert(moves, r)
            end
            for _, m in ipairs(moves) do
              m.from = { path[m.j][2 * m.k], path[m.j][2 * m.k + 1] }
            end
            drag = { i = i, moves = moves, d = { 0, 0 }, x0 = e.x, y0 = e.y }
          end
        end
      elseif drag then
        -- A held point snaps, and the rest move by what it moved; a part moves by snapped steps.
        local dx, dy = (e.x - drag.x0) / C.zoom, (e.y - drag.y0) / C.zoom
        if drag.ids then
          drag.d = { snap(dx), snap(dy) }
        else
          local from = drag.moves[1].from
          drag.d = { snap(from[1] + dx) - from[1], snap(from[2] + dy) - from[2] }
        end
        if e.phase == "end" then
          if drag.ids then
            write_moved(parts)
          end
          for _, m in ipairs(drag.moves or {}) do
            local cmd = { "views", M.view, "parts", selected, "shapes", drag.i, "path", m.j }
            local px, py = table.clone(cmd), table.clone(cmd)
            table.insert(px, 2 * m.k)
            table.insert(py, 2 * m.k + 1)
            M.asset:set(px, m.from[1] + drag.d[1])
            M.asset:set(py, m.from[2] + drag.d[2])
          end
          drag = nil
          Inspector.rev += 1 -- the canvas rebuilds from the mirror, which has it by then
        end
      end
    end,
  })
end

local function part_row(p)
  local on = p.id == selected
  return ui.button({
    id = "part:" .. p.id,
    px = 12,
    py = 8,
    gap = 8,
    align_center = true,
    radius = 6,
    fill = on and C.row_selected or C.panel,
    hover_fill = on and C.row_selected or C.row_hover,
    on_click = function()
      selected = p.id
    end,
    ui.text({ p.id, color = on and C.accent or C.text, font_size = 13, no_wrap = true }),
    p.parent and ui.text({ "in " .. p.parent, color = C.muted, font_size = 11, no_wrap = true })
      or false,
  })
end

local function entity_row(r)
  return ui.row({
    px = 12,
    py = 8,
    gap = 8,
    align_center = true,
    ui.text({ r.id, color = C.text, font_size = 13, no_wrap = true }),
    ui.text({ r.note, color = C.muted, font_size = 11, no_wrap = true }),
  })
end

return function()
  local stage = M.scenes[M.name]
  local parts = not stage and M.parts() or nil
  local tabs, rows = {}, {}
  for _, name in ipairs(M.names) do
    table.insert(tabs, asset_tab(name))
  end
  for name in pairs(M.scenes) do
    table.insert(tabs, asset_tab(name))
  end
  local picked = nil
  for _, p in ipairs(parts or {}) do
    table.insert(rows, part_row(p))
    picked = p.id == selected and p or picked
  end
  for _, r in ipairs(stage and Stage.rows() or {}) do
    table.insert(rows, entity_row(r))
  end
  local title = M.name .. " · " .. (stage and "stage" or M.view)
  local listing = stage and "entities, back to front" or "parts, back to front"
  local body = stage and Stage.view()
    or parts and canvas(parts)
    or ui.text({ "opening the asset…", color = C.muted, no_wrap = true })
  return ui.row({
    full = true,
    fill = C.bg,
    ui.col({
      grow = true,
      fill = C.canvas,
      pad = 12,
      gap = 12,
      parts and clip_bar() or stage and Stage.bar(chip) or false,
      -- Ctrl+wheel zooms around the pointer and a drag on empty space pans; clicks still
      -- report canvas units, zoom undone.
      ui.col({
        id = "viewport",
        grow = true,
        w_full = true,
        center = true,
        zoomable = true,
        zoom_max = 20, -- on top of the canvas's own 2.5×: 50× the asset, for fingers and eyelashes
        on_zoom = function(e)
          seen_at = e.scale
        end,
        body,
      }),
    }),
    ui.col({
      w = C.panel_w,
      no_shrink = true,
      fill = C.panel,
      pad = 12,
      gap = 4,
      ui.row({ gap = 6, mb = 8, wrap = true, tabs }),
      ui.text({ title, color = C.text, font_size = 15, no_wrap = true }),
      ui.text({ listing, color = C.muted, font_size = 11, no_wrap = true }),
      rows,
      parts and Inspector.vars() or false,
      Inspector.view(picked),
    }),
  })
end

06 / PHYSICS

A 2D physics engine, driven from Lua.

Gravity, wells, forces, joints and a grabbing hand are properties in the app description. This took a Rust engine change plus a Lua surface; the scene itself is one app.

-- Siege: a side-on gravity world with a chain of causes. A slingshot fires a stone (dotted arc
-- previews the shot under the world's gravity) at a castle of stone, wood and glass; the collapse
-- topples a run of dominoes, the last pushes a marble down a ramp into a bucket that lights up.
-- Pigs score when they fall to the ground. Also a toy: a swinging wrecking ball on a chain
-- (rope joints), a seesaw (pin joint), bomb (`blast`), moon/flip gravity, a gravity well, and any
-- loose body can be dragged and thrown (`grab` / `release`). Nothing runs per frame: Lua
-- describes bodies and hears zones; Rapier does the moving. Lua cannot read a body's position, so
-- nothing here depends on one.
local W, H, FY = 960, 520, 480 -- the world, and the floor's top
local C = {
  bg = "#0b0d16", panel = "#1a1e2e", text = "#e8ebf5", muted = "#8f96b0", accent = "#ffd166",
  button = "#2a3050", button_hover = "#3a4270", lit = "#ffe066",
}
local G_NORMAL, G_MOON, G_FLIP = { 0, 900 }, { 0, 280 }, { 0, -900 }

-- ── drawing ───────────────────────────────────────────────────────────────────────────────────
local function rectp(x, y, w, h)
  return { { "move", x, y }, { "line", x + w, y }, { "line", x + w, y + h }, { "line", x, y + h }, { "close" } }
end
local function disc(cx, cy, r)
  local k = r * 0.5523
  return { { "move", cx, cy - r }, { "cubic", cx + k, cy - r, cx + r, cy - k, cx + r, cy },
    { "cubic", cx + r, cy + k, cx + k, cy + r, cx, cy + r }, { "cubic", cx - k, cy + r, cx - r, cy + k, cx - r, cy },
    { "cubic", cx - r, cy - k, cx - k, cy - r, cx, cy - r }, { "close" } }
end
local function ellipse(cx, cy, rx, ry)
  local kx, ky = rx * 0.5523, ry * 0.5523
  return { { "move", cx, cy - ry }, { "cubic", cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy },
    { "cubic", cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry }, { "cubic", cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy },
    { "cubic", cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry }, { "close" } }
end
local function drawing(w, h, shapes)
  return gfx.drawing({ size = { w, h }, parts = { { id = "it", pivot = { w / 2, h / 2 }, shapes = shapes } } })
end
local cache = {}
local function memo(key, make)
  if not cache[key] then cache[key] = make() end
  return cache[key]
end

-- Interpolates two #rrggbb colours: stacked bands make a gradient, since a drawing's fill is solid.
local function mix(a, b, t)
  local function ch(s, i) return tonumber(s:sub(i, i + 1), 16) end
  local out = "#"
  for i = 2, 6, 2 do out = out .. string.format("%02x", math.floor(ch(a, i) + (ch(b, i) - ch(a, i)) * t + 0.5)) end
  return out
end
local function bands(x, y, w, h, stops, n)
  local shapes = {}
  for i = 0, n - 1 do
    local t = i / (n - 1) * (#stops - 1)
    local s = math.min(#stops - 2, math.floor(t))
    shapes[#shapes + 1] = { path = rectp(x, y + h * i / n, w, h / n + 1), fill = mix(stops[s + 1], stops[s + 2], t - s) }
  end
  return shapes
end

local function sky()
  return memo("sky", function()
    local s = bands(0, 0, W, FY, { "#0e1432", "#2c2457", "#7a3a6e", "#e0667a", "#ffb56b" }, 30)
    for i = 1, 40 do -- stars, thinning toward the horizon
      local x, y = (i * 137) % W, (i * 71) % 200
      s[#s + 1] = { path = disc(x, y, 0.8 + (i % 3) * 0.4), fill = "#ffffff" .. (i % 2 == 0 and "cc" or "77") }
    end
    s[#s + 1] = { path = disc(780, 90, 34), fill = "#fff1c9" }
    s[#s + 1] = { path = disc(792, 84, 30), fill = "#ffe29a44" }
    -- far hills
    s[#s + 1] = { path = { { "move", 0, FY }, { "line", 0, 380 }, { "quad", 140, 330, 260, 372 }, { "quad", 380, 410, 520, 360 },
      { "quad", 660, 310, 800, 372 }, { "quad", 900, 410, W, 380 }, { "line", W, FY }, { "close" } }, fill = "#3b2a5acc" }
    s[#s + 1] = { path = { { "move", 0, FY }, { "line", 0, 430 }, { "quad", 200, 390, 360, 440 }, { "quad", 560, 480, 700, 430 },
      { "quad", 840, 390, W, 440 }, { "line", W, FY }, { "close" } }, fill = "#2a1f45" }
    return drawing(W, FY, s)
  end)
end

local function ground()
  return memo("ground", function()
    local s = bands(0, 0, W, H - FY, { "#4a7a3a", "#2f5a2c", "#2a2018", "#18120d" }, 14)
    s[#s + 1] = { path = rectp(0, 0, W, 3), fill = "#8fd45a" }
    return drawing(W, H - FY, s)
  end)
end

-- A lit block: body, bright top edge, dark bottom edge, outline.
local function slab(w, h, base, hi, lo, edge)
  return memo(table.concat({ "slab", w, h, base }, ":"), function()
    return drawing(w, h, {
      { path = rectp(0, 0, w, h), fill = base }, { path = rectp(0, 0, w, math.min(3, h / 3)), fill = hi },
      { path = rectp(0, h - math.min(3, h / 3), w, math.min(3, h / 3)), fill = lo },
      { path = rectp(0.5, 0.5, w - 1, h - 1), stroke = { 1, edge or "#00000066" } },
    })
  end)
end
local function ball(r, base, hi, lo, mark)
  return memo(table.concat({ "ball", r, base }, ":"), function()
    local s = { { path = disc(r, r, r), fill = lo }, { path = disc(r - 1, r - 1, r - 2), fill = base },
      { path = disc(r * 0.7, r * 0.65, r * 0.38), fill = hi } }
    if mark then s[#s + 1] = { path = rectp(r - 1.5, 2, 3, r * 0.6), fill = mark } end
    s[#s + 1] = { path = disc(r, r, r - 0.5), stroke = { 1, "#00000077" } }
    return drawing(2 * r, 2 * r, s)
  end)
end
local function pig(r)
  return memo("pig" .. r, function()
    local e = r * 0.42
    return drawing(2 * r, 2 * r, {
      { path = disc(r, r, r), fill = "#4f9d3a" }, { path = disc(r - 1, r - 1, r - 2), fill = "#7bd357" },
      { path = disc(r, r * 1.2, r * 0.42), fill = "#5aa840" }, -- snout
      { path = disc(r - 3, r * 1.2, 1.6), fill = "#2d5a1f" }, { path = disc(r + 3, r * 1.2, 1.6), fill = "#2d5a1f" },
      { path = disc(r - e, r * 0.75, 3.2), fill = "#ffffff" }, { path = disc(r + e, r * 0.75, 3.2), fill = "#ffffff" },
      { path = disc(r - e, r * 0.78, 1.5), fill = "#102010" }, { path = disc(r + e, r * 0.78, 1.5), fill = "#102010" },
      { path = disc(r, r, r - 0.5), stroke = { 1, "#1f4a14" } },
    })
  end)
end
local function invisible(w, h)
  return memo("inv" .. w .. "x" .. h, function() return drawing(w, h, { { path = rectp(0, 0, w, h), fill = "#00000000" } }) end)
end

local KIND = { -- how each block is drawn and made
  stone = { base = "#8d93a6", hi = "#c5cad9", lo = "#5b6075", mat = { bounce = 0.05, friction = 0.02, grip = 0.9, density = 3 } },
  wood = { base = "#c58b4e", hi = "#e8b27a", lo = "#8a5a2c", mat = { bounce = 0.3, friction = 0.02, grip = 0.7, density = 0.7 } },
  glass = { base = "#8fe3f8aa", hi = "#ffffffcc", lo = "#4aa7c4aa", edge = "#e8fbffcc",
    mat = { bounce = 0.5, friction = 0.02, grip = 0.4, density = 0.8 } },
  domino = { base = "#e0505a", hi = "#ff9aa0", lo = "#9a2a35", mat = { bounce = 0.2, friction = 0.02, grip = 0.9, density = 1.5 } },
}

-- ── the scene as data ─────────────────────────────────────────────────────────────────────────
local gen = 0 -- bumps on reset: fresh ids respawn everything where it began
local function N(name) return gen == 0 and name or (name .. "~" .. gen) end

local function castle()
  local b = {}
  local function add(name, kind, x, y, w, h) b[#b + 1] = { name = name, kind = kind, x = x, y = y, w = w, h = h } end
  for i = 0, 4 do -- two stone pillars of five
    add("sl" .. i, "stone", 440, FY - 28 * (i + 1), 28, 28)
    add("sr" .. i, "stone", 516, FY - 28 * (i + 1), 28, 28)
  end
  add("beam1", "wood", 432, 326, 120, 14)
  add("wl", "wood", 448, 282, 16, 44)
  add("wr", "wood", 524, 282, 16, 44)
  add("beam2", "wood", 432, 270, 120, 12)
  add("g1", "glass", 440, 254, 16, 16)
  add("g2", "glass", 536, 254, 16, 16)
  add("g3", "glass", 466, 300, 10, 26)
  add("g4", "glass", 512, 300, 10, 26)
  -- dominoes grow taller along the run, so each hits harder than the last; the last is a hammer
  local x = 600
  for i, h in ipairs({ 30, 34, 38, 43, 48, 54, 60 }) do
    add("d" .. (i - 1), "domino", x, 440 - h, 8, h)
    x = x + 8 + 0.45 * h
  end
  add("crate1", "wood", 478, FY - 24, 24, 24)
  add("seesaw_c1", "wood", 196, FY - 58, 22, 24)
  return b
end
local blocks = castle()

local state -- everything the game remembers; a reset replaces it
local function fresh()
  return { score = 0, pigs = { pig_roof = true, pig_mid = true }, goal = false, shots = {}, nshot = 0, aim = nil,
    g = G_NORMAL, gname = "normal", well = false, status = "Pull the stone back and let go", pending = nil }
end
state = fresh()

local SLING = { 96, 392 } -- where the stone rests; pulled back, up to PULL away
local PULL, POWER, PIPS, BAND = 72, 11, 16, 6

local grabbable = {} -- ids a drag may take; rebuilt each view

local function loose(id, x, y, drawing_, collider, mat, extra)
  grabbable[id] = true
  local e = { id = id, pos = { x, y }, drawing = drawing_, collider = collider, loose = mat }
  for k, v in pairs(extra or {}) do e[k] = v end
  return e
end

-- ── entities ──────────────────────────────────────────────────────────────────────────────────
local function build()
  grabbable = {}
  local out = {}
  local function add(e) out[#out + 1] = e end
  add({ id = "sky", pos = { 0, 0 }, drawing = sky() })
  -- contact shadows: soft dark ellipses on the ground under the structures
  add({ id = "shadow", pos = { 405, FY - 7 }, drawing = memo("shadow", function()
    return drawing(190, 14, { { path = ellipse(95, 7, 95, 6), fill = "#00000030" }, { path = ellipse(95, 7, 70, 4.5), fill = "#00000040" } })
  end) })
  add({ id = "shadow2", pos = { 580, FY - 41 }, drawing = memo("shadow2", function()
    return drawing(250, 5, { { path = ellipse(125, 2.5, 125, 2.5), fill = "#00000040" } })
  end) })
  -- floor, plinth, shelf and ramp: fixed colliders
  add({ id = "floor", pos = { 0, FY }, drawing = ground(), collider = { rect = { W, H - FY } } })
  add({ id = "wall:t", pos = { 0, -20 }, drawing = invisible(W, 20), collider = { rect = { W, 20 } } }) -- flipped gravity piles up here
  add({ id = "wall:l", pos = { -20, 0 }, drawing = invisible(20, H), collider = { rect = { 20, H } } })
  add({ id = "wall:r", pos = { W, 0 }, drawing = invisible(20, H), collider = { rect = { 20, H } } })
  add({ id = "plinth", pos = { 82, 424 }, drawing = slab(28, 56, "#6b5b4a", "#9a8670", "#3f3428"), collider = { rect = { 28, 56 } } })
  add({ id = "shelf", pos = { 580, 440 }, drawing = slab(240, 40, "#5b6075", "#8d93a6", "#3a3e50"), collider = { rect = { 240, 40 } } })
  local RS, RW = 18, 4.5
  for i = 0, RS - 1 do -- a slope in fine steps, the box colliders being axis-aligned
    local top = 440 + (i + 1) * (FY - 440 - 2) / RS
    add({ id = "ramp" .. i, pos = { 820 + RW * i, top }, drawing = slab(RW + 0.5, FY - top, "#5b6075", "#8d93a6", "#3a3e50", "#5b6075"),
      collider = { rect = { RW, FY - top } } })
  end
  add({ id = "bucket:wall", pos = { 948, 420 }, drawing = slab(10, 60, "#7a6a55", "#a89478", "#4a3f30"), collider = { rect = { 10, 60 } } })
  -- the bucket's inside lights up when the marble lands
  add({ id = "bucket", pos = { 900, 452 }, drawing = memo("bucket" .. tostring(state.goal), function()
    local c = state.goal and C.lit or "#4a4033"
    return drawing(48, 28, { { path = rectp(0, 0, 48, 28), fill = c .. (state.goal and "" or "55") },
      { path = rectp(0, 24, 48, 4), fill = state.goal and "#fff6b0" or "#2a2118" } })
  end), sensor = { rect = { 48, 28 } } })
  if state.goal then
    add({ id = "glow", pos = { 870, 400 }, drawing = memo("glow", function()
      return drawing(110, 90, { { path = ellipse(55, 45, 55, 45), fill = "#ffe06622" }, { path = ellipse(55, 50, 38, 32), fill = "#ffe06633" },
        { path = ellipse(55, 56, 22, 18), fill = "#fff6b055" } })
    end) })
  end
  add({ id = "goal", pos = { 906, 452 }, drawing = invisible(40, 28), sensor = { rect = { 40, 28 } } })
  -- the kill zone along the ground where the castle stands: a pig that reaches it is down
  add({ id = "killzone", pos = { 380, FY - 18 }, drawing = invisible(240, 18), sensor = { rect = { 240, 18 } } })

  if state.flash then add({ id = "flash", pos = { 0, 0 }, drawing = memo("flash", function() return drawing(W, H, { { path = rectp(0, 0, W, H), fill = "#ffffff1c" } }) end) }) end
  -- the sling: two posts and the stone in it (not a body: it is moved by hand with `set`)
  add({ id = "sling", pos = { 84, 372 }, drawing = memo("sling", function()
    return drawing(30, 52, { { path = rectp(10, 18, 8, 34), fill = "#5a3b22" }, { path = rectp(2, 6, 7, 20), fill = "#7a5230" },
      { path = rectp(19, 6, 7, 20), fill = "#7a5230" } })
  end) })
  for i = 1, BAND do add({ id = "band" .. i, pos = { -40, -40 }, drawing = memo("bandpip", function() return drawing(5, 5, { { path = disc(2.5, 2.5, 2.5), fill = "#d9b07a" } }) end) }) end
  for i = 1, PIPS do add({ id = "arc" .. i, pos = { -40, -40 }, drawing = memo("arcpip" .. (i % 3), function()
    return drawing(6, 6, { { path = disc(3, 3, 3 - (i % 3) * 0.4), fill = "#ffffffcc" } })
  end) }) end
  add({ id = "ammo", pos = { SLING[1] - 11, SLING[2] - 11 }, drawing = ball(11, "#9aa0b3", "#d9deee", "#5b6075") })

  -- the castle and dominoes: loose bodies of mixed material
  for _, k in ipairs(blocks) do
    local kd = KIND[k.kind]
    add(loose(N(k.name), k.x, k.y, slab(k.w, k.h, kd.base, kd.hi, kd.lo, kd.edge), { rect = { k.w, k.h } }, kd.mat))
  end
  -- pigs: scored when they fall
  local PIGM = { bounce = 0.35, friction = 0.02, grip = 0.5, density = 0.9 }
  if state.pigs.pig_roof then add(loose(N("pig_roof"), 478, 256 - 14, pig(14), { circle = 14, at = { 14, 14 } }, PIGM)) end
  if state.pigs.pig_mid then add(loose(N("pig_mid"), 482, 326 - 24, pig(12), { circle = 12, at = { 12, 12 } }, PIGM)) end
  -- the marble waiting on a post at the shelf's edge, where the hammer will strike it
  add({ id = "post", pos = { 788, 400 }, drawing = slab(12, 40, "#7a6a55", "#a89478", "#4a3f30"), collider = { rect = { 12, 40 } } })
  add(loose(N("marble"), 784, 380, ball(10, "#6fa8ff", "#d6e6ff", "#2a4a8a"), { circle = 10, at = { 10, 10 } },
    { bounce = 0.3, friction = 0.01, grip = 0.6, density = 0.5 }))
  -- the pendulum: a chain of small links on rope joints, ending in an iron ball
  local ax, ay, LINKS = 390, 24, 9
  for i = 0, LINKS - 1 do
    add(loose(N("link" .. i), ax - 7, ay + 26 * (i + 1) - 7, ball(7, "#7d8497", "#dfe3f0", "#3d4152"), { circle = 7, at = { 7, 7 } },
      { bounce = 0.1, friction = 0.05, grip = 0.1, density = 3 }))
  end
  add(loose(N("bob"), ax - 22, ay + 26 * (LINKS + 1) - 22, ball(22, "#4a4f63", "#9aa0b8", "#1f2230"), { circle = 22, at = { 22, 22 } },
    { bounce = 0.2, friction = 0.03, grip = 0.3, density = 4 }))
  add({ id = "anchor", pos = { ax - 8, ay - 8 }, drawing = ball(8, "#d9b07a", "#fff3d0", "#7a5230") })
  -- the seesaw: a plank on a pin joint, centred on a fulcrum
  add({ id = "fulcrum", pos = { 250, FY - 22 }, drawing = memo("fulcrum", function()
    return drawing(40, 22, { { path = { { "move", 20, 0 }, { "line", 40, 22 }, { "line", 0, 22 }, { "close" } }, fill = "#7a6a55" } })
  end) })
  add(loose(N("seesaw"), 190, FY - 22 - 12, slab(160, 10, "#c58b4e", "#e8b27a", "#8a5a2c"), { rect = { 160, 10 } },
    { bounce = 0.1, friction = 0.02, grip = 0.8, density = 1 }))
  -- the shots fired so far
  for _, s in ipairs(state.shots) do
    add(loose(s, state[s].x - 11, state[s].y - 11, ball(11, "#9aa0b3", "#d9deee", "#5b6075"), { circle = 11, at = { 11, 11 } },
      { bounce = 0.1, friction = 0.02, grip = 0.6, density = 14 }))
  end
  return out
end

local function joints()
  local j, LINKS = {}, 9
  local function rope(id, a, b, b_at, length)
    j[#j + 1] = { id = id, kind = "rope", a = a, a_at = { 7, 7 }, b = b, b_at = b_at, length = length }
  end
  rope("chain0", N("link0"), nil, { 390, 24 }, 26)
  for i = 1, LINKS - 1 do rope("chain" .. i, N("link" .. i), N("link" .. (i - 1)), { 7, 7 }, 26) end
  j[#j + 1] = { id = "chainbob", kind = "rope", a = N("bob"), a_at = { 22, 22 }, b = N("link" .. (LINKS - 1)), b_at = { 7, 7 }, length = 26 }
  j[#j + 1] = { id = "seesaw", kind = "pin", a = N("seesaw"), a_at = { 80, 5 }, b_at = { 270, FY - 22 - 7 }, limits = { -12, 12 } }
  return j
end

-- ── interaction ───────────────────────────────────────────────────────────────────────────────
local function arc_from(px, py, vx, vy)
  local g = state.g[2]
  local pts = {}
  for i = 1, PIPS do
    local t = 0.07 * i
    pts[i] = { px + vx * t, py + vy * t + 0.5 * g * t * t }
  end
  return pts
end

local function show_aim(px, py)
  local w = world("lab")
  local vx, vy = (SLING[1] - px) * POWER, (SLING[2] - py) * POWER
  w:set("ammo", { pos = { px - 11, py - 11 } })
  for i = 1, BAND do
    local t = i / (BAND + 1)
    w:set("band" .. i, { pos = { SLING[1] + (px - SLING[1]) * t - 2.5, SLING[2] + (py - SLING[2]) * t - 2.5 } })
  end
  for i, p in ipairs(arc_from(px, py, vx, vy)) do w:set("arc" .. i, { pos = { p[1] - 3, p[2] - 3 } }) end
end

local function hide_aim()
  local w = world("lab")
  state.aim = nil
  w:set("ammo", { pos = { SLING[1] - 11, SLING[2] - 11 } })
  for i = 1, BAND do w:set("band" .. i, { pos = { -40, -40 } }) end
  for i = 1, PIPS do w:set("arc" .. i, { pos = { -40, -40 } }) end
end

local function clamp_pull(x, y)
  local dx, dy = x - SLING[1], y - SLING[2]
  local d = math.sqrt(dx * dx + dy * dy)
  if d > PULL then dx, dy = dx / d * PULL, dy / d * PULL end
  return SLING[1] + dx, SLING[2] + dy
end

local function fire(px, py)
  local vx, vy = (SLING[1] - px) * POWER, (SLING[2] - py) * POWER
  state.nshot = state.nshot + 1
  local id = N("shot" .. state.nshot)
  state[id] = { x = px, y = py }
  state.shots[#state.shots + 1] = id
  if #state.shots > 3 then table.remove(state.shots, 1) end
  state.pending = { id = id, vx = vx, vy = vy }
  state.status = "Fire!"
  world("lab"):after(0.03, "launch")
end

local function on_drag(e)
  local w = world("lab")
  if e.phase == "start" then
    state.aim, state.held = nil, nil
    if e.shape == "ammo" then
      state.aim = true
    elseif grabbable[e.shape] then
      state.held = e.shape
    end
    return
  end
  if state.aim then
    local px, py = clamp_pull(e.x, e.y)
    if e.phase == "move" then
      show_aim(px, py)
    else
      local dx, dy = SLING[1] - px, SLING[2] - py
      hide_aim()
      if dx * dx + dy * dy > 144 then fire(px, py) end
    end
  elseif state.held then
    if e.phase == "end" then w:release(state.held) state.held = nil else w:grab(state.held, { e.x, e.y }) end
  end
end

local function on_zone(e)
  if e.phase ~= "enter" then return end
  local who = e.who
  if e.id == "killzone" then
    for _, name in ipairs({ "pig_roof", "pig_mid" }) do
      if who == N(name) and state.pigs[name] then
        state.pigs[name] = nil
        state.score = state.score + 100
        state.status = "Pig down!  +100"
      end
    end
  elseif e.id == "goal" and who == N("marble") and not state.goal then
    state.goal = true
    state.score = state.score + 500
    state.status = "GOAL!  +500"
  end
end

-- Lua cannot read where a hit landed, so a hard one flashes the whole sky instead.
local function on_hit(e)
  if e.speed > 450 and not state.flash then
    state.flash = true
    world("lab"):after(0.09, "unflash")
  end
end

local function on_timer(e)
  if e.name == "unflash" then state.flash = nil end
  if e.name == "launch" and state.pending then
    local p = state.pending
    state.pending = nil
    world("lab"):set(p.id, { velocity = { p.vx, p.vy } })
  end
end

local function reset()
  gen = gen + 1
  state = fresh()
  state.status = "Reset"
  world("lab"):cancel("launch")
end

local function cycle_gravity()
  if state.gname == "normal" then state.g, state.gname = G_MOON, "moon"
  elseif state.gname == "moon" then state.g, state.gname = G_FLIP, "flip"
  else state.g, state.gname = G_NORMAL, "normal" end
  state.status = "Gravity: " .. state.gname
end

local function button(id, label, on_click)
  return ui.button({
    id = id, px = 16, py = 8, radius = 8, fill = C.button, hover_fill = C.button_hover, press_scale = 0.97,
    ui.text({ label, color = C.text, font_size = 14, no_wrap = true }),
    on_click = on_click,
  })
end

return function()
  local lab = {
    id = "lab", width = W, height = H, radius = 10,
    gravity = state.g,
    wells = state.well and { { at = { 490, 330 }, radius = 200, pull = 1700 } } or {},
    joints = joints(),
    on_drag = on_drag, on_zone = on_zone, on_timer = on_timer, on_hit = on_hit,
  }
  for _, e in ipairs(build()) do lab[#lab + 1] = e end

  return ui.col({
    full = true, center = true, gap = 10, fill = C.bg,
    ui.row({
      gap = 28, align_center = true,
      ui.text({ "SIEGE", color = C.accent, font_size = 20, no_wrap = true }),
      ui.text({ "score " .. state.score, id = "score", color = C.text, font_size = 16, no_wrap = true }),
      ui.text({ state.status, id = "status", color = C.muted, font_size = 14, no_wrap = true }),
    }),
    ui.world(lab),
    ui.row({
      gap = 10,
      button("swing", "Swing", function() world("lab"):push(N("bob"), { 520, -60 }) state.status = "Wrecking ball!" end),
      button("bomb", "Bomb", function() world("lab"):blast({ 486, 400 }, { speed = 620, radius = 200 }) state.status = "BOOM" end),
      button("gravity", "Gravity: " .. state.gname, cycle_gravity),
      button("well", state.well and "Well: on" or "Well: off", function()
        state.well = not state.well
        state.status = state.well and "Gravity well pulls on the castle" or "Well closed"
      end),
      button("reset", "Reset", reset),
    }),
  })
end

07 / VECTORS

Interactive vector graphics and data.

Diagrams, node graphs, charts and meshes are drawn natively and respond to the pointer: dragging, hovering, selecting ranges.

local C = require("theme")
local V = require("voronoi")
local M = require("model")

local KAPPA = 0.5522847498307933

local function poly_path(poly, ox, oy)
  local cmds = { { "move", poly[1][1] - ox, poly[1][2] - oy } }
  for i = 2, #poly do
    cmds[i] = { "line", poly[i][1] - ox, poly[i][2] - oy }
  end
  cmds[#cmds + 1] = { "close" }
  return gfx.path(cmds)
end

local function circle_path(cx, cy, r)
  local k = r * KAPPA
  return gfx.path({
    { "move", cx + r, cy },
    { "cubic", cx + r, cy + k, cx + k, cy + r, cx, cy + r },
    { "cubic", cx - k, cy + r, cx - r, cy + k, cx - r, cy },
    { "cubic", cx - r, cy - k, cx - k, cy - r, cx, cy - r },
    { "cubic", cx + k, cy - r, cx + r, cy - k, cx + r, cy },
    { "close" },
  })
end

local function on_hover(e)
  local phase, shape, sx, sy = e.phase, e.shape, e.sx, e.sy
  if phase == "leave" then
    M.hit = nil
  else
    M.hit = shape and { shape = shape, sx = sx, sy = sy } or nil
  end
end

-- A drag past the press slop cancels the click, so selection and dragging share the frame without
-- a guard between them.
local function on_drag(e)
  local phase, x, y, dx, dy, scale, origin_x, origin_y, shape, sx, sy = e.phase, e.x, e.y, e.dx, e.dy, e.scale, e.origin_x, e.origin_y, e.shape, e.sx, e.sy
  if phase == "start" then
    M.grab(shape, sx, sy)
  end
  M.drag_to(x, y)
  if phase == "end" then
    M.drag = nil
  end
end

local function on_click(e)
  local i = M.index_of(e.shape)
  M.sel = M.sel ~= i and i or nil
end

local function label(text, color, size)
  return ui.text({ text, color = color, font_size = size, no_wrap = true })
end

local function readout(live_name)
  local d = M.drag
  if d then
    local s = M.sites[d.i]
    return string.format(
      "holding %s   site (%.0f, %.0f)   grabbed at sx %.1f  sy %.1f   pointer over %s",
      d.shape,
      s.x,
      s.y,
      d.sx,
      d.sy,
      live_name
    )
  end

  local h = M.hit
  if not h then
    return "hover a cell"
  end
  local s = M.sites[M.index_of(h.shape)]
  return string.format(
    "%s   site (%.0f, %.0f)   sx %.1f  sy %.1f",
    h.shape,
    s.x,
    s.y,
    h.sx,
    h.sy
  )
end

return function()
  local sites = M.sites
  local cells = V.cells(sites, C.w, C.h)

  -- The frame's own hit test, which is resampled every frame — so the lit cell follows a drifting
  -- diagram under a still pointer without a second copy of the pick math living here.
  local hot_kind, hot_i
  if M.hit then
    hot_kind, hot_i = M.hit.shape:match("^(%a+):(%d+)")
    hot_i = tonumber(hot_i)
  end

  -- Each cell is drawn in its own site's frame, so the pointer reports sx, sy as an offset from
  -- that site instead of repeating the frame coordinates it already gets as x, y.
  local items, paths, live = { width = C.w, height = C.h }, {}, 0
  for i = 1, #cells do
    local poly = cells[i]
    if #poly >= 3 then
      live = live + 1
      local s = sites[i]
      paths[i] = poly_path(poly, s.x, s.y)
      local lit = hot_kind == "cell" and hot_i == i
      items[#items + 1] = gfx.group({
        id = "cell:" .. i,
        transform = { 1, 0, 0, 1, s.x, s.y },
        gfx.fill({
          path = paths[i],
          brush = gfx.solid(lit and C.cells_hot[i] or C.cells[i]),
        }),
        gfx.stroke({
          path = paths[i],
          brush = gfx.solid(C.edge),
          width = C.edge_w,
        }),
      })
    end
  end

  -- the halo rides an unnamed group so it paints over every cell without claiming the pointer
  if M.sel and paths[M.sel] then
    items[#items + 1] = gfx.group({
      transform = { 1, 0, 0, 1, sites[M.sel].x, sites[M.sel].y },
      gfx.stroke({
        path = paths[M.sel],
        brush = gfx.solid(C.halo),
        width = C.halo_w,
        join = "round",
      }),
    })
  end

  -- dots last: where two named shapes overlap, the later one answers the pointer.
  -- Each is a named group holding one dot drawn at the origin, so the pointer reports
  -- sx, sy as an offset from its site rather than as frame coordinates again.
  local rim, rim_brush = circle_path(0, 0, C.site_hit), gfx.solid(C.site_rim)
  local core, core_brush = circle_path(0, 0, C.site_r), gfx.solid(C.site_core)
  local core_lit, core_lit_brush = circle_path(0, 0, C.site_r + 1.5), gfx.solid(C.site_core_hot)
  for i = 1, #sites do
    local s = sites[i]
    local lit = hot_kind == "site" and hot_i == i
    items[#items + 1] = gfx.group({
      id = "site:" .. i,
      transform = { 1, 0, 0, 1, s.x, s.y },
      gfx.fill({ path = rim, brush = rim_brush }),
      gfx.fill({
        path = lit and core_lit or core,
        brush = lit and core_lit_brush or core_brush,
      }),
    })
  end

  local canvas = {
    id = "canvas",
    visual = gfx.frame(items),
    on_hover = on_hover,
    on_click = on_click,
    on_drag = on_drag,
  }
  if not M.paused then
    canvas.on_frame = M.step
  end

  local live_name = hot_kind and (hot_kind .. ":" .. hot_i) or "nothing"

  return ui.col({
    id = "voronoi",
    grow = true,
    fill = C.bg,
    pad = 20,
    gap = 14,
    align_center = true,
    ui.row({
      w = C.w,
      gap = 12,
      align_center = true,
      label("Voronoi", C.text, 16),
      label("every cell is clipped out of the rectangle, this frame — drag a site", C.dim, 12),
      ui.col({ grow = true }),
      label(live .. " / " .. #sites .. " cells", C.dim, 12),
      ui.button({
        id = "pause",
        h = 28,
        px = 12,
        radius = 7,
        center = true,
        fill = C.accent,
        hover_fill = C.accent_hot,
        press_scale = 0.96,
        label(M.paused and "Resume" or "Pause", "#ffffff", 12),
        on_click = function()
          M.paused = not M.paused
        end,
      }),
    }),
    ui.frame(canvas),
    ui.col({
      w = C.w,
      gap = 4,
      label(readout(live_name), C.text, 12),
      label(
        M.sel and ("selected cell:" .. M.sel .. " — click it again to clear")
          or "click to select a cell; drag a site, or anywhere in a cell, to move it",
        C.dim,
        12
      ),
    }),
  })
end

08 / DETERMINISM

Recorded by script, frame for frame.

The shell runs windowless on a virtual clock that only moves when asked, so layout and time-dependent behaviour are exact. Every clip here was produced by a script, not a screen recorder.

-- Physics playground: a top-down table where Rapier does all the moving. Lua describes bodies,
-- hears pockets and drags, and commands velocities; nothing runs per frame. The world has no
-- gravity, so "falling" is a launched velocity and pegs are fixed colliders.
local C = {
  bg = "#12131c", felt = "#1f5c46", rim = "#4a2f1d", pocket = "#0a0b10", peg = "#e9a23b",
  cue = "#f4f1e8", text = "#e7e9f3", muted = "#8a8fa8", button = "#2c3048", button_hover = "#3a3f5e",
  aim = "#f4f1e8",
}
local W, H, T = 880, 500, 14 -- the table, wall thickness
local POCKET = 30
local CUE_POS = { 150, H / 2 - 14 }

-- Park-Miller, so a recording of the same clicks is the same chaos.
local seed = 20261018
local function rnd()
  seed = (seed * 16807) % 2147483647
  return seed / 2147483647
end

local function disc(r, fill, stroke)
  local k = r * 0.5523
  return { path = { {"move",0,r}, {"cubic",0,r-k,r-k,0,r,0}, {"cubic",r+k,0,2*r,r-k,2*r,r},
    {"cubic",2*r,r+k,r+k,2*r,r,2*r}, {"cubic",r-k,2*r,0,r+k,0,r}, {"close"} },
    fill = fill, stroke = stroke }
end
local function rect(w, h, fill)
  return { path = { {"move",0,0}, {"line",w,0}, {"line",w,h}, {"line",0,h}, {"close"} }, fill = fill }
end
local function drawing(w, h, shapes)
  return gfx.drawing({ size = { w, h }, parts = { { id = "it", pivot = { w / 2, h / 2 }, shapes = shapes } } })
end

-- A ball carries a light tick so its spin is visible; drawings are made once per size and colour.
local balls_cache = {}
local function ball_drawing(r, fill)
  local key = r .. fill
  if not balls_cache[key] then
    local tick = { path = { {"move",r-1.5,2}, {"line",r+1.5,2}, {"line",r+1.5,r*0.7}, {"line",r-1.5,r*0.7},
      {"close"} }, fill = "#ffffff55" }
    balls_cache[key] = drawing(2 * r, 2 * r, { disc(r, fill, { 1.5, "#00000055" }), tick })
  end
  return balls_cache[key]
end

local KINDS = {
  { r = 9, bounce = 0.98, friction = 0.08, colors = { "#ff6b6b", "#ffd166", "#ff9f43" } }, -- quick and bouncy
  { r = 14, bounce = 0.85, friction = 0.25, colors = { "#4dabf7", "#63e6be", "#b197fc" } },
  { r = 22, bounce = 0.45, friction = 0.8, colors = { "#868e96", "#5c677d" } }, -- heavy, dies fast
}

local function block(id, x, y, w, h)
  return { id = id, pos = { x, y }, drawing = drawing(w, h, { rect(w, h, C.rim) }), collider = { rect = { w, h } } }
end
local function peg(id, cx, cy, r)
  return { id = id, pos = { cx - r, cy - r }, drawing = drawing(2 * r, 2 * r, { disc(r, C.peg, { 3, "#fff3" }) }),
    collider = { circle = r, at = { r, r } } }
end
-- Corner pockets: the sensor reaches past the drawn hole so a ball at the wall still counts.
local function pocket(id, cx, cy)
  local R = POCKET
  return { id = id, pos = { cx - R, cy - R }, drawing = drawing(2 * R, 2 * R, { disc(R, C.pocket) }),
    sensor = { circle = R + 8, at = { R, R } } }
end

local balls = {} -- { id, x, y, r, bounce, friction, color }
local launches = {} -- { id, vx, vy }, set once the entities exist
local serial = 0
local pocketed, scratches = 0, 0
local aim = nil -- { x, y } where the cue was grabbed

local function add_ball(kind, x, y, color)
  serial = serial + 1
  local id = "b" .. serial
  balls[#balls + 1] = { id = id, x = x, y = y, r = kind.r, bounce = kind.bounce, friction = kind.friction,
    color = color or kind.colors[1 + math.floor(rnd() * #kind.colors)] }
  return id
end

local function clear()
  balls, launches, pocketed, scratches = {}, {}, 0, 0
  world("table"):cancel("launch")
end

-- Fifteen same-size balls in a triangle: one break shot scatters them.
local function rack()
  clear()
  local kind = KINDS[2]
  local d = 2 * kind.r + 1
  local palette = { "#ff6b6b", "#ffd166", "#4dabf7", "#63e6be", "#b197fc", "#ff9f43", "#f783ac" }
  for row = 0, 4 do
    for i = 0, row do
      local n = #balls
      add_ball(kind, 640 + row * d * 0.87, H / 2 - row * d / 2 + i * d - kind.r, palette[1 + n % #palette])
    end
  end
  world("table"):set("cue", { pos = CUE_POS, velocity = { 0, 0 } })
end

-- A volley from the top edge, aimed down at the pegs: mixed sizes, speeds and bounce.
local function burst()
  for i = 0, 13 do
    local kind = KINDS[1 + math.floor(rnd() * #KINDS)]
    local id = add_ball(kind, 250 + (i % 7) * 60, 40 + math.floor(i / 7) * 50)
    launches[#launches + 1] = { id = id, vx = (rnd() - 0.5) * 500, vy = 350 + rnd() * 450 }
  end
  world("table"):after(0.03, "launch")
end

local function button(id, label, on_click)
  return ui.button({
    id = id, px = 18, py = 9, radius = 8, fill = C.button, hover_fill = C.button_hover, press_scale = 0.97,
    ui.text({ label, color = C.text, font_size = 14, no_wrap = true }),
    on_click = on_click,
  })
end

local PIPS = 6
local function hide_aim()
  aim = nil
  for i = 1, PIPS do world("table"):set("pip" .. i, { pos = { -50, -50 } }) end
end

local pip = drawing(8, 8, { disc(4, C.aim) })

return function()
  local entities = {
    block("wall:top", 0, 0, W, T), block("wall:bottom", 0, H - T, W, T),
    block("wall:left", 0, 0, T, H), block("wall:right", W - T, 0, T, H),
    pocket("pocket:tl", 0, 0), pocket("pocket:tr", W, 0), pocket("pocket:bl", 0, H), pocket("pocket:br", W, H),
    peg("peg:mid", 440, 260, 26), peg("peg:tl", 350, 200, 18), peg("peg:tr", 530, 200, 18),
    peg("peg:bl", 350, 320, 18), peg("peg:br", 530, 320, 18),
    { id = "cue", pos = CUE_POS, drawing = ball_drawing(14, C.cue),
      collider = { circle = 14, at = { 14, 14 } }, loose = { bounce = 0.92, friction = 0.3, grip = 0.25 } },
  }
  for _, b in ipairs(balls) do
    entities[#entities + 1] = { id = b.id, pos = { b.x - b.r, b.y - b.r }, drawing = ball_drawing(b.r, b.color),
      collider = { circle = b.r, at = { b.r, b.r } }, loose = { bounce = b.bounce, friction = b.friction, grip = 0.2 } }
  end
  for i = 1, PIPS do entities[#entities + 1] = { id = "pip" .. i, pos = { -50, -50 }, drawing = pip } end

  local table_el = {
    id = "table", width = W, height = H, fill = C.felt, radius = 10, stroke = { 3, C.rim },
    on_zone = function(e)
      if e.phase ~= "enter" then return end
      if e.who == "cue" then
        scratches = scratches + 1
        world("table"):set("cue", { pos = CUE_POS, velocity = { 0, 0 } })
        return
      end
      for i, b in ipairs(balls) do
        if b.id == e.who then
          table.remove(balls, i)
          pocketed = pocketed + 1
          break
        end
      end
    end,
    on_timer = function(e)
      if e.name ~= "launch" then return end
      for _, l in ipairs(launches) do world("table"):set(l.id, { velocity = { l.vx, l.vy } }) end
      launches = {}
    end,
    -- Pull the cue back like a slingshot: dots show the line it will take.
    on_drag = function(e)
      if e.phase == "start" then
        aim = e.shape == "cue" and { x = e.x, y = e.y } or nil
        return
      end
      if not aim then return end
      local px, py = aim.x - e.x, aim.y - e.y
      if e.phase == "move" then
        for i = 1, PIPS do
          world("table"):set("pip" .. i, { pos = { aim.x + px * i / PIPS - 4, aim.y + py * i / PIPS - 4 } })
        end
      else
        local len = math.sqrt(px * px + py * py)
        local k = len > 1400 / 6 and 1400 / len or 6
        if len > 6 then world("table"):set("cue", { velocity = { px * k, py * k } }) end
        hide_aim()
      end
    end,
  }
  for _, en in ipairs(entities) do table_el[#table_el + 1] = en end

  return ui.col({
    full = true, center = true, gap = 12, fill = C.bg,
    ui.row({
      gap = 24, align_center = true,
      ui.text({ "Physics playground", color = C.text, font_size = 22, no_wrap = true }),
      ui.text({ "pocketed " .. pocketed .. " · scratches " .. scratches, id = "score", color = C.muted,
        font_size = 14, no_wrap = true }),
    }),
    ui.world(table_el),
    ui.row({
      gap = 12,
      button("rack", "Rack", rack),
      button("burst", "Burst", burst),
      button("clear", "Clear", clear),
    }),
  })
end

ALL RECORDINGS

Everything
we have recorded.

Click any clip to enlarge it.

Platform

Sync, agents and the shell itself.

Physics

A 2D engine the apps drive from Lua.

Vectors and data

Interactive drawing, no webview.

STATUS

Experimental.
Not for sale, not yet a product.

Nothing here is a mock-up, and nothing is packaged for download yet. The source is on GitHub; the docs describe the architecture. Tinline, from the same team, ships first.

Read the architecture ↗

GRANTS RECEIVED FROM

FOSS UnitedZerodhaKSUM