Files
feedBack/static/slopsmith.d.ts
T
6f433d9cea types: add static/slopsmith.d.ts ambient plugin contract
Declares the plugin-facing JS surface as ambient types: window.slopsmith
(event bus + audio/diagnostics namespaces), window.highway (the full
createHighway() renderer API), the highway WebSocket message union
(discriminated on `type`, matching the switch in highway.js), the
setRenderer visualization contract + draw(bundle) shape, and the
keyboard-shortcut API.

Signatures verified against the live source: the `return api` object
at the tail of createHighway(), the `window.slopsmith = Object.assign`
block in app.js, audio-mixer.js, diagnostics.js, and the shortcut
registry in app.js.

Declarations only -- no file carries `// @ts-check` yet, so typecheck
stays trivially green. `tsc --noEmit --listFiles` confirms the .d.ts
is in the program and static/vendor/** is not.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 00:31:43 -07:00

376 lines
14 KiB
TypeScript

/**
* Slopsmith core — ambient plugin-contract declarations.
*
* This file is the typed source of truth for the plugin-facing JS surface:
* `window.slopsmith`, `window.highway`, the highway WebSocket message shape,
* and the `setRenderer` visualization contract. It is loaded into the `tsc`
* program (see `tsconfig.json` `include`) as ambient types — core files that
* carry `// @ts-check` pick these globals up without any `import`.
*
* Per the Constitution (Principle IV) these contracts are stable: a breaking
* change here requires a CHANGELOG entry under "Migration notes".
*
* Hand-maintained — keep in sync with the live source. The runtime files
* (`static/diagnostics.js`, `audio-mixer.js`, `tour-engine.js`,
* `lottie-api.js`) are declared here but not themselves `@ts-check`'d yet.
*/
// ─── Chart data wire shapes ─────────────────────────────────────────────────
/** A single fretted note as streamed over the highway WebSocket. */
interface SlopsmithNote {
/** Time, seconds. */
t: number;
/** String index (0 = lowest). */
s: number;
/** Fret number (0 = open). */
f: number;
/** Sustain length, seconds. */
sus?: number;
/** Hammer-on flag. */
ho?: number;
/** Pull-off flag. */
po?: number;
/** Slide-to fret. */
sl?: number;
/** Bend flag / amount. */
bn?: number;
[key: string]: unknown;
}
/** A chord event: a cluster of notes sharing a time. */
interface SlopsmithChord {
/** Time, seconds. */
t: number;
/** Component notes (string/fret/sustain, no own `t`). */
notes: Array<Omit<SlopsmithNote, 't'> & { t?: number }>;
/** Index into the chord-templates table. */
id?: number;
[key: string]: unknown;
}
interface SlopsmithBeat { time: number; measure: number; }
interface SlopsmithSection { time: number; name: string; }
interface SlopsmithAnchor { time: number; fret: number; width: number; }
interface SlopsmithChordTemplate { name: string; frets: number[]; fingers?: number[]; }
interface SlopsmithLyric { w: string; t: number; d: number; }
interface SlopsmithToneChange { time: number; name: string; }
/** Per-note scoring judgment published via `highway.setNoteStateProvider`. */
interface SlopsmithNoteState {
state: 'hit' | 'active' | 'miss';
alpha: number;
color: string | null;
}
/** A scorer callback: returns a judgment for a chart note, or nullish. */
type SlopsmithNoteStateProvider = (
note: SlopsmithNote,
chartTime: number,
) => SlopsmithNoteState | 'hit' | 'active' | 'miss' | null | undefined;
// ─── Highway WebSocket protocol ─────────────────────────────────────────────
/** `song_info` — song metadata frame. */
interface SlopsmithSongInfo {
type: 'song_info';
title: string;
artist: string;
arrangement: string;
arrangement_index: number;
arrangements: Array<{ name: string; index: number }>;
duration: number;
/** Tuning offsets — length 6 for guitar, 4 for bass. */
tuning: number[];
capo: number;
format: 'psarc' | 'sloppak' | 'loose' | string;
/** `null` when audio is unavailable. */
audio_url: string | null;
/** Non-null only when `audio_url` is null. */
audio_error: string | null;
/** Always present; empty array when there are no split stems. */
stems: string[];
/** Chart `<offset>`, seconds. Absent for sources without one. */
offset?: number;
/** Active-arrangement string count, when the server emits it. */
stringCount?: number;
[key: string]: unknown;
}
/**
* A frame streamed over `/ws/highway/{filename}`. Discriminated on `type`;
* the case set is the `switch (msg.type)` in `highway.js`.
*/
type SlopsmithHighwayMessage =
| { type: 'loading'; stage: string }
| SlopsmithSongInfo
| { type: 'beats'; data: SlopsmithBeat[] }
| { type: 'sections'; data: SlopsmithSection[] }
| { type: 'anchors'; data: SlopsmithAnchor[] }
| { type: 'chord_templates'; data: SlopsmithChordTemplate[] }
| { type: 'lyrics'; data: SlopsmithLyric[] }
| { type: 'tone_changes'; base: string; data: SlopsmithToneChange[] }
| { type: 'notes'; data: SlopsmithNote[] }
| { type: 'chords'; data: SlopsmithChord[] }
| { type: 'handshapes'; data: unknown[] }
| { type: 'phrases'; data: unknown[]; total: number }
| { type: 'ready' };
// ─── Visualization renderer (setRenderer) contract ──────────────────────────
/**
* Per-frame snapshot handed to a renderer's `draw()` / `init()`. All chart
* arrays are difficulty-filter-aware.
*/
interface SlopsmithRenderBundle {
currentTime: number;
songInfo: SlopsmithSongInfo | Record<string, never>;
isReady: boolean;
notes: SlopsmithNote[];
chords: SlopsmithChord[];
anchors: SlopsmithAnchor[];
beats: SlopsmithBeat[];
sections: SlopsmithSection[];
chordTemplates: SlopsmithChordTemplate[];
stringCount: number;
lyrics: SlopsmithLyric[];
toneChanges: SlopsmithToneChange[];
toneBase: string;
mastery: number;
hasPhraseData: boolean;
inverted: boolean;
lefty: boolean;
renderScale: number;
lyricsVisible: boolean;
/** 2D-highway depth projection for a time offset. */
project: (tOffset: number) => { x: number; y: number; scale: number };
/** 2D-highway horizontal fret position. */
fretX: (fret: number, scale: number, w: number) => number;
/** Per-note scoring state, or null when no provider is registered. */
getNoteState: (note: SlopsmithNote, chartTime: number) => SlopsmithNoteState | null;
}
/**
* A renderer instance returned by a `window.slopsmithViz_<id>` factory.
* Subject to multiple `init() … destroy()` cycles on one instance.
*/
interface SlopsmithRenderer {
/** Required canvas context type; defaults to `'2d'` when omitted. */
contextType?: '2d' | 'webgl2';
init(canvas: HTMLCanvasElement, bundle: SlopsmithRenderBundle): void;
draw(bundle: SlopsmithRenderBundle): void;
resize?(w: number, h: number): void;
destroy?(): void;
}
/** A `window.slopsmithViz_<id>` factory: fresh renderer per call. */
interface SlopsmithVizFactory {
(): SlopsmithRenderer;
/** Auto-mode predicate; static on the factory, not the instance. */
matchesArrangement?: (songInfo: SlopsmithSongInfo | Record<string, never>) => unknown;
/** Optional static mirror of the instance `contextType`. */
contextType?: '2d' | 'webgl2';
}
// ─── window.highway — the renderer API ──────────────────────────────────────
/** The object returned by `createHighway()` and exposed as `window.highway`. */
interface SlopsmithHighway {
init(canvas: HTMLCanvasElement, container?: HTMLElement | null): void;
resize(): void;
connect(wsUrl: string, opts?: {
onError?: (err: string) => void;
onReady?: () => void;
[key: string]: unknown;
}): void;
reconnect(filename: string, arrangement: number): void;
stop(): void;
setRenderScale(scale: number): void;
getRenderScale(): number;
getInverted(): boolean;
setInverted(v: boolean): void;
getLefty(): boolean;
setLefty(on: boolean): void;
setMastery(fraction: number): void;
getMastery(): number;
hasPhraseData(): boolean;
setTime(t: number): void;
getTime(): number;
setAvOffset(ms: number): void;
getAvOffset(): number;
getBPM(t: number): number;
getBeats(): SlopsmithBeat[];
getAudioElement(): HTMLAudioElement | null;
setVisible(v: boolean | null): void;
isVisible(): boolean;
getNotes(): SlopsmithNote[];
getChords(): SlopsmithChord[];
getChordTemplates(): SlopsmithChordTemplate[];
getToneChanges(): SlopsmithToneChange[];
getToneBase(): string;
getSections(): SlopsmithSection[];
getSongInfo(): SlopsmithSongInfo | Record<string, never>;
getStringCount(): number;
addDrawHook(fn: (ctx: CanvasRenderingContext2D, w: number, h: number) => void): void;
removeDrawHook(fn: (ctx: CanvasRenderingContext2D, w: number, h: number) => void): void;
fireDrawHooks(ctx: CanvasRenderingContext2D, w: number, h: number): void;
setNoteStateProvider(fn: SlopsmithNoteStateProvider | null): void;
getNoteStateProvider(): SlopsmithNoteStateProvider | null;
getNoteState(note: SlopsmithNote, chartTime: number): SlopsmithNoteState | null;
project(tOffset: number): { x: number; y: number; scale: number };
fretX(fret: number, scale: number, w: number): number;
fillTextUnmirrored(text: string, x: number, y: number): void;
toggleLyrics(): void;
getLyricsVisible(): boolean;
setLyricsVisible(v: boolean): void;
setOnLyricsChange(fn: (visible: boolean) => void): void;
setRenderer(r: SlopsmithRenderer | null | undefined): void;
isDefaultRenderer(): boolean;
}
// ─── window.slopsmith — event bus + namespaces ──────────────────────────────
/** Named event payloads carried on `window.slopsmith` CustomEvents (`event.detail`). */
interface SlopsmithEventMap {
'song:ready': { songInfo: SlopsmithSongInfo };
'song:play': unknown;
'song:pause': unknown;
'song:seek': { from: number; to: number; reason: string | null };
'highway:visibility': { visible: boolean; canvas: HTMLCanvasElement };
'highway:canvas-replaced': {
oldCanvas: HTMLCanvasElement;
newCanvas: HTMLCanvasElement;
contextType: '2d' | 'webgl2';
};
'viz:reverted': unknown;
[event: string]: unknown;
}
/** A labeled audio fader registered with the mixer (slopsmith#87). */
interface SlopsmithFaderSpec {
id: string;
label: string;
unit?: string;
min: number;
max: number;
step: number;
defaultValue: number;
getValue: () => number;
setValue: (v: number) => void;
}
/** `window.slopsmith.audio` — mixer + song-volume surface (audio-mixer.js). */
interface SlopsmithAudioApi {
registerFader(spec: SlopsmithFaderSpec): void;
unregisterFader(id: string): void;
getFaders(): SlopsmithFaderSpec[];
openMixer(): void;
closeMixer(restoreFocus?: boolean): void;
toggleMixer(): void;
applySongVolume(v?: number | null): Promise<number>;
readSongVolume(): number;
}
/** `window.slopsmith.diagnostics` — client diagnostics surface (diagnostics.js). */
interface SlopsmithDiagnosticsApi {
contribute(pluginId: string, payload: unknown): void;
snapshot(): unknown;
snapshotConsole(): unknown;
snapshotHardware(): Promise<unknown>;
snapshotUa(): unknown;
snapshotLocalStorage(): unknown;
snapshotContributions(): unknown;
}
/** `window.slopsmith` — the plugin event bus (an `EventTarget`). */
interface SlopsmithApi extends EventTarget {
currentSong: unknown;
isPlaying: boolean;
navigate(screenId: string, params?: Record<string, unknown>): void;
getNavParams(): Record<string, unknown>;
emit<K extends keyof SlopsmithEventMap>(event: K, detail: SlopsmithEventMap[K]): void;
emit(event: string, detail?: unknown): void;
on(event: string, fn: (e: Event) => void, options?: AddEventListenerOptions | boolean): void;
off(event: string, fn: (e: Event) => void, options?: EventListenerOptions | boolean): void;
setLoop(a: number, b: number): unknown;
clearLoop(): void;
getLoop(): { loopA: number | null; loopB: number | null };
/** Attached by audio-mixer.js after it loads. */
audio?: SlopsmithAudioApi;
/** Attached by diagnostics.js early in `<head>`. */
diagnostics?: SlopsmithDiagnosticsApi;
}
// ─── Keyboard-shortcut API ──────────────────────────────────────────────────
/** A keyboard shortcut registration (see `window.registerShortcut`). */
interface SlopsmithShortcutSpec {
/** `e.key` value or `e.code`. */
key: string;
description: string;
scope?: 'global' | 'player' | 'library' | 'settings' | string;
condition?: () => boolean;
handler: (e: KeyboardEvent) => void;
}
/** A panel-scoped shortcut registry returned by `createShortcutPanel`. */
interface SlopsmithShortcutPanel {
registerShortcut(spec: SlopsmithShortcutSpec): void;
unregisterShortcut(key: string, scope?: string): boolean;
clearShortcuts(): void;
}
// ─── Global augmentations ───────────────────────────────────────────────────
declare global {
interface Window {
/** Plugin event bus + namespaces. Owned by app.js. */
slopsmith: SlopsmithApi;
/** The shared highway renderer instance. */
highway: SlopsmithHighway;
/** Factory for additional highway instances (splitscreen panels). */
createHighway(): SlopsmithHighway;
/** Loads and plays a song into the player. */
playSong(filename: string, arrangement?: number): Promise<void>;
/** Switches the active single-page-app screen. */
showScreen(id: string): Promise<void>;
/** Keyboard-shortcut registry. */
registerShortcut(options: SlopsmithShortcutSpec): void;
unregisterShortcut(key: string, scope?: string): boolean;
createShortcutPanel(id: string): SlopsmithShortcutPanel;
setActiveShortcutPanel(id: string): void;
getActiveShortcutPanel(): string;
clearWindowShortcuts(windowId: string): number;
getShortcutWindowId(): string;
/** Desktop JUCE audio bridge — present only in slopsmith-desktop. */
jucePlayer?: unknown;
_juceMode?: boolean;
_juceAudioUrl?: string;
/** Lottie animation helper (lottie-api.js). */
slopsmithLottie?: unknown;
/** Guided-tour engine (tour-engine.js). */
slopsmithTour?: unknown;
/**
* Visualization factories — one per `type: "visualization"` plugin,
* keyed `slopsmithViz_<id>`. Indexed access is intentionally loose.
*/
[vizFactory: `slopsmithViz_${string}`]: SlopsmithVizFactory;
}
}
export {};