Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Handpan Chords

One self-contained file: index.html. No build step, no dependencies beyond Google Fonts (loaded from CDN; falls back to system fonts offline).

index.html is the shipped product: what is in the repo is what GitHub Pages serves, byte for byte. The scale engine lives twice - as the readable modules under src/engine/ and as a copy pasted between the <!-- engine:... --> markers in index.html. Keeping the copy current is a sync step, not a build step: python3 tools/inline_engine.py rewrites those regions from src/engine/*.js, and --check reports whether they already match. Nothing is compiled, minified or generated on the way to the browser, and the file still opens straight from disk. Never hand-edit inside an engine region - change the module and re-run the tool. python3 tools/validate.py fails CI if the two ever drift.

Decks included: D Kurd 10, 49 cards (blue/brick); D Amara 10, 29 cards (green/crimson); D Amara 9, 27 cards (teal/amber); C# Hijaz 9, 19 cards (pink/orange); F3 Low Pygmy 18, 53 cards (purple/gold). 177 cards total, with the same diagrams, voicings, pitch-class highlighting, and typography as the printed sets. The visual design (Marcellus / Bitter / Nunito Sans, single-colour root-frame borders, per-deck palettes) is original to this project. The two beginner decks (Kurd 10 and Amara 10) are engine output and print from the app only; they have no committed PDF.

The scale engine that generates and ranks those cards is eleven modules under src/engine/: src/engine/core.js (note parsing and pitch classes), src/engine/voicing.js (the cluster and spelling rules), src/engine/naming.js (chord symbols and subtitles), src/engine/select.js (which chords a pan gets and in what order), src/engine/layout.js (the tonefield solver and label sizes), src/engine/share.js (URL encoding for the seed behind a generated deck) and src/engine/fontdata.js (GENERATED by tools/inline_fonts.py: the five print faces subset to printable ASCII plus the degree sign, with their advances normalised to 1000 units per em, so the browser can embed the same type the print pipeline uses). src/engine/pdf.js (the PDF writer the browser emitter draws through, so a phone can produce the print sheet itself instead of asking a print dialog for a paper size it will not honour). src/engine/pdfdeck.js (a port of decks.from_generated: the engine's deck plus the two dozen card-copy and geometry keys the print builders read; it also adapts built-in decks via fromBuiltin, so both deck kinds reach the same print builders through this one module). src/engine/pdfcards.js (a port of tools/hifi.py: the cards, the pan diagram, the crop marks and the calibration bar, held glyph for glyph against the print pipeline by tests/test_pdf_parity.py). src/engine/sequence.js (chord-sequence mode: anchor selection and the connect rule that picks a loop of 2 to 6 chords, by tier). docs/ENGINE-SPEC.md is the spec; docs/SCALE_ENGINE_PLAN.md records the decisions behind it.

Use it

  • Tap the card to flip. Swipe or use the arrow buttons/keys to move.
  • Name -> Notes: front shows the chord name; back shows the pan diagram and voicing. Notes -> Name reverses it (read the diagram, name the chord).
  • Shuffle randomizes order. Deck and mode choices persist between visits (when the browser allows storage).
  • + ADD A SCALE (in the settings panel, under Scales) opens the scale sheet: type your pan low to high on one line, the ding in round brackets and each bottom note in square brackets where its pitch falls, e.g. [C] (D) A C D E F G A C. If the pan has inner notes, put a | before them. The line under the box counts the top, inner and bottom notes it read. Pick a palette, then GENERATE CARDS. No pan is too big; a crowded one warns that its labels print small. Scales saved before this version still open. A saved scale with an even number of rim notes now draws with its sides swapped, to match every other pan; ADJUST LAYOUT, under the pan preview, opens the layout drawer, where MIRROR TOP and MIRROR BOTTOM swap them back. The drawer also holds HANDPAN ORIENTATION (#1 CENTRED, note 1 alone nearest you, or #1 / #2, notes 1 and 2 side by side), a tap-then-tap or hold-and-drag swap of two notes in one ring (a seat's number stays put, only the note moves), finer PREVIOUS/NEXT NOTE and SEAT buttons behind HARD TO TAP? SHOW FINER CONTROLS, and RESET LAYOUT, which returns notes, both mirrors and the orientation to the default. Regenerating the same scale replaces that deck in place. The sheet is a real page, not an overlay: opening it pushes #add, and reopening a saved one pushes #edit-<id>, so the back gesture closes the sheet instead of leaving the app. A page route pasted into the address bar opens nothing - it is replaced away at boot, because a restored page with no entry behind it is one back cannot leave.
  • Your scales persist in localStorage and come back on the next visit. Tapping a custom deck's chip when that deck is already selected reopens the sheet with its name and notes filled in - that is the one way in, and the pencil on the chip marks it. DELETE removes the deck and asks for a second tap to confirm. A generated deck also travels as a #s= link: the app opens one from the address bar with no account and no server, though nothing in the UI builds or copies that link yet. Saved scales live under the storage key hpfc.scales.v3; the first boot of this version copies the old hpfc.scales list there. A scale saved in a tab still running the previous version of the app lands under the old storage key and does not appear in the new app; that needs a tab held open across an update, and reloading it fixes the tab. A built-in (seeded) deck's selected chip carries the same pencil a custom deck's does. Tapping it opens the Edit sheet with SAVE CHANGES and DELETE, the same two actions a custom deck gets. SAVE writes a custom record with the record option replaces naming the seeded id; that record shows in the seed's slot of the chip strip, and the seeded data itself is never written. DELETE on a seeded deck stores its id in the hidden field of the single hpfc blob, written only while at least one seeded deck is hidden; deleting a replacement hides its seed as well, and only RESET SEEDED DECKS brings it back. The last remaining deck of any kind cannot be deleted. A changed SAVE on a seeded deck is refused with "Another deck already uses this scale." when a saved custom deck already holds that scale, and SAVE on a seeded deck with nothing changed closes the sheet and writes nothing. RESET SEEDED DECKS in the settings panel appears once any seeded deck is edited or deleted; the first tap arms it with a warning that those edits will be lost, and the armed button disarms on blur, on a tap anywhere else, and when the panel closes. The second tap clears hidden and every replacement and, if the deck on screen was removed, lands on the default deck. It clears every saved edit of a seeded deck, including an older one no longer shown in the seed's slot. Plain custom decks are kept. A SAVE in an older build of the app rebuilds the record without replaces, so the deck becomes a plain custom deck and the seed comes back beside it; nothing else is lost. Clearing site data also brings every built-in back. A scale's deck id is a 32-bit hash of its notes, so two different scales can in principle share an id; the app keeps one record per id, so on a collision one of the two is kept. It is vanishingly rare and not guarded against.
  • Print: every deck - built-in and custom alike - shows one DOWNLOAD PDF button (the chord-only sheets) plus a Letter/A4 size picker. Tapping it does not open a pre-built file; the PDF is built in the browser, on the spot, by the scale engine's PDF modules (HPE.pdfdeck adapts the deck, HPE.pdfcards draws it - a separate renderer from the one the flip cards use), so a built-in deck's cards and a custom deck's cards go through one path with one set of controls. The paper choice persists between visits, and the button disables while a PDF is being built so a second tap can't start a duplicate one. Off iOS the file downloads directly; on iOS it opens in a new tab where the platform allows it, falling back to replacing the current page where it does not.

Tests

./tests/run.sh                       # validate, boot sim, python + node
./tests/run.sh python                # python suites only
./tests/run.sh node                  # js suites only
python3 tests/suite_health.py        # per-file test-count floors
./tests/run.sh mutants               # the red-proof gate (separate; slow)

./tests/run.sh with no argument does NOT run the mutation gate or the suite health check - run those two yourself before pushing. The gate mutates the working tree in place, so nothing else may run alongside it, and it refuses to start if the tree already modifies a file one of its patches touches (README.md and data/decks.json are both such files): commit first.

Needs pip install reportlab pymupdf fonttools - fonttools rebuilds the font subsets that tools/validate.py check 5 holds src/engine/fontdata.js against. No JavaScript dependencies: the browser tests drive an already-installed Chrome over the DevTools Protocol, so there is still no package.json, no lockfile and no build step.

What is covered:

  • Deck data - the five instrument layouts are pinned against the tables in CLAUDE.md, plus the voicing rules (ding never voiced, no doubled pitch classes, power chords are a root and a fifth).
  • Print layout - text fitting against what the renderer actually draws, the 3x3 page grid, and the tonefield drawing recipe.
  • App logic - highlighting derivation, chord-spelling order, navigation and wrap, shuffle, and the localStorage guards, booted headless from index.html.
  • App vs print agreement - the two renderers are independent implementations of the same conventions (and use opposite y-axis signs), so all 177 cards are compared on position, highlight state, note order and badge.
  • The scale engine - each module under src/engine/ has its own node suite: chord selection and ranking, voicing, naming, the layout solver swept over N=5..19 pans, and share-URL round trips.
  • The README itself - tests/test_readme_currency.py reads the deck data, the engine module list and the mutant corpus and fails if the counts and names on this page have gone stale.
  • Browser e2e - real Chromium: deck switching, the 3D card flip, keyboard and touch navigation, reload persistence, clipping at a 380px viewport, and the scale sheet's modality, focus handling and 44px hit areas.
  • Mutation gate - 939 mutant patches under tests/mutants/, each one a deliberate break that some test must catch. The gate is all-or-nothing: one survivor fails it. A test nothing can kill does not count as coverage.

That is 23 node suites (tests/*.test.js) and 14 python suites (tests/test_*.py), plus tests/suite_health.py, which holds a per-file floor on the number of tests collected so a suite cannot quietly stop running.

tests/CONTRACT.md is the binding guide for adding tests, including the traps that have already cost a debugging cycle.

Host on GitHub Pages

  1. Create a repo and add index.html at the root.
  2. Repo Settings -> Pages -> Source: "Deploy from a branch" -> main / root.
  3. Your app is at https://<user>.github.io/<repo>/.

Private repo caveat: GitHub only serves Pages from private repos on paid plans (Pro/Team). On a free account the repo must be public for Pages to work. The published page itself is always public either way - Pages has no auth. If you need the app private, use Cloudflare Pages with Cloudflare Access, or Netlify with password protection, both of which deploy this same single file unchanged.

Quickest local test: tools/preview.sh (or tools/preview.sh <branch> / tools/preview.sh pr:<n> to preview before merging), then open the printed http://localhost:8000 on your laptop or the LAN URL on your phone (same Wi-Fi).

Regenerating deck data

data/decks.json is the one canonical copy of the deck data. The app's embedded const DECKS line, the browser's own PDF builder, and the print generator all derive from it. Edit that file, then run python3 tools/sync_decks.py (re-injects it into index.html) and python3 tools/decks.py (rebuilds the three committed chord-only PDFs through tools/hifi.py - the print-shop artifacts under tests/test_pdf_build.py's staleness gate, not what the app's DOWNLOAD PDF button builds).

Layout notes (intentional, verified against the physical instruments - do not "correct"):

  • F3 Low Pygmy top shell: field 1 (G3) sits bottom-RIGHT, zig-zag runs the same way as the other decks (odd fields right, even left) with bottom centre between fields 1 and 2, Eb5 at top centre, F5/G5 inside beside the ding (10 left, 11 right).
  • F3 Low Pygmy bottom notes (U1-U6) are drawn as an outer ring in x-ray view (seen from above): U1 C3 lower-right, U2 Db3 lower-left, U3 Eb3 right, U4 Bb3 left, U5 Db4 upper-right, U6 Ab5 upper-left.
  • Generated layouts put the ding in the centre unless the scale has inner notes.
  • Highlighting is pitch-class complete (every instance of a chord's pitch classes lights up, ding included), matching the verified convention of the original D Amara reference deck.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages