diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..67c6577 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,42 @@ +# Publishes the static marketing site in website/ to GitHub Pages. +# Enable it once under Settings -> Pages -> Source: "GitHub Actions". +name: pages + +on: + push: + branches: [main] + paths: + - "website/**" + - "docs/**" + - "tools/build-docs.py" + - ".github/workflows/pages.yml" + workflow_dispatch: + +# Allow this workflow to publish a Pages deployment. +permissions: + contents: read + pages: write + id-token: write + +# One concurrent deploy; let an in-progress run finish. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + - uses: actions/configure-pages@v5 + # Regenerate the styled doc pages from docs/*.md so the deploy is never + # stale (no extra dependencies — the generator is pure-Python stdlib). + - run: python3 tools/build-docs.py + - uses: actions/upload-pages-artifact@v3 + with: + path: website + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/tools/build-docs.py b/tools/build-docs.py new file mode 100644 index 0000000..c81b672 --- /dev/null +++ b/tools/build-docs.py @@ -0,0 +1,395 @@ +#!/usr/bin/env python3 +"""Render docs/*.md into styled, self-contained HTML pages under website/docs/. + +Dependency-free on purpose: OXT/web sessions and CI may have no network for +pip, so this ships its own small Markdown converter covering exactly what the +Box2Dxt docs use (ATX headings with GitHub-style anchors, fenced code, GFM +pipe tables, blockquotes, nested lists, inline code/links/bold/italic, rules). + +Run after editing any doc: python3 tools/build-docs.py +The site links to these pages instead of raw .md files on GitHub. +""" + +import html +import pathlib +import re + +ROOT = pathlib.Path(__file__).resolve().parent.parent +SRC = ROOT / "docs" +OUT = ROOT / "website" / "docs" +GH = "https://github.com/SethMorrowSoftware/Box2Dxt" + +# Host only the approachable, user-facing docs. The deeper/internal material +# (raw API, architecture, build-from-source, design/roadmap) stays on GitHub — +# links to those .md files are rewritten to GitHub automatically. +DOCS = [ + ("getting-started", "Getting started", "Install the library and build your first draggable scene."), + ("kit-guide", "Kit guide", "The friendly b2k… layer, taught step by step."), + ("kit-reference", "Kit reference", "Every b2k… call, one line each — keep it open while you build."), +] +TITLES = {k: (t, d) for k, t, d in DOCS} + +# Shown on the manual overview as "going deeper" — these live on GitHub. +EXTERNAL = [ + ("API reference", "The raw b2… extension surface.", "api-reference.md"), + ("Architecture", "How the three layers fit together.", "architecture.md"), + ("Game engine spec", "The Game Kit design, in depth.", "game-engine-spec.md"), + ("Building", "Compile the native library yourself.", "building.md"), + ("Expansion prep", "The internal roadmap & intake plan.", "expansion-prep.md"), +] + + +# --------------------------------------------------------------------------- # +# link rewriting: keep doc-to-doc links in-site, send everything else to GH +# --------------------------------------------------------------------------- # +def rewrite_href(href): + href = href.strip() + if href.startswith("#") or href.startswith("mailto:") or re.match(r"^https?://", href): + return href + path, _, frag = href.partition("#") + frag = ("#" + frag) if frag else "" + + m = re.match(r"^(?:\./)?([\w-]+)\.md$", path) + if m: + name = m.group(1) + if name in TITLES: + return f"{name}.html{frag}" + return f"{GH}/blob/main/docs/{name}.md{frag}" + + if path.startswith(".."): + rel = re.sub(r"^(\.\./)+", "", path) + if rel.rstrip("/") == "releases": + return f"{GH}/releases{frag}" + if path.endswith("/") or "." not in rel.split("/")[-1]: + return f"{GH}/tree/main/{rel.rstrip('/')}{frag}" + return f"{GH}/blob/main/{rel}{frag}" + + if path: # any other repo-relative path lives under docs/ + return f"{GH}/blob/main/docs/{path}{frag}" + return href + + +# --------------------------------------------------------------------------- # +# inline (runs on prose, table cells, list items, headings) +# --------------------------------------------------------------------------- # +def inline(text): + spans = [] + + def stash(m): + spans.append(html.escape(m.group(1))) + return f"\x00{len(spans) - 1}\x00" + + text = re.sub(r"`([^`]+)`", stash, text) # protect inline code + text = html.escape(text, quote=False) # escape the rest + text = re.sub(r"!\[([^\]]*)\]\(([^)\s]+)[^)]*\)", + lambda m: f'{m.group(1)}', text) + text = re.sub(r"\[([^\]]+)\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)", + lambda m: f'{m.group(1)}', text) + text = re.sub(r"<(https?://[^&\s]+)>", + lambda m: f'{m.group(1)}', text) + text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) # bold may wrap italics + text = re.sub(r"__(.+?)__", r"\1", text) + text = re.sub(r"(?\1", text) + text = re.sub(r"(?\1", text) + text = re.sub(r"\x00(\d+)\x00", lambda m: f"{spans[int(m.group(1))]}", text) + return text + + +def slugify(text, used): + t = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) # link -> its text + t = re.sub(r"[`*_]", "", t).strip().lower() + t = re.sub(r"[^\w\s-]", "", t) + t = re.sub(r"\s", "-", t) # GitHub does NOT collapse: "a & b" -> "a--b" + t = t.strip("-") + base, k = t, 1 + while t in used: + t = f"{base}-{k}" + k += 1 + used.add(t) + return t + + +# --------------------------------------------------------------------------- # +# block parser +# --------------------------------------------------------------------------- # +ITEM_RE = re.compile(r"^(\s*)([-*+]|\d+[.)])\s+(.*)$") + + +def is_table_sep(s): + s = s.strip() + return bool(s) and set(s) <= set("|:- ") and "-" in s and "|" in s + + +def split_row(line): + line = line.strip() + if line.startswith("|"): + line = line[1:] + if line.endswith("|"): + line = line[:-1] + cells = re.split(r"(?"] + for it in items: + subblock = [s[base:] if len(s) >= base else s for s in it["sub"]] + # split wrapped continuation text (before any nested list) from nested items + cont_lines, nested_lines, seen = [], [], False + for s in subblock: + if ITEM_RE.match(s): + seen = True + (nested_lines if seen else cont_lines).append(s) + raw = it["text"] + cont = " ".join(s.strip() for s in cont_lines if s.strip()) + if cont: # join so inline runs once (bold can wrap lines) + raw = raw + " " + cont + body = inline(raw) + if any(ITEM_RE.match(s) for s in nested_lines): + body += render_list(nested_lines) + out.append(f"
  • {body}
  • ") + out.append(f"") + return "\n".join(out) + + +def convert(md): + lines = md.replace("\r\n", "\n").split("\n") + n = len(lines) + out, used = [], set() + i = 0 + while i < n: + line = lines[i] + + if not line.strip(): + i += 1 + continue + + # fenced code + fence = re.match(r"^\s*(`{3,}|~{3,})(.*)$", line) + if fence: + marker = fence.group(1)[0] + buf, i = [], i + 1 + while i < n and not re.match(rf"^\s*{re.escape(marker)}{{3,}}\s*$", lines[i]): + buf.append(lines[i]) + i += 1 + i += 1 # skip closing fence + code = html.escape("\n".join(buf)) + out.append(f'
    {code}
    ') + continue + + # heading + h = re.match(r"^(#{1,6})\s+(.*?)\s*#*\s*$", line) + if h: + level = len(h.group(1)) + raw = h.group(2) + sid = slugify(raw, used) + out.append(f'{inline(raw)}') + i += 1 + continue + + # horizontal rule + if re.match(r"^\s*([-*_])(\s*\1){2,}\s*$", line): + out.append("
    ") + i += 1 + continue + + # table + if "|" in line and i + 1 < n and is_table_sep(lines[i + 1]): + header = split_row(line) + i += 2 + rows = [] + while i < n and lines[i].strip() and "|" in lines[i]: + rows.append(split_row(lines[i])) + i += 1 + thead = "".join(f"{inline(c)}" for c in header) + tbody = "" + for r in rows: + r = (r + [""] * len(header))[:len(header)] + tbody += "" + "".join(f"{inline(c)}" for c in r) + "" + out.append(f'
    {thead}' + f"{tbody}
    ") + continue + + # blockquote + if re.match(r"^\s*>", line): + buf = [] + while i < n and re.match(r"^\s*>", lines[i]): + buf.append(re.sub(r"^\s*>\s?", "", lines[i])) + i += 1 + out.append(f"
    {convert(chr(10).join(buf))}
    ") + continue + + # list + if ITEM_RE.match(line): + base = len(re.match(r"\s*", line).group()) + buf = [] + while i < n: + l = lines[i] + if not l.strip(): + j = i + 1 + if j < n and (ITEM_RE.match(lines[j]) or (lines[j][:1] == " " and lines[j].strip())): + buf.append(l) + i += 1 + continue + break + if ITEM_RE.match(l) or l.startswith(" "): + buf.append(l) + i += 1 + continue + break + out.append(render_list(buf)) + continue + + # paragraph + buf = [] + while i < n and lines[i].strip() and not re.match(r"^\s*(#{1,6}\s|>|```|~~~)", lines[i]) \ + and not ITEM_RE.match(lines[i]) \ + and not re.match(r"^\s*([-*_])(\s*\1){2,}\s*$", lines[i]) \ + and not ("|" in lines[i] and i + 1 < n and is_table_sep(lines[i + 1])): + buf.append(lines[i].rstrip()) + i += 1 + out.append(f"

    {inline(' '.join(buf))}

    ") + + return "\n".join(out) + + +# --------------------------------------------------------------------------- # +# page templates +# --------------------------------------------------------------------------- # +CRATE = ('') + + +def menubar(): + return f"""""" + + +def sidebar(active): + rows = ['Overview' % (' class="active"' if active == "index" else "")] + for key, title, _ in DOCS: + cls = ' class="active"' if key == active else "" + rows.append(f'{title}') + rows.append(f'Full docs on GitHub →') + return ('") + + +FOOTER = f"""""" + + +def shell(title, desc, active, main_html): + return f""" + + + + +{html.escape(title)} — Box2Dxt docs + + + + + + + + + +{CRATE} +{menubar()} +{main_html} +{FOOTER} + + + +""" + + +def doc_page(key): + title, desc = TITLES[key] + body = convert((SRC / f"{key}.md").read_text(encoding="utf-8")) + main = f"""
    +{sidebar(key)} +
    +
    {key}.md
    +
    {body}
    +
    +
    """ + return shell(title, desc, key, main) + + +def index_page(): + nums = ["①", "②", "③"] + cards = "\n".join( + f'

    {nums[i]} {t}

    {html.escape(d)}

    ' + for i, (k, t, d) in enumerate(DOCS) + ) + deeper = "\n".join( + f'
  • {t} — {html.escape(d)}
  • ' + for t, d, path in EXTERNAL + ) + main = f"""
    +{sidebar("index")} +
    +
    the manual
    +
    +

    The Box2Dxt manual

    +

    New here? You only need two things: Getting started to install Box2Dxt and drop your first body, then the Kit guide to learn the rest. Keep the Kit reference open while you build.

    +
    {cards}
    +

    Going deeper

    +

    The low-level b2… API, the engine internals, and build-from-source notes are for the curious — they live on GitHub:

    +
      {deeper}
    +
    +
    +
    """ + return shell("Documentation", "The Box2Dxt guides — install, learn the Kit, and a quick reference.", "index", main) + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + (OUT / "index.html").write_text(index_page(), encoding="utf-8") + n = 1 + for key, _, _ in DOCS: + (OUT / f"{key}.html").write_text(doc_page(key), encoding="utf-8") + n += 1 + print(f"Built {n} doc pages -> {OUT.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..dad47b4 --- /dev/null +++ b/website/README.md @@ -0,0 +1,115 @@ +# Box2Dxt — project website + +A small, self-contained marketing site for Box2Dxt. No build step, no +framework, no required network dependencies — it's three static files plus a +live physics demo written in plain JavaScript. + +**Personality: HyperCard heritage.** The site leans into the xTalk lineage — +warm "paper" background, classic-Mac window chrome (striped title bars, square +close boxes), hard ink borders with solid offset shadows, a pixel font for +chrome, and the card/stack metaphor as the actual layout. The one signature +colour is the Box2D crate-orange, and the hero demo is a pile of tumbling, +stacking crates. Retro motifs, modern layout discipline. + +**The landing page IS a HyperCard stack.** Instead of one long scroll, the +home page is a stack of cards you flip through, one at a time: + +- **Navigate** with the menu, the Home-card launcher, the ◄ / ► card-nav bar, + the title-bar Home box, or the keyboard (`←` `→` flip, `H` home, `M` message). +- **Card-flip transitions** (dissolve + directional slide; an iris for Home), + honouring `prefers-reduced-motion`. +- **Deep links + history**: each card has a hash (`#examples`) and Back/Forward + work. The doc pages' menu links jump straight to the right card. +- **The Message Box** — a HyperTalk-ish console (press `M`). Try `go to + examples`, `next` / `prev` / `home`, `spawn a crate`, `reset`, `gravity`, + `help`. +- **Graceful fallback**: an inline `` flag gates single-card + mode, so with JavaScript off the cards simply stack into a normal scroll page. + +``` +website/ +├── index.html # the landing page (menu bar + window-framed "cards") +├── styles.css # the paper/ink/orange design system + Mac window chrome + .prose +├── app.js # crate-physics toy + the card-stack navigation + Message Box +└── docs/ # the user-facing guides, rendered from docs/*.md (generated) + ├── index.html # the manual overview + └── .html # getting-started, kit-guide, kit-reference +``` + +## The docs are rendered from Markdown + +The pages under `website/docs/` are **generated** from the repository's +`docs/*.md` by [`tools/build-docs.py`](../tools/build-docs.py) — a small, +dependency-free Markdown→HTML converter (no `pip`/network needed). + +**Only the user-facing guides are hosted on the site** — Getting started, the +Kit guide, and the Kit reference (the `DOCS` list in the generator). The deeper +material (the raw `b2…` API, architecture, build-from-source, the design/roadmap +docs — the `EXTERNAL` list) is intentionally left on GitHub and linked from the +manual overview; doc-to-doc links pointing at those are rewritten to GitHub +automatically. To host or unhost a doc, move it between `DOCS` and `EXTERNAL`. + +Re-run it whenever a doc changes: + +```sh +python3 tools/build-docs.py # rewrites website/docs/*.html +``` + +> Don't hand-edit `website/docs/*.html` — they're overwritten on the next +> build. Edit the source `docs/*.md` and regenerate. The GitHub Pages workflow +> runs the generator before every deploy, so the live site never goes stale. + +## View it locally + +Just open `index.html` in a browser, or serve the folder: + +```sh +cd website +python3 -m http.server 8000 +# then visit http://localhost:8000 +``` + +(Serving is recommended over `file://` so the Google Fonts request and relative +asset paths behave exactly as they will in production. The site degrades +gracefully to system fonts offline.) + +## Deploy to GitHub Pages + +A workflow at [`.github/workflows/pages.yml`](../.github/workflows/pages.yml) +publishes this folder automatically. To turn it on, in the repository: + +1. **Settings → Pages → Build and deployment → Source: GitHub Actions.** +2. Push to `main` (or run the workflow manually from the Actions tab). + +The site will be served at `https://sethmorrowsoftware.github.io/Box2Dxt/`. + +> Prefer the classic "deploy from a branch" flow instead? Point Pages at the +> `main` branch and a `/docs` folder, then copy these three files there — but +> the Actions workflow above keeps the site isolated in `website/` and needs no +> file moves. + +## What it advertises + +- **Ease of use** — the sixty-second `b2kQuickStart` snippet and the + paste-and-run examples. +- **Abilities** — the full Box2D surface (joints, sensors, ray casts, chains), + the Game Kit (player controller, camera, sprites, sound), and the safety + model (generation-tagged handles). +- **Reach** — prebuilt cross-platform binaries and links into every doc. + +## Editing + +Everything is hand-written and dependency-free: + +- Copy lives directly in `index.html` (sections are styled as numbered + "cards" inside Mac windows — the `.win` / `.win-bar` components). +- The palette and tokens are CSS custom properties at the top of `styles.css` + (`--paper`, `--ink`, `--orange`, plus `--blue` / `--green` / `--red` / `--hl`). + Fonts: Space Grotesk (display/body), JetBrains Mono (code), Silkscreen (the + pixel chrome). +- The hero demo is a compact impulse-based solver in `app.js` — circle + colliders for stability, rendered as rotating crates + a few cannonballs. + Tune the constants near the top (`GRAV`, `REST`, `MAX_BODIES`, `CRATES`, …). + +If you change any GitHub link, the repository slug `SethMorrowSoftware/Box2Dxt` +appears throughout `index.html`. diff --git a/website/app.js b/website/app.js new file mode 100644 index 0000000..3208c61 --- /dev/null +++ b/website/app.js @@ -0,0 +1,384 @@ +/* =========================================================== + Box2Dxt site — the interactive HyperCard stack + 1) Mobile menu toggle + 2) The hero physics toy: tumbling, stacking crates (exposes a + small API so the Message Box can drive it) + 3) Stack navigation: flip cards with transitions, keyboard, + deep links + history, a Home launcher + 4) The Message Box: type HyperTalk-ish commands + Vanilla JS, no libraries. Degrades to a plain page without JS. + =========================================================== */ +(function () { + "use strict"; + var INK = "#17140d"; + var reduceMotion = window.matchMedia && matchMedia("(prefers-reduced-motion: reduce)").matches; + + /* ---------- Mobile menu ---------- */ + var toggle = document.getElementById("menuToggle"); + var links = document.getElementById("menuLinks"); + if (toggle && links) { + toggle.addEventListener("click", function () { + var open = links.classList.toggle("open"); + toggle.setAttribute("aria-expanded", open ? "true" : "false"); + }); + } + + /* ========================================================= + Physics toy + ========================================================= */ + var demo = null; + var canvas = document.getElementById("physics"); + if (canvas && canvas.getContext) { + var ctx = canvas.getContext("2d"); + var CRATES = ["#e8702a", "#2f5fae", "#3f8f5b", "#d23b3b", "#e8a23a"]; + var BALLS = ["#2a2620", "#3a3530"]; + var GRAV = 1700, REST = 0.14, WALL_REST = 0.3, FRICTION = 0.18; + var SLOP = 0.5, CORRECT = 0.8, ITER = 6, DT = 1 / 120, MAXV = 2600, MAXSPIN = 16, MAX_BODIES = 24; + + var W = 0, H = 0, dpr = 1, bodies = [], gravityOn = true; + var held = null, heldSavedInv = 0; + var pointer = { x: 0, y: 0, active: false }; + var interacted = false, running = false, last = 0, acc = 0; + + var rand = function (a, b) { return a + Math.random() * (b - a); }; + + function roundRect(x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); + } + function resize() { + var rect = canvas.getBoundingClientRect(); + W = Math.max(1, rect.width); H = Math.max(1, rect.height); + dpr = Math.min(window.devicePixelRatio || 1, 2); + canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + for (var i = 0; i < bodies.length; i++) { + bodies[i].x = Math.min(Math.max(bodies[i].r, bodies[i].x), W - bodies[i].r); + bodies[i].y = Math.min(Math.max(bodies[i].r, bodies[i].y), H - bodies[i].r); + } + } + function makeBody(x, y, r, shape, color) { + return { x: x, y: y, vx: rand(-40, 40), vy: rand(0, 40), r: r, + angle: rand(-0.3, 0.3), spin: rand(-2, 2), shape: shape, color: color, invMass: 1 / (r * r) }; + } + function spawn(x, y) { + var r = rand(15, 27), ball = Math.random() < 0.22; + return makeBody(x, y, r, ball ? "ball" : "crate", + ball ? BALLS[(Math.random() * BALLS.length) | 0] : CRATES[(Math.random() * CRATES.length) | 0]); + } + function seed() { + bodies = []; + var n = W < 420 ? 9 : 13; + for (var i = 0; i < n; i++) bodies.push(spawn(rand(30, W - 30), rand(-H, H * 0.35))); + } + function dropAt(x, y) { + if (bodies.length >= MAX_BODIES) bodies.shift(); + var b = spawn(x, y); b.vx = rand(-60, 60); b.vy = rand(-30, 40); b.spin = rand(-5, 5); + bodies.push(b); + } + function step() { + var i, j, b; + for (i = 0; i < bodies.length; i++) { + b = bodies[i]; + b.angle += b.spin * DT; b.spin *= 0.992; + if (b === held) continue; + if (gravityOn) b.vy += GRAV * DT; + b.x += b.vx * DT; b.y += b.vy * DT; + var sp = Math.hypot(b.vx, b.vy); + if (sp > MAXV) { b.vx *= MAXV / sp; b.vy *= MAXV / sp; } + if (b.spin > MAXSPIN) b.spin = MAXSPIN; else if (b.spin < -MAXSPIN) b.spin = -MAXSPIN; + } + if (held) { + var px = Math.min(Math.max(held.r, pointer.x), W - held.r); + var py = Math.min(Math.max(held.r, pointer.y), H - held.r); + held.vx = (px - held.x) / DT; held.vy = (py - held.y) / DT; + held.x = px; held.y = py; + } + for (var it = 0; it < ITER; it++) { + for (i = 0; i < bodies.length; i++) { + for (j = i + 1; j < bodies.length; j++) { + var a = bodies[i], c = bodies[j]; + var dx = c.x - a.x, dy = c.y - a.y, dist = Math.hypot(dx, dy), min = a.r + c.r; + if (dist >= min || dist === 0) continue; + var nx = dx / dist, ny = dy / dist, pen = min - dist, im = a.invMass + c.invMass; + var corr = (Math.max(pen - SLOP, 0) / im) * CORRECT; + a.x -= nx * corr * a.invMass; a.y -= ny * corr * a.invMass; + c.x += nx * corr * c.invMass; c.y += ny * corr * c.invMass; + var rvx = c.vx - a.vx, rvy = c.vy - a.vy, vn = rvx * nx + rvy * ny; + if (vn < 0) { + var jn = -(1 + REST) * vn / im; + a.vx -= jn * nx * a.invMass; a.vy -= jn * ny * a.invMass; + c.vx += jn * nx * c.invMass; c.vy += jn * ny * c.invMass; + var tx = -ny, ty = nx, vt = (c.vx - a.vx) * tx + (c.vy - a.vy) * ty; + var jt = -vt * FRICTION / im; + a.vx -= jt * tx * a.invMass; a.vy -= jt * ty * a.invMass; + c.vx += jt * tx * c.invMass; c.vy += jt * ty * c.invMass; + var kick = vt * 0.03; a.spin -= kick; c.spin += kick; + } + } + } + for (i = 0; i < bodies.length; i++) { + b = bodies[i]; + if (b === held) continue; + if (b.x < b.r) { b.x = b.r; if (b.vx < 0) b.vx = -b.vx * WALL_REST; b.spin *= 0.8; } + else if (b.x > W - b.r) { b.x = W - b.r; if (b.vx > 0) b.vx = -b.vx * WALL_REST; b.spin *= 0.8; } + if (b.y < b.r) { b.y = b.r; if (b.vy < 0) b.vy = -b.vy * WALL_REST; } + else if (b.y > H - b.r) { + b.y = H - b.r; if (b.vy > 0) b.vy = -b.vy * WALL_REST; + b.vx *= 0.985; b.spin = b.spin * 0.6 + (b.vx / b.r) * 0.4; + } + } + } + } + function drawCrate(b) { + var s = b.r * 1.5, in1 = s * 0.13; + roundRect(b.x - s / 2 + 3, b.y - s / 2 + 4, s, s, s * 0.16); + ctx.fillStyle = "rgba(23,20,13,0.16)"; ctx.fill(); + ctx.save(); ctx.translate(b.x, b.y); ctx.rotate(b.angle); + roundRect(-s / 2, -s / 2, s, s, s * 0.16); + ctx.fillStyle = b.color; ctx.fill(); + ctx.lineWidth = 2.4; ctx.strokeStyle = INK; ctx.stroke(); + var k = s * 0.5 - in1; + ctx.lineWidth = 2; ctx.strokeStyle = "rgba(23,20,13,0.8)"; + ctx.beginPath(); ctx.moveTo(-k, -k); ctx.lineTo(k, k); ctx.moveTo(k, -k); ctx.lineTo(-k, k); ctx.stroke(); + ctx.lineWidth = 1.5; ctx.strokeStyle = "rgba(23,20,13,0.4)"; + roundRect(-s / 2 + in1, -s / 2 + in1, s - in1 * 2, s - in1 * 2, s * 0.08); ctx.stroke(); + ctx.restore(); + } + function drawBall(b) { + ctx.beginPath(); ctx.arc(b.x + 2.5, b.y + 4, b.r, 0, 7); ctx.fillStyle = "rgba(23,20,13,0.16)"; ctx.fill(); + ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, 7); ctx.fillStyle = b.color; ctx.fill(); + ctx.lineWidth = 2.4; ctx.strokeStyle = INK; ctx.stroke(); + ctx.save(); ctx.translate(b.x, b.y); ctx.rotate(b.angle); + ctx.beginPath(); ctx.arc(-b.r * 0.34, -b.r * 0.34, b.r * 0.24, 0, 7); ctx.fillStyle = "rgba(255,255,255,0.55)"; ctx.fill(); + ctx.beginPath(); ctx.arc(b.r * 0.5, 0, b.r * 0.13, 0, 7); ctx.fillStyle = "rgba(255,255,255,0.35)"; ctx.fill(); + ctx.restore(); + } + function draw() { + ctx.clearRect(0, 0, W, H); + for (var i = 0; i < bodies.length; i++) bodies[i].shape === "ball" ? drawBall(bodies[i]) : drawCrate(bodies[i]); + } + function frame(t) { + if (!running) return; + if (!last) last = t; + acc += Math.min((t - last) / 1000, 0.05); last = t; + var guard = 0; + while (acc >= DT && guard < 8) { step(); acc -= DT; guard++; } + draw(); + requestAnimationFrame(frame); + } + function start() { if (!running) { running = true; last = 0; requestAnimationFrame(frame); } } + function stop() { running = false; } + + function pt(e) { + var r = canvas.getBoundingClientRect(); + return { x: (e.touches ? e.touches[0].clientX : e.clientX) - r.left, + y: (e.touches ? e.touches[0].clientY : e.clientY) - r.top }; + } + function onDown(e) { + var p = pt(e); pointer.x = p.x; pointer.y = p.y; pointer.active = true; + interacted = true; hideHint(); + var best = null, bestD = Infinity; + for (var i = 0; i < bodies.length; i++) { + var b = bodies[i], d = Math.hypot(b.x - p.x, b.y - p.y); + if (d <= b.r + 6 && d < bestD) { best = b; bestD = d; } + } + if (best) { held = best; heldSavedInv = best.invMass; best.invMass = 0; } else dropAt(p.x, p.y); + e.preventDefault(); + } + function onMove(e) { if (pointer.active) { var p = pt(e); pointer.x = p.x; pointer.y = p.y; e.preventDefault(); } } + function onUp() { + pointer.active = false; + if (held) { held.invMass = heldSavedInv; held.spin = Math.max(-MAXSPIN, Math.min(MAXSPIN, -held.vx / held.r * 0.4)); held = null; } + } + canvas.addEventListener("mousedown", onDown); + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + canvas.addEventListener("touchstart", onDown, { passive: false }); + canvas.addEventListener("touchmove", onMove, { passive: false }); + window.addEventListener("touchend", onUp); + + var resetBtn = document.getElementById("demoReset"); + var gravBtn = document.getElementById("demoGravity"); + var hint = document.getElementById("demoHint"); + function hideHint() { if (hint) { hint.style.transition = "opacity .4s"; hint.style.opacity = "0"; } } + + function toggleGravity() { + gravityOn = !gravityOn; + if (gravBtn) gravBtn.textContent = "GRAVITY: " + (gravityOn ? "ON" : "OFF"); + if (!gravityOn) for (var i = 0; i < bodies.length; i++) bodies[i].vy -= 120; + return gravityOn; + } + if (resetBtn) resetBtn.addEventListener("click", function () { seed(); interacted = true; hideHint(); }); + if (gravBtn) gravBtn.addEventListener("click", function () { toggleGravity(); interacted = true; hideHint(); }); + + resize(); seed(); + window.addEventListener("resize", resize); + document.addEventListener("visibilitychange", function () { if (document.hidden) stop(); }); + setTimeout(function () { if (!interacted) hideHint(); }, 6000); + + demo = { + start: start, stop: stop, + drop: function () { dropAt(rand(W * 0.3, W * 0.7), 30); interacted = true; hideHint(); }, + reset: function () { seed(); }, + gravity: toggleGravity + }; + // if there's no stack to drive start/stop, just run it + if (!document.getElementById("stackBody")) start(); + } + + /* ========================================================= + The stack: flip between cards + ========================================================= */ + var stackBody = document.getElementById("stackBody"); + if (stackBody) { + var order = ["home", "about", "paste", "features", "how", "examples", "docs", "start"]; + var cards = {}, titles = {}; + [].forEach.call(document.querySelectorAll(".card"), function (c) { + cards[c.dataset.card] = c; titles[c.dataset.card] = c.dataset.title || c.dataset.card; + }); + var current = "home"; + var stackTitle = document.getElementById("stackTitle"); + var cardName = document.getElementById("cardName"); + var cardIdx = document.getElementById("cardIdx"); + var cardTot = document.getElementById("cardTot"); + if (cardTot) cardTot.textContent = order.length; + + function setChrome(name) { + var idx = order.indexOf(name); + if (stackTitle) stackTitle.textContent = "Box2Dxt — " + titles[name]; + if (cardName) cardName.textContent = titles[name]; + if (cardIdx) cardIdx.textContent = idx + 1; + if (links) [].forEach.call(links.querySelectorAll("a[data-go]"), function (a) { + a.classList.toggle("active", a.dataset.go === name); + }); + } + + function show(name, hist) { + if (!cards[name]) name = "home"; + if (name === current) return; + var inc = cards[name], out = cards[current]; + var dir = order.indexOf(name) >= order.indexOf(current) ? "next" : "prev"; + if (name === "home") dir = "iris"; + inc.scrollTop = 0; + + if (reduceMotion) { + for (var k in cards) if (cards[k] !== inc) { cards[k].classList.remove("is-active"); cards[k].style.cssText = ""; } + inc.classList.add("is-active"); + } else { + inc.classList.add("is-active"); + inc.style.transition = "none"; + inc.style.opacity = "0"; + inc.style.transform = dir === "prev" ? "translateX(-36px)" : dir === "iris" ? "scale(.97)" : "translateX(36px)"; + void inc.offsetWidth; // reflow so the start state sticks + inc.style.transition = ""; + inc.style.opacity = "1"; + inc.style.transform = "none"; + if (out && out !== inc) { + out.style.transition = ""; + out.style.opacity = "0"; + out.style.transform = dir === "prev" ? "translateX(36px)" : dir === "iris" ? "scale(1.03)" : "translateX(-36px)"; + (function (o) { setTimeout(function () { o.classList.remove("is-active"); o.style.cssText = ""; }, 320); })(out); + } + } + current = name; + setChrome(name); + if (demo) { if (name === "home") demo.start(); else demo.stop(); } + + if (hist === "replace") history.replaceState({ card: name }, "", "#" + name); + else if (hist !== false) { try { history.pushState({ card: name }, "", "#" + name); } catch (e) { location.hash = name; } } + } + + function go(delta) { + var i = order.indexOf(current); + show(order[(i + delta + order.length) % order.length]); + } + + // wire nav + any [data-go] element + var navHome = document.getElementById("navHome"); + var navPrev = document.getElementById("navPrev"); + var navNext = document.getElementById("navNext"); + var winHome = document.getElementById("winHome"); + if (navHome) navHome.addEventListener("click", function () { show("home"); }); + if (winHome) winHome.addEventListener("click", function () { show("home"); }); + if (navPrev) navPrev.addEventListener("click", function () { go(-1); }); + if (navNext) navNext.addEventListener("click", function () { go(1); }); + + document.addEventListener("click", function (e) { + var t = e.target.closest ? e.target.closest("[data-go]") : null; + if (!t) return; + e.preventDefault(); + show(t.dataset.go); + if (links) { links.classList.remove("open"); if (toggle) toggle.setAttribute("aria-expanded", "false"); } + }); + + window.addEventListener("popstate", function (e) { + var name = (e.state && e.state.card) || (location.hash || "").replace("#", "") || "home"; + show(name, false); + }); + + /* ---------- The Message Box ---------- */ + var msgbox = document.getElementById("msgbox"); + var msgInput = document.getElementById("msgInput"); + var msgOut = document.getElementById("msgOut"); + var msgToggle = document.getElementById("msgToggle"); + function toggleMsg(force) { + var open = force !== undefined ? force : msgbox.hidden; + msgbox.hidden = !open; + if (msgToggle) msgToggle.textContent = open ? "Message ▾" : "Message ▸"; + if (open && msgInput) msgInput.focus(); + } + function say(s) { if (msgOut) msgOut.textContent = s; } + var ALIAS = { start: "start", "get started": "start", "getting started": "start", learn: "docs", + documentation: "docs", example: "examples", feature: "features", "what it is": "about", overview: "home" }; + function resolveCard(t) { + t = t.trim(); + if (cards[t]) return t; + for (var k in cards) if (titles[k].toLowerCase() === t) return k; + if (ALIAS[t]) return ALIAS[t]; + for (var k2 in cards) if (titles[k2].toLowerCase().indexOf(t) >= 0 || k2.indexOf(t) >= 0) return k2; + return null; + } + function runMsg(raw) { + var s = (raw || "").trim().toLowerCase(); + if (!s) return; + if (msgInput) msgInput.value = ""; + if (s === "help" || s === "?") { say("go to · next · prev · home · spawn · reset · gravity"); return; } + if (s === "home" || s === "go home") { show("home"); say(""); return; } + if (s === "next" || s === "go next") { go(1); say(""); return; } + if (s === "prev" || s === "back" || s === "go prev" || s === "go back") { go(-1); say(""); return; } + var m = s.match(/^(?:go to|go|find|show|open)\s+(?:card\s+)?["']?([a-z ]+?)["']?$/); + if (m) { var key = resolveCard(m[1]); if (key) { show(key); say(""); } else say('No card "' + m[1] + '".'); return; } + if (/spawn|crate|\bbox\b|drop|b2kspawn/.test(s)) { if (current !== "home") show("home"); if (demo) demo.drop(); say("+1 crate"); return; } + if (/reset/.test(s)) { if (current !== "home") show("home"); if (demo) demo.reset(); say("reset"); return; } + if (/gravity/.test(s)) { if (current !== "home") show("home"); var g = demo && demo.gravity(); say("gravity " + (g ? "on" : "off")); return; } + say('Can\'t understand "' + raw.trim() + '".'); + } + if (msgToggle) msgToggle.addEventListener("click", function () { toggleMsg(); }); + if (msgbox) msgbox.addEventListener("submit", function (e) { e.preventDefault(); runMsg(msgInput.value); }); + if (msgInput) msgInput.addEventListener("keydown", function (e) { if (e.key === "Escape") toggleMsg(false); }); + + /* ---------- Keyboard ---------- */ + document.addEventListener("keydown", function (e) { + if (e.metaKey || e.ctrlKey || e.altKey) return; + var tag = (e.target.tagName || "").toLowerCase(); + if (tag === "input" || tag === "textarea" || e.target.isContentEditable) return; + if (e.key === "ArrowRight") { go(1); e.preventDefault(); } + else if (e.key === "ArrowLeft") { go(-1); e.preventDefault(); } + else if (e.key === "Home" || e.key === "h" || e.key === "H") show("home"); + else if (e.key === "m" || e.key === "M") toggleMsg(); + }); + + /* ---------- Boot ---------- */ + setChrome("home"); + if (demo) demo.start(); + var initial = (location.hash || "").replace("#", ""); + if (initial && cards[initial] && initial !== "home") show(initial, "replace"); + else history.replaceState({ card: "home" }, "", "#home"); + } +})(); diff --git a/website/docs/getting-started.html b/website/docs/getting-started.html new file mode 100644 index 0000000..6f1c371 --- /dev/null +++ b/website/docs/getting-started.html @@ -0,0 +1,145 @@ + + + + + +Getting started — Box2Dxt docs + + + + + + + + + + + +
    + +
    +
    getting-started.md
    +

    Getting Started with Box2Dxt

    +

    This guide takes you from nothing to a running, draggable physics scene in OpenXTalk (OXT) — or any compatible LiveCode 9.6.3+ IDE. It assumes no C toolchain: you'll use a prebuilt native library.

    +

    Just want to play, not code? If you were handed the prebuilt platformer package (a zip with the extension, the native libraries, and a saved platformer.livecode), open its INSTALL.md and follow three steps — no scripting. This guide is for building your own scenes from scratch.

    + +
    +

    1. Get the native library

    +

    Box2Dxt is a real physics engine compiled to a native shared library. Grab the file for your platform from prebuilt/ (or from the Releases page for a specific version):

    +
    PlatformDownloadRename it to
    Windows x64prebuilt/box2dxt-windows-x64.dllbox2dxt.dll
    macOS (Intel/Apple Silicon)prebuilt/libbox2dxt-macos-universal.dylibbox2dxt.dylib
    Linux x86-64prebuilt/libbox2dxt-linux-x86_64.sobox2dxt.so
    +

    OXT's loader resolves the name box2dxt to the bare platform filename with no lib prefix (the table above). This bites on Linux especially: the committed file is libbox2dxt-linux-x86_64.so, but OXT asks dlopen for box2dxt.so — leaving the lib prefix on is the single most common cause of "unable to load foreign library".

    +

    Put the renamed file where the loader looks at run time:

    +
      +
    • Windows / macOS: next to the stack you're editing works.
    • +
    • Linux: the dynamic loader does not search the stack's folder. Copy the file to a search path and refresh the cache: `` sudo cp box2dxt.so /usr/lib/ && sudo ldconfig ` (or place it next to the OXT engine binary, or add its folder to LD_LIBRARY_PATH` before launching OXT).
    • +
    +

    If a particular engine asks for the lib-prefixed name instead, provide that too — a copy or symlink alongside is harmless.

    +

    Prefer building it yourself? See building.md. It's two cmake commands.

    +

    2. Load the extension

    +

    src/box2dxt.lcb is the extension that exposes the b2… handlers.

    +
      +
    • From the IDE: Tools → Extension Manager → add src/box2dxt.lcb, then Load it. (During development you can also use Tools → Extension Builder → Test to compile and load it in one step.)
    • +
    • From script: `` load extension from file (the defaultFolder & "/box2dxt.lcb") ``
    • +
    +

    Foreign bindings resolve on first use, so the native library only has to be findable when a b2… handler actually runs — not when the extension loads.

    +

    3. Sanity check

    +

    Open the Message Box and run:

    +
    put b2Version()
    +

    You should see 4 (the shim ABI version). If you get an error instead, the extension didn't load or the native library can't be found — jump to Troubleshooting.

    +

    4. Your first scene (the Kit)

    +

    The Kit (box2dxt-kit.livecodescript) is the friendly layer: you work in pixels, screen coordinates and degrees, and it owns the world, the fixed-timestep loop, and per-frame control updates.

    +
      +
    1. Open your stack's script (Object → Stack Script) and paste in the entire contents of src/box2dxt-kit.livecodescript. (For reuse across stacks, save it as a library stack and start using it.)
    2. +
    3. Add these handlers to the card script:
    4. +
    +
    on openCard
    +   b2kQuickStart                          -- world + gravity + card-edge walls + go
    +   b2kSpawnBall 200, 80, 50               -- create & drop a 50px ball
    +   b2kSpawnBox 260, 80, 60, 40, "orange"  -- (read `the result` for the ref)
    +end openCard
    +
    +on mouseDown
    +   get b2kGrab(the mouseH, the mouseV)    -- grab a body under the pointer
    +end mouseDown
    +
    +on mouseUp
    +   b2kRelease
    +end mouseUp
    +
    +on closeCard
    +   b2kStop                                -- stop the loop, free the world
    +end closeCard
    +

    Open the card. A ball and a box fall, settle on the bottom edge of the card, and you can drag them with the mouse. That's it — a live physics scene.

    +

    b2kQuickStart is the one-call setup: it creates the world, applies gravity, builds static walls around the card edges, and starts the loop. From there, b2kSpawnBall/b2kSpawnBox create controls and their bodies in one go.

    +

    Try a few more things in the Message Box while the card is open:

    +
    b2kSpawnCapsule 220, 60, 70, 28, "teal"   -- a pill-shaped body
    +put the result into tPill                  -- every b2kSpawn… reports its ref
    +b2kImpulse tPill, 0, -12                    -- a sharp upward kick (mass-aware)
    +b2kSpawnBox 320, 40, 50, 50, "purple"      -- drop another box in
    +

    Negative y is up here (screen coordinates). The Kit Reference lists the full spawn / force / query surface.

    +

    5. Attach controls you designed in the IDE

    +

    Prefer to draw your objects in the IDE? Attach physics to any control — graphics, images, buttons, fields. Graphics and dynamic images rotate with their body; buttons/fields and other controls follow position only (upright).

    +
    b2kSetup                                     -- world + gravity, auto origin
    +b2kAddStatic the long id of graphic "Floor"  -- immovable
    +b2kAddBox    the long id of graphic "Crate"  -- dynamic box
    +b2kAddBall   the long id of graphic "Ball"   -- dynamic circle
    +b2kContactTarget the long id of me           -- receive `on b2kContact pA, pB`
    +b2kStart                                      -- begin the loop
    +

    Now Crate and Ball fall and collide with Floor, and your card gets a b2kContact message whenever two attached controls begin touching.

    +

    See the Kit Reference for the full b2k… surface (materials, joints, forces, queries, events).

    +

    6. Run the full demo

    +

    examples/box2dxt-demo.livecodescript is a self-building, multi-scene testbed — it creates its own buttons, HUD and scenes at runtime, so there's nothing to lay out.

    +

    The demo is self-contained: it bundles a copy of the Kit, so it runs from a single paste — no separate setup.

    +
      +
    1. Make sure the extension is loaded and put b2Version() returns 4.
    2. +
    3. Paste the whole of examples/box2dxt-demo.livecodescript into a stack's script (Object → Stack Script). If you previously pasted a demo or the Kit into the card script, clear that first.
    4. +
    5. Reopen the card, or run startBox2DDemo in the Message Box (stopBox2DDemo stops it).
    6. +
    +

    The demo embeds a verbatim copy of the Kit purely for one-paste convenience. For your own projects, use the standalone Kit (src/box2dxt-kit.livecodescript) as shown in steps 4–5 above.

    +

    Click the tabs up top to switch scenes:

    +
    SceneShows off
    Playgroundboxes, a ball, a capsule, polygons, a hinged pendulum, a powered windmill (hinge motor), and a see-saw
    Pyramida tall stack; pick the Bomb tool and click beside it for a blast
    Cradlea Newton's cradle — hinge joints + restitution
    Bridgea plank bridge of hinge joints you can load up
    Vehiclea car (wheel joints + motor + spring suspension) you drive with ←/→ over bumps
    Lidara live ray-cast scanner that follows your mouse and stops each ray at the nearest shape
    +

    Across every scene you can drag any dynamic body, click empty space to drop the selected shape (Box/Ball/Capsule/Poly/Bomb), and bodies flash white the instant they begin touching (contact events). A HUD shows live FPS and body count. The whole demo is written with b2k… calls — read it as a worked example of the Kit.

    +

    Troubleshooting

    +
    SymptomLikely cause & fix
    b2Version() throws / "handler not found"The extension isn't loaded. Re-add and Load box2dxt.lcb in the Extension Manager.
    First b2… call (or b2Version()) errors "unable to load foreign library"The native library isn't found or is misnamed. Use the no-lib bare name: box2dxt.dll / box2dxt.dylib / box2dxt.so. On Linux the stack folder isn't searched — put it in /usr/lib then run sudo ldconfig, or place it next to the OXT engine. (Tip: launching OXT from a terminal prints dlopen failed <name> showing the exact filename it wants.)
    b2Version() returns a different numberYour box2dxt.lcb and native library are from different versions. Rebuild/redownload both from the same tag.
    Library won't load on an older PC (Linux/Windows)The CPU may lack AVX2. Build with -DBOX2D_DISABLE_SIMD=ON (see building.md), or grab a Release binary built for older CPUs.
    Bodies jitter or behave non-deterministicallyYou're stepping with a variable timestep. Let the Kit drive the loop, or step in fixed 1/60 s chunks (see API Reference → Notes).
    Objects fly off instantly / explodeSizes are wrong for Box2D's MKS units. Keep moving objects roughly 4–400 px at the default 40 px/m scale.
    +

    Where to go next

    +
      +
    • Kit Guide — the complete, teach-you-everything walkthrough of the b2k… toolkit, with runnable examples.
    • +
    • Kit Reference — the same b2k… API as quick-lookup tables.
    • +
    • Example games — beyond the demo, examples/ ships a full platformer, an angry-birds-style slingshot, and a contraption builder, each a single self-contained paste. Hand one to someone else as a zero-setup zip with tools/make-release.py.
    • +
    • API Reference — the low-level b2… API and units/gotchas.
    • +
    • Architecture — how it all works under the hood.
    • +
    • Building — compile the native library yourself.
    • +
    +
    +
    + + + + diff --git a/website/docs/index.html b/website/docs/index.html new file mode 100644 index 0000000..5c26132 --- /dev/null +++ b/website/docs/index.html @@ -0,0 +1,59 @@ + + + + + +Documentation — Box2Dxt docs + + + + + + + + + + + +
    + +
    +
    the manual
    +
    +

    The Box2Dxt manual

    +

    New here? You only need two things: Getting started to install Box2Dxt and drop your first body, then the Kit guide to learn the rest. Keep the Kit reference open while you build.

    + +

    Going deeper

    +

    The low-level b2… API, the engine internals, and build-from-source notes are for the curious — they live on GitHub:

    + +
    +
    +
    + + + + diff --git a/website/docs/kit-guide.html b/website/docs/kit-guide.html new file mode 100644 index 0000000..35cfce5 --- /dev/null +++ b/website/docs/kit-guide.html @@ -0,0 +1,661 @@ + + + + + +Kit guide — Box2Dxt docs + + + + + + + + + + + +
    + +
    +
    kit-guide.md
    +

    Box2Dxt Kit — The Complete Guide

    +

    A friendly, start-to-finish guide to the Box2Dxt Kit for xTalk (LiveCode / OpenXTalk) users. By the end you'll know how to drop physics onto any control, build joints and machines, react to collisions, filter what hits what, sculpt terrain, and tune the world — using pixels, screen coordinates and degrees the whole way.

    +

    Already know your way around? The terse lookup tables live in kit-reference.md. This document is the teaching version — read it top-to-bottom the first time, then keep the reference handy.

    +

    Contents

    +
      +
    1. What the Kit is (and why)
    2. +
    3. Install and your first scene
    4. +
    5. The mental model: coordinates & the loop
    6. +
    7. Making bodies: attach vs. spawn
    8. +
    9. Shapes and reshaping
    10. +
    11. Materials: bounce, friction, weight
    12. +
    13. Body settings
    14. +
    15. Making things move
    16. +
    17. Reading what's happening
    18. +
    19. Joints: building machines
    20. +
    21. Dragging with the mouse
    22. +
    23. Reacting to events
    24. +
    25. Sensors (trigger zones)
    26. +
    27. Collision filtering
    28. +
    29. Terrain and smooth chains
    30. +
    31. Asking the world questions (queries)
    32. +
    33. Tuning and performance
    34. +
    35. Dropping to the core b2… API
    36. +
    37. A complete worked example: a little car
    38. +
    39. Building a whole game (the micro-game pattern)
    40. +
    41. xTalk gotchas worth knowing
    42. +
    43. Complete API index
    44. +
    +
    +

    1. What the Kit is (and why)

    +

    box2dxt is a binding of the Box2D 3.x physics engine for LiveCode/OpenXTalk. The raw engine speaks metres, radians, and y-up — fine for an engine, awkward for an xTalk programmer used to pixels, screen coordinates, and degrees.

    +

    The Kit (src/box2dxt-kit.livecodescript) is a pure-xTalk layer on top that:

    +
      +
    • creates and owns the physics world,
    • +
    • runs a fixed-timestep loop and moves your controls each frame,
    • +
    • converts coordinates and angles for you, and
    • +
    • gives every handler a tidy b2k… name.
    • +
    +

    You give the Kit a control; it gives that control gravity, collisions, and the ability to be dragged. Three layers exist, and you can mix them:

    +
    LayerUnitsNamesUse it when…
    Kitpixels / degrees / screenb2k…almost always — start here
    Extension (b2…)metres / radians / y-upb2…you need something the Kit doesn't wrap
    Native shimC ABIb2lc_…never, directly
    +

    This guide is entirely about the Kit layer.

    +
    +

    2. Install and your first scene

    +

    Requirements: the box2dxt extension loaded. Check with put b2Version() — it should return 3. The Kit runs in OpenXTalk and LiveCode 9.6.3+.

    +

    Install: paste the contents of src/box2dxt-kit.livecodescript into your card or stack script. (Or save it as a library stack and start using it.)

    +

    Now the famous sixty-second scene — a ball and a box that drop, bounce, and can be flung with the mouse:

    +
    on openCard
    +   b2kQuickStart                         -- world + gravity + walls around the card + go
    +   b2kSpawnBall 200, 80, 50         -- create and drop a 50px ball
    +   b2kSpawnBox 260, 80, 60, 40, "orange"
    +   b2kContactTarget the long id of me    -- (optional) collision messages to this card
    +end openCard
    +
    +on mouseDown
    +   b2kGrab the mouseH, the mouseV   -- grab whatever body is under the pointer
    +end mouseDown
    +
    +on mouseUp
    +   b2kRelease
    +end mouseUp
    +
    +on closeCard
    +   b2kStop
    +end closeCard
    +

    b2kQuickStart is the one-liner that does everything: makes the world, sets gravity, builds static walls around the card edges, and starts the loop. The two b2kSpawn… calls each create a graphic and give it a body — we wrap them in get because they return the new control and we don't need the value here.

    +

    That's a complete, playable physics toy. Everything below is about doing more.

    +
    +

    3. The mental model: coordinates & the loop

    +

    Coordinates and angles

    +

    You always work in screen pixels and degrees, exactly like the rest of xTalk:

    +
      +
    • A position is x,y in card pixels (x right, y down).
    • +
    • An angle is in degrees, clockwise-positive (because y is down).
    • +
    • Sizes (a ball's diameter, a wall's length) are in pixels.
    • +
    +

    The Kit converts to Box2D's metres/radians internally using a scale of 40 pixels per metre by default. You rarely touch this, but you can:

    +
    b2kSetScale 50          -- 50 px = 1 metre (objects behave a touch "smaller/heavier")
    +b2kSetOrigin 0, 0       -- which screen point maps to the world origin (auto by default)
    +

    Keep objects a sensible size. At the default scale, dynamic objects behave best between roughly 4 px and 400 px. A 4000-px boulder or a 1-px pebble will feel wrong, just like in real Box2D.

    +

    The loop

    +

    Physics advances in a fixed-timestep loop the Kit runs for you. You start and stop it; you can pause, resume, and single-step it:

    +
    b2kStart                 -- begin stepping (b2kQuickStart already did this)
    +b2kPause                 -- freeze, but keep the world intact
    +b2kResume                -- carry on
    +b2kStepOnce              -- advance exactly one step (works even while paused) — great for a Step button
    +b2kStop                  -- end the loop
    +put b2kIsRunning()       -- true while actively stepping (not stopped, not paused)
    +

    Each frame, after the physics steps, the Kit moves every attached control to match its body and (optionally) sends you an on b2kFrame message — your hook for motors, input, scorekeeping, and custom drawing (see §12).

    +

    Tearing down. b2kStop ends the loop. b2kClear removes every body and Kit-spawned control but keeps the world. b2kTeardown destroys the world and all Kit state — call it before rebuilding a scene from scratch.

    +
    b2kClear                 -- empty the scene, keep the world running
    +b2kTeardown              -- nuke everything (world + state); next run starts fresh
    +
    +

    4. Making bodies: attach vs. spawn

    +

    There are two ways to get a physical object, and you'll use both.

    +

    A. Attach physics to a control you already designed

    +

    Lay out a graphic, image, button, or field in the IDE, then hand its reference to the Kit. Pass controls by the long id of … — it's the reference that stays valid even if names or layers change.

    +
    b2kSetup                                        -- world + gravity, auto origin
    +b2kAddStatic the long id of graphic "Floor"     -- never moves
    +b2kAddBox    the long id of graphic "Crate"     -- a dynamic box (the default)
    +b2kAddBall   the long id of graphic "Marble"    -- a dynamic circle
    +b2kStart
    +

    The attach handlers, and the shape each gives the control:

    +
    HandlerShape
    b2kAddBox ctrl [,dyn]rectangle (from the control's rect)
    b2kAddBall ctrl [,dyn]circle (from the control's width)
    b2kAddCapsule ctrl [,dyn]pill — long side is the axis, short side the diameter
    b2kAddPolygon ctrl [,dyn]convex polygon from a graphic's points
    b2kAddStatic ctrlimmovable body matching the control
    +

    The optional dyn flag forces dynamic (true) or static (false); it defaults to dynamic for the b2kAdd… shape handlers.

    +

    Any control type works — it falls, collides, and is draggable. What differs is how it's drawn to follow its body:

    +
    ControlFollows positionRotates
    Graphic (rectangle/oval/polygon)yesyes
    Image (dynamic)yesyes — via the angle
    Button / field / otheryesno — rotation is locked so the sim matches the upright render
    +

    B. Spawn a brand-new control with its body in one call

    +

    When you just want objects fast, let the Kit create the graphic too. Each returns the new control's long id.

    +
    local tBall, tBox
    +b2kSpawnBall 200, 80, 50              -- x, y, diameter [, color]
    +put the result into tBall
    +b2kSpawnBox 260, 80, 60, 40, "orange"  -- x, y, w, h     [, color]
    +put the result into tBox
    +b2kSpawnCapsule 320, 80, 80, 30, "120,200,232"   -- x, y, len, thick [, color]
    +

    The optional colour is a LiveCode colour name ("orange") or an "r,g,b" triple ("120,200,232"). Keep the returned reference if you want to act on the object later.

    +
    +

    5. Shapes and reshaping

    +

    A body's collision shape is set when you attach/spawn it. If you resize the control later (or want to switch shape), rebuild the shape on the same body so any joints attached to it survive:

    +
    set the width of graphic "Crate" to 120
    +b2kReshape the long id of graphic "Crate", "box"     -- "box" | "ball" | "capsule" | "poly"
    +

    b2kReshape reads the control's current size (or points, for "poly") and fits a fresh shape to it. It's atomic — the body is never momentarily shapeless, and a sensor stays a sensor across the reshape.

    +

    A reshape resets the shape's material and collision filter to defaults. If you'd set a custom bounce/friction/density or a collision layer, re-apply them after reshaping.

    +
    +

    6. Materials: bounce, friction, weight

    +

    Three knobs control how a surface behaves. Set them any time after the body exists:

    +
    b2kSetBounce   tBall, 0.8     -- restitution 0..1 (0 = dead, 1 = perfectly elastic)
    +b2kSetFriction tBox, 0.3      -- 0 = ice, 1 = grippy
    +b2kSetDensity  tBox, 2.0      -- mass per area; heavier bodies shrug off impulses
    +

    Density changes a body's mass, which is why a denser box is harder to push and pushes lighter things around more convincingly.

    +
    +

    7. Body settings

    +

    Beyond materials, each body has behaviour switches:

    +
    b2kSetBullet         tBall, true    -- continuous collision: a fast small body won't tunnel through walls
    +b2kSetFixedRotation  tBox,  true    -- never spin (good for characters/markers)
    +b2kSetGravityScale   tBalloon, -0.4 -- per-body gravity multiplier (negative floats it up)
    +b2kSetDamping        tBox, 0.5, 0.8 -- linear drag, and optional angular drag (air resistance)
    +

    Sleeping

    +

    Resting bodies "sleep" to save CPU and wake on contact. You can nudge this:

    +
    b2kWake  tBox                      -- force-wake a body
    +b2kSleep tBox                      -- send it to sleep until something disturbs it
    +b2kSetSleepThreshold tBox, 12      -- speed (px/s) below which it's allowed to nod off
    +b2kEnableSleeping true             -- world-wide on/off (on by default)
    +

    Body type — static, dynamic, kinematic

    +
      +
    • dynamic — moved by forces and collisions (the usual).
    • +
    • static — never moves; the world flows around it (floors, walls).
    • +
    • kinematicyou move it (via velocity); it shoves dynamic bodies but ignores gravity and collisions itself — perfect for moving platforms.
    • +
    +
    b2kSetStatic    tBox               -- freeze in place
    +b2kSetDynamic   tBox               -- unfreeze
    +b2kSetKinematic tPlatform          -- a driven platform
    +b2kSetType      tBox, "dynamic"    -- ...or set it by name: static | kinematic | dynamic
    +
    +b2kSetKinematic tPlatform          -- example: a platform that slides right forever
    +b2kSetVelocity  tPlatform, 60, 0
    +

    Taking a body out of the simulation

    +
    b2kDisable tBox     -- removed from the sim: no gravity, no collisions, stays put on screen
    +b2kEnable  tBox     -- put it back, exactly where it is
    +
    +

    8. Making things move

    +

    There's a verb for every way you might want to push something. The rule of thumb: impulses are one-shot, forces are continuous (call them every frame).

    +

    Linear

    +
    b2kPush         tBox, 0, -300    -- one-shot change in velocity (px/s): an instant shove. Ignores mass.
    +b2kImpulse      tBox, 0, -300    -- one-shot impulse, mass-aware: heavy things move less
    +b2kForce        tBox, 0, -50     -- continuous force — call each frame for thrust, wind, a tractor beam
    +b2kSetVelocity  tBox, 120, 0     -- hard-set the linear velocity (px/s)
    +

    Rotational

    +
    b2kSpin            tWheel, 360    -- set angular velocity (deg/sec): a full turn each second
    +b2kSpinBy          tWheel, 90     -- add to the current angular velocity
    +b2kTorque          tWheel, 500    -- continuous turning force — call each frame; sign sets direction
    +b2kAngularImpulse  tWheel, 200    -- one-shot turning impulse, mass-aware (the angular partner of b2kImpulse)
    +

    Teleport and blast

    +
    b2kMoveTo  tBox, 300, 120, 45    -- teleport to a screen point, optionally to an angle (deg)
    +b2kExplode 300, 200              -- radial blast at a point: kicks nearby dynamic bodies outward
    +b2kExplode 300, 200, 240, 1200   -- ...with explicit radius (px) and power (defaults 180, 900)
    +

    b2kExplode uses Box2D's native, shape-aware blast — a wide plank catches more of it than a small ball, and it affects every dynamic body in range. b2kExplodeLegacy reproduces the older, size-blind velocity kick if you want it.

    +

    Remove

    +
    b2kRemove tBox     -- destroy the body (and the control too, if the Kit spawned it)
    +
    +

    9. Reading what's happening

    +

    Every getter takes a control and returns screen-friendly values.

    +
    put b2kPosition(tBox)       -- "x,y" centre, in screen pixels
    +put b2kWorldCenter(tBox)    -- "x,y" centre of mass (the true pivot for spin/torque)
    +put b2kVelocity(tBox)       -- "vx,vy" in px/s
    +put b2kSpeed(tBox)          -- scalar speed (px/s)
    +put b2kAngle(tBox)          -- rotation in degrees
    +put b2kSpinRate(tBox)       -- rotation speed in deg/s
    +put b2kMass(tBox)           -- mass (sim kg)
    +put b2kBodyType(tBox)       -- "static" | "kinematic" | "dynamic"
    +put b2kIsAwake(tBox)        -- true if actively simulating
    +put b2kIsBullet(tBox)       -- continuous-collision flag
    +put b2kIsEnabled(tBox)      -- in the simulation?
    +put b2kGravityScale(tBox)   -- getter for b2kSetGravityScale
    +put b2kDamping(tBox)        -- "linear,angular" (getter for b2kSetDamping)
    +

    Scene-wide counts:

    +
    put b2kBodyCount()          -- bodies the Kit is tracking
    +put b2kAwakeCount()         -- dynamic bodies currently awake
    +

    Find a body by where it is on screen:

    +
    put b2kControlAt(the mouseH, the mouseV)              -- the control whose body covers a point (or empty)
    +put b2kControlContains(tBox, the mouseH, the mouseV)  -- is a point inside this body's actual (rotated) shape?
    +

    (Ray and region queries are in §16.)

    +
    +

    10. Joints: building machines

    +

    Joints connect two bodies (or one body to the world). Every constructor returns a joint handle — keep it, because motors, limits, springs, and read-outs all take that handle. Remove a joint with b2kRemoveJoint joint.

    +

    For the "to the world" variants, pass empty as the second control to pin the first body to a fixed point in space.

    +

    Hinge (revolute) — a pin things rotate around

    +
    local tArm, tPivot
    +b2kHinge tArm, empty, 200, 100     -- pin tArm to the world at (200,100): it swings freely
    +put the result into tPivot
    +-- or join two parts: b2kHinge tArmA, tArmB, x, y
    +b2kMotor      tPivot, 180, 1500                     -- drive it: 180 deg/s, max torque 1500 (default 1000)
    +b2kHingeLimit tPivot, -45, 45                       -- clamp the angle to a range (degrees)
    +put b2kHingeAngle(tPivot)                            -- read the current angle
    +b2kMotorOff      tPivot                              -- let it swing free again
    +b2kHingeLimitOff tPivot                              -- remove the limits
    +

    Weld — glue two bodies rigidly

    +
    local tJoint
    +b2kWeld tA, tB
    +put the result into tJoint
    +b2kWeldSpring tJoint, 4, 0.7      -- make the weld springy (hertz, damping); 0 hertz = rock-rigid
    + +
    local tRope
    +b2kRope tBall, tAnchor        -- length defaults to the current gap between centres
    +put the result into tRope
    +b2kRope tBall, tAnchor, 150              -- ...or set the length in pixels
    +b2kRopeRange     tRope, 40, 200               -- allow the length to vary between min/max (px)
    +b2kRopeSetLength tRope, 120                    -- set the exact rest length (px)
    +put b2kRopeLength(tRope)                        -- read the current length (px)
    +b2kSpring        tRope, 3, 0.6                 -- make it springy (hertz, damping) — a bungee
    +

    Slider (prismatic) — travel along an axis

    +
    local tSlide
    +b2kSlider tDoor, empty, 0     -- slide along a 0° (horizontal) axis vs. the world (90 = vertical)
    +put the result into tSlide
    +b2kSliderMotor tSlide, 80, 600                 -- drive it: 80 px/s, max force 600
    +b2kSliderLimit tSlide, 0, 160                  -- clamp the travel (px)
    +put b2kSliderPos(tSlide)                         -- read the current translation (px)
    +b2kSliderMotorOff tSlide
    +b2kSliderLimitOff tSlide
    +

    Wheel — a sprung, spinning axle (for vehicles)

    +
    local tAxle
    +b2kWheel tChassis, tWheel, the mouseH, the mouseV  -- pivot at a point; axis defaults to vertical
    +put the result into tAxle
    +b2kWheelMotor  tAxle, 600, 800       -- drive the wheel: 600 deg/s, max torque 800
    +b2kWheelSpring tAxle, 5, 0.7         -- suspension stiffness: hertz, damping
    +b2kWheelMotorOff tAxle
    +

    Motor-to — drive toward a pose (a soft, self-righting target)

    +

    b2kMotorTo drives one body toward a position/angle offset from another body (or from the world). Unlike a weld, it yields under load and springs back — for return-to-home arms, soft platforms, and self-righting parts.

    +
    -- hold tMover 0px across / 60px below tRef, upright, with limited force/torque:
    +local tServo
    +b2kMotorTo tMover, tRef, 0, 60, 0, 800, 800   -- ref empty = relative to the world
    +put the result into tServo
    +

    Stop two parts colliding without a visible joint

    +
    b2kNoCollide tA, tB     -- a filter joint: tA and tB simply pass through each other
    +

    Tip: a motor with no maxTorque/maxForce defaults to a strong value (1000 for hinges). If a motor "can't lift" its load, raise the max; if it's twitchy, lower it.

    +
    +

    11. Dragging with the mouse

    +

    b2kGrab attaches a temporary mouse joint to whatever body is under a point and returns that control (or empty). b2kRelease lets go. The body then follows the pointer until you release — springy and stable, exactly what you want for a toy.

    +
    on mouseDown
    +   if b2kGrab(the mouseH, the mouseV) is not empty then
    +      -- optionally remember/highlight the grabbed control
    +   end if
    +end mouseDown
    +
    +on mouseUp
    +   b2kRelease
    +end mouseUp
    +

    The Kit updates the grab target to the current mouse position automatically each frame, so you don't need mouseMove.

    +
    +

    12. Reacting to events

    +

    The Kit can send you messages as things happen. Point it at a receiver first:

    +
    b2kContactTarget the long id of me    -- gets contact + sensor messages
    +b2kFrameTarget   the long id of me    -- gets an on b2kFrame message each frame
    +

    Per-frame

    +

    on b2kFrame fires once per simulated frame, after bodies have moved — the right place to run motors, read input, update a HUD, or draw:

    +
    on b2kFrame
    +   -- e.g. keep a fan blowing while running
    +   if the mouse is down then b2kForce tBall, 0, -40
    +end b2kFrame
    +

    Contacts

    +
    on b2kContact pCtrlA, pCtrlB
    +   -- two attached controls just BEGAN touching.
    +   -- Either id is empty if that side is a wall, the ground, or an untracked body.
    +   beep
    +end b2kContact
    +
    +on b2kEndContact pCtrlA, pCtrlB
    +   -- ...and when they STOP touching.
    +end b2kEndContact
    +

    Polling instead of messages

    +

    Sometimes a tight on b2kFrame loop is cleaner than message handlers. Read this frame's touch pairs directly (indices are 1-based; an accessor is empty for a wall/ground/untracked body):

    +
    on b2kFrame
    +   local i
    +   repeat with i = 1 to b2kContactCount()
    +      -- b2kContactA(i) and b2kContactB(i) are the two controls that began touching
    +   end repeat
    +   -- and b2kEndContactCount() with b2kEndContactA(i)/b2kEndContactB(i)
    +end b2kFrame
    +

    Keyboard input (b2kInputOn)

    +

    Games need held keys (run while the arrow is down), chords (run + jump), and clean edges (jump on the frame the key goes down) — none of which the classic arrowKey/keyDown messages give you, because they arrive at the OS auto-repeat rate and stop the moment a field steals focus. The Kit's input module polls instead: while armed, it samples the keysDown once per frame and diffs it against the previous frame, so state and edges are exact and nothing depends on focus or the message path.

    +
    on openCard
    +   b2kQuickStart
    +   b2kSpawnCapsule 200, 100, 64, 30, "gold"
    +   put the result into gHero
    +   b2kSetFixedRotation gHero, true     -- a character stays upright
    +   b2kInputOn                          -- arm the sampler (installs defaults)
    +   b2kBindAction "jump", "space,up,w"  -- any of these counts as "jump"
    +   b2kFrameTarget the long id of me
    +end openCard
    +
    +on b2kFrame
    +   -- axis: -1 / 0 / +1 from left,a vs right,d (a default binding)
    +   b2kSetVelocity gHero, b2kAxis("moveX") * 240, item 2 of b2kVelocity(gHero)
    +   -- edge: true only on the frame the action went down
    +   if b2kActionPressed("jump") then b2kPush gHero, 0, -420
    +end b2kFrame
    +

    Key names are friendly ("left", "space", "a"; raw keycodes also work), and letters match both their shifted and unshifted codes. Read anything per frame: b2kKeyIsDown/Pressed/Released for single keys, b2kActionIsDown/Pressed/Released for named sets, b2kAxis for paired directions (both held = 0), b2kKeysHeld() for a debug HUD, and b2kFrameMS() for the frame's real elapsed milliseconds (drive animations from that, never the step count). The examples/box2dxt-platformer.livecodescript stack is this section turned into a playable scene — grounded checks, variable-height jumps, and a jump-through ledge included.

    +

    Sprites and spritesheets (b2kSheetLoadAtlas, b2kSpriteNew)

    +

    A sheet registers the frames inside one image — either a uniform grid (b2kSheetLoad name, path, frameW, frameH) or a packed atlas whose XML names each region (b2kSheetLoadAtlas; the Spritesheets/ folder in this repo is that format). A sprite is a transparent button the Kit drives: name animations once, then play them by name — b2kSpritePlay is free to call every frame, so a state machine stays one line per state.

    +
    b2kSheetLoadAtlas "chars", tFolder & "/spritesheet-characters-default.png"
    +b2kAnimDef "chars", "idle", "character_beige_idle", 2, true
    +b2kAnimDef "chars", "walk", "character_beige_walk_a,character_beige_walk_b", 6, true
    +
    +b2kSpriteNew "chars", "character_beige_idle", 200, 100
    +put the result into gHeroSpr
    +
    +on b2kFrame
    +   -- the body's state picks the animation; flipping mirrors the art
    +   if b2kAxis("moveX") is 0 then
    +      b2kSpritePlay gHeroSpr, "idle"
    +   else
    +      b2kSpritePlay gHeroSpr, "walk"
    +      b2kSpriteFlipH gHeroSpr, (b2kAxis("moveX") < 0)
    +   end if
    +end b2kFrame
    +

    A sprite is an ordinary control: give it a body directly (b2kAddCapsule the long id of …), or — the better pattern for characters, whose art is bigger than their collision shape — give an invisible control the body and b2kSpriteBind the sprite to it. Non-looping animations can announce themselves: b2kSpriteOnFinish gHeroSpr, "heroHitDone" sends your handler a message when the hit/attack/death pose finishes. The platformer example wires all of it together: an atlas-driven hero, spinning coin pickups, a bee on a flight path, and a saw hazard that triggers the hit-then-respawn chain.

    +

    Not a Kenney sheet? Every layout loads. Grids with borders or gutters pass them straight in (b2kSheetLoad "run", tPath, 32, 48, 0, 2, 1 — 2px margin, 1px spacing). A packed sheet with no XML at all loads with frame size 0 (no grid) and you name each region yourself:

    +
    b2kSheetLoad "boss", tPath, 0, 0          -- source only, no grid
    +b2kSheetAddFrame "boss", "idle", 0, 0, 96, 80
    +b2kSheetAddFrame "boss", "roar", 96, 0, 128, 80
    +b2kAnimDef "boss", "wake", "idle,roar", 4, false
    +

    b2kSheetFrameNames("chars") lists every frame key of a sheet you didn't make — the quickest way to find what an atlas calls things.

    +

    The player controller (b2kPlayerMake)

    +

    Everything the input and sprite snippets above hand-roll — and the parts everyone gets wrong the first time — exists as one module. A player is a vertical capsule with fixed rotation, sleep disabled and low friction; every frame the controller reads the axis moveX and the action jump, accelerates vx toward axis × moveSpeed, probes the ground with three short rays (a hit counts only while its surface normal is within maxSlopeDeg of straight up, so slopes walk and walls don't), and picks the matching animation. Jump feel is built in: coyote time (a jump still fires ~90 ms after running off a ledge), jump buffering (pressed just before touchdown fires on landing), and jump-cut (tap = hop, hold = full height).

    +
    on openCard
    +   b2kQuickStart
    +   b2kSheetLoadAtlas "chars", tFolder & "/spritesheet-characters-default.png"
    +   b2kAnimDef "chars", "idle", "character_beige_idle", 2, true
    +   b2kAnimDef "chars", "walk", "character_beige_walk_a,character_beige_walk_b", 6, true
    +   b2kAnimDef "chars", "jump", "character_beige_jump", 1, true
    +   b2kPlayerMake 200, 100, 32, 48, "chars"   -- body + sprite + controller
    +   put the result into gHero
    +   b2kPlayerAnims "idle", "walk", "jump", "jump"
    +   b2kFrameTarget the long id of me
    +end openCard
    +

    That is a complete, well-tuned character — arrows/WASD run, space jumps, DOWN ducks, one-way decks drop through, ladders climb, water swims (§21). Feel lives in b2kPlayerSet knobs (moveSpeed, accel, airAccel, jumpSpeed, jumpCut, coyoteMs, bufferMs, maxFall, maxSlopeDeg, plus Wave 2's dropMs, climbSpeed, hurtPopX/Y, hurtMs, invulnMs, and Wave 4's swimSpeed/swimJump/swimGravity/swimMaxFall); read the character back with b2kPlayerState() (idle/run/jump/fall/duck/climb/hurt/swim, plus land for exactly one frame on touch-down — perfect for dust and sound), b2kPlayerOnGround() and b2kPlayerFacing(). Already have a body or sprite? b2kPlayerAttach adopts it instead of making one. Springs, bounces and powerups call b2kPlayerJump 700 — always use it for external boosts: a raw upward b2kSetVelocity on a grounded player is treated as solver rebound and snapped flat. Contact damage calls b2kPlayerHurt (the knockback standard, §21); for cutscenes and scripted deaths, b2kPlayerControl false makes the controller observe only — your code owns velocity and animations until you hand control back. Under the hood the controller guarantees consistent feel (sim-time reaction windows, dead landings, hysteresis against solver blips) — all of it asserted by the self-test harness. The platformer example's whole movement system is the four lines above.

    +

    Sound effects (b2kToneMake, b2kSound)

    +

    Sounds are named audioClips — the engine plays one at a time (a new play cuts the previous), which is exactly right for short retro SFX. You can import files (b2kSoundLoad "boom", tPath), but the fun path needs no files at all: b2kToneMake synthesizes a clip from a list of note frequencies, square (retro) or sine (soft), with a per-note decay.

    +
    b2kToneMake "jump", "392,587", 40          -- a quick up-chirp
    +b2kToneMake "coin", "1319,1760", 36, 45    -- a bright blip
    +b2kToneMake "win", "523,659,784,1047", 110 -- a four-note fanfare
    +
    +-- hooks: the player's land state, your sensors, your win handler
    +on b2kFrame
    +   if b2kPlayerState() is "land" then b2kSound "land"
    +end b2kFrame
    +

    b2kSoundMute true silences everything (a preference — it survives b2kTeardown); b2kSoundVolume drives the engine-global loudness. On an engine with no working audio the Kit degrades to silence rather than errors — check b2kSoundStatus() if you hear nothing. The platformer's eight cues are all synthesized; press M in it to mute.

    +
    +

    13. Sensors (trigger zones)

    +

    A sensor is a non-solid fixture: bodies pass straight through it, but it reports the overlap. Perfect for tripwires, goals, and pickup zones. The Kit enables sensor events on every body it creates, so sensors detect them automatically.

    +
    b2kAddSensor the long id of graphic "Goal"          -- a static box sensor (shape: "box" | "ball" | "capsule")
    +b2kAddSensor the long id of graphic "Ring", "ball"
    +

    You can also flip an existing solid body into a sensor and back — the Kit rebuilds its shape, keeping the new sensor state:

    +
    b2kSetSensor tBox, true     -- tBox becomes a non-solid trigger
    +b2kSetSensor tBox, false    -- ...solid again
    +

    React with messages (sent to your b2kContactTarget):

    +
    on b2kSensorEnter pSensorCtrl, pVisitorCtrl
    +   put "Goal!" into field "status"
    +end b2kSensorEnter
    +
    +on b2kSensorExit pSensorCtrl, pVisitorCtrl
    +end b2kSensorExit
    +

    …or poll this frame's overlaps (1-based):

    +
    put b2kSensorCount()              -- enters this frame
    +put b2kSensorEnterSensor(1)       -- the sensor control of the 1st enter
    +put b2kSensorEnterVisitor(1)      -- the body that entered it
    +put b2kSensorExitCount()          -- leaves this frame
    +put b2kSensorExitSensor(1)        -- ...and b2kSensorExitVisitor(1)
    +

    One sensor, many visitors. Each enter/exit fires per body. If you want "fire once while occupied," count enters minus exits yourself and act on the 0→1 and 1→0 transitions.

    +
    +

    One-shots vs. presence. Enter/exit messages are perfect for one-shot triggers — a coin is removed on first fire, a checkpoint sets a flag. But for presence (a pressure plate that must stay pressed while anything sits on it), don't count enters minus exits: counting drifts, and Box2D's sensor begin/end around settling and sleeping bodies is exactly the edge a plate lives on. Poll instead — if b2kOverlap(x1,y1,x2,y2) is not empty each frame is stateless and still sees sleeping bodies — but use b2kOverlapMoving: the pad region sits on its floor, and the broadphase's fattened boxes make a plain b2kOverlap report the floor itself, forever. Add a short release debounce (~200 ms) so a settling crate's micro-bounces don't flap your door.

    +

    14. Collision filtering

    +

    By default everything collides with everything. Three independent tools change that.

    +

    Named layers (categories & masks)

    +

    The flexible system: a body is on one or more layers (its category), and it collides with a set of layers (its mask). Two bodies touch only when each one's category is in the other's mask. Up to 32 layers; name them or use numbers.

    +
    b2kDefineLayer "enemies"     -- define/fetch a named layer (returns its bit) — optional; names auto-define
    +b2kSetCategory tGhost, "enemies"             -- tGhost IS an "enemy"
    +b2kSetMask     tGhost, "walls,player"        -- ...and only collides with walls and the player
    +

    Pass layers as a comma/space list of names or numbers. (b2kLayerBits is the helper that turns such a list into a bitmask if you ever need the raw value.)

    +

    Groups — a quick "these ignore each other"

    +

    A simpler override: bodies sharing a negative group never collide; a positive group always collide. 0 (the default) means "use category/mask."

    +
    b2kSetCollisionGroup tWheelA, -1     -- everything on group -1 passes through everything else on -1
    +b2kSetCollisionGroup tWheelB, -1
    +

    Just these two

    +
    b2kNoCollide tArm, tBody)    -- exempt one specific pair (a filter joint
    +
    +

    15. Terrain and smooth chains

    +

    Stacked boxes make lumpy ground that fast bodies can catch on. A chain is a single smooth surface built from a list of x,y screen points (≥ 4) — no inner corners to snag. Chains are invisible; draw a matching graphic over them.

    +
    local tPts
    +-- six points = four segments, of which the OUTER TWO are ghost anchors
    +-- (see below): the solid ground here runs from 520,360 back to 80,400
    +put "600,380" & cr & "520,360" & cr & "360,420" & cr & "200,360" & cr & "80,400" & cr & "20,400" into tPts
    +b2kChain tPts               -- an open smooth ground line
    +b2kChain tPts, true         -- pass true to close it into a loop (all solid)
    +b2kSmoothGround tPts        -- alias for an open chain
    +

    To make a chain that tracks a control (so you can move/draw the terrain as one graphic), give it the control plus the points in the control's own outline:

    +
    b2kAddChain the long id of graphic "Hill", the points of graphic "Hill", true
    +

    Winding matters. A chain's solid side is to the right of the direction the points travel. For ground you stand on, list the top surface right-to-left so the solid side faces up. (If bodies fall through your terrain everywhere, reverse the point order.)

    +

    The ghost rule. An open chain's first and last segments are Box2D's ghost anchors — they smooth the junctions but don't collide (N points ⇒ N−3 solid segments). Always run the chain one segment past the surface you need on each side; over solid ground the tails can just continue flat. If bodies fall through your platform only near its ends, this is why — the chain's endpoints are sitting at the platform's edges. Closed loops (pLoop true) have no ends, so every segment is solid.

    +
    +

    16. Asking the world questions (queries)

    +

    Point

    +
    put b2kControlAt(x, y)                 -- control whose body covers a point (empty if none)
    +put b2kControlContains(tBox, x, y)     -- is a point inside tBox's actual rotated shape?
    +

    Region

    +
    put b2kOverlap(100, 100, 300, 240)     -- newline list of controls overlapping a screen rect
    +put b2kOverlapCircle(200, 170, 60)     -- ...overlapping a screen circle (x, y, radius)
    +

    Ray casts

    +

    A single closest hit, then read the result functions:

    +
    if b2kRayHit(0, 200, 600, 200) then
    +   put b2kRayHitX() & "," & b2kRayHitY()           -- the hit point (screen px)
    +   put b2kRayHitNormalX() & "," & b2kRayHitNormalY()-- the surface normal at the hit
    +   put b2kRayDist()                                  -- distance from the ray start (px)
    +end if
    +

    Or every control a ray crosses, nearest-first:

    +
    put b2kRayHitAll(0, 200, 600, 200)     -- newline list, closest first
    +
    +

    17. Tuning and performance

    +

    Most scenes never need these, but they're here when you want a specific feel or a performance HUD.

    +

    Solver feel

    +
    b2kSetSubsteps 4                 -- solver sub-steps per step (higher = more stable, more CPU)
    +b2kSetRestitutionThreshold 30    -- speed (px/s) below which bounces are killed (less jitter)
    +b2kSetContactTuning 30, 10, 5    -- contact stiffness: hertz, damping, max push-out (px)
    +b2kSetJointTuning   60, 2        -- default joint stiffness: hertz, damping
    +b2kSetMaxSpeed 1800              -- clamp how fast any body may move (px/s)
    +b2kEnableWarmStarting true       -- reuse last frame's solution (on by default; faster + stabler)
    +b2kEnableContinuous true         -- world-wide continuous collision (CCD)
    +b2kSetGravity 0, 600             -- change gravity any time (px-down is positive)
    +

    Measuring

    +
    put b2kProfile()           -- "totalStep,collide,solve" ms for the last step — a perf HUD
    +put b2kAwakeBodyCount()    -- awake dynamic bodies (native count)
    +

    Performance habits the Kit already follows: sleeping is on, the renderer syncs from Box2D body-move events instead of scanning every body each frame, angle reads are skipped for non-rotating controls, pixel-identical redraws are skipped, joint markers that haven't moved aren't redrawn, the sprite tick walks only bound/playing sprites (a hundred static tiles cost nothing per frame), input bindings resolve their keycodes at bind time, and the player tick reads pre-baked tuning over raw body handles. Keep sleeping enabled, avoid heavy work every on b2kFrame, and big scenes stay smooth.

    +

    Performance habits for YOUR game code (the engine is a single interpreted thread, and every property set risks a redraw):

    +
      +
    1. Throttle your HUD. Setting a field's text re-lays-out and redraws it — a readout that changes every frame costs a redraw every frame. Update HUDs at ~4 Hz (if the milliseconds < gHudNextMS then …), and still skip the set when the text is unchanged. Both game examples do this.
    2. +
    3. Write properties and velocities only on change. Track the last value you applied (the platformer's gate writes its kinematic velocity only when the target flips).
    4. +
    5. Read the clock once per handler, not once per entity.
    6. +
    7. Build heavy things once. Sounds survive b2kTeardown; tiles are create-at-level-build; sheets slice lazily and share frames.
    8. +
    +
    +

    18. Dropping to the core b2… API

    +

    When you need something the Kit doesn't wrap, reach through to the extension and mix both layers freely:

    +
    put b2kWorld()                 -- the underlying world handle
    +put b2kBodyOf(tBox)            -- the underlying b2… body handle for a control
    +put b2kToWorldX(the mouseH)    -- screen px -> Box2D metres (and b2kToWorldY)
    +put b2kToScreenX(2.5)          -- Box2D metres -> screen px (and b2kToScreenY)
    +

    With the world and a body handle plus the converters, every raw b2… call (see api-reference.md) is available — set an exotic shape property, then let the Kit keep drawing the control each frame.

    +
    +

    19. A complete worked example: a little car

    +

    Putting it together — a two-wheeled car on smooth ground that you drive with the arrow keys. Paste into a card script with the Kit installed.

    +
    local sCar, sWheelL, sWheelR, sAxleL, sAxleR
    +
    +on openCard
    +   b2kSetup                                  -- world + gravity, auto origin
    +   buildGround
    +   buildCar
    +   b2kFrameTarget the long id of me          -- we'll drive the wheels in on b2kFrame
    +   b2kStart
    +end openCard
    +
    +on closeCard
    +   b2kStop
    +end closeCard
    +
    +on buildGround
    +   local tPts
    +   -- top surface listed right-to-left so the solid side faces up
    +   put "620,360" & cr & "420,330" & cr & "220,370" & cr & "20,340" into tPts
    +   b2kChain tPts                         -- invisible smooth ground
    +   -- (draw a matching graphic over it if you want it visible)
    +end buildGround
    +
    +on buildCar
    +   b2kSpawnBox 200, 120, 90, 30, "70,130,210"
    +   put the result into sCar
    +   b2kSetDensity sCar, 1.0
    +   b2kSpawnBall 168, 150, 36, "30,30,36"
    +   put the result into sWheelL
    +   b2kSpawnBall 232, 150, 36, "30,30,36"
    +   put the result into sWheelR
    +   b2kSetFriction sWheelL, 1.0
    +   b2kSetFriction sWheelR, 1.0
    +   -- sprung axles that can also be driven
    +   b2kWheel sCar, sWheelL, 168, 150
    +   put the result into sAxleL
    +   b2kWheel sCar, sWheelR, 232, 150
    +   put the result into sAxleR
    +   b2kWheelSpring sAxleL, 6, 0.7
    +   b2kWheelSpring sAxleR, 6, 0.7
    +end buildCar
    +
    +on b2kFrame
    +   local tDrive
    +   put 0 into tDrive
    +   if the keysDown contains 124 then put 700 into tDrive    -- right arrow
    +   if the keysDown contains 123 then put -700 into tDrive   -- left arrow
    +   b2kWheelMotor sAxleL, tDrive, 900
    +   b2kWheelMotor sAxleR, tDrive, 900
    +end b2kFrame
    +

    That's a complete vehicle: a chassis, two sprung-and-driven wheels on wheel joints, smooth chain terrain, and a per-frame motor driven by the keyboard.

    +
    +

    20. Building a whole game (the micro-game pattern)

    +

    The micro-game pattern is the recommended skeleton for a green-field game on the Kit: a complete game — start screen, levels, a win screen — in a few hundred lines of card logic, with nothing to install beyond the extension (embed the hero sheet as base64; synthesize every sound with b2kToneMake). A dedicated micro-game example once shipped this verbatim; the repo now concentrates its game work on the platformer showcase, but the pattern below is exactly the one to copy when you start your own game. Its skeleton is four ideas:

    +

    1. A game-state machine, gated by b2kPlayerControl. One gMode local (menu / play / won) decides what clicks and keys mean. The world is built and running behind the menu — the hero idles, sweepers patrol — but b2kPlayerControl false means the keys do nothing until mgBegin hands them over. Hit poses and the win screen reuse the same switch.

    +

    2. Levels are data; the interpreter is yours. Each level is a few lines of text, one verb per line:

    +
    bounds 1024,640
    +spawn 110,500
    +slab 0,576,1024,640
    +ledge 620,860,420
    +coin 460,448
    +spike 250,330,560
    +door 945,478
    +

    …and mgBuild is a ~100-line switch that tears the world down (b2kClear + b2kTeardown), interprets the lines, then makes the player and hands the camera its bounds. Verbs are cheap — when your game needs a new object, add a case and a line format. This is the Kit's intended scene pattern: the format belongs to your game, the heavy lifting (bodies, sprites, camera, controller) is already API. Two details worth stealing: the ledge verb ghost-pads its chain automatically (see §15), and door is just a sensor plus a gDoorOpen flag the frame hook flips when the coin count is full.

    +

    3. One call makes the player. b2kPlayerMake gSpawnX, gSpawnY, 32, 56, "hero" creates the capsule body host, the bound sprite, the controller, and arms input. After it: map the anims, set two tuning knobs, b2kCamFollow. The micro-game's whole "character system" is six lines.

    +

    4. Game events ride the hooks you already have. Coins/spikes/door are sensors (on b2kSensorEnter); landing and jump sounds key off b2kPlayerState() in on b2kFrame; respawn is a non-looping hit animation whose b2kSpriteOnFinish message teleports the hero home. No new machinery — a game is the Kit's events plus your rules.

    +

    Play order: openCard builds level 1 and shows the menu → click → mgBegin → door (all coins) → mgAdvance → level 2 → door → mgShowWin → click → back to level 1. R rebuilds the current level, ESC pauses, M mutes.

    +
    +

    21. Player actions: duck, drop-through, ladders, knockback, swim

    +

    Wave 2 builds four standard platformer verbs into the controller. They cost nothing until used (each idles at one compare per frame) and they compose — a drop-through can fall into a ladder grab; a knockback ends a climb and restores gravity itself.

    +

    Duck needs no setup: DOWN while grounded brakes the player to a stop and shows the duck anim slot (b2kPlayerAnims's sixth argument; it falls back to the idle pose). The hitbox does not shrink this wave — you cannot duck under a saw yet; capsule reshaping is scheduled with Wave 5. Say so in your help text if your level dangles something head-high.

    +

    Drop-through works on every b2kChain/b2kSmoothGround deck automatically — chains carry a reserved collision category, and DOWN+JUMP while standing on one masks that category off the player for dropMs (~260 ms), long enough to fall clear; the deck is solid again on the next landing. On solid ground the same press just ducks (and is eaten — no buffered launch when DOWN releases). Two level-design rules: the deck needs head-room below (a solid platform parked less than a player-height under a one-way deck means the drop can't clear it — the mask is restored by a hard deadline and the player may pop back on top), and remember the ghost rule (§15) so the deck's ends are solid in the first place.

    +

    Ladders are zones, not bodies: b2kPlayerAddLadder x1,y1,x2,y2 registers a screen-px rect; presence is a pure per-frame poll (the presence doctrine — no sensors, no contacts). In-zone, UP enters the climb state: gravity parks at 0, y runs at climbSpeed off the moveY axis (neither held = hang), x at half moveSpeed. JUMP exits with a normal jump; sliding out of the zone or climbing down onto ground restores gravity. DOWN grabs the ladder only while airborne — a grounded DOWN is a duck — so to descend from a platform top, run the zone a little above the platform and walk off its edge holding DOWN. Draw your own rungs (a few lines, or the tiles sheet's ladder_* frames); zones are world state, wiped by b2kClear with everything else.

    +

    Knockback (b2kPlayerHurt fromX) is the contact-damage standard the games share. It pops the player away from fromX (hurtPopX/hurtPopY, exempt from the ground-snap), holds the hurt state with input suppressed until hurtMs or the first landing after half of it (whichever is later), then opens an invulnMs mercy window during which b2kPlayerHurt no-ops and b2kPlayerHurtIs() answers true — gate your hazard checks on it. The split that makes games feel right: contact damage knocks back in place; lethal hits (pits, kill planes) keep your respawn flow. Your respawn's b2kPlayerControl false call also cancels any knockback in flight, so the two paths hand over cleanly when a knockback ends in a pit. One art note: if your game uses b2kSpriteOnFinish on the player's sprite (the respawn-on-finish pattern), map a looping animation to the hurt anim slot — a non-looping pose would finish mid-knockback and fire your respawn.

    +

    Swim (Wave 4) is the buoyant parallel to the climb. b2kPlayerAddWater x1,y1,x2,y2 registers a water zone (polled presence, world state, wiped by b2kClear like a ladder). While the player's centre is submerged the swim state owns the controller: gravity scales to swimGravity (the body's own scale is saved and restored, so a game-tuned floatiness survives), the sink caps at swimMaxFall, UP/DOWN swim at swimSpeed, and a JUMP press is a repeatable upward stroke of swimJump — no grounded/coyote/buffer gate. Leaving the zone or a hurt restores gravity; swim and climb are mutually exclusive (the tick starts only one). Map a swim frame with b2kPlayerAnims's ninth argument (it falls back to the fall/jump pose, so a sheet without one still reads).

    +

    Two things the OXT rounds taught: (1) tuningswimGravity sets only how fast you sink between strokes; the single-stroke escape height is swimJump ALONE (the stroke sets velocity directly, then full air-gravity governs the apex once you break the surface), so to make climbing out of a pool harder, lower swimJump, not the gravity. (2) layout — a swim pool can't be a pit below the ground, because b2kCamBounds clamps the camera at the world's bottom edge and anything lower is off-screen; build the pool as a RAISED basin between two banks (or raise the whole ground), then hop in, dive for the coins, and stroke up + hold-forward to hop out the far bank.

    +

    Wave 5 actions: double-jump, wall-jump, dash, crawl, carry

    +

    The Wave 5 moves are all opt-in through b2kPlayerSet knobs — the defaults leave the controller exactly as the Wave 2/4 chapters describe, and each idle path costs one compare per frame, so you only pay for what you turn on. Turn them on for a modern-feeling platformer:

    +
    b2kPlayerSet "airJumps", 1            -- a second jump in mid-air (double-jump)
    +b2kPlayerSet "wallJumpX", 240         -- wall-slide + wall-jump (away + up)...
    +b2kPlayerSet "wallJumpY", 430
    +b2kPlayerSet "wallSlideMax", 120      -- ...the slowed slide down a wall
    +b2kPlayerSet "dashSpeed", 560         -- DASH on the "dash" action (SHIFT/X)
    +b2kPlayerSet "duckScale", 0.6         -- DOWN now CRAWLS under a low gap
    +b2kPlayerSet "platformCarry", 1       -- ride a moving kinematic platform
    +
      +
    • airJumps is the air-jump budget, refilled on every landing — 1 is a classic double-jump, 2 a triple. (For a powerup double-jump, grant it with b2kPlayerJump from a sensor instead.)
    • +
    • Wall moves arm when wallJumpX > 0 (or wallSlideMax > 0). While airborne and pressing INTO a wall you wallslide (the fall caps at wallSlideMax); JUMP launches up and away with a brief steer-lock so the launch carries clear. wallJumpY falls back to jumpSpeed.
    • +
    • Dash is a flat horizontal burst (gravity parked) on the dash action — bound to SHIFT/X by default; rebind with b2kBindAction "dash", …. It runs dashMs, then dashCooldownMs must pass before the next, and it yields to climb/swim.
    • +
    • duckScale < 1 turns the Wave 2 brake-duck into a real crawl: the capsule reshapes (feet-anchored) to that fraction of its standing height, so you fit under a low overhead, and stands back up only when there's headroom. Read the live half-height with b2kPlayerHalfH() for any head-reach logic, and size crawl gaps against it.
    • +
    • platformCarry 1 makes a grounded player inherit the velocity of the moving kinematic body under it — so a moving platform carries you instead of sliding out from under. The platform must move by velocity (a kinematic body with b2kSetVelocity), not by position, because carry reads that velocity; flip the velocity at the patrol endpoints (write-on-change).
    • +
    +

    New conveniences: b2kPlayerHalfH()/b2kPlayerHalfW() (live capsule extents), b2kPlayerInLadder()/b2kPlayerInWater() (this frame's zone membership, for prompts/effects), and b2kPlayerRespawn x, y (teleport + zero velocity + clean state in one call). The platformer example is the marquee showcase for all of these.

    +
    +

    22. xTalk gotchas worth knowing

    +

    A few things that trip up LiveCode/OpenXTalk users specifically:

    +
      +
    • Pass controls by the long id of …. Short names break if you rename or re-layer; long ids stay valid. Every ctrl parameter wants a reference.
    • +
    • Identifiers are case-insensitive — dodge reserved words. xTalk treats players, Players, and pLayers as the same name, and many words (type, name, layer, number, time, id, mode…) are reserved. The Kit prefixes everything (b2k…, internal s…); prefix your variables too (tBox, gScore) so you never collide with a keyword.
    • +
    • get vs. put for functions that return. b2kSpawn…, b2kGrab, and the joint constructors return a value. Use put … into tVar to keep it, or get … to discard it. Calling them as a bare statement is a syntax error.
    • +
    • Custom properties stick to objects. A handy pattern is to stash per-object data as set the uColor of tBox to … and read it back later — the Kit and the examples use u… custom properties throughout.
    • +
    • One world at a time. The Kit owns a single world. b2kTeardown before you build a fresh scene, or b2kClear to empty the current one.
    • +
    +
    +

    23. Complete API index

    +

    Every public handler, grouped. [f] marks a function (returns a value — call it with () / get / put); everything else is a command (a statement). Optional arguments are in […].

    +

    World & lifecycle

    +

    b2kSetup [gx, gy] · b2kQuickStart [gy] · b2kStart · b2kStop · b2kPause · b2kResume · b2kStepOnce · b2kIsRunning() [f] · b2kAddWalls · b2kAddGround [screenY] · b2kWall x1,y1,x2,y2 · b2kClear · b2kTeardown · b2kVersion() [f] · b2kWorld() [f]

    +

    Configuration & coordinates

    +

    b2kSetScale px · b2kSetOrigin x,y · b2kSetGravity gx,gy · b2kSetSubsteps n · b2kContactTarget obj · b2kFrameTarget obj · b2kEnableSleeping flag · b2kEnableContinuous flag · b2kToScreenX(m) [f] · b2kToScreenY(m) [f] · b2kToWorldX(px) [f] · b2kToWorldY(px) [f]

    +

    Attach & spawn

    +

    b2kAddBox ctrl [,dyn] · b2kAddBall ctrl [,dyn] · b2kAddCapsule ctrl [,dyn] · b2kAddPolygon ctrl [,dyn] · b2kAddStatic ctrl · b2kReshape ctrl, shape · b2kSpawnBox x,y,w,h [,color] [f] · b2kSpawnBall x,y,diam [,color] [f] · b2kSpawnCapsule x,y,len,thick [,color] [f]

    +

    Materials & body settings

    +

    b2kSetBounce ctrl,0..1 · b2kSetFriction ctrl,0..1 · b2kSetDensity ctrl,d · b2kSetBullet ctrl,flag · b2kSetFixedRotation ctrl,flag · b2kSetGravityScale ctrl,s · b2kSetDamping ctrl,lin [,ang] · b2kWake ctrl · b2kSleep ctrl · b2kSetSleepEnabled ctrl,flag · b2kSetSleepThreshold ctrl,pxPerSec · b2kSetStatic ctrl · b2kSetDynamic ctrl · b2kSetKinematic ctrl · b2kSetType ctrl,name · b2kDisable ctrl · b2kEnable ctrl

    +

    Act on bodies

    +

    b2kPush ctrl,dvx,dvy · b2kImpulse ctrl,ix,iy · b2kForce ctrl,fx,fy · b2kSetVelocity ctrl,vx,vy · b2kSpin ctrl,deg/s · b2kSpinBy ctrl,deg/s · b2kTorque ctrl,t · b2kAngularImpulse ctrl,imp · b2kMoveTo ctrl,x,y [,deg] · b2kExplode x,y [,radius] [,power] · b2kExplodeLegacy x,y [,radius] [,power] · b2kRemove ctrl

    +

    Read state

    +

    b2kBodyOf(ctrl) [f] · b2kPosition(ctrl) [f] · b2kWorldCenter(ctrl) [f] · b2kVelocity(ctrl) [f] · b2kSpeed(ctrl) [f] · b2kAngle(ctrl) [f] · b2kSpinRate(ctrl) [f] · b2kMass(ctrl) [f] · b2kBodyType(ctrl) [f] · b2kGravityScale(ctrl) [f] · b2kDamping(ctrl) [f] · b2kIsAwake(ctrl) [f] · b2kIsBullet(ctrl) [f] · b2kIsEnabled(ctrl) [f] · b2kBodyCount() [f] · b2kAwakeCount() [f] · b2kControlAt(x,y) [f] · b2kControlContains(ctrl,x,y) [f]

    +

    Joints

    +

    b2kHinge a,b,x,y [f] · b2kWeld a,b [f] · b2kRope a,b [,len] [f] · b2kSlider a,b,axisDeg [f] · b2kWheel chassis,wheel,x,y [,axisDeg] [f] · b2kMotorTo mover,ref,dx,dy,deg [,maxF,maxT] [f] · b2kNoCollide a,b [f] · b2kRemoveJoint joint &nbsp;&nbsp;Drive/limit/spring: b2kMotor j,deg/s [,maxT] · b2kHingeLimit j,lo,hi · b2kHingeAngle(j) [f] · b2kMotorOff j · b2kHingeLimitOff j · b2kSliderMotor j,px/s [,maxF] · b2kSliderLimit j,lo,hi · b2kSliderPos(j) [f] · b2kSliderMotorOff j · b2kSliderLimitOff j · b2kWheelMotor j,deg/s [,maxT] · b2kWheelSpring j,hz [,damp] · b2kWheelMotorOff j · b2kRope… readouts: b2kRopeRange j,min,max · b2kRopeLength(j) [f] · b2kRopeSetLength j,px · b2kSpring j,hz [,damp] · b2kWeldSpring j,hz [,damp]

    +

    Drag

    +

    b2kGrab(x,y) [f] · b2kRelease

    +

    Input (keyboard)

    +

    b2kInputOn · b2kInputOff · b2kInputIsOn() [f] · b2kKeyIsDown(key) [f] · b2kKeyPressed(key) [f] · b2kKeyReleased(key) [f] · b2kKeysHeld() [f] · b2kBindAction name,keys · b2kActionIsDown(name) [f] · b2kActionPressed(name) [f] · b2kActionReleased(name) [f] · b2kBindAxis name,negKeys,posKeys · b2kAxis(name) [f] · b2kInputInject keys · b2kInputInjectOff · b2kKeyCodes(key) [f] · b2kKeyName(code) [f] · b2kFrameMS() [f]

    +

    Sprites & sheets

    +

    b2kSheetLoad name,path,fw,fh [,n,margin,spacing] · b2kSheetLoadAtlas name,png [,xml] · b2kSheetFromImage name,img,fw,fh [,n,margin,spacing] · b2kSheetAddFrame sheet,frame,x,y,w,h · b2kSheetFrames(name) [f] · b2kSheetHasFrame(name,frame) [f] · b2kSheetFrameNames(name) [f] · b2kSheetScale name,factor · b2kSheetFrameSize(name,frame) [f] · b2kAnimDef sheet,anim,frames,fps [,loop] · b2kSpriteNew sheet [,frame,x,y] · b2kSpriteFromGIF path [,x,y] · b2kSpritePlay spr,anim [,restart] · b2kSpriteStop spr · b2kSpriteAnim(spr) [f] · b2kSpriteSetFrame spr,f · b2kSpriteFrame(spr) [f] · b2kSpriteFPS spr,fps · b2kSpriteFlipH spr,flag · b2kSpriteFlipped(spr) [f] · b2kSpriteOnFinish spr,msg · b2kSpriteMoveTo spr,x,y · b2kSpriteBind spr,bodyCtrl [,dx,dy] · b2kSpriteUnbind spr · b2kSpriteRemove spr

    +

    Player (the platformer controller)

    +

    b2kPlayerMake x,y,w,h [,sheet] · b2kPlayerAttach ctrl · b2kPlayerAnims idle,run,jump [,fall] [,land] [,duck] [,climb] [,hurt] [,swim] · b2kPlayerSet key,value · b2kPlayerGet(key) [f] · b2kPlayerOnGround() [f] · b2kPlayerState() [f] · b2kPlayerFacing() [f] · b2kPlayerJump [speed] · b2kPlayerControl flag · b2kPlayerAddLadder x1,y1,x2,y2 · b2kPlayerAddWater x1,y1,x2,y2 · b2kPlayerHurt [fromX] · b2kPlayerHurtIs() [f] · b2kPlayer() [f] · b2kPlayerSprite() [f] · b2kPlayerRemove

    +

    Camera

    +

    b2kCamOn [rect] · b2kCamOff · b2kCamIsOn() [f] · b2kCamGroup() [f] · b2kCamAdopt ctrl · b2kCamFollow ctrl [,lerp] · b2kCamUnfollow · b2kCamDeadzone w,h · b2kCamBounds x1,y1,x2,y2 · b2kCamGoto x,y · b2kCamPos() [f] · b2kCamShake ampPx,ms · b2kCamStatus() [f] · b2kCamLocSemantics() [f] · b2kCamMouseX() [f] · b2kCamMouseY() [f]

    +

    Sound

    +

    b2kSoundLoad name,path · b2kToneMake name,freqs,msPerNote [,vol,shape] · b2kSound name · b2kSoundLoop name · b2kSoundStop · b2kSoundMute flag · b2kSoundMuted() [f] · b2kSoundVolume pct · b2kSoundIsLoaded(name) [f] · b2kSoundStatus() [f]

    +

    Events (handlers you write)

    +

    on b2kFrame · on b2kContact pA,pB · on b2kEndContact pA,pB · on b2kSensorEnter pSensor,pVisitor · on b2kSensorExit pSensor,pVisitor &nbsp;&nbsp;Polling: b2kContactCount() [f] · b2kContactA(i) [f] · b2kContactB(i) [f] · b2kEndContactCount() [f] · b2kEndContactA(i) [f] · b2kEndContactB(i) [f]

    +

    Sensors

    +

    b2kAddSensor ctrl [,shape] [f] · b2kSetSensor ctrl,flag · b2kSensorCount() [f] · b2kSensorEnterSensor(i) [f] · b2kSensorEnterVisitor(i) [f] · b2kSensorExitCount() [f] · b2kSensorExitSensor(i) [f] · b2kSensorExitVisitor(i) [f]

    +

    Collision filtering

    +

    b2kDefineLayer name · b2kLayerBits(list) [f] · b2kSetCategory ctrl,layers · b2kSetMask ctrl,layers · b2kSetCollisionGroup ctrl,n · b2kNoCollide a,b [f]

    +

    Terrain & chains

    +

    b2kChain points [,loop] · b2kSmoothGround points · b2kAddChain ctrl,points [,loop]

    +

    Queries

    +

    b2kOverlap x1,y1,x2,y2 [f] · b2kOverlapMoving x1,y1,x2,y2 [f] · b2kOverlapCircle x,y,r [f] · b2kRayHit(x1,y1,x2,y2) [f] · b2kRayHitX() [f] · b2kRayHitY() [f] · b2kRayHitNormalX() [f] · b2kRayHitNormalY() [f] · b2kRayDist() [f] · b2kRayHitAll x1,y1,x2,y2 [f]

    +

    Tuning & profiling

    +

    b2kSetRestitutionThreshold px/s · b2kSetContactTuning hz,damp,pushPx · b2kSetJointTuning hz,damp · b2kSetMaxSpeed px/s · b2kEnableWarmStarting flag · b2kProfile() [f] · b2kAwakeBodyCount() [f]

    +

    Internal helpers (you usually won't call these)

    +

    The Kit also defines handlers it uses on your behalf — the loop and renderer (b2kSync, b2kDrawPoly, b2kDrawBall, b2kDrawImage, b2kDispatchContacts, b2kDispatchSensors), construction primitives (b2kEdge, b2kRegister, b2kResetTables), and math helpers (b2kLocalAnchor, b2kQueryToControls, b2kCorner, b2kCapsuleVerts). They're listed here for completeness; reach for the public handlers above instead.

    +
    +

    See also: getting-started.md · kit-reference.md (quick tables) · api-reference.md (the core b2… layer) · architecture.md (how the three layers fit together).

    +
    +
    + + + + diff --git a/website/docs/kit-reference.html b/website/docs/kit-reference.html new file mode 100644 index 0000000..c98a647 --- /dev/null +++ b/website/docs/kit-reference.html @@ -0,0 +1,188 @@ + + + + + +Kit reference — Box2Dxt docs + + + + + + + + + + + +
    + +
    +
    kit-reference.md
    +

    Box2Dxt Kit Reference (b2k…)

    +

    The Kit (src/box2dxt-kit.livecodescript) is a batteries-included, pure-xTalk toolkit over the box2dxt extension. You work in pixels, screen coordinates and degrees; the Kit hides the world, the fixed-timestep loop, coordinate conversion, and the per-frame control updates — and it runs with the demo's performance practices built in.

    +

    Setup: paste the Kit into your card/stack script (or save it as a library stack and start using it). It requires the box2dxt extension loaded (put b2Version() should return 4).

    +

    New here? Read the Complete Guide first — it teaches the Kit start-to-finish with runnable examples. This page is the quick-lookup reference.

    + +
    +

    60-second start

    +
    on openCard
    +   b2kQuickStart                       -- world + gravity + card-edge walls + go
    +   b2kSpawnBall 200, 80, 50            -- create & drop a 50px ball
    +   b2kSpawnBox 260, 80, 60, 40, "orange"  -- (read `the result` for the ref)
    +   b2kContactTarget the long id of me  -- (optional) collision messages
    +end openCard
    +on mouseDown ; get b2kGrab(the mouseH, the mouseV) ; end mouseDown
    +on mouseUp   ; b2kRelease ; end mouseUp
    +on closeCard ; b2kStop ; end closeCard
    +

    Or attach controls you designed in the IDE (graphics rotate; other control types follow position):

    +
    b2kSetup                                       -- world + gravity, auto origin
    +b2kAddStatic the long id of graphic "Floor"
    +b2kAddBox    the long id of graphic "Crate"
    +b2kAddBall   the long id of graphic "Ball"
    +b2kContactTarget the long id of me             -- on b2kContact pA, pB
    +b2kStart
    +

    World & loop

    +
    HandlerPurpose
    b2kSetup [gx, gy]Create the world + gravity, auto-detect the origin.
    b2kQuickStart [gy]One call: world + gravity + card-edge walls + start the loop.
    b2kStart / b2kStopBegin / end the simulation loop.
    b2kPause / b2kResumeFreeze / resume stepping without tearing down.
    b2kStepOnceAdvance exactly one fixed step (even while paused) — drives a Step button.
    b2kIsRunning()True while the loop is stepping (not stopped or paused).
    b2kAddWallsStatic walls around the current card edges.
    b2kAddGround [screenY]A static floor across the card (optionally at a given Y).
    b2kWall x1, y1, x2, y2A static collision segment between two screen points (custom walls, ramps, ledges). Two-sided — bodies collide from both sides whichever way you list the points; for one-sided (jump-through) surfaces use b2kChain/b2kSmoothGround. Invisible — draw your own graphic to match. For level edges a character is driven against, prefer a thick invisible static box just outside the play space — it gives the solver a cleaner stop than a thin segment.
    b2kKillFloor screenYDestroy any moving Kit body whose centre falls below this line (empty = off) — crates shoved into pits, enemies knocked off the level: removed instead of falling forever. The player's body is exempt (your game owns its respawn). Each removal first sends b2kFell ctrl to the frame target so you can clean up companions (bound sprites, table slots); don't delete the control inside that handler.
    b2kClearRemove all bodies/controls the Kit created, keep the world.
    b2kTeardownStop and destroy the world and all Kit state.
    b2kVersion()Native shim ABI version (4) — a load / in-sync check from Kit-only code.
    +

    Configuration

    +
    HandlerPurpose
    b2kSetScale pxPixels per metre (default 40).
    b2kSetOrigin x, yScreen point that maps to the world origin.
    b2kSetGravity gx, gyChange gravity (pixels-down is positive).
    b2kSetSubsteps nSolver sub-steps per step (≈ 4).
    b2kContactTarget objObject that receives on b2kContact / on b2kEndContact messages.
    b2kFrameTarget objObject that receives an on b2kFrame message once per simulated frame.
    b2kEnableSleeping flagToggle island sleeping (saves CPU when bodies rest).
    b2kEnableContinuous flagToggle continuous collision (CCD) for the world.
    +

    Attach & spawn

    +

    Attach physics to existing controls, or spawn new ones. Pass controls by reference (the long id of … is safest). Add ,true/,false to force dynamic/static where a handler takes an optional dyn flag.

    +

    Any control type works — it falls, collides, and is draggable. How it's drawn to follow its body depends on the type:

    +
    ControlFollows positionRotates
    Graphic (box → polygon, or ball)
    Image (dynamic)✅ via the angle
    Button / field / other❌ rotation locked so the sim matches the upright render
    +
    HandlerPurpose
    b2kAddBox ctrl [,dyn]Treat a control as a dynamic (default) box.
    b2kAddBall ctrl [,dyn]Treat a control as a circle.
    b2kAddCapsule ctrl [,dyn]Treat a control as a capsule (pill); long side = axis, short side = diameter.
    b2kAddPolygon ctrl [,dyn]Treat a graphic's points as a convex polygon.
    b2kAddStatic ctrlImmovable body matching the control.
    b2kAddGround [screenY]Static floor (see above).
    b2kSpawnBox x, y, w, h [,color] → controlCreate a control and its box body.
    b2kSpawnBall x, y, diam [,color] → controlCreate a control and its ball body.
    b2kSpawnCapsule x, y, len, thick [,color] → controlCreate a pill-shaped control and its capsule body.
    b2kReshape ctrl, "box"|"ball"|"capsule"|"poly"Rebuild a body's collision shape from the control's current size/points, on the same body (joints survive). Resets material + filter — re-apply them after.
    +

    Materials

    +
    HandlerPurpose
    b2kSetBounce ctrl, 0..1Restitution.
    b2kSetFriction ctrl, 0..1Friction.
    b2kSetDensity ctrl, dDensity (affects mass).
    +

    Body settings

    +
    HandlerPurpose
    b2kSetBullet ctrl, flagContinuous collision for fast bodies.
    b2kSetFixedRotation ctrl, flagStop a body from rotating.
    b2kSetGravityScale ctrl, sPer-body gravity multiplier.
    b2kSetDamping ctrl, lin [,ang]Linear (and optional angular) damping.
    b2kWake ctrlWake a sleeping body.
    b2kSleep ctrlSend a body to sleep until something wakes it.
    b2kSetSleepEnabled ctrl, flagAllow / forbid this body ever sleeping (vs b2kEnableSleeping, which switches the whole world). The player controller forbids it on its body.
    b2kSetSleepThreshold ctrl, pxPerSecSpeed below which a body may fall asleep.
    b2kSetStatic ctrl / b2kSetDynamic ctrl / b2kSetKinematic ctrlChange body type at runtime (freeze/unfreeze, moving platforms).
    b2kSetType ctrl, "static"|"kinematic"|"dynamic"Set body type by name.
    b2kDisable ctrl / b2kEnable ctrlTake a body out of / put it back into the simulation.
    +

    Act on bodies

    +
    HandlerPurpose
    b2kPush ctrl, dvx, dvyAdd a one-shot impulse (change in velocity, px/s).
    b2kForce ctrl, fx, fyApply a continuous force (call each frame for thrust/wind).
    b2kImpulse ctrl, ix, iyOne-shot impulse, mass-aware (heavier bodies move less).
    b2kTorque ctrl, torqueContinuous turning force (call each frame); +/- sets direction.
    b2kAngularImpulse ctrl, impOne-shot turning impulse, mass-aware (the angular partner of b2kImpulse).
    b2kSetVelocity ctrl, vx, vySet linear velocity (px/s). Wakes the body — setting a velocity means "move" (raw b2SetVelocity does not wake, which once froze a sleeping kinematic gate).
    b2kSpin ctrl, degPerSecSet angular velocity.
    b2kSpinBy ctrl, degPerSecAdd to the current angular velocity.
    b2kMoveTo ctrl, x, y [,deg]Teleport a body.
    b2kExplode x, y [,radius] [,power]Radial impulse from a point.
    b2kRemove ctrlDestroy a body (and the control if the Kit spawned it).
    +

    Read state

    +
    HandlerReturns
    b2kBodyOf(ctrl)The underlying b2… body handle.
    b2kPosition(ctrl)Body centre as x,y in screen pixels (the position partner of b2kVelocity).
    b2kWorldCenter(ctrl)Centre of mass as x,y in screen pixels (the true pivot for spin/torque).
    b2kVelocity(ctrl)vx,vy in px/s.
    b2kSpeed(ctrl)Scalar speed (px/s).
    b2kAngle(ctrl)Rotation in degrees.
    b2kMass(ctrl)Body mass (kg, sim units).
    b2kGravityScale(ctrl)Per-body gravity multiplier (getter for b2kSetGravityScale).
    b2kDamping(ctrl)Drag as linear,angular (getter for b2kSetDamping).
    b2kBodyType(ctrl)Body type as a word: static / kinematic / dynamic.
    b2kBodyCount()How many bodies the Kit is tracking.
    b2kAwakeCount()How many dynamic bodies are currently awake (active).
    b2kIsAwake(ctrl)Whether the body is awake.
    b2kSpinRate(ctrl)Rotation speed in degrees/sec (getter for b2kSpin).
    b2kIsBullet(ctrl)Whether continuous (CCD) collision is on.
    b2kIsEnabled(ctrl)Whether the body is in the simulation.
    b2kControlAt(x, y)The control whose body covers a screen point.
    b2kControlContains(ctrl, x, y)True if a screen point is inside this control's actual (rotated) collision shape.
    b2kRayHit(x1, y1, x2, y2)True if a ray hits; then read the result functions below.
    b2kRayHitX() / b2kRayHitY()Hit point in screen pixels.
    b2kRayHitNormalX() / b2kRayHitNormalY()Surface normal at the hit (screen-oriented unit vector).
    b2kRayDist()Distance in pixels from the ray start to the hit.
    +

    Joints

    +

    All joint constructors return a joint handle; pass it to the motor/limit/spring helpers or to b2kRemoveJoint.

    +
    HandlerPurpose
    b2kHinge ctrlA, ctrlB, x, y → jointRevolute pivot at a screen point (ctrlB empty = pin ctrlA to the world).
    b2kWeld ctrlA, ctrlB → jointRigidly glue two controls.
    b2kRope ctrlA, ctrlB [,len] → jointMaximum-distance link.
    b2kSlider ctrlA, ctrlB, axisDeg → jointPrismatic: slide ctrlA along an axis (0 = horizontal, 90 = vertical) relative to ctrlB or the world.
    b2kWheel chassis, wheel, x, y [,axisDeg] → jointWheel joint: sprung sliding axis + free spin (vehicle wheels).
    b2kRemoveJoint jointRemove a joint.
    +

    Motors, limits, springs & readouts:

    +
    HandlerPurpose
    b2kMotor joint, degPerSec [,maxTorque]Drive a hinge joint.
    b2kHingeLimit joint, lowerDeg, upperDegConstrain a hinge's angle.
    b2kHingeAngle(joint) → degreesCurrent hinge angle.
    b2kSliderMotor joint, pxPerSec [,maxForce]Drive a slider.
    b2kSliderLimit joint, lowerPx, upperPxConstrain a slider's travel.
    b2kSliderPos(joint) → pixelsCurrent slider translation.
    b2kWheelMotor joint, degPerSec [,maxTorque]Drive a wheel.
    b2kWheelSpring joint, hertz [,damping]Tune wheel suspension.
    b2kRopeRange joint, minPx, maxPxSet a distance joint's min/max length.
    b2kRopeLength(joint) → pixelsCurrent distance-joint length.
    b2kRopeSetLength joint, pxSet a distance joint's exact rest length.
    b2kSpring joint, hertz [,damping]Make a rope (distance) joint springy.
    b2kWeldSpring joint, hertz [,damping]Make a weld springy (0 hertz = rigid).
    b2kMotorOff jointTurn a hinge motor off (free swing).
    b2kHingeLimitOff jointRemove a hinge's angle limits.
    b2kSliderMotorOff jointTurn a slider motor off.
    b2kSliderLimitOff jointRemove a slider's travel limits.
    b2kWheelMotorOff jointTurn a wheel motor off.
    +

    Input (keyboard)

    +

    Poll-based held-key input for games: arm it once, then read state from on b2kFrame. The Kit samples the keysDown once per frame and diffs against the previous frame, so held keys are smooth (no OS auto-repeat artifacts), no focus or message-path setup is needed, and pressed/released edges are exact. Key names: left right up down space return escape tab shift control alt backspace delete, single characters ("a""z", digits — letters match both shifted and unshifted), or raw keycodes.

    +
    HandlerPurpose
    b2kInputOn / b2kInputOffArm / disarm the per-frame keyboard sample. b2kInputOn installs starter bindings: axis moveX (left,a / right,d), axis moveY (up,w / down,s), action jump (space).
    b2kInputInject keys / b2kInputInjectOffReplace the keyboard with a scripted key set (friendly names or codes; empty = nothing held) until turned off. Deterministic input for the self-test harness, input replays, and cutscene "ghost" players — edges fall out of the normal frame diff exactly as with real keys.
    b2kKeyIsDown(key)Key currently held.
    b2kKeyPressed(key) / b2kKeyReleased(key)True only on the frame the key went down / came up.
    b2kKeysHeld()Everything held now, as friendly names (debug HUDs).
    b2kBindAction name, keyListName a key set: b2kBindAction "jump", "space,up,w".
    b2kActionIsDown(name) / b2kActionPressed(name) / b2kActionReleased(name)Action-level queries — any bound key counts; edges treat the set as one logical key.
    b2kBindAxis name, negKeys, posKeysDefine a -1/0/+1 axis.
    b2kAxis(name)Read an axis; both directions held = 0.
    b2kKeyCodes(key) / b2kKeyName(code)The name↔keycode maps the module uses (handy for debugging).
    b2kFrameMS()Real elapsed ms folded into the last frame — drive animations and timers from this, not the step count.
    +
    on openCard
    +   b2kQuickStart
    +   b2kSpawnCapsule 200, 100, 64, 30, "gold"
    +   put the result into gHero
    +   b2kSetFixedRotation gHero, true
    +   b2kInputOn
    +   b2kFrameTarget the long id of me
    +end openCard
    +
    +on b2kFrame
    +   b2kSetVelocity gHero, b2kAxis("moveX") * 240, item 2 of b2kVelocity(gHero)
    +   if b2kActionPressed("jump") then b2kPush gHero, 0, -420
    +end b2kFrame
    +

    See examples/box2dxt-platformer.livecodescript for the full pattern (grounded check, jump-cut, one-way ledge).

    +

    Sprites & animation

    +

    Spritesheet animation on the icon-button backend (chosen by the Phase 0 benchmarks): a sheet registers named or numbered frame regions of one hidden source image; each region is sliced into its own hidden image lazily, on first use; a sprite is a transparent button whose icon is the current frame — all sprites of a sheet share the same frame images. Sheets persist until b2kTeardown; sprites are Kit-created controls, so b2kClear removes them with everything else, and every teardown also sweeps orphaned sprite controls left over from a previous session (script state resets when a stack reopens; the controls don't — without the sweep they'd linger as ghost sprites frozen on their last frame).

    +
    HandlerPurpose
    b2kSheetLoad name, path, fw, fh [,count] [,margin] [,spacing] → countRegister an image file as a uniform grid of fw×fh frames, numbered 1..N row-major. margin = outer border px, spacing = gap between cells (both default 0, edge-to-edge). Frame size 0 = no grid: name regions yourself with b2kSheetAddFrame.
    b2kSheetLoadAtlas name, pngPath [,xmlPath] → countRegister a packed atlas: PNG + TextureAtlas XML naming its regions (the Kenney format — see Spritesheets/ in this repo). Frames are addressed by name ("coin_gold"). XML path defaults to the PNG path with .xml.
    b2kSheetFromImage name, imgRef, fw, fh [,count] [,margin] [,spacing] → countRegister an image already in the stack (e.g. base64-embedded art) as a grid sheet. Same grid arguments as b2kSheetLoad.
    b2kSheetAddFrame sheet, frame, x, y, w, hName one region yourself — the no-XML path for packed sheets in any layout: load the source with frame size 0, then add each frame by name (any size/position; redefining re-bakes). Also works on top of a grid or atlas.
    b2kSheetFrames(name) / b2kSheetHasFrame(name, frame) / b2kSheetFrameNames(name)Frame count / existence / every frame key one per line (introspect an atlas you didn't make).
    b2kSheetScale name, factorDisplay scale for the sheet's frames (default 1, range 0.05–8) — the engine resamples at slice time, so any frame size displays at any sprite size. Set it right after loading, before creating sprites or anims.
    b2kSheetFrameSize(name, frame) → "w,h"A frame's display size (region × scale) — lay out tiles and platforms from this instead of hard-coding pixels.
    b2kSheetPersist flag / b2kSheetPersists()Opt-in (default off). When on, loaded sheets are treated as assets that survive b2kTeardown (like synthesized sounds) — so a multi-LEVEL game loads its atlases once, not on every rebuild (re-decoding/re-parsing/re-slicing is the costliest thing the Kit does). An identical reload becomes a no-op, and because the Kit's source/frame images are named deterministically (b2ksheet_<name> / b2kfr_<sheet>_<n>), a saved stack carries the cache: on reopen the load adopts the in-stack images instead of importing from disk. b2kSheetsWipe is still the explicit purge (e.g. after the user picks a different asset folder).
    b2kAnimDef sheet, anim, frames, fps [,loop]Name an animation: frames is a comma list of names and/or indices, numeric ranges ("1-8") expand. loop defaults true.
    b2kSpriteNew sheet [,frame, x, y] → controlCreate a sprite showing frame (default: the sheet's first), sized to the frame. An ordinary Kit control: give it a body (b2kAddCapsule …) or bind it to one.
    b2kSpriteFromGIF path [,x, y] → controlAn animated-GIF sprite (the engine plays it; play/stop/frame map to repeatCount/currentFrame).
    b2kSpritePlay spr, anim [,restart]Start a named animation — a no-op if already playing it, so calling it every frame from a state machine is free.
    b2kSpriteStop spr / b2kSpriteAnim(spr)Freeze on the current frame / what's playing.
    b2kSpriteSetFrame spr, frame / b2kSpriteFrame(spr)Manual frame control (stops any animation).
    b2kSpriteFPS spr, fpsPer-sprite speed override (empty = each animation's own fps).
    b2kSpriteFlipH spr, flag / b2kSpriteFlipped(spr)Face left/right — mirrored frames are flip-cloned lazily, shared like the originals.
    b2kSpriteOnFinish spr, messageWhen a non-looping animation ends, send message spr, anim to the frame target (attack/death/effect chains).
    b2kSpriteMoveTo spr, x, yMove any sprite/control to a world position, camera-correct — use this instead of set the loc for hand-animated paths (patrols, hovering).
    b2kSpriteBind spr, bodyCtrl [,dx, dy] / b2kSpriteUnbind sprPin the sprite to another control's position each frame — the standard "art bigger than the collision shape" pattern: an invisible control owns the body, the sprite follows it.
    b2kSpriteRemove sprRemove the sprite (and its body, if it has one).
    +
    b2kSheetLoadAtlas "chars", tFolder & "/spritesheet-characters-default.png"
    +b2kAnimDef "chars", "walk", "character_beige_walk_a,character_beige_walk_b", 6, true
    +b2kSpriteNew "chars", "character_beige_idle", 200, 100
    +put the result into tSpr
    +b2kSpritePlay tSpr, "walk"
    +b2kSpriteFlipH tSpr, true       -- face left
    +

    Performance notes (Phase 0 measurements, modest Win32 hardware): a frame switch is one icon set; ~25 moving animated sprites ≈ 40 fps, a player plus a handful sits at the loop ceiling. Create sprites at scene load (not mid-play) and before enabling acceleratedRendering where possible — bulk control creation under the compositor caused the spike's one-time stall.

    +

    Player (the platformer controller)

    +

    A complete keyboard character controller for one player: a vertical capsule with fixed rotation, sleep disabled, and low friction, steered every frame from the Input module's axes moveX/moveY and action jump (rebind those names to remap the controls). Horizontal motion is velocity-driven — vx accelerates toward axis × moveSpeed — and grounding comes from three short downward rays whose surface normal must point up within maxSlopeDeg, so walkable slope vs wall is one comparison. The jump feel platformers expect is built in: coyote time (jump shortly after running off a ledge), jump buffering (press just before landing), jump-cut (tap = hop, hold = full height), and a terminal fall speed. With animations mapped, the controller drives b2kSpritePlay/b2kSpriteFlipH itself.

    +

    Wave 2 actions, all built in: duck (DOWN while grounded brakes to a crouch — the hitbox does not shrink this wave), drop-through (DOWN+JUMP while standing on a one-way b2kChain/b2kSmoothGround deck opens a dropMs window in which chains alone stop colliding; on solid ground the press is simply eaten), climb (UP — or DOWN while airborne — inside a b2kPlayerAddLadder zone parks gravity at 0 and runs y at climbSpeed; JUMP exits with a normal jump), and the hurt-knockback standard (b2kPlayerHurt, below).

    +

    Wave 4 action: swim. While the player's centre is inside a b2kPlayerAddWater zone the controller swims — gravity scales down to swimGravity (buoyant), the sink caps at swimMaxFall (well below the air terminal), UP/DOWN drive vy at swimSpeed, and a JUMP press is a repeatable upward stroke (swimJump) with no ground gate. State reads swim; leaving the zone (or a hurt) restores the saved gravity scale exactly once. Mutually exclusive with the climb. Sized for a raised-bank basin — a sub-ground pit falls below the camera (see kit-guide §21).

    +

    Wave 5 actions, each opt-in through a knob (the defaults leave the controller byte-for-byte as above, and every idle path is one compare per frame): double-jump (airJumps — extra mid-air jumps, refilled on landing), wall-slide + wall-jump (wallSlideMax caps the fall while you press into a wall; wallJumpX/wallJumpY launch up and away with a brief steer lock — states wallslide and a side ray that runs only while airborne), dash (dashSpeed on the new dash action — a flat horizontal burst for dashMs with gravity parked, cooldown-gated; state dash; yields to climb/swim), duck capsule reshape (duckScale < 1 turns the Wave 2 brake into a real crawl — a feet-anchored b2kReshape to a shorter capsule with a headroom check before standing), and platform carry (platformCarry 1 — a grounded player inherits the velocity of the moving kinematic body it rides; a vertical lift's carry is exempt from the ground-snap). The marquee showcase is the platformer example.

    +
    HandlerPurpose
    b2kPlayerMake x, y, w, h [,sheet] → controlOne call: a capsule body host (w×h collision box — a visible capsule graphic, or invisible with a bound sprite of sheet's first frame on top), controller armed, input on. Reports the player control.
    b2kPlayerAttach ctrlAdopt an existing control (or sprite) as the player. A capsule body is added if it has none (then the controller also sets low friction); a body you made yourself keeps your material. Also sets fixed rotation + sleep-off and arms input.
    b2kPlayerAnims idle, run, jump [,fall] [,land] [,duck] [,climb] [,hurt] [,swim] [,wall] [,dash]Map states to the art's animation names (fall defaults to jump; duck to idle; climb and hurt to jump; swim and wall to fall; dash to run — sheets without those frames still read correctly). land is an optional non-looping touch-down flourish, held for its own duration. Map a LOOPING animation to hurt if your game uses b2kSpriteOnFinish on the player's art — a non-looping hurt pose fires that finish message mid-knockback. The art is the player control itself if it is a sprite, else the first sprite b2kSpriteBind-pinned to it.
    b2kPlayerSet key, value / b2kPlayerGet(key)Tuning knobs (table below). Settable any time; b2kClear keeps them (config, like input bindings), b2kTeardown/b2kPlayerRemove wipe them.
    b2kPlayerOnGround()Grounded this frame (post-tick; false on the frame a jump launches).
    b2kPlayerState()idle / run / jump / fall / duck / climb / hurt / swim / wallslide / dash, plus land for exactly one frame on touch-down (dust puffs, sounds — read it in on b2kFrame). A drop-through renders as fall; a knockback's own landing shows no land tick. The Wave 5 states (wallslide, dash) appear only when their knobs are enabled.
    b2kPlayerFacing()1 right / -1 left — the last horizontal intent.
    b2kPlayerHalfH() / b2kPlayerHalfW()The capsule's current half-extents in px — the half-height drops while in a reshaped duck/crawl. Read these live for head-reach logic (never bake a constant: a hitbox taller than the visible art bumps things the head never touches).
    b2kPlayerInLadder() / b2kPlayerInWater()This frame's ladder / water zone membership (the controller computes them every tick anyway) — for "press UP to climb" prompts, splash effects, a breath meter.
    b2kPlayerRespawn x, yTeleport to a screen-px point and reset to a clean standing idle: velocity zeroed; the jump/hurt/dash/climb/swim/drop/duck state cleared; the air and air-jump budgets refreshed. The respawn most games hand-roll (move + zero velocity + clear a pile of flags) in one call. Tuning and zones are kept. Empty x/y reset in place.
    b2kPlayerJump [speed]Programmatic jump (springs, double-jump powerups): the same launch as a pressed jump but without the grounded/coyote gate — the caller decides when it is allowed.
    b2kPlayerAddLadder x1, y1, x2, y2Register a ladder zone (screen-px rect, any corner order; purely polled — no physics object). Zones are world state: b2kClear wipes them with everything else. Run the zone a little above a platform at the ladder's top so walking off that edge holding DOWN grabs it.
    b2kPlayerAddWater x1, y1, x2, y2Register a water/swim zone (screen-px rect, any corner order; purely polled). World state, wiped by b2kClear like ladders. Top the zone a little above the drawn surface so the dive-in and surface-out break the water where the art is. The pool is a raised basin between banks (a sub-ground pit clamps below the camera).
    b2kPlayerHurt [fromX]The contact-damage knockback standard: an away-pop (hurtPopX/hurtPopY, ground-snap-exempt; the sign of fromX vs the player picks the direction — empty pops back off the facing), the hurt state/anim, input suppressed until hurtMs or the first landing after half of it (whichever is later), then an invulnMs mercy window in which this command no-ops. Keep your respawn flow for lethal hits (pits, kill planes): contact damage knocks back, falling dies.
    b2kPlayerHurtIs()True through the knockback and the mercy window — the one gate your hazard checks need.
    b2kPlayerControl flagfalse = the controller only observes (state/ground/facing stay fresh) and writes neither velocity nor animations — cutscenes, hit poses, scripted deaths. The maxFall clamp stays live. true re-asserts the state animation. An explicit call (either way) cancels a knockback in flight — your respawn flow takes the body over cleanly, and no mercy window is granted.
    b2kPlayer() / b2kPlayerSprite()The player control / the art control the controller animates.
    b2kPlayerRemoveTear down the controller (tuning included). The body and sprite remain yours — remove them with b2kRemove / b2kSpriteRemove.
    +

    Tuning keys (b2kPlayerSet), with defaults for a 32×48 px player at scale 40: moveSpeed 220 px/s · accel 1800 px/s² · airAccel 1100 px/s² · jumpSpeed 460 px/s · jumpCut 0.45 (velocity multiplier on early release) · coyoteMs 90 · bufferMs 110 · maxFall 900 px/s · maxSlopeDeg 50 (steeper than this is a wall, not ground) · dropMs 260 (the drop-through window) · climbSpeed 160 px/s (ladder rate; x runs at half moveSpeed while climbing) · swimSpeed 150 / swimJump 300 px/s (water move speed + the repeatable stroke) · swimGravity 0.35 / swimMaxFall 150 (buoyancy: the between-stroke sink scale and its cap — swimJump alone sets the escape height, so lower IT to make climbing out harder) · hurtPopX 220 / hurtPopY 320 px/s (knockback launch) · hurtMs 700 (control-off span) · invulnMs 900 (post-hurt mercy).

    +

    Wave 5 action keys — all opt-in (these defaults leave the controller exactly as above): airJumps 0 (extra mid-air jumps; 1 = double-jump, refilled on landing) · wallJumpX 0 / wallJumpY 0 (wall-jump launch px/s, away + up; wallJumpX > 0 arms the wall system, wallJumpY falls back to jumpSpeed) · wallSlideMax 0 (capped fall px/s while pressing into a wall — the wallslide state) · dashSpeed 0 (a flat horizontal burst on the dash action, default keys SHIFT/X; 0 = off) / dashMs 160 / dashCooldownMs 500 · duckScale 1 (ducked capsule height ÷ standing; < 1 reshapes to a crawl so the hero slips under low gaps — feet-anchored, with a headroom check before standing) · platformCarry 0 (1 = a grounded player inherits the velocity of a moving kinematic platform it rides; costs two reads per grounded frame, and changes how a player rides any kinematic body).

    +
    -- a playable character in four lines (after b2kQuickStart + sheet load):
    +b2kPlayerMake 200, 100, 32, 48, "chars"
    +put the result into gHero
    +b2kPlayerAnims "idle", "walk", "jump", "jump"
    +b2kCamFollow gHero
    +

    One player at a time: attaching a new player replaces the old controller state (the old body/sprite stay). The platformer example adopts its own invisible body host with b2kPlayerAttach — its entire movement, ground-probe and animation code is these four calls.

    +

    Feel guarantees (all asserted by the self-test): the coyote/buffer windows run on sim time (frame-coherent on slow machines); bodies the controller creates get zero restitution and low friction (landings are dead, walls don't glue); a touchdown is a land only after real airtime (landing hysteresis — solver blips can't double-fire it); and a ground-snap removes the solver's post-landing rebound on flat ground — slopes are exempt via the surface normal, and external boosts must use b2kPlayerJump (a raw upward b2kSetVelocity on a grounded player is treated as solver noise and snapped).

    +

    Camera (scrolling levels)

    +

    A clipped viewport group the loop scrolls (the mechanism the Phase 0 spike benchmarked). While the camera is on, Kit-created controls — spawns and sprites — are placed inside the viewport, and their locs stay world pixels: contents of a scrolled group keep their coordinates, so physics and the body sync are untouched and the scroll is pure presentation. Chrome stays on the card; anything layered behind the (transparent) group reads as an infinite-distance parallax backdrop. Level coordinates should start at 0,0.

    +
    HandlerPurpose
    b2kCamOn [rect]Create the viewport (default: the card rect). Do this before building the level, after any static backdrop.
    b2kCamOffDissolve the viewport — children return to the card with their world locs. Called automatically by b2kTeardown.
    b2kCamIsOn() / b2kCamGroup()State / the group's ref — build your own level controls inside it: create graphic "x" in b2kCamGroup().
    b2kCamAdopt ctrlMove an existing control (IDE-designed levels, fallback shapes) into the viewport.
    b2kCamFollow ctrl [,lerp] / b2kCamUnfollowTrack a control; lerp 0.01–1 smooths (default 0.15).
    b2kCamDeadzone w, hChase only once the target leaves a centre box (0 = always centre).
    b2kCamBounds x1, y1, x2, y2Clamp the view to the level.
    b2kCamGoto x, y / b2kCamPos()Cut to a world point / read the view centre.
    b2kCamShake ampPx, msA decaying random view offset (impacts, blasts).
    b2kCamStatus()Empty = healthy; otherwise why the camera degraded (group/scroll failures). Check it after b2kCamOn and fall back gracefully.
    b2kCamLocSemantics()"visual" or "content" — which grouped-loc coordinate model the startup probe detected. Diagnostic; the Kit compensates automatically.
    b2kCamMouseX() / b2kCamMouseY()The mouse in world pixels — use for b2kGrab, click-picking, spawning at the pointer. (The Kit's own drag already uses them.)
    +
    b2kCamOn
    +b2kCamBounds 0, 0, 3072, 640
    +b2kCamDeadzone 140, 600
    +b2kCamFollow gHero, 0.18
    +-- in mouseDown:  get b2kGrab(b2kCamMouseX(), b2kCamMouseY())
    +

    The platformer example is a complete scrolling level on this module.

    +

    Sound (SFX — with or without asset files)

    +

    Named sounds over audioClips — the one LiveCode sound path with no external media-layer dependency. The engine plays one clip at a time (a new play cuts the previous); that's the classic LC limit, and short retro SFX suit it. b2kToneMake synthesizes a clip in pure script (8-bit mono WAV, square or sine, a comma list of note frequencies with a per-note decay), so a self-contained stack ships sound with zero files. Sounds are assets like sheets: b2kTeardown wipes them (names are stable, so re-making replaces rather than accumulates). Failures degrade to silence, never errors — the first play that throws trips a dead-flag.

    +
    HandlerPurpose
    b2kSoundLoad name, pathImport a WAV/AIFF/AU file as a named sound (replaces any same name).
    b2kToneMake name, freqs, msPerNote [,vol] [,shape]Synthesize a sound: freqs like "1319,1760" (Hz; 0 = a rest), vol 0–100 (default 60), shape "square" (default, retro) or "sine" (soft). ~22 KB per second — keep SFX short.
    b2kSound namePlay (cuts whatever is playing). Stale-safe: unknown names and dead audio no-op.
    b2kSoundLoop name / b2kSoundStopLoop ambience until stopped / stop the current sound.
    b2kSoundMute flag / b2kSoundMuted()Swallow play calls — a user preference that survives b2kTeardown.
    b2kSoundVolume pctThe engine-global playLoudness (0–100) — it affects every stack's audio, so expose it, don't hardcode it.
    b2kSoundIsLoaded(name) / b2kSoundStatus()Loaded check / empty = healthy, else the most recent reason audio degraded.
    +
    b2kToneMake "coin", "1319,1760", 36          -- a two-note blip
    +b2kToneMake "win", "523,659,784,1047", 110   -- a little fanfare
    +-- in your coin handler:  b2kSound "coin"
    +

    The platformer's eight cues (jump, land, coin, stomp, hurt, checkpoint, gate, win) are all synthesized — no asset folder involved.

    +

    Drag & events

    +
    HandlerPurpose
    b2kGrab(x, y) → controlGrab the body under a point (mouse joint). Returns the grabbed control, or empty.
    b2kReleaseRelease a grabbed body.
    on b2kContact pCtrlA, pCtrlBSent to your b2kContactTarget when two attached controls begin touching. Long ids are empty for walls/ground.
    on b2kEndContact pCtrlA, pCtrlBSent when two attached controls stop touching.
    on b2kFrameSent to your b2kFrameTarget once per simulated frame (after bodies sync) — use it for motors, input, and custom drawing.
    on b2kFell pCtrlSent to your b2kFrameTarget just before the Kit removes a body that crossed the b2kKillFloor line.
    +

    Frame-exact events: contact and sensor events are harvested after every fixed step and buffered for the frame — a frame that ran two physics steps loses nothing, and a frame that ran zero repeats nothing. The messages and the polling accessors below both read the same frame buffer.

    +

    Polling contacts — instead of (or alongside) the messages above, read this frame's touch pairs directly, e.g. from on b2kFrame. Indices are 1-based; each accessor returns a control (empty for a wall, the ground, or any untracked body).

    +
    HandlerReturns
    b2kContactCount()How many pairs began touching this frame.
    b2kContactA(i) / b2kContactB(i)The two controls of the i-th begin-touch pair.
    b2kEndContactCount()How many pairs stopped touching this frame.
    b2kEndContactA(i) / b2kEndContactB(i)The two controls of the i-th end-touch pair.
    +

    Sensors (trigger zones)

    +

    One-shots vs. presence: sensor enter/exit messages are ideal for one-shot triggers (coins, checkpoints, goals — consumed or guarded on first fire). For presence state that must stay true while something sits there (pressure plates, buttons, zones), poll b2kOverlapMoving of the region each frame instead of counting enters minus exits: a poll is stateless (it cannot drift), it still sees sleeping bodies — a crate that settles and sleeps keeps holding the plate — and the Moving variant filters out the floor the pad sits on (the broadphase's fattened boxes would otherwise report it forever). The platformer's plate works this way, with a ~0.2 s release debounce for feel.

    +

    Non-solid fixtures that report overlaps but never block. The Kit enables sensor events on every body it creates, so sensors detect them automatically.

    +
    HandlerPurpose
    b2kAddSensor ctrl [,shape] → bodyAttach a static sensor (shape = box/ball/capsule).
    b2kSetSensor ctrl, flagFlip an existing body between solid and sensor (rebuilds the shape, keeping the new sensor state).
    on b2kSensorEnter pSensorCtrl, pVisitorCtrlSent to your b2kContactTarget when a body enters a sensor.
    on b2kSensorExit pSensorCtrl, pVisitorCtrlSent when it leaves.
    b2kSensorCount() / b2kSensorEnterSensor(i) / b2kSensorEnterVisitor(i)Poll this frame's enters (1-based).
    b2kSensorExitCount() / b2kSensorExitSensor(i) / b2kSensorExitVisitor(i)Poll this frame's leaves (1-based).
    +

    Collision filtering (named layers)

    +

    Up to 31 nameable layers (bits 2⁰–2³⁰; the top bit 2³¹ is the Kit's reserved oneway chain layer — usable by name in any list, never handed out by b2kDefineLayer). Two shapes collide only if each one's category is in the other's mask (and no shared negative group forbids it).

    +
    HandlerPurpose
    b2kDefineLayer name → bitDefine/fetch a named layer.
    b2kSetCategory ctrl, layersSet which layer(s) a control is (comma/space list of names or numbers).
    b2kSetMask ctrl, layersSet which layer(s) it collides with. The reserved oneway bit is included automatically — a custom-masked body still stands on b2kChain terrain (object rules shouldn't silently mean "fall through the ground"). To genuinely pass through chains, use the raw b2SetShapeFilter.
    b2kSetCollisionGroup ctrl, nNegative = never collide with same group; positive = always.
    b2kNoCollide ctrlA, ctrlB → jointStop just these two from colliding (a filter joint).
    +

    Chains & terrain

    +
    HandlerPurpose
    b2kChain pointList [,loop] → chainSmooth static terrain from a list of x,y screen points (≥ 4). No inner corners for fast bodies to catch on. Invisible — draw a matching graphic.
    b2kSmoothGround pointList → chainAn open chain (alias for b2kChain …, false).
    b2kAddChain ctrl, pointList [,loop]A smooth chain that tracks a control (move/draw the terrain as one graphic). Points are the control's own outline.
    +

    One-way chains (Wave 2): every b2kChain/b2kSmoothGround chain carries the Kit's reserved oneway collision category (bit 2³¹) with an all-bits mask, so nothing collides any differently by default — but the player's drop-through (DOWN+JUMP while standing on one) can mask out chains alone for a dropMs window. b2kAddChain outlines are solid terrain and are not tagged. Don't park a solid platform less than a player-height under a one-way deck — a drop that can't clear the deck is restored by a hard deadline and may pop back on top.

    +

    Winding: a chain's solid side is to the right of the point-travel direction. For ground you stand on, list the top surface right-to-left so the solid side faces up. (Bodies falling through everywhere? Reverse the point order.)

    +

    The ghost rule (open chains): Box2D treats an open chain's first and last segments as ghost anchors — they don't collide (N points ⇒ N−3 solid segments). Run the chain one segment past the surface you need on each side; over solid ground the tails can simply continue flat. Endpoints placed at a platform's edges leave its ends intangible — bodies fall through the outer tiles. (Bodies falling through near the ends? This, not winding.)

    +

    Region & ray queries

    +
    HandlerReturns
    b2kOverlap x1, y1, x2, y2Newline list of controls whose body overlaps a screen rect. Broadphase boxes are fattened (~0.1 m ≈ a few px), so a region touching the floor reports the floor — use the Moving variant for presence.
    b2kOverlapMoving x1, y1, x2, y2The same query with static bodies filtered out — the presence poll for pressure plates and buttons ("is some thing on me?"). Dynamic and kinematic count; sleeping bodies still register.
    b2kOverlapCircle x, y, r…overlapping a screen circle.
    b2kRayHitAll x1, y1, x2, y2Every control a ray crosses, nearest-first (vs b2kRayHit's single closest).
    +

    Motors & tuning

    +
    HandlerPurpose
    b2kMotorTo mover, ref, dxPx, dyPx, deg [,maxF, maxT] → jointDrive mover toward a pose offset from ref (a moving anchor; empty = world).
    b2kExplode x, y [,radius] [,power]Native radial blast (shape-aware, affects all dynamic bodies). b2kExplodeLegacy keeps the old velocity-based feel.
    b2kSetRestitutionThreshold px/s · b2kSetContactTuning hz, damp, pushPx · b2kSetJointTuning hz, damp · b2kSetMaxSpeed px/s · b2kEnableWarmStarting flagWorld solver tuning.
    b2kProfile()"totalStep,collide,solve" ms for the last step (a perf HUD).
    b2kAwakeBodyCount()Awake dynamic bodies (native count).
    +

    Tips

    +
      +
    • Keep moving objects a sensible on-screen size (default scale 40 px/m, so roughly 4–400 px).
    • +
    • Graphic boxes/polygons and dynamic images rotate with their body (images via the angle). Buttons, fields, and other controls follow position only and have their rotation locked, so the simulation stays consistent with the upright render.
    • +
    • The Kit render loop uses Box2D body-move events, so it only considers bodies that moved in the latest step, avoids per-body awake/position polling for sleeping objects, and skips redraws when the rounded screen pixel/angle did not change.
    • +
    • When you need something the Kit doesn't expose, drop to the core b2… API: b2kWorld() returns the world and b2kBodyOf(control) the body, and b2kToWorldX/Y(px) / b2kToScreenX/Y(m) convert between screen pixels and Box2D metres — so you can mix both layers.
    • +
    +
    +
    + + + + diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..09794c3 --- /dev/null +++ b/website/index.html @@ -0,0 +1,320 @@ + + + + + +Box2Dxt — Real 2D physics for OpenXTalk & the xTalk family + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + Box2Dxt — Home + +
    + +
    + + +
    +
    +
    + Box2D v3.1.0 · OpenXTalk & LiveCode 9.6.3+ +

    Real 2D physics
    for the card & stack people.

    +

    The battle-tested Box2D engine, dropped into OpenXTalk and LiveCode. Your controls fall, roll, bounce, hinge, and collide — in plain xTalk.

    + +

    Flip the stack with , the arrows below, or the menu. Press M for the message box.

    +
    + +
    +
    +
    + + PHYSICS — DRAG THE CRATES +
    + +
    + + + click empty space to drop a crate +
    +
    +
    +
    + +
    + GO TO CARD: + + + + + + + +
    +
    + + +
    + CARD 02 · WHAT IT IS +

    A real physics engine,
    wearing xTalk's clothes.

    +

    Box2D powers countless games. Box2Dxt packages Box2D v3.1.0 as a drop-in module for OpenXTalk and LiveCode 9.6.3+, so you get true rigid-body simulation — gravity, friction, restitution, joints, sensors — while writing the plain, English-like xTalk you already know.

    +
    +
    370+engine handlers
    +
    300+friendly kit calls
    +
    6paste-and-run examples
    +
    ~125self-tests, one click
    +
    +
      +
    • Works in pixels & degrees — the units you already think in.
    • +
    • Bodies ride on ordinary LiveCode controls; the Kit moves them each frame.
    • +
    • Prebuilt for Windows, macOS, and Linux — no C toolchain to install.
    • +
    +

    Next up: how little code it takes.

    +
    + + +
    + CARD 03 · ONE PASTE +
    +
    +

    A whole scene,
    in a single paste.

    +

    b2kQuickStart stands up a world with gravity and card-edge walls; every b2kSpawn… makes a control and its body at once. Grab and fling with the mouse in three lines — the Kit owns the loop and the redraws.

    +
      +
    • Paste it into a stack script and open the card.
    • +
    • No setup, no assets, no build step.
    • +
    • Every call reads like a sentence.
    • +
    +
    +
    +
    openCard — stack script
    +
    on openCard
    +   b2kQuickStart                          -- world + gravity + walls + go
    +   b2kSpawnBall 200, 80, 50               -- create & drop a ball
    +   b2kSpawnBox 260, 80, 60, 40, "orange"  -- read the result for the ref
    +end openCard
    +
    +on mouseDown
    +   get b2kGrab(the mouseH, the mouseV) -- grab the body under the pointer
    +end mouseDown
    +
    +on mouseUp
    +   b2kRelease
    +end mouseUp
    +
    +
    +
    + + +
    + CARD 04 · WHAT YOU GET +

    Friendly on top. Serious underneath.

    +
    +
    +
    +

    Friendly by default

    +

    The Kit speaks pixels and degrees, binds bodies to your controls, and runs the loop. b2kQuickStart gives a live, draggable world in one line.

    +
    +
    +
    +

    The full Box2D surface

    +

    370+ handlers: bodies, shapes, joints, chains, sensors, ray casts, queries, contact events, and world tuning — in metres and radians when you want the metal.

    +
    +
    +
    +

    A built-in Game Kit

    +

    A player controller (run, double-jump, wall-jump, dash, climb, crawl, swim), a scrolling camera, spritesheets, input, and sound — all wired and verified.

    +
    +
    +
    +

    Safe by design

    +

    Every handle is generation-tagged and validated in the C shim. A stale or invalid handle is a harmless no-op — never a crash.

    +
    +
    +
    +

    Cross-platform, prebuilt

    +

    Drop-in libraries for Windows (x64/x86), macOS (universal), and Linux (x86-64/i686). Or build it yourself with two CMake commands.

    +
    +
    +
    +

    Paste-and-run examples

    +

    Every example is a single self-contained script — no setup, no external assets. Read one as a worked tutorial.

    +
    +
    +
    + + +
    + CARD 05 · HOW IT WORKS +

    Three clean layers. One native library.

    +

    Call the friendly layer and ignore the rest — or drop down a level whenever you need to. It's a stack, naturally.

    +
    +
    +
    your stack
    +
    your code

    Your xTalk script

    Plain handlers — on openCard, on mouseDown, your game logic.

    +
    +
    b2k… pixels · degrees · screen coords
    +
    +
    box2dxt-kit.livecodescript
    +
    the Kit

    The Kit

    Pure xTalk sugar. Owns the world and the loop, binds bodies to controls, converts units. This is what most users call.

    +
    +
    b2… metres · radians · handles
    +
    +
    box2dxt.lcb
    +
    the extension

    The extension

    The xTalk Builder binding — foreign handlers plus a public b2… wrapper for the whole Box2D v3.1 surface.

    +
    +
    FFI ints & doubles · opaque handles
    +
    +
    box2d_lc.c + Box2D v3.1.0
    +
    native

    The C shim & the engine

    Compiled into one shared library. Handles are stored in a table, validated, and generation-tagged for safety.

    +
    +
    +
    + + +
    + CARD 06 · SEE IT RUN +

    Six examples, each a single paste.

    + +
    + + +
    + CARD 07 · LEARN IT +

    Guides, not homework.

    +

    Two short guides take a LiveCode user from install to a running scene, plus a one-liner reference for while you build. That's the whole manual.

    + +

    Going deeper — the raw b2… API, the engine internals, and build-from-source notes — lives on GitHub →

    +
    + + +
    + CARD 08 · GET STARTED +

    Up and running in four steps.

    +
      +
    1. 1

      Get the native library

      Download the file for your platform from prebuilt/ and rename it to the bare name: box2dxt.dll / .dylib / .so (no lib prefix).

    2. +
    3. 2

      Load the extension

      Add box2dxt.lcb in the Extension Manager and Load it — or load extension from file … from script.

    4. +
    5. 3

      Sanity check

      In the Message Box, run put b2Version(). You should see 4 — the engine is wired up.

    6. +
    7. 4

      Paste an example & reopen the card

      Drop any example script into a stack script and reopen the card. You're simulating real physics.

    8. +
    + +
    + +
    + + +
    + + + Home · 1/8 + + +
    + + + +
    +
    + +

    + Box2Dxt · MIT licensed · built on Box2D © Erin Catto + · + GitHub +

    +
    + + + + diff --git a/website/styles.css b/website/styles.css new file mode 100644 index 0000000..9dfe772 --- /dev/null +++ b/website/styles.css @@ -0,0 +1,607 @@ +/* =========================================================== + Box2Dxt — "card & stack" identity + HyperCard / classic-Mac heritage, executed with modern + layout discipline. Paper + ink + one signature orange, + hard borders, solid offset shadows, real Mac window chrome. + =========================================================== */ + +:root { + --paper: #f1ece1; /* desktop paper */ + --card: #fbf8f1; /* window / card surface */ + --ink: #17140d; /* near-black ink */ + --ink-soft: #50473a; /* body text */ + --ink-faint:#8b8170; /* muted captions */ + + --orange: #e8702a; /* the Box2D crate — our one signature colour */ + --orange-d: #bd551b; + --blue: #2f5fae; + --green: #3f8f5b; + --red: #d23b3b; + --hl: #ffd23f; /* highlighter */ + + --line: 2px solid var(--ink); + --shadow: 5px 5px 0 var(--ink); + --shadow-sm:3px 3px 0 var(--ink); + --shadow-lg:8px 8px 0 var(--ink); + --radius: 9px; /* the Atkinson roundrect */ + --radius-sm:6px; + + --maxw: 1120px; + + --sans: "Space Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; + --pixel: "Silkscreen", "JetBrains Mono", monospace; +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } + +body { + margin: 0; + color: var(--ink); + font-family: var(--sans); + font-size: 17px; + line-height: 1.6; + background-color: var(--paper); + /* faint 1-bit "desktop" dot grid */ + background-image: radial-gradient(var(--ink) 0.5px, transparent 0.6px); + background-size: 18px 18px; + -webkit-font-smoothing: antialiased; + overflow-x: hidden; +} + +a { color: inherit; text-decoration: none; } + +.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: 22px; } + +h1, h2, h3, h4 { margin: 0; font-weight: 700; line-height: 1.05; letter-spacing: -0.02em; } +h1 { font-size: clamp(2.5rem, 6.2vw, 4.3rem); } +h2 { font-size: clamp(1.8rem, 3.8vw, 2.7rem); } +p { margin: 0; } + +code, .mono { font-family: var(--mono); } +:not(pre) > code { + font-size: 0.85em; + background: #fff; + border: 1.5px solid var(--ink); + border-radius: 5px; + padding: 0.05em 0.36em; +} + +/* highlighter pen on key phrases */ +mark { + background: linear-gradient(120deg, var(--hl) 0%, var(--hl) 100%); + background-repeat: no-repeat; + background-size: 100% 55%; + background-position: 0 78%; + color: inherit; + padding: 0 1px; +} + +/* pixel-font chrome label */ +.label { + font-family: var(--pixel); + font-size: 0.62rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +/* ---------- Buttons (tactile Mac roundrects) ---------- */ +.btn { + display: inline-flex; align-items: center; gap: 8px; + font-family: var(--sans); + font-weight: 600; font-size: 0.98rem; + padding: 11px 20px; + border: var(--line); + border-radius: var(--radius); + background: var(--card); + color: var(--ink); + cursor: pointer; + box-shadow: var(--shadow-sm); + transition: transform .06s ease, box-shadow .06s ease; +} +.btn:hover { transform: translate(-1px,-1px); box-shadow: 4px 4px 0 var(--ink); } +.btn:active { transform: translate(3px,3px); box-shadow: 0 0 0 var(--ink); } +.btn-primary { background: var(--orange); color: #fff; } +.btn svg { display: block; } + +/* ---------- Classic Mac window ---------- */ +.win { + background: var(--card); + border: var(--line); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; +} +.win-bar { + position: relative; + display: flex; align-items: center; gap: 7px; + height: 27px; padding: 0 9px; + background: var(--card); + border-bottom: var(--line); +} +.win-stripes { + flex: 1; height: 11px; + background: repeating-linear-gradient(to bottom, var(--ink) 0 1.5px, transparent 1.5px 4px); +} +.win-title { + position: absolute; left: 50%; transform: translateX(-50%); + background: var(--card); padding: 0 11px; + font-family: var(--pixel); font-size: 0.6rem; letter-spacing: 0.04em; + white-space: nowrap; +} +.win-box { width: 11px; height: 11px; border: 2px solid var(--ink); background: #fff; flex: none; } +.win-body { padding: 24px; } + +/* ---------- Menu bar (nav) ---------- */ +.menubar { + position: sticky; top: 0; z-index: 60; + background: var(--card); + border-bottom: var(--line); +} +.menubar-inner { display: flex; align-items: center; height: 42px; gap: 4px; } +.brand { display: inline-flex; align-items: center; gap: 9px; font-weight: 700; font-size: 1.12rem; letter-spacing: -0.02em; margin-right: 18px; } +.brand svg { display: block; } +.brand b { color: var(--orange); font-weight: 700; } +.menu-links { display: flex; gap: 2px; } +.menu-links a { + font-family: var(--pixel); font-size: 0.62rem; letter-spacing: 0.03em; + padding: 7px 11px; border-radius: 5px; +} +.menu-links a:hover { background: var(--ink); color: var(--paper); } +.menu-right { margin-left: auto; display: flex; align-items: center; gap: 8px; } +.menu-ghost { + display: inline-flex; align-items: center; gap: 7px; + font-family: var(--pixel); font-size: 0.62rem; + border: var(--line); border-radius: 6px; padding: 6px 11px; + background: var(--card); box-shadow: var(--shadow-sm); + transition: transform .06s, box-shadow .06s; +} +.menu-ghost:hover { transform: translate(-1px,-1px); box-shadow: 4px 4px 0 var(--ink); } +.menu-toggle { display: none; } + +/* ---------- Section scaffolding ---------- */ +section { scroll-margin-top: 56px; } +.cardhead { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 30px; } +.cardnum { + font-family: var(--pixel); font-size: 0.66rem; + background: var(--ink); color: var(--paper); + padding: 5px 9px; border-radius: 5px; white-space: nowrap; +} +.cardhead h2 { flex: 1 1 320px; } +.cardhead p { width: 100%; color: var(--ink-soft); max-width: 62ch; } +.band { padding: clamp(54px, 8vw, 92px) 0; border-bottom: var(--line); } +.band-alt { background: rgba(0,0,0,0.025); } + +/* ---------- Hero ---------- */ +.hero { padding: clamp(40px, 6vw, 76px) 0 clamp(46px, 6vw, 80px); border-bottom: var(--line); } +.hero-grid { display: grid; grid-template-columns: 1fr 1.02fr; gap: clamp(30px, 5vw, 60px); align-items: center; } +.eyebrow { + display: inline-flex; align-items: center; gap: 8px; + font-family: var(--pixel); font-size: 0.62rem; letter-spacing: 0.03em; + border: var(--line); border-radius: 999px; padding: 6px 13px; + background: var(--card); box-shadow: var(--shadow-sm); margin-bottom: 24px; +} +.eyebrow .blip { width: 8px; height: 8px; background: var(--orange); border: 1.5px solid var(--ink); border-radius: 50%; } +.hero h1 { margin-bottom: 20px; } +.hero h1 .amp { color: var(--orange); font-style: italic; } +.lede { font-size: clamp(1.05rem, 1.5vw, 1.2rem); color: var(--ink-soft); max-width: 40ch; } +.hero-actions { display: flex; flex-wrap: wrap; gap: 13px; margin: 28px 0 22px; } +.hero-facts { display: flex; flex-wrap: wrap; gap: 8px 18px; padding: 0; margin: 0; list-style: none; } +.hero-facts li { font-family: var(--pixel); font-size: 0.58rem; letter-spacing: 0.02em; color: var(--ink-soft); display: flex; align-items: center; gap: 7px; } +.hero-facts li::before { content: ""; width: 7px; height: 7px; background: var(--ink); flex: none; } + +/* hero demo */ +.demo-frame { box-shadow: var(--shadow-lg); } +#physics { display: block; width: 100%; height: 348px; background: #fffdf7; touch-action: none; cursor: grab; } +#physics:active { cursor: grabbing; } +.demo-controls { + display: flex; align-items: center; gap: 9px; + padding: 10px 12px; border-top: var(--line); background: var(--card); +} +.chip { + font-family: var(--pixel); font-size: 0.56rem; + border: 1.5px solid var(--ink); border-radius: 5px; + background: #fff; padding: 6px 9px; cursor: pointer; + box-shadow: 2px 2px 0 var(--ink); + transition: transform .06s, box-shadow .06s; +} +.chip:hover { transform: translate(-1px,-1px); box-shadow: 3px 3px 0 var(--ink); } +.chip:active { transform: translate(2px,2px); box-shadow: 0 0 0 var(--ink); } +.demo-hint { margin-left: auto; font-family: var(--pixel); font-size: 0.52rem; color: var(--ink-faint); } +.demo-note { margin-top: 13px; text-align: center; font-size: 0.84rem; color: var(--ink-faint); } +.demo-note b { color: var(--orange-d); } + +/* ---------- Stats / info strip ---------- */ +.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } +.stat { + background: var(--card); border: var(--line); border-radius: var(--radius); + box-shadow: var(--shadow-sm); padding: 18px 16px; text-align: center; +} +.stat-num { display: block; font-size: clamp(1.7rem, 3vw, 2.4rem); font-weight: 700; letter-spacing: -0.03em; } +.stat-num .u { color: var(--orange); } +.stat-label { font-family: var(--pixel); font-size: 0.54rem; color: var(--ink-soft); letter-spacing: 0.02em; } + +/* ---------- One paste (code) ---------- */ +.paste-grid { display: grid; grid-template-columns: 0.92fr 1.08fr; gap: clamp(26px, 4vw, 52px); align-items: center; } +.paste-copy h2 { margin-bottom: 16px; } +.paste-copy > p { color: var(--ink-soft); } +.ticklist { list-style: none; padding: 0; margin: 22px 0 0; display: grid; gap: 11px; } +.ticklist li { position: relative; padding-left: 30px; color: var(--ink-soft); } +.ticklist li b { color: var(--ink); } +.ticklist li::before { + content: "x"; position: absolute; left: 0; top: 1px; + width: 20px; height: 20px; display: grid; place-items: center; + font-family: var(--pixel); font-size: 0.6rem; color: #fff; + background: var(--orange); border: 1.5px solid var(--ink); border-radius: 4px; +} +pre.code { margin: 0; padding: 18px 20px; overflow-x: auto; font-size: 0.8rem; line-height: 1.8; background: var(--card); } +pre.code code { color: var(--ink); } +.tok-kw { color: var(--blue); font-weight: 600; } +.tok-fn { color: var(--orange-d); font-weight: 600; } +.tok-str { color: var(--green); } +.tok-num { color: var(--red); } +.tok-com { color: var(--ink-faint); font-style: italic; } + +/* ---------- Features ---------- */ +.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; } +.feature { + background: var(--card); border: var(--line); border-radius: var(--radius); + box-shadow: var(--shadow); padding: 24px; + transition: transform .08s ease, box-shadow .08s ease; +} +.feature:hover { transform: translate(-2px,-2px); box-shadow: var(--shadow-lg); } +.badge { + width: 46px; height: 46px; display: grid; place-items: center; + border: var(--line); border-radius: 10px; box-shadow: 2px 2px 0 var(--ink); + margin-bottom: 16px; background: var(--orange); +} +.badge.b-blue { background: var(--blue); } +.badge.b-green { background: var(--green); } +.badge.b-red { background: var(--red); } +.badge.b-ink { background: var(--ink); } +.badge svg { display: block; } +.feature h3 { font-size: 1.16rem; margin-bottom: 9px; } +.feature p { color: var(--ink-soft); font-size: 0.96rem; } +.feature p b { color: var(--ink); } + +/* ---------- How it works: the literal stack ---------- */ +.stackdiag { max-width: 720px; margin: 0 auto; } +.layer { position: relative; margin-left: 0; } +.layer .win-body { padding: 18px 22px; } +.layer-tag { font-family: var(--pixel); font-size: 0.56rem; color: var(--ink-faint); display: block; margin-bottom: 5px; } +.layer h4 { font-family: var(--mono); font-size: 1rem; margin-bottom: 6px; } +.layer p { color: var(--ink-soft); font-size: 0.92rem; } +.layer p b { color: var(--ink); } +.flow { + display: flex; align-items: center; justify-content: center; gap: 10px; + padding: 9px 0; font-size: 0.82rem; color: var(--ink-faint); +} +.flow .pin { + font-family: var(--mono); font-weight: 600; color: #fff; background: var(--ink); + border-radius: 999px; padding: 2px 10px; font-size: 0.74rem; +} +.flow .arr { font-weight: 700; color: var(--ink); } +/* slight stagger so it reads as a stack of cards */ +.stackdiag .win:nth-child(1) { transform: rotate(-0.4deg); } +.stackdiag .win:nth-child(5) { transform: rotate(0.4deg); } +.stackdiag .win:nth-child(7) { transform: rotate(-0.3deg); } + +/* ---------- Examples ---------- */ +.ex-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; } +.ex { display: flex; flex-direction: column; box-shadow: var(--shadow); transition: transform .08s, box-shadow .08s; } +.ex:hover { transform: translate(-2px,-2px); box-shadow: var(--shadow-lg); } +.ex .win-body { display: flex; flex-direction: column; flex: 1; gap: 9px; padding: 18px 20px 20px; } +.ex-art { height: 128px; border-bottom: var(--line); position: relative; overflow: hidden; image-rendering: pixelated; } +.ex h3 { font-size: 1.1rem; } +.ex p { color: var(--ink-soft); font-size: 0.9rem; flex: 1; } +.ex p em { font-style: italic; color: var(--ink); } +.ex .go { font-family: var(--pixel); font-size: 0.56rem; color: var(--orange-d); } +/* flat 1-bit art fills */ +.art-demo { background: #fffdf7; background-image: radial-gradient(var(--orange) 26%, transparent 27%), radial-gradient(var(--blue) 26%, transparent 27%); background-size: 26px 26px; background-position: 0 0, 13px 13px; } +.art-contraption { background: #fffdf7 repeating-linear-gradient(45deg, var(--ink) 0 2px, transparent 2px 11px); } +.art-platformer { background: #fffdf7; background-image: linear-gradient(var(--green) 0 0); background-size: 100% 30px; background-position: 0 100%; background-repeat: no-repeat; } +.art-platformer::before { content:""; position:absolute; inset:0 0 30px 0; background: repeating-linear-gradient(90deg, transparent 0 30px, var(--ink) 30px 32px); opacity:.18; } +.art-slingshot { background: #fffdf7; background-image: radial-gradient(circle at 72% 64%, var(--red) 0 16px, transparent 17px), repeating-linear-gradient(0deg, var(--ink) 0 2px, transparent 2px 13px); } +.art-selftest { background: #fffdf7; } +.art-more { background: #fffdf7 repeating-linear-gradient(135deg, var(--ink) 0 2px, transparent 2px 11px); } +.art-glyph { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--pixel); color: var(--ink); } + +/* ---------- Docs ---------- */ +.docs-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; } +.doc { + display: block; background: var(--card); border: var(--line); border-radius: var(--radius-sm); + box-shadow: var(--shadow-sm); padding: 17px; + transition: transform .06s, box-shadow .06s; +} +.doc:hover { transform: translate(-1px,-1px); box-shadow: var(--shadow); } +.doc h4 { font-size: 0.98rem; margin-bottom: 5px; } +.doc p { font-size: 0.82rem; color: var(--ink-soft); } +.doc-all { background: var(--orange); color: #fff; } +.doc-all p { color: rgba(255,255,255,.85); } + +/* ---------- Get started ---------- */ +.steps { list-style: none; counter-reset: s; padding: 0; margin: 0 auto; max-width: 760px; display: grid; gap: 15px; } +.step { + display: flex; gap: 17px; align-items: flex-start; + background: var(--card); border: var(--line); border-radius: var(--radius); + box-shadow: var(--shadow-sm); padding: 20px 22px; +} +.step-n { + flex: none; width: 38px; height: 38px; display: grid; place-items: center; + font-family: var(--pixel); font-size: 0.9rem; color: #fff; + background: var(--ink); border-radius: 8px; +} +.step h4 { font-size: 1.04rem; margin-bottom: 4px; } +.step p { color: var(--ink-soft); font-size: 0.93rem; } +.step a { color: var(--orange-d); font-weight: 600; text-decoration: underline; text-decoration-thickness: 2px; } +.start-cta { display: flex; flex-wrap: wrap; gap: 13px; justify-content: center; margin-top: 32px; } + +/* ---------- Footer ---------- */ +.footer { padding: 50px 0 30px; } +.footer-inner { display: grid; grid-template-columns: 1.5fr 2fr; gap: 36px; } +.footer-brand .brand { font-size: 1.2rem; } +.footer-brand p { color: var(--ink-soft); font-size: 0.9rem; margin-top: 12px; max-width: 42ch; } +.footer-brand a { color: var(--orange-d); text-decoration: underline; } +.footer-cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; } +.footer-cols h5 { font-family: var(--pixel); font-size: 0.56rem; color: var(--ink-faint); margin: 0 0 12px; } +.footer-cols a { display: block; color: var(--ink-soft); font-size: 0.9rem; padding: 4px 0; } +.footer-cols a:hover { color: var(--ink); text-decoration: underline; } +.footer-bottom { + display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; + margin-top: 40px; padding-top: 20px; border-top: var(--line); + font-family: var(--pixel); font-size: 0.56rem; color: var(--ink-faint); +} +.footer-bottom a:hover { color: var(--ink); } + +/* =========================================================== + Responsive + =========================================================== */ +@media (max-width: 920px) { + .hero-grid, .paste-grid { grid-template-columns: 1fr; } + .hero-demo { order: -1; } + .lede { max-width: none; } + .feature-grid, .ex-grid { grid-template-columns: repeat(2, 1fr); } + .docs-grid { grid-template-columns: repeat(2, 1fr); } + .footer-inner { grid-template-columns: 1fr; } +} +@media (max-width: 680px) { + body { font-size: 16px; } + .menu-links, .menu-right { display: none; } + .menu-links.open { + display: flex; flex-direction: column; gap: 0; + position: absolute; top: 42px; left: 0; right: 0; + background: var(--card); border-bottom: var(--line); box-shadow: var(--shadow); + padding: 6px; + } + .menu-links.open a { padding: 12px 14px; } + .menu-toggle { + display: inline-flex; margin-left: auto; align-items: center; justify-content: center; + width: 38px; height: 30px; border: var(--line); border-radius: 6px; + background: var(--card); box-shadow: var(--shadow-sm); cursor: pointer; + font-family: var(--pixel); font-size: 0.7rem; + } + .stats-grid { grid-template-columns: repeat(2, 1fr); } + .feature-grid, .ex-grid, .docs-grid { grid-template-columns: 1fr; } + .footer-cols { grid-template-columns: 1fr 1fr; } + .stackdiag .win { transform: none !important; } + #physics { height: 300px; } +} + +/* =========================================================== + Documentation pages (rendered from docs/*.md) + =========================================================== */ +.doc-wrap { + display: grid; grid-template-columns: 232px minmax(0, 1fr); + gap: 28px; align-items: start; + max-width: var(--maxw); margin-inline: auto; + padding: 28px 22px 64px; +} +.doc-side { position: sticky; top: 54px; } +.doc-side-inner { + background: var(--card); border: var(--line); border-radius: var(--radius); + box-shadow: var(--shadow-sm); padding: 14px; +} +.doc-side .label { display: block; color: var(--ink-faint); margin: 2px 0 10px; } +.doc-side a { + display: block; padding: 7px 10px; border-radius: 6px; + font-size: 0.92rem; color: var(--ink-soft); +} +.doc-side a:hover { background: rgba(0,0,0,0.05); color: var(--ink); } +.doc-side a.active { background: var(--orange); color: #fff; font-weight: 600; } + +.doc-win { box-shadow: var(--shadow); min-width: 0; } +.doc-win .win-body { padding: clamp(20px, 3vw, 36px); } + +/* prose */ +.prose { color: var(--ink-soft); font-size: 1rem; line-height: 1.72; } +.prose > :first-child { margin-top: 0; } +.prose h1, .prose h2, .prose h3, .prose h4, .prose h5 { color: var(--ink); } +.prose h1 { font-size: clamp(1.8rem, 3.5vw, 2.3rem); margin: 0 0 16px; } +.prose h2 { font-size: 1.5rem; margin: 40px 0 14px; padding-bottom: 8px; border-bottom: var(--line); } +.prose h3 { font-size: 1.2rem; margin: 30px 0 10px; } +.prose h4 { font-size: 1.04rem; margin: 22px 0 8px; } +.prose h2, .prose h3, .prose h4 { scroll-margin-top: 54px; } +.prose p { margin: 0 0 15px; } +.prose strong { color: var(--ink); } +.prose a { color: var(--orange-d); text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 2px; } +.prose a:hover { background: var(--hl); text-decoration: none; } +.prose ul, .prose ol { margin: 0 0 15px; padding-left: 24px; } +.prose li { margin: 5px 0; } +.prose li > ul, .prose li > ol { margin: 5px 0; } +.prose ul li::marker { color: var(--orange); } +.prose ol li::marker { color: var(--orange-d); font-family: var(--mono); font-weight: 600; } +.prose pre { + margin: 0 0 16px; padding: 15px 18px; overflow-x: auto; + background: var(--card); border: var(--line); border-radius: var(--radius-sm); + box-shadow: var(--shadow-sm); +} +.prose pre code { font-family: var(--mono); font-size: 0.82rem; line-height: 1.65; background: none; border: 0; padding: 0; color: var(--ink); } +.prose blockquote { + margin: 0 0 16px; padding: 12px 16px; + border: var(--line); border-left: 5px solid var(--orange); + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + background: rgba(232,112,42,0.07); +} +.prose blockquote > :last-child { margin-bottom: 0; } +.prose hr { border: 0; border-top: 2px dashed var(--ink); opacity: 0.35; margin: 28px 0; } +.prose img { max-width: 100%; } +.md-tablewrap { overflow-x: auto; margin: 0 0 18px; } +.prose table { + border-collapse: collapse; width: 100%; font-size: 0.9rem; + background: var(--card); border: var(--line); border-radius: var(--radius-sm); overflow: hidden; +} +.prose th, .prose td { padding: 8px 12px; border: 1.5px solid var(--ink); text-align: left; vertical-align: top; } +.prose thead th { background: var(--ink); color: var(--paper); font-family: var(--pixel); font-size: 0.6rem; letter-spacing: 0.03em; } +.prose tbody tr:nth-child(even) { background: rgba(0,0,0,0.03); } +.prose .docs-grid h4 { color: var(--ink); margin: 0 0 5px; } + +.doc-side-gh { + margin-top: 8px; padding-top: 10px !important; + border-top: 1.5px dashed var(--ink); + color: var(--orange-d) !important; font-weight: 600; +} + +/* pared-down docs section on the home page: 3 guides + a deeper link */ +.docs-grid-3 { grid-template-columns: repeat(3, 1fr); } +.docs-more { margin-top: 18px; text-align: center; color: var(--ink-soft); font-size: 0.93rem; } +.docs-more a { color: var(--orange-d); font-weight: 600; text-decoration: underline; text-decoration-thickness: 2px; } +.prose .docs-grid-3 .doc h4 { font-size: 1rem; } + +@media (max-width: 920px) { + .doc-wrap { grid-template-columns: 1fr; gap: 18px; } + .doc-side { position: static; } + .doc-side-inner { display: flex; flex-wrap: wrap; gap: 4px; } + .doc-side .label { width: 100%; } + .doc-side a { font-size: 0.84rem; padding: 6px 9px; } + .doc-side-gh { flex-basis: 100%; } + .docs-grid-3 { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 680px) { + .docs-grid-3 { grid-template-columns: 1fr; } +} + +/* =========================================================== + The HyperCard stack (home page) + =========================================================== */ +.desktop { + min-height: calc(100vh - 44px); + display: flex; flex-direction: column; + align-items: center; justify-content: center; + gap: 12px; padding: 20px 18px 14px; +} +.stack-deck { position: relative; width: 100%; max-width: 1060px; } +/* the "rest of the stack" peeking out behind the active card */ +.stack-deck::before, .stack-deck::after { + content: ""; position: absolute; inset: 0; z-index: 0; + background: var(--card); border: var(--line); border-radius: var(--radius); +} +.stack-deck::after { transform: translate(6px, 7px) rotate(-0.5deg); } +.stack-deck::before { transform: translate(12px, 13px) rotate(0.55deg); } +.stack-win { position: relative; z-index: 1; display: flex; flex-direction: column; box-shadow: var(--shadow-lg); } + +.win-home { cursor: pointer; padding: 0; transition: background .12s; } +.win-home:hover, .win-home:focus-visible { background: var(--orange); outline: none; } + +.stack-body { position: relative; background: var(--card); } +.card { padding: clamp(22px, 3.2vw, 40px); } +.card-h { font-size: clamp(1.55rem, 3.2vw, 2.35rem); margin-bottom: 14px; } +.card-lead { color: var(--ink-soft); max-width: 64ch; margin-bottom: 22px; } +.card .cardnum { display: inline-block; margin-bottom: 14px; } + +/* single-card mode (JS on) */ +.js .stack-body { height: min(650px, 72vh); overflow: hidden; } +.js .card { + position: absolute; inset: 0; overflow-y: auto; + opacity: 0; visibility: hidden; +} +.js .card.is-active { opacity: 1; visibility: visible; z-index: 2; } +@media (prefers-reduced-motion: no-preference) { + .js .card { transition: opacity .3s ease, transform .3s ease; } +} + +/* HOME card */ +.home-grid { display: grid; grid-template-columns: 1.04fr 1fr; gap: clamp(22px, 3.5vw, 46px); align-items: center; } +.home-copy h1 { font-size: clamp(2rem, 4.4vw, 3.3rem); margin-bottom: 16px; } +.home-copy h1 .amp { color: var(--orange); font-style: italic; } +.flip-hint { margin-top: 20px; font-size: 0.82rem; color: var(--ink-faint); line-height: 1.9; } +kbd { + font-family: var(--mono); font-size: 0.74em; color: var(--ink); + background: #fff; border: 1.5px solid var(--ink); border-bottom-width: 3px; + border-radius: 5px; padding: 1px 6px; margin: 0 1px; +} +.launcher { + display: flex; flex-wrap: wrap; align-items: center; gap: 9px; + margin-top: 26px; padding-top: 20px; border-top: 2px dashed rgba(23,20,13,.25); +} +.launcher .label { color: var(--ink-faint); margin-right: 4px; } +.lbtn { + font-family: var(--sans); font-weight: 600; font-size: 0.86rem; color: var(--ink); + background: var(--card); border: var(--line); border-radius: 999px; + padding: 7px 15px; cursor: pointer; box-shadow: 2px 2px 0 var(--ink); + transition: transform .06s, box-shadow .06s, background .12s, color .12s; +} +.lbtn:hover { transform: translate(-1px,-1px); box-shadow: 3px 3px 0 var(--ink); background: var(--orange); color: #fff; } +.lbtn:active { transform: translate(2px,2px); box-shadow: 0 0 0 var(--ink); } + +.card-next { margin-top: 22px; color: var(--ink-soft); } +.link-go { + background: none; border: 0; cursor: pointer; padding: 0; font: inherit; + color: var(--orange-d); font-weight: 600; + text-decoration: underline; text-decoration-thickness: 2px; +} +.about-ticks { margin-top: 8px; } + +/* card navigation bar */ +.cardbar { + display: flex; align-items: center; gap: 8px; + padding: 9px 12px; border-top: var(--line); background: var(--card); +} +.navbtn { + font-family: var(--sans); font-weight: 600; font-size: 0.82rem; color: var(--ink); + background: #fff; border: 1.5px solid var(--ink); border-radius: 7px; + padding: 6px 12px; cursor: pointer; box-shadow: 2px 2px 0 var(--ink); + transition: transform .06s, box-shadow .06s, background .12s, color .12s; +} +.navbtn:hover { transform: translate(-1px,-1px); box-shadow: 3px 3px 0 var(--ink); background: var(--orange); color: #fff; } +.navbtn:active { transform: translate(2px,2px); box-shadow: 0 0 0 var(--ink); } +.nav-arrow { font-family: var(--mono); } +.cardcount { font-family: var(--pixel); font-size: 0.6rem; color: var(--ink-soft); padding: 0 6px; white-space: nowrap; } +.cc-sep { color: var(--ink-faint); } +.msg-toggle { margin-left: auto; } + +/* the message box (HyperTalk-ish console) */ +.msgbox[hidden] { display: none; } +.msgbox { + display: flex; align-items: center; gap: 9px; + padding: 9px 12px; border-top: var(--line); + background: repeating-linear-gradient(45deg, rgba(232,112,42,.06) 0 8px, transparent 8px 16px), var(--card); +} +.msgbox .label { color: var(--ink-faint); flex: none; } +#msgInput { + flex: 1; min-width: 0; font-family: var(--mono); font-size: 0.86rem; color: var(--ink); + background: #fff; border: 1.5px solid var(--ink); border-radius: 6px; padding: 7px 11px; +} +#msgInput:focus { outline: 2px solid var(--orange); outline-offset: 1px; } +.msg-out { font-family: var(--mono); font-size: 0.76rem; color: var(--orange-d); flex: none; max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* desktop footer line + menu active state */ +.deskfoot { display: flex; gap: 10px; justify-content: center; align-items: center; flex-wrap: wrap; font-family: var(--pixel); font-size: 0.54rem; color: var(--ink-faint); text-align: center; } +.deskfoot a { color: var(--ink-soft); } +.deskfoot a:hover { color: var(--ink); } +.deskfoot-sep { opacity: .5; } +.menu-links a.active { background: var(--ink); color: var(--paper); } + +@media (max-width: 920px) { + .home-grid { grid-template-columns: 1fr; } + .home-demo { order: -1; } +} +@media (max-width: 680px) { + .desktop { min-height: 0; padding: 14px 12px; justify-content: flex-start; } + .js .stack-body { height: min(560px, 78vh); } + .stack-deck::before, .stack-deck::after { display: none; } + .lbtn { font-size: 0.8rem; padding: 6px 12px; } + .cardbar { flex-wrap: wrap; } + .msg-out { display: none; } +}