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:
parent
45e59e1a23
commit
0a17b3e76e
3 changed files with 303 additions and 0 deletions
187
assets/media/showcase/index.html
Normal file
187
assets/media/showcase/index.html
Normal 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 0–1 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 0–1 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 & 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>
|
||||
|
|
@ -668,6 +668,26 @@ function HelpDrawer() {
|
|||
re-roll (set the behaviour in the Learning drawer). Went too far → undo. The dock reveals
|
||||
exactly as much machinery as you reach for.
|
||||
</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
96
scripts/build-firmware-arch.sh
Executable 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)."
|
||||
Loading…
Reference in a new issue