Restructure (1/2): Concepts = landing with live embeds; shared chrome partials; Showcase redesign
- Concepts is now the landing (/): index.html is the form-factor gallery with the LIVE widget embedded in every box (editor/teacher/stage/micro/showcase/initial), on the shared header/footer. concepts.html retired; every "Concepts" link → /. - New shared chrome partials src/header.html, src/footer.html, src/chrome.js (assembled by build.sh) + .site-foot / details.spec styles in base.css. Applied to the landing + showcase this pass. - Showcase redesign per spec: the pendulum bar IS the display — each lane's subdivisions/accents ride along the rod as moving RGB light (all meters combined); transparent outside the body (no black window); a printed tempo scale on the vertical axis with a draggable weight to set tempo; start is an external button (the real unit starts when lifted from its holder). Next pass: roll the shared header/footer onto the remaining pages (incl. the editor header-above-toolbar), merge Open=Info into one page per form factor with the expandable Info & BOM, and add teacher-style dimensioned views to Stage/Micro/Showcase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fb55a84aa2
commit
be00ebf097
22 changed files with 253 additions and 401 deletions
|
|
@ -20,9 +20,8 @@ State (set lists, the practice log, theme and UI preferences) lives in `localSto
|
||||||
|
|
||||||
| URL | What |
|
| URL | What |
|
||||||
|-----|------|
|
|-----|------|
|
||||||
| [`/`](https://metronome.varasys.io/) `index.html` | **Landing** — the PolyMeter front door (hero + form‑factor cards) |
|
| [`/`](https://metronome.varasys.io/) `index.html` | **Concepts** — the landing / form‑factor gallery; each box embeds the live widget |
|
||||||
| `/editor.html` | **PE‑1 — PolyMeter Editor** (the main app) |
|
| `/editor.html` | **PE‑1 — PolyMeter Editor** (the main app) |
|
||||||
| `/concepts.html` | **PolyMeter Concepts** — the form‑factor gallery (cards → live page + info) |
|
|
||||||
| `/player.html` | **PM‑1 Initial** — idealized concept device (full display + set‑list nav, theme, fullscreen "stage" view) |
|
| `/player.html` | **PM‑1 Initial** — idealized concept device (full display + set‑list nav, theme, fullscreen "stage" view) |
|
||||||
| `/teacher.html` | **PM‑1 Teacher** — studio / lesson console (colour TFT, arcade buttons, 1/4″ instrument pass‑through with analog click injection) |
|
| `/teacher.html` | **PM‑1 Teacher** — studio / lesson console (colour TFT, arcade buttons, 1/4″ instrument pass‑through with analog click injection) |
|
||||||
| `/stage.html` | **PM‑1 Stage** — foot‑pedal stompbox (two footswitches, expression‑pedal in, RGB beat light, instrument pass‑through) |
|
| `/stage.html` | **PM‑1 Stage** — foot‑pedal stompbox (two footswitches, expression‑pedal in, RGB beat light, instrument pass‑through) |
|
||||||
|
|
@ -232,9 +231,9 @@ Push the tag, then deploy.
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `index.html` | the **landing page** (site front door) |
|
| `index.html` | the **Concepts** landing / gallery (embeds each widget live) |
|
||||||
| `editor.html` | the **PE‑1 editor** app (source, with `@BUILD:*` markers) |
|
| `editor.html` | the **PE‑1 editor** app (source, with `@BUILD:*` markers) |
|
||||||
| `concepts.html` | the form‑factor gallery |
|
| `src/header.html` · `src/footer.html` · `src/chrome.js` | shared header / footer / theme chrome, inlined into every page |
|
||||||
| `player.html` · `teacher.html` · `stage.html` · `micro.html` · `showcase.html` | the device mockups (PM‑1 Initial / Teacher / Stage, PM‑µ Micro, PM‑S Showcase) |
|
| `player.html` · `teacher.html` · `stage.html` · `micro.html` · `showcase.html` | the device mockups (PM‑1 Initial / Teacher / Stage, PM‑µ Micro, PM‑S Showcase) |
|
||||||
| `info-*.html` | per‑form‑factor info pages (purpose + priced BOM for buildable hardware) |
|
| `info-*.html` | per‑form‑factor info pages (purpose + priced BOM for buildable hardware) |
|
||||||
| `embed.html` · `embed.js` | embed docs and the drop‑in widget loader |
|
| `embed.html` · `embed.js` | embed docs and the drop‑in widget loader |
|
||||||
|
|
|
||||||
2
build.sh
2
build.sh
|
|
@ -32,7 +32,7 @@ def build(name):
|
||||||
return out.stat().st_size
|
return out.stat().st_size
|
||||||
|
|
||||||
for name in ("index.html","editor.html","player.html","teacher.html","stage.html","micro.html","showcase.html",
|
for name in ("index.html","editor.html","player.html","teacher.html","stage.html","micro.html","showcase.html",
|
||||||
"concepts.html","embed.html",
|
"embed.html",
|
||||||
"info-editor.html","info-initial.html","info-teacher.html","info-stage.html","info-micro.html","info-showcase.html"):
|
"info-editor.html","info-initial.html","info-teacher.html","info-stage.html","info-micro.html","info-showcase.html"):
|
||||||
print("built %s (%dKB)" % (name, build(name) // 1024))
|
print("built %s (%dKB)" % (name, build(name) // 1024))
|
||||||
pathlib.Path("dist/embed.js").write_text(pathlib.Path("embed.js").read_text()) # loader, served as-is
|
pathlib.Path("dist/embed.js").write_text(pathlib.Path("embed.js").read_text()) # loader, served as-is
|
||||||
|
|
|
||||||
143
concepts.html
143
concepts.html
|
|
@ -1,143 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>PolyMeter Concepts — VARASYS</title>
|
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
|
||||||
<!-- The PolyMeter concept library: an ever-expanding gallery of form factors
|
|
||||||
(the PE-1 editor + the PM-1/PM-µ hardware mockups + web widgets). Static page. -->
|
|
||||||
<script>
|
|
||||||
(function(){ try{
|
|
||||||
var p = localStorage.getItem("metronome.theme");
|
|
||||||
if (p!=="light" && p!=="dark" && p!=="system") p = "system";
|
|
||||||
document.documentElement.dataset.theme = p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
|
|
||||||
} catch(e){ document.documentElement.dataset.theme = "dark"; } })();
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
/*@BUILD:include:src/base.css@*/
|
|
||||||
:root{ --bg1:#12151c; --bg2:#05070a; --txt:#c7d0db; --muted:#7f8b9a; --link:#6cb6ff;
|
|
||||||
--panel-bg:#161b22; --panel-bd:#2a313c; }
|
|
||||||
:root[data-theme="light"]{ --bg1:#f5f8fc; --bg2:#dde4ec; --txt:#1e2630; --muted:#5c6776; --link:#1769c4;
|
|
||||||
--panel-bg:#ffffff; --panel-bd:#d2dae4; }
|
|
||||||
body{ margin:0; min-height:100vh; padding:22px 16px 56px; color:var(--txt);
|
|
||||||
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); }
|
|
||||||
a{ color:var(--link); }
|
|
||||||
main{ width:100%; max-width:980px; margin:26px auto 0; }
|
|
||||||
h1{ font-size:24px; margin:0 0 4px; }
|
|
||||||
.lead{ margin:0 0 22px; color:var(--muted); font-size:14px; line-height:1.55; max-width:62ch; }
|
|
||||||
.grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(250px, 1fr)); gap:16px; }
|
|
||||||
.card{ background:var(--panel-bg); border:1px solid var(--panel-bd); border-radius:14px; padding:16px;
|
|
||||||
display:flex; flex-direction:column; gap:9px; }
|
|
||||||
.card h3{ margin:0; font-size:16px; }
|
|
||||||
.chip{ align-self:flex-start; font-size:10px; text-transform:uppercase; letter-spacing:.08em;
|
|
||||||
padding:2px 9px; border-radius:999px; border:1px solid var(--panel-bd); color:var(--muted); }
|
|
||||||
.chip.hw{ color:var(--cyan); border-color:rgba(10,179,247,.45); }
|
|
||||||
.chip.app{ color:#2fe07a; border-color:rgba(47,224,160,.45); }
|
|
||||||
.card p{ margin:0; font-size:13px; color:var(--muted); line-height:1.5; flex:1; }
|
|
||||||
.card .links{ display:flex; gap:16px; margin-top:4px; }
|
|
||||||
.card .links a{ color:var(--link); text-decoration:none; font-size:13px; font-weight:600; }
|
|
||||||
.card.soon{ opacity:.65; border-style:dashed; }
|
|
||||||
.site-foot{ max-width:980px; margin:40px auto 0; font-size:12px; color:var(--muted); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<header class="site-head">
|
|
||||||
<div class="head-left">
|
|
||||||
<a class="brand" href="/" title="VARASYS — Simplifying Complexity">
|
|
||||||
<img class="brand-logo brand-dark" src="data:image/png;base64,@BUILD:logo-dark@" alt="VARASYS — Simplifying Complexity" />
|
|
||||||
<img class="brand-logo brand-light" src="data:image/png;base64,@BUILD:logo-light@" alt="VARASYS — Simplifying Complexity" />
|
|
||||||
</a>
|
|
||||||
<span class="page-name"><b>PolyMeter</b> · Concepts</span>
|
|
||||||
</div>
|
|
||||||
<nav class="site-nav">
|
|
||||||
<a href="/editor.html">Editor</a>
|
|
||||||
<span class="here">Concepts</span>
|
|
||||||
<a href="/embed.html">Embed</a>
|
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<h1>PolyMeter Concepts</h1>
|
|
||||||
<p class="lead">One polymeter engine, many form factors. The same firmware/share-language drives the
|
|
||||||
web editor and every device idea below — an ever-expanding library of concepts, from a full editor to
|
|
||||||
pocket practice hardware. Open one to try it live.</p>
|
|
||||||
|
|
||||||
<div class="grid">
|
|
||||||
<div class="card">
|
|
||||||
<span class="chip app">Web app</span>
|
|
||||||
<h3>PE‑1 — PolyMeter Editor</h3>
|
|
||||||
<p>The full editor: stack meter lanes, per‑step accents / ghosts / mutes, swing & ratio polyrhythm,
|
|
||||||
set lists, and shareable links. This is where you design grooves.</p>
|
|
||||||
<div class="links"><a href="/editor.html">Open ↗</a><a href="/info-editor.html">Info ⓘ</a></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<span class="chip">Concept</span>
|
|
||||||
<h3>PM‑1 — Initial</h3>
|
|
||||||
<p>The original idealized device mock — full multi‑lane display and set‑list navigation. A north‑star
|
|
||||||
concept (more than a single small unit can really show); the buildable take is Teacher.</p>
|
|
||||||
<div class="links"><a href="/player.html">Open ↗</a><a href="/info-initial.html">Info ⓘ</a></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<span class="chip hw">Hardware</span>
|
|
||||||
<h3>PM‑1 — Teacher</h3>
|
|
||||||
<p>Full‑feature desktop console for studio & lessons: 2.0″ colour TFT showing every lane, arcade buttons,
|
|
||||||
thumb‑roller, 1/4″ instrument pass‑through with analog click injection + balanced‑TRS out, USB‑C powered.</p>
|
|
||||||
<div class="links"><a href="/teacher.html">Open ↗</a><a href="/info-teacher.html">Info & BOM ⓘ</a></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<span class="chip hw">Hardware</span>
|
|
||||||
<h3>PM‑1 — Stage</h3>
|
|
||||||
<p>Foot‑pedal stompbox for live use: two heavy footswitches (tap / next), an expression‑pedal input, a
|
|
||||||
big floor‑readable RGB beat light, instrument pass‑through with analog click, dual‑USB‑C daisy‑chain.</p>
|
|
||||||
<div class="links"><a href="/stage.html">Open ↗</a><a href="/info-stage.html">Info & BOM ⓘ</a></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<span class="chip hw">Hardware</span>
|
|
||||||
<h3>PM‑µ — Micro</h3>
|
|
||||||
<p>Long, narrow inline practice bar: instrument in one end, amp/headphones out the other, click mixed in.
|
|
||||||
Clickable thumb‑roller, amber 14‑segment display, USB‑C powered.</p>
|
|
||||||
<div class="links"><a href="/micro.html">Open ↗</a><a href="/info-micro.html">Info & BOM ⓘ</a></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<span class="chip hw">Hardware</span>
|
|
||||||
<h3>PM‑S — Showcase</h3>
|
|
||||||
<p>A display piece shaped like a classic pyramid wind‑up metronome — the pendulum is RGB light easing to
|
|
||||||
the beat, with light rows showing every lane's subdivisions, accents & mutes. USB‑C powered.</p>
|
|
||||||
<div class="links"><a href="/showcase.html">Open ↗</a><a href="/info-showcase.html">Info & BOM ⓘ</a></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card soon">
|
|
||||||
<span class="chip">Coming</span>
|
|
||||||
<h3>More form factors</h3>
|
|
||||||
<p>The library keeps growing — desktop, Eurorack, wearable, headless module… each a widget you can
|
|
||||||
embed. Ideas welcome.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<div class="site-foot">VARASYS · Simplifying Complexity — <span id="appVersion">v0.0.1-dev</span></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const APP_VERSION = "v0.0.1-dev";
|
|
||||||
const $ = (id)=>document.getElementById(id);
|
|
||||||
try{ $("appVersion").textContent = "v"+APP_VERSION.replace(/^v/,""); }catch(e){}
|
|
||||||
const THEMES=["system","light","dark"];
|
|
||||||
function effectiveTheme(p){ return p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches?"light":"dark") : p; }
|
|
||||||
function themePref(){ try{ const p=localStorage.getItem("metronome.theme"); return (p==="light"||p==="dark"||p==="system")?p:"system"; }catch(e){ return "system"; } }
|
|
||||||
function applyTheme(p){ try{ localStorage.setItem("metronome.theme",p); }catch(e){}
|
|
||||||
document.documentElement.dataset.theme = effectiveTheme(p);
|
|
||||||
$("themeBtn").textContent = p==="system" ? "◐" : p==="light" ? "☀" : "☾"; $("themeBtn").title="Theme: "+p+" (system → light → dark)"; }
|
|
||||||
$("themeBtn").onclick = ()=> applyTheme(THEMES[(THEMES.indexOf(themePref())+1)%THEMES.length]);
|
|
||||||
matchMedia("(prefers-color-scheme: light)").addEventListener("change", ()=>{ if(themePref()==="system") applyTheme("system"); });
|
|
||||||
applyTheme(themePref());
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -41,13 +41,14 @@ fi
|
||||||
# stamp the version into the built copy only (source stays clean)
|
# stamp the version into the built copy only (source stays clean)
|
||||||
echo "deployed v$BUILD -> $DEST_DIR"
|
echo "deployed v$BUILD -> $DEST_DIR"
|
||||||
for f in index.html editor.html player.html teacher.html stage.html micro.html showcase.html \
|
for f in index.html editor.html player.html teacher.html stage.html micro.html showcase.html \
|
||||||
concepts.html embed.html \
|
embed.html \
|
||||||
info-editor.html info-initial.html info-teacher.html info-stage.html info-micro.html info-showcase.html; do
|
info-editor.html info-initial.html info-teacher.html info-stage.html info-micro.html info-showcase.html; do
|
||||||
sed "s|const APP_VERSION = \"[^\"]*\";|const APP_VERSION = \"$BUILD\";|" "$DIST_DIR/$f" > "$DEST_DIR/$f"
|
sed "s|const APP_VERSION = \"[^\"]*\";|const APP_VERSION = \"$BUILD\";|" "$DIST_DIR/$f" > "$DEST_DIR/$f"
|
||||||
echo " $f ($(stat -c '%s' "$DEST_DIR/$f") bytes)"
|
echo " $f ($(stat -c '%s' "$DEST_DIR/$f") bytes)"
|
||||||
done
|
done
|
||||||
cp "$DIST_DIR/embed.js" "$DEST_DIR/embed.js"; echo " embed.js ($(stat -c '%s' "$DEST_DIR/embed.js") bytes)"
|
cp "$DIST_DIR/embed.js" "$DEST_DIR/embed.js"; echo " embed.js ($(stat -c '%s' "$DEST_DIR/embed.js") bytes)"
|
||||||
rm -f "$DEST_DIR/player-asbuilt.html" # renamed to teacher.html
|
rm -f "$DEST_DIR/player-asbuilt.html" # renamed to teacher.html
|
||||||
|
rm -f "$DEST_DIR/concepts.html" # Concepts is now the landing (/)
|
||||||
# (stage.html / info-stage.html are deployed again — now the foot-pedal Stage stompbox)
|
# (stage.html / info-stage.html are deployed again — now the foot-pedal Stage stompbox)
|
||||||
|
|
||||||
# If real audio samples are added later (see the plan's GM-sample note),
|
# If real audio samples are added later (see the plan's GM-sample note),
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,7 @@
|
||||||
<div class="row appheader" style="align-items:center; flex-wrap:wrap; gap:6px 14px; margin-bottom:8px">
|
<div class="row appheader" style="align-items:center; flex-wrap:wrap; gap:6px 14px; margin-bottom:8px">
|
||||||
<h1 style="margin:0">PE‑1 <span style="font-weight:400; opacity:.75">PolyMeter Editor</span> <span class="lane-meta" id="appVersion" title="build version">v0.0.1-dev</span></h1>
|
<h1 style="margin:0">PE‑1 <span style="font-weight:400; opacity:.75">PolyMeter Editor</span> <span class="lane-meta" id="appVersion" title="build version">v0.0.1-dev</span></h1>
|
||||||
<div class="appheader-ctrls" style="display:flex; align-items:center; gap:10px">
|
<div class="appheader-ctrls" style="display:flex; align-items:center; gap:10px">
|
||||||
<a href="/concepts.html" style="font-size:13px; color:var(--muted); text-decoration:none" title="Concept gallery — hardware & widget form factors">Concepts</a>
|
<a href="/" style="font-size:13px; color:var(--muted); text-decoration:none" title="Concept gallery — hardware & widget form factors">Concepts</a>
|
||||||
<a href="/info-editor.html" style="font-size:13px; color:var(--muted); text-decoration:none" title="About the PolyMeter Editor">Info</a>
|
<a href="/info-editor.html" style="font-size:13px; color:var(--muted); text-decoration:none" title="About the PolyMeter Editor">Info</a>
|
||||||
<a href="/embed.html" style="font-size:13px; color:var(--muted); text-decoration:none" title="Embed a PolyMeter widget">Embed</a>
|
<a href="/embed.html" style="font-size:13px; color:var(--muted); text-decoration:none" title="Embed a PolyMeter widget">Embed</a>
|
||||||
<button id="themeBtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" title="toggle light / dark theme">☀</button>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Editor</a>
|
<a href="/editor.html">Editor</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<span class="here">Embed</span>
|
<span class="here">Embed</span>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
189
index.html
189
index.html
|
|
@ -3,13 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>VARASYS PolyMeter — polymetric groove trainer & metronome</title>
|
<title>VARASYS PolyMeter — Concepts (polymetric groove trainer & metronome)</title>
|
||||||
<meta name="description" content="PolyMeter — a polymetric groove trainer and metronome. One engine, many form factors: a free web editor, hardware concepts, and an embeddable widget." />
|
<meta name="description" content="PolyMeter — a polymetric groove trainer and metronome. One engine, many form factors: a free web editor, hardware concepts, and an embeddable widget. Try each one live." />
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
||||||
<!-- Landing page: the front door to the PolyMeter family. The app itself is /editor.html. Static page. -->
|
|
||||||
<script>
|
<script>
|
||||||
(function(){ try{
|
(function(){ try{ var p = localStorage.getItem("metronome.theme");
|
||||||
var p = localStorage.getItem("metronome.theme");
|
|
||||||
if (p!=="light" && p!=="dark" && p!=="system") p = "system";
|
if (p!=="light" && p!=="dark" && p!=="system") p = "system";
|
||||||
document.documentElement.dataset.theme = p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
|
document.documentElement.dataset.theme = p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
|
||||||
} catch(e){ document.documentElement.dataset.theme = "dark"; } })();
|
} catch(e){ document.documentElement.dataset.theme = "dark"; } })();
|
||||||
|
|
@ -23,65 +21,45 @@
|
||||||
body{ margin:0; min-height:100vh; padding:22px 16px 56px; color:var(--txt);
|
body{ margin:0; min-height:100vh; padding:22px 16px 56px; color:var(--txt);
|
||||||
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); }
|
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); }
|
||||||
a{ color:var(--link); }
|
a{ color:var(--link); }
|
||||||
main{ width:100%; max-width:980px; margin:0 auto; }
|
main{ width:100%; max-width:1040px; margin:0 auto; }
|
||||||
|
|
||||||
/* hero */
|
.hero{ text-align:center; padding:40px 12px 26px; }
|
||||||
.hero{ text-align:center; padding:54px 12px 38px; }
|
.hero h1{ font-size:clamp(34px, 7vw, 58px); margin:0; letter-spacing:-.02em; line-height:1;
|
||||||
.hero h1{ font-size:clamp(40px, 9vw, 76px); margin:0; letter-spacing:-.02em; line-height:1;
|
|
||||||
background:linear-gradient(90deg, var(--cyan), #6cb6ff); -webkit-background-clip:text; background-clip:text; color:transparent; }
|
background:linear-gradient(90deg, var(--cyan), #6cb6ff); -webkit-background-clip:text; background-clip:text; color:transparent; }
|
||||||
.hero .tagline{ margin:16px auto 0; font-size:clamp(16px, 2.6vw, 21px); color:var(--txt); font-weight:600; }
|
.hero .tagline{ margin:14px auto 0; font-size:clamp(15px, 2.4vw, 19px); color:var(--txt); font-weight:600; }
|
||||||
.hero .pitch{ margin:14px auto 0; max-width:60ch; color:var(--muted); font-size:15px; line-height:1.6; }
|
.hero .pitch{ margin:12px auto 0; max-width:64ch; color:var(--muted); font-size:14.5px; line-height:1.6; }
|
||||||
.cta{ display:flex; gap:12px; justify-content:center; flex-wrap:wrap; margin-top:26px; }
|
|
||||||
.btn{ display:inline-flex; align-items:center; gap:6px; text-decoration:none; font-weight:600; font-size:15px;
|
|
||||||
padding:11px 20px; border-radius:10px; border:1px solid var(--panel-bd); color:var(--txt); background:var(--panel-bg);
|
|
||||||
transition:.14s; }
|
|
||||||
.btn:hover{ border-color:var(--cyan); }
|
|
||||||
.btn.primary{ color:#04121b; border-color:transparent; background:linear-gradient(180deg, #34c6ff, var(--cyan)); }
|
|
||||||
.btn.primary:hover{ filter:brightness(1.06); }
|
|
||||||
|
|
||||||
/* form-factor cards (shared look with the Concepts gallery) */
|
.section-label{ text-align:center; font-size:11px; text-transform:uppercase; letter-spacing:.12em; color:var(--muted); margin:26px 0 14px; }
|
||||||
.section-label{ text-align:center; font-size:11px; text-transform:uppercase; letter-spacing:.12em; color:var(--muted); margin:30px 0 14px; }
|
.grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); gap:18px; align-items:start; }
|
||||||
.grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(230px, 1fr)); gap:16px; }
|
.card{ background:var(--panel-bg); border:1px solid var(--panel-bd); border-radius:14px; padding:14px;
|
||||||
.card{ background:var(--panel-bg); border:1px solid var(--panel-bd); border-radius:14px; padding:16px;
|
display:flex; flex-direction:column; gap:10px; }
|
||||||
display:flex; flex-direction:column; gap:9px; }
|
.card.wide{ grid-column:1 / -1; }
|
||||||
|
.card-head{ display:flex; align-items:center; gap:9px; flex-wrap:wrap; }
|
||||||
.card h3{ margin:0; font-size:16px; }
|
.card h3{ margin:0; font-size:16px; }
|
||||||
.chip{ align-self:flex-start; font-size:10px; text-transform:uppercase; letter-spacing:.08em;
|
.chip{ font-size:10px; text-transform:uppercase; letter-spacing:.08em; padding:2px 9px; border-radius:999px;
|
||||||
padding:2px 9px; border-radius:999px; border:1px solid var(--panel-bd); color:var(--muted); }
|
border:1px solid var(--panel-bd); color:var(--muted); }
|
||||||
.chip.hw{ color:var(--cyan); border-color:rgba(10,179,247,.45); }
|
.chip.hw{ color:var(--cyan); border-color:rgba(10,179,247,.45); }
|
||||||
.chip.app{ color:#2fe07a; border-color:rgba(47,224,160,.45); }
|
.chip.app{ color:#2fe07a; border-color:rgba(47,224,160,.45); }
|
||||||
.card p{ margin:0; font-size:13px; color:var(--muted); line-height:1.5; flex:1; }
|
/* the embedded live widget sits in a recessed frame */
|
||||||
.card .links{ display:flex; gap:16px; margin-top:4px; }
|
.widget{ border:1px solid var(--panel-bd); border-radius:10px; overflow:hidden; background:radial-gradient(circle at 50% 0, rgba(255,255,255,.02), transparent); }
|
||||||
|
.widget [data-varasys-metronome], .widget iframe{ display:block; width:100%; }
|
||||||
|
.card p{ margin:0; font-size:13px; color:var(--muted); line-height:1.5; }
|
||||||
|
.card .links{ display:flex; gap:16px; }
|
||||||
.card .links a{ color:var(--link); text-decoration:none; font-size:13px; font-weight:600; }
|
.card .links a{ color:var(--link); text-decoration:none; font-size:13px; font-weight:600; }
|
||||||
.more{ text-align:center; margin-top:18px; font-size:14px; }
|
|
||||||
/* philosophy section */
|
/* philosophy */
|
||||||
.philosophy{ margin-top:34px; }
|
.philosophy{ margin-top:34px; }
|
||||||
.phil-grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); gap:16px; }
|
.phil-grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); gap:16px; }
|
||||||
.phil{ background:var(--panel-bg); border:1px solid var(--panel-bd); border-radius:14px; padding:18px 18px 16px; }
|
.phil{ background:var(--panel-bg); border:1px solid var(--panel-bd); border-radius:14px; padding:18px 18px 16px; }
|
||||||
.phil h3{ margin:0 0 8px; font-size:15px; display:flex; align-items:center; gap:8px; }
|
.phil h3{ margin:0 0 8px; font-size:15px; display:flex; align-items:center; gap:8px; }
|
||||||
.phil .ic{ font-size:17px; line-height:1; filter:grayscale(.1); }
|
.phil .ic{ font-size:17px; line-height:1; }
|
||||||
.phil p{ margin:0; font-size:13.5px; color:var(--muted); line-height:1.62; }
|
.phil p{ margin:0; font-size:13.5px; color:var(--muted); line-height:1.62; }
|
||||||
.phil p b{ color:var(--txt); }
|
.phil p b{ color:var(--txt); }
|
||||||
.site-foot{ max-width:980px; margin:42px auto 0; font-size:12px; color:var(--muted); text-align:center; }
|
|
||||||
.site-foot a{ color:var(--muted); }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<header class="site-head">
|
/*@BUILD:include:src/header.html@*/
|
||||||
<div class="head-left">
|
|
||||||
<a class="brand" href="/" title="VARASYS — Simplifying Complexity">
|
|
||||||
<img class="brand-logo brand-dark" src="data:image/png;base64,@BUILD:logo-dark@" alt="VARASYS — Simplifying Complexity" />
|
|
||||||
<img class="brand-logo brand-light" src="data:image/png;base64,@BUILD:logo-light@" alt="VARASYS — Simplifying Complexity" />
|
|
||||||
</a>
|
|
||||||
<span class="page-name"><b>PolyMeter</b></span>
|
|
||||||
</div>
|
|
||||||
<nav class="site-nav">
|
|
||||||
<a href="/editor.html">Editor</a>
|
|
||||||
<a href="/concepts.html">Concepts</a>
|
|
||||||
<a href="/embed.html">Embed</a>
|
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
|
|
@ -89,108 +67,99 @@
|
||||||
<p class="tagline">Polymetric grooves — one engine, many form factors.</p>
|
<p class="tagline">Polymetric grooves — one engine, many form factors.</p>
|
||||||
<p class="pitch">Stack independent meter lanes, each with its own subdivision, drum voice and per‑step
|
<p class="pitch">Stack independent meter lanes, each with its own subdivision, drum voice and per‑step
|
||||||
accents, to build true polymeter and ratio polyrhythm. Design it in the browser, save it as a compact
|
accents, to build true polymeter and ratio polyrhythm. Design it in the browser, save it as a compact
|
||||||
program string, and play it back on the editor, the hardware concepts, or an embedded widget — all the
|
program string, then run it on any device below — they're all the same engine. Each box is <b>live</b>:
|
||||||
same engine.</p>
|
press play and try it right here.</p>
|
||||||
<div class="cta">
|
|
||||||
<a class="btn primary" href="/editor.html">Open the Editor →</a>
|
|
||||||
<a class="btn" href="/concepts.html">Browse concepts</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="section-label">The PolyMeter family</div>
|
<div class="section-label">The PolyMeter family — try each one live</div>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card">
|
|
||||||
<span class="chip app">Web app</span>
|
<div class="card wide">
|
||||||
<h3>PE‑1 — PolyMeter Editor</h3>
|
<div class="card-head"><span class="chip app">Web app</span><h3>PE‑1 — PolyMeter Editor</h3></div>
|
||||||
<p>The full editor: stack meter lanes, per‑step accents / ghosts / mutes, swing & ratio polyrhythm,
|
<div class="widget"><div data-varasys-metronome="editor" data-patch="v1;t120;b16;kick:4=X..x;snare:4=.X.X;hatClosed:4/4;tom:3" data-height="560"></div></div>
|
||||||
set lists, and shareable links. This is where you design grooves.</p>
|
<p>The full workbench: stack meter lanes, per‑step accents / ghosts / mutes, swing & ratio polyrhythm,
|
||||||
<div class="links"><a href="/editor.html">Open ↗</a><a href="/info-editor.html">Info ⓘ</a></div>
|
set lists and shareable links. This is where you design grooves.</p>
|
||||||
|
<div class="links"><a href="/editor.html">Open ↗</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<span class="chip hw">Hardware</span>
|
<div class="card-head"><span class="chip hw">Hardware</span><h3>PM‑1 — Teacher</h3></div>
|
||||||
<h3>PM‑1 — Teacher</h3>
|
<div class="widget"><div data-varasys-metronome="teacher" data-patch="v1;t120;b16;kick:4=X.x.;snare:4=.X.X;hatClosed:4/4" data-height="440"></div></div>
|
||||||
<p>Full‑feature desktop console for studio & lessons: 2.0″ colour TFT showing every lane, arcade buttons,
|
<p>Full‑feature desktop console for studio & lessons: colour TFT showing every lane, arcade buttons,
|
||||||
thumb‑roller, 1/4″ instrument pass‑through with analog click injection + balanced‑TRS out. USB‑C powered.</p>
|
thumb‑roller, instrument pass‑through with analog click.</p>
|
||||||
<div class="links"><a href="/teacher.html">Open ↗</a><a href="/info-teacher.html">Info & BOM ⓘ</a></div>
|
<div class="links"><a href="/teacher.html">Open ↗</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<span class="chip hw">Hardware</span>
|
<div class="card-head"><span class="chip hw">Hardware</span><h3>PM‑1 — Stage</h3></div>
|
||||||
<h3>PM‑1 — Stage</h3>
|
<div class="widget"><div data-varasys-metronome="stage" data-patch="v1;t120;b16;kick:4=X.x.;snare:4=.X.X;hatClosed:4/4" data-height="430"></div></div>
|
||||||
<p>Foot‑pedal stompbox: two footswitches (tap / next), expression‑pedal input, a big floor‑readable RGB
|
<p>Foot‑pedal stompbox: two footswitches (tap / next), expression‑pedal input, a big floor‑readable RGB
|
||||||
beat light, instrument pass‑through with analog click. Dual‑USB‑C daisy‑chain.</p>
|
beat light, instrument pass‑through. Dual‑USB‑C daisy‑chain.</p>
|
||||||
<div class="links"><a href="/stage.html">Open ↗</a><a href="/info-stage.html">Info & BOM ⓘ</a></div>
|
<div class="links"><a href="/stage.html">Open ↗</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<span class="chip hw">Hardware</span>
|
<div class="card-head"><span class="chip hw">Hardware</span><h3>PM‑µ — Micro</h3></div>
|
||||||
<h3>PM‑µ — Micro</h3>
|
<div class="widget"><div data-varasys-metronome="micro" data-patch="v1;t120;kick:4;snare:4=.X.X;hatClosed:4/2" data-height="240"></div></div>
|
||||||
<p>Long, narrow inline practice bar: instrument in one end, amp/headphones out the other, click mixed in.
|
<p>Long, narrow inline practice bar: instrument in one end, amp/headphones out the other, click mixed in.
|
||||||
Clickable thumb‑roller, amber 14‑segment display, USB‑C powered.</p>
|
Clickable thumb‑roller, amber 14‑segment display, USB‑C.</p>
|
||||||
<div class="links"><a href="/micro.html">Open ↗</a><a href="/info-micro.html">Info & BOM ⓘ</a></div>
|
<div class="links"><a href="/micro.html">Open ↗</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<span class="chip hw">Hardware</span>
|
<div class="card-head"><span class="chip hw">Hardware</span><h3>PM‑S — Showcase</h3></div>
|
||||||
<h3>PM‑S — Showcase</h3>
|
<div class="widget"><div data-varasys-metronome="showcase" data-patch="v1;t108;kick:4=X..x;snare:4=.X.X;hatClosed:4/4;tom:3~" data-height="540"></div></div>
|
||||||
<p>A display piece shaped like a classic pyramid wind‑up metronome — an RGB‑light pendulum easing to the
|
<p>A display piece shaped like a classic pyramid wind‑up metronome — the pendulum is RGB light easing to
|
||||||
beat, with light rows for every lane's subdivisions, accents & mutes. USB‑C powered.</p>
|
the beat, with light rows for every lane's subdivisions, accents & mutes.</p>
|
||||||
<div class="links"><a href="/showcase.html">Open ↗</a><a href="/info-showcase.html">Info & BOM ⓘ</a></div>
|
<div class="links"><a href="/showcase.html">Open ↗</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<span class="chip">Widget</span>
|
<div class="card-head"><span class="chip">Concept</span><h3>PM‑1 — Initial</h3></div>
|
||||||
<h3>Embed anywhere</h3>
|
<div class="widget"><div data-varasys-metronome="initial" data-patch="v1;t120;b16;kick:4=X.x.;snare:4=.X.X;hatClosed:4/4" data-height="440"></div></div>
|
||||||
<p>Drop any form factor into your own page with one <code><div></code> + a script — preloaded with a
|
<p>The original idealized device render — full multi‑lane display and set‑list navigation. The north‑star
|
||||||
program string and auto‑sizing. Our own pages use the same loader.</p>
|
concept; the buildable take is the Teacher.</p>
|
||||||
<div class="links"><a href="/embed.html">Docs ↗</a></div>
|
<div class="links"><a href="/player.html">Open ↗</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head"><span class="chip">Widget</span><h3>Embed anywhere</h3></div>
|
||||||
|
<p style="flex:1">Every form factor above is an embeddable widget — drop one into your own page with a single
|
||||||
|
<code><div></code> + a script, preloaded with a program string and auto‑sizing. (Exactly how the
|
||||||
|
boxes on this page work.)</p>
|
||||||
|
<div class="links"><a href="/embed.html">Embed docs ↗</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p class="more"><a href="/concepts.html">See all concepts, including the PM‑1 Initial render →</a></p>
|
|
||||||
|
|
||||||
<section class="philosophy">
|
<section class="philosophy">
|
||||||
<div class="section-label">Philosophy</div>
|
<div class="section-label">Philosophy</div>
|
||||||
<div class="phil-grid">
|
<div class="phil-grid">
|
||||||
<div class="phil">
|
<div class="phil">
|
||||||
<h3><span class="ic">🛠️</span> Program on the web, play on any device</h3>
|
<h3><span class="ic">🛠️</span> Program on the web, play on any device</h3>
|
||||||
<p>The website is the workbench. Design your grooves in the <a href="/editor.html">PE‑1 editor</a> —
|
<p>The website is the workbench. Design your grooves in the <a href="/editor.html">PE‑1 editor</a> and
|
||||||
stack meters, set per‑step accents, build set lists — and every pattern saves to a compact
|
every pattern saves to a compact <b>program string</b> (a whole set list to a single code). That same
|
||||||
<b>program string</b> (a whole set list to a single code). That same string loads into whichever
|
string loads into whichever form factor fits the moment — Teacher on a studio desk, Stage on a
|
||||||
form factor fits the moment: the <a href="/teacher.html">Teacher</a> on a studio desk, the
|
pedalboard, Micro inline at the practice desk, or an <a href="/embed.html">embedded widget</a>. One
|
||||||
<a href="/micro.html">Micro</a> inline at the practice desk, or an <a href="/embed.html">embedded
|
engine, one language: author once, run it anywhere.</p>
|
||||||
widget</a> in someone else's app. One engine, one language — you author once and run it anywhere,
|
|
||||||
choosing the device by the use scenario rather than re‑learning a new box each time.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="phil">
|
<div class="phil">
|
||||||
<h3><span class="ic">🔌</span> USB‑C power everywhere — no batteries</h3>
|
<h3><span class="ic">🔌</span> USB‑C power everywhere — no batteries</h3>
|
||||||
<p>Every device in the family is powered over a single <b>USB‑C</b> port — no internal battery to
|
<p>Every device is powered over a single <b>USB‑C</b> port — no internal battery to wear out. Plug into a
|
||||||
swell, leak or wear out, and nothing proprietary to replace. Plug into a wall adapter for a
|
wall adapter for a permanent install or carry a power bank like you already do for your phone; the
|
||||||
permanent install, or carry a power bank exactly the way you already do for your phone. Standardising
|
larger units add a second USB‑C <b>pass‑through</b> so pedals daisy‑chain off one source. One connector
|
||||||
on one connector across the whole range keeps the builds simple and <b>future‑proofs</b> the
|
across the range keeps builds simple and <b>future‑proof</b>.</p>
|
||||||
project as USB‑C becomes universal.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="site-foot">VARASYS · Simplifying Complexity ·
|
/*@BUILD:include:src/footer.html@*/
|
||||||
<a href="https://codeberg.org/VARASYS/metronome" target="_blank" rel="noopener">source</a> —
|
|
||||||
<span id="appVersion">v0.0.1-dev</span></div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_VERSION = "v0.0.1-dev";
|
const APP_VERSION = "v0.0.1-dev";
|
||||||
const $ = (id)=>document.getElementById(id);
|
/*@BUILD:include:src/chrome.js@*/
|
||||||
try{ $("appVersion").textContent = "v"+APP_VERSION.replace(/^v/,""); }catch(e){}
|
|
||||||
const THEMES=["system","light","dark"];
|
|
||||||
function effectiveTheme(p){ return p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches?"light":"dark") : p; }
|
|
||||||
function themePref(){ try{ const p=localStorage.getItem("metronome.theme"); return (p==="light"||p==="dark"||p==="system")?p:"system"; }catch(e){ return "system"; } }
|
|
||||||
function applyTheme(p){ try{ localStorage.setItem("metronome.theme",p); }catch(e){}
|
|
||||||
document.documentElement.dataset.theme = effectiveTheme(p);
|
|
||||||
$("themeBtn").textContent = p==="system" ? "◐" : p==="light" ? "☀" : "☾"; $("themeBtn").title="Theme: "+p+" (system → light → dark)"; }
|
|
||||||
$("themeBtn").onclick = ()=> applyTheme(THEMES[(THEMES.indexOf(themePref())+1)%THEMES.length]);
|
|
||||||
matchMedia("(prefers-color-scheme: light)").addEventListener("change", ()=>{ if(themePref()==="system") applyTheme("system"); });
|
|
||||||
applyTheme(themePref());
|
|
||||||
</script>
|
</script>
|
||||||
|
<script src="/embed.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Open ↗</a>
|
<a href="/editor.html">Open ↗</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<a href="/embed.html">Embed</a>
|
<a href="/embed.html">Embed</a>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Editor</a>
|
<a href="/editor.html">Editor</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<a href="/player.html">Open ↗</a>
|
<a href="/player.html">Open ↗</a>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Editor</a>
|
<a href="/editor.html">Editor</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<a href="/micro.html">Open ↗</a>
|
<a href="/micro.html">Open ↗</a>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Editor</a>
|
<a href="/editor.html">Editor</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<a href="/showcase.html">Open ↗</a>
|
<a href="/showcase.html">Open ↗</a>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Editor</a>
|
<a href="/editor.html">Editor</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<a href="/stage.html">Open ↗</a>
|
<a href="/stage.html">Open ↗</a>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Editor</a>
|
<a href="/editor.html">Editor</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<a href="/teacher.html">Open ↗</a>
|
<a href="/teacher.html">Open ↗</a>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Editor</a>
|
<a href="/editor.html">Editor</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<a href="/info-micro.html">Info</a>
|
<a href="/info-micro.html">Info</a>
|
||||||
<a href="/embed.html">Embed</a>
|
<a href="/embed.html">Embed</a>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Editor</a>
|
<a href="/editor.html">Editor</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<a href="/info-initial.html">Info</a>
|
<a href="/info-initial.html">Info</a>
|
||||||
<a href="/embed.html">Embed</a>
|
<a href="/embed.html">Embed</a>
|
||||||
<button id="fsBtn" class="tbtn" title="Full screen (landscape)">⛶</button>
|
<button id="fsBtn" class="tbtn" title="Full screen (landscape)">⛶</button>
|
||||||
|
|
|
||||||
221
showcase.html
221
showcase.html
|
|
@ -6,7 +6,6 @@
|
||||||
<title>VARASYS PM‑S — Showcase (RGB pendulum metronome)</title>
|
<title>VARASYS PM‑S — Showcase (RGB pendulum metronome)</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
||||||
<script>
|
<script>
|
||||||
/* ?embed=1 → strip site chrome + auto-size to the host */
|
|
||||||
(function(){ if(!/[?&]embed=1/.test(location.search)) return;
|
(function(){ if(!/[?&]embed=1/.test(location.search)) return;
|
||||||
document.documentElement.dataset.embed="1";
|
document.documentElement.dataset.embed="1";
|
||||||
function ph(){ try{ parent.postMessage({type:"varasys-h",h:Math.ceil(document.documentElement.getBoundingClientRect().height)},"*"); }catch(e){} }
|
function ph(){ try{ parent.postMessage({type:"varasys-h",h:Math.ceil(document.documentElement.getBoundingClientRect().height)},"*"); }catch(e){} }
|
||||||
|
|
@ -15,12 +14,13 @@
|
||||||
</script>
|
</script>
|
||||||
<!--
|
<!--
|
||||||
PM-S "Showcase" — a display-piece metronome shaped like a classic pyramid wind-up
|
PM-S "Showcase" — a display-piece metronome shaped like a classic pyramid wind-up
|
||||||
unit, but the swinging pendulum is simulated with RGB light (a glowing bob on a
|
unit. The pendulum is the whole show: a single RGB LED bar where every lane's
|
||||||
rod, easing to the extremes on each beat exactly like the mechanical original),
|
subdivisions / accents are combined ALONG its length (each lane is a moving point
|
||||||
and rows of RGB segment lights show every lane's subdivisions / accents / mutes.
|
of light), it carries a printed TEMPO scale on the vertical axis, and a sliding
|
||||||
Same RGB-everywhere RP2040 firmware/engine; USB-C powered (dual-port daisy-chain).
|
WEIGHT sets the tempo (drag it — up = slower, like the real thing). The canvas is
|
||||||
Visuals are latency-compensated so the swing lands when the click is HEARD.
|
transparent outside the body. There's no on-screen power switch: the real unit
|
||||||
Shares src/engine.js.
|
starts when you lift it from its holder / set it swinging — here that's an external
|
||||||
|
button. Latency-compensated visuals. Shares src/engine.js.
|
||||||
-->
|
-->
|
||||||
<script>
|
<script>
|
||||||
(function(){ try{ var p = localStorage.getItem("metronome.theme");
|
(function(){ try{ var p = localStorage.getItem("metronome.theme");
|
||||||
|
|
@ -31,24 +31,24 @@
|
||||||
<style>
|
<style>
|
||||||
/*@BUILD:include:src/base.css@*/
|
/*@BUILD:include:src/base.css@*/
|
||||||
:root{ --bg1:#12151c; --bg2:#05070a; --txt:#c7d0db; --muted:#7f8b9a; --link:#6cb6ff;
|
:root{ --bg1:#12151c; --bg2:#05070a; --txt:#c7d0db; --muted:#7f8b9a; --link:#6cb6ff;
|
||||||
--panel-bd:#2a313c; --field-bg:#0e1116; --field-bd:#2a313c; --cyan:#0AB3F7; }
|
--panel-bd:#2a313c; --panel-bg:#161b22; --field-bg:#0e1116; --field-bd:#2a313c; --cyan:#0AB3F7; }
|
||||||
:root[data-theme="light"]{ --bg1:#f5f8fc; --bg2:#dde4ec; --txt:#1e2630; --muted:#5c6776; --link:#1769c4; --panel-bd:#d2dae4; --field-bg:#f1f4f8; --field-bd:#d2dae4 }
|
:root[data-theme="light"]{ --bg1:#f5f8fc; --bg2:#dde4ec; --txt:#1e2630; --muted:#5c6776; --link:#1769c4; --panel-bd:#d2dae4; --panel-bg:#fff; --field-bg:#f1f4f8; --field-bd:#d2dae4 }
|
||||||
body{ margin:0; min-height:100vh; padding:24px 14px 44px;
|
body{ margin:0; min-height:100vh; padding:24px 14px 44px;
|
||||||
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); color:var(--txt);
|
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); color:var(--txt);
|
||||||
display:flex; flex-direction:column; align-items:center; gap:14px }
|
display:flex; flex-direction:column; align-items:center; gap:16px }
|
||||||
a{ color:var(--link) }
|
a{ color:var(--link) }
|
||||||
|
main{ display:flex; flex-direction:column; align-items:center; gap:16px; width:100% }
|
||||||
|
|
||||||
.device{ width:100%; max-width:300px; filter:drop-shadow(0 22px 34px rgba(0,0,0,.6)); }
|
.device{ width:100%; max-width:300px; filter:drop-shadow(0 22px 34px rgba(0,0,0,.55)); }
|
||||||
#stage{ display:block; width:100%; height:auto }
|
#stage{ display:block; width:100%; height:auto; touch-action:none; cursor:ns-resize }
|
||||||
|
|
||||||
.ctrls{ display:flex; align-items:center; gap:14px; flex-wrap:wrap; justify-content:center }
|
.ctrls{ display:flex; align-items:center; gap:14px; flex-wrap:wrap; justify-content:center }
|
||||||
.ctrls button{ background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:9px;
|
.ctrls button{ background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:9px;
|
||||||
padding:7px 12px; font-size:14px; line-height:1; cursor:pointer }
|
padding:7px 12px; font-size:14px; line-height:1; cursor:pointer }
|
||||||
.ctrls button:hover{ border-color:var(--cyan) }
|
.ctrls button:hover{ border-color:var(--cyan) }
|
||||||
#play{ min-width:46px; font-size:15px }
|
#play{ min-width:64px; font-size:14px }
|
||||||
.tempo, .trk{ display:flex; align-items:center; gap:8px; font-size:13px; color:var(--muted) }
|
.trk{ display:flex; align-items:center; gap:8px; font-size:13px; color:var(--muted) }
|
||||||
.tempo b, .trk b{ color:var(--txt); min-width:34px; text-align:center; display:inline-block; font-variant-numeric:tabular-nums }
|
.trk b{ color:var(--txt); min-width:34px; text-align:center; display:inline-block; font-variant-numeric:tabular-nums }
|
||||||
.u{ font-size:10px; letter-spacing:.08em; text-transform:uppercase }
|
|
||||||
|
|
||||||
.hint{ max-width:340px; text-align:center; font-size:11px; color:var(--muted); line-height:1.5 }
|
.hint{ max-width:340px; text-align:center; font-size:11px; color:var(--muted); line-height:1.5 }
|
||||||
[data-embed] .hint{ display:none !important }
|
[data-embed] .hint{ display:none !important }
|
||||||
|
|
@ -56,34 +56,22 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<header class="site-head">
|
/*@BUILD:include:src/header.html@*/
|
||||||
<div class="head-left">
|
|
||||||
<a class="brand" href="/" title="VARASYS — Simplifying Complexity">
|
|
||||||
<img class="brand-logo brand-dark" src="data:image/png;base64,@BUILD:logo-dark@" alt="VARASYS — Simplifying Complexity" />
|
|
||||||
<img class="brand-logo brand-light" src="data:image/png;base64,@BUILD:logo-light@" alt="VARASYS — Simplifying Complexity" />
|
|
||||||
</a>
|
|
||||||
<span class="page-name"><b>PM‑S</b> · Showcase (RGB pendulum)</span>
|
|
||||||
</div>
|
|
||||||
<nav class="site-nav">
|
|
||||||
<a href="/editor.html">Editor</a>
|
|
||||||
<a href="/concepts.html">Concepts</a>
|
|
||||||
<a href="/info-showcase.html">Info</a>
|
|
||||||
<a href="/embed.html">Embed</a>
|
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="device"><canvas id="stage" width="300" height="440" aria-label="RGB pendulum metronome"></canvas></div>
|
<main>
|
||||||
|
<div class="device"><canvas id="stage" width="300" height="470" aria-label="RGB pendulum metronome"></canvas></div>
|
||||||
|
|
||||||
<div class="ctrls">
|
<div class="ctrls">
|
||||||
<button id="play" title="Start / stop (Space)">▶</button>
|
<button id="play" title="Start / stop (Space) — the real unit starts when lifted from its holder">▶ Start</button>
|
||||||
<div class="tempo"><button id="slower" title="Slower">−</button><b id="bpmLbl">120</b><span class="u">bpm</span><button id="faster" title="Faster">+</button></div>
|
|
||||||
<div class="trk"><button id="prev" title="Previous">‹</button><b id="trkLbl">—</b><button id="next" title="Next">›</button></div>
|
<div class="trk"><button id="prev" title="Previous">‹</button><b id="trkLbl">—</b><button id="next" title="Next">›</button></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hint">A showcase piece: the <b>RGB pendulum</b> swings in time (easing to the beat like a real wind‑up
|
<div class="hint">The pendulum <b>is</b> the display: every lane's subdivisions & accents ride along the bar as
|
||||||
metronome); the light rows below show each lane's <b>subdivisions, accents & mutes</b>. Click the piece to
|
moving RGB light. Drag the <b>weight</b> up/down (or scroll) to set tempo — the scale is printed on the bar,
|
||||||
start/stop · scroll it for tempo.</div>
|
just like a wind‑up metronome. (No power switch: the real one starts when you lift it from its holder.)</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
/*@BUILD:include:src/footer.html@*/
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_VERSION = "v0.0.1-dev";
|
const APP_VERSION = "v0.0.1-dev";
|
||||||
|
|
@ -134,89 +122,75 @@ function loadTrack(i){
|
||||||
const was=state.running; if(was){ clearInterval(schedulerTimer); schedulerTimer=null; state.running=false; }
|
const was=state.running; if(was){ clearInterval(schedulerTimer); schedulerTimer=null; state.running=false; }
|
||||||
if(was) startAudio(); syncBtns();
|
if(was) startAudio(); syncBtns();
|
||||||
}
|
}
|
||||||
function syncBtns(){ $("play").textContent = state.running ? "■" : "▶";
|
function syncBtns(){ $("play").textContent = state.running ? "■ Stop" : "▶ Start";
|
||||||
$("bpmLbl").textContent = state.bpm; $("trkLbl").textContent = (trackIdx+1)+"/"+tracks.length; }
|
$("trkLbl").textContent = (trackIdx+1)+"/"+tracks.length; }
|
||||||
|
|
||||||
/* ========================= RGB PENDULUM + LANE LIGHTS (canvas) =============== */
|
/* ========================= RGB PENDULUM (canvas) ============================= */
|
||||||
const cv=$("stage"), g=cv.getContext("2d"), CW=300, CH=440;
|
const cv=$("stage"), g=cv.getContext("2d"), CW=300, CH=470;
|
||||||
(function(){ const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1)); cv.width=CW*dpr; cv.height=CH*dpr; g.scale(dpr,dpr); })();
|
(function(){ const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1)); cv.width=CW*dpr; cv.height=CH*dpr; g.scale(dpr,dpr); })();
|
||||||
const MAXANG=0.42; // pendulum swing (rad) ~24°
|
const MAXANG=0.40;
|
||||||
|
const PIVX=150, PIVY=380, ROD=292; // pivot near the base; rod points up
|
||||||
|
const F_FAST=0.30, F_SLOW=0.94; // weight fraction along rod at 240 / 40 BPM (top=slow)
|
||||||
let beatCount=-1, lastBeatTime=0, pend=0, flash=0, flashAccent=false;
|
let beatCount=-1, lastBeatTime=0, pend=0, flash=0, flashAccent=false;
|
||||||
function audioLatency(){ return audioCtx ? (audioCtx.outputLatency || audioCtx.baseLatency || 0) : 0; }
|
function audioLatency(){ return audioCtx ? (audioCtx.outputLatency || audioCtx.baseLatency || 0) : 0; }
|
||||||
|
const LEVELCOL = { 2:[255,155,46], 1:[51,208,255], 3:[155,123,255], 0:[70,80,95] }; // accent / normal / ghost / mute (rgb)
|
||||||
|
|
||||||
const LEVELCOL = { 2:"#ff9b2e", 1:"#33d0ff", 3:"#9b7bff", 0:"#39424f" }; // accent / normal / ghost / mute
|
function bpmToFrac(b){ return F_SLOW - (Math.max(40,Math.min(240,b))-40)/200*(F_SLOW-F_FAST); }
|
||||||
|
function fracToBpm(f){ return Math.round(240 - (Math.max(F_FAST,Math.min(F_SLOW,f))-F_FAST)/(F_SLOW-F_FAST)*200); }
|
||||||
function roundRect(x,y,w,h,r){ g.beginPath(); g.moveTo(x+r,y); g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r); g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); g.closePath(); }
|
|
||||||
|
|
||||||
function drawBody(){
|
function drawBody(){
|
||||||
// truncated-pyramid silhouette (classic metronome), matte graphite
|
g.clearRect(0,0,CW,CH); // transparent everywhere outside the body
|
||||||
g.clearRect(0,0,CW,CH);
|
const tlx=98,trx=202,topY=18, blx=20,brx=280,botY=440;
|
||||||
const tlx=92, trx=208, blx=24, brx=276, topY=14, botY=420;
|
const grd=g.createLinearGradient(0,0,0,botY); grd.addColorStop(0,"#2c2e34"); grd.addColorStop(1,"#131419");
|
||||||
const grd=g.createLinearGradient(0,0,0,botY); grd.addColorStop(0,"#2c2e34"); grd.addColorStop(1,"#141518");
|
|
||||||
g.beginPath(); g.moveTo(tlx,topY); g.lineTo(trx,topY); g.lineTo(brx,botY); g.lineTo(blx,botY); g.closePath();
|
g.beginPath(); g.moveTo(tlx,topY); g.lineTo(trx,topY); g.lineTo(brx,botY); g.lineTo(blx,botY); g.closePath();
|
||||||
g.fillStyle=grd; g.fill();
|
g.fillStyle=grd; g.fill(); g.lineWidth=1.5; g.strokeStyle="rgba(255,255,255,.06)"; g.stroke();
|
||||||
g.lineWidth=1.5; g.strokeStyle="rgba(255,255,255,.06)"; g.stroke();
|
|
||||||
// left-edge sheen
|
|
||||||
g.beginPath(); g.moveTo(tlx,topY); g.lineTo(blx,botY); g.lineWidth=2; g.strokeStyle="rgba(255,255,255,.05)"; g.stroke();
|
g.beginPath(); g.moveTo(tlx,topY); g.lineTo(blx,botY); g.lineWidth=2; g.strokeStyle="rgba(255,255,255,.05)"; g.stroke();
|
||||||
// brand silk
|
|
||||||
g.textAlign="center"; g.fillStyle="#aab2bc";
|
g.textAlign="center"; g.fillStyle="#aab2bc";
|
||||||
g.font="700 9px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText("V A R A S Y S", CW/2, 30);
|
g.font="700 9px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText("V A R A S Y S", CW/2, 33);
|
||||||
g.font="600 7.5px 'Segoe UI',Roboto,Arial,sans-serif"; g.globalAlpha=.8; g.fillText("PM‑S SHOWCASE", CW/2, 41); g.globalAlpha=1;
|
g.globalAlpha=.8; g.font="600 7.5px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText("PM‑S SHOWCASE", CW/2, 44); g.globalAlpha=1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawWindow(){
|
function drawPendulum(){
|
||||||
// recessed dark window for the pendulum
|
g.save(); g.translate(PIVX,PIVY); g.rotate(pend); // rod frame: up = -y, tilts with the swing
|
||||||
roundRect(70,56,160,212,10); g.fillStyle="#05070a"; g.fill();
|
|
||||||
g.lineWidth=1; g.strokeStyle="rgba(0,0,0,.8)"; g.stroke();
|
|
||||||
// faint scale ticks (an arc the bob sweeps across)
|
|
||||||
const px=CW/2, py=258; // pivot near the bottom of the window
|
|
||||||
g.strokeStyle="rgba(255,255,255,.07)"; g.lineWidth=1;
|
|
||||||
for(let k=-3;k<=3;k++){ const a=(k/3)*MAXANG; const x1=px+Math.sin(a)*150, y1=py-Math.cos(a)*150, x2=px+Math.sin(a)*162, y2=py-Math.cos(a)*162;
|
|
||||||
g.beginPath(); g.moveTo(px+Math.sin(a)*70,py-Math.cos(a)*70); g.lineTo(px+Math.sin(a)*150,py-Math.cos(a)*150); g.globalAlpha=.05; g.stroke(); g.globalAlpha=1; }
|
|
||||||
return {px,py};
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawPendulum(piv){
|
|
||||||
const px=piv.px, py=piv.py, L=176, a=pend;
|
|
||||||
const tipX=px+Math.sin(a)*L, tipY=py-Math.cos(a)*L;
|
|
||||||
const bobX=px+Math.sin(a)*L*0.66, bobY=py-Math.cos(a)*L*0.66;
|
|
||||||
// rod
|
// rod
|
||||||
g.strokeStyle="rgba(170,180,196,.5)"; g.lineWidth=3; g.lineCap="round";
|
g.strokeStyle="rgba(150,160,176,.45)"; g.lineWidth=3.5; g.lineCap="round";
|
||||||
g.beginPath(); g.moveTo(px,py); g.lineTo(tipX,tipY); g.stroke();
|
g.beginPath(); g.moveTo(0,0); g.lineTo(0,-ROD); g.stroke();
|
||||||
|
// printed tempo scale (numbers along the bar)
|
||||||
|
g.textAlign="right"; g.font="600 8px 'Segoe UI',Roboto,Arial,sans-serif";
|
||||||
|
[40,60,80,100,120,160,200,240].forEach(function(b){ const y=-bpmToFrac(b)*ROD;
|
||||||
|
g.strokeStyle="rgba(180,190,205,.5)"; g.lineWidth=1; g.beginPath(); g.moveTo(4,y); g.lineTo(10,y); g.stroke();
|
||||||
|
g.fillStyle="rgba(180,190,205,.6)"; g.fillText(String(b), 2, y+3); });
|
||||||
|
// combined-meter LEDs: each lane is a moving point of light along the bar (its current step position)
|
||||||
|
for(const m of meters){ if(m.currentStep<0 || !state.running) continue;
|
||||||
|
const steps=m.beatsPerBar*m.stepsPerBeat, fr=steps?((m.currentStep%steps)/steps):0;
|
||||||
|
const y=-(0.16 + fr*(0.96-0.16))*ROD, lvl=m.beatsOn[m.currentStep]|0; if(lvl===0) continue;
|
||||||
|
const c=LEVELCOL[lvl]||LEVELCOL[1], rgb="rgb("+c[0]+","+c[1]+","+c[2]+")";
|
||||||
|
g.shadowColor=rgb; g.shadowBlur=14; g.fillStyle=rgb;
|
||||||
|
g.beginPath(); g.arc(0,y, lvl>=2?6:4.5, 0,7); g.fill(); g.shadowBlur=0;
|
||||||
|
}
|
||||||
|
// fixed bob (drives the swing) near the bottom of the rod
|
||||||
|
g.fillStyle="#2a2f37"; roundRectP(-9,-58,18,30,4); g.fill();
|
||||||
|
g.fillStyle="rgba(255,255,255,.06)"; roundRectP(-9,-58,18,5,2); g.fill();
|
||||||
|
// sliding WEIGHT = tempo; glows on the beat
|
||||||
|
const wy=-bpmToFrac(state.bpm)*ROD, lit=Math.max(0,flash);
|
||||||
|
const wc = flashAccent ? "rgb(255,155,46)" : "rgb(51,208,255)";
|
||||||
|
g.shadowColor=wc; g.shadowBlur=8+22*lit;
|
||||||
|
g.fillStyle="#3a4049"; roundRectP(-15,wy-11,30,22,4); g.fill(); g.shadowBlur=0;
|
||||||
|
g.fillStyle=wc; g.globalAlpha=.30+0.7*lit; roundRectP(-13,wy-3.5,26,7,2); g.fill(); g.globalAlpha=1;
|
||||||
|
g.fillStyle="rgba(255,255,255,.10)"; roundRectP(-15,wy-11,30,5,2); g.fill();
|
||||||
// pivot hub
|
// pivot hub
|
||||||
g.beginPath(); g.arc(px,py,6,0,7); g.fillStyle="#2a2f37"; g.fill();
|
g.restore();
|
||||||
g.beginPath(); g.arc(px,py,2.5,0,7); g.fillStyle="#aab2bc"; g.fill();
|
g.beginPath(); g.arc(PIVX,PIVY,6,0,7); g.fillStyle="#2a2f37"; g.fill();
|
||||||
// glowing RGB bob (brightens on the beat tick)
|
g.beginPath(); g.arc(PIVX,PIVY,2.5,0,7); g.fillStyle="#aab2bc"; g.fill();
|
||||||
const col = flashAccent ? "#ff9b2e" : "#33d0ff", lit=0.4+0.6*Math.max(0,flash);
|
|
||||||
g.shadowColor=col; g.shadowBlur=18+26*Math.max(0,flash);
|
|
||||||
g.beginPath(); g.arc(bobX,bobY,12,0,7);
|
|
||||||
const bg=g.createRadialGradient(bobX-3,bobY-3,2,bobX,bobY,12); bg.addColorStop(0,"#ffffff"); bg.addColorStop(.4,col); bg.addColorStop(1,"rgba(0,0,0,.2)");
|
|
||||||
g.globalAlpha=lit; g.fillStyle=bg; g.fill(); g.globalAlpha=1; g.shadowBlur=0;
|
|
||||||
// tip glow dot
|
|
||||||
g.beginPath(); g.arc(tipX,tipY,3.5,0,7); g.fillStyle=col; g.shadowColor=col; g.shadowBlur=8*Math.max(.2,flash); g.fill(); g.shadowBlur=0;
|
|
||||||
}
|
}
|
||||||
|
function roundRectP(x,y,w,h,r){ g.beginPath(); g.moveTo(x+r,y); g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r); g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); g.closePath(); }
|
||||||
|
|
||||||
function drawLanes(){
|
function drawReadout(){
|
||||||
// RGB segment rows for each lane: subdivisions / accents / mutes, with the playhead
|
g.textAlign="center";
|
||||||
const lanes=meters.slice(0,4), x0=40, x1=260, top=294, rowH=26;
|
g.fillStyle="#c7d0db"; g.font="700 20px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText(String(state.bpm), CW/2-2, 420);
|
||||||
g.textAlign="left";
|
g.fillStyle="#7f8b9a"; g.font="600 8px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText("BPM", CW/2+38, 420);
|
||||||
for(let li=0; li<lanes.length; li++){ const m=lanes[li], steps=m.beatsPerBar*m.stepsPerBeat, y=top+li*rowH;
|
|
||||||
const cw=(x1-x0)/Math.max(1,steps), pad=Math.min(3, cw*0.18);
|
|
||||||
for(let s=0;s<steps;s++){ const lvl=m.beatsOn[s]|0, x=x0+s*cw, isBeat=(s%m.stepsPerBeat===0);
|
|
||||||
const cur = state.running && s===m.currentStep;
|
|
||||||
const base = cur ? 1 : (lvl===0 ? 0.10 : 0.18);
|
|
||||||
g.globalAlpha = base; g.fillStyle = LEVELCOL[lvl]||LEVELCOL[0];
|
|
||||||
const h = isBeat ? 13 : 9, yy = y + (13-h)/2;
|
|
||||||
if(cur){ g.shadowColor=LEVELCOL[lvl]||"#33d0ff"; g.shadowBlur=12; }
|
|
||||||
roundRect(x+pad, yy, cw-2*pad, h, 2); g.fill(); g.shadowBlur=0; g.globalAlpha=1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// BPM + item name on the base plinth
|
|
||||||
g.textAlign="center"; g.fillStyle="#c7d0db"; g.font="700 22px 'Segoe UI',Roboto,Arial,sans-serif";
|
|
||||||
g.fillText(String(state.bpm), CW/2-2, 410); g.font="600 9px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillStyle="#7f8b9a";
|
|
||||||
g.fillText("BPM", CW/2+44, 410);
|
|
||||||
const nm=(tracks[trackIdx]&&tracks[trackIdx].name)||"—";
|
const nm=(tracks[trackIdx]&&tracks[trackIdx].name)||"—";
|
||||||
g.fillStyle="#8f9aa6"; g.font="600 9px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText(nm.length>30?nm.slice(0,29)+"…":nm, CW/2, 396);
|
g.fillStyle="#8f9aa6"; g.font="600 8.5px 'Segoe UI',Roboto,Arial,sans-serif"; g.fillText(nm.length>32?nm.slice(0,31)+"…":nm, CW/2, 432);
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw(){
|
function draw(){
|
||||||
|
|
@ -235,37 +209,29 @@ function draw(){
|
||||||
if(m.vqPtr>512){ m.vq=m.vq.slice(m.vqPtr); m.vqPtr=0; }
|
if(m.vqPtr>512){ m.vq=m.vq.slice(m.vqPtr); m.vqPtr=0; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// pendulum target angle (eases to extreme each beat, like a real metronome)
|
|
||||||
let tgt=0;
|
let tgt=0;
|
||||||
if(state.running && beatCount>=0){ let frac=(now-lastBeatTime)/(60/state.bpm); if(frac<0)frac=0; if(frac>1.2)frac=1.2;
|
if(state.running && beatCount>=0){ let fr=(now-lastBeatTime)/(60/state.bpm); if(fr<0)fr=0; if(fr>1.2)fr=1.2;
|
||||||
tgt = MAXANG*Math.cos(Math.PI*(beatCount+frac)); }
|
tgt = MAXANG*Math.cos(Math.PI*(beatCount+fr)); }
|
||||||
pend += (tgt-pend)*(state.running?1:0.12);
|
pend += (tgt-pend)*(state.running?1:0.12);
|
||||||
flash = Math.max(0, flash-0.08);
|
flash = Math.max(0, flash-0.08);
|
||||||
|
drawBody(); drawPendulum(); drawReadout();
|
||||||
drawBody(); const piv=drawWindow(); drawLanes(); drawPendulum(piv);
|
|
||||||
requestAnimationFrame(draw);
|
requestAnimationFrame(draw);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================= CONTROLS ========================================== */
|
/* ========================= CONTROLS ========================================== */
|
||||||
cv.addEventListener("click", ()=>toggle());
|
// drag anywhere on the piece to set tempo via the weight (vertical axis); scroll too.
|
||||||
cv.addEventListener("wheel", (e)=>{ e.preventDefault(); setBpm(state.bpm+(e.deltaY<0?(e.shiftKey?5:1):(e.shiftKey?-5:-1))); syncBtns(); }, {passive:false});
|
let dragging=false;
|
||||||
$("play").onclick = (e)=>{ e.stopPropagation(); toggle(); };
|
function tempoFromY(clientY){ const r=cv.getBoundingClientRect(); const yy=(clientY-r.top)/r.height*CH;
|
||||||
$("slower").onclick = ()=>{ setBpm(state.bpm-1); syncBtns(); };
|
const f=(PIVY-yy)/ROD; setBpm(fracToBpm(f)); syncBtns(); }
|
||||||
$("faster").onclick = ()=>{ setBpm(state.bpm+1); syncBtns(); };
|
cv.addEventListener("pointerdown",(e)=>{ dragging=true; try{cv.setPointerCapture(e.pointerId);}catch(_){ } tempoFromY(e.clientY); });
|
||||||
|
cv.addEventListener("pointermove",(e)=>{ if(dragging) tempoFromY(e.clientY); });
|
||||||
|
cv.addEventListener("pointerup",()=>{ dragging=false; });
|
||||||
|
cv.addEventListener("pointercancel",()=>{ dragging=false; });
|
||||||
|
cv.addEventListener("wheel",(e)=>{ e.preventDefault(); setBpm(state.bpm+(e.deltaY<0?(e.shiftKey?5:1):(e.shiftKey?-5:-1))); syncBtns(); }, {passive:false});
|
||||||
|
$("play").onclick = ()=>toggle();
|
||||||
$("prev").onclick = ()=>loadTrack(trackIdx-1);
|
$("prev").onclick = ()=>loadTrack(trackIdx-1);
|
||||||
$("next").onclick = ()=>loadTrack(trackIdx+1);
|
$("next").onclick = ()=>loadTrack(trackIdx+1);
|
||||||
|
|
||||||
/* theme toggle */
|
|
||||||
const THEMES=["system","light","dark"];
|
|
||||||
function effTheme(p){ return p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches?"light":"dark") : p; }
|
|
||||||
function themePref(){ try{ const p=localStorage.getItem("metronome.theme"); return (p==="light"||p==="dark"||p==="system")?p:"system"; }catch(e){ return "system"; } }
|
|
||||||
function applyTheme(p){ try{ localStorage.setItem("metronome.theme",p); }catch(e){}
|
|
||||||
document.documentElement.dataset.theme = effTheme(p);
|
|
||||||
$("themeBtn").textContent = p==="system" ? "◐" : p==="light" ? "☀" : "☾"; $("themeBtn").title="Theme: "+p; }
|
|
||||||
$("themeBtn").onclick = ()=> applyTheme(THEMES[(THEMES.indexOf(themePref())+1)%THEMES.length]);
|
|
||||||
matchMedia("(prefers-color-scheme: light)").addEventListener("change", ()=>{ if(themePref()==="system") applyTheme("system"); });
|
|
||||||
applyTheme(themePref());
|
|
||||||
|
|
||||||
addEventListener("keydown",(e)=>{
|
addEventListener("keydown",(e)=>{
|
||||||
const tag=e.target?e.target.tagName:""; if(tag==="INPUT"||tag==="TEXTAREA"||tag==="SELECT") return;
|
const tag=e.target?e.target.tagName:""; if(tag==="INPUT"||tag==="TEXTAREA"||tag==="SELECT") return;
|
||||||
if(e.key===" "){ e.preventDefault(); toggle(); }
|
if(e.key===" "){ e.preventDefault(); toggle(); }
|
||||||
|
|
@ -279,6 +245,7 @@ addEventListener("keydown",(e)=>{
|
||||||
{ const ht=tracksFromHash(); if(ht) tracks=ht; }
|
{ const ht=tracksFromHash(); if(ht) tracks=ht; }
|
||||||
loadTrack(0); syncBtns();
|
loadTrack(0); syncBtns();
|
||||||
requestAnimationFrame(draw);
|
requestAnimationFrame(draw);
|
||||||
|
/*@BUILD:include:src/chrome.js@*/
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
18
src/base.css
18
src/base.css
|
|
@ -45,3 +45,21 @@ body {
|
||||||
.bom .part { color:var(--txt,#c7d0db); }
|
.bom .part { color:var(--txt,#c7d0db); }
|
||||||
.bom .part .spec { color:var(--muted,#7f8b9a); font-weight:400; }
|
.bom .part .spec { color:var(--muted,#7f8b9a); font-weight:400; }
|
||||||
.bom tr.total td { font-weight:700; color:var(--txt,#c7d0db); border-top:2px solid var(--panel-bd,#2a313c); border-bottom:none; padding-top:8px; }
|
.bom tr.total td { font-weight:700; color:var(--txt,#c7d0db); border-top:2px solid var(--panel-bd,#2a313c); border-bottom:none; padding-top:8px; }
|
||||||
|
|
||||||
|
/* ---- shared site footer ---- */
|
||||||
|
.site-foot { width:100%; max-width:980px; margin:40px auto 0; font-size:12px; color:var(--muted,#7f8b9a);
|
||||||
|
text-align:center; display:flex; align-items:center; justify-content:center; gap:8px; flex-wrap:wrap; }
|
||||||
|
.site-foot a { color:var(--muted,#7f8b9a); }
|
||||||
|
.site-foot a:hover { color:var(--txt,#c7d0db); }
|
||||||
|
.site-foot .dot { opacity:.5; }
|
||||||
|
|
||||||
|
/* ---- expandable "Spec & BOM" disclosure (merged onto each form-factor page) ---- */
|
||||||
|
details.spec { width:100%; max-width:760px; margin:18px auto 0; border:1px solid var(--panel-bd,#2a313c);
|
||||||
|
border-radius:12px; background:var(--panel-bg,#161b22); }
|
||||||
|
details.spec > summary { cursor:pointer; padding:13px 16px; font-weight:600; font-size:14px; color:var(--txt,#c7d0db);
|
||||||
|
list-style:none; display:flex; align-items:center; gap:9px; }
|
||||||
|
details.spec > summary::-webkit-details-marker { display:none; }
|
||||||
|
details.spec > summary::before { content:"▸"; color:var(--muted,#7f8b9a); transition:transform .15s; }
|
||||||
|
details.spec[open] > summary::before { transform:rotate(90deg); }
|
||||||
|
details.spec .spec-body { padding:2px 16px 16px; }
|
||||||
|
[data-embed] details.spec { display:none !important; }
|
||||||
|
|
|
||||||
19
src/chrome.js
Normal file
19
src/chrome.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* Shared site chrome — assembled into every page by build.sh.
|
||||||
|
Wires the header theme toggle (system - light - dark, shared "metronome.theme")
|
||||||
|
and stamps the footer version. Expects a global APP_VERSION declared earlier in
|
||||||
|
the page (deploy.sh rewrites that line) and the header/footer markup present. */
|
||||||
|
(function () {
|
||||||
|
var byId = function (id) { return document.getElementById(id); };
|
||||||
|
try { var v = byId("appVersion"); if (v && typeof APP_VERSION !== "undefined") v.textContent = "v" + APP_VERSION.replace(/^v/, ""); } catch (e) {}
|
||||||
|
var THEMES = ["system", "light", "dark"];
|
||||||
|
function eff(p) { return p === "system" ? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p; }
|
||||||
|
function pref() { try { var p = localStorage.getItem("metronome.theme"); return (p === "light" || p === "dark" || p === "system") ? p : "system"; } catch (e) { return "system"; } }
|
||||||
|
function apply(p) {
|
||||||
|
try { localStorage.setItem("metronome.theme", p); } catch (e) {}
|
||||||
|
document.documentElement.dataset.theme = eff(p);
|
||||||
|
var b = byId("themeBtn"); if (b) { b.textContent = p === "system" ? "◐" : p === "light" ? "☀" : "☾"; b.title = "Theme: " + p + " (system → light → dark)"; }
|
||||||
|
}
|
||||||
|
var btn = byId("themeBtn"); if (btn) btn.onclick = function () { apply(THEMES[(THEMES.indexOf(pref()) + 1) % THEMES.length]); };
|
||||||
|
try { matchMedia("(prefers-color-scheme: light)").addEventListener("change", function () { if (pref() === "system") apply("system"); }); } catch (e) {}
|
||||||
|
apply(pref());
|
||||||
|
})();
|
||||||
8
src/footer.html
Normal file
8
src/footer.html
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<!-- Shared site footer — assembled into every page by build.sh. -->
|
||||||
|
<footer class="site-foot">
|
||||||
|
<span>VARASYS · Simplifying Complexity</span>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<a href="https://codeberg.org/VARASYS/metronome" target="_blank" rel="noopener">source</a>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span id="appVersion">v0.0.1-dev</span>
|
||||||
|
</footer>
|
||||||
14
src/header.html
Normal file
14
src/header.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!-- Shared site header — assembled into every page by build.sh.
|
||||||
|
Brand goes to Concepts (the landing); nav: Concepts / Editor / Embed / Theme. -->
|
||||||
|
<header class="site-head">
|
||||||
|
<a class="brand" href="/" title="VARASYS PolyMeter — Concepts (home)">
|
||||||
|
<img class="brand-logo brand-dark" src="data:image/png;base64,@BUILD:logo-dark@" alt="VARASYS — Simplifying Complexity" />
|
||||||
|
<img class="brand-logo brand-light" src="data:image/png;base64,@BUILD:logo-light@" alt="VARASYS — Simplifying Complexity" />
|
||||||
|
</a>
|
||||||
|
<nav class="site-nav">
|
||||||
|
<a href="/">Concepts</a>
|
||||||
|
<a href="/editor.html">Editor</a>
|
||||||
|
<a href="/embed.html">Embed</a>
|
||||||
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Editor</a>
|
<a href="/editor.html">Editor</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<a href="/info-stage.html">Info</a>
|
<a href="/info-stage.html">Info</a>
|
||||||
<a href="/embed.html">Embed</a>
|
<a href="/embed.html">Embed</a>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@
|
||||||
</div>
|
</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="/editor.html">Editor</a>
|
<a href="/editor.html">Editor</a>
|
||||||
<a href="/concepts.html">Concepts</a>
|
<a href="/">Concepts</a>
|
||||||
<a href="/info-teacher.html">Info</a>
|
<a href="/info-teacher.html">Info</a>
|
||||||
<a href="/embed.html">Embed</a>
|
<a href="/embed.html">Embed</a>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue