diff --git a/BUILD_GUIDE.md b/BUILD_GUIDE.md index de619ea5..e2c4a4fe 100644 --- a/BUILD_GUIDE.md +++ b/BUILD_GUIDE.md @@ -4,6 +4,8 @@ This guide walks you through building a Patternflow on the v3 board from scratch. v3.9 is the current revision; a v3.0 board follows the same steps (its USB-C footprint stays unpopulated, see ยง2). No prior soldering experience needed โ€” every joint is big, forgiving through-hole, and the board was deliberately kept that simple so a first-time solderer can finish it. +> ๐Ÿงญ **The same build, in 3D.** [patternflow.work/guide/build](https://patternflow.work/guide/build) walks through this guide step by step on a 3D Patternflow: the parts list, the soldering order with each part going into its real holes, the case, the wiring, first light. This document stays the written reference; follow whichever suits you, or both. + **Estimated build time:** about 1 hour of hands-on work (~30 min soldering + ~30 min assembly), plus ~10 hours of 3D printing. Parts shipping typically takes ~2 weeks โ€” order first, build later. **What's new in v3** (vs. v2.x): diff --git a/CHANGELOG.md b/CHANGELOG.md index 205bd59e..0a051000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ All notable changes to Patternflow will be documented in this file, newest first ### Web -- **A guide you can play along with, in three parts, at [/guide](https://patternflow.work/guide)** (work in progress, marked WIP). `/guide` is where you pick one by where your Patternflow is, each with its chapters, in order. **Build**, at [/guide/build](https://patternflow.work/guide/build), is for soldering one from bare parts: 01 Gather (the parts list is `bom_v3.9.csv` itself), 02 Order & print, 03 Solder, 04 Into the case, 05 Wire & power, 06 Firmware (which hands over to Play's 01 Flash) and 07 Check & close, each step played on the real v3.9 board and case in 3D, in the order the build happens; the board's known issues stay in `BUILD_GUIDE.md` ยง10, linked. **Play**, at [/guide/play](https://patternflow.work/guide/play), is the first hour with a built one, 01 Flash, 02 Knobs, 03 Patterns, 04 Console, on the real v3.9 hardware in 3D, running a port of the firmware's input logic and screens, with the real flasher dialogs and a live copy of the device console. **Make**, at [/guide/make](https://patternflow.work/guide/make), is 01 Community and 02 Pattern Lab, done by hand: on a big enough screen your own Pattern Lab, a practice community (placeholder patterns, connected to nothing) and a practice AI with set answers sit beside the steps, and a pointer shows each move and waits for yours; phones get the real screens. Sound, MIDI & OSC, MQTT, Clock, Performance and Editions join Make as they are written. Each guide numbers its own chapters from 01. Links into the first version, when Play was `/guide` itself (`/guide#flash`, `/guide#knobs-3`), land on the same step under `/guide/play`. English and Korean. Every step has a "Stuck here?" link to a GitHub issue that already says which guide, chapter and step (`guide_stuck.yml`). +- **A guide you can play along with, in three parts, at [/guide](https://patternflow.work/guide)** (work in progress, marked WIP). `/guide` is where you pick one by where your Patternflow is, each with its chapters, in order. **Build**, at [/guide/build](https://patternflow.work/guide/build), is for soldering one from bare parts: 01 Gather (the parts list is `bom_v3.9.csv` itself), 02 Order & print, 03 Solder, 04 Into the case, 05 Wire & power, 06 Firmware (which hands over to Play's 01 Flash) and 07 Check & close, each step played on the real v3.9 board and case in 3D, in the order the build happens; the board's known issues stay in `BUILD_GUIDE.md` ยง10, linked. **Play**, at [/guide/play](https://patternflow.work/guide/play), is the first hour with a built one, 01 Flash, 02 Knobs, 03 Patterns, 04 Console, on the real v3.9 hardware in 3D, running a port of the firmware's input logic and screens, with the real flasher dialogs and a live copy of the device console. **Make**, at [/guide/make](https://patternflow.work/guide/make), is 01 Community and 02 Pattern Lab, done by hand: on a big enough screen your own Pattern Lab, a practice community (placeholder patterns, connected to nothing) and a practice AI with set answers sit beside the steps, and a pointer shows each move and waits for yours; phones get the real screens. Sound, MIDI & OSC, MQTT, Clock, Performance and Editions join Make as they are written. Each guide numbers its own chapters from 01. Links into the first version, when Play was `/guide` itself (`/guide#flash`, `/guide#knobs-3`), land on the same step under `/guide/play`. English and Korean. Every step has a "Stuck here?" link to a GitHub issue that already says which guide, chapter and step (`guide_stuck.yml`). The 3D stage is one continuous world: moving between the hub, Build and Play is a scene rather than a page load, and pointing at a guide on the hub previews it on the device (Build comes apart, Play turns a knob, Make redraws the panel). The LED panel lights the room with the pattern it is playing, and the page's accent follows that colour. Pretendard is now served in four script-range files, so an English page no longer downloads the Hangul glyphs (a Korean page renders identically). README, BUILD_GUIDE, PATTERN_GUIDE and the assembly map link to the guide. - **The community's Performances note says .pfs**, which is what the Director saves, instead of "Save-JSON". - **A moderator can take a pattern or deck off the wall without deleting it.** Somebody else's public pattern or deck now offers moderators *Make private*, with an optional reason: the softer removal, for things like the same pattern posted a fifth time. Nothing about the work changes and its author keeps it โ€” they can still open it, edit it, take it into the lab โ€” it just stops being shown to anyone else. On a pattern the reason is posted underneath as the moderator's comment, where the author can answer it; either way the author is told in their alerts. While the take-down stands the author cannot make it public again (a take-down its subject can undo in one click is a request); *Restore to public* puts it back exactly as it was. A moderator still cannot publish what an author made private themselves. A deck slot left empty by a take-down reads "made private by a moderator", not "by its author". Moderators can now comment on private patterns and keep their alerts about them, so the thread under a take-down works both ways. Migration `0025` adds `hidden_at` to patterns and decks; `check:hidemod` drives it all through the real routes. - **One press of Publish is one post.** A double-click on *Publish to the wall*, or a mouse switch that bounces, could put the same pattern on the wall twice in the same second ([#455](https://github.com/engmung/Patternflow/issues/455)). The button's disabled state was React state, a render too late for the second click, and after a successful publish it came back enabled while the page changed. The publish, thread, reply, deck, header, performance and report forms now refuse a second press in the same tick (`useSubmitLatch`), and a published pattern keeps its button down until the page changes; a thread whose files failed to attach offers *Open the thread* instead of posting it again. Behind them the server answers an identical submission from the same account within a minute with the post it already made, looked up and inserted in one synchronous transaction so two requests arriving together cannot both get through. `check:publish` fires two identical publishes into the route at the same instant; a version that awaits between the lookup and the insert fails it. diff --git a/PATTERN_GUIDE.md b/PATTERN_GUIDE.md index 5f85f14b..ca4cf717 100644 --- a/PATTERN_GUIDE.md +++ b/PATTERN_GUIDE.md @@ -7,6 +7,10 @@ Lab, verify them on real hardware, and share them back.** Assembly and the first flash are [BUILD_GUIDE.md](BUILD_GUIDE.md). This guide is what comes after. +Rather do it than read it? [patternflow.work/guide/make](https://patternflow.work/guide/make) is the +same loop as a hands-on tutorial: a practice community, your real Pattern Lab +in a window, and a practice AI to take the two prompts. + - [0. Before you start](#0-before-you-start) - [1. One concept: a pattern is a small file](#1-one-concept-a-pattern-is-a-small-file) - [2. A look around the community](#2-a-look-around-the-community) diff --git a/README.md b/README.md index ad61ecaa..638929be 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ What a knob does to a running pattern is the pattern's own decision, so the same The device functions themselves are fixed. Encoder 1 is brightness. Encoder 4 opens the pattern list, where you turn to browse the names and long-press again to load one, choosing from whatever is installed at the time. Encoder 2 puts the board's IP address on the panel, and typing that into a browser opens the web console, which carries every feature the device has and works from a phone. -[patternflow.work/guide/play](https://patternflow.work/guide/play) has all of this on a Patternflow you can turn, press and hold, from flashing the ESP32 to the console. +[patternflow.work/guide/play](https://patternflow.work/guide/play) has all of this on a Patternflow you can turn, press and hold, from flashing the ESP32 to the console. The same guide builds one with you ([Build](https://patternflow.work/guide/build)) and takes you through the community and Pattern Lab ([Make](https://patternflow.work/guide/make)); [patternflow.work/guide](https://patternflow.work/guide) is where to start. ## On the device @@ -187,6 +187,7 @@ Patternflow is built around a standalone ESP32-S3 driving a HUB75 RGB LED matrix | `integrations/` | Host-software bridges: Ableton Live / Max for Live (OSC knob mapping) | | `.github/` | Issue and PR templates, and the CI that runs on every pull request (web, every firmware edition, doc links, console pages) | +**Guide (interactive):** [Start here](https://patternflow.work/guide) ยท [Build it](https://patternflow.work/guide/build) ยท [Play it](https://patternflow.work/guide/play) ยท [Make patterns](https://patternflow.work/guide/make): each step shown on a 3D Patternflow you can turn, in English and Korean **Build:** [Full Build Guide](BUILD_GUIDE.md) ยท [Assembly Map](docs/assembly/README.md) ยท [Panel Compatibility](docs/panel-compatibility.md) ยท [Hardware files](hardware/README.md) **Play:** [Pattern Guide](PATTERN_GUIDE.md) ยท [Custom Patterns](firmware/CUSTOM_PATTERNS.md) ยท [Audio Guide](AUDIO_GUIDE.md) ยท [MIDI in Ableton](docs/midi-ableton.md) **Extend:** [Feature Guide](FEATURE_GUIDE.md) ยท [Editions](docs/EDITIONS.md) ยท [Firmware build](firmware/README.md) ยท [Web architecture](web/ARCHITECTURE.md) ยท [HTTP API](docs/rest-api.md) ยท [OSC Spec](docs/osc-spec.md) ยท [MIDI Spec](docs/midi-spec.md) ยท [Director โ†’ MIDI](docs/director-midi.md) diff --git a/docs/assembly/README.md b/docs/assembly/README.md index 5f1e0b6c..776e7cc0 100644 --- a/docs/assembly/README.md +++ b/docs/assembly/README.md @@ -18,7 +18,7 @@ Build those two, flash the firmware, and your Patternflow is alive. | --- | --- | --- | --- | | [3D printed enclosure](../../hardware/case/README.md) | [Custom PCB, hand-soldered](../../hardware/pcb/README.md) | [Browser flash](../../BUILD_GUIDE.md#8-firmware) ยท [your own patterns](../../PATTERN_GUIDE.md) | **Current โ€” fully documented** | -This is the route [BUILD_GUIDE.md](../../BUILD_GUIDE.md) walks start to finish: PLA parts on any 256 mm-bed FDM printer, the hand-soldered v3.9 board (all through-hole โ€” deliberately kept first-timer easy), and firmware flashed from the browser. Two ordering shortcuts are wired straight to it: +This is the route [BUILD_GUIDE.md](../../BUILD_GUIDE.md) walks start to finish, and the one the [interactive build guide](https://patternflow.work/guide/build) shows in 3D: PLA parts on any 256 mm-bed FDM printer, the hand-soldered v3.9 board (all through-hole โ€” deliberately kept first-timer easy), and firmware flashed from the browser. Two ordering shortcuts are wired straight to it: - **PCB** โ€” the [PCBWay shared project](https://www.pcbway.com/project/shareproject/Patternflow_An_LED_synthesizer_776d796c.html): no Gerber upload, and ordering there supports Patternflow development. - **Case** โ€” the [MakerWorld listing](https://makerworld.com/en/models/3072492-patternflow-open-source-led-synthesizer-case#profileId-3459015): tuned one-click print profiles for Bambu printers (STLs in `hardware/case/` for everyone else). diff --git a/web/public/fonts/Pretendard-Bold.hangul.woff2 b/web/public/fonts/Pretendard-Bold.hangul.woff2 new file mode 100644 index 00000000..84626af4 Binary files /dev/null and b/web/public/fonts/Pretendard-Bold.hangul.woff2 differ diff --git a/web/public/fonts/Pretendard-Bold.latin.woff2 b/web/public/fonts/Pretendard-Bold.latin.woff2 new file mode 100644 index 00000000..d045d82f Binary files /dev/null and b/web/public/fonts/Pretendard-Bold.latin.woff2 differ diff --git a/web/public/fonts/Pretendard-Bold.other.woff2 b/web/public/fonts/Pretendard-Bold.other.woff2 new file mode 100644 index 00000000..ed5e315a Binary files /dev/null and b/web/public/fonts/Pretendard-Bold.other.woff2 differ diff --git a/web/public/fonts/Pretendard-Bold.symbols.woff2 b/web/public/fonts/Pretendard-Bold.symbols.woff2 new file mode 100644 index 00000000..03b653fe Binary files /dev/null and b/web/public/fonts/Pretendard-Bold.symbols.woff2 differ diff --git a/web/public/fonts/Pretendard-Regular.hangul.woff2 b/web/public/fonts/Pretendard-Regular.hangul.woff2 new file mode 100644 index 00000000..cb744201 Binary files /dev/null and b/web/public/fonts/Pretendard-Regular.hangul.woff2 differ diff --git a/web/public/fonts/Pretendard-Regular.latin.woff2 b/web/public/fonts/Pretendard-Regular.latin.woff2 new file mode 100644 index 00000000..c70d4dd3 Binary files /dev/null and b/web/public/fonts/Pretendard-Regular.latin.woff2 differ diff --git a/web/public/fonts/Pretendard-Regular.other.woff2 b/web/public/fonts/Pretendard-Regular.other.woff2 new file mode 100644 index 00000000..63e55ef9 Binary files /dev/null and b/web/public/fonts/Pretendard-Regular.other.woff2 differ diff --git a/web/public/fonts/Pretendard-Regular.symbols.woff2 b/web/public/fonts/Pretendard-Regular.symbols.woff2 new file mode 100644 index 00000000..6123d287 Binary files /dev/null and b/web/public/fonts/Pretendard-Regular.symbols.woff2 differ diff --git a/web/public/guide/look/pcb-v39-copper.webp b/web/public/guide/look/pcb-v39-copper.webp new file mode 100644 index 00000000..fd5f2c4a Binary files /dev/null and b/web/public/guide/look/pcb-v39-copper.webp differ diff --git a/web/src/app/globals.css b/web/src/app/globals.css index f5e24c49..a1360c8c 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -1,18 +1,76 @@ @import "tailwindcss"; + /* Pretendard, each weight in four parts. The whole file is 770โ€“790 KB, most + of it Hangul, and a page fetched all of it for one glyph Inter lacks โ€” + an English page for the arrow in a link. The parts are the same glyphs, + outlines and metrics, cut by script (pyftsubset on the files beside + them); their ranges cover all of Unicode between them, so whatever the + whole file drew, one part still draws. A page now fetches only the parts + it has characters for: an arrow costs 38 KB, and the Hangul comes with + the first Korean word. */ @font-face { font-family: 'Pretendard'; font-weight: 400; font-style: normal; font-display: swap; - src: url('/fonts/Pretendard-Regular.woff2') format('woff2'); + src: url('/fonts/Pretendard-Regular.latin.woff2') format('woff2'); + unicode-range: U+0000-052F, U+1E00-1EFF; + } + @font-face { + font-family: 'Pretendard'; + font-weight: 400; + font-style: normal; + font-display: swap; + src: url('/fonts/Pretendard-Regular.hangul.woff2') format('woff2'); + unicode-range: U+1100-11FF, U+3130-318F, U+A960-A97F, U+AC00-D7FF; + } + @font-face { + font-family: 'Pretendard'; + font-weight: 400; + font-style: normal; + font-display: swap; + src: url('/fonts/Pretendard-Regular.symbols.woff2') format('woff2'); + unicode-range: U+2000-2BFF; + } + @font-face { + font-family: 'Pretendard'; + font-weight: 400; + font-style: normal; + font-display: swap; + src: url('/fonts/Pretendard-Regular.other.woff2') format('woff2'); + unicode-range: U+0530-10FF, U+1200-1DFF, U+1F00-1FFF, U+2C00-312F, U+3190-A95F, U+A980-ABFF, U+D800-10FFFF; + } + @font-face { + font-family: 'Pretendard'; + font-weight: 700; + font-style: normal; + font-display: swap; + src: url('/fonts/Pretendard-Bold.latin.woff2') format('woff2'); + unicode-range: U+0000-052F, U+1E00-1EFF; + } + @font-face { + font-family: 'Pretendard'; + font-weight: 700; + font-style: normal; + font-display: swap; + src: url('/fonts/Pretendard-Bold.hangul.woff2') format('woff2'); + unicode-range: U+1100-11FF, U+3130-318F, U+A960-A97F, U+AC00-D7FF; + } + @font-face { + font-family: 'Pretendard'; + font-weight: 700; + font-style: normal; + font-display: swap; + src: url('/fonts/Pretendard-Bold.symbols.woff2') format('woff2'); + unicode-range: U+2000-2BFF; } @font-face { font-family: 'Pretendard'; font-weight: 700; font-style: normal; font-display: swap; - src: url('/fonts/Pretendard-Bold.woff2') format('woff2'); + src: url('/fonts/Pretendard-Bold.other.woff2') format('woff2'); + unicode-range: U+0530-10FF, U+1200-1DFF, U+1F00-1FFF, U+2C00-312F, U+3190-A95F, U+A980-ABFF, U+D800-10FFFF; } :root { diff --git a/web/src/app/guide/layout.tsx b/web/src/app/guide/layout.tsx new file mode 100644 index 00000000..3a48a119 --- /dev/null +++ b/web/src/app/guide/layout.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from "react"; +import GuideWorld from "@/components/guide/world/GuideWorld"; + +// Everything under /guide โ€” the hub, Build, Play and Make, in both languages +// โ€” is one world: this layout mounts the guide's stage once (GuideWorld), and +// it stays up while the reader moves between the pages. A layout is not +// rendered again on a navigation inside it, so the 3D canvas, the camera, the +// simulated board and the loaded models carry over; each page only says +// which one it is (components/guide/world/usePage.ts). + +export default function GuideLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/web/src/components/guide/CommunityShots.module.css b/web/src/components/guide/CommunityShots.module.css index 45494d26..396c1a2c 100644 --- a/web/src/components/guide/CommunityShots.module.css +++ b/web/src/components/guide/CommunityShots.module.css @@ -65,7 +65,7 @@ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.55), - 0 0 18px rgba(255, 106, 61, 0.45); + 0 0 18px rgb(var(--g-led-rgb, 255 106 61) / 0.45); pointer-events: none; transition: left 0.45s cubic-bezier(0.2, 0.7, 0.2, 1), @@ -93,13 +93,13 @@ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.55), - 0 0 18px rgba(255, 106, 61, 0.45); + 0 0 18px rgb(var(--g-led-rgb, 255 106 61) / 0.45); } 50% { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.55), - 0 0 26px 3px rgba(255, 106, 61, 0.6); + 0 0 26px 3px rgb(var(--g-led-rgb, 255 106 61) / 0.6); } } @@ -137,7 +137,7 @@ inset: -6px -3px; } .dot[data-done="1"] { - border-color: rgba(255, 106, 61, 0.35); + border-color: rgb(var(--g-led-rgb, 255 106 61) / 0.35); color: var(--g-ink-2, rgba(239, 233, 221, 0.78)); } .dot[data-on="1"] { @@ -168,6 +168,7 @@ margin-top: 10px; } .text { + text-wrap: pretty; grid-area: 1 / 1; margin: 0; font-size: 13px; diff --git a/web/src/components/guide/ConsoleWindow.tsx b/web/src/components/guide/ConsoleWindow.tsx index 8421e54d..e4b653d3 100644 --- a/web/src/components/guide/ConsoleWindow.tsx +++ b/web/src/components/guide/ConsoleWindow.tsx @@ -634,10 +634,21 @@ export default function ConsoleWindow({ variant, page = "home", lang }: { varian return () => window.removeEventListener("keydown", key); }, [lift, close]); - const closed = useCallback(() => { - setLift(null); + const closed = useCallback(() => setLift(null), []); + // Focus goes back to the button that opened the window โ€” once the window is + // gone. Asked for while it was still mounted, its focus trap (Lifted's + // `keep`) took the focus straight back to its own Close button, the window + // then went, and the keyboard was left on , at the top of the page. + const wasLifted = useRef(false); + useEffect(() => { + if (lift) { + wasLifted.current = true; + return; + } + if (!wasLifted.current) return; + wasLifted.current = false; opener.current?.focus({ preventScroll: true }); - }, []); + }, [lift]); const host = HOST[variant]; const view = VIEW[variant]; diff --git a/web/src/components/guide/Extras.tsx b/web/src/components/guide/Extras.tsx index 34e86258..18ce919a 100644 --- a/web/src/components/guide/Extras.tsx +++ b/web/src/components/guide/Extras.tsx @@ -27,9 +27,10 @@ import type { BuildCard } from "./build/cards"; // board's own page installing a pack, the console itself โ€” working, on a // simulated board. -const EspWebInstallButton = "esp-web-install-button" as unknown as React.ElementType<{ +const EspWebInstallButton = "esp-web-install-button" as unknown as React.FC<{ children: React.ReactNode; manifest: string; + ref?: React.Ref; }>; /** Runs `fn(elapsedMs)` on an interval while the element is on screen. */ @@ -76,11 +77,26 @@ function stageAt(t: number, durations: number[]) { // captured), then the real button: it opens the same dialog, for real. function FlashBlock({ lang }: { lang: GuideLang }) { const ui = COPY[lang].ui; + // The install button is a custom element with a shadow root, and the site's + // session recorder keeps a handler on every shadow root made while it runs, + // for good. The root holds its host, the host its parent, and so on up: each + // visit to Play left the whole page's DOM alive behind it (some 730 + // elements a visit). So when the page has gone, the button is taken out of + // it: what stays held is the button alone. + const button = useRef(null); + useEffect(() => { + const el = button.current; + return () => { + // Only once the page is really out of the document (in development an + // effect is run, undone and run again on a page that is still there). + if (el && !el.isConnected) el.remove(); + }; + }, []); return (