mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-15 09:20:06 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd5c46ed72 | ||
|
|
beefd7bc09 | ||
|
|
a7ea719652 | ||
|
|
1e2d5a6162 |
@@ -52,7 +52,7 @@ jobs:
|
||||
run: pytest
|
||||
|
||||
- name: Run JS plugin-API tests
|
||||
run: node --test tests/js/*.test.js 'tests/plugins/*/js/*.test.js'
|
||||
run: node --test tests/js/*.test.js 'tests/plugins/*/js/*.test.js' 'plugins/*/tests/*.test.js'
|
||||
|
||||
tailwind-fresh:
|
||||
# Guard that the committed static/tailwind.min.css is in sync with source.
|
||||
|
||||
@@ -44,6 +44,12 @@ plugins/minigames/__pycache__/
|
||||
plugins/tuner/__pycache__/
|
||||
!plugins/input_setup/
|
||||
!plugins/input_setup/**
|
||||
!plugins/drum_highway_3d/
|
||||
!plugins/drum_highway_3d/**
|
||||
plugins/drum_highway_3d/__pycache__/
|
||||
!plugins/keys_highway_3d/
|
||||
!plugins/keys_highway_3d/**
|
||||
plugins/keys_highway_3d/__pycache__/
|
||||
node_modules/
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
+3
-3
@@ -7,10 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- **Player frame-time hotspots removed (trace-backed) + weak-hardware hardening.** A Chrome performance trace of a 3D-highway session surfaced two core per-frame layout-thrash sources, now fixed: the highway's visibility check read `canvas.offsetParent` every rAF frame (forces style/layout recalc — now sampled every 10th frame with a cached value, force-refreshed on init/canvas-replace/resize/override-clear), and the v3 player chrome loop called `matches(':hover')` per frame and unconditionally rewrote the Up-Next pill's `textContent`/bar width at 6 Hz (now hover-tracked via mouseenter/mouseleave, DOM writes only on value change, progress bar moved from `width` to compositor-only `scaleX`). The 3D highway pre-warms shader programs (`ren.compile`) and deterministic label textures at init — and chart-dependent chord/section label textures on first draw — so first-appearance shader-compile/texture-upload frame spikes move into the load spinner. For weaker hardware: the per-frame renderer bundle is now a single reused object instead of a fresh ~35-field allocation per frame (object identity is stable and meaningless; array fields still swap reference on chart changes), custom viz get `bundle.lowerBoundT`/`bundle.lowerBoundTime` binary-search helpers for visible-window culling, the default 2D highway's beat lines no longer scan every beat in the song per frame, and the 3D highway stops reading `localStorage` per frame (1 Hz poll) and caches its lyrics text-measurement layout per displayed line instead of re-measuring every syllable every frame. A second, throttled-CPU trace pass additionally removed: shader-program re-resolution churn from label texture swaps (`material.needsUpdate` is now only set on a null↔texture transition — swapping between two cached label textures never changes the compiled program), the 3D highway's per-frame `getBoundingClientRect` layout read in its canvas-size self-check (now every 10th frame, still immediate on backing-store change), and the core 60 Hz HUD clock rewriting `textContent` on every tick (now write-on-change, ~1/s). The dominant residual — steady `getParameters` shader-program re-resolution (~4% of throttled main thread) — turned out to be Three r158+'s transparent-DoubleSide two-pass rendering, which sets `material.needsUpdate` twice per object per frame; all 18 of the 3D highway's transparent DoubleSide materials are flat unlit quads (labels, rails, chord frames, lanes), so they now declare `forceSinglePass: true`, eliminating the recompile churn and halving those objects' draw calls.
|
||||
|
||||
### Added
|
||||
- **3D Drum Highway: bloom glow + adaptive-resolution support — the first slice of visual parity with the guitar highway.** The drum highway now renders through the same post-processing path as `highway_3d`: an `UnrealBloomPass` (strength 0.65, radius 0.5, threshold 0.82 — high, so only emissive/bright surfaces bleed) on a multisampled HalfFloat target with ACES filmic tone mapping, so the white hit-line bar and proximity-lit notes get a real glow instead of a flat emissive tint. **On by default**, with a new **Graphics → "Glow (bloom)"** toggle in the plugin settings (`drum_h3d_bg_bloom`, applies live, no reload); if the vendored postprocessing addons can't load (older self-hosted core), the plugin silently falls back to the direct render path. The plugin also now honors the host's **adaptive render scale** (`bundle.renderScale` — the Quality/"Min res" controls that the guitar highway already respected), multiplying it into the device pixel ratio, and caps DPR at 1.25 when more than one viz instance is live (splitscreen) so two panels don't double the GPU fill cost. Groundwork for the rest of the parity series: an FX-settings scaffold (`FX_DEFAULTS`/`readFxSettings`/`window.drumH3dSetFx`, `drum_h3d_bg_*` localStorage keys) that the sparks/themes/backgrounds PRs extend, plus a first node test suite for the plugin (`plugins/drum_highway_3d/tests/data_layer.test.js` — vm-loaded like the keys plugin's, covering the hit-variant precedence, the Auto-mode steal-guard predicate, and FX defaults; 8 tests, runs in CI via the `plugins/*/tests/*.test.js` glob).
|
||||
- **3D Keys Highway: sharp HiDPI rendering, bloom glow, a live combo HUD, and a graphics settings panel — the first slice of visual parity with the guitar highway.** The biggest single fix is resolution: the plugin never called `setPixelRatio`, so on HiDPI/retina displays (and Windows display scaling) it rendered at CSS resolution and was upscaled — soft and aliased. It now multiplies the device pixel ratio (capped at 2, or 1.25 when two viz panels are live in splitscreen) with the host's **adaptive render scale** (`bundle.renderScale`, the Quality/"Min res" controls), exactly like `highway_3d`. On top of that: the same **bloom** post-processing path as the guitar highway (UnrealBloomPass 0.65/0.5/0.82 on a multisampled HalfFloat target + ACES tone mapping — the cyan hit-line, hit flames and the sustain "consume" glow finally bleed light instead of reading flat), **on by default** with a graceful direct-render fallback when the vendored addons can't load. The plugin gains its first **settings panel** (`settings.html`, Settings → graphics category, `"settings"` block in plugin.json) with a live-applying "Glow (bloom)" toggle (`keys3d_bg_bloom`), plus the FX scaffold (`FX_DEFAULTS`/`readFxSettings`/`window.keys3dSetFx`, `keys3d_bg_*` keys) the later parity PRs extend. And the score state the plugin was already tracking is finally visible: a **combo / accuracy / best-streak HUD** overlay (drum-highway pattern), shown only while a MIDI keyboard session is wired so it never renders a frozen 0× combo. Tests: `plugins/keys_highway_3d/tests/fx_settings.test.js` (defaults, localStorage overrides + type coercion, setter persist/dispatch/unknown-key guard; 3 tests alongside the existing 20).
|
||||
- **The 3D Drum Highway and 3D Keys Highway are now bundled core plugins** (`plugins/drum_highway_3d/`, `plugins/keys_highway_3d/`), imported from their former standalone repos (`feedBack-plugin-drum-highway-3d`, `feedBack-plugin-keys-highway-3d`, now archived) via `git subtree` so their history is preserved. They join the other in-tree plugins-as-plugins: the loader treats them identically to user-installed ones, both are marked `"bundled": true` in their manifests, and `.gitignore` gains the matching `!plugins/<id>/` exceptions. This puts all three 3D highways (guitar, drums, keys) in one repo ahead of a visual-parity pass that ports the guitar highway's polish (bloom, sparks, themes, reactive backgrounds) to the other two — shared helper code and theme tables can now be reviewed and kept in sync in a single place. The keys plugin's existing node test suite is wired into CI (the JS test step gains a `plugins/*/tests/*.test.js` glob, +20 tests), and `static/tailwind.min.css` is regenerated since the core Tailwind build scans `plugins/**`. One deliberate behavior change ships with the bundling: the drum highway's Auto-mode predicate is **narrowed** (it used to claim any pack with `has_drum_tab` — a pack-level flag — which, now that the plugin ships to everyone and sorts before `highway_3d` in first-match-wins Auto order, would have stolen full-band packs from the guitar highway even on Lead/Bass arrangements; it now claims only drum arrangements, or packs nothing more specific can render). Picking the drum highway manually from the viz picker is unchanged.
|
||||
- **The tuner now tracks what tuning your instrument is *actually* in, so it prompts you to retune in BOTH directions — down to a song's tuning, and back up when the next song needs it.** The coverage check used to compare each song against your fixed instrument-profile tuning, so it only ever prompted you *away* from "home" (e.g. E → Drop C#) and stayed silent coming back (Drop C# → E), even though you'd physically retuned. It now reads the host's live **per-instrument working tuning** (`window.feedBack.workingTuning`) — what your selected instrument is currently in — so coverage is measured against your *actual* tuning and fires both ways. When you clear an auto-opened tuner, the tuner publishes that song's tuning as your instrument's live working tuning (`assumed` — an explicit "I tuned / Skip" refines it in a later PR), so the next song is judged against where you now are. **Per-instrument** — your guitar's and bass's tunings are tracked separately (keyed like the selector), so switching instruments uses the right one. Feature-detected: on a host without the working-tuning capability it falls back to the static `/api/settings` tuning (today's behavior). `plugins/tuner/screen.js` (`_playerTuning` reads `workingTuning` keyed by the selected instrument; `_publishWorkingTuning` writes on clear). Builds on the host `workingTuning` foundation (PR 1 of the series) + the instrument→chart routing (PR 2). Tests: `tests/js/tuner_auto_open.test.js` (both-directions coverage via a live Drop-D working tuning; publish-on-clear targets the right instrument slot) — 29 pass.
|
||||
- **`.jsonc` support for feedpak data files** (feedpak-spec §8, FEP #3 / PR #13). Hand-edited packs may now use the `.jsonc` extension (JSON with C-style `//` line and `/* */` block comments) for any data file the manifest points at — arrangements, notation sidecars, `drum_tab`, `song_timeline`, `lyrics`, and `keys`. New shared `lib/jsonc.py` provides `parse_jsonc(text)` + `load_json(path)` (auto-detects `.jsonc` by suffix, string-aware so comment-like text inside JSON string values is preserved) and is now used by every reader in `lib/sloppak.py` (six side-file sites) and `scripts/lift_keys_notation.py` (three arrangement / song_timeline read sites). The strip regex mirrors the reference validator in `feedpak-spec/tools/validate.py`. This is an additive (MINOR) change: `.jsonc` is opt-in, so any pack that keeps its data files as `.json` is unaffected and needs no regeneration. Note that a `.jsonc` file containing real comments only loads on a reader that implements §8 — a pre-this-change reader calls bare `json.loads` and fails on the comments rather than ignoring them, so don't hand out `.jsonc` packs to older hosts. Tests: `tests/test_sloppak_jsonc_load.py` (covers all six side-file types, the lift helper, and the string-boundary preservation rule end-to-end).
|
||||
- **The highway now loads the part that matches your selected instrument — a bass player gets the Bass arrangement, not the default Lead/guitar chart.** When you open a song without an explicit arrangement, the WebSocket handler (`server.py` `highway_ws`) reads your selected `instrument` from `config.json` (the same file it already reads for your default-arrangement preference) and routes to the matching part: **bass → the Bass arrangement**; guitar — and any unknown/future instrument (drums, keys) — falls through to the existing preference/most-notes default, which already lands on a guitar part. Previously the instrument selector only fed the tuner, so a bass player was handed a guitar chart (and a tune/coverage check then compared a 4-string bass against a 6-string part). An **explicit arrangement request always wins** (a manual arrangement switch is untouched), and a bass player's saved default-arrangement preference is still honored **within** the bass parts (so a preferred `Bass 2` / `Alt. Bass` wins over the canonical Bass), so this only changes the *default* part chosen on load. Server-only — every launch path already flows through the WS, so there's no client change. This is the instrument↔chart-routing piece the working-tuning series leans on (otherwise coverage compares across instruments). Tests: `tests/test_highway_ws_instrument_routing.py` (bass→Bass, bass-honors-pref, bass-no-bass-part→guitar, guitar→default, explicit-wins).
|
||||
|
||||
@@ -231,18 +231,6 @@ window.feedBackViz_my_viz = function () {
|
||||
// toneChanges, toneBase, mastery, hasPhraseData, inverted,
|
||||
// lefty, renderScale, lyricsVisible, the 2D coordinate
|
||||
// helpers project and fretX, and getNoteState (see below).
|
||||
// The bundle OBJECT is reused across frames (mutated in
|
||||
// place — no per-frame allocation): never cache it or
|
||||
// compare its identity between frames; field values are
|
||||
// only valid for the current draw call. Array FIELDS still
|
||||
// swap reference when chart data changes, so field-identity
|
||||
// caches (`myRef !== bundle.chords`) remain valid.
|
||||
// Windowed-iteration helpers (stable fn refs): bundle
|
||||
// .lowerBoundT(arr, time) is a lower-bound binary search on
|
||||
// `.t` (notes/chords); bundle.lowerBoundTime(arr, time) on
|
||||
// `.time` (beats/anchors/sections). Use these to cull to
|
||||
// the visible window instead of full-scanning chart arrays
|
||||
// per frame.
|
||||
// `stringCount` is the active arrangement's string count (4
|
||||
// for bass, 6 for guitar, 7+ for extended-range GP imports —
|
||||
// size string-indexed geometry against this, not a hardcoded
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -0,0 +1,82 @@
|
||||
# 3D Drum Highway — Mockup
|
||||
|
||||

|
||||
|
||||
An exploratory **pure-visual** sibling of `highway_3d`. Renders an 8-lane
|
||||
drum highway (7 lanes for hand pieces + a full-width kick bar) populated
|
||||
from a hardcoded demo pattern that loops indefinitely. Not yet wired to
|
||||
song data, hit detection, audio, or note_detect — this is here to play
|
||||
with the look-and-feel.
|
||||
|
||||
To see it, load any song in the player, then pick **3D Drum Highway**
|
||||
from the viz picker. The mockup animates regardless of what song is
|
||||
playing.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
[HH] [SNR] [TM1] [TM2] [FT] [CR] [RD] <- 7 lanes, left to right
|
||||
| | | | | | |
|
||||
v v v v v v v
|
||||
----- hit line -----------------------------
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓ KICK BAR ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ <- full-width kick lane
|
||||
```
|
||||
|
||||
- **Drums** (snare, toms, floor tom): flat disc geometry (`CylinderGeometry`),
|
||||
palette-tinted, with subtle emissive pulse on approach. Snare gets a
|
||||
thin white "wires" stripe.
|
||||
- **Cymbals** (hi-hat, crash, ride): faceted gem geometry (truncated
|
||||
`CylinderGeometry`), metallic material, slightly translucent.
|
||||
- **Kick**: full-width amber bar across the base, brighter on approach.
|
||||
|
||||
## Variants visible in the demo
|
||||
|
||||
| Variant | How it reads |
|
||||
|---|---|
|
||||
| `accent` | Bigger note + white halo ring |
|
||||
| `ghost` | Hollow ring (~65% size) instead of a solid disc |
|
||||
| `flam` | Main note + a small grace disc offset slightly before |
|
||||
| `bell` | Ride only — adds a bright dot in the center of the gem |
|
||||
|
||||
The **Fill showcase** demo pattern fires every variant at least once in
|
||||
a few bars, plus a tom roll down the kit. Best read of what's possible.
|
||||
|
||||
## Settings (Settings → Plugins → 3D Drum Highway)
|
||||
|
||||
- **Palette** — shared 3 palettes with `highway_3d` (default / neon / pastel).
|
||||
- **Demo pattern** — rock backbeat / jazz swing / fill showcase.
|
||||
- **Camera angle** — 0 (down the lanes) to 1 (top-down). Default 0.35.
|
||||
|
||||
All settings persist in `localStorage` under the `drum_h3d_*` prefix.
|
||||
|
||||
## What this plugin is *not* doing yet
|
||||
|
||||
(Historical note: this started as a pure-visual mockup; it now reads
|
||||
`bundle.drumTab` + `bundle.currentTime` and scores MIDI hits against the
|
||||
chart, so the old "no song-data wiring" caveats are gone.)
|
||||
|
||||
- Sustain trails (drums don't sustain meaningfully)
|
||||
- Sticking labels, double kick, rolls — see the TODOs in `screen.js`
|
||||
for the variant backlog
|
||||
|
||||
## Ported helpers (keep in sync with highway_3d)
|
||||
|
||||
Visual-parity code copied from `plugins/highway_3d/screen.js` — same
|
||||
function names, signatures, and constants on purpose, marked with
|
||||
`PORTED FROM highway_3d` comments at each site. If the guitar highway
|
||||
tunes one of these, mirror the change here (and in `keys_highway_3d`):
|
||||
|
||||
- `_bloomEnsure()` / `_bloomDispose()` — EffectComposer + UnrealBloomPass
|
||||
(0.65/0.5/0.82) on a multisampled HalfFloat target, ACES↔None tone-
|
||||
mapping switch in `draw()`; addons dynamic-imported from
|
||||
`/static/vendor/three/addons/` (no CDN fallback — direct render is the
|
||||
graceful degrade)
|
||||
|
||||
## Why a separate plugin (vs. drum mode inside highway_3d)?
|
||||
|
||||
Cleaner iteration. The drum highway is its own geometry, its own
|
||||
gameplay assumptions (lanes ≠ strings, no frets, no chord shapes,
|
||||
no sustains), and likely its own chart format. Forking the visuals
|
||||
in a sibling plugin lets the mockup move fast without risking
|
||||
regressions in the guitar viz that ships today. If the drum highway
|
||||
eventually matures, we can decide whether to merge or keep separate.
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" role="img" aria-label="3D Drum Highway placeholder thumbnail">
|
||||
<rect width="256" height="256" rx="36" fill="#0f172a"/>
|
||||
<rect x="18" y="18" width="220" height="220" rx="28" fill="#1e293b" stroke="#334155" stroke-width="3"/>
|
||||
<text x="128" y="150" font-family="Rubik, Arial, sans-serif" font-size="104" font-weight="800" fill="#38bdf8" text-anchor="middle">3D</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 464 B |
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "drum_highway_3d",
|
||||
"name": "3D Drum Highway",
|
||||
"version": "0.3.0",
|
||||
"type": "visualization",
|
||||
"bundled": true,
|
||||
"script": "screen.js",
|
||||
"settings": {
|
||||
"html": "settings.html",
|
||||
"category": "graphics"
|
||||
},
|
||||
"standards": [
|
||||
"capability-pipelines.v1",
|
||||
"plugin-runtime-idempotent.v1"
|
||||
],
|
||||
"capabilities": {
|
||||
"midi-input": {
|
||||
"roles": [
|
||||
"requester"
|
||||
],
|
||||
"requests": [
|
||||
"discover",
|
||||
"list-sources",
|
||||
"select-source",
|
||||
"open-source",
|
||||
"close-source"
|
||||
],
|
||||
"mode": "active",
|
||||
"compatibility": "degrade-noop",
|
||||
"ownership": "requester-only",
|
||||
"safety": "sensitive",
|
||||
"description": "Reads the e-kit/MIDI-pad input through the core midi-input domain (Web-MIDI provider ships built-in with the domain). Absent domain \u2192 no MIDI devices, fail-soft.",
|
||||
"version": 1
|
||||
}
|
||||
},
|
||||
"category": "practice",
|
||||
"description": "3D note highway for drum charts.",
|
||||
"icon": "assets/thumb.svg"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,456 @@
|
||||
<div role="group" aria-labelledby="drumh3d-heading">
|
||||
<h3 id="drumh3d-heading" class="text-sm font-medium text-gray-400 mb-2">3D Drum Highway</h3>
|
||||
<p class="text-xs text-gray-500 mb-3">
|
||||
Pick a song with a drum tab (sloppak-spec §5.3), choose <em>3D
|
||||
Drum Highway</em> from the viz picker. The plugin auto-attaches
|
||||
to your MIDI controller and routes chart pieces through the kit
|
||||
you configure below.
|
||||
</p>
|
||||
|
||||
<!-- Palette -->
|
||||
<div class="mt-3">
|
||||
<label for="drumh3d-palette" class="text-xs font-medium text-gray-400 mb-1 block">Palette</label>
|
||||
<select id="drumh3d-palette"
|
||||
onchange="window.drumH3dSetPalette && window.drumH3dSetPalette(this.value); (window._drumH3dRenderPaletteSwatches || function(){})(this.value)"
|
||||
class="w-full bg-dark-700 border border-gray-800 rounded-lg px-3 py-2 text-xs text-gray-300 outline-none">
|
||||
<option value="default">Default (Classic)</option>
|
||||
<option value="neon">Neon (saturated, electric)</option>
|
||||
<option value="pastel">Pastel (soft, low contrast)</option>
|
||||
</select>
|
||||
<div class="mt-2 flex items-center gap-1" aria-hidden="true">
|
||||
<span id="drumh3d-swatch-0" class="inline-block h-6 w-5 rounded border border-gray-700/60"></span>
|
||||
<span id="drumh3d-swatch-1" class="inline-block h-6 w-5 rounded border border-gray-700/60"></span>
|
||||
<span id="drumh3d-swatch-2" class="inline-block h-6 w-5 rounded border border-gray-700/60"></span>
|
||||
<span id="drumh3d-swatch-3" class="inline-block h-6 w-5 rounded border border-gray-700/60"></span>
|
||||
<span id="drumh3d-swatch-4" class="inline-block h-6 w-5 rounded border border-gray-700/60"></span>
|
||||
<span id="drumh3d-swatch-5" class="inline-block h-6 w-5 rounded border border-gray-700/60"></span>
|
||||
<span id="drumh3d-swatch-6" class="inline-block h-6 w-5 rounded border border-gray-700/60"></span>
|
||||
<span id="drumh3d-swatch-7" class="inline-block h-6 w-5 rounded border border-gray-700/60"
|
||||
style="background:#ffa030"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Camera angle -->
|
||||
<div class="mt-4">
|
||||
<label for="drumh3d-camera" class="text-xs font-medium text-gray-400 mb-1 block">
|
||||
Camera angle <span id="drumh3d-camera-val" class="text-gray-500 font-mono">0.35</span>
|
||||
</label>
|
||||
<input type="range" id="drumh3d-camera"
|
||||
min="0" max="1" step="0.05" value="0.35"
|
||||
oninput="window.drumH3dSetCameraAngle && window.drumH3dSetCameraAngle(this.value); document.getElementById('drumh3d-camera-val').textContent = parseFloat(this.value).toFixed(2)"
|
||||
class="w-full">
|
||||
<p class="text-xs text-gray-500 mt-1">0 = down the lanes · 1 = top-down</p>
|
||||
</div>
|
||||
|
||||
<!-- Graphics -->
|
||||
<div class="mt-6 border-t border-gray-800 pt-4">
|
||||
<h4 class="text-xs font-medium text-gray-300 mb-2">Graphics</h4>
|
||||
<label for="drumh3d-fx-bloom" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer">
|
||||
<input type="checkbox" id="drumh3d-fx-bloom" checked
|
||||
onchange="window.drumH3dSetFx && window.drumH3dSetFx('bloom', this.checked)">
|
||||
Glow (bloom)
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Soft light-bleed around the hit line and bright notes. Applies
|
||||
live; turn off to reclaim GPU headroom on weak machines.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- MIDI input -->
|
||||
<div class="mt-6 border-t border-gray-800 pt-4">
|
||||
<h4 class="text-xs font-medium text-gray-300 mb-2">MIDI input</h4>
|
||||
<label for="drumh3d-midi-input" class="text-xs font-medium text-gray-400 mb-1 block">Device</label>
|
||||
<select id="drumh3d-midi-input"
|
||||
onchange="window.drumH3dSetMidiInput && window.drumH3dSetMidiInput(this.value)"
|
||||
class="w-full bg-dark-700 border border-gray-800 rounded-lg px-3 py-1.5 text-xs text-gray-300 outline-none">
|
||||
<option value="">— none —</option>
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Auto-picks the first non-passthrough input on load. Pick "none" to pause hit and miss tracking.
|
||||
</p>
|
||||
|
||||
<label for="drumh3d-synth-vol" class="text-xs font-medium text-gray-400 mb-1 mt-3 block">
|
||||
Kit volume <span id="drumh3d-synth-vol-val" class="text-gray-500 font-mono">0.70</span>
|
||||
</label>
|
||||
<input type="range" id="drumh3d-synth-vol"
|
||||
min="0" max="1" step="0.01" value="0.7"
|
||||
oninput="window.drumH3dSetSynthVolume && window.drumH3dSetSynthVolume(this.value); document.getElementById('drumh3d-synth-vol-val').textContent = parseFloat(this.value).toFixed(2)"
|
||||
class="w-full">
|
||||
</div>
|
||||
|
||||
<!-- Kit configuration -->
|
||||
<div class="mt-6 border-t border-gray-800 pt-4">
|
||||
<h4 class="text-xs font-medium text-gray-300 mb-2">My kit</h4>
|
||||
<p class="text-xs text-gray-500 mb-3">
|
||||
Add the pieces you actually have, in the order you want them
|
||||
on the highway (left → right). The default fallback routing
|
||||
covers most common kit setups; use the fallbacks panel below
|
||||
to reroute any chart piece to a different lane on your kit,
|
||||
or set it to "—" to silently drop it.
|
||||
</p>
|
||||
|
||||
<label class="text-xs font-medium text-gray-400 mb-1 block">Kit name</label>
|
||||
<input type="text" id="drumh3d-kit-name" maxlength="80"
|
||||
class="w-full bg-dark-700 border border-gray-800 rounded-lg px-3 py-1.5 text-xs text-gray-300 outline-none mb-3"
|
||||
oninput="window._drumH3dKit && window._drumH3dKit.onNameInput(this.value)">
|
||||
|
||||
<label class="text-xs font-medium text-gray-400 mb-1 block">Lanes (left → right on the highway; kick is always a full-width bar)</label>
|
||||
<div id="drumh3d-kit-lanes" class="space-y-1 mb-2"></div>
|
||||
|
||||
<div class="mt-2 mb-3">
|
||||
<label class="text-xs font-medium text-gray-400 mb-1 block">Add a piece</label>
|
||||
<select id="drumh3d-kit-add"
|
||||
onchange="window._drumH3dKit && window._drumH3dKit.onAddPiece(this.value); this.value=''"
|
||||
class="w-full bg-dark-700 border border-gray-800 rounded-lg px-3 py-1.5 text-xs text-gray-300 outline-none">
|
||||
<option value="">— pick a piece to add —</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<details class="mt-2 mb-3">
|
||||
<summary class="text-xs text-gray-400 cursor-pointer">Chart fallbacks for pieces I don't have</summary>
|
||||
<p class="text-xs text-gray-500 mt-2 mb-2">
|
||||
When a song uses a piece that isn't on your kit, route it
|
||||
to one of your lanes. Set "—" to silently drop that piece.
|
||||
</p>
|
||||
<div id="drumh3d-kit-fallbacks" class="space-y-1"></div>
|
||||
</details>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 mt-3">
|
||||
<button type="button"
|
||||
onclick="window._drumH3dKit && window._drumH3dKit.copyToClipboard()"
|
||||
class="px-3 py-1.5 bg-dark-600 hover:bg-dark-500 rounded-lg text-xs">📋 Copy kit</button>
|
||||
<button type="button"
|
||||
onclick="document.getElementById('drumh3d-kit-import-row').classList.toggle('hidden')"
|
||||
class="px-3 py-1.5 bg-dark-600 hover:bg-dark-500 rounded-lg text-xs">📥 Import kit…</button>
|
||||
<button type="button"
|
||||
onclick="window._drumH3dKit && window._drumH3dKit.resetDefault()"
|
||||
class="px-3 py-1.5 bg-dark-600 hover:bg-dark-500 rounded-lg text-xs">Reset</button>
|
||||
<span id="drumh3d-kit-status" class="text-xs text-gray-500 ml-auto"></span>
|
||||
</div>
|
||||
|
||||
<div id="drumh3d-kit-import-row" class="hidden mt-3">
|
||||
<label class="text-xs font-medium text-gray-400 mb-1 block">Paste a shared kit</label>
|
||||
<textarea id="drumh3d-kit-import-text" rows="3"
|
||||
placeholder="Paste a base64 kit string here…"
|
||||
class="w-full bg-dark-700 border border-gray-800 rounded-lg px-3 py-2 text-[10px] font-mono text-gray-300 outline-none"></textarea>
|
||||
<button type="button"
|
||||
onclick="window._drumH3dKit && window._drumH3dKit.importFromTextarea()"
|
||||
class="mt-2 px-3 py-1.5 bg-emerald-700 hover:bg-emerald-600 rounded-lg text-xs">Apply imported kit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// Mirror of PALETTES in screen.js — kept in sync for swatch previews.
|
||||
const PALETTES = {
|
||||
default: [0xff2828, 0xffd400, 0x2080ff, 0xff8020, 0x30d040, 0xa040ff, 0xff6bd5, 0x6bffe6],
|
||||
neon: [0xff0030, 0xffe800, 0x0080ff, 0xff8030, 0x40ff50, 0xb050ff, 0xff40d0, 0x40ffd0],
|
||||
pastel: [0xe89aa0, 0xefdf90, 0x9adfee, 0xefb898, 0xa6e0a8, 0xc4a6e0, 0xe0a6c8, 0xa6e0d8],
|
||||
};
|
||||
|
||||
function toHex(n) { return '#' + n.toString(16).padStart(6, '0'); }
|
||||
|
||||
// Palette swatch preview: read the FIRST 8 pieces of the active
|
||||
// kit's lanes (or pad to 8) for colour reference.
|
||||
window._drumH3dRenderPaletteSwatches = function (id) {
|
||||
const pal = PALETTES[id] || PALETTES.default;
|
||||
const kit = (window.drumH3dGetKit && window.drumH3dGetKit()) || null;
|
||||
const lanes = (kit && kit.lanes) || [];
|
||||
// The kick always shows amber — same as the renderer. Other
|
||||
// lanes follow the piece's palette index from screen.js,
|
||||
// retrieved via drumH3dGetPiecePaletteIdx() so settings.html
|
||||
// stays in sync when pieces are added without any copy-paste.
|
||||
const PALETTE_IDX = (window.drumH3dGetPiecePaletteIdx && window.drumH3dGetPiecePaletteIdx()) || {
|
||||
kick: -1, // sentinel — render amber
|
||||
snare: 0, snare_xstick: 0,
|
||||
hh_closed: 7, hh_open: 7, hh_pedal: 7,
|
||||
tom_hi: 4, tom_mid: 2, tom_low: 5, tom_floor: 5,
|
||||
crash_l: 1, crash_r: 1, splash: 1, china: 1,
|
||||
ride: 3, ride_bell: 3,
|
||||
};
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const el = document.getElementById('drumh3d-swatch-' + i);
|
||||
if (!el) continue;
|
||||
const ln = lanes[i];
|
||||
if (!ln) { el.style.background = '#000'; el.style.opacity = '0.3'; continue; }
|
||||
el.style.opacity = '1';
|
||||
const idx = PALETTE_IDX[ln.piece];
|
||||
if (idx === -1) { el.style.background = '#ffa030'; continue; }
|
||||
el.style.background = toHex(pal[idx ?? 0]);
|
||||
}
|
||||
};
|
||||
|
||||
// Read fresh inside renderKit() — they're set by screen.js's IIFE,
|
||||
// which may not have finished evaluating when this inline script
|
||||
// first runs if the user opens Settings before the player ever loaded.
|
||||
|
||||
// Friendly piece labels — self-contained so renderKit's callbacks
|
||||
// (which run after our local PIECE_LABELS scope has cleared)
|
||||
// still work.
|
||||
const FRIENDLY = {
|
||||
kick: 'Kick',
|
||||
snare: 'Snare', snare_xstick: 'Snare (cross-stick)',
|
||||
hh_closed: 'Hi-hat (closed)', hh_open: 'Hi-hat (open)', hh_pedal: 'Hi-hat (pedal)',
|
||||
tom_hi: 'Tom — high', tom_mid: 'Tom — mid', tom_low: 'Tom — low', tom_floor: 'Floor tom',
|
||||
crash_l: 'Crash (left)', crash_r: 'Crash (right)', splash: 'Splash', china: 'China',
|
||||
ride: 'Ride', ride_bell: 'Ride bell',
|
||||
};
|
||||
function friendly(p) { return FRIENDLY[p] || p; }
|
||||
|
||||
// Render the current kit's lane list with reorder + remove buttons.
|
||||
function renderKit() {
|
||||
const kit = window.drumH3dGetKit ? window.drumH3dGetKit() : null;
|
||||
if (!kit) {
|
||||
// screen.js hasn't published its API yet — defer one tick.
|
||||
// Happens when the settings panel opens before the player
|
||||
// has loaded a song.
|
||||
setTimeout(renderKit, 100);
|
||||
return;
|
||||
}
|
||||
const PIECE_CATEGORY = window.drumH3dGetPieceCategory ? window.drumH3dGetPieceCategory() : {};
|
||||
const ALL_PIECES = window.drumH3dGetAllPieces ? window.drumH3dGetAllPieces() : [];
|
||||
document.getElementById('drumh3d-kit-name').value = kit.name || '';
|
||||
|
||||
// Lanes panel — order matters: up/down buttons reorder, x
|
||||
// removes (also clears any fallback that pointed at the
|
||||
// removed piece on the way out).
|
||||
const lanesEl = document.getElementById('drumh3d-kit-lanes');
|
||||
lanesEl.innerHTML = '';
|
||||
const taken = new Set(kit.lanes.map(l => l.piece));
|
||||
kit.lanes.forEach((ln, i) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-center gap-2 px-2 py-1 bg-dark-700/60 rounded';
|
||||
// Screen readers should hear the user-facing label
|
||||
// ("Hi-hat (closed)") not the internal piece id ("hh_closed").
|
||||
const pieceName = friendly(ln.piece);
|
||||
row.innerHTML =
|
||||
`<span class="font-mono text-[10px] text-gray-500 w-6 text-center">${i}</span>` +
|
||||
`<span class="flex-1 text-xs text-gray-300">${pieceName}</span>` +
|
||||
`<button type="button" data-act="up" data-i="${i}" class="px-1.5 py-0.5 text-xs text-gray-400 hover:text-white" ${i === 0 ? 'disabled' : ''} title="Move up" aria-label="Move ${pieceName} up">▲</button>` +
|
||||
`<button type="button" data-act="down" data-i="${i}" class="px-1.5 py-0.5 text-xs text-gray-400 hover:text-white" ${i === kit.lanes.length - 1 ? 'disabled' : ''} title="Move down" aria-label="Move ${pieceName} down">▼</button>` +
|
||||
`<button type="button" data-act="rm" data-i="${i}" class="px-1.5 py-0.5 text-xs text-red-400 hover:text-red-300" title="Remove from kit" aria-label="Remove ${pieceName} from kit">✗</button>`;
|
||||
lanesEl.appendChild(row);
|
||||
});
|
||||
lanesEl.querySelectorAll('button').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
const i = parseInt(btn.dataset.i, 10);
|
||||
const act = btn.dataset.act;
|
||||
const next = JSON.parse(JSON.stringify(kit));
|
||||
if (act === 'up' && i > 0) {
|
||||
[next.lanes[i - 1], next.lanes[i]] = [next.lanes[i], next.lanes[i - 1]];
|
||||
} else if (act === 'down' && i < next.lanes.length - 1) {
|
||||
[next.lanes[i + 1], next.lanes[i]] = [next.lanes[i], next.lanes[i + 1]];
|
||||
} else if (act === 'rm') {
|
||||
const removed = next.lanes[i].piece;
|
||||
next.lanes.splice(i, 1);
|
||||
// Drop any fallback that targeted the removed piece
|
||||
// — it's no longer a valid target.
|
||||
for (const k of Object.keys(next.fallbacks || {})) {
|
||||
if (next.fallbacks[k] === removed) delete next.fallbacks[k];
|
||||
}
|
||||
} else { return; }
|
||||
// Guard: drumH3dSetKit returns false when validation
|
||||
// rejects the kit (e.g. empty lanes[] after last removal).
|
||||
if (!window.drumH3dSetKit(next)) {
|
||||
setStatus('Kit must have at least one lane', 'err');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Add-a-piece dropdown — pieces NOT already in lanes, grouped
|
||||
// by category (kick / drums / cymbals).
|
||||
const addSel = document.getElementById('drumh3d-kit-add');
|
||||
addSel.innerHTML = '<option value="">— pick a piece to add —</option>';
|
||||
const groups = { kick: [], drum: [], cymbal: [] };
|
||||
for (const p of ALL_PIECES) {
|
||||
if (taken.has(p)) continue;
|
||||
const cat = PIECE_CATEGORY[p] || 'drum';
|
||||
if (groups[cat]) groups[cat].push(p);
|
||||
}
|
||||
for (const [cat, label] of [['kick', 'Kick'], ['drum', 'Drums'], ['cymbal', 'Cymbals']]) {
|
||||
if (!groups[cat].length) continue;
|
||||
const og = document.createElement('optgroup');
|
||||
og.label = label;
|
||||
for (const p of groups[cat]) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p;
|
||||
opt.textContent = friendly(p);
|
||||
og.appendChild(opt);
|
||||
}
|
||||
addSel.appendChild(og);
|
||||
}
|
||||
|
||||
// Fallback list — every piece NOT in lanes gets a dropdown
|
||||
// mapping it to one of the user's pieces (or "—" to drop).
|
||||
const fbEl = document.getElementById('drumh3d-kit-fallbacks');
|
||||
fbEl.innerHTML = '';
|
||||
for (const p of ALL_PIECES) {
|
||||
if (taken.has(p)) continue;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-center gap-2 px-2 py-1';
|
||||
const labelTxt = friendly(p);
|
||||
const current = (kit.fallbacks && kit.fallbacks[p]) || '';
|
||||
let optsHtml = '<option value="">— drop —</option>';
|
||||
for (const ln of kit.lanes) {
|
||||
const sel = ln.piece === current ? ' selected' : '';
|
||||
optsHtml += `<option value="${ln.piece}"${sel}>${friendly(ln.piece)}</option>`;
|
||||
}
|
||||
row.innerHTML =
|
||||
`<span class="flex-1 text-xs text-gray-400">${labelTxt}</span>` +
|
||||
`<span class="text-xs text-gray-600">→</span>` +
|
||||
`<select data-from="${p}" class="bg-dark-700 border border-gray-800 rounded px-2 py-1 text-xs text-gray-300 outline-none">${optsHtml}</select>`;
|
||||
fbEl.appendChild(row);
|
||||
}
|
||||
fbEl.querySelectorAll('select').forEach(s => {
|
||||
s.onchange = () => {
|
||||
const next = JSON.parse(JSON.stringify(kit));
|
||||
next.fallbacks = next.fallbacks || {};
|
||||
if (s.value) next.fallbacks[s.dataset.from] = s.value;
|
||||
else delete next.fallbacks[s.dataset.from];
|
||||
window.drumH3dSetKit(next); // event-driven renderKit() via drum_h3d:kit
|
||||
};
|
||||
});
|
||||
|
||||
// Refresh palette swatches so they reflect the new lane order.
|
||||
if (window._drumH3dRenderPaletteSwatches) {
|
||||
const pSel = document.getElementById('drumh3d-palette');
|
||||
window._drumH3dRenderPaletteSwatches(pSel ? pSel.value : 'default');
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(msg, kind) {
|
||||
const el = document.getElementById('drumh3d-kit-status');
|
||||
if (!el) return;
|
||||
el.textContent = msg;
|
||||
el.className = 'text-xs ml-auto ' + (kind === 'err' ? 'text-red-400' : kind === 'ok' ? 'text-emerald-400' : 'text-gray-500');
|
||||
setTimeout(() => { if (el.textContent === msg) el.textContent = ''; }, 3000);
|
||||
}
|
||||
|
||||
// Expose the kit interaction surface to the inline onclick / onchange
|
||||
// attributes used by the markup above.
|
||||
window._drumH3dKit = {
|
||||
onNameInput(name) {
|
||||
// Use the name-only setter to avoid triggering 'drum_h3d:kit'
|
||||
// on every keystroke — that event causes a full WebGL scene
|
||||
// teardown/reinit which produces visible stutter while typing.
|
||||
window.drumH3dSetKitName && window.drumH3dSetKitName(name);
|
||||
},
|
||||
onAddPiece(piece) {
|
||||
if (!piece) return;
|
||||
const kit = window.drumH3dGetKit();
|
||||
kit.lanes.push({ piece });
|
||||
window.drumH3dSetKit(kit); // event-driven renderKit() via drum_h3d:kit
|
||||
},
|
||||
copyToClipboard() {
|
||||
const b64 = window.drumH3dExportKit && window.drumH3dExportKit();
|
||||
if (!b64) return setStatus('export failed', 'err');
|
||||
const _showImportFallback = (str) => {
|
||||
// Clipboard unavailable — reveal the import row so the user can
|
||||
// copy the string from the textarea manually.
|
||||
document.getElementById('drumh3d-kit-import-row').classList.remove('hidden');
|
||||
const ta = document.getElementById('drumh3d-kit-import-text');
|
||||
ta.value = str;
|
||||
ta.focus();
|
||||
ta.select();
|
||||
setStatus('clipboard blocked — copy from the textarea below', 'err');
|
||||
};
|
||||
if (!navigator.clipboard) {
|
||||
_showImportFallback(b64);
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(b64).then(
|
||||
() => setStatus('✓ kit copied', 'ok'),
|
||||
() => _showImportFallback(b64),
|
||||
);
|
||||
},
|
||||
importFromTextarea() {
|
||||
const txt = document.getElementById('drumh3d-kit-import-text').value;
|
||||
if (!txt.trim()) return setStatus('paste a kit string first', 'err');
|
||||
const ok = window.drumH3dImportKit && window.drumH3dImportKit(txt);
|
||||
// drumH3dImportKit calls drumH3dSetKit which dispatches drum_h3d:kit;
|
||||
// the event listener re-renders the panel, so no explicit renderKit().
|
||||
if (ok) { setStatus('✓ kit imported', 'ok'); }
|
||||
else setStatus('invalid kit string', 'err');
|
||||
},
|
||||
resetDefault() {
|
||||
window.drumH3dResetKit && window.drumH3dResetKit();
|
||||
// drumH3dResetKit calls drumH3dSetKit → dispatches drum_h3d:kit
|
||||
// → event listener re-renders. No explicit renderKit() needed.
|
||||
setStatus('✓ reset to default', 'ok');
|
||||
},
|
||||
};
|
||||
|
||||
// Re-render the kit list whenever the kit changes (covers external
|
||||
// changes from setKit() / importKit() not initiated by this panel).
|
||||
window.addEventListener('drum_h3d:kit', () => renderKit());
|
||||
|
||||
// ─── MIDI device picker ───────────────────────────────
|
||||
function renderMidiPicker() {
|
||||
const sel = document.getElementById('drumh3d-midi-input');
|
||||
if (!sel) return;
|
||||
const inputs = window.drumH3dListMidiInputs ? window.drumH3dListMidiInputs() : [];
|
||||
const current = window.drumH3dGetMidiInputId ? window.drumH3dGetMidiInputId() : '';
|
||||
sel.innerHTML = '<option value="">— none —</option>';
|
||||
for (const inp of inputs) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = inp.id;
|
||||
opt.textContent = inp.name;
|
||||
if (inp.id === current) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
// Sync volume slider with the actual persisted value (default
|
||||
// 0.7 if storage was empty or never written).
|
||||
const vol = window.drumH3dGetSynthVolume ? window.drumH3dGetSynthVolume() : 0.7;
|
||||
const volSlider = document.getElementById('drumh3d-synth-vol');
|
||||
const volLabel = document.getElementById('drumh3d-synth-vol-val');
|
||||
if (volSlider) volSlider.value = String(vol);
|
||||
if (volLabel) volLabel.textContent = parseFloat(vol).toFixed(2);
|
||||
}
|
||||
window.addEventListener('drum_h3d:midi_devices', renderMidiPicker);
|
||||
// Kick off MIDI initialisation if the player viz never ran (user
|
||||
// opened Settings → 3D Drum Highway directly). _midiInit is
|
||||
// idempotent so this is safe even when the viz is also active.
|
||||
// Re-render the picker when the access promise settles.
|
||||
(function pollMidi() {
|
||||
if (window.drumH3dEnsureMidiInit) {
|
||||
Promise.resolve(window.drumH3dEnsureMidiInit()).then(renderMidiPicker);
|
||||
renderMidiPicker(); // first paint with whatever's cached
|
||||
} else {
|
||||
setTimeout(pollMidi, 100);
|
||||
}
|
||||
})();
|
||||
|
||||
// Hydrate controls from stored config on first paint.
|
||||
// Narrow the try/catch to just localStorage reads — renderKit() and
|
||||
// swatches must always run even when storage is blocked/disabled.
|
||||
const pSel = document.getElementById('drumh3d-palette');
|
||||
const cam = document.getElementById('drumh3d-camera');
|
||||
const camVal = document.getElementById('drumh3d-camera-val');
|
||||
try {
|
||||
const storedPal = localStorage.getItem('drum_h3d_palette');
|
||||
if (storedPal && PALETTES[storedPal]) pSel.value = storedPal;
|
||||
|
||||
const storedCam = parseFloat(localStorage.getItem('drum_h3d_camera_angle'));
|
||||
if (Number.isFinite(storedCam)) {
|
||||
const c = Math.min(1, Math.max(0, storedCam));
|
||||
cam.value = String(c);
|
||||
camVal.textContent = c.toFixed(2);
|
||||
}
|
||||
|
||||
// FX toggles (drum_h3d_bg_* — guitar-parity graphics controls).
|
||||
// Only explicit values override; absent/corrupt keys keep the
|
||||
// default (ON), matching screen.js readFxSettings.
|
||||
const storedBloom = localStorage.getItem('drum_h3d_bg_bloom');
|
||||
if (storedBloom === '1' || storedBloom === 'true') {
|
||||
document.getElementById('drumh3d-fx-bloom').checked = true;
|
||||
} else if (storedBloom === '0' || storedBloom === 'false') {
|
||||
document.getElementById('drumh3d-fx-bloom').checked = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Drum-Hwy3D settings] hydration failed:', e);
|
||||
}
|
||||
renderKit();
|
||||
window._drumH3dRenderPaletteSwatches(pSel.value);
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
@@ -0,0 +1,94 @@
|
||||
// Pure data-layer tests: load screen.js in a bare vm window and exercise the
|
||||
// __test exports (no DOM, no WebGL, no network). Doubles as a lint that no
|
||||
// module-scope code touches document/localStorage outside a try/catch —
|
||||
// the vm window deliberately provides neither.
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
function load() {
|
||||
const window = {
|
||||
console,
|
||||
location: { protocol: 'http:', host: 'localhost' },
|
||||
slopsmith: {},
|
||||
};
|
||||
window.window = window;
|
||||
window.globalThis = window;
|
||||
const context = vm.createContext(window);
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'screen.js'), 'utf8');
|
||||
vm.runInContext(src, context, { filename: 'screen.js' });
|
||||
return window.slopsmithViz_drum_highway_3d;
|
||||
}
|
||||
|
||||
test('module loads in a bare vm (no DOM / localStorage at module scope)', () => {
|
||||
const factory = load();
|
||||
assert.equal(typeof factory, 'function');
|
||||
assert.equal(factory.contextType, 'webgl2');
|
||||
});
|
||||
|
||||
test('_variantForHit: ghost > flam > bell > accent > normal precedence', () => {
|
||||
const { _variantForHit } = load().__test;
|
||||
assert.equal(_variantForHit({ g: true, f: true, v: 120 }), 'ghost');
|
||||
assert.equal(_variantForHit({ f: true, v: 120 }), 'flam');
|
||||
assert.equal(_variantForHit({ p: 'ride_bell' }), 'bell');
|
||||
assert.equal(_variantForHit({ v: 100 }), 'accent');
|
||||
assert.equal(_variantForHit({ v: 127 }), 'accent');
|
||||
assert.equal(_variantForHit({ v: 99 }), 'normal');
|
||||
// Missing velocity defaults to 100 → accent.
|
||||
assert.equal(_variantForHit({}), 'accent');
|
||||
});
|
||||
|
||||
test('matchesArrangement: claims drum arrangements', () => {
|
||||
const matches = load().matchesArrangement;
|
||||
assert.equal(matches({ has_drum_tab: true, arrangement: 'Drums' }), true);
|
||||
assert.equal(matches({ has_drum_tab: true, arrangement: 'Drum Kit' }), true);
|
||||
assert.equal(matches({ has_drum_tab: true, arrangement: 'Percussion' }), true);
|
||||
});
|
||||
|
||||
test('matchesArrangement: never claims without a drum tab', () => {
|
||||
const matches = load().matchesArrangement;
|
||||
assert.equal(matches(null), false);
|
||||
assert.equal(matches({}), false);
|
||||
assert.equal(matches({ arrangement: 'Drums' }), false);
|
||||
});
|
||||
|
||||
test('matchesArrangement: steal-guard — guitar arrangements stay with highway_3d', () => {
|
||||
const matches = load().matchesArrangement;
|
||||
// Full-band pack (drum_tab present) playing a guitar-family part:
|
||||
// first-match-wins Auto order must not hand these to the drum highway.
|
||||
for (const arr of ['Lead', 'Rhythm', 'Bass', 'Combo', 'Guitar 22', 'Alt. Lead']) {
|
||||
assert.equal(matches({ has_drum_tab: true, arrangement: arr }), false, arr);
|
||||
}
|
||||
// Keys notation present → the keys/staff viz take it.
|
||||
assert.equal(matches({ has_drum_tab: true, has_notation: true, arrangement: 'Piano' }), false);
|
||||
});
|
||||
|
||||
test('matchesArrangement: claims packs nothing more specific can render', () => {
|
||||
const matches = load().matchesArrangement;
|
||||
// Drum tab + nondescript arrangement, no notation → drummable, claim it.
|
||||
assert.equal(matches({ has_drum_tab: true, arrangement: '' }), true);
|
||||
assert.equal(matches({ has_drum_tab: true }), true);
|
||||
// Word-boundary check: "BasslineKeys"-style names don't contain a
|
||||
// guitar-family word as a whole word.
|
||||
assert.equal(matches({ has_drum_tab: true, arrangement: 'Bassline' }), true);
|
||||
});
|
||||
|
||||
test('readFxSettings: defaults survive a localStorage-less environment', () => {
|
||||
const { readFxSettings, FX_DEFAULTS } = load().__test;
|
||||
// The vm window has no localStorage — the try/catch must eat the
|
||||
// ReferenceError and hand back pure defaults (everything ON).
|
||||
assert.deepEqual(readFxSettings(), FX_DEFAULTS);
|
||||
assert.equal(FX_DEFAULTS.bloom, true);
|
||||
});
|
||||
|
||||
test('MIDI map: open hi-hat is a first-class piece (46 → hh_open)', () => {
|
||||
const { MIDI_TO_PIECE, HIT_TOLERANCE_S } = load().__test;
|
||||
assert.equal(MIDI_TO_PIECE[46], 'hh_open');
|
||||
assert.equal(MIDI_TO_PIECE[42], 'hh_closed');
|
||||
assert.equal(MIDI_TO_PIECE[35], 'kick');
|
||||
assert.equal(MIDI_TO_PIECE[36], 'kick');
|
||||
// ±50 ms window matches the 2D drums plugin.
|
||||
assert.equal(HIT_TOLERANCE_S, 0.05);
|
||||
});
|
||||
@@ -157,8 +157,6 @@ Every per-frame renderer call receives a `bundle` from feedBack core. Fields use
|
||||
|
||||
`tuning` and `capo` aren't consumed by this plugin.
|
||||
|
||||
Core reuses the bundle OBJECT across frames (mutated in place); never cache it or compare its identity between frames — field values are only valid for the current draw call. Field-identity caches on ARRAY fields (this plugin's `_mergeCacheChordsRef === bundle.chords` etc.) remain valid: arrays still swap reference when chart data changes. Core also exposes `bundle.lowerBoundT(arr, time)` (lower-bound on `.t`, notes/chords) and `bundle.lowerBoundTime(arr, time)` (on `.time`, beats/anchors/sections) — prefer these over the local `lowerBoundT` helper when a downlevel-host fallback isn't needed.
|
||||
|
||||
### Score FX (notedetect game-scoring layer)
|
||||
|
||||
- **"+N" score pops** → `_fxSpawnPop()` from `drawNote()` (just after the provider verdict-override block), drawn by `drawScoreFx()` (called from the `lyricsCtx` block in `draw()`, right after `drawNotedetectLabels()`). Fixed 24-slot pool (`_fxPops`), deduped per `popKey` via the TTL'd `_fxSeen` map (pruned in `drawScoreFx`). Pops rise/fade over 700 ms; font size scales with the multiplier tier.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "highway_3d",
|
||||
"name": "3D Highway",
|
||||
"version": "3.31.2",
|
||||
"version": "3.30.2",
|
||||
"type": "visualization",
|
||||
"bundled": true,
|
||||
"script": "screen.js",
|
||||
|
||||
+89
-243
@@ -1236,11 +1236,6 @@
|
||||
|
||||
// Binary lower-bound: returns the first index i in arr where arr[i].t >= t.
|
||||
// Assumes arr is sorted ascending by .t (bundle.notes / bundle.chords always are).
|
||||
// Byte-identical to core's bundle.lowerBoundT — kept as a local because this
|
||||
// plugin must run on downlevel hosts whose bundles don't carry the helper
|
||||
// (it's called from ~30 sites incl. top-level helpers that don't receive a
|
||||
// bundle). New code that already holds a bundle should prefer
|
||||
// bundle.lowerBoundT / bundle.lowerBoundTime.
|
||||
function lowerBoundT(arr, t) {
|
||||
let lo = 0, hi = arr.length;
|
||||
while (lo < hi) {
|
||||
@@ -3983,10 +3978,6 @@
|
||||
let _laneRailBoundsRefTpl = null;
|
||||
let _laneRailBoundsRefNotes = null;
|
||||
let _lastHwW = 0, _lastHwH = 0;
|
||||
// Frame counter for throttling the CSS-box drift check in draw()
|
||||
// (getBoundingClientRect is a forced layout read; see the comment
|
||||
// at the check).
|
||||
let _boxCheckCountdown = 0;
|
||||
// Last logical (CSS px) size handed to applySize(). #highway is a
|
||||
// flex:1 item, so its real rendered box (canvasSize()) can change as
|
||||
// the player layout settles after a song opens WITHOUT the backing
|
||||
@@ -4686,12 +4677,10 @@
|
||||
// the full look (re-enable the rail bloom) per browser, no rebuild:
|
||||
// localStorage.h3d_full_sus = '1' // re-enable rail bloom halo
|
||||
// delete localStorage.h3d_full_sus // back to lean default
|
||||
// Polled at ~1 Hz at the top of update() (perf: localStorage reads
|
||||
// are synchronous) so the console flag still takes effect live.
|
||||
// The bloom pool/material/gaussian texture are kept intact
|
||||
// Read once per frame at the top of update() so the flag takes effect
|
||||
// live. The bloom pool/material/gaussian texture are kept intact
|
||||
// (still pinned by the bloom unit tests and used by the opt-out path).
|
||||
let _leanSus = true;
|
||||
let _leanSusPollCounter = 0;
|
||||
|
||||
// Lifecycle flags
|
||||
let _isReady = false;
|
||||
@@ -5278,17 +5267,7 @@
|
||||
// appearing on top without depthTest.
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
// forceSinglePass accompanies EVERY transparent DoubleSide
|
||||
// material in this file: without it, Three r158+ renders
|
||||
// each such object in TWO passes (back side then front),
|
||||
// setting material.needsUpdate on both — which forces a
|
||||
// full getParameters/program-cache lookup per object per
|
||||
// frame (profiled at ~4% of throttled main-thread time)
|
||||
// and doubles the draw calls. The two-pass path exists to
|
||||
// fix self-occlusion sorting on closed transparent meshes;
|
||||
// all our DoubleSide materials are flat unlit quads
|
||||
// (labels, rails, frames, lanes) where it buys nothing.
|
||||
side: T.DoubleSide, forceSinglePass: true,
|
||||
side: T.DoubleSide,
|
||||
});
|
||||
sm.userData.h3dTechMeshMat = base;
|
||||
}
|
||||
@@ -6271,12 +6250,6 @@
|
||||
return boxH;
|
||||
}
|
||||
|
||||
// Lyrics layout cache — measureText per syllable + row wrapping
|
||||
// only changes when the displayed line(s), font size, or canvas
|
||||
// width change, not per frame. Keyed below; the per-frame work is
|
||||
// just drawing over the cached widths.
|
||||
let _lyrRowsCache = null;
|
||||
|
||||
function drawLyrics(lyrics, currentTime, ctx, W, H) {
|
||||
if (!lyrics._lines) {
|
||||
const lines = [];
|
||||
@@ -6324,51 +6297,37 @@
|
||||
const sylText = s => { const t = s.w || ''; return (t.endsWith('+') || t.endsWith('-')) ? t.slice(0, -1) : t; };
|
||||
|
||||
ctx.font = `bold ${fontSize}px sans-serif`;
|
||||
let rows, spaceWidth, bgWidth;
|
||||
const _lc = _lyrRowsCache;
|
||||
if (_lc && _lc.lyricsRef === lyrics && _lc.idx === currentIdx
|
||||
&& _lc.shown === linesToShow.length
|
||||
&& _lc.fontSize === fontSize && _lc.W === W) {
|
||||
rows = _lc.rows; spaceWidth = _lc.spaceWidth; bgWidth = _lc.bgWidth;
|
||||
} else {
|
||||
spaceWidth = ctx.measureText(' ').width;
|
||||
const maxWidth = W * 0.8;
|
||||
const spaceWidth = ctx.measureText(' ').width;
|
||||
const maxWidth = W * 0.8;
|
||||
|
||||
rows = [];
|
||||
for (const authoredLine of linesToShow) {
|
||||
let row = [], rowWidth = 0;
|
||||
for (const wordSyls of authoredLine.words) {
|
||||
const parts = [];
|
||||
let wordWidth = 0;
|
||||
for (const s of wordSyls) {
|
||||
const text = sylText(s);
|
||||
const w = ctx.measureText(text).width;
|
||||
parts.push({ syl: s, text, width: w });
|
||||
wordWidth += w;
|
||||
}
|
||||
const advance = wordWidth + spaceWidth;
|
||||
if (row.length > 0 && rowWidth + advance > maxWidth) { rows.push(row); row = []; rowWidth = 0; }
|
||||
row.push({ parts, advance });
|
||||
rowWidth += advance;
|
||||
const rows = [];
|
||||
for (const authoredLine of linesToShow) {
|
||||
let row = [], rowWidth = 0;
|
||||
for (const wordSyls of authoredLine.words) {
|
||||
const parts = [];
|
||||
let wordWidth = 0;
|
||||
for (const s of wordSyls) {
|
||||
const text = sylText(s);
|
||||
const w = ctx.measureText(text).width;
|
||||
parts.push({ syl: s, text, width: w });
|
||||
wordWidth += w;
|
||||
}
|
||||
if (row.length) rows.push(row);
|
||||
const advance = wordWidth + spaceWidth;
|
||||
if (row.length > 0 && rowWidth + advance > maxWidth) { rows.push(row); row = []; rowWidth = 0; }
|
||||
row.push({ parts, advance });
|
||||
rowWidth += advance;
|
||||
}
|
||||
|
||||
bgWidth = 0;
|
||||
for (const row of rows) {
|
||||
const rw = row.reduce((s, w) => s + w.advance, 0) - spaceWidth;
|
||||
if (rw > bgWidth) bgWidth = rw;
|
||||
}
|
||||
bgWidth = Math.min(bgWidth + 30, W * 0.85);
|
||||
_lyrRowsCache = {
|
||||
lyricsRef: lyrics, idx: currentIdx,
|
||||
shown: linesToShow.length, fontSize, W,
|
||||
rows, spaceWidth, bgWidth,
|
||||
};
|
||||
if (row.length) rows.push(row);
|
||||
}
|
||||
|
||||
const rowHeight = fontSize + 6;
|
||||
const totalHeight = rows.length * rowHeight + 10;
|
||||
let bgWidth = 0;
|
||||
for (const row of rows) {
|
||||
const rw = row.reduce((s, w) => s + w.advance, 0) - spaceWidth;
|
||||
if (rw > bgWidth) bgWidth = rw;
|
||||
}
|
||||
bgWidth = Math.min(bgWidth + 30, W * 0.85);
|
||||
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.7)';
|
||||
ctx.beginPath();
|
||||
@@ -6670,7 +6629,7 @@
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: T.AdditiveBlending,
|
||||
side: T.DoubleSide, forceSinglePass: true,
|
||||
side: T.DoubleSide,
|
||||
fog: true,
|
||||
}));
|
||||
mAccentHaloNear = mkAccentHaloMats(ACCENT_HALO_OP_NEAR);
|
||||
@@ -6730,7 +6689,7 @@
|
||||
new T.MeshBasicMaterial({
|
||||
vertexColors: true,
|
||||
transparent: true, opacity: 1.0, depthWrite: false,
|
||||
blending: T.AdditiveBlending, side: T.DoubleSide, forceSinglePass: true, fog: false,
|
||||
blending: T.AdditiveBlending, side: T.DoubleSide, fog: false,
|
||||
}),
|
||||
));
|
||||
// Notedetect feedback outline (issue #9): hot magenta-red (0xff0066, hue
|
||||
@@ -6870,7 +6829,7 @@
|
||||
emissiveIntensity: 0.9,
|
||||
transparent: true,
|
||||
opacity: 0.85,
|
||||
side: T.DoubleSide, forceSinglePass: true,
|
||||
side: T.DoubleSide,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
});
|
||||
@@ -6897,7 +6856,7 @@
|
||||
color: CHORD_BOX_TEAL_HEX,
|
||||
transparent: true, opacity: 0.85,
|
||||
depthTest: false, depthWrite: false,
|
||||
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
fog: false, side: T.DoubleSide,
|
||||
});
|
||||
pSusRail = pool(noteG, () => {
|
||||
const m = new T.Mesh(gSusRail, mSusRailBase.clone());
|
||||
@@ -6918,7 +6877,7 @@
|
||||
transparent: true, opacity: 0.55,
|
||||
blending: T.AdditiveBlending,
|
||||
depthTest: false, depthWrite: false,
|
||||
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
fog: false, side: T.DoubleSide,
|
||||
});
|
||||
pSusRailBloom = pool(noteG, () => {
|
||||
const m = new T.Mesh(gSusRailBloom, mSusRailBloomBase.clone());
|
||||
@@ -6932,7 +6891,7 @@
|
||||
gTechPlane = new T.PlaneGeometry(1, 1);
|
||||
pTechPlane = pool(noteG, () => {
|
||||
const m = new T.Mesh(gTechPlane, new T.MeshBasicMaterial({
|
||||
transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide,
|
||||
}));
|
||||
m.renderOrder = 1000;
|
||||
return m;
|
||||
@@ -6986,7 +6945,7 @@
|
||||
uniforms: { map: { value: spriteMat.map } },
|
||||
vertexShader: _imTechVert,
|
||||
fragmentShader: _imTechFrag,
|
||||
transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide,
|
||||
});
|
||||
const im = new T.InstancedMesh(geo, mat, IM_TECH_CAP);
|
||||
im.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
||||
@@ -7099,7 +7058,7 @@
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
fog: false,
|
||||
side: T.DoubleSide, forceSinglePass: true,
|
||||
side: T.DoubleSide,
|
||||
}),
|
||||
));
|
||||
pChordBox = pool(noteG, () => new T.Mesh(
|
||||
@@ -7111,7 +7070,7 @@
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
fog: false,
|
||||
side: T.DoubleSide, forceSinglePass: true,
|
||||
side: T.DoubleSide,
|
||||
}),
|
||||
));
|
||||
|
||||
@@ -7194,7 +7153,7 @@
|
||||
_imPMXFillMat = new T.ShaderMaterial({
|
||||
vertexShader: _imFillVert, fragmentShader: _imFillFrag,
|
||||
transparent: true, depthTest: false, depthWrite: false,
|
||||
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
fog: false, side: T.DoubleSide,
|
||||
});
|
||||
imPMXFill = new T.InstancedMesh(gPMXFill, _imPMXFillMat, IM_STRUM_CAP);
|
||||
imPMXFill.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
||||
@@ -7274,7 +7233,7 @@
|
||||
_imFHXFillMat = new T.ShaderMaterial({
|
||||
vertexShader: _imFillVert, fragmentShader: _imFillFrag,
|
||||
transparent: true, depthTest: false, depthWrite: false,
|
||||
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
fog: false, side: T.DoubleSide,
|
||||
});
|
||||
imFHXFill = new T.InstancedMesh(gFHXFill, _imFHXFillMat, IM_STRUM_CAP);
|
||||
imFHXFill.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
||||
@@ -7355,7 +7314,7 @@
|
||||
_imPMXLinesMat = new T.ShaderMaterial({
|
||||
vertexShader: _imLinesVert, fragmentShader: _imLinesFrag,
|
||||
transparent: true, depthTest: false, depthWrite: false,
|
||||
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
fog: false, side: T.DoubleSide,
|
||||
});
|
||||
imPMXLines = new T.InstancedMesh(gPMXLines, _imPMXLinesMat, IM_STRUM_CAP);
|
||||
imPMXLines.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
||||
@@ -7437,7 +7396,7 @@
|
||||
_imFHXLinesMat = new T.ShaderMaterial({
|
||||
vertexShader: _imLinesVert, fragmentShader: _imLinesFrag,
|
||||
transparent: true, depthTest: false, depthWrite: false,
|
||||
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
fog: false, side: T.DoubleSide,
|
||||
});
|
||||
imFHXLines = new T.InstancedMesh(gFHXLines, _imFHXLinesMat, IM_STRUM_CAP);
|
||||
imFHXLines.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
||||
@@ -7459,28 +7418,28 @@
|
||||
gPMXFill,
|
||||
new T.MeshBasicMaterial({
|
||||
color: 0x000000, transparent: true, opacity: 1,
|
||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide,
|
||||
}),
|
||||
));
|
||||
pFHXFill = pool(noteG, () => new T.Mesh(
|
||||
gFHXFill,
|
||||
new T.MeshBasicMaterial({
|
||||
color: 0x000000, transparent: true, opacity: 1,
|
||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide,
|
||||
}),
|
||||
));
|
||||
pMuteXLines = pool(noteG, () => new T.Mesh(
|
||||
gPMXLines,
|
||||
new T.MeshBasicMaterial({
|
||||
color: 0xffffff, transparent: true, opacity: 1,
|
||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide,
|
||||
}),
|
||||
));
|
||||
pFHXLines = pool(noteG, () => new T.Mesh(
|
||||
gFHXLines,
|
||||
new T.MeshBasicMaterial({
|
||||
color: 0xffffff, transparent: true, opacity: 1,
|
||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide,
|
||||
}),
|
||||
));
|
||||
|
||||
@@ -9927,105 +9886,6 @@
|
||||
}
|
||||
|
||||
/* ── Per-frame rendering ─────────────────────────────────────────── */
|
||||
// ── GPU pre-warm (perf: first-appearance hitches) ─────────────────
|
||||
// Three.js compiles a material's shader program and uploads a
|
||||
// texture the first frame the owning object renders — profiled as
|
||||
// mid-song frame spikes (getParameters / texSubImage2D). Pay those
|
||||
// costs during init (load spinner) instead:
|
||||
// _prewarmStatic() — ren.compile() over the fully-built scene
|
||||
// + deterministic label textures (fret
|
||||
// numbers in every per-frame style/colour
|
||||
// combo).
|
||||
// _prewarmChart(bundle) — chart-dependent labels (chord template
|
||||
// names, section names); needs the ready
|
||||
// bundle, so it runs once from the first
|
||||
// draw() after each init.
|
||||
// txtMat() rasterises into the unbounded cache these draws hit
|
||||
// anyway; ren.initTexture() forces the GPU upload now.
|
||||
// Swap a pooled label sprite's cached texture WITHOUT recompiling.
|
||||
// Setting material.needsUpdate bumps material.version, which forces
|
||||
// Three.js through getParameters/getProgramCacheKey on the next
|
||||
// render. Swapping one non-null texture for another does NOT change
|
||||
// the compiled program (the USE_MAP define is unchanged); only a
|
||||
// null <-> non-null transition does, and pooled label sprites are
|
||||
// constructed with a non-null map, so in practice this never
|
||||
// recompiles. (Note: the DOMINANT getParameters churn turned out to
|
||||
// be Three's transparent-DoubleSide two-pass path — see the
|
||||
// forceSinglePass comment in _spriteMat2MeshMat — this helper
|
||||
// removes the label-swap contribution on top of that.)
|
||||
function _setLabelMap(sprite, srcMat) {
|
||||
const m = sprite.material;
|
||||
if (m.map === srcMat.map) return;
|
||||
const nullnessChanged = (m.map == null) !== (srcMat.map == null);
|
||||
m.map = srcMat.map;
|
||||
if (nullnessChanged) m.needsUpdate = true;
|
||||
}
|
||||
|
||||
let _chartPrewarmed = false;
|
||||
function _prewarmTex(mat) {
|
||||
if (mat && mat.map && ren) ren.initTexture(mat.map);
|
||||
}
|
||||
function _prewarmStatic() {
|
||||
// MAINTENANCE NOTE: this list must cover every deterministic
|
||||
// (chart-independent) material/texture the per-frame paths can
|
||||
// request lazily. Adding a new label style or sprite factory to
|
||||
// drawNote()/update() without warming it here silently
|
||||
// reintroduces a first-appearance texSubImage2D/compile spike
|
||||
// mid-song. Chart-dependent labels (chord names, section names)
|
||||
// live in _prewarmChart.
|
||||
try {
|
||||
if (ren && scene && cam) ren.compile(scene, cam);
|
||||
} catch (e) { console.warn('[3D-Hwy] prewarm compile:', e); }
|
||||
try {
|
||||
// Fret-number labels in the per-frame style/colour combos.
|
||||
for (let f = 0; f <= NFRETS; f++) {
|
||||
_prewarmTex(txtMat(f, FRET_LABEL_GOLD_HEX, false, 'noteFret'));
|
||||
_prewarmTex(txtMat(f, FRET_LABEL_GOLD_HEX, false, 'fretRow'));
|
||||
_prewarmTex(txtMat(f, FRET_LABEL_IDLE_HEX, false, 'fretRow'));
|
||||
_prewarmTex(txtMat(f, '#ffffff', false, 'ghostFret'));
|
||||
}
|
||||
// Teaching marks (drawNote _drawTeachMark): finger hints
|
||||
// T/1-4 (teachFg) and scale degrees 0-11 (teachSd).
|
||||
_prewarmTex(txtMat('T', '#7fd1ff', false, 'teachFg'));
|
||||
for (let i = 1; i <= 4; i++) _prewarmTex(txtMat(String(i), '#7fd1ff', false, 'teachFg'));
|
||||
for (let i = 0; i <= 11; i++) _prewarmTex(txtMat(String(i), '#ffcc66', false, 'teachSd'));
|
||||
// Technique sprite factories (own caches, keyed by packed
|
||||
// number): PM/FH mute X, hammer/pull triangles, bend
|
||||
// chevron stacks, slide direction arrows — per string
|
||||
// colour of the active palette.
|
||||
_prewarmTex(palmMuteXSpriteMat());
|
||||
_prewarmTex(fretHandMuteXSpriteMat());
|
||||
const _nWarm = Math.min(
|
||||
Math.max(nStr, 6),
|
||||
(activePalette && activePalette.length) || 0);
|
||||
for (let s = 0; s < _nWarm; s++) {
|
||||
const hex = activePalette[s] || 0xffffff;
|
||||
_prewarmTex(triMat(true, hex));
|
||||
_prewarmTex(triMat(false, hex));
|
||||
for (let st = 1; st <= 4; st++) _prewarmTex(bendChevronMat(st, hex));
|
||||
const arrowHex = darkenHex(hex, 0.55);
|
||||
_prewarmTex(slideArrowMat(true, arrowHex));
|
||||
_prewarmTex(slideArrowMat(false, arrowHex));
|
||||
}
|
||||
} catch (e) { console.warn('[3D-Hwy] prewarm labels:', e); }
|
||||
}
|
||||
function _prewarmChart(bundle) {
|
||||
try {
|
||||
const tpls = bundle && bundle.chordTemplates;
|
||||
if (Array.isArray(tpls)) {
|
||||
for (const tpl of tpls) {
|
||||
if (tpl && tpl.name) _prewarmTex(txtMat(tpl.name, '#e8d080', true, 'chord'));
|
||||
}
|
||||
}
|
||||
const secs = bundle && bundle.sections;
|
||||
if (Array.isArray(secs)) {
|
||||
for (const s of secs) {
|
||||
if (s && s.name) _prewarmTex(txtMat(s.name, '#00cccc', true, 'section'));
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn('[3D-Hwy] prewarm chart labels:', e); }
|
||||
}
|
||||
|
||||
function update(bundle) {
|
||||
pbBeg(0);
|
||||
// [verdict glow] Apply the level-driven verdict brightness captured
|
||||
@@ -10049,14 +9909,10 @@
|
||||
// Lean sustain rendering is the default (see declaration above):
|
||||
// the trail/ribbon outline always draws; only the additive rail
|
||||
// bloom halo is dropped. The full look (with bloom) is an opt-out.
|
||||
// localStorage.getItem is a synchronous storage read — polled at
|
||||
// ~1 Hz instead of every frame; the console flag still takes
|
||||
// effect live (within a second).
|
||||
if ((_leanSusPollCounter++ % 60) === 0) {
|
||||
try {
|
||||
_leanSus = localStorage.getItem('h3d_full_sus') !== '1';
|
||||
} catch (_) { _leanSus = true; }
|
||||
}
|
||||
// Cheap per-frame read so the console flag takes effect live.
|
||||
try {
|
||||
_leanSus = localStorage.getItem('h3d_full_sus') !== '1';
|
||||
} catch (_) { _leanSus = true; }
|
||||
// Materialize the text-size multiplier from the user's slider.
|
||||
// textSize ∈ [0,1]; _textSizeMul ∈ [0.5, 1.5] with 0.5 ↦ 1.0×
|
||||
// so default behaviour matches what the renderer did pre-slider.
|
||||
@@ -12000,7 +11856,7 @@
|
||||
const lblW = 28 * K, lblH = 9 * K;
|
||||
const lbl = pChordLbl.get();
|
||||
const mat = txtMat(chordName, '#e8d080', true, 'chord');
|
||||
_setLabelMap(lbl, mat);
|
||||
if (lbl.material.map !== mat.map) { lbl.material.map = mat.map; lbl.material.needsUpdate = true; }
|
||||
lbl.material.opacity = Math.min(1, 0.3 + fade * 0.7) * chordTailMul;
|
||||
// Gold chord name: slight +X shift from flush-left so it sits farther right.
|
||||
const lblWS = lblW * _textSizeMul;
|
||||
@@ -12036,7 +11892,7 @@
|
||||
if (!text) return;
|
||||
const s = pChordLbl.get();
|
||||
const m = txtMat(text, colorHex, true, 'chord');
|
||||
_setLabelMap(s, m);
|
||||
if (s.material.map !== m.map) { s.material.map = m.map; s.material.needsUpdate = true; }
|
||||
s.material.opacity = opacity;
|
||||
s.position.set(baseX, hy, z);
|
||||
s.scale.set(hlW, hlH, 1);
|
||||
@@ -12160,7 +12016,10 @@
|
||||
_seenChordFrets.add(f);
|
||||
const lbl = pNoteFretLabel.get();
|
||||
const mat = txtMat(f, FRET_LABEL_GOLD_HEX, false, 'noteFret');
|
||||
_setLabelMap(lbl, mat);
|
||||
if (lbl.material.map !== mat.map) {
|
||||
lbl.material.map = mat.map;
|
||||
lbl.material.needsUpdate = true;
|
||||
}
|
||||
lbl.position.set(xFretMid(f), yMinF, z);
|
||||
lbl.renderOrder = renderOrderForLayerAtZ(z, 'CHORD_FRET_LABEL');
|
||||
const _flS = 7.0 * K * (1 + 0.4 * chDt / AHEAD) * _textSizeMul * fretLabelScaleForFret(f);
|
||||
@@ -12783,7 +12642,10 @@
|
||||
const color = '#888888';
|
||||
const sp = pFretColMarker.get();
|
||||
const m = txtMat(f, color, false, 'noteFret');
|
||||
_setLabelMap(sp, m);
|
||||
if (sp.material.map !== m.map) {
|
||||
sp.material.map = m.map;
|
||||
sp.material.needsUpdate = true;
|
||||
}
|
||||
sp.material.opacity = 0.85 * _colFadeIn;
|
||||
sp.position.set(xFretMid(f), labelY, z);
|
||||
// Z-proportional: sits between chord frame and note gem
|
||||
@@ -14066,7 +13928,10 @@
|
||||
_frameLabeledKeys.add(_flFrameKey);
|
||||
const fretLabel = pNoteFretLabel.get();
|
||||
const cachedMat = txtMat(n.f, FRET_LABEL_GOLD_HEX, false, 'noteFret');
|
||||
_setLabelMap(fretLabel, cachedMat);
|
||||
if (fretLabel.material.map !== cachedMat.map) {
|
||||
fretLabel.material.map = cachedMat.map;
|
||||
fretLabel.material.needsUpdate = true;
|
||||
}
|
||||
fretLabel.position.set(x, labelY, noteZ);
|
||||
fretLabel.renderOrder = renderOrderForLayerAtZ(noteZ,
|
||||
_isArpNote
|
||||
@@ -14091,7 +13956,10 @@
|
||||
if (!text) return;
|
||||
const spr = pTeachMarkLbl.get();
|
||||
const m = txtMat(text, colorHex, false, cacheKey);
|
||||
_setLabelMap(spr, m);
|
||||
if (spr.material.map !== m.map) {
|
||||
spr.material.map = m.map;
|
||||
spr.material.needsUpdate = true;
|
||||
}
|
||||
spr.position.set(x + dx, labelY, noteZ);
|
||||
spr.renderOrder = renderOrderForLayerAtZ(noteZ,
|
||||
_isArpNote ? 'ARP_NOTE_FRET_LABEL' : 'NOTE_FRET_LABEL');
|
||||
@@ -14125,7 +13993,10 @@
|
||||
const _isArp2 = arpBounds !== null;
|
||||
const fl2 = pNoteFretLabel.get();
|
||||
const cm2 = txtMat(n.f, FRET_LABEL_GOLD_HEX, false, 'noteFret');
|
||||
_setLabelMap(fl2, cm2);
|
||||
if (fl2.material.map !== cm2.map) {
|
||||
fl2.material.map = cm2.map;
|
||||
fl2.material.needsUpdate = true;
|
||||
}
|
||||
fl2.position.set(x, _labelY2, noteZ);
|
||||
fl2.renderOrder = renderOrderForLayerAtZ(noteZ,
|
||||
_isArp2
|
||||
@@ -15091,12 +14962,6 @@
|
||||
_invertedForBoard = _invertedCached;
|
||||
_leftyForBoard = _leftyCached;
|
||||
if (!initScene()) { _unsubscribeFocus(); _rejectReady(new Error('initScene failed')); return; }
|
||||
// Pre-compile shaders + upload deterministic label
|
||||
// textures while the load spinner is still up; the
|
||||
// chart-dependent half runs on first draw() (bundle
|
||||
// arrays are only guaranteed populated post-ready).
|
||||
_prewarmStatic();
|
||||
_chartPrewarmed = false;
|
||||
const sz = canvasSize(highwayCanvas);
|
||||
// Mark ready before RAF so any resize(w,h) calls that arrive
|
||||
// in the meantime (e.g. from sizeCanvases()) are applied directly.
|
||||
@@ -15135,10 +15000,6 @@
|
||||
|
||||
draw(bundle) {
|
||||
if (!_isReady) return;
|
||||
if (!_chartPrewarmed) {
|
||||
_chartPrewarmed = true;
|
||||
_prewarmChart(bundle);
|
||||
}
|
||||
_invertedCached = !!bundle.inverted;
|
||||
_leftyCached = !!bundle.lefty;
|
||||
const newNStr = resolveStringCount(bundle);
|
||||
@@ -15181,40 +15042,25 @@
|
||||
// for the pre-settle (too-tall) size and crops the near strings
|
||||
// / fret numbers until the user un/re-maximizes the window.
|
||||
if (highwayCanvas) {
|
||||
// Backing-store drift (branch 1) is detected with cheap
|
||||
// property reads every frame. The CSS-box checks (branches
|
||||
// 2/3) need canvasSize() → getBoundingClientRect(), a
|
||||
// forced layout read — profiled at ~1.2% of throttled
|
||||
// main-thread time when run per frame. Throttle the box
|
||||
// read to every 10th frame (plus whenever the backing
|
||||
// store changed or the wrap isn't pinned yet): the layout
|
||||
// settle it exists to catch plays out over hundreds of ms
|
||||
// right after a song opens, so a ~166 ms detection cadence
|
||||
// loses nothing visible.
|
||||
const _bsChanged = highwayCanvas.width !== _lastHwW
|
||||
|| highwayCanvas.height !== _lastHwH;
|
||||
_boxCheckCountdown = (_boxCheckCountdown + 1) % 10;
|
||||
if (_bsChanged || !_wrapPinned || _boxCheckCountdown === 0) {
|
||||
const box = canvasSize(highwayCanvas);
|
||||
if (_bsChanged) {
|
||||
_lastHwW = highwayCanvas.width;
|
||||
_lastHwH = highwayCanvas.height;
|
||||
if (box.w > 0 && box.h > 0) applySize(box.w, box.h);
|
||||
} else if (box.w > 0 && box.h > 0 &&
|
||||
(Math.abs(box.w - _appliedW) > 1 || Math.abs(box.h - _appliedH) > 1)) {
|
||||
applySize(box.w, box.h);
|
||||
} else if (!_wrapPinned && box.w > 0 && box.h > 0 &&
|
||||
highwayCanvas.offsetWidth > 0 && highwayCanvas.offsetHeight > 0) {
|
||||
// 3. The overlay pin couldn't be applied at init because
|
||||
// #highway had no layout yet (offsetWidth/Height === 0),
|
||||
// so applySize() only set the wrap height. The canvas has
|
||||
// now laid out but to the same logical size, so neither
|
||||
// drift branch above fires — re-run applySize to pin the
|
||||
// wrap to the canvas box now that its offsets are real.
|
||||
// Otherwise the overlay stays at top:0;left:0;right:0 and
|
||||
// a strip of #highway is exposed on first load / split.
|
||||
applySize(box.w, box.h);
|
||||
}
|
||||
const box = canvasSize(highwayCanvas);
|
||||
if (highwayCanvas.width !== _lastHwW || highwayCanvas.height !== _lastHwH) {
|
||||
_lastHwW = highwayCanvas.width;
|
||||
_lastHwH = highwayCanvas.height;
|
||||
if (box.w > 0 && box.h > 0) applySize(box.w, box.h);
|
||||
} else if (box.w > 0 && box.h > 0 &&
|
||||
(Math.abs(box.w - _appliedW) > 1 || Math.abs(box.h - _appliedH) > 1)) {
|
||||
applySize(box.w, box.h);
|
||||
} else if (!_wrapPinned && box.w > 0 && box.h > 0 &&
|
||||
highwayCanvas.offsetWidth > 0 && highwayCanvas.offsetHeight > 0) {
|
||||
// 3. The overlay pin couldn't be applied at init because
|
||||
// #highway had no layout yet (offsetWidth/Height === 0),
|
||||
// so applySize() only set the wrap height. The canvas has
|
||||
// now laid out but to the same logical size, so neither
|
||||
// drift branch above fires — re-run applySize to pin the
|
||||
// wrap to the canvas box now that its offsets are real.
|
||||
// Otherwise the overlay stays at top:0;left:0;right:0 and
|
||||
// a strip of #highway is exposed on first load / split.
|
||||
applySize(box.w, box.h);
|
||||
}
|
||||
}
|
||||
update(bundle);
|
||||
|
||||
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Keys Highway 3D
|
||||
|
||||
RS+-style falling-note 3D piano highway for [Slopsmith](https://github.com/got-feedback/feedback), fed by the **Sloppak Notation Format** (sloppak-spec §5.3) — part of the piano/keys first-class epic (slopsmith#828, plugin workstream slopsmith#824).
|
||||
|
||||
- Consumes the `notation_info` / `notation_measures` highway-WS stream over a private per-instance socket and flattens measure → staff → voice → beat → note into `{midi, t, durSec, hand}` (durations derived from written `dur`/`dot`/`tu` at the running tempo; ties extend; overlap-clamped).
|
||||
- 3D perspective highway to a vanishing point with a real white/black-key keyboard; per-key **pitch-class colours** (Synthesia convention — C red, D yellow, E blue, …) with hand (rh/lh) as a secondary brightness cue.
|
||||
- Full RS+ visual treatment: key **letter glyphs** printed on the active-range key tops (cached CanvasTextures), **bevelled gem-style note blocks** (ExtrudeGeometry, geometry/material caches keyed by size and pitch-class×hand), **floating bar numbers** scrolling with the notes, **active-range lane dimming** so the playable span pops, and a **glowing pulsing hit-line** (layered additive gradient planes — no postprocessing).
|
||||
- Performance discipline: no per-frame allocations or DOM queries in `draw()`. Chart-scoped resources — note geometries/materials, bar-number and glow textures — are cached and disposed on chart teardown; the key-letter glyph `CanvasTexture`s live in a shared module-level cache that survives teardown and is reused across instances.
|
||||
- Auto-selected for arrangements with notation via `matchesArrangement(songInfo.has_notation)`; capability-native `visualization` provider declaration.
|
||||
- **Web MIDI input scoring**: module-level MIDI singleton (one access per tab, focused-instance routing) with device auto-connect by saved id+name, loopback blocklist, channel filter, transpose and CC64 sustain (`keys3d_` localStorage prefix; `window.keysH3d*` settings API). Hit detection matches played MIDI against the flattened chart notes within ±0.10 s with per-note dedupe and a missed-note sweep (only while a device is connected — never retroactive across a mid-song connect).
|
||||
- **Live hit feedback on the MIDI path** (not the chart): key depress (~4° back-edge pivot, ~120 ms spring; the key letter rides along), wrong-note red key flash, and a vertical flame flare on hits (pooled additive sprites, white-hot base fading into the pitch-class colour, ~400 ms).
|
||||
- **End-of-run stats**: POSTs `/api/stats` `{filename, arrangement, score, accuracy}` exactly once per run with the same formula as the guitar notedetect path (`accuracy = hits / max(1, hits+misses)`, `score = round(hits·100·accuracy)`), then notifies the progression core when present.
|
||||
- **Capability wiring** (all guarded for servers without the hosts): registers as a note-detection `midi` provider (`keys-midi`, `verify.target`), opens a per-song binding scoped to the chart's keys range, reports hit/miss observability events, and exposes Web MIDI inputs to the audio-input domain with pseudonymized labels (`midi-input-1`, …) via `source.enumerate/describe/open/close`.
|
||||
- Headless test hook: `window.__keysHwTest = { injectNoteOn(midi, when), getScore() }`.
|
||||
|
||||
## Tests
|
||||
|
||||
```
|
||||
node --test tests/*.test.js
|
||||
```
|
||||
|
||||
## Ported helpers (keep in sync with highway_3d)
|
||||
|
||||
Visual-parity code copied from `plugins/highway_3d/screen.js` — same
|
||||
function names, signatures, and constants on purpose, marked with
|
||||
`PORTED FROM highway_3d` comments at each site. If the guitar highway
|
||||
tunes one of these, mirror the change here (and in `drum_highway_3d`):
|
||||
|
||||
- `_bloomEnsure()` / `_bloomDispose()` — EffectComposer + UnrealBloomPass
|
||||
(0.65/0.5/0.82) on a multisampled HalfFloat target, ACES↔None tone-
|
||||
mapping switch in `draw()`; addons dynamic-imported from
|
||||
`/static/vendor/three/addons/` (no CDN fallback — direct render is the
|
||||
graceful degrade)
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0.
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" role="img" aria-label="Keys Highway 3D placeholder thumbnail">
|
||||
<rect width="256" height="256" rx="36" fill="#0f172a"/>
|
||||
<rect x="18" y="18" width="220" height="220" rx="28" fill="#1e293b" stroke="#334155" stroke-width="3"/>
|
||||
<text x="128" y="150" font-family="Rubik, Arial, sans-serif" font-size="104" font-weight="800" fill="#38bdf8" text-anchor="middle">KH</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 464 B |
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"id": "keys_highway_3d",
|
||||
"name": "Keys Highway 3D",
|
||||
"version": "0.1.0",
|
||||
"description": "RS+-style 3D falling-note piano highway fed by the Sloppak Notation Format, with Web MIDI input scoring.",
|
||||
"type": "visualization",
|
||||
"bundled": true,
|
||||
"script": "screen.js",
|
||||
"settings": {
|
||||
"html": "settings.html",
|
||||
"category": "graphics"
|
||||
},
|
||||
"standards": [
|
||||
"capability-pipelines.v1",
|
||||
"plugin-runtime-idempotent.v1"
|
||||
],
|
||||
"capabilities": {
|
||||
"visualization": {
|
||||
"roles": [
|
||||
"provider"
|
||||
],
|
||||
"operations": [
|
||||
"renderer.create",
|
||||
"renderer.destroy"
|
||||
],
|
||||
"emits": [
|
||||
"renderer-ready",
|
||||
"renderer-failed"
|
||||
],
|
||||
"mode": "active",
|
||||
"safety": "safe"
|
||||
},
|
||||
"note-detection": {
|
||||
"roles": [
|
||||
"provider"
|
||||
],
|
||||
"operations": [
|
||||
"verify.target"
|
||||
],
|
||||
"emits": [
|
||||
"hit",
|
||||
"miss"
|
||||
],
|
||||
"mode": "active",
|
||||
"safety": "sensitive"
|
||||
},
|
||||
"midi-input": {
|
||||
"roles": [
|
||||
"requester"
|
||||
],
|
||||
"requests": [
|
||||
"discover",
|
||||
"list-sources",
|
||||
"select-source",
|
||||
"open-source",
|
||||
"close-source"
|
||||
],
|
||||
"mode": "active",
|
||||
"compatibility": "degrade-noop",
|
||||
"ownership": "requester-only",
|
||||
"safety": "sensitive",
|
||||
"description": "Reads the MIDI keyboard input through the core midi-input domain (Web-MIDI provider ships built-in with the domain). Absent domain \u2192 no MIDI devices, fail-soft.",
|
||||
"version": 1
|
||||
}
|
||||
},
|
||||
"category": "practice",
|
||||
"icon": "assets/thumb.svg"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
<div role="group" aria-labelledby="keysh3d-heading">
|
||||
<h3 id="keysh3d-heading" class="text-sm font-medium text-gray-400 mb-2">3D Keys Highway</h3>
|
||||
<p class="text-xs text-gray-500 mb-3">
|
||||
Pick a song with keys notation (sloppak-spec §5.3) and choose
|
||||
<em>Keys Highway 3D</em> from the viz picker (Auto selects it for
|
||||
notation charts). The plugin auto-attaches to your MIDI keyboard;
|
||||
device, channel and transpose are configured from the player for
|
||||
now — this panel holds the graphics controls.
|
||||
</p>
|
||||
|
||||
<!-- Graphics -->
|
||||
<div class="mt-3">
|
||||
<h4 class="text-xs font-medium text-gray-300 mb-2">Graphics</h4>
|
||||
<label for="keysh3d-fx-bloom" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer">
|
||||
<input type="checkbox" id="keysh3d-fx-bloom" checked
|
||||
onchange="window.keys3dSetFx && window.keys3dSetFx('bloom', this.checked)">
|
||||
Glow (bloom)
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Soft light-bleed around the hit line, hit flames and consumed
|
||||
sustains. Applies live; turn off to reclaim GPU headroom on
|
||||
weak machines.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
// Hydrate controls from stored config on first paint. Narrow the
|
||||
// try/catch to just the localStorage reads (drum_highway_3d
|
||||
// settings-hydration convention).
|
||||
try {
|
||||
// FX toggles (keys3d_bg_* — guitar-parity graphics controls).
|
||||
// Only explicit values override; absent/corrupt keys keep the
|
||||
// default (ON), matching screen.js readFxSettings.
|
||||
const storedBloom = localStorage.getItem('keys3d_bg_bloom');
|
||||
if (storedBloom === '1' || storedBloom === 'true') {
|
||||
document.getElementById('keysh3d-fx-bloom').checked = true;
|
||||
} else if (storedBloom === '0' || storedBloom === 'false') {
|
||||
document.getElementById('keysh3d-fx-bloom').checked = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Keys-Hwy3D settings] hydration failed:', e);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
@@ -0,0 +1,190 @@
|
||||
// Pure data-layer tests: load screen.js in a bare vm window and exercise the
|
||||
// __test exports (no DOM, no WebGL, no network).
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
function load() {
|
||||
const window = {
|
||||
console,
|
||||
location: { protocol: 'http:', host: 'localhost' },
|
||||
slopsmith: {},
|
||||
};
|
||||
window.window = window;
|
||||
window.globalThis = window;
|
||||
const context = vm.createContext(window);
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'screen.js'), 'utf8');
|
||||
vm.runInContext(src, context, { filename: 'screen.js' });
|
||||
return window.slopsmithViz_keys_highway_3d.__test;
|
||||
}
|
||||
|
||||
test('beatDurSec: base, dotted, double-dotted, tuplet', () => {
|
||||
const { beatDurSec } = load();
|
||||
// 120 BPM → quarter = 0.5s
|
||||
assert.equal(beatDurSec({ dur: 4 }, 120), 0.5);
|
||||
assert.equal(beatDurSec({ dur: 2 }, 120), 1.0);
|
||||
assert.equal(beatDurSec({ dur: 8 }, 120), 0.25);
|
||||
// dotted quarter = 0.75s; double-dotted = 0.875s
|
||||
assert.equal(beatDurSec({ dur: 4, dot: 1 }, 120), 0.75);
|
||||
assert.equal(beatDurSec({ dur: 4, dot: 2 }, 120), 0.875);
|
||||
// triplet eighth: 0.25 * 2/3
|
||||
assert.ok(Math.abs(beatDurSec({ dur: 8, tu: [3, 2] }, 120) - 0.25 * 2 / 3) < 1e-9);
|
||||
// invalid → null
|
||||
assert.equal(beatDurSec({ dur: 0 }, 120), null);
|
||||
assert.equal(beatDurSec({ dur: 4 }, null), null);
|
||||
});
|
||||
|
||||
function measure(idx, t, opts = {}) {
|
||||
return { idx, t, ...opts };
|
||||
}
|
||||
|
||||
test('flattenNotation: basic two-hand flatten, sorted, durSec from tempo', () => {
|
||||
const { flattenNotation } = load();
|
||||
const notes = flattenNotation([
|
||||
{
|
||||
idx: 1, t: 0, tempo: 120,
|
||||
staves: {
|
||||
rh: { voices: [{ v: 1, beats: [
|
||||
{ t: 0.5, dur: 4, notes: [{ midi: 64 }] },
|
||||
{ t: 1.0, dur: 8, notes: [{ midi: 67 }] },
|
||||
] }] },
|
||||
lh: { voices: [{ v: 1, beats: [
|
||||
{ t: 0.0, dur: 2, notes: [{ midi: 48 }] },
|
||||
] }] },
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.equal(notes.length, 3);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(notes.map(n => n.midi))), [48, 64, 67]); // time-sorted
|
||||
assert.equal(notes[0].hand, 'lh');
|
||||
assert.equal(notes[0].durSec, 1.0); // half at 120
|
||||
assert.equal(notes[1].durSec, 0.5); // quarter
|
||||
assert.equal(notes[2].durSec, 0.25); // eighth
|
||||
assert.equal(notes[1].measureIdx, 1);
|
||||
});
|
||||
|
||||
test('flattenNotation: tempo state carries across measures and changes apply', () => {
|
||||
const { flattenNotation } = load();
|
||||
const notes = flattenNotation([
|
||||
measure(1, 0, { tempo: 120, staves: { rh: { voices: [{ v: 1, beats: [{ t: 0, dur: 4, notes: [{ midi: 60 }] }] }] } } }),
|
||||
measure(2, 2, { staves: { rh: { voices: [{ v: 1, beats: [{ t: 2, dur: 4, notes: [{ midi: 62 }] }] }] } } }),
|
||||
measure(3, 4, { tempo: 60, staves: { rh: { voices: [{ v: 1, beats: [{ t: 4, dur: 4, notes: [{ midi: 64 }] }] }] } } }),
|
||||
]);
|
||||
assert.equal(notes[0].durSec, 0.5); // 120 BPM
|
||||
assert.equal(notes[1].durSec, 0.5); // tempo carried
|
||||
assert.equal(notes[2].durSec, 1.0); // 60 BPM
|
||||
});
|
||||
|
||||
test('flattenNotation: tied notes extend instead of emitting a new block', () => {
|
||||
const { flattenNotation } = load();
|
||||
const notes = flattenNotation([
|
||||
{
|
||||
idx: 1, t: 0, tempo: 120,
|
||||
staves: { rh: { voices: [{ v: 1, beats: [
|
||||
{ t: 0.0, dur: 2, notes: [{ midi: 60 }] },
|
||||
{ t: 1.0, dur: 2, notes: [{ midi: 60, tied: true }] },
|
||||
] }] } },
|
||||
},
|
||||
]);
|
||||
assert.equal(notes.length, 1);
|
||||
assert.equal(notes[0].durSec, 2.0); // half + tied half
|
||||
});
|
||||
|
||||
test('flattenNotation: no tempo anywhere falls back to next-onset gap', () => {
|
||||
const { flattenNotation } = load();
|
||||
const notes = flattenNotation([
|
||||
{
|
||||
idx: 1, t: 0,
|
||||
staves: { rh: { voices: [{ v: 1, beats: [
|
||||
{ t: 0.0, dur: 4, notes: [{ midi: 60 }] },
|
||||
{ t: 0.8, dur: 4, notes: [{ midi: 62 }] },
|
||||
] }] } },
|
||||
},
|
||||
]);
|
||||
assert.ok(Math.abs(notes[0].durSec - 0.8) < 1e-9);
|
||||
assert.equal(notes[1].durSec, 2.0); // final-beat fallback
|
||||
});
|
||||
|
||||
test('flattenNotation: overlap clamp against next same-hand same-midi onset', () => {
|
||||
const { flattenNotation } = load();
|
||||
const notes = flattenNotation([
|
||||
{
|
||||
idx: 1, t: 0, tempo: 30, // whole note = 8s — way past the next onset
|
||||
staves: { rh: { voices: [{ v: 1, beats: [
|
||||
{ t: 0.0, dur: 1, notes: [{ midi: 60 }] },
|
||||
{ t: 1.0, dur: 1, notes: [{ midi: 60 }] },
|
||||
] }] } },
|
||||
},
|
||||
]);
|
||||
assert.equal(notes[0].durSec, 1.0); // clamped to next onset
|
||||
});
|
||||
|
||||
test('flattenNotation: rests, malformed beats, and out-of-range midi are skipped', () => {
|
||||
const { flattenNotation } = load();
|
||||
const notes = flattenNotation([
|
||||
{
|
||||
idx: 1, t: 0, tempo: 120,
|
||||
staves: { rh: { voices: [{ v: 1, beats: [
|
||||
{ t: 0.0, dur: 4, rest: true },
|
||||
{ t: 0.5, dur: 4, notes: [{ midi: 200 }] },
|
||||
null,
|
||||
{ t: 1.0, dur: 4, notes: [{ midi: 64 }] },
|
||||
] }] } },
|
||||
},
|
||||
]);
|
||||
assert.equal(notes.length, 1);
|
||||
assert.equal(notes[0].midi, 64);
|
||||
});
|
||||
|
||||
test('keyRange pads and clamps to the 88-key piano, keeps active span explicit', () => {
|
||||
const { keyRange } = load();
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(keyRange([{ midi: 60 }, { midi: 72 }]))),
|
||||
{ low: 58, high: 74, activeLow: 60, activeHigh: 72 },
|
||||
);
|
||||
// At the clamp edges the active span still reflects the chart extremes
|
||||
// (not low+pad — that would mark A0/C8 inactive when actually played).
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(keyRange([{ midi: 21 }, { midi: 108 }]))),
|
||||
{ low: 21, high: 108, activeLow: 21, activeHigh: 108 },
|
||||
);
|
||||
const empty = keyRange([]);
|
||||
assert.ok(empty.low < 60 && empty.high > 60);
|
||||
assert.ok(empty.activeLow > empty.activeHigh, 'empty chart has an empty active span');
|
||||
});
|
||||
|
||||
test('noteLetter maps midi to pitch-class letters', () => {
|
||||
const { noteLetter } = load();
|
||||
assert.equal(noteLetter(60), 'C');
|
||||
assert.equal(noteLetter(61), 'C#');
|
||||
assert.equal(noteLetter(69), 'A');
|
||||
assert.equal(noteLetter(71), 'B');
|
||||
assert.equal(noteLetter(72), 'C'); // octave wraps
|
||||
assert.equal(noteLetter(21), 'A'); // A0
|
||||
});
|
||||
|
||||
test('scrollZ: events sit at hitZ exactly at their time and approach from -Z', () => {
|
||||
const { scrollZ } = load();
|
||||
const hitZ = -0.5, speed = 2.0;
|
||||
// At now === eventT the event is exactly on the hit-line.
|
||||
assert.equal(scrollZ(10, 10, hitZ, speed), hitZ);
|
||||
// 1s before its time it is `speed` units further away (towards -Z).
|
||||
assert.equal(scrollZ(10, 9, hitZ, speed), hitZ - speed);
|
||||
// After its time it has moved past the hit-line (towards +Z).
|
||||
assert.equal(scrollZ(10, 11, hitZ, speed), hitZ + speed);
|
||||
// Marker and note-front-edge maths agree by construction: a note of
|
||||
// length L positioned at scrollZ(t) - L/2 has its front edge at
|
||||
// scrollZ(t).
|
||||
const len = 0.8;
|
||||
assert.equal(scrollZ(10, 10, hitZ, speed) - len / 2 + len / 2, hitZ);
|
||||
});
|
||||
|
||||
test('measureMarkers extracts idx/t pairs', () => {
|
||||
const { measureMarkers } = load();
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(measureMarkers([{ idx: 1, t: 0 }, { idx: 2, t: 2.5 }, { bogus: true }]))),
|
||||
[{ idx: 1, t: 0 }, { idx: 2, t: 2.5 }],
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
// FX-settings scaffold tests (guitar-highway parity controls). Same bare-vm
|
||||
// harness as data_layer.test.js — no DOM, no localStorage — which doubles as
|
||||
// a lint that the new module-scope FX code stays side-effect safe.
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
function load(extraWindow) {
|
||||
const window = {
|
||||
console,
|
||||
location: { protocol: 'http:', host: 'localhost' },
|
||||
slopsmith: {},
|
||||
...extraWindow,
|
||||
};
|
||||
window.window = window;
|
||||
window.globalThis = window;
|
||||
const context = vm.createContext(window);
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'screen.js'), 'utf8');
|
||||
vm.runInContext(src, context, { filename: 'screen.js' });
|
||||
return window;
|
||||
}
|
||||
|
||||
test('readFxSettings: defaults survive a localStorage-less environment', () => {
|
||||
const { readFxSettings, FX_DEFAULTS } = load().slopsmithViz_keys_highway_3d.__test;
|
||||
assert.deepEqual(readFxSettings(), FX_DEFAULTS);
|
||||
assert.equal(FX_DEFAULTS.bloom, true); // effects on by default
|
||||
});
|
||||
|
||||
test('readFxSettings: reads keys3d_bg_* overrides and coerces types', () => {
|
||||
const store = { keys3d_bg_bloom: '0' };
|
||||
const win = load({
|
||||
localStorage: {
|
||||
getItem: (k) => (k in store ? store[k] : null),
|
||||
setItem: (k, v) => { store[k] = v; },
|
||||
},
|
||||
});
|
||||
const { readFxSettings } = win.slopsmithViz_keys_highway_3d.__test;
|
||||
assert.equal(readFxSettings().bloom, false);
|
||||
store.keys3d_bg_bloom = 'true';
|
||||
assert.equal(readFxSettings().bloom, true);
|
||||
store.keys3d_bg_bloom = 'false';
|
||||
assert.equal(readFxSettings().bloom, false);
|
||||
// Corrupt/foreign value → keep the default rather than silently
|
||||
// disabling the effect.
|
||||
store.keys3d_bg_bloom = 'banana';
|
||||
assert.equal(readFxSettings().bloom, true);
|
||||
});
|
||||
|
||||
test('keys3dSetFx: persists, coerces, and ignores unknown keys', () => {
|
||||
const store = {};
|
||||
const events = [];
|
||||
const win = load({
|
||||
localStorage: {
|
||||
getItem: (k) => (k in store ? store[k] : null),
|
||||
setItem: (k, v) => { store[k] = v; },
|
||||
},
|
||||
dispatchEvent: (ev) => { events.push(ev); return true; },
|
||||
CustomEvent: class CustomEvent {
|
||||
constructor(type, opts) { this.type = type; this.detail = opts && opts.detail; }
|
||||
},
|
||||
});
|
||||
win.keys3dSetFx('bloom', false);
|
||||
assert.equal(store.keys3d_bg_bloom, '0');
|
||||
// String forms round-trip like the reader's accepted representations.
|
||||
win.keys3dSetFx('bloom', 'false');
|
||||
assert.equal(store.keys3d_bg_bloom, '0');
|
||||
win.keys3dSetFx('bloom', 'true');
|
||||
assert.equal(store.keys3d_bg_bloom, '1');
|
||||
win.keys3dSetFx('bloom', false);
|
||||
assert.equal(events.length, 4);
|
||||
assert.equal(events[0].type, 'keys3d:settings');
|
||||
// Field-wise (the detail object was built inside the vm realm, so a
|
||||
// deep-strict compare would trip on its foreign Object.prototype).
|
||||
assert.equal(events[0].detail.fx.bloom, false);
|
||||
assert.deepEqual(Object.keys(events[0].detail.fx), ['bloom']);
|
||||
// Unknown key: no write, no event.
|
||||
win.keys3dSetFx('nonsense', 1);
|
||||
assert.equal(events.length, 4);
|
||||
assert.ok(!('keys3d_bg_nonsense' in store));
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
// Pure MIDI-scoring tests: load screen.js in a bare vm window and exercise
|
||||
// the __test exports (no DOM, no WebGL, no MIDI device, no network).
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
function load() {
|
||||
const window = {
|
||||
console,
|
||||
location: { protocol: 'http:', host: 'localhost' },
|
||||
slopsmith: {},
|
||||
};
|
||||
window.window = window;
|
||||
window.globalThis = window;
|
||||
const context = vm.createContext(window);
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'screen.js'), 'utf8');
|
||||
vm.runInContext(src, context, { filename: 'screen.js' });
|
||||
return window.slopsmithViz_keys_highway_3d.__test;
|
||||
}
|
||||
|
||||
const TOL = 0.10;
|
||||
|
||||
test('accuracyOf/scoreOf mirror the notedetect stats formula', () => {
|
||||
const { accuracyOf, scoreOf } = load();
|
||||
// accuracy = hits / max(1, hits + misses)
|
||||
assert.equal(accuracyOf(0, 0), 0);
|
||||
assert.equal(accuracyOf(10, 0), 1);
|
||||
assert.equal(accuracyOf(3, 1), 0.75);
|
||||
// score = round(hits * 100 * accuracy)
|
||||
assert.equal(scoreOf(0, 0), 0);
|
||||
assert.equal(scoreOf(10, 0), 1000);
|
||||
assert.equal(scoreOf(3, 1), Math.round(3 * 100 * 0.75));
|
||||
// Monotonic in accuracy at fixed hits.
|
||||
assert.ok(scoreOf(5, 0) > scoreOf(5, 5));
|
||||
});
|
||||
|
||||
test('judgeHit: exact note inside ±0.10 s window hits; outside misses', () => {
|
||||
const { judgeHit } = load();
|
||||
const notes = [
|
||||
{ midi: 60, t: 1.0 },
|
||||
{ midi: 64, t: 1.0 },
|
||||
{ midi: 62, t: 2.0 },
|
||||
];
|
||||
const hitKeys = new Set();
|
||||
// On time.
|
||||
assert.equal(judgeHit(notes, 60, 1.0, hitKeys, TOL), '1.000|60');
|
||||
// Near the edge of the window (the exact ±0.10 boundary is float-
|
||||
// representation dependent, same as the piano plugin).
|
||||
assert.equal(judgeHit(notes, 62, 2.099, hitKeys, TOL), '2.000|62');
|
||||
assert.equal(judgeHit(notes, 64, 0.901, hitKeys, TOL), '1.000|64');
|
||||
// Just outside the window.
|
||||
assert.equal(judgeHit(notes, 60, 1.11, hitKeys, TOL), null);
|
||||
// Wrong note — nothing at that midi anywhere near.
|
||||
assert.equal(judgeHit(notes, 65, 1.0, hitKeys, TOL), null);
|
||||
});
|
||||
|
||||
test('judgeHit: dedupes by t|midi — a chart note can only be hit once', () => {
|
||||
const { judgeHit } = load();
|
||||
const notes = [{ midi: 60, t: 1.0 }, { midi: 60, t: 1.15 }];
|
||||
const hitKeys = new Set();
|
||||
const first = judgeHit(notes, 60, 1.02, hitKeys, TOL);
|
||||
assert.equal(first, '1.000|60');
|
||||
hitKeys.add(first);
|
||||
// Second strike near the same time falls through to the NEXT un-hit
|
||||
// chart note at the same midi (double-stop repeats).
|
||||
const second = judgeHit(notes, 60, 1.06, hitKeys, TOL);
|
||||
assert.equal(second, '1.150|60');
|
||||
hitKeys.add(second);
|
||||
// Third strike: both consumed → wrong note.
|
||||
assert.equal(judgeHit(notes, 60, 1.1, hitKeys, TOL), null);
|
||||
});
|
||||
|
||||
test('judgeHit: empty/absent chart never judges', () => {
|
||||
const { judgeHit } = load();
|
||||
assert.equal(judgeHit([], 60, 1.0, new Set(), TOL), null);
|
||||
assert.equal(judgeHit(null, 60, 1.0, new Set(), TOL), null);
|
||||
});
|
||||
|
||||
test('sweepMissed: marks elapsed unhit notes once, respects hit + floor', () => {
|
||||
const { sweepMissed, noteKey } = load();
|
||||
const notes = [
|
||||
{ midi: 60, t: 1.0 },
|
||||
{ midi: 62, t: 1.5 },
|
||||
{ midi: 64, t: 5.0 },
|
||||
];
|
||||
const hitKeys = new Set([noteKey(1.0, 60)]); // 60@1.0 was hit
|
||||
const missedKeys = new Set();
|
||||
const missed = [];
|
||||
// At t=2.0 the windows for 1.0 and 1.5 have elapsed; 5.0 is pending.
|
||||
const n1 = sweepMissed(notes, 2.0, hitKeys, missedKeys, TOL, null, n => missed.push(n.midi));
|
||||
assert.equal(n1, 1);
|
||||
assert.deepEqual(missed, [62]);
|
||||
assert.ok(missedKeys.has(noteKey(1.5, 62)));
|
||||
// Sweeping again counts nothing new (idempotent per note).
|
||||
assert.equal(sweepMissed(notes, 2.1, hitKeys, missedKeys, TOL, null), 0);
|
||||
// Floor: a device connected at t=6 must not retro-miss the 5.0 note.
|
||||
const hk2 = new Set(), mk2 = new Set();
|
||||
assert.equal(sweepMissed(notes, 6.0, hk2, mk2, TOL, 6.0), 0);
|
||||
});
|
||||
|
||||
test('sweepMissed: a note exactly at the connect floor is not retro-missed', () => {
|
||||
// Off-by-one guard: floor is the connect instant; a note whose onset
|
||||
// equals it (device connected exactly as the onset passed) must be
|
||||
// excluded, not swept. Floor comparison is `<=`, not `<`.
|
||||
const { sweepMissed } = load();
|
||||
const notes = [{ midi: 60, t: 5.0 }, { midi: 62, t: 6.0 }];
|
||||
const missedKeys = new Set();
|
||||
const missed = [];
|
||||
const n = sweepMissed(notes, 7.0, new Set(), missedKeys, TOL, 5.0,
|
||||
m => missed.push(m.midi));
|
||||
assert.equal(n, 1);
|
||||
assert.deepEqual(missed, [62]);
|
||||
});
|
||||
|
||||
test('sweepMissed: a long frame stall cannot let elapsed notes slip past', () => {
|
||||
const { sweepMissed } = load();
|
||||
const notes = [{ midi: 60, t: 1.0 }, { midi: 62, t: 3.0 }];
|
||||
const missedKeys = new Set();
|
||||
// The previous sweep ran at t≈0; the next runs 10 s later (backgrounded
|
||||
// tab / render hitch). Both elapsed notes must still be counted.
|
||||
assert.equal(sweepMissed(notes, 10.0, new Set(), missedKeys, TOL, null), 2);
|
||||
});
|
||||
|
||||
test('sweepMissed: cursor advances monotonically and never recounts', () => {
|
||||
const { sweepMissed } = load();
|
||||
const notes = [
|
||||
{ midi: 60, t: 1.0 },
|
||||
{ midi: 62, t: 2.0 },
|
||||
{ midi: 64, t: 9.0 },
|
||||
];
|
||||
const hitKeys = new Set(), missedKeys = new Set();
|
||||
const cursor = { idx: 0 };
|
||||
assert.equal(sweepMissed(notes, 1.5, hitKeys, missedKeys, TOL, null, null, cursor), 1);
|
||||
assert.equal(cursor.idx, 1);
|
||||
// Stall to t=8: the 2.0 note is counted exactly once from the cursor.
|
||||
assert.equal(sweepMissed(notes, 8.0, hitKeys, missedKeys, TOL, null, null, cursor), 1);
|
||||
assert.equal(cursor.idx, 2);
|
||||
// Seek BACKWARDS: the cursor does not rewind, nothing is recounted.
|
||||
assert.equal(sweepMissed(notes, 1.5, hitKeys, missedKeys, TOL, null, null, cursor), 0);
|
||||
// The cursor still advances past pre-floor notes without counting them.
|
||||
const c2 = { idx: 0 };
|
||||
const mk2 = new Set();
|
||||
assert.equal(sweepMissed(notes, 8.0, new Set(), mk2, TOL, 5.0, null, c2), 0);
|
||||
assert.equal(c2.idx, 2);
|
||||
});
|
||||
|
||||
test('noteKey quantises time to ms so float drift cannot double-count', () => {
|
||||
const { noteKey } = load();
|
||||
assert.equal(noteKey(1.0004, 60), noteKey(1.0001, 60));
|
||||
assert.notEqual(noteKey(1.002, 60), noteKey(1.0001, 60));
|
||||
assert.notEqual(noteKey(1.0, 60), noteKey(1.0, 61));
|
||||
});
|
||||
+1
-12
@@ -9777,12 +9777,6 @@ async function startSongCountIn() {
|
||||
|
||||
// Time display + highway sync
|
||||
let lastAudioTime = 0;
|
||||
// hud-time write cache: the 60 Hz tick below used to rewrite textContent
|
||||
// (and getElementById) every tick even though the mm:ss display only
|
||||
// changes once a second — each write invalidates layout. Write-on-change
|
||||
// with a cached element ref (re-resolved if detached).
|
||||
let _hudTimeEl = null;
|
||||
let _hudTimeLast = '';
|
||||
setInterval(() => {
|
||||
let ct = _audioTime();
|
||||
const dur = _audioDuration();
|
||||
@@ -9810,12 +9804,7 @@ setInterval(() => {
|
||||
ct = lastAudioTime;
|
||||
}
|
||||
lastAudioTime = ct;
|
||||
const hudText = `${formatTime(ct)} / ${formatTime(dur)}`;
|
||||
if (hudText !== _hudTimeLast) {
|
||||
if (!_hudTimeEl || !_hudTimeEl.isConnected) _hudTimeEl = document.getElementById('hud-time');
|
||||
if (_hudTimeEl) _hudTimeEl.textContent = hudText;
|
||||
_hudTimeLast = hudText;
|
||||
}
|
||||
document.getElementById('hud-time').textContent = `${formatTime(ct)} / ${formatTime(dur)}`;
|
||||
if (dur) {
|
||||
_maybeRefreshSectionPracticeDuration(dur);
|
||||
}
|
||||
|
||||
+92
-169
@@ -122,24 +122,6 @@ function createHighway() {
|
||||
// where offsetParent === null isn't enough.
|
||||
let _visibleOverride = null;
|
||||
let _lastVisible = null;
|
||||
// Throttled DOM visibility sampling. Reading canvas.offsetParent
|
||||
// every rAF frame forces a style/layout recalc — profiled at ~0.5 s
|
||||
// main-thread self-time over a 63 s session. The displayed state
|
||||
// changes rarely (navigate / splitscreen panel toggle), so the DOM
|
||||
// is only re-sampled every _DOM_VIS_CHECK_FRAMES frames; the cached
|
||||
// value serves the frames in between (worst-case transition latency
|
||||
// ~10 frames ≈ 166 ms at 60 Hz — fine for a hide/show pause signal).
|
||||
// Set _domVisSampledFrame to NaN to force a fresh sample on the next
|
||||
// check (done on init, canvas replace, resize, and override-clear so
|
||||
// deliberate transitions don't wait out the throttle window).
|
||||
// NOTE those manual resets are LATENCY optimizations, not correctness
|
||||
// requirements: the periodic re-sample runs every _DOM_VIS_CHECK_FRAMES
|
||||
// frames regardless, so a visibility-affecting path that forgets to
|
||||
// reset self-heals within ~10 frames — stale visibility can never be
|
||||
// served indefinitely.
|
||||
const _DOM_VIS_CHECK_FRAMES = 10;
|
||||
let _domVisCached = false;
|
||||
let _domVisSampledFrame = NaN;
|
||||
let animFrame = null;
|
||||
// Paused-render throttle (feedBack#654). The rAF loop runs
|
||||
// unconditionally and only gates on visibility + ready, never on
|
||||
@@ -751,121 +733,103 @@ function createHighway() {
|
||||
// on the freshly-mounted canvas.
|
||||
let _currentCanvasContextType = '2d';
|
||||
|
||||
// One persistent bundle object per createHighway() instance —
|
||||
// _makeBundle() mutates its fields in place each call instead of
|
||||
// allocating a fresh ~35-field object per rAF frame (steady GC churn
|
||||
// on weak hardware, ×N under splitscreen). Consequences for
|
||||
// consumers: the bundle OBJECT's identity is stable across frames
|
||||
// and carries no meaning; its field values are only valid for the
|
||||
// duration of the current draw call. Array fields (`notes`,
|
||||
// `chords`, `handShapes`, `chordTemplates`, ...) still swap
|
||||
// reference whenever chart data changes — field-identity caches
|
||||
// (e.g. highway_3d's merge caches) rely on that invariant.
|
||||
const _bundleReused = {};
|
||||
|
||||
function _makeBundle() {
|
||||
// Snapshot of current factory state passed to each renderer call.
|
||||
// Arrays and songInfo are LIVE references, not copies — the
|
||||
// bundle's `notes`, `chords`, `anchors`, `beats`, etc. point at
|
||||
// closure state. Renderers MUST NOT mutate these; treat them as
|
||||
// read-only. We don't Object.freeze or deep-copy for per-frame
|
||||
// cost reasons.
|
||||
const b = _bundleReused;
|
||||
// Timing
|
||||
b.currentTime = currentTime;
|
||||
b.songInfo = songInfo;
|
||||
b.isReady = ready;
|
||||
// True while the chart clock is actively advancing; false when
|
||||
// audio is paused / stalled / mid-seek (setTime has kept getting
|
||||
// the same t for > _CHART_MAX_INTERP_MS). This is the same
|
||||
// predicate getTime() uses to decide raw-vs-interpolated, and the
|
||||
// no-anchor boot state reads as not-playing — matching getTime()
|
||||
// returning raw chartTime there. Renderers that run their own
|
||||
// sub-frame clock (highway_3d's smoothNow) gate on this to fall
|
||||
// back to raw instead of extrapolating forward against a frozen
|
||||
// audio sample. Undefined on downlevel hosts → those renderers
|
||||
// keep their own staleness-based fallback.
|
||||
b.isPlaying = !Number.isNaN(_chartAnchorPerfNow)
|
||||
&& (performance.now() - _chartLastAdvanceAt) <= _CHART_MAX_INTERP_MS;
|
||||
// Arrays and songInfo are LIVE references, not copies — the bundle
|
||||
// itself is rebuilt each frame but its `notes`, `chords`,
|
||||
// `anchors`, `beats`, etc. point at closure state. Renderers
|
||||
// MUST NOT mutate these; treat them as read-only. We don't
|
||||
// Object.freeze or deep-copy for per-frame allocation cost reasons.
|
||||
return {
|
||||
// Timing
|
||||
currentTime,
|
||||
songInfo,
|
||||
isReady: ready,
|
||||
// True while the chart clock is actively advancing; false when
|
||||
// audio is paused / stalled / mid-seek (setTime has kept getting
|
||||
// the same t for > _CHART_MAX_INTERP_MS). This is the same
|
||||
// predicate getTime() uses to decide raw-vs-interpolated, and the
|
||||
// no-anchor boot state reads as not-playing — matching getTime()
|
||||
// returning raw chartTime there. Renderers that run their own
|
||||
// sub-frame clock (highway_3d's smoothNow) gate on this to fall
|
||||
// back to raw instead of extrapolating forward against a frozen
|
||||
// audio sample. Undefined on downlevel hosts → those renderers
|
||||
// keep their own staleness-based fallback.
|
||||
isPlaying: !Number.isNaN(_chartAnchorPerfNow)
|
||||
&& (performance.now() - _chartLastAdvanceAt) <= _CHART_MAX_INTERP_MS,
|
||||
|
||||
// Chart content (filter-aware — difficulty-filtered arrays
|
||||
// preferred; raw arrays are the fallback when no ladder data).
|
||||
b.notes = _filteredNotes !== null ? _filteredNotes : notes;
|
||||
b.chords = _filteredChords !== null ? _filteredChords : chords;
|
||||
b.anchors = _filteredAnchors !== null ? _filteredAnchors : anchors;
|
||||
b.beats = beats;
|
||||
b.sections = sections;
|
||||
b.chordTemplates = chordTemplates;
|
||||
b.stringCount = stringCount;
|
||||
// Mirrors song_info tuning capo offsets (±semitones from the
|
||||
// instrument’s standard open-string layout). Live reference.
|
||||
b.tuning = songInfo?.tuning;
|
||||
b.capo = songInfo?.capo;
|
||||
b.lyrics = lyrics;
|
||||
b.lyricsSource = lyricsSource;
|
||||
b.toneChanges = toneChanges;
|
||||
b.toneBase = toneBase;
|
||||
// Drum tab payload (or null when the active arrangement has
|
||||
// no drum_tab). Live reference — renderers MUST treat as
|
||||
// read-only. Plugins should prefer this over decoding the
|
||||
// standard `notes` stream when present; absence is the
|
||||
// signal to fall back to legacy MIDI-encoded drums.
|
||||
b.drumTab = drumTab;
|
||||
// Chart content (filter-aware — difficulty-filtered arrays
|
||||
// preferred; raw arrays are the fallback when no ladder data).
|
||||
notes: _filteredNotes !== null ? _filteredNotes : notes,
|
||||
chords: _filteredChords !== null ? _filteredChords : chords,
|
||||
anchors: _filteredAnchors !== null ? _filteredAnchors : anchors,
|
||||
beats,
|
||||
sections,
|
||||
chordTemplates,
|
||||
stringCount,
|
||||
// Mirrors song_info tuning capo offsets (±semitones from the
|
||||
// instrument’s standard open-string layout). Live reference.
|
||||
tuning: songInfo?.tuning,
|
||||
capo: songInfo?.capo,
|
||||
lyrics,
|
||||
lyricsSource,
|
||||
toneChanges,
|
||||
toneBase,
|
||||
// Drum tab payload (or null when the active arrangement has
|
||||
// no drum_tab). Live reference — renderers MUST treat as
|
||||
// read-only. Plugins should prefer this over decoding the
|
||||
// standard `notes` stream when present; absence is the
|
||||
// signal to fall back to legacy MIDI-encoded drums.
|
||||
drumTab,
|
||||
|
||||
// Master-difficulty (feedBack#48)
|
||||
b.mastery = _mastery;
|
||||
b.hasPhraseData = !!(_phrases && _phrases.length > 0);
|
||||
// When phrase data authored ANY handshape, respect the filtered
|
||||
// list strictly (even when this difficulty leaves it empty) —
|
||||
// otherwise low-mastery levels would surface arp hints that
|
||||
// don't belong. Only fall back to the flat list when the
|
||||
// phrase data carries no handshapes at all (common on DLC
|
||||
// where handshapes ship on the arrangement root).
|
||||
b.handShapes = (_filteredHandShapes !== null && _phrasesHaveHandShapes)
|
||||
? _filteredHandShapes
|
||||
: handShapes;
|
||||
// Master-difficulty (feedBack#48)
|
||||
mastery: _mastery,
|
||||
hasPhraseData: !!(_phrases && _phrases.length > 0),
|
||||
// When phrase data authored ANY handshape, respect the filtered
|
||||
// list strictly (even when this difficulty leaves it empty) —
|
||||
// otherwise low-mastery levels would surface arp hints that
|
||||
// don't belong. Only fall back to the flat list when the
|
||||
// phrase data carries no handshapes at all (common on DLC
|
||||
// where handshapes ship on the arrangement root).
|
||||
handShapes: (_filteredHandShapes !== null && _phrasesHaveHandShapes)
|
||||
? _filteredHandShapes
|
||||
: handShapes,
|
||||
|
||||
// Display flags
|
||||
b.inverted = _inverted;
|
||||
b.lefty = _lefty;
|
||||
b.renderScale = _effectiveRenderScale();
|
||||
b.lyricsVisible = showLyrics;
|
||||
// Teaching marks sd/ch overlay pref (§6.2.2) so custom renderers
|
||||
// (e.g. the 3D highway) can mirror the 2D opt-in toggle. The fg
|
||||
// finger-hint pref rides alongside (default on, independently hideable).
|
||||
b.teachingMarksVisible = _showTeachingMarks;
|
||||
b.fingerHintsVisible = _showFingerHints;
|
||||
// Display flags
|
||||
inverted: _inverted,
|
||||
lefty: _lefty,
|
||||
renderScale: _effectiveRenderScale(),
|
||||
lyricsVisible: showLyrics,
|
||||
// Teaching marks sd/ch overlay pref (§6.2.2) so custom renderers
|
||||
// (e.g. the 3D highway) can mirror the 2D opt-in toggle. The fg
|
||||
// finger-hint pref rides alongside (default on, independently hideable).
|
||||
teachingMarksVisible: _showTeachingMarks,
|
||||
fingerHintsVisible: _showFingerHints,
|
||||
|
||||
// 2D-style helpers (renderers that don't need these can ignore).
|
||||
// `fillTextUnmirrored` is deliberately NOT exposed here —
|
||||
// the factory-level version writes to the default renderer's
|
||||
// closure ctx, which is null for custom renderers. Renderers
|
||||
// that need lefty-aware text should check `bundle.lefty` and
|
||||
// apply the mirror transform themselves on their own context.
|
||||
b.project = project;
|
||||
b.fretX = fretX;
|
||||
// Windowed-iteration helpers (stable references): lower-bound
|
||||
// binary searches so custom viz don't full-scan chart arrays per
|
||||
// frame. lowerBoundT keys on `.t` (notes / chords); lowerBoundTime
|
||||
// keys on `.time` (beats / anchors / sections).
|
||||
b.lowerBoundT = bsearch;
|
||||
b.lowerBoundTime = bsearchTime;
|
||||
// 2D-style helpers (renderers that don't need these can ignore).
|
||||
// `fillTextUnmirrored` is deliberately NOT exposed here —
|
||||
// the factory-level version writes to the default renderer's
|
||||
// closure ctx, which is null for custom renderers. Renderers
|
||||
// that need lefty-aware text should check `bundle.lefty` and
|
||||
// apply the mirror transform themselves on their own context.
|
||||
project,
|
||||
fretX,
|
||||
|
||||
// Per-note judgment overlay (feedBack#254). Renderers call
|
||||
// this per visible note / chord-note to find out whether a
|
||||
// scorer (note_detect) has flagged it hit / actively-held /
|
||||
// missed, so the gem itself can light up instead of relying
|
||||
// on an overlay ring. Returns null when no provider is set
|
||||
// or it reports nothing for this note; otherwise
|
||||
// { state: 'hit'|'active'|'miss', alpha: 0..1, color: string|null }.
|
||||
b.getNoteState = _noteState; // stable reference
|
||||
// Lets custom renderers (e.g. highway_3d) tell "is a provider
|
||||
// attached" apart from "no provider, getNoteState always
|
||||
// returns null" — `getNoteState` always exists on the bundle
|
||||
// so its presence alone isn't a useful "detect mode" signal.
|
||||
// Renderers gate verdict-window cull / draw extensions on this.
|
||||
b.getNoteStateProvider = _getNoteStateProvider; // stable — see above
|
||||
return b;
|
||||
// Per-note judgment overlay (feedBack#254). Renderers call
|
||||
// this per visible note / chord-note to find out whether a
|
||||
// scorer (note_detect) has flagged it hit / actively-held /
|
||||
// missed, so the gem itself can light up instead of relying
|
||||
// on an overlay ring. Returns null when no provider is set
|
||||
// or it reports nothing for this note; otherwise
|
||||
// { state: 'hit'|'active'|'miss', alpha: 0..1, color: string|null }.
|
||||
getNoteState: _noteState, // stable reference — no per-frame allocation
|
||||
// Lets custom renderers (e.g. highway_3d) tell "is a provider
|
||||
// attached" apart from "no provider, getNoteState always
|
||||
// returns null" — `getNoteState` always exists on the bundle
|
||||
// so its presence alone isn't a useful "detect mode" signal.
|
||||
// Renderers gate verdict-window cull / draw extensions on this.
|
||||
getNoteStateProvider: _getNoteStateProvider, // stable — see above
|
||||
};
|
||||
}
|
||||
|
||||
const _defaultRenderer = {
|
||||
@@ -1083,7 +1047,6 @@ function createHighway() {
|
||||
// suppress the first transition. Reset to null so the next
|
||||
// rAF tick re-emits unconditionally.
|
||||
_lastVisible = null;
|
||||
_domVisSampledFrame = NaN; // fresh canvas → fresh DOM sample
|
||||
// Defensive notify for plugins / overlays that cache the
|
||||
// canvas element across events. Lazy lookups via
|
||||
// getElementById('highway') do not need this — they'll pick
|
||||
@@ -1283,14 +1246,7 @@ function createHighway() {
|
||||
// hosts that need those use setVisible() instead.
|
||||
function _isHighwayVisible() {
|
||||
if (_visibleOverride !== null) return _visibleOverride;
|
||||
// Throttled offsetParent read — see _DOM_VIS_CHECK_FRAMES above.
|
||||
if (Number.isNaN(_domVisSampledFrame)
|
||||
|| ((_frameIdx - _domVisSampledFrame) | 0) >= _DOM_VIS_CHECK_FRAMES
|
||||
|| ((_frameIdx - _domVisSampledFrame) | 0) < 0) {
|
||||
_domVisCached = !!(canvas && canvas.offsetParent !== null);
|
||||
_domVisSampledFrame = _frameIdx;
|
||||
}
|
||||
return _domVisCached;
|
||||
return !!(canvas && canvas.offsetParent !== null);
|
||||
}
|
||||
|
||||
// Emit only on transition so renderer-side listeners aren't woken
|
||||
@@ -1557,13 +1513,7 @@ function createHighway() {
|
||||
}
|
||||
|
||||
function drawBeats(W, H) {
|
||||
// Window the beat scan — a long song carries thousands of beats
|
||||
// and iterating (and projecting) all of them per frame was pure
|
||||
// waste; project() culling stays as the safety net.
|
||||
const lo = bsearchTime(beats, currentTime - 0.25);
|
||||
const hi = bsearchTime(beats, currentTime + VISIBLE_SECONDS + 0.25);
|
||||
for (let i = lo; i < hi; i++) {
|
||||
const beat = beats[i];
|
||||
for (const beat of beats) {
|
||||
const tOff = beat.time - currentTime;
|
||||
const p = project(tOff);
|
||||
if (!p || p.scale < 0.06) continue;
|
||||
@@ -2674,19 +2624,6 @@ function createHighway() {
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
// Lower-bound binary search for `.time`-keyed arrays (beats, anchors,
|
||||
// sections) — bsearch/bsearchChords key on `.t` and would compare
|
||||
// against undefined here. Exposed to custom viz as
|
||||
// bundle.lowerBoundTime.
|
||||
function bsearchTime(arr, time) {
|
||||
let lo = 0, hi = arr.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (arr[mid].time < time) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
|
||||
// ── Chord rendering — chains, frames, fretline preview (feedBack#88) ──
|
||||
//
|
||||
@@ -3019,7 +2956,6 @@ function createHighway() {
|
||||
init(canvasEl, container) {
|
||||
canvas = canvasEl;
|
||||
_resizeContainer = container || null;
|
||||
_domVisSampledFrame = NaN; // new mount → fresh DOM sample
|
||||
// Size the canvas BEFORE installing the renderer so
|
||||
// _setRenderer's init/resize calls see the real dimensions
|
||||
// instead of the default 300x150 backing store. Otherwise
|
||||
@@ -3061,9 +2997,6 @@ function createHighway() {
|
||||
|
||||
resize() {
|
||||
if (!canvas) return;
|
||||
// Layout just changed (window resize / container swap) —
|
||||
// re-sample DOM visibility on the next check.
|
||||
_domVisSampledFrame = NaN;
|
||||
let w, h;
|
||||
if (_resizeContainer) {
|
||||
const rect = _resizeContainer.getBoundingClientRect();
|
||||
@@ -3838,10 +3771,6 @@ function createHighway() {
|
||||
// rAF tick.
|
||||
setVisible(v) {
|
||||
_visibleOverride = (v === null || v === undefined) ? null : !!v;
|
||||
// Clearing the override resumes DOM-based detection — force a
|
||||
// fresh offsetParent sample so the resulting transition (if
|
||||
// any) emits now, not after the throttle window.
|
||||
if (_visibleOverride === null) _domVisSampledFrame = NaN;
|
||||
_emitVisibilityIfChanged();
|
||||
},
|
||||
// Snapshot of the current visibility state (the override if
|
||||
@@ -3850,12 +3779,6 @@ function createHighway() {
|
||||
// can call this once to sync their initial state — the event
|
||||
// is transition-only and won't re-fire for late subscribers.
|
||||
isVisible() {
|
||||
// Force a fresh DOM sample — this is a documented "live DOM
|
||||
// check" for late subscribers seeding initial state, so it
|
||||
// must not serve the rAF loop's throttled cache (up to
|
||||
// ~166 ms stale). Called rarely; the layout-read cost that
|
||||
// motivated the throttle only matters per-frame.
|
||||
_domVisSampledFrame = NaN;
|
||||
return _isHighwayVisible();
|
||||
},
|
||||
getNotes() { return notes; },
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@
|
||||
|
||||
<!-- Hidden file input shared by the navbar "Upload" link. Kept at body
|
||||
level so it stays reachable regardless of which screen is active. -->
|
||||
<input type="file" id="upload-songs-file" accept=".sloppak" multiple class="hidden" onchange="uploadSongs(this.files); this.value=''">
|
||||
<input type="file" id="upload-songs-file" accept=".feedpak,.sloppak" multiple class="hidden" onchange="uploadSongs(this.files); this.value=''">
|
||||
|
||||
<!-- ══ HOME (Hero + Library) ══════════════════════════════════════════ -->
|
||||
<div id="home" class="screen active">
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+14
-78
@@ -24,18 +24,6 @@
|
||||
let rafId = null, running = false;
|
||||
let lastMove = 0, lastUpNext = 0;
|
||||
let openPop = null; // { btn, pop }
|
||||
// Hover state over #player-controls, maintained by mouseenter/mouseleave
|
||||
// (wired in start()). tickIdle previously called matches(':hover') every
|
||||
// rAF frame, which forces a style recalc — profiled hot (feedBack perf).
|
||||
let overControls = false;
|
||||
const _onControlsEnter = () => { overControls = true; };
|
||||
const _onControlsLeave = () => { overControls = false; };
|
||||
// Up-Next pill: cached element refs (re-resolved when detached) and
|
||||
// last-written values, so the 6 Hz recompute only touches the DOM when
|
||||
// something actually changed — unconditional textContent/width writes
|
||||
// re-triggered layout every tick.
|
||||
let upnextEls = null; // { pill, nm, eta, fill }
|
||||
let upnextLast = { name: null, eta: null, prog: -1, hidden: null };
|
||||
|
||||
// ── v3 UI signal + plugin-control slot API ───────────────────────────────
|
||||
// Lets plugins detect v3 (window.feedBack.uiVersion === 'v3') and mount
|
||||
@@ -181,52 +169,28 @@
|
||||
}
|
||||
|
||||
// ── Up Next pill ─────────────────────────────────────────────────────────
|
||||
function _upnextRefs() {
|
||||
// Cache refs; re-resolve only when a node detached (screen re-mount).
|
||||
if (!upnextEls || !upnextEls.pill || !upnextEls.pill.isConnected) {
|
||||
const pill = $('v3-upnext');
|
||||
if (!pill) return null;
|
||||
upnextEls = {
|
||||
pill,
|
||||
nm: $('v3-upnext-name'),
|
||||
eta: $('v3-upnext-eta'),
|
||||
fill: $('v3-upnext-bar-fill'),
|
||||
};
|
||||
// Fresh nodes → forget last-written state so everything re-syncs.
|
||||
upnextLast = { name: null, eta: null, prog: -1, hidden: null };
|
||||
}
|
||||
return upnextEls;
|
||||
}
|
||||
function _upnextSetHidden(els, hidden) {
|
||||
if (upnextLast.hidden === hidden) return;
|
||||
upnextLast.hidden = hidden;
|
||||
els.pill.classList.toggle('hidden', hidden);
|
||||
}
|
||||
function updateUpNext() {
|
||||
const els = _upnextRefs();
|
||||
if (!els) return;
|
||||
const pill = $('v3-upnext');
|
||||
if (!pill) return;
|
||||
// Gated by the core "Show 'Up Next'" pref (Gameplay tab, default ON).
|
||||
if (window.feedBack && window.feedBack.showUpNext === false) { _upnextSetHidden(els, true); return; }
|
||||
if (window.feedBack && window.feedBack.showUpNext === false) { pill.classList.add('hidden'); return; }
|
||||
const hw = window.highway;
|
||||
const secs = (hw && typeof hw.getSections === 'function') ? hw.getSections() : null;
|
||||
const t = (hw && typeof hw.getTime === 'function') ? hw.getTime() : null;
|
||||
if (!Array.isArray(secs) || !secs.length || t == null || isNaN(t)) { _upnextSetHidden(els, true); return; }
|
||||
if (!Array.isArray(secs) || !secs.length || t == null || isNaN(t)) { pill.classList.add('hidden'); return; }
|
||||
let next = null;
|
||||
for (let i = 0; i < secs.length; i++) {
|
||||
if (typeof secs[i].time === 'number' && secs[i].time > t + 0.05) { next = secs[i]; break; }
|
||||
}
|
||||
if (!next) { _upnextSetHidden(els, true); return; }
|
||||
if (!next) { pill.classList.add('hidden'); return; }
|
||||
const dt = Math.max(0, next.time - t);
|
||||
// Coarsened eta (whole seconds from 10 s out, 1 decimal inside) +
|
||||
// write-on-change: drops textContent writes (each a layout pass)
|
||||
// from ~6/s to ~1/s.
|
||||
const name = next.name || '—';
|
||||
const etaText = 'in ' + (dt >= 10 ? Math.round(dt) + '' : dt.toFixed(1)) + 's';
|
||||
if (els.nm && name !== upnextLast.name) { upnextLast.name = name; els.nm.textContent = name; }
|
||||
if (els.eta && etaText !== upnextLast.eta) { upnextLast.eta = etaText; els.eta.textContent = etaText; }
|
||||
const nm = $('v3-upnext-name'), eta = $('v3-upnext-eta');
|
||||
if (nm) nm.textContent = next.name || '—';
|
||||
if (eta) eta.textContent = 'in ' + dt.toFixed(1) + 's';
|
||||
// Progress bar: fraction of the current section elapsed toward `next`.
|
||||
// Previous boundary is the last section at/before now (else song start).
|
||||
if (els.fill) {
|
||||
const fill = $('v3-upnext-bar-fill');
|
||||
if (fill) {
|
||||
let prevT = 0;
|
||||
for (let i = 0; i < secs.length; i++) {
|
||||
if (typeof secs[i].time === 'number' && secs[i].time <= t) prevT = secs[i].time;
|
||||
@@ -234,15 +198,9 @@
|
||||
}
|
||||
const span = next.time - prevT;
|
||||
const prog = span > 0 ? Math.max(0, Math.min(1, (t - prevT) / span)) : 0;
|
||||
const q = Math.round(prog * 1000) / 1000;
|
||||
if (q !== upnextLast.prog) {
|
||||
upnextLast.prog = q;
|
||||
// scaleX is compositor-only — width writes re-ran layout.
|
||||
// Pairs with transform-origin:left on #v3-upnext-bar-fill.
|
||||
els.fill.style.transform = 'scaleX(' + q + ')';
|
||||
}
|
||||
fill.style.width = (prog * 100).toFixed(1) + '%';
|
||||
}
|
||||
_upnextSetHidden(els, false);
|
||||
pill.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// ── Speed visual (bars + chevrons reflect #speed-slider) ──────────────────
|
||||
@@ -272,8 +230,8 @@
|
||||
if (!p) return;
|
||||
const playBtn = $('btn-play');
|
||||
const playing = playBtn && playBtn.getAttribute('aria-pressed') === 'true';
|
||||
// overControls maintained by mouseenter/mouseleave (see start()) —
|
||||
// matches(':hover') here forced a per-frame style recalc.
|
||||
const controls = $('player-controls');
|
||||
const overControls = controls && typeof controls.matches === 'function' && controls.matches(':hover');
|
||||
// Keep the transport up while paused, hovering it, or a popover is open.
|
||||
if (openPop || overControls || !playing) { lastMove = now(); return; }
|
||||
if (now() - lastMove > IDLE_MS) {
|
||||
@@ -291,16 +249,6 @@
|
||||
// Re-sync the lyrics icon so programmatic highway.setLyricsVisible()
|
||||
// (e.g. from lyrics_karaoke) isn't left stale; cheap + idempotent.
|
||||
syncLyricsIcon();
|
||||
// Reconcile the edge-driven hover flag against ground truth at
|
||||
// this throttled cadence (~6 Hz, not per frame). Covers both
|
||||
// failure modes of pure mouseenter/mouseleave tracking: a
|
||||
// missed mouseleave (controls hidden/detached under the
|
||||
// pointer → flag stuck true, transport never auto-hides) and
|
||||
// a re-created #player-controls node whose listeners were
|
||||
// lost (flag stuck false-ish / dead). matches(':hover') on a
|
||||
// detached node is simply false, so this also self-clears.
|
||||
const c = $('player-controls');
|
||||
overControls = !!(c && typeof c.matches === 'function' && c.matches(':hover'));
|
||||
}
|
||||
tickIdle();
|
||||
rafId = requestAnimationFrame(loop);
|
||||
@@ -315,12 +263,6 @@
|
||||
wireRail();
|
||||
p.addEventListener('mousemove', revealChrome);
|
||||
p.addEventListener('touchstart', revealChrome, { passive: true });
|
||||
const c = $('player-controls');
|
||||
if (c) {
|
||||
overControls = typeof c.matches === 'function' && c.matches(':hover');
|
||||
c.addEventListener('mouseenter', _onControlsEnter);
|
||||
c.addEventListener('mouseleave', _onControlsLeave);
|
||||
}
|
||||
const s = $('speed-slider');
|
||||
if (s && !s.dataset.pcVizWired) { s.dataset.pcVizWired = '1'; s.addEventListener('input', updateSpeedViz); }
|
||||
updateSpeedViz();
|
||||
@@ -339,12 +281,6 @@
|
||||
p.removeEventListener('touchstart', revealChrome);
|
||||
p.classList.remove('chrome-active', 'chrome-idle');
|
||||
}
|
||||
const c = $('player-controls');
|
||||
if (c) {
|
||||
c.removeEventListener('mouseenter', _onControlsEnter);
|
||||
c.removeEventListener('mouseleave', _onControlsLeave);
|
||||
}
|
||||
overControls = false;
|
||||
closePop();
|
||||
}
|
||||
function syncActivation() {
|
||||
|
||||
+2
-7
@@ -367,15 +367,10 @@ input, textarea, select,
|
||||
}
|
||||
#player-hud .v3-upnext .v3-upnext-bar-fill {
|
||||
height: 100%;
|
||||
/* Fill is driven via transform: scaleX(0..1) from player-chrome.js —
|
||||
compositor-only, unlike the previous width writes which re-ran
|
||||
layout on every update tick. */
|
||||
width: 100%;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
width: 0%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #22d3ee, #a855f7, #f472b6);
|
||||
transition: transform .12s linear;
|
||||
transition: width .12s linear;
|
||||
}
|
||||
|
||||
/* — Live performance HUD (top-right, read-only) — */
|
||||
|
||||
@@ -62,7 +62,7 @@ test('bundle exposes handShapes to renderers with flat-list fallback', () => {
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
assert.match(
|
||||
src,
|
||||
/\bhandShapes\s*[:=]\s*\([^)]*_filteredHandShapes[^)]*\)\s*\?\s*_filteredHandShapes\s*:\s*handShapes\b/,
|
||||
/\bhandShapes:\s*\([^)]*_filteredHandShapes[^)]*\)\s*\?\s*_filteredHandShapes\s*:\s*handShapes\b/,
|
||||
'bundle must expose handShapes with the _filteredHandShapes-vs-handShapes ternary fallback',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ function src(file) {
|
||||
test('highway renderer bundles surface the core lefty flag', () => {
|
||||
assert.match(
|
||||
src(HIGHWAY_JS),
|
||||
/lefty\s*[:=]\s*_lefty/,
|
||||
/lefty\s*:\s*_lefty/,
|
||||
'custom renderer bundles must include lefty: _lefty',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -61,25 +61,10 @@ test('draw() reads the live canvas box once per frame', () => {
|
||||
test('backing-store drift branch is preserved (splitscreen path)', () => {
|
||||
// The original check that catches the splitscreen hw.resize override
|
||||
// resizing the element without calling renderer.resize() must remain.
|
||||
// The comparison is hoisted into _bsChanged (checked with cheap property
|
||||
// reads every frame, and it forces the throttled box read to run on the
|
||||
// same frame); the branch body is unchanged.
|
||||
assert.match(
|
||||
src,
|
||||
/const\s+_bsChanged\s*=\s*highwayCanvas\.width\s*!==\s*_lastHwW\s*\|\|\s*highwayCanvas\.height\s*!==\s*_lastHwH\s*;/,
|
||||
'the backing-store (canvas.width/height) comparison must run every frame',
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/if\s*\(\s*_bsChanged\s*\)\s*\{\s*_lastHwW\s*=\s*highwayCanvas\.width\s*;\s*_lastHwH\s*=\s*highwayCanvas\.height\s*;\s*if\s*\(\s*box\.w\s*>\s*0\s*&&\s*box\.h\s*>\s*0\s*\)\s*applySize\(\s*box\.w\s*,\s*box\.h\s*\)\s*;/,
|
||||
'the backing-store drift branch must still re-apply',
|
||||
);
|
||||
// The throttle must never delay the backing-store path: _bsChanged is
|
||||
// part of the gate that forces the box read on the same frame.
|
||||
assert.match(
|
||||
src,
|
||||
/if\s*\(\s*_bsChanged\s*\|\|\s*!_wrapPinned\s*\|\|\s*_boxCheckCountdown\s*===\s*0\s*\)/,
|
||||
'the box-read gate must include _bsChanged so backing-store changes re-apply immediately',
|
||||
/if\s*\(\s*highwayCanvas\.width\s*!==\s*_lastHwW\s*\|\|\s*highwayCanvas\.height\s*!==\s*_lastHwH\s*\)\s*\{\s*_lastHwW\s*=\s*highwayCanvas\.width\s*;\s*_lastHwH\s*=\s*highwayCanvas\.height\s*;\s*if\s*\(\s*box\.w\s*>\s*0\s*&&\s*box\.h\s*>\s*0\s*\)\s*applySize\(\s*box\.w\s*,\s*box\.h\s*\)\s*;/,
|
||||
'the backing-store (canvas.width/height) drift branch must still re-apply',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -43,13 +43,13 @@ test('core _makeBundle exposes isPlaying derived from the chart-clock anchor', (
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const fn = extractBlock(src, 'function _makeBundle()');
|
||||
// Field present in the bundle.
|
||||
assert.match(fn, /\bisPlaying\s*[:=]/, 'bundle must expose isPlaying');
|
||||
assert.match(fn, /\bisPlaying\s*:/, 'bundle must expose isPlaying');
|
||||
// It is computed from the same anchor/advance state getTime() uses, not a
|
||||
// hardcoded literal — anchor must exist AND the clock must have advanced
|
||||
// within the interp cap.
|
||||
assert.match(
|
||||
fn,
|
||||
/isPlaying\s*[:=]\s*!Number\.isNaN\(\s*_chartAnchorPerfNow\s*\)/,
|
||||
/isPlaying\s*:\s*!Number\.isNaN\(\s*_chartAnchorPerfNow\s*\)/,
|
||||
'isPlaying must gate on a live anchor (_chartAnchorPerfNow not NaN)',
|
||||
);
|
||||
assert.match(
|
||||
|
||||
@@ -82,7 +82,7 @@ test('draw() only adapts during active playback and feeds the HUD', () => {
|
||||
|
||||
test('bundle + canvas sizing use the effective scale, not the raw user value', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
assert.match(src, /renderScale\s*[:=]\s*_effectiveRenderScale\(\)/, 'bundle.renderScale must be the effective scale');
|
||||
assert.match(src, /renderScale:\s*_effectiveRenderScale\(\)/, 'bundle.renderScale must be the effective scale');
|
||||
assert.match(src, /canvas\.width\s*=\s*Math\.round\(w\s*\*\s*_effectiveRenderScale\(\)\)/, 'canvas backing store must use effective scale');
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ test('_makeBundle exposes getNoteState (stable reference, no per-frame alloc)',
|
||||
const fn = extractBlock(src, 'function _makeBundle()');
|
||||
// The bundle field must point straight at _noteState — not a fresh
|
||||
// arrow each frame (the per-frame allocation the review flagged).
|
||||
assert.match(fn, /getNoteState\s*[:=]\s*_noteState\b/, 'bundle.getNoteState must be the stable _noteState reference');
|
||||
assert.match(fn, /getNoteState:\s*_noteState\b/, 'bundle.getNoteState must be the stable _noteState reference');
|
||||
});
|
||||
|
||||
test('_makeBundle exposes getNoteStateProvider as a stable reference (feedBack#254)', () => {
|
||||
@@ -64,7 +64,7 @@ test('_makeBundle exposes getNoteStateProvider as a stable reference (feedBack#2
|
||||
// identity-based guards in renderer code.
|
||||
assert.match(
|
||||
fn,
|
||||
/getNoteStateProvider\s*[:=]\s*_getNoteStateProvider\b/,
|
||||
/getNoteStateProvider:\s*_getNoteStateProvider\b/,
|
||||
'bundle.getNoteStateProvider must be the stable _getNoteStateProvider reference (not a per-frame arrow)'
|
||||
);
|
||||
// Sanity: the stable accessor exists per-createHighway-instance
|
||||
|
||||
Reference in New Issue
Block a user