From b7c428f6dfec67ec1874b426dd5644d471e368e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:33:08 +0000 Subject: [PATCH 1/6] Add project marketing website MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A self-contained, dependency-free landing page for Box2Dxt in website/: - index.html / styles.css / app.js — modern dark theme, fully responsive, with an interactive hero physics toy (a hand-written impulse-based circle solver: gravity, walls, ball-ball collisions, mouse/touch grab + fling, click-to-spawn) that demonstrates the project's premise viscerally. - Sections cover ease of use (the sixty-second b2kQuickStart snippet, paste-and-run examples), abilities (full Box2D surface, the Game Kit, the safety model), the three-layer architecture, all six examples, the docs, and a four-step get-started guide. - website/README.md documents local preview and deployment. - .github/workflows/pages.yml publishes website/ to GitHub Pages on push to main (enable Pages with the GitHub Actions source once). No build step; opens directly in a browser and degrades gracefully to system fonts offline. --- .github/workflows/pages.yml | 37 +++ website/README.md | 63 +++++ website/app.js | 317 +++++++++++++++++++++++++ website/index.html | 434 +++++++++++++++++++++++++++++++++++ website/styles.css | 444 ++++++++++++++++++++++++++++++++++++ 5 files changed, 1295 insertions(+) create mode 100644 .github/workflows/pages.yml create mode 100644 website/README.md create mode 100644 website/app.js create mode 100644 website/index.html create mode 100644 website/styles.css diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..4e6e40b --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,37 @@ +# 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/**" + - ".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 + - uses: actions/upload-pages-artifact@v3 + with: + path: website + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..c39b3d7 --- /dev/null +++ b/website/README.md @@ -0,0 +1,63 @@ +# 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. + +``` +website/ +├── index.html # the page +├── styles.css # dark, modern theme (palette echoes the project's own demos) +└── app.js # mobile nav + the interactive hero physics toy +``` + +## 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`. +- The colour palette and layout are CSS custom properties at the top of + `styles.css` (`--orange`, `--teal`, `--purple`, …). +- The hero demo is a compact impulse-based circle solver in `app.js`; tune the + constants near the top (`GRAV`, `REST`, `MAX_BODIES`, …). + +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..55c6d36 --- /dev/null +++ b/website/app.js @@ -0,0 +1,317 @@ +/* =========================================================== + Box2Dxt site — interactive bits + 1) Mobile nav toggle + 2) A tiny self-contained 2D physics toy for the hero + (impulse-based circle solver: gravity, walls, ball-ball + collisions, mouse/touch grab + fling, click-to-spawn). + No libraries. This is a *toy* — Box2Dxt is the real engine. + =========================================================== */ +(function () { + "use strict"; + + /* ---------- Mobile nav ---------- */ + var toggle = document.getElementById("navToggle"); + var links = document.querySelector(".nav-links"); + if (toggle && links) { + toggle.addEventListener("click", function () { + var open = links.classList.toggle("open"); + toggle.setAttribute("aria-expanded", open ? "true" : "false"); + }); + links.addEventListener("click", function (e) { + if (e.target.tagName === "A") { + links.classList.remove("open"); + toggle.setAttribute("aria-expanded", "false"); + } + }); + } + + /* ---------- Physics toy ---------- */ + var canvas = document.getElementById("physics"); + if (!canvas || !canvas.getContext) return; + var ctx = canvas.getContext("2d"); + + var COLORS = ["#ff7a45", "#2dd4bf", "#a78bfa", "#ffd479", "#ff5e62", "#5b8cff"]; + var GRAV = 1700; // px / s^2 + var REST = 0.16; // ball-ball restitution (low → settles into a pile) + var WALL_REST = 0.32; // wall bounce + var FRICTION = 0.04; // tangential damping on contact + var SLOP = 0.5; // penetration allowance + var CORRECT = 0.8; // positional correction factor + var ITER = 6; // solver iterations per step + var DT = 1 / 120; // fixed timestep + var MAXV = 2600; // velocity clamp (anti-tunnel) + var MAX_BODIES = 26; + + var W = 0, H = 0, dpr = 1; + var bodies = []; + var gravityOn = true; + var held = null, heldSavedInv = 0; + var pointer = { x: 0, y: 0, active: false }; + var interacted = false; + var acc = 0, last = 0, running = false; + + function rand(a, b) { return a + Math.random() * (b - a); } + + 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); + // keep existing bodies inside the new bounds + for (var i = 0; i < bodies.length; i++) { + var b = bodies[i]; + b.x = Math.min(Math.max(b.r, b.x), W - b.r); + b.y = Math.min(Math.max(b.r, b.y), H - b.r); + } + } + + function makeBody(x, y, r, color) { + return { + x: x, y: y, vx: rand(-40, 40), vy: rand(0, 40), + r: r, color: color || COLORS[(Math.random() * COLORS.length) | 0], + invMass: 1 / (r * r) + }; + } + + function seed() { + bodies = []; + var n = W < 420 ? 9 : 13; + for (var i = 0; i < n; i++) { + var r = rand(13, 26); + bodies.push(makeBody(rand(r, W - r), rand(-H, H * 0.4), r)); + } + } + + function spawnAt(x, y) { + if (bodies.length >= MAX_BODIES) bodies.shift(); + var r = rand(13, 26); + var b = makeBody(x, y, r); + b.vx = rand(-60, 60); b.vy = rand(-40, 40); + bodies.push(b); + } + + /* one fixed physics step */ + function step() { + var i, j, b; + + // integrate + for (i = 0; i < bodies.length; i++) { + b = bodies[i]; + if (b === held) continue; // held body is driven by the pointer + if (gravityOn) b.vy += GRAV * DT; + b.x += b.vx * DT; + b.y += b.vy * DT; + // clamp speed + var sp = Math.hypot(b.vx, b.vy); + if (sp > MAXV) { b.vx *= MAXV / sp; b.vy *= MAXV / sp; } + } + + // drive held body from the pointer; give it velocity for the fling + 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; + } + + // collisions (several iterations for a stable pile) + for (var it = 0; it < ITER; it++) { + // ball vs ball + 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; + var dist = Math.hypot(dx, dy); + var min = a.r + c.r; + if (dist >= min || dist === 0) continue; + + var nx = dx / dist, ny = dy / dist; + var pen = min - dist; + var im = a.invMass + c.invMass; + + // positional correction + 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; + + // velocity impulse + var rvx = c.vx - a.vx, rvy = c.vy - a.vy; + var 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; + // tangential friction + var tx = -ny, ty = nx; + var 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; + } + } + } + + // walls + 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; } + else if (b.x > W - b.r) { b.x = W - b.r; if (b.vx > 0) b.vx = -b.vx * WALL_REST; } + 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; // floor friction so the pile settles + } + } + } + } + + function draw() { + ctx.clearRect(0, 0, W, H); + for (var i = 0; i < bodies.length; i++) { + var b = bodies[i]; + // soft shadow + ctx.beginPath(); + ctx.arc(b.x, b.y + 3, b.r, 0, Math.PI * 2); + ctx.fillStyle = "rgba(0,0,0,0.18)"; + ctx.fill(); + // ball with a top-left highlight for depth + var g = ctx.createRadialGradient( + b.x - b.r * 0.35, b.y - b.r * 0.4, b.r * 0.1, + b.x, b.y, b.r + ); + g.addColorStop(0, lighten(b.color, 0.35)); + g.addColorStop(1, b.color); + ctx.beginPath(); + ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); + ctx.fillStyle = g; + ctx.fill(); + ctx.lineWidth = 1; + ctx.strokeStyle = "rgba(0,0,0,0.22)"; + ctx.stroke(); + } + } + + // quick hex lighten + function lighten(hex, amt) { + var n = parseInt(hex.slice(1), 16); + var r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; + r = Math.round(r + (255 - r) * amt); + g = Math.round(g + (255 - g) * amt); + b = Math.round(b + (255 - b) * amt); + return "rgb(" + r + "," + g + "," + b + ")"; + } + + function frame(t) { + if (!running) return; + if (!last) last = t; + var elapsed = (t - last) / 1000; + last = t; + acc += Math.min(elapsed, 0.05); // cap to avoid spiral of death + var guard = 0; + while (acc >= DT && guard < 8) { step(); acc -= DT; guard++; } + draw(); + requestAnimationFrame(frame); + } + + function start() { + if (running) return; + running = true; last = 0; + requestAnimationFrame(frame); + } + function stop() { running = false; } + + /* ---------- Pointer interaction ---------- */ + function canvasPoint(e) { + var rect = canvas.getBoundingClientRect(); + var cx = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left; + var cy = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top; + return { x: cx, y: cy }; + } + + function onDown(e) { + var p = canvasPoint(e); + pointer.x = p.x; pointer.y = p.y; pointer.active = true; + interacted = true; hideHint(); + + // grab the nearest body under the pointer + var best = null, bestD = Infinity; + for (var i = 0; i < bodies.length; i++) { + var b = bodies[i]; + var 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; // immovable while held → shoves others, stays put + } else { + spawnAt(p.x, p.y); // empty space → drop a new body + } + e.preventDefault(); + } + + function onMove(e) { + if (!pointer.active) return; + var p = canvasPoint(e); + pointer.x = p.x; pointer.y = p.y; + e.preventDefault(); + } + + function onUp() { + pointer.active = false; + if (held) { + held.invMass = heldSavedInv; // restore mass; keeps tracked velocity → fling + 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); + + /* ---------- Controls ---------- */ + 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"; } } + + if (resetBtn) resetBtn.addEventListener("click", function () { seed(); interacted = true; hideHint(); }); + if (gravBtn) gravBtn.addEventListener("click", function () { + gravityOn = !gravityOn; + gravBtn.textContent = "Gravity: " + (gravityOn ? "on" : "off"); + if (!gravityOn) { for (var i = 0; i < bodies.length; i++) { bodies[i].vy -= 120; } } // little float-up nudge + interacted = true; hideHint(); + }); + + /* ---------- Lifecycle: only run while visible ---------- */ + resize(); + seed(); + window.addEventListener("resize", resize); + + if ("IntersectionObserver" in window) { + var io = new IntersectionObserver(function (entries) { + if (entries[0].isIntersecting) start(); else stop(); + }, { threshold: 0.05 }); + io.observe(canvas); + } else { + start(); + } + document.addEventListener("visibilitychange", function () { + if (document.hidden) stop(); else start(); + }); + + // auto-hide the hint after a few seconds even without interaction + setTimeout(function () { if (!interacted) hideHint(); }, 6000); +})(); diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..67f8692 --- /dev/null +++ b/website/index.html @@ -0,0 +1,434 @@ + + + + + +Box2Dxt — Real 2D physics for OpenXTalk & xTalk + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Box2D v3.1.0 · OpenXTalk & LiveCode 9.6.3+ +

Real 2D physics
for xTalk.

+

+ Box2Dxt drops the battle-tested Box2D engine — the one behind + countless games — straight into OpenXTalk and LiveCode. + Write plain xTalk in pixels and degrees; your controls fall, roll, bounce, + hinge, and collide. +

+ +
    +
  • MIT licensed
  • +
  • Windows · macOS · Linux
  • +
  • Prebuilt & ready
  • +
+
+ +
+
+
+ + live physics — drag the shapes +
+ +
+ + + click empty space to drop a body +
+
+

This very widget is a hand-written toy solver — Box2Dxt gives your xTalk app the real thing.

+
+
+
+ + +
+
+
370+engine handlers
+
300+friendly Kit calls
+
6paste-and-run examples
+
~125self-tests, one click
+
+
+ + +
+
+
+ Sixty seconds to a scene +

One paste. A world that falls.

+

+ The Kit owns the world, gravity, the fixed-timestep loop, and + per-frame redraws — so you don't. b2kQuickStart spins up a world with + gravity and card-edge walls, then every b2kSpawn… creates a control + and its physics body in one call. Grab and fling with the mouse in three lines. +

+
    +
  • Speaks pixels & degrees — the units you already think in.
  • +
  • Bodies are bound to ordinary LiveCode controls.
  • +
  • No C toolchain required — drop in a prebuilt library and go.
  • +
+
+ +
+
livecodescript
+
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
+
+
+
+ + +
+
+
+ What you get +

Approachable on the surface. Serious underneath.

+

A friendly layer for everyday use, the full engine when you need it, and a game toolkit on top.

+
+ +
+
+
+

Friendly by default

+

The Kit speaks pixels and degrees, binds bodies to your controls, and runs the loop for you. b2kQuickStart gives you a live, draggable world in a single 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 — getters return zero, actions do nothing. Never a crash.

+
+ +
+
💻
+

Cross-platform, prebuilt

+

Drop-in native 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. Paste it into a stack and it runs — no setup, no external assets required. Read one as a worked tutorial.

+
+
+
+
+ + +
+
+
+ How it works +

Three clean layers, one native library

+

Call the friendly layer and ignore the rest — or reach down a level whenever you need to.

+
+ +
+
+
+ your code +

Your xTalk script

+

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

+
+
+
b2k… pixels · degrees · screen coords
+ +
+
+ the Kit +

box2dxt-kit.livecodescript

+

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
+ +
+
+ the extension +

box2dxt.lcb

+

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

+
+
+
FFI ints & doubles · opaque handles
+ +
+
+ native +

box2d_lc.c + Box2D v3.1.0

+

The C shim and the engine, compiled into one shared library. Handles are stored in a table, validated, and generation-tagged for safety.

+
+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ Get started +

Up and running in four steps

+

No C toolchain needed — grab a prebuilt library and paste a script.

+
+ +
    +
  1. + 1 +
    +

    Get the native library

    +

    Download the file for your platform from prebuilt/ and rename it to the bare name the loader expects: box2dxt.dll / box2dxt.dylib / box2dxt.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. +
+ + +
+
+ + + + + + + diff --git a/website/styles.css b/website/styles.css new file mode 100644 index 0000000..3fd278d --- /dev/null +++ b/website/styles.css @@ -0,0 +1,444 @@ +/* =========================================================== + Box2Dxt marketing site — styles + Dark, modern, dependency-free. Palette echoes the project's + own code samples: orange boxes, teal balls, purple polys. + =========================================================== */ + +:root { + --bg: #0a0e17; + --bg-2: #0d1220; + --surface: #121a2b; + --surface-2: #161f33; + --border: #233049; + --border-2: #2c3a57; + + --text: #e7ecf6; + --muted: #9aa7c2; + --faint: #6c7997; + + --orange: #ff7a45; + --orange-2: #ff5e62; + --teal: #2dd4bf; + --teal-2: #22d3ee; + --purple: #a78bfa; + + --grad: linear-gradient(100deg, var(--orange), var(--orange-2) 55%, var(--purple)); + --radius: 16px; + --radius-sm: 10px; + --maxw: 1120px; + --shadow: 0 24px 60px -28px rgba(0,0,0,.7); + + --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + overflow-x: hidden; +} + +/* Ambient background glows */ +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + background: + radial-gradient(60% 50% at 80% -5%, rgba(255,122,69,.16), transparent 60%), + radial-gradient(50% 45% at 5% 8%, rgba(45,212,191,.12), transparent 60%), + radial-gradient(60% 60% at 50% 110%, rgba(167,139,250,.10), transparent 60%); + pointer-events: none; +} + +a { color: inherit; text-decoration: none; } + +.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: 24px; } + +h1, h2, h3, h4 { line-height: 1.12; letter-spacing: -0.02em; margin: 0; font-weight: 700; } +h1 { font-size: clamp(2.5rem, 6vw, 4.1rem); font-weight: 800; } +h2 { font-size: clamp(1.7rem, 3.6vw, 2.6rem); } +p { margin: 0; } + +code, pre, .mono { font-family: var(--mono); } +:not(pre) > code { + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.1em 0.4em; + font-size: 0.86em; + color: #ffd9c4; +} + +.grad { + background: var(--grad); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +/* ---------- Buttons ---------- */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 0.97rem; + padding: 12px 22px; + border-radius: 999px; + border: 1px solid transparent; + cursor: pointer; + transition: transform .15s ease, box-shadow .2s ease, background .2s ease, border-color .2s ease; + white-space: nowrap; +} +.btn:active { transform: translateY(1px); } +.btn-primary { + background: var(--grad); + color: #1a0f08; + box-shadow: 0 10px 30px -10px rgba(255,122,69,.6); +} +.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 16px 36px -10px rgba(255,122,69,.7); } +.btn-ghost { + background: rgba(255,255,255,.03); + border-color: var(--border-2); + color: var(--text); +} +.btn-ghost:hover { background: rgba(255,255,255,.07); border-color: var(--teal); } + +/* ---------- Nav ---------- */ +.nav { + position: sticky; + top: 0; + z-index: 50; + backdrop-filter: blur(12px); + background: rgba(10,14,23,.72); + border-bottom: 1px solid var(--border); +} +.nav-inner { display: flex; align-items: center; gap: 20px; height: 64px; } +.brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 800; font-size: 1.15rem; letter-spacing: -0.02em; } +.brand-mark { display: block; } +.brand-accent { color: var(--orange); } +.nav-links { display: flex; gap: 26px; margin-left: 14px; } +.nav-links a { color: var(--muted); font-size: 0.95rem; font-weight: 500; transition: color .15s; } +.nav-links a:hover { color: var(--text); } +.nav-cta { margin-left: auto; } +.nav-toggle { display: none; } + +/* ---------- Hero ---------- */ +.hero { padding: clamp(48px, 8vw, 96px) 0 clamp(40px, 6vw, 80px); } +.hero-grid { + display: grid; + grid-template-columns: 1.05fr 1fr; + gap: clamp(32px, 5vw, 64px); + align-items: center; +} +.eyebrow { + display: inline-block; + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--teal); + background: rgba(45,212,191,.08); + border: 1px solid rgba(45,212,191,.25); + padding: 6px 12px; + border-radius: 999px; + margin-bottom: 22px; +} +.hero h1 { margin-bottom: 20px; } +.lede { color: var(--muted); font-size: clamp(1.05rem, 1.6vw, 1.22rem); max-width: 36ch; } +.hero-copy strong { color: var(--text); font-weight: 600; } +.hero-actions { display: flex; flex-wrap: wrap; gap: 14px; margin: 30px 0 24px; } +.hero-badges { list-style: none; display: flex; flex-wrap: wrap; gap: 10px 22px; padding: 0; margin: 0; } +.hero-badges li { color: var(--faint); font-size: 0.88rem; position: relative; padding-left: 18px; } +.hero-badges li::before { + content: ""; + position: absolute; left: 0; top: 50%; transform: translateY(-50%); + width: 7px; height: 7px; border-radius: 50%; background: var(--teal); +} + +/* ---------- Hero demo ---------- */ +.demo-frame { + background: linear-gradient(180deg, var(--surface), var(--bg-2)); + border: 1px solid var(--border-2); + border-radius: var(--radius); + overflow: hidden; + box-shadow: var(--shadow); +} +.demo-titlebar { + display: flex; align-items: center; gap: 7px; + padding: 11px 14px; + background: rgba(255,255,255,.025); + border-bottom: 1px solid var(--border); +} +.demo-titlebar .dot { width: 11px; height: 11px; border-radius: 50%; background: #34405c; } +.demo-titlebar .dot:nth-child(1) { background: #ff5f57; } +.demo-titlebar .dot:nth-child(2) { background: #febc2e; } +.demo-titlebar .dot:nth-child(3) { background: #28c840; } +.demo-title { margin-left: 8px; font-size: 0.78rem; color: var(--faint); font-family: var(--mono); } +#physics { display: block; width: 100%; height: 340px; touch-action: none; cursor: grab; } +#physics:active { cursor: grabbing; } +.demo-controls { + display: flex; align-items: center; gap: 10px; + padding: 10px 14px; + border-top: 1px solid var(--border); + background: rgba(255,255,255,.02); +} +.chip { + font-family: var(--mono); + font-size: 0.76rem; + color: var(--muted); + background: var(--surface-2); + border: 1px solid var(--border-2); + border-radius: 7px; + padding: 5px 11px; + cursor: pointer; + transition: border-color .15s, color .15s; +} +.chip:hover { color: var(--text); border-color: var(--teal); } +.demo-hint { margin-left: auto; font-size: 0.74rem; color: var(--faint); font-family: var(--mono); } +.demo-note { margin-top: 14px; font-size: 0.82rem; color: var(--faint); text-align: center; } +.demo-note em { color: var(--orange); font-style: normal; font-weight: 600; } + +/* ---------- Stats ---------- */ +.stats { padding: 18px 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); background: rgba(255,255,255,.012); } +.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; text-align: center; } +.stat { display: flex; flex-direction: column; gap: 2px; padding: 10px; } +.stat-num { font-size: clamp(1.6rem, 3vw, 2.3rem); font-weight: 800; letter-spacing: -0.03em; background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; } +.stat-label { font-size: 0.85rem; color: var(--muted); } + +/* ---------- Section scaffolding ---------- */ +section { scroll-margin-top: 80px; } +.section-head { max-width: 640px; margin: 0 auto clamp(34px, 5vw, 54px); text-align: center; } +.kicker { display: inline-block; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--orange); margin-bottom: 14px; } +.section-sub { color: var(--muted); margin-top: 14px; font-size: 1.05rem; } +.sixty, .features, .how, .examples, .docs, .start { padding: clamp(56px, 8vw, 100px) 0; } +.features, .examples { border-top: 1px solid var(--border); } + +/* ---------- Sixty seconds ---------- */ +.sixty-grid { display: grid; grid-template-columns: 1fr 1.1fr; gap: clamp(28px, 5vw, 56px); align-items: center; } +.sixty .kicker { color: var(--teal); } +.sixty h2 { margin-bottom: 18px; } +.sixty-copy > p { color: var(--muted); } +.sixty-copy strong { color: var(--text); } +.ticklist { list-style: none; padding: 0; margin: 24px 0 0; display: grid; gap: 12px; } +.ticklist li { position: relative; padding-left: 30px; color: var(--muted); } +.ticklist li strong { color: var(--text); font-weight: 600; } +.ticklist li::before { + content: "✓"; + position: absolute; left: 0; top: 0; + width: 20px; height: 20px; + display: grid; place-items: center; + font-size: 0.7rem; font-weight: 800; + color: var(--teal); + background: rgba(45,212,191,.12); + border: 1px solid rgba(45,212,191,.3); + border-radius: 6px; +} + +/* ---------- Code card ---------- */ +.code-card { + background: var(--bg-2); + border: 1px solid var(--border-2); + border-radius: var(--radius); + overflow: hidden; + box-shadow: var(--shadow); +} +.code-head { display: flex; align-items: center; padding: 10px 16px; background: rgba(255,255,255,.025); border-bottom: 1px solid var(--border); } +.code-lang { font-family: var(--mono); font-size: 0.74rem; color: var(--faint); letter-spacing: 0.04em; } +pre.code { margin: 0; padding: 20px; overflow-x: auto; font-size: 0.82rem; line-height: 1.75; } +pre.code code { color: #cdd6ea; } +.tok-kw { color: #ff9d6b; } +.tok-fn { color: #5fe3cf; font-weight: 600; } +.tok-str { color: #ffd479; } +.tok-num { color: #c3a6ff; } +.tok-com { color: #5c6884; font-style: italic; } + +/* ---------- Feature grid ---------- */ +.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; } +.card { + background: linear-gradient(180deg, var(--surface), var(--surface-2)); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 26px; + transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; +} +.card:hover { transform: translateY(-4px); border-color: var(--border-2); box-shadow: 0 20px 44px -26px rgba(0,0,0,.8); } +.card-icon { + width: 46px; height: 46px; + display: grid; place-items: center; + font-size: 1.4rem; + border-radius: 12px; + margin-bottom: 16px; +} +.i-orange { background: rgba(255,122,69,.13); border: 1px solid rgba(255,122,69,.3); } +.i-teal { background: rgba(45,212,191,.12); border: 1px solid rgba(45,212,191,.3); } +.i-purple { background: rgba(167,139,250,.13); border: 1px solid rgba(167,139,250,.3); } +.card h3 { font-size: 1.18rem; margin-bottom: 10px; } +.card p { color: var(--muted); font-size: 0.95rem; } +.card strong { color: var(--text); font-weight: 600; } + +/* ---------- How it works (layer stack) ---------- */ +.stack { max-width: 760px; margin: 0 auto; } +.layer { + border-radius: var(--radius); + border: 1px solid var(--border-2); + background: linear-gradient(180deg, var(--surface), var(--surface-2)); + position: relative; + overflow: hidden; +} +.layer::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; } +.layer-1::before { background: var(--text); } +.layer-2::before { background: var(--teal); } +.layer-3::before { background: var(--orange); } +.layer-4::before { background: var(--purple); } +.layer-body { padding: 18px 24px; } +.layer-tag { + display: inline-block; font-family: var(--mono); font-size: 0.68rem; + text-transform: uppercase; letter-spacing: 0.1em; color: var(--faint); + margin-bottom: 6px; +} +.layer h4 { font-family: var(--mono); font-size: 1rem; margin-bottom: 6px; font-weight: 600; } +.layer p { color: var(--muted); font-size: 0.92rem; } +.layer strong { color: var(--text); } +.flow { + text-align: center; + font-size: 0.8rem; + color: var(--faint); + padding: 9px 0; + position: relative; +} +.flow span { + font-family: var(--mono); font-weight: 600; color: var(--teal); + background: var(--surface); border: 1px solid var(--border); + padding: 2px 9px; border-radius: 999px; margin-right: 8px; font-size: 0.78rem; +} +.flow::after { + content: "↓"; display: block; color: var(--border-2); font-size: 1rem; margin-top: 2px; +} + +/* ---------- Examples ---------- */ +.example-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; } +.ex-card { + display: flex; flex-direction: column; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; +} +.ex-card:hover { transform: translateY(-4px); border-color: var(--teal); box-shadow: 0 22px 46px -26px rgba(0,0,0,.85); } +.ex-art { height: 150px; position: relative; overflow: hidden; border-bottom: 1px solid var(--border); } +.ex-art::after { + content: ""; position: absolute; inset: 0; + background: linear-gradient(180deg, transparent 55%, rgba(10,14,23,.55)); +} +.art-demo { background: radial-gradient(circle at 30% 30%, #2dd4bf55, transparent 45%), radial-gradient(circle at 70% 60%, #ff7a4555, transparent 45%), repeating-linear-gradient(45deg, #141d30, #141d30 12px, #18223a 12px, #18223a 24px); } +.art-contraption { background: radial-gradient(circle at 50% 40%, #a78bfa55, transparent 50%), conic-gradient(from 0deg at 50% 50%, #18223a, #1c2742, #18223a); } +.art-platformer { background: linear-gradient(180deg, #1a2740, #16203a), radial-gradient(circle at 20% 80%, #2dd4bf44, transparent 40%); } +.art-platformer::before { content:""; position:absolute; left:0; right:0; bottom:0; height:42px; background: repeating-linear-gradient(90deg,#ff7a4533,#ff7a4533 22px,#22304d33 22px,#22304d33 44px); } +.art-slingshot { background: radial-gradient(circle at 75% 35%, #ff5e6255, transparent 45%), linear-gradient(180deg,#1b2336,#141b2c); } +.art-selftest { background: linear-gradient(180deg,#13243a,#101a2c); } +.art-selftest::before { content:"✓ ✓ ✓"; position:absolute; inset:0; display:grid; place-items:center; color:#2dd4bf66; font-family:var(--mono); font-size:1.6rem; letter-spacing:.4em; } +.art-more { background: repeating-linear-gradient(135deg,#141d30,#141d30 14px,#18223a 14px,#18223a 28px); } +.art-more::before { content:"+"; position:absolute; inset:0; display:grid; place-items:center; color:#ff7a4577; font-size:3rem; font-weight:800; } +.ex-body { padding: 20px 22px 22px; display: flex; flex-direction: column; flex: 1; } +.ex-body h3 { font-size: 1.12rem; margin-bottom: 8px; } +.ex-body p { color: var(--muted); font-size: 0.92rem; flex: 1; } +.ex-body em { color: var(--text); font-style: italic; } +.ex-link { margin-top: 14px; font-size: 0.86rem; font-weight: 600; color: var(--teal); } + +/* ---------- Docs ---------- */ +.docs-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } +.doc { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 20px; + transition: transform .15s ease, border-color .15s ease; +} +.doc:hover { transform: translateY(-3px); border-color: var(--orange); } +.doc h4 { font-size: 1rem; margin-bottom: 6px; } +.doc p { color: var(--muted); font-size: 0.86rem; } +.doc-more { background: linear-gradient(135deg, rgba(255,122,69,.1), rgba(167,139,250,.1)); border-color: var(--border-2); } + +/* ---------- Get started steps ---------- */ +.steps { list-style: none; counter-reset: step; padding: 0; margin: 0 auto; max-width: 760px; display: grid; gap: 16px; } +.step { display: flex; gap: 18px; align-items: flex-start; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px 24px; } +.step-n { + flex: none; + width: 38px; height: 38px; + display: grid; place-items: center; + font-weight: 800; font-size: 1.05rem; + border-radius: 10px; + background: var(--grad); + color: #1a0f08; +} +.step h4 { font-size: 1.05rem; margin-bottom: 5px; } +.step p { color: var(--muted); font-size: 0.94rem; } +.step a { color: var(--teal); font-weight: 600; } +.start-cta { display: flex; flex-wrap: wrap; gap: 14px; justify-content: center; margin-top: 36px; } + +/* ---------- Footer ---------- */ +.footer { border-top: 1px solid var(--border); padding: 56px 0 28px; background: rgba(255,255,255,.012); } +.footer-inner { display: grid; grid-template-columns: 1.4fr 2fr; gap: 40px; } +.footer-brand .brand-name { font-size: 1.2rem; font-weight: 800; } +.footer-brand p { color: var(--muted); font-size: 0.9rem; margin-top: 12px; max-width: 40ch; } +.footer-brand a { color: var(--teal); } +.footer-cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; } +.footer-cols h5 { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--faint); margin: 0 0 12px; font-weight: 700; } +.footer-cols a { display: block; color: var(--muted); font-size: 0.92rem; padding: 4px 0; transition: color .15s; } +.footer-cols a:hover { color: var(--text); } +.footer-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 44px; padding-top: 22px; border-top: 1px solid var(--border); color: var(--faint); font-size: 0.84rem; } +.footer-bottom a { color: var(--muted); } +.footer-bottom a:hover { color: var(--text); } + +/* =========================================================== + Responsive + =========================================================== */ +@media (max-width: 920px) { + .hero-grid { grid-template-columns: 1fr; } + .hero-demo { order: -1; } + .lede { max-width: none; } + .sixty-grid { grid-template-columns: 1fr; } + .feature-grid { grid-template-columns: repeat(2, 1fr); } + .example-grid { grid-template-columns: repeat(2, 1fr); } + .docs-grid { grid-template-columns: repeat(2, 1fr); } + .footer-inner { grid-template-columns: 1fr; gap: 32px; } +} + +@media (max-width: 680px) { + .nav-links, .nav-cta { display: none; } + .nav-links.open { + display: flex; flex-direction: column; gap: 0; + position: absolute; top: 64px; left: 0; right: 0; + background: var(--bg-2); border-bottom: 1px solid var(--border); + padding: 8px 0; margin: 0; + } + .nav-links.open a { padding: 13px 24px; } + .nav-toggle { + display: flex; flex-direction: column; justify-content: center; gap: 5px; + margin-left: auto; width: 42px; height: 42px; + background: transparent; border: 1px solid var(--border-2); border-radius: 10px; cursor: pointer; + padding: 0; align-items: center; + } + .nav-toggle span { display: block; width: 18px; height: 2px; background: var(--text); border-radius: 2px; transition: .2s; } + .nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); } + .nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; } + .nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); } + + .stats-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; } + .feature-grid, .example-grid, .docs-grid { grid-template-columns: 1fr; } + .footer-cols { grid-template-columns: 1fr 1fr; } + .footer-bottom { flex-direction: column; gap: 10px; text-align: center; } + #physics { height: 300px; } +} From 6662a60ab2ad02b9193a6bf7243be022fb17f07c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 01:02:11 +0000 Subject: [PATCH 2/6] Redesign website with a HyperCard-heritage identity Replace the generic dark-SaaS look with a personality rooted in the project itself: the xTalk/HyperCard lineage and Box2D's signature crate. - Warm 'paper' background with a faint 1-bit dot grid; ink-on-paper with one signature colour (Box2D crate-orange). - Authentic classic-Mac window chrome (striped title bars, square close boxes) reused across the hero demo, code sample, the layer diagram, and example cards; hard ink borders with solid offset shadows; tactile press states on buttons. - The card/stack metaphor as the actual layout: sections are numbered 'cards' and the three-layer architecture is drawn as a literal stack of windows. - Pixel font (Silkscreen) for chrome/labels; Space Grotesk for display; JetBrains Mono for code. Flat outlined SVG icon badges replace emoji. - Hero demo reborn as tumbling, stacking Box2D crates (+ cannonballs): circle colliders for stability, rendered as rotating crates with X-braces; grab/fling now imparts spin. - Copy re-voiced around cards & stacks ('physics for the card & stack people', 'a whole scene in a single paste'). Verified: node --check clean, HTML balanced, zero curly quotes, 25 links correct; re-screenshotted at desktop + mobile. --- website/README.md | 27 +- website/app.js | 278 +++++++++---------- website/index.html | 426 +++++++++++++---------------- website/styles.css | 654 +++++++++++++++++++++------------------------ 4 files changed, 630 insertions(+), 755 deletions(-) diff --git a/website/README.md b/website/README.md index c39b3d7..e8dd196 100644 --- a/website/README.md +++ b/website/README.md @@ -4,11 +4,18 @@ 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. + ``` website/ -├── index.html # the page -├── styles.css # dark, modern theme (palette echoes the project's own demos) -└── app.js # mobile nav + the interactive hero physics toy +├── index.html # the page (menu bar + window-framed "cards") +├── styles.css # the paper/ink/orange design system + Mac window chrome +└── app.js # menu toggle + the interactive hero crate-physics toy ``` ## View it locally @@ -53,11 +60,15 @@ The site will be served at `https://sethmorrowsoftware.github.io/Box2Dxt/`. Everything is hand-written and dependency-free: -- Copy lives directly in `index.html`. -- The colour palette and layout are CSS custom properties at the top of - `styles.css` (`--orange`, `--teal`, `--purple`, …). -- The hero demo is a compact impulse-based circle solver in `app.js`; tune the - constants near the top (`GRAV`, `REST`, `MAX_BODIES`, …). +- 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 index 55c6d36..9f9fd4d 100644 --- a/website/app.js +++ b/website/app.js @@ -1,17 +1,20 @@ /* =========================================================== Box2Dxt site — interactive bits - 1) Mobile nav toggle - 2) A tiny self-contained 2D physics toy for the hero - (impulse-based circle solver: gravity, walls, ball-ball - collisions, mouse/touch grab + fling, click-to-spawn). - No libraries. This is a *toy* — Box2Dxt is the real engine. + 1) Mac-style menu toggle (mobile) + 2) A tiny self-contained physics toy for the hero: tumbling, + stacking Box2D-style crates (+ a few cannonballs). Circle + colliders keep it rock-stable; bodies carry an angle so they + render as rotating crates. Grab + fling, click to drop, + gravity toggle. No libraries — Box2Dxt is the real engine. =========================================================== */ (function () { "use strict"; - /* ---------- Mobile nav ---------- */ - var toggle = document.getElementById("navToggle"); - var links = document.querySelector(".nav-links"); + var INK = "#17140d"; + + /* ---------- Menu (mobile) ---------- */ + var toggle = document.getElementById("menuToggle"); + var links = document.getElementById("menuLinks"); if (toggle && links) { toggle.addEventListener("click", function () { var open = links.classList.toggle("open"); @@ -30,37 +33,39 @@ if (!canvas || !canvas.getContext) return; var ctx = canvas.getContext("2d"); - var COLORS = ["#ff7a45", "#2dd4bf", "#a78bfa", "#ffd479", "#ff5e62", "#5b8cff"]; + var CRATES = ["#e8702a", "#2f5fae", "#3f8f5b", "#d23b3b", "#e8a23a"]; + var BALLS = ["#2a2620", "#3a3530"]; var GRAV = 1700; // px / s^2 - var REST = 0.16; // ball-ball restitution (low → settles into a pile) - var WALL_REST = 0.32; // wall bounce - var FRICTION = 0.04; // tangential damping on contact - var SLOP = 0.5; // penetration allowance - var CORRECT = 0.8; // positional correction factor - var ITER = 6; // solver iterations per step - var DT = 1 / 120; // fixed timestep - var MAXV = 2600; // velocity clamp (anti-tunnel) - var MAX_BODIES = 26; + var REST = 0.14; // restitution (low → settles into a pile) + var WALL_REST = 0.3; + var FRICTION = 0.18; // tangential damping on contact + var SLOP = 0.5, CORRECT = 0.8, ITER = 6; + var DT = 1 / 120, MAXV = 2600, MAXSPIN = 16, MAX_BODIES = 24; var W = 0, H = 0, dpr = 1; - var bodies = []; - var gravityOn = true; - var held = null, heldSavedInv = 0; + var bodies = [], gravityOn = true; + var held = null, heldSavedInv = 0, lastHeldX = 0; var pointer = { x: 0, y: 0, active: false }; - var interacted = false; - var acc = 0, last = 0, running = false; + var interacted = false, running = false, last = 0, acc = 0; function rand(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); + 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); + canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - // keep existing bodies inside the new bounds for (var i = 0; i < bodies.length; i++) { var b = bodies[i]; b.x = Math.min(Math.max(b.r, b.x), W - b.r); @@ -68,28 +73,36 @@ } } - function makeBody(x, y, r, color) { + function makeBody(x, y, r, shape, color) { return { x: x, y: y, vx: rand(-40, 40), vy: rand(0, 40), - r: r, color: color || COLORS[(Math.random() * COLORS.length) | 0], - invMass: 1 / (r * r) + 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); + var ball = Math.random() < 0.22; + var b = makeBody(x, y, r, + ball ? "ball" : "crate", + ball ? BALLS[(Math.random() * BALLS.length) | 0] : CRATES[(Math.random() * CRATES.length) | 0]); + return b; + } + function seed() { bodies = []; var n = W < 420 ? 9 : 13; for (var i = 0; i < n; i++) { - var r = rand(13, 26); - bodies.push(makeBody(rand(r, W - r), rand(-H, H * 0.4), r)); + var b = spawn(rand(30, W - 30), rand(-H, H * 0.35)); + bodies.push(b); } } - function spawnAt(x, y) { + function dropAt(x, y) { if (bodies.length >= MAX_BODIES) bodies.shift(); - var r = rand(13, 26); - var b = makeBody(x, y, r); - b.vx = rand(-60, 60); b.vy = rand(-40, 40); + var b = spawn(x, y); + b.vx = rand(-60, 60); b.vy = rand(-30, 40); b.spin = rand(-5, 5); bodies.push(b); } @@ -97,182 +110,158 @@ function step() { var i, j, b; - // integrate for (i = 0; i < bodies.length; i++) { b = bodies[i]; - if (b === held) continue; // held body is driven by the pointer + 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; - // clamp speed + 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; } - // drive held body from the pointer; give it velocity for the fling 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.vx = (px - held.x) / DT; held.vy = (py - held.y) / DT; held.x = px; held.y = py; } - // collisions (several iterations for a stable pile) for (var it = 0; it < ITER; it++) { - // ball vs ball + // crate vs crate (circle colliders) 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; - var dist = Math.hypot(dx, dy); - var min = a.r + c.r; + var dist = Math.hypot(dx, dy), min = a.r + c.r; if (dist >= min || dist === 0) continue; - var nx = dx / dist, ny = dy / dist; - var pen = min - dist; - var im = a.invMass + c.invMass; + var pen = min - dist, im = a.invMass + c.invMass; - // positional correction 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; + a.x -= nx * corr * a.invMass; a.y -= ny * corr * a.invMass; + c.x += nx * corr * c.invMass; c.y += ny * corr * c.invMass; - // velocity impulse var rvx = c.vx - a.vx, rvy = c.vy - a.vy; var 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; - // tangential friction + // tangential friction + tumble var tx = -ny, ty = nx; var 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; } } } - // walls 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; } - else if (b.x > W - b.r) { b.x = W - b.r; if (b.vx > 0) b.vx = -b.vx * WALL_REST; } + 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; // floor friction so the pile settles + b.vx *= 0.985; // floor friction + b.spin = b.spin * 0.6 + (b.vx / b.r) * 0.4; // roll to match motion } } } } + function drawCrate(b) { + var s = b.r * 1.5, in1 = s * 0.13; + // ground shadow (axis-aligned, hard offset) + 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(); + // X brace + 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(); + // inner plank frame + 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++) { - var b = bodies[i]; - // soft shadow - ctx.beginPath(); - ctx.arc(b.x, b.y + 3, b.r, 0, Math.PI * 2); - ctx.fillStyle = "rgba(0,0,0,0.18)"; - ctx.fill(); - // ball with a top-left highlight for depth - var g = ctx.createRadialGradient( - b.x - b.r * 0.35, b.y - b.r * 0.4, b.r * 0.1, - b.x, b.y, b.r - ); - g.addColorStop(0, lighten(b.color, 0.35)); - g.addColorStop(1, b.color); - ctx.beginPath(); - ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); - ctx.fillStyle = g; - ctx.fill(); - ctx.lineWidth = 1; - ctx.strokeStyle = "rgba(0,0,0,0.22)"; - ctx.stroke(); + if (bodies[i].shape === "ball") drawBall(bodies[i]); + else drawCrate(bodies[i]); } } - // quick hex lighten - function lighten(hex, amt) { - var n = parseInt(hex.slice(1), 16); - var r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; - r = Math.round(r + (255 - r) * amt); - g = Math.round(g + (255 - g) * amt); - b = Math.round(b + (255 - b) * amt); - return "rgb(" + r + "," + g + "," + b + ")"; - } - function frame(t) { if (!running) return; if (!last) last = t; - var elapsed = (t - last) / 1000; - last = t; - acc += Math.min(elapsed, 0.05); // cap to avoid spiral of death + 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) return; - running = true; last = 0; - requestAnimationFrame(frame); - } + function start() { if (!running) { running = true; last = 0; requestAnimationFrame(frame); } } function stop() { running = false; } - /* ---------- Pointer interaction ---------- */ - function canvasPoint(e) { + /* ---------- Pointer ---------- */ + function pt(e) { var rect = canvas.getBoundingClientRect(); - var cx = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left; - var cy = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top; - return { x: cx, y: cy }; + return { + x: (e.touches ? e.touches[0].clientX : e.clientX) - rect.left, + y: (e.touches ? e.touches[0].clientY : e.clientY) - rect.top + }; } - function onDown(e) { - var p = canvasPoint(e); - pointer.x = p.x; pointer.y = p.y; pointer.active = true; + var p = pt(e); pointer.x = p.x; pointer.y = p.y; pointer.active = true; interacted = true; hideHint(); - - // grab the nearest body under the pointer var best = null, bestD = Infinity; for (var i = 0; i < bodies.length; i++) { - var b = bodies[i]; - var d = Math.hypot(b.x - p.x, b.y - p.y); + 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; // immovable while held → shoves others, stays put - } else { - spawnAt(p.x, p.y); // empty space → drop a new body - } - e.preventDefault(); - } - - function onMove(e) { - if (!pointer.active) return; - var p = canvasPoint(e); - pointer.x = p.x; pointer.y = p.y; + if (best) { held = best; heldSavedInv = best.invMass; best.invMass = 0; lastHeldX = best.x; } + 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; // restore mass; keeps tracked velocity → fling + held.invMass = heldSavedInv; + held.spin = Math.max(-MAXSPIN, Math.min(MAXSPIN, -held.vx / held.r * 0.4)); // tumble with the throw held = null; } } - canvas.addEventListener("mousedown", onDown); window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); @@ -284,34 +273,21 @@ 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"; } } - if (resetBtn) resetBtn.addEventListener("click", function () { seed(); interacted = true; hideHint(); }); if (gravBtn) gravBtn.addEventListener("click", function () { gravityOn = !gravityOn; - gravBtn.textContent = "Gravity: " + (gravityOn ? "on" : "off"); - if (!gravityOn) { for (var i = 0; i < bodies.length; i++) { bodies[i].vy -= 120; } } // little float-up nudge + gravBtn.textContent = "GRAVITY: " + (gravityOn ? "ON" : "OFF"); + if (!gravityOn) for (var i = 0; i < bodies.length; i++) bodies[i].vy -= 120; interacted = true; hideHint(); }); - /* ---------- Lifecycle: only run while visible ---------- */ - resize(); - seed(); + /* ---------- Lifecycle ---------- */ + resize(); seed(); window.addEventListener("resize", resize); - if ("IntersectionObserver" in window) { - var io = new IntersectionObserver(function (entries) { - if (entries[0].isIntersecting) start(); else stop(); - }, { threshold: 0.05 }); - io.observe(canvas); - } else { - start(); - } - document.addEventListener("visibilitychange", function () { - if (document.hidden) stop(); else start(); - }); - - // auto-hide the hint after a few seconds even without interaction + new IntersectionObserver(function (en) { if (en[0].isIntersecting) start(); else stop(); }, { threshold: 0.05 }).observe(canvas); + } else { start(); } + document.addEventListener("visibilitychange", function () { if (document.hidden) stop(); else start(); }); setTimeout(function () { if (!interacted) hideHint(); }, 6000); })(); diff --git a/website/index.html b/website/index.html index 67f8692..e8c1896 100644 --- a/website/index.html +++ b/website/index.html @@ -3,57 +3,53 @@ -Box2Dxt — Real 2D physics for OpenXTalk & xTalk - - - - +Box2Dxt — Real 2D physics for OpenXTalk & the xTalk family + + - - + + - - + + - - + - - - -
-
-
- Box2D v3.1.0 · OpenXTalk & LiveCode 9.6.3+ -

Real 2D physics
for the card &
stack people.

-

- Box2Dxt drops the battle-tested Box2D engine into - OpenXTalk and LiveCode. You already think in - cards and stacks — now your controls fall, roll, bounce, hinge, and collide, - in plain xTalk. -

- -
    -
  • MIT licensed
  • -
  • Windows · macOS · Linux
  • -
  • No build step
  • -
-
- -
-
-
- - - PHYSICS — DRAG THE CRATES - -
- -
- - - click empty space to drop a crate -
-
-

A hand-built toy solver runs this box — Box2Dxt gives your app the real engine.

-
-
-
- - -
-
-
370+engine handlers
-
300+friendly kit calls
-
6paste-and-run examples
-
~125self-tests, one click
-
-
- - -
-
-
- CARD 01 · ONE PASTE -

A whole scene,
in a single paste.

-

- The Kit owns the world, gravity, the fixed-timestep loop, and the - per-frame redraws — so you don't. 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. -

-
    -
  • Speaks pixels & degrees — the units you already use.
  • -
  • Bodies ride on ordinary LiveCode controls.
  • -
  • No C toolchain: drop in a prebuilt library and go.
  • -
-
- -
+ +
+
+
- - openCard — stack script + + + 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
@@ -143,229 +151,169 @@ 

A whole scene,
in a single paste.

on mouseUp b2kRelease end mouseUp
-
-
-
- - -
-
-
- CARD 02 · WHAT YOU GET -

Friendly on top.
Serious underneath.

-

A gentle layer for everyday work, the whole engine when you need it, and a game toolkit sitting on top.

-
- -
-
-
- -
-

Friendly by default

-

The Kit speaks pixels and degrees, binds bodies to your controls, and runs the loop for you. 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 — getters return zero, actions do nothing. Never a crash.

-
- -
-
- -
-

Cross-platform, prebuilt

-

Drop-in native 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. Paste it into a stack and it runs — no setup, no external assets. Read one as a worked tutorial.

-
-
-
-
- - -
-
-
- CARD 03 · 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 live-object surface.

+
+
+
+ + +
+ 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 + +
-
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 05 · 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 06 · GET STARTED -

Up and running in four steps.

-

No C toolchain needed — grab a prebuilt library and paste a script.

-
- -
    -
  1. 1

    Get the native library

    Download the file for your platform from prebuilt/ and rename it to the bare name the loader expects: box2dxt.dll / box2dxt.dylib / box2dxt.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. -
- - -
-
- - - + + + + + +

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

+ diff --git a/website/styles.css b/website/styles.css index 6e6fa97..9dfe772 100644 --- a/website/styles.css +++ b/website/styles.css @@ -481,3 +481,127 @@ pre.code code { color: var(--ink); } @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; } +} From fc812fb472e5e5246985e0f20de94cb02c2fa815 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 17:51:46 +0000 Subject: [PATCH 6/6] Update website for current main (bundled-extension install, bigger platformer) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged latest main and refreshed the site's content to match: - Get started: rewritten to the new install flow — Package box2dxt.lcb into a .lce and install it; the per-platform native library is bundled inside and loads automatically (no download, rename, sudo, or /usr/lib). - 'What it is' + Self-test stats: ~125 -> ~180 deterministic assertions (harness is now v22); 'prebuilt' bullet now describes the bundled, auto-loading library. - Examples: platformer is now six scrolling biome levels (grass, stone, ice, haunted, desert, cavern) with a bestiary, joints, sprites, and audio. - Game Kit feature: player moves updated (duck, drop-through). - Docs: regenerated from the updated docs/*.md (the new install guide flows into the hosted Getting started page); added the new Asset expansion plan to the 'going deeper' GitHub list. Verified: all pages tag-balanced, JS clean, no stale install/count copy; the 2 unresolved in-page anchors are the pre-existing kit-guide TOC nit. --- tools/build-docs.py | 1 + website/docs/getting-started.html | 40 +++++++++++++++---------------- website/docs/index.html | 3 ++- website/docs/kit-guide.html | 15 ++++++------ website/docs/kit-reference.html | 2 +- website/index.html | 14 +++++------ 6 files changed, 38 insertions(+), 37 deletions(-) diff --git a/tools/build-docs.py b/tools/build-docs.py index c81b672..483c5f8 100644 --- a/tools/build-docs.py +++ b/tools/build-docs.py @@ -36,6 +36,7 @@ ("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"), + ("Asset expansion plan", "Using the whole spritesheet library.", "asset-expansion-plan.md"), ] diff --git a/website/docs/getting-started.html b/website/docs/getting-started.html index 6f1c371..d7ec8e2 100644 --- a/website/docs/getting-started.html +++ b/website/docs/getting-started.html @@ -35,11 +35,11 @@
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.

+

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 install a prebuilt extension (the native library is bundled in).

+

Just want to play, not code? If you were handed the prebuilt platformer package (a zip with the extension — native library bundled in — and a saved stack), open its INSTALL.md and follow two 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.

+

1. Install the extension

+

Box2Dxt is a real physics engine compiled to a native library — and that library ships inside the extension, so there's nothing to download, rename, or place by hand. Installing the extension is the only setup step, and it's identical on Windows, macOS, and Linux:

+
    +
  1. In OXT, open Tools → Extension Builder and open src/box2dxt.lcb.
  2. +
  3. Click Package — this rolls the per-platform libraries (committed under src/code/<arch>-<platform>/) into a box2dxt.lce — then install that .lce via Tools → Extension Manager.
  4. +
+

The engine then loads the correct library for your platform automatically — no /usr/lib, no sudo, no LD_LIBRARY_PATH, no renaming.

+

The committed prebuilt/ binaries are the source of those bundled libraries; python3 tools/package-extension.py lays them into src/code/ (already done in the repo). Prefer building the library yourself? See building.md.

+

2. Load it while developing

+

You don't have to repackage on every edit:

    -
  • 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") ``
  • +
  • Extension Builder → Test compiles and loads src/box2dxt.lcb in place — it reads the code/ folder beside the .lcb, so the native library loads too.
  • +
  • From script: load extension from file (the defaultFolder & "/box2dxt.lcb")
  • +
  • No-packaging fallback: drop box2dxt.{so,dll,dylib} (bare name) next to your saved stack; the Kit points the engine at it on b2kSetup, so even Linux needs no system path.
-

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.

+

Foreign bindings resolve on first use, so the library only has to be in place by the time a b2… handler first runs — not when the extension loads.

3. Sanity check

Open the Message Box and run:

put b2Version()
@@ -124,7 +122,7 @@

6. Run the full demo

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.
+
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 extension loaded without its bundled library. Install the packaged extension (Extension Builder → Package src/box2dxt.lcb → install the .lce), or Test it with the code/ folder beside the .lcb. Quick fallback: drop box2dxt.{dll,dylib,so} (bare name) next to your saved stack. (Tip: launch OXT from a terminal — it prints dlopen failed <name> showing the 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.
  • diff --git a/website/docs/index.html b/website/docs/index.html index 5c26132..dbe9765 100644 --- a/website/docs/index.html +++ b/website/docs/index.html @@ -46,7 +46,8 @@

    Going deeper

  • Architecture — How the three layers fit together.
  • Game engine spec — The Game Kit design, in depth.
  • Building — Compile the native library yourself.
  • -
  • Expansion prep — The internal roadmap & intake plan.
+
  • Expansion prep — The internal roadmap & intake plan.
  • +
  • Asset expansion plan — Using the whole spritesheet library.
  • diff --git a/website/docs/kit-guide.html b/website/docs/kit-guide.html index 35cfce5..7e1399c 100644 --- a/website/docs/kit-guide.html +++ b/website/docs/kit-guide.html @@ -77,7 +77,7 @@

    1. What the Kit is (and why)

    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+.

    +

    Requirements: the box2dxt extension loaded. Check with put b2Version() — it should return 4 (the shim ABI version). 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
    @@ -375,6 +375,7 @@ 

    Sprites and spr 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.

    +

    Persisting sheets across rebuilds. By default a sheet is torn down with the world, so a single-shot scene reloads its art each run. A game that rebuilds the world per level can keep its sheets cached instead: b2kSheetPersist true makes sheets survive b2kTeardown (exactly like synthesized sounds) and reuse an identical reload rather than re-slicing it — b2kClear/b2kTeardown then wipe only the sprite instances. It's OFF by default; b2kSheetsWipe forces a clean reload (e.g. after the player picks a new asset folder), and b2kSheetPersists() reads the flag back.

    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
    @@ -399,7 +400,7 @@ 

    Sound effects (b2kToneMake

    -

    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.

    +

    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 cues are all synthesized this way; 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.

    @@ -487,7 +488,7 @@

    Measuring

    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. 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. The platformer and slingshot both do this.
    4. 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).
    5. Read the clock once per handler, not once per entity.
    6. Build heavy things once. Sounds survive b2kTeardown; tiles are create-at-level-build; sheets slice lazily and share frames.
    7. @@ -555,7 +556,7 @@

      19. A complete worked example

      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:

      +

      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; it has since been retired (the repo now concentrates its game work on the platformer and slingshot showcases), but the pattern is preserved here because it remains exactly the skeleton to copy when you start your own green-field 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
      @@ -566,9 +567,9 @@ 

      20. Building a whole ga 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.

      +

      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. A whole "character system" in this pattern 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.

      +

      The pattern's flow: openCard builds level 1 and shows the menu → click → mgBegin → door (all coins) → mgAdvance → level 2 → door → mgShowWin → click → back to level 1, with R to rebuild the current level, ESC to pause, M to mute.


      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.

      @@ -627,7 +628,7 @@

      Drag

      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

      +

      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 · b2kSheetPersist flag · b2kSheetPersists() [f] · b2kSheetsWipe · 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

      diff --git a/website/docs/kit-reference.html b/website/docs/kit-reference.html index c98a647..8ef2dbe 100644 --- a/website/docs/kit-reference.html +++ b/website/docs/kit-reference.html @@ -125,7 +125,7 @@

      Player (the platformer controller)

      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).

      +

      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 / airDash 1 (1 = dash works mid-air too; 0 = grounded-only) · 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
      diff --git a/website/index.html b/website/index.html
      index 09794c3..56b51cf 100644
      --- a/website/index.html
      +++ b/website/index.html
      @@ -113,12 +113,12 @@ 

      A real physics engine,
      wearing xTalk's clothes.

      370+engine handlers
      300+friendly kit calls
      6paste-and-run examples
      -
      ~125self-tests, one click
      +
      ~180self-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.
      • +
      • The native library is bundled in the extension and auto-loads on Windows, macOS & Linux — no download, rename, or sudo.

      Next up: how little code it takes.

      @@ -173,7 +173,7 @@

      The full Box2D surface

      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.

      +

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

      @@ -239,7 +239,7 @@

      Six examples, each a single paste.

      platformer
      -

      Platformer Showcase

      The Game Kit pushed hard: scrolling camera, spritesheets, moving platforms, coin puzzles, a hilltop swim pool.

      READ THE SCRIPT →
      +

      Platformer Showcase

      Six scrolling biome levels — grass, stone, ice, haunted, desert, cavern — with a full player controller, a bestiary (bats, a mimic, piranhas, a ghost, a kickable shell, crushers), joints, spritesheets, and synthesized audio.

      READ THE SCRIPT →
      slingshot
      @@ -249,7 +249,7 @@

      Six examples, each a single paste.

      self-test
      [x] [x] [x]
      -

      Self-Test Harness

      ~125 deterministic assertions driving the real Kit — proving everything from physics events to player feel in one click.

      READ THE SCRIPT →
      +

      Self-Test Harness

      ~180 deterministic assertions driving the real Kit — proving everything from physics events to player feel in one click.

      READ THE SCRIPT →
      examples/
      @@ -277,8 +277,8 @@

      Guides, not homework.

      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. 1

        Package the extension

        Open box2dxt.lcb in OXT's Extension Builder and click Package — it rolls in the per-platform native libraries for you.

      6. +
      7. 2

        Install the .lce

        Add the packaged box2dxt.lce in the Extension Manager. The right native library is bundled inside and loads itself — no download, rename, sudo, or /usr/lib.

      8. 3

        Sanity check

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

      9. 4

        Paste an example & reopen the card

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