feat(animations+firmware): /next/animations showcase (videos + interactive demos), in-app Help link, Arch firmware build script

- assets/media/showcase/index.html: standalone showcase on Manifold tokens —
  the 4 Manim explainers + 2 live interactive demos (knob->number, XY->two sliders).
- Help drawer links to it (animations/).
- scripts/build-firmware-arch.sh: Omarchy/Arch firmware build (pacman deps +
  arduino-cli + delegates to setup-firmware-toolchain.sh; clear error on the
  unreachable memllib pin).
This commit is contained in:
monkey-w1n5t0n 2026-06-28 04:42:27 +02:00
parent 45e59e1a23
commit 0a17b3e76e
3 changed files with 303 additions and 0 deletions

View file

@ -0,0 +1,187 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Manifold — How it works</title>
<style>
:root{
--bg:#0d0d0d; --bg-1:#141414; --bg-2:#1c1c1c; --bg-3:#242424;
--fg:#e8e8e8; --fg-mute:#9a9a9a; --fg-dim:#5a5a5a;
--line:#2a2a2a; --line-strong:#3a3a3a;
--accent:#ff6a00; --accent-2:#00ccff; --good:#6bc26b;
--font-mono:'JetBrains Mono','Fira Code','SF Mono',Menlo,Consolas,monospace;
--glow:0 0 14px rgba(255,106,0,.35);
}
*{box-sizing:border-box}
html,body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--font-mono);
font-size:15px;line-height:1.55;-webkit-font-smoothing:antialiased}
a{color:var(--accent-2);text-decoration:none}
.wrap{max-width:960px;margin:0 auto;padding:48px 24px 96px}
header h1{font-size:34px;margin:0 0 6px;letter-spacing:-.01em}
header h1 b{color:var(--accent)}
header p{color:var(--fg-mute);margin:0 0 8px}
.crumb{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--fg-dim)}
section{margin-top:56px}
.lbl{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);margin-bottom:10px}
h2{font-size:22px;margin:0 0 6px}
.cap{color:var(--fg-mute);margin:0 0 16px;max-width:70ch}
video{width:100%;border:1px solid var(--line);border-radius:8px;background:#000;display:block}
.tag{display:inline-block;font-size:10px;letter-spacing:.08em;text-transform:uppercase;
border:1px solid var(--line-strong);border-radius:999px;padding:2px 9px;color:var(--fg-mute);margin-left:8px}
.tag.live{color:var(--good);border-color:var(--good)}
.demo{border:1px solid var(--line);border-radius:10px;background:var(--bg-1);padding:22px;display:flex;
gap:28px;flex-wrap:wrap;align-items:center;justify-content:center}
canvas{touch-action:none;cursor:pointer}
.readouts{display:flex;flex-direction:column;gap:14px;min-width:220px}
.ro{display:flex;align-items:center;gap:12px}
.ro span{width:54px;font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--fg-mute)}
.bar{flex:1;height:8px;background:var(--bg-3);border:1px solid var(--line);border-radius:999px;overflow:hidden;position:relative}
.bar i{position:absolute;left:0;top:0;bottom:0;display:block;border-radius:999px}
.num{width:4ch;text-align:right;font-variant-numeric:tabular-nums;color:var(--fg)}
.hint{color:var(--fg-dim);font-size:12px;margin-top:10px}
footer{margin-top:72px;color:var(--fg-dim);font-size:12px;border-top:1px solid var(--line);padding-top:18px}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="crumb">Manifold · meml.lnfinitemonkeys.org/next</div>
<h1><b>Manifold</b> — how it works</h1>
<p>A few short explainers of the core idea: a control is just a number; a network in the middle turns a few input numbers into many output numbers; and your thumbs-up/down shapes that mapping live.</p>
<p class="crumb"><a href="../">back to the instrument</a></p>
</header>
<!-- Interactive 1 -->
<section>
<div class="lbl">Try it · interactive</div>
<h2>A control is just a number<span class="tag live">live</span></h2>
<p class="cap">Drag the knob. The number is the knob — one value in [0,1]. Every control in Manifold is, underneath, exactly this.</p>
<div class="demo">
<canvas id="knob" width="200" height="200"></canvas>
<div class="readouts">
<div class="ro"><span>value</span><div class="bar"><i id="kbar" style="background:var(--accent);box-shadow:var(--glow)"></i></div><div class="num" id="knum">0.50</div></div>
<div class="hint">drag up/down or around the knob</div>
</div>
</div>
</section>
<!-- Video 1 -->
<section>
<div class="lbl">01 · Knob = 1:1 mapping</div>
<h2>Knob → one number</h2>
<p class="cap">A knob rotates while a paired 01 number rises and falls in lockstep — a control is a readout of one number.</p>
<video controls preload="metadata" src="01-knob-mapping.mp4"></video>
</section>
<!-- Interactive 2 -->
<section>
<div class="lbl">Try it · interactive</div>
<h2>An XY pad is two numbers<span class="tag live">live</span></h2>
<p class="cap">Move the dot. The pad projects to two independent values A (X) and B (Y). Two inputs — the smallest manifold.</p>
<div class="demo">
<canvas id="pad" width="220" height="220"></canvas>
<div class="readouts">
<div class="ro"><span>A · x</span><div class="bar"><i id="abar" style="background:var(--accent);box-shadow:var(--glow)"></i></div><div class="num" id="anum">0.50</div></div>
<div class="ro"><span>B · y</span><div class="bar"><i id="bbar" style="background:var(--accent-2);box-shadow:0 0 14px rgba(0,204,255,.35)"></i></div><div class="num" id="bnum">0.50</div></div>
<div class="hint">drag anywhere in the pad</div>
</div>
</div>
</section>
<!-- Video 2 -->
<section>
<div class="lbl">02 · XY pad → two numbers</div>
<h2>XY pad → A and B</h2>
<p class="cap">A 2-D pad projects to two independent 01 values, each shown as a slider, as the dot moves.</p>
<video controls preload="metadata" src="02-xy-pad.mp4"></video>
</section>
<!-- Video 3 -->
<section>
<div class="lbl">03 · Dimensionality fan-out</div>
<h2>A few inputs → many outputs</h2>
<p class="cap">The same 2-D control driving 3 → 4 → 5 outputs, then the control type cycled across knob, fader, touchpad, and 2-D / 3-D joysticks. A few input dimensions map to many outputs through the learned mapping.</p>
<video controls preload="metadata" src="03-fanout.mp4"></video>
</section>
<!-- Video 4 -->
<section>
<div class="lbl">04 · Feedback &amp; training</div>
<h2>How your verdicts reshape the mapping</h2>
<p class="cap">Training on the mapping surface, and how a verdict reshapes it — contrasting the two feedback modes: <b style="color:var(--fg)">geometric push-away</b> (directed repulsion from what you like) versus <b style="color:var(--fg)">explore-and-place</b> (randomise → audition → place → interpolate, the recommended default).</p>
<video controls preload="metadata" src="04-feedback.mp4"></video>
</section>
<footer>
Videos rendered with Manim CE (1920×1080 · H.264). The two “live” demos above are interactive; the
same idea drives the real instrument at <a href="../">/next</a>. Sources in <code>assets/media/src/</code>.
</footer>
</div>
<script>
(() => {
const TAU = Math.PI * 2;
const clamp = (v) => Math.max(0, Math.min(1, v));
const css = (n) => getComputedStyle(document.documentElement).getPropertyValue(n).trim();
const accent = css('--accent') || '#ff6a00', cyan = css('--accent-2') || '#00ccff',
line = css('--line-strong') || '#3a3a3a', bg3 = css('--bg-3') || '#242424';
// ---- Interactive 1: knob -> number ----
const kc = document.getElementById('knob'), kx = kc.getContext('2d');
const kbar = document.getElementById('kbar'), knum = document.getElementById('knum');
let kv = 0.5, kdrag = false, kref = 0.5, ky0 = 0;
const A0 = Math.PI * 0.75, A1 = Math.PI * 2.25; // 270° sweep from lower-left
function drawKnob() {
const w = kc.width, h = kc.height, cx = w/2, cy = h/2, r = 74;
kx.clearRect(0,0,w,h);
kx.lineWidth = 10; kx.lineCap = 'round';
kx.strokeStyle = bg3; kx.beginPath(); kx.arc(cx,cy,r,A0,A1); kx.stroke();
kx.strokeStyle = accent; kx.shadowColor = accent; kx.shadowBlur = 12;
kx.beginPath(); kx.arc(cx,cy,r,A0,A0 + (A1-A0)*kv); kx.stroke(); kx.shadowBlur = 0;
const a = A0 + (A1-A0)*kv;
kx.strokeStyle = '#0d0d0d'; kx.lineWidth = 4;
kx.beginPath(); kx.moveTo(cx + Math.cos(a)*(r-22), cy + Math.sin(a)*(r-22));
kx.lineTo(cx + Math.cos(a)*(r+2), cy + Math.sin(a)*(r+2)); kx.stroke();
kx.fillStyle = accent; kx.font = '600 26px ' + css('--font-mono');
kx.textAlign = 'center'; kx.textBaseline = 'middle';
kx.fillText(kv.toFixed(2), cx, cy);
kbar.style.width = (kv*100) + '%'; knum.textContent = kv.toFixed(2);
}
kc.addEventListener('pointerdown', e => { kdrag = true; kref = kv; ky0 = e.clientY; kc.setPointerCapture(e.pointerId); });
kc.addEventListener('pointermove', e => { if (!kdrag) return; kv = clamp(kref - (e.clientY - ky0)/160); drawKnob(); });
kc.addEventListener('pointerup', () => kdrag = false);
drawKnob();
// ---- Interactive 2: XY pad -> two sliders ----
const pc = document.getElementById('pad'), px = pc.getContext('2d');
const abar = document.getElementById('abar'), bbar = document.getElementById('bbar');
const anum = document.getElementById('anum'), bnum = document.getElementById('bnum');
let ax = 0.5, ay = 0.5, pdrag = false;
function drawPad() {
const w = pc.width, h = pc.height, pad = 10;
px.clearRect(0,0,w,h);
px.strokeStyle = 'rgba(255,255,255,0.05)'; px.lineWidth = 1;
for (let i=1;i<4;i++){ const t=i/4; px.beginPath(); px.moveTo(pad+t*(w-2*pad),pad); px.lineTo(pad+t*(w-2*pad),h-pad); px.stroke();
px.beginPath(); px.moveTo(pad,pad+t*(h-2*pad)); px.lineTo(w-pad,pad+t*(h-2*pad)); px.stroke(); }
px.strokeStyle = line; px.strokeRect(pad+0.5,pad+0.5,w-2*pad-1,h-2*pad-1);
const dx = pad + ax*(w-2*pad), dy = pad + (1-ay)*(h-2*pad);
px.strokeStyle = 'rgba(255,255,255,0.08)';
px.beginPath(); px.moveTo(pad,dy); px.lineTo(w-pad,dy); px.moveTo(dx,pad); px.lineTo(dx,h-pad); px.stroke();
px.fillStyle = accent; px.shadowColor = accent; px.shadowBlur = 16;
px.beginPath(); px.arc(dx,dy,9,0,TAU); px.fill(); px.shadowBlur = 0;
px.fillStyle = '#0d0d0d'; px.beginPath(); px.arc(dx,dy,3,0,TAU); px.fill();
abar.style.width = (ax*100)+'%'; bbar.style.width = (ay*100)+'%';
anum.textContent = ax.toFixed(2); bnum.textContent = ay.toFixed(2);
}
function padSet(e){ const r = pc.getBoundingClientRect(), pad = 10;
ax = clamp((e.clientX - r.left - pad)/(pc.width-2*pad));
ay = clamp(1 - (e.clientY - r.top - pad)/(pc.height-2*pad)); drawPad(); }
pc.addEventListener('pointerdown', e => { pdrag = true; pc.setPointerCapture(e.pointerId); padSet(e); });
pc.addEventListener('pointermove', e => { if (pdrag) padSet(e); });
pc.addEventListener('pointerup', () => pdrag = false);
drawPad();
})();
</script>
</body>
</html>

View file

@ -668,6 +668,26 @@ function HelpDrawer() {
re-roll (set the behaviour in the Learning drawer). Went too far undo. The dock reveals re-roll (set the behaviour in the Learning drawer). Went too far undo. The dock reveals
exactly as much machinery as you reach for. exactly as much machinery as you reach for.
</p> </p>
<SectionLabel>Learn more</SectionLabel>
<a
href="animations/"
target="_blank"
rel="noopener"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
fontSize: 'var(--fs-xs)',
color: 'var(--accent-2)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-1)',
padding: '5px 10px',
width: 'fit-content',
textDecoration: 'none',
}}
>
Watch the explainers + interactive demos
</a>
</> </>
); );
} }

96
scripts/build-firmware-arch.sh Executable file
View file

@ -0,0 +1,96 @@
#!/usr/bin/env bash
#
# build-firmware-arch.sh — build the MEMLNaut RP2350 firmware on Arch Linux
# (incl. Omarchy). Checks + installs system deps via pacman/yay, brings up the
# arduino-cli toolchain + rp2040 core, and compiles a firmware variant.
#
# Idempotent: safe to re-run. This is a thin Arch-specific front-end around the
# cross-platform scripts/setup-firmware-toolchain.sh + scripts/build-firmware.sh.
#
# Usage:
# scripts/build-firmware-arch.sh [VARIANT] # default VARIANT=PAFSynth
# scripts/build-firmware-arch.sh --setup-only # toolchain only, no compile
# scripts/build-firmware-arch.sh --list # list firmware variants
#
# The rp2040 core download is large (hundreds of MB — it bundles arm-none-eabi
# GCC). Run on your laptop, on mains power, with a good connection.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
VARIANT="PAFSynth"
SETUP_ONLY=0
for arg in "$@"; do
case "$arg" in
--setup-only) SETUP_ONLY=1 ;;
--list) grep -oE '"[A-Za-z0-9]+"' "$REPO_ROOT/scripts/build-firmware.sh" 2>/dev/null | head; exit 0 ;;
-h|--help) sed -n '2,18p' "$0"; exit 0 ;;
-*) echo "unknown flag: $arg" >&2; exit 2 ;;
*) VARIANT="$arg" ;;
esac
done
log() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
warn() { printf '\033[1;33mwarning: %s\033[0m\n' "$*" >&2; }
die() { printf '\033[1;31merror: %s\033[0m\n' "$*" >&2; exit 1; }
# ---- 0. sanity: are we on Arch? ----------------------------------------
command -v pacman >/dev/null 2>&1 || die "pacman not found — this script is for Arch Linux / Omarchy. Use scripts/setup-firmware-toolchain.sh on other distros."
# ---- 1. system deps via pacman -----------------------------------------
# The rp2040 core ships its own arm-none-eabi GCC, so we only need the basics
# (git/curl/unzip) + python (some core post-install steps want it).
need_pkgs=()
for bin_pkg in "git:git" "curl:curl" "unzip:unzip" "python:python"; do
bin="${bin_pkg%%:*}"; pkg="${bin_pkg##*:}"
command -v "$bin" >/dev/null 2>&1 || need_pkgs+=("$pkg")
done
if (( ${#need_pkgs[@]} )); then
log "Installing system deps via pacman: ${need_pkgs[*]}"
sudo pacman -S --needed --noconfirm "${need_pkgs[@]}"
else
log "System deps present (git, curl, unzip, python)"
fi
# ---- 2. arduino-cli -----------------------------------------------------
# Prefer the official binary (matches the cross-platform setup script's pin);
# fall back to AUR (yay) if the user already has arduino-cli from there.
if ! command -v arduino-cli >/dev/null 2>&1; then
if command -v yay >/dev/null 2>&1; then
log "Installing arduino-cli from the AUR (yay)"
yay -S --needed --noconfirm arduino-cli || warn "yay install failed; the toolchain script will fetch the official binary into ~/.local/bin"
else
log "arduino-cli not found; the toolchain script will install it into ~/.local/bin"
fi
fi
# ---- 3. submodule sanity (the orphan-pin footgun) -----------------------
# memllib provides the audio/synth/hardware tree; if its pin is unreachable the
# build cannot be assembled. Fail loudly with the fix rather than 100 cryptic
# missing-header errors deep in the compile.
log "Checking the memllib submodule"
if ! git -C "$REPO_ROOT" submodule update --init --recursive 2>/tmp/mf-submod.err; then
cat /tmp/mf-submod.err >&2 || true
die "git submodule update failed. If this is the 'not our ref' / unreachable-pin error, the recorded memllib commit is on an unpushed branch (feat/nisps-core-swap). Push that memllib branch to a reachable remote, or bump the submodule pointer to a published commit, then re-run."
fi
if [[ ! -e "$REPO_ROOT/firmware/MEMLNaut-NISPS/src/memllib/hardware/memlnaut" ]]; then
die "src/memllib looks empty after submodule init — the memllib pin is unreachable. See the message above."
fi
# ---- 4. toolchain bring-up (delegates the heavy lifting) ----------------
log "Bringing up the arduino-cli toolchain + rp2040 core + libraries"
if (( SETUP_ONLY )); then
"$REPO_ROOT/scripts/setup-firmware-toolchain.sh" --no-build
log "Setup complete (--setup-only). Run without --setup-only to compile."
exit 0
fi
"$REPO_ROOT/scripts/setup-firmware-toolchain.sh" --no-build
# ---- 5. build the requested variant ------------------------------------
export PATH="$HOME/.local/bin:$PATH"
log "Building firmware variant: $VARIANT"
"$REPO_ROOT/scripts/build-firmware.sh" "$VARIANT"
log "Done. Flash with: scripts/flash-firmware.sh (hold BOOTSEL while plugging in the RP2350)."