From d38c253c93d5b6fa1cbd2ec9727467c9386a3697 Mon Sep 17 00:00:00 2001 From: "Yifeng \"Evan\" Wang" <7312949+doodlewind@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:24:48 +0800 Subject: [PATCH 1/4] feat(site): rotate a machine matrix in the hero, retire the homepage 3D stage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hero keeps the baked demo wall but replaces the 3D PSP with a machine matrix: seven targets (PSP, PS Vita, Nokia E7, iPhone 2G, PocketBook Verse, ESP32-P4, macOS) with their silicon — CPU, RAM, display, year — and one fixed roster of seven flagship apps re-lit per machine, so partial support is visible instead of implied. The matrix auto-rotates every 6 s, pauses on hover/focus/off-screen, and parks on any manual pick; reduced-motion never rotates. Every dot is repo-claimed: mint = verified on the machine, sky = builds today with the hardware pass pending, hollow = not yet. The device taxonomy chips catch up with the same sweep: iPhone 2G joins as live, M5Stack Tab5 becomes live (renderer hardware-verified at 720p), Playdate and PocketBook move to in-progress per their own READMEs, and iOS moves to in-progress now that #256 plays on the simulator. The 3D stage is now playground-only: the homepage no longer loads pocket-stage-web.js, while the build keeps packaging /stage/ (model, profile, launcher payloads) because the playground wraps its live framebuffer in the same model. verify-playground-stage passes against the rebuilt dist; site-stage tests re-pin the new split. Co-Authored-By: Claude Fable 5 --- site/assets/home.css | 255 ++++++++++++++++++++++++++++----------- site/assets/home.js | 97 +++++++++++---- site/home.html | 195 +++++++++++++++++++++++++----- tests/site-stage.test.ts | 58 +++++---- 4 files changed, 456 insertions(+), 149 deletions(-) diff --git a/site/assets/home.css b/site/assets/home.css index 4d76f0ce..e443a0a6 100644 --- a/site/assets/home.css +++ b/site/assets/home.css @@ -2,9 +2,10 @@ PocketJS — "Build modern apps for impossible devices" landing page Direction: Docs navy with brushed metal accents. All classes are .lp-* scoped. The hero plays the baked demo wall (site/bake-demo-wall.ts) as a full-bleed - background and mounts the demand-rendered Pocket Stage below its CTA. The - page then walks the platform story: proof strip → target selector → - .pocket → device taxonomy → showcase → closing CTA. + background and rotates the machine matrix (.lp-mx) below its CTA: one fixed + roster of flagship apps re-lit per machine. The page then walks the platform + story: proof strip → target selector → .pocket → device taxonomy → + showcase → closing CTA. ========================================================================== */ @font-face { @@ -389,94 +390,204 @@ a.lp-btn--primary:hover { margin-top: 2.1rem; } -/* ---- Live Pocket Stage ---------------------------------------------------- */ -.lp-stage { - width: min(100%, 980px); - margin: clamp(1.5rem, 3.2vw, 2.35rem) auto 0; +/* ---- Machine matrix -------------------------------------------------------- + The hero's proof object: pick a machine (or let it rotate), read its silicon, + and see the same seven flagship apps re-lit for that target. Dots reuse the + taxonomy palette: mint = verified on the machine, sky = builds today, + hollow = not yet. Panels are static HTML; home.js rotates and toggles. */ +.lp-mx { + width: min(100%, 900px); + margin: clamp(1.6rem, 3.4vw, 2.5rem) auto 0; + border-radius: 18px; + border: 1px solid var(--line-2); + background: + linear-gradient(180deg, rgba(17, 24, 42, 0.88), rgba(7, 10, 17, 0.94)); + box-shadow: 0 46px 90px -46px rgba(3, 9, 20, 0.94); + backdrop-filter: blur(10px); + overflow: hidden; + text-align: left; } -.lp-stage__viewport { +.lp-mx__chips { + display: flex; + gap: 0.35rem; + padding: 0.75rem 0.9rem; + border-bottom: 1px solid var(--line); + background: rgba(20, 29, 49, 0.5); + overflow-x: auto; + scrollbar-width: none; +} +.lp-mx__chips::-webkit-scrollbar { display: none; } +.lp-mx__chip { position: relative; - width: 100%; - height: clamp(285px, 39vw, 455px); + flex: none; + appearance: none; + border: 1px solid var(--line-2); + border-radius: 999px; + padding: 0.44rem 0.78rem; + background: rgba(5, 7, 13, 0.5); + color: var(--ink-2); + font: 600 12.5px/1 var(--font); + cursor: pointer; + white-space: nowrap; overflow: hidden; - background: transparent; + transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease; } -.lp-stage__canvas { - display: block; - width: 100%; - height: 100%; - outline: none; - cursor: grab; - touch-action: none; +.lp-mx__chip:hover { color: var(--ink); border-color: rgba(96, 165, 250, 0.4); } +.lp-mx__chip.is-active { + color: #04060b; + font-weight: 700; + border-color: rgba(226, 232, 240, 0.5); + background: var(--metal-lite); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55), inset 0 -6px 10px rgba(20, 37, 64, 0.12); } -.lp-stage__canvas:active { cursor: grabbing; } -.lp-stage__canvas:focus-visible { - box-shadow: inset 0 0 0 2px rgba(157, 229, 199, 0.88); +.lp-mx__chip:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; } +.lp-mx__chip-year { + margin-left: 0.38rem; + font: 500 10px/1 var(--mono); + opacity: 0.6; } -.lp-stage__screen { display: none; } -.lp-stage__status { +/* auto-rotate countdown: a thin sweep along the active chip's bottom edge */ +.lp-mx__chip::after { + content: ""; position: absolute; - left: 50%; - bottom: 1.15rem; - display: inline-flex; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: rgba(23, 37, 62, 0.55); + transform: scaleX(0); + transform-origin: left; +} +.lp-mx.is-auto .lp-mx__chip.is-active::after { + animation: lp-mx-sweep var(--mx-period, 6000ms) linear forwards; +} +@keyframes lp-mx-sweep { + from { transform: scaleX(0); } + to { transform: scaleX(1); } +} +.lp-mx__panel { + display: grid; + gap: 1.1rem 1.7rem; + padding: 1.2rem 1.35rem 1.25rem; +} +@media (min-width: 800px) { .lp-mx__panel { grid-template-columns: 0.92fr 1.08fr; } } +.lp-mx__panel[hidden] { display: none; } +.lp-mx__id h3 { + display: flex; + flex-wrap: wrap; align-items: center; gap: 0.55rem; - padding: 0.48rem 0.8rem; - border: 1px solid rgba(85, 103, 133, 0.56); + font-size: 1.12rem; + font-weight: 700; + color: var(--ink); +} +.lp-mx__badge { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.5rem; border-radius: 999px; + font: 600 10px/1.3 var(--mono); + letter-spacing: 0.05em; + text-transform: uppercase; +} +.lp-mx__badge--hw { + color: var(--mint); + border: 1px solid rgba(157, 229, 199, 0.34); + background: rgba(157, 229, 199, 0.08); +} +.lp-mx__badge--wip { + color: var(--sky); + border: 1px solid rgba(96, 165, 250, 0.36); + background: rgba(96, 165, 250, 0.08); +} +.lp-mx__specs { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-top: 0.7rem; +} +.lp-mx__specs span { + padding: 0.26rem 0.5rem; + border: 1px solid var(--line-2); + border-radius: 7px; + background: rgba(5, 7, 13, 0.55); + color: var(--steel-1); + font: 500 11px/1.2 var(--mono); + white-space: nowrap; +} +.lp-mx__blurb { + margin-top: 0.75rem; color: var(--ink-2); - background: rgba(6, 10, 18, 0.78); - font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.025em; - transform: translateX(-50%); - backdrop-filter: blur(8px); - transition: opacity 0.25s ease, transform 0.25s ease; -} -.lp-stage__status-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--mint); - box-shadow: 0 0 10px rgba(157, 229, 199, 0.82); - animation: lp-stage-pulse 1.1s ease-in-out infinite alternate; + font-size: 0.88rem; + line-height: 1.6; } -.lp-stage.is-ready .lp-stage__status { - opacity: 0; - transform: translate(-50%, 5px); - pointer-events: none; +.lp-mx__link { + display: inline-block; + margin-top: 0.8rem; + font-size: 0.85rem; + font-weight: 600; + color: var(--ink-2); + text-decoration: none; + border-bottom: 1px solid var(--line-2); + padding-bottom: 0.1rem; + transition: color 0.2s ease, border-color 0.2s ease; } -.lp-stage.has-error .lp-stage__status { - color: #f0b8b8; - border-color: rgba(239, 143, 143, 0.4); +.lp-mx__link:hover { color: var(--ink); border-color: var(--muted); } +.lp-mx__apps { + list-style: none; + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.44rem; + margin: 0; + padding: 0; } -.lp-stage.has-error .lp-stage__status-dot { - background: #ef8f8f; - box-shadow: none; - animation: none; +.lp-mx__apps li { + display: grid; + grid-template-columns: 13px 138px 1fr; + align-items: baseline; + gap: 0.6rem; + font-size: 0.84rem; + line-height: 1.45; +} +.lp-mx__apps li strong { color: var(--ink); font-weight: 600; white-space: nowrap; } +.lp-mx__apps li span { color: var(--muted); font-size: 0.8rem; } +.lp-mx__apps li b, +.lp-mx__legend b { + width: 9px; + height: 9px; + align-self: start; + margin-top: 0.32em; + border-radius: 50%; + flex: none; } -.lp-stage__caption { +.lp-mx__legend b { align-self: center; margin-top: 0; } +.lp-mx__apps li.is-hw b, +.lp-mx__legend b.is-hw { background: var(--mint); box-shadow: 0 0 8px rgba(157, 229, 199, 0.55); } +.lp-mx__apps li.is-built b, +.lp-mx__legend b.is-built { background: var(--sky); box-shadow: 0 0 8px rgba(96, 165, 250, 0.5); } +.lp-mx__apps li.is-no b, +.lp-mx__legend b.is-no { background: transparent; border: 1px solid var(--steel-3); } +.lp-mx__apps li.is-no strong { color: var(--muted); font-weight: 500; } +.lp-mx__apps li.is-no span { color: var(--steel-3); } +.lp-mx__legend { display: flex; flex-wrap: wrap; - justify-content: space-between; - gap: 0.45rem 1.25rem; - padding: 0.75rem 0.35rem 0; - color: rgba(169, 181, 201, 0.72); - font-size: 11px; - line-height: 1.5; - text-align: left; + gap: 0.45rem 1.3rem; + padding: 0.7rem 1.35rem 0.85rem; + border-top: 1px solid var(--line); + background: rgba(20, 29, 49, 0.32); } -.lp-stage__caption strong { color: rgba(226, 234, 246, 0.88); font-weight: 600; } -.lp-stage__caption a { color: rgba(190, 206, 228, 0.86); text-underline-offset: 2px; } -.lp-stage__caption a:hover { color: #fff; } -.lp-stage__credit { margin-left: auto; } -@keyframes lp-stage-pulse { - from { opacity: 0.35; transform: scale(0.78); } - to { opacity: 1; transform: scale(1); } +.lp-mx__legend span { + display: inline-flex; + align-items: center; + gap: 0.45rem; + color: var(--muted); + font: 500 11px/1.4 var(--mono); } -@media (max-width: 640px) { - .lp-stage__viewport { height: clamp(245px, 65vw, 330px); } - .lp-stage__credit { margin-left: 0; } +@media (max-width: 560px) { + .lp-mx__apps li { grid-template-columns: 13px 1fr; } + .lp-mx__apps li span { grid-column: 2; margin-top: -0.15rem; } } /* ---- Proof strip ---------------------------------------------------------- diff --git a/site/assets/home.js b/site/assets/home.js index bc605f62..cf260971 100644 --- a/site/assets/home.js +++ b/site/assets/home.js @@ -1,7 +1,8 @@ // site/assets/home.js — homepage behaviors. The background remains a cheap -// baked demo wall; the live Pocket Stage below the CTA is code-split and boots -// only when it approaches the viewport. Tab groups (framework tabs on the code -// card, target chips on the selector) are static HTML — JS only toggles state. +// baked demo wall; the machine matrix below the CTA auto-rotates through the +// machines until the visitor takes over. Tab groups (framework tabs on the +// code card, target chips on the selector, machine chips on the matrix) are +// static HTML — JS only toggles state. function setupTabs(tabAttr, panelAttr) { const tabs = [...document.querySelectorAll(`[data-${tabAttr}]`)]; @@ -63,30 +64,82 @@ function setupDemoWall() { io.observe(video); } -function setupPocketStage() { - const root = document.querySelector("[data-pocket-stage]"); +// The machine matrix: chip tabs like the other groups, plus a rotation that +// advances every MX_PERIOD ms. Rotation pauses while the matrix is hovered, +// focused, or off screen, and stops for good on any manual pick — the sweep +// on the active chip only runs while `is-auto` is set. +function setupMachineMatrix() { + const root = document.querySelector("[data-mx]"); if (!root) return; - let booted = false; - const boot = async () => { - if (booted) return; - booted = true; - try { - const { mountPocketStage } = await import("/assets/pocket-stage-web.js"); - await mountPocketStage(root); - } catch (error) { - root.classList.add("has-error"); - const status = root.querySelector("[data-stage-status]"); - if (status) status.textContent = "Pocket Stage could not be loaded."; - console.error("Pocket Stage module failed", error); + const chips = root.querySelector(".lp-mx__chips"); + const tabs = [...root.querySelectorAll("[data-mx-tab]")]; + const panels = [...root.querySelectorAll("[data-mx-panel]")]; + if (tabs.length === 0) return; + + const MX_PERIOD = 6000; + root.style.setProperty("--mx-period", `${MX_PERIOD}ms`); + const reduced = matchMedia("(prefers-reduced-motion: reduce)"); + let index = 0; + let timer = 0; + let visible = false; + let engaged = false; // hover or focus within + let manual = false; // a real pick parks the carousel + + const select = (name) => { + index = Math.max(0, tabs.findIndex((tab) => tab.dataset.mxTab === name)); + for (const tab of tabs) { + const active = tab.dataset.mxTab === name; + tab.classList.toggle("is-active", active); + tab.setAttribute("aria-selected", active ? "true" : "false"); + } + for (const panel of panels) { + panel.hidden = panel.dataset.mxPanel !== name; } + const active = tabs[index]; + if (chips && active && chips.scrollWidth > chips.clientWidth) { + chips.scrollTo({ left: Math.max(0, active.offsetLeft - 24), behavior: "smooth" }); + } + }; + + const stop = () => { + if (timer) clearInterval(timer); + timer = 0; + root.classList.remove("is-auto"); }; + const start = () => { + if (timer || manual || engaged || !visible || reduced.matches) return; + // Re-arm the sweep animation even when the active chip didn't change. + root.classList.remove("is-auto"); + void root.offsetWidth; + root.classList.add("is-auto"); + timer = setInterval(() => { + select(tabs[(index + 1) % tabs.length].dataset.mxTab); + }, MX_PERIOD); + }; + + for (const tab of tabs) { + tab.addEventListener("click", () => { + manual = true; + stop(); + select(tab.dataset.mxTab); + }); + } + root.addEventListener("mouseenter", () => { engaged = true; stop(); }); + root.addEventListener("mouseleave", () => { engaged = false; start(); }); + root.addEventListener("focusin", () => { engaged = true; stop(); }); + root.addEventListener("focusout", (event) => { + if (root.contains(event.relatedTarget)) return; + engaged = false; + start(); + }); + reduced.addEventListener?.("change", () => (reduced.matches ? stop() : start())); const io = new IntersectionObserver( (entries) => { - if (!entries.some((entry) => entry.isIntersecting)) return; - io.disconnect(); - void boot(); + for (const e of entries) visible = e.isIntersecting; + if (visible) start(); + else stop(); }, - { rootMargin: "240px 0px", threshold: 0.01 }, + { threshold: 0.25 }, ); io.observe(root); } @@ -95,4 +148,4 @@ setupTabs("code-tab", "code-panel"); setupCodeCardName(); setupTabs("tgt-tab", "tgt-panel"); setupDemoWall(); -setupPocketStage(); +setupMachineMatrix(); diff --git a/site/home.html b/site/home.html index 2dda8b30..77b55ca7 100644 --- a/site/home.html +++ b/site/home.html @@ -47,43 +47,173 @@ Motion studies by (yui540) · credited per author request
- + New - useState at 333 MHz: Octane Is PocketJS’s Third Framework + Pocket Voxel: A Creature-RPG From First Principles

Rich interactive JavaScriptwhere no browser fits.

- UI, games, 3D and AI-native apps. One compact runtime across - radically different devices: consoles, phones, e‑readers, - microcontrollers. A tiny JS guest where it fits; the framework - compiled away where it doesn’t. + UI, games, 3D and AI-native apps from one compact runtime. + A tiny JS guest where it fits; the framework compiled away where + it doesn’t. Below, every machine it ships on today — + and what each one already runs.

-
-
- - -
- - Loading Pocket Stage… + + +
+
+ + + + + + + +
+ +
+
+

Sony PSP hardware-verified

+

333 MHz MIPS32 MB RAM480×2722004

+

Where PocketJS started: no OS to lean on, an 8 MB app arena, and now the most complete machine in the registry.

+ How PocketJS started here → +
+
    +
  • Pocket Figma14,430 nodes, panned and zoomed at 60 fps
  • +
  • Pocket YouTubesearch, seek and sound over one USB cable
  • +
  • OpenStrikeCS-like FPS, locked 60 fps with bots
  • +
  • Pocket Voxelcreature-RPG diorama, whole game in the guest
  • +
  • Pocket Characternot yet
  • +
  • Pocket Pinot yet
  • +
  • Launcher + app deckCover Flow switching across 17 apps
  • +
+
+ + + + + + -
- The live Pocket Launcher runs inside the model’s real screen material. Browse the deck, launch any app, press SELECT to switch. - - PSP model by Dibad - · CC BY 4.0 - -
-
+ + + + + + + +
+ verified on the machine + builds today, hardware pass pending + not yet +
+
@@ -120,7 +250,7 @@

Games

3D experiences

-

A portable BSP engine, a painter-sorted motion pipeline, and VRM digital humans with spring-bone physics. The same scene runs on desktop wgpu or the PSP’s GE. The launcher above is one. One process, whole character →

+

A portable BSP engine, a painter-sorted motion pipeline, and VRM digital humans with spring-bone physics. The same scene runs on desktop wgpu or the PSP’s GE. One process, whole character →

AI-native apps

@@ -385,7 +515,7 @@

System software, built into the runtime

App launcher

-

A Cover Flow deck that cold-boots, freezes and switches whole apps. It runs live in the PSP at the top of this page.

+

A Cover Flow deck that cold-boots, freezes and switches whole apps, verified on the PSP, the Vita and a 2011 Nokia.

Capability contracts

@@ -430,7 +560,7 @@

Consoles & handhelds

  • Game Boy Advance
  • Game Boy
  • NES
  • -
  • Playdate
  • +
  • Playdate
  • Nintendo 3DS
  • @@ -439,6 +569,7 @@

    Abandoned smartphones

    The vendor moved on. The hardware didn’t.