@tensordoc/prism 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scott Penberthy and Prism contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # @tensordoc/prism
2
+
3
+ Two-line embed for audio-reactive visualizations. Drop a `<div>`,
4
+ call `new PrismPlayer({ container })`, get a Milkdrop preset or
5
+ Shadertoy fragment shader playing in the browser — reacting to
6
+ whatever audio you connect.
7
+
8
+ ```bash
9
+ npm install @tensordoc/prism
10
+ ```
11
+
12
+ ```html
13
+ <div id="viz" style="width:100vw;height:100vh"></div>
14
+ <script type="module">
15
+ import { PrismPlayer } from "@tensordoc/prism";
16
+ new PrismPlayer({ container: "viz" });
17
+ </script>
18
+ ```
19
+
20
+ That's it. The visualization runs against a built-in synthetic signal
21
+ until you connect real audio.
22
+
23
+ ## Live demo
24
+
25
+ [**prism.scott.ai**](https://prism.scott.ai) — the deployed site uses this
26
+ exact package. The "Two-line embed" preview at
27
+ [prism.scott.ai/examples/embed.html](https://prism.scott.ai/examples/embed.html)
28
+ is the smallest possible HTML page using it.
29
+
30
+ ## What you get
31
+
32
+ **Two rendering backends** that swap automatically based on the
33
+ graph you load:
34
+
35
+ - **Milkdrop** via [butterchurn](https://github.com/jberg/butterchurn) —
36
+ ~80 named presets ship in the bundle
37
+ - **Shadertoy** — any GLSL fragment shader following Shadertoy's
38
+ `mainImage()` convention; loaded from a URL
39
+
40
+ **Six audio sources** the player accepts:
41
+
42
+ ```js
43
+ new PrismPlayer({
44
+ container: "viz",
45
+ audio: "mic", // getUserMedia
46
+ audio: "tab", // getDisplayMedia
47
+ audio: someAudioNode, // your Web Audio graph
48
+ audio: someMediaStream, // anything with audio tracks
49
+ // audio: undefined (default) → built-in synthetic signal
50
+ });
51
+ ```
52
+
53
+ **Six image sources** for shader inputs (`iChannel1`):
54
+
55
+ ```js
56
+ new PrismPlayer({
57
+ container: "viz",
58
+ image: "https://example.com/a.jpg", // single URL
59
+ image: ["a.jpg", "b.jpg", "c.jpg"], // built-in crossfading slideshow
60
+ image: "webcam", // getUserMedia({video: true})
61
+ image: "tab", // getDisplayMedia({video: true})
62
+ image: someVideoElement, // <video>
63
+ image: someCanvas, // your own renderer
64
+ });
65
+
66
+ // Tune slideshow timing
67
+ new PrismPlayer({ container, image: ["a.jpg","b.jpg"], holdSeconds: 6 });
68
+ ```
69
+
70
+ **Methods** for runtime control:
71
+
72
+ ```js
73
+ player.load(graphOrShortId); // swap visualization
74
+ player.connectAudio("mic"); // change audio source
75
+ player.disconnectAudio(); // revert to synthetic signal
76
+ player.connectImage("webcam"); // change image feed
77
+ player.disconnectImage(); // release webcam track, etc.
78
+ player.destroy(); // clean up everything
79
+ ```
80
+
81
+ **Readonly handles** for power users (the underlying primitives are
82
+ exposed; you can call them directly):
83
+
84
+ ```js
85
+ player.audioCtx // AudioContext
86
+ player.activeBackend // "milkdrop" | "shadertoy"
87
+ player.milkdrop // butterchurn handle
88
+ player.shadertoy // shader runtime
89
+ player.synth // synthetic signal driver
90
+ player.runtime // graph executor
91
+ ```
92
+
93
+ ## Share-by-URL with short IDs
94
+
95
+ Every curated catalog entry has a permanent **6-character base62
96
+ share token**. Same idea as YouTube video IDs:
97
+
98
+ ```js
99
+ new PrismPlayer({ container: "viz", graph: "7Hq3pK" });
100
+ ```
101
+
102
+ The lookup happens against the bundled registry — no network call. The
103
+ companion site exposes `prism.scott.ai/?g=<id>` as a one-click shareable URL.
104
+
105
+ ```js
106
+ import { lookup, shortIdToGraph } from "@tensordoc/prism";
107
+ lookup("7Hq3pK"); // { name, source_type, source_url, ... }
108
+ shortIdToGraph("7Hq3pK"); // ready-to-use PrismGraph
109
+ ```
110
+
111
+ ## Constructor options (full)
112
+
113
+ ```ts
114
+ new PrismPlayer({
115
+ container: HTMLElement | string, // required — DOM id or element
116
+ graph?: PrismGraph | string, // short_id or full graph
117
+ audio?: "mic" | "tab" | MediaStream | AudioNode,
118
+ audioCtx?: AudioContext, // bring your own
119
+ image?: ImageSource, // see "image sources" above
120
+ holdSeconds?: number, // slideshow timing, default 6
121
+ defaultImage?: string, // static fallback URL
122
+ initialPresetName?: string, // cold-open Milkdrop preset
123
+ onReady?: () => void, // first-frame callback
124
+ onError?: (err: Error) => void,
125
+ });
126
+ ```
127
+
128
+ ## Optional: draggable picture-in-picture overlay
129
+
130
+ If you want a Picture-in-Picture viewer for the slideshow, ship with
131
+ `ImageOverlay` — a separate export that gives you a draggable,
132
+ resizable, collapsible card. The player stays headless; you compose:
133
+
134
+ ```js
135
+ import { ImageOverlay } from "@tensordoc/prism";
136
+ const overlay = new ImageOverlay({ mount: document.body });
137
+ // overlay.element is a DOM div you can position or style;
138
+ // overlay.rect is the current rectangle, updated on drag/resize
139
+ ```
140
+
141
+ ## Use from a CDN (no install)
142
+
143
+ ```html
144
+ <script type="module">
145
+ import { PrismPlayer } from "https://esm.sh/@tensordoc/prism";
146
+ new PrismPlayer({ container: "viz" });
147
+ </script>
148
+ ```
149
+
150
+ ## Audio context note
151
+
152
+ Browsers require a user gesture before audio starts. The player
153
+ creates an AudioContext but leaves it suspended; resume it on the
154
+ first interaction:
155
+
156
+ ```js
157
+ const resume = () => player.audioCtx.resume();
158
+ window.addEventListener("pointerdown", resume, { once: true });
159
+ window.addEventListener("keydown", resume, { once: true });
160
+ ```
161
+
162
+ ## Bundle size
163
+
164
+ ESM ~25 KB gzipped (excluding butterchurn). Butterchurn itself ships
165
+ in the bundle as a regular dependency (~500 KB). If you don't need
166
+ Milkdrop and only want shader visualizations, you can tree-shake
167
+ butterchurn out by importing only what you use:
168
+
169
+ ```js
170
+ import { createShadertoyBackground } from "@tensordoc/prism";
171
+ ```
172
+
173
+ ## License
174
+
175
+ [MIT](./LICENSE)
@@ -0,0 +1,28 @@
1
+ export interface MilkdropBg {
2
+ /** Pretty name of the currently-loaded preset (live; updates on rotation). */
3
+ readonly presetName: string;
4
+ /** Raw preset key (matches catalog `preset_id`). Stable for share / API use. */
5
+ readonly currentPresetId: string;
6
+ /** Swap the audio source — call when real tab-audio comes in. */
7
+ connectAudio: (node: AudioNode) => void;
8
+ /** Load a new random preset (with a blend transition). Returns the
9
+ * pretty name of the newly-loaded preset. */
10
+ loadRandom: (blendSeconds?: number) => string;
11
+ /** Load a specific preset by its raw key (matches catalog `preset_id`).
12
+ * Returns the pretty name on success; `null` if no preset with that key
13
+ * exists in the bundled library. */
14
+ loadByName: (name: string, blendSeconds?: number) => string | null;
15
+ /** Load a .milk preset from a URL: fetch text → convertPreset → loadPreset.
16
+ * Used for the 526 favorites and future contributor uploads which live
17
+ * at public/presets/milkdrop/<slug>.milk. Returns the pretty name on
18
+ * success; throws on fetch / parse error so callers can surface it. */
19
+ loadFromUrl: (url: string, blendSeconds?: number) => Promise<string>;
20
+ destroy: () => void;
21
+ }
22
+ export interface MilkdropBgOptions {
23
+ /** Preset key to load on cold open. Falls back to a random pick if the
24
+ * name doesn't exist in the bundle. Lets the landing pin a curated
25
+ * "atelier" default instead of getting random_$$$ Royal Mashup. */
26
+ initialPresetName?: string;
27
+ }
28
+ export declare function createMilkdropBackground(audioCtx: AudioContext, canvas: HTMLCanvasElement, silentSource: AudioNode, onReady?: () => void, options?: MilkdropBgOptions): MilkdropBg;
@@ -0,0 +1,22 @@
1
+ export interface ShadertoyBg {
2
+ /** Pretty name from the source URL, useful for SKILL readout. */
3
+ readonly presetName: string;
4
+ /** URL of the currently-loaded shader. */
5
+ readonly currentUrl: string | null;
6
+ /** Connect a Web Audio source — Shadertoy's iChannel0 sees its FFT. */
7
+ connectAudio: (node: AudioNode) => void;
8
+ /** Fetch + compile + render a new shader. Resolves once first frame
9
+ * paints. Throws on compile error. */
10
+ loadFromUrl: (url: string) => Promise<string>;
11
+ /** Bind an image URL as iChannel1. Resolves once decoded + uploaded.
12
+ * Pass null to clear (resets to the 1x1 placeholder). */
13
+ bindImage: (url: string | null) => Promise<void>;
14
+ /** Pipe a live source (e.g. a slideshow's canvas) into iChannel1 — its
15
+ * contents are uploaded to the GPU every frame, so when the slideshow
16
+ * advances to the next image, the shader sees it immediately. Pass
17
+ * null to disable + revert to whatever was last bound via bindImage. */
18
+ setLiveSource: (source: HTMLCanvasElement | HTMLVideoElement | null) => void;
19
+ /** Pause the render loop + free GL resources. */
20
+ destroy: () => void;
21
+ }
22
+ export declare function createShadertoyBackground(audioCtx: AudioContext, canvas: HTMLCanvasElement, silentSource: AudioNode): ShadertoyBg;
@@ -0,0 +1,32 @@
1
+ export interface SlideshowOptions {
2
+ /** How long (seconds) each image is held on-screen before the
3
+ * crossfade to the next begins. Defaults to 6. */
4
+ holdSeconds?: number;
5
+ }
6
+ export declare class HeadlessSlideshow {
7
+ /** The output canvas — bind this to the shader via setLiveSource. */
8
+ readonly canvas: HTMLCanvasElement;
9
+ private readonly ctx;
10
+ private readonly urls;
11
+ private readonly holdMs;
12
+ private readonly images;
13
+ private currentIndex;
14
+ private nextIndex;
15
+ /** Wall-clock time the current crossfade started, or null if we're
16
+ * in the steady-hold portion of the cycle. */
17
+ private crossfadeStartMs;
18
+ private holdTimer;
19
+ private rafHandle;
20
+ private destroyed;
21
+ constructor(urls: string[], opts?: SlideshowOptions);
22
+ destroy(): void;
23
+ private preload;
24
+ private start;
25
+ private scheduleNext;
26
+ private advance;
27
+ private tickCrossfade;
28
+ /** Draw the current image fully opaque. */
29
+ private drawFrame;
30
+ /** Draw the current image, then the next one at alpha=t on top. */
31
+ private drawCrossfade;
32
+ }
@@ -0,0 +1,84 @@
1
+ export interface Rect {
2
+ x: number;
3
+ y: number;
4
+ w: number;
5
+ h: number;
6
+ }
7
+ export interface OverlayState {
8
+ rect: Rect;
9
+ collapsed: boolean;
10
+ }
11
+ export interface ImageOverlayOptions {
12
+ /** Element to mount the card div in. Defaults to document.body. */
13
+ mount?: HTMLElement;
14
+ /** Initial card rect in CSS pixels. Defaults to a centered card sized
15
+ * to ~58% of viewport width. */
16
+ initialRect?: Rect;
17
+ /** BEM class prefix for the card and its parts. Defaults to
18
+ * "prism-overlay" — produces classes `.prism-overlay`,
19
+ * `.prism-overlay__handle`, `.prism-overlay__collapse`. */
20
+ className?: string;
21
+ /** Thumbnail (collapsed) width in CSS pixels. Defaults to 200. */
22
+ thumbWidth?: number;
23
+ /** Min visible margin in pixels — drag is clamped so this much of
24
+ * the card always stays in the viewport. Defaults to 60. */
25
+ minVisible?: number;
26
+ /** Min top edge (pixels from viewport top), e.g. to clear a status
27
+ * bar. Defaults to 36. */
28
+ topMargin?: number;
29
+ /** Minimum card dimensions while resizing. Defaults to 80×45 (16:9). */
30
+ minWidth?: number;
31
+ minHeight?: number;
32
+ /** Called whenever the rect or collapsed state changes (drag, resize,
33
+ * collapse, expand, window resize re-anchor). */
34
+ onChange?: (state: OverlayState) => void;
35
+ }
36
+ export declare class ImageOverlay {
37
+ readonly element: HTMLElement;
38
+ private readonly mount;
39
+ private readonly className;
40
+ private readonly thumbW;
41
+ private readonly thumbH;
42
+ private readonly thumbMargin;
43
+ private readonly statusBarH;
44
+ private readonly minVisible;
45
+ private readonly minWidth;
46
+ private readonly minHeight;
47
+ private readonly onChange?;
48
+ private _rect;
49
+ private _collapsed;
50
+ private uncollapsedRect;
51
+ private drag;
52
+ private destroyed;
53
+ constructor(opts?: ImageOverlayOptions);
54
+ /** Current card rect (live; do not mutate — call setRect instead). */
55
+ get rect(): Rect;
56
+ isCollapsed(): boolean;
57
+ /** Programmatically set the rect. Silent: does NOT call onChange.
58
+ * Use this to push externally-managed state into the overlay
59
+ * without echoing back into your own change handler. The internal
60
+ * drag/resize handlers emit onChange themselves — that's the only
61
+ * source of change notifications. */
62
+ setRect(next: Rect): void;
63
+ /** Programmatically collapse. Silent — see setRect. */
64
+ collapse(): void;
65
+ /** Programmatically expand. Silent — see setRect. */
66
+ expand(): void;
67
+ show(): void;
68
+ hide(): void;
69
+ /** Whether the card is currently visible (DOM attribute is the
70
+ * source of truth, so this stays accurate if anyone toggles it
71
+ * externally for testing). */
72
+ isVisible(): boolean;
73
+ destroy(): void;
74
+ private buildDom;
75
+ private applyRectToDom;
76
+ private emit;
77
+ private defaultRect;
78
+ private thumbRect;
79
+ private clampRect;
80
+ private readonly onWindowResize;
81
+ private readonly onPointerDown;
82
+ private readonly onPointerMove;
83
+ private readonly onPointerEnd;
84
+ }
@@ -0,0 +1,9 @@
1
+ export { PrismPlayer, type PrismPlayerOptions, type GraphInput, type ImageSource, } from './player';
2
+ export { HeadlessSlideshow, type SlideshowOptions } from './image-feed';
3
+ export { ImageOverlay, type ImageOverlayOptions, type OverlayState, type Rect, } from './image-overlay';
4
+ export { GraphRuntime, type ApplyResult, type RuntimeContext } from './runtime';
5
+ export { SyntheticSignal } from './synth';
6
+ export { createMilkdropBackground, type MilkdropBg, type MilkdropBgOptions, } from './backends/milkdrop';
7
+ export { createShadertoyBackground, type ShadertoyBg, } from './backends/shadertoy';
8
+ export { lookup, shortIds, shortIdToGraph, type RegistryEntry } from './registry';
9
+ export * from './types';
@@ -0,0 +1,150 @@
1
+ import { MilkdropBg } from './backends/milkdrop';
2
+ import { ShadertoyBg } from './backends/shadertoy';
3
+ import { GraphRuntime, ApplyResult } from './runtime';
4
+ import { SyntheticSignal } from './synth';
5
+ import { PrismGraph } from './types';
6
+ /** Anything `load()` and the `graph` option accept. A string is treated
7
+ * as a catalog short_id (6-char base62) and resolved against the
8
+ * bundled registry. */
9
+ export type GraphInput = PrismGraph | string;
10
+ /** Anything the `image` option / `connectImage()` accepts. Strings
11
+ * beyond the "webcam" / "tab" sentinels are treated as static URLs. */
12
+ export type ImageSource = "webcam" | "tab" | string | string[] | HTMLCanvasElement | HTMLVideoElement | MediaStream;
13
+ export interface PrismPlayerOptions {
14
+ /** Where to mount the visualization canvases. Pass either an element
15
+ * (preferred from frameworks — React refs, Vue template refs, etc.)
16
+ * or a DOM id string for hand-written HTML embeds. The player
17
+ * creates its own canvases inside; it does not touch any children
18
+ * that already exist there. */
19
+ container: HTMLElement | string;
20
+ /** Optional initial graph. Pass a PrismGraph object directly, or a
21
+ * 6-char short_id string (e.g. "7Hq3pK") to look up an entry from
22
+ * the bundled catalog. Without it, cold-opens on a curated milkdrop
23
+ * preset; you can call load() later. */
24
+ graph?: GraphInput;
25
+ /** Audio source. An AudioNode is connected directly; "mic" and "tab"
26
+ * request the matching media stream via getUserMedia /
27
+ * getDisplayMedia and connect that. Undefined → the built-in
28
+ * SyntheticSignal drives both backends. */
29
+ audio?: "mic" | "tab" | MediaStream | AudioNode;
30
+ /** Bring your own AudioContext (useful when the embedder already has
31
+ * one; web pages can only have a small number of them). */
32
+ audioCtx?: AudioContext;
33
+ /** iChannel1 fallback used by shader backends until something more
34
+ * specific is bound by the graph or setLiveSource(). */
35
+ defaultImage?: string;
36
+ /** Image feed for shader backends (bound to iChannel1). Accepts:
37
+ * - A single URL → static image
38
+ * - An array of URLs → built-in crossfading slideshow
39
+ * - "webcam" / "tab" → getUserMedia / getDisplayMedia
40
+ * - a MediaStream / HTMLVideoElement → live video
41
+ * - an HTMLCanvasElement → live canvas (e.g. your own
42
+ * renderer; the player just
43
+ * re-uploads its contents every
44
+ * frame)
45
+ * Use this for "I want pictures showing through the shader" without
46
+ * thinking about iChannel1 or setLiveSource. */
47
+ image?: ImageSource;
48
+ /** Seconds each image is held on-screen before the crossfade to the
49
+ * next begins. Only used when `image` is a URL array. Defaults to 6. */
50
+ holdSeconds?: number;
51
+ /** Milkdrop preset to load on cold-open. Falls back to a random preset
52
+ * from the bundled library if the name isn't found. */
53
+ initialPresetName?: string;
54
+ /** Called once the first frame has painted. */
55
+ onReady?: () => void;
56
+ /** Async failures (audio capture rejection, graph load errors that
57
+ * cannot be surfaced from a synchronous call) land here. */
58
+ onError?: (err: Error) => void;
59
+ }
60
+ export declare class PrismPlayer {
61
+ /** AudioContext driving both backends + the default synthetic signal.
62
+ * Created on construction unless one was passed in `opts.audioCtx`. */
63
+ readonly audioCtx: AudioContext;
64
+ /** Default audio driver: a silent "pink-noise-ish" pad that keeps the
65
+ * visualization animated when no real audio is connected. Kept alive
66
+ * for the lifetime of the player — call `player.synth.stop()` if you
67
+ * want to free it after switching to a real audio source. */
68
+ readonly synth: SyntheticSignal;
69
+ /** Milkdrop (butterchurn) backend handle. Cold-opens on a random
70
+ * preset (or `initialPresetName` if you passed one). */
71
+ readonly milkdrop: MilkdropBg;
72
+ /** Shadertoy backend handle. Idle until a graph with `lf.shadertoy` is
73
+ * loaded (or you call `shadertoy.loadFromUrl` directly). */
74
+ readonly shadertoy: ShadertoyBg;
75
+ /** Graph executor — dispatches to the right backend based on the
76
+ * graph's light-field generator node. */
77
+ readonly runtime: GraphRuntime;
78
+ /** Tracks which backend's canvas is currently visible. */
79
+ activeBackend: "milkdrop" | "shadertoy";
80
+ private readonly milkdropCanvas;
81
+ private readonly shadertoyCanvas;
82
+ private readonly ownsAudioCtx;
83
+ /** Default hold per image when `image` is a URL list; set in ctor. */
84
+ private readonly defaultHoldSeconds;
85
+ /** Owned media resources (MediaStreams we started, <video> elements
86
+ * we created internally, headless slideshow). Cleaned up by
87
+ * connectImage() before re-binding and by destroy() on teardown. */
88
+ private currentSlideshow;
89
+ private currentVideo;
90
+ private currentStream;
91
+ /** Audio MediaStream the player started itself ("mic" / "tab"). Held
92
+ * so disconnectAudio() can stop the tracks. Null if the caller
93
+ * passed in their own stream/node (we never stop tracks we don't own). */
94
+ private currentAudioOwnedStream;
95
+ constructor(opts: PrismPlayerOptions);
96
+ /** Swap to a new graph. Accepts either a full PrismGraph object or a
97
+ * 6-char short_id string from the bundled registry. Returns the
98
+ * ApplyResult the runtime emits so the caller can react to backend
99
+ * switches / errors. An unknown short_id returns
100
+ * `{ ok: false, error: "unknown short_id ..." }` without touching
101
+ * the running visualization. */
102
+ load(graph: GraphInput, blendSeconds?: number): ApplyResult;
103
+ /** Connect a new audio source. Accepts:
104
+ * - "mic" — getUserMedia({ audio: true })
105
+ * - "tab" — getDisplayMedia({ audio: true })
106
+ * - MediaStream — wrapped in a MediaStreamAudioSourceNode
107
+ * - AudioNode — connected directly
108
+ * Routes the source to both backends so whichever one is active sees
109
+ * reactivity. Tracks any MediaStream the player started itself so
110
+ * disconnectAudio() can stop the tracks (mic indicator off, etc.). */
111
+ connectAudio(source: "mic" | "tab" | MediaStream | AudioNode): Promise<AudioNode>;
112
+ /** Disconnect the current audio source and revert both backends to
113
+ * the built-in SyntheticSignal. Stops any MediaStream tracks the
114
+ * player started itself (mic / tab capture indicator goes off).
115
+ * Streams or AudioNodes the caller passed in are left untouched —
116
+ * the caller owns their lifetime. Idempotent. */
117
+ disconnectAudio(): void;
118
+ /** Pipe a live source (e.g. a slideshow's canvas) into iChannel1 — its
119
+ * contents are re-uploaded each frame. Pass null to disable.
120
+ * Most embedders should prefer connectImage(), which handles
121
+ * webcam/tab/slideshow plumbing for you. */
122
+ setLiveSource(source: HTMLCanvasElement | HTMLVideoElement | null): void;
123
+ /** Connect an image source for the shader's iChannel1. Symmetric to
124
+ * connectAudio. Replaces any prior image feed; old MediaStream
125
+ * tracks are stopped and internal slideshow timers are cleared.
126
+ * See ImageSource for the accepted shapes. */
127
+ connectImage(source: ImageSource): Promise<void>;
128
+ /** Stop the current image feed and unbind from the shader. Releases
129
+ * resources the player owned: slideshow timer cleared, MediaStream
130
+ * tracks stopped (webcam light off), internal <video> detached.
131
+ * Canvas/video elements the caller passed in are left untouched.
132
+ * After this, the shader reverts to whatever was last bound via
133
+ * `defaultImage` or `shadertoy.bindImage()` (or the 1×1 placeholder).
134
+ * Idempotent. */
135
+ disconnectImage(): void;
136
+ /** Toggle which backend's canvas is visible. Called by GraphRuntime;
137
+ * callers can flip it manually too (rare). */
138
+ setActiveBackend(which: "milkdrop" | "shadertoy"): void;
139
+ /** Stop animation, free GL resources, detach canvases, close the
140
+ * AudioContext (only if the player created it itself). */
141
+ destroy(): void;
142
+ /** Wrap a MediaStream in an autoplaying <video> and bind that as the
143
+ * live source. The video element is held internally; teardown is
144
+ * handled by teardownImageFeed(). */
145
+ private bindStream;
146
+ /** Stop the current image feed (slideshow timer, MediaStream tracks,
147
+ * internal <video>) and unbind from the shader. Idempotent. */
148
+ private teardownImageFeed;
149
+ private resolveAudioNode;
150
+ }