From b7c428f6dfec67ec1874b426dd5644d471e368e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:33:08 +0000 Subject: [PATCH 1/5] 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/5] 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; } +}