@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 +21 -0
- package/README.md +175 -0
- package/dist/backends/milkdrop.d.ts +28 -0
- package/dist/backends/shadertoy.d.ts +22 -0
- package/dist/image-feed.d.ts +32 -0
- package/dist/image-overlay.d.ts +84 -0
- package/dist/index.d.ts +9 -0
- package/dist/player.d.ts +150 -0
- package/dist/prism.cjs +37 -0
- package/dist/prism.cjs.map +1 -0
- package/dist/prism.mjs +1271 -0
- package/dist/prism.mjs.map +1 -0
- package/dist/registry.d.ts +18 -0
- package/dist/runtime.d.ts +25 -0
- package/dist/synth.d.ts +47 -0
- package/dist/types.d.ts +28 -0
- package/package.json +58 -0
- package/src/backends/milkdrop.ts +190 -0
- package/src/backends/shadertoy.ts +290 -0
- package/src/image-feed.ts +185 -0
- package/src/image-overlay.ts +302 -0
- package/src/index.ts +27 -0
- package/src/peer-types.d.ts +18 -0
- package/src/player.ts +395 -0
- package/src/registry.generated.json +2022 -0
- package/src/registry.ts +70 -0
- package/src/runtime.ts +100 -0
- package/src/synth.ts +251 -0
- package/src/types.ts +98 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// image-overlay.ts — draggable, resizable, collapsible PiP card.
|
|
2
|
+
//
|
|
3
|
+
// A pure-UI companion to PrismPlayer. The card has no opinion about
|
|
4
|
+
// what's inside it — it just provides a positioned rectangle (the
|
|
5
|
+
// `rect` property) that an embedder can sync to a slideshow's render
|
|
6
|
+
// region, a video element, or anything else that wants to live in a
|
|
7
|
+
// PiP. Drag from the body to move; drag from the 4 corner handles to
|
|
8
|
+
// resize; click the × button to collapse to a thumbnail in the upper
|
|
9
|
+
// right; click the thumbnail to expand again.
|
|
10
|
+
//
|
|
11
|
+
// Styling is class-based with a customisable prefix so the host page
|
|
12
|
+
// can theme it. Defaults to `prism-overlay` with BEM-style modifiers
|
|
13
|
+
// (`__handle`, `__collapse`); prism.scott.ai passes `slideshow-card` to
|
|
14
|
+
// inherit its existing styles unchanged.
|
|
15
|
+
|
|
16
|
+
export interface Rect {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
w: number;
|
|
20
|
+
h: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OverlayState {
|
|
24
|
+
rect: Rect;
|
|
25
|
+
collapsed: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ImageOverlayOptions {
|
|
29
|
+
/** Element to mount the card div in. Defaults to document.body. */
|
|
30
|
+
mount?: HTMLElement;
|
|
31
|
+
/** Initial card rect in CSS pixels. Defaults to a centered card sized
|
|
32
|
+
* to ~58% of viewport width. */
|
|
33
|
+
initialRect?: Rect;
|
|
34
|
+
/** BEM class prefix for the card and its parts. Defaults to
|
|
35
|
+
* "prism-overlay" — produces classes `.prism-overlay`,
|
|
36
|
+
* `.prism-overlay__handle`, `.prism-overlay__collapse`. */
|
|
37
|
+
className?: string;
|
|
38
|
+
/** Thumbnail (collapsed) width in CSS pixels. Defaults to 200. */
|
|
39
|
+
thumbWidth?: number;
|
|
40
|
+
/** Min visible margin in pixels — drag is clamped so this much of
|
|
41
|
+
* the card always stays in the viewport. Defaults to 60. */
|
|
42
|
+
minVisible?: number;
|
|
43
|
+
/** Min top edge (pixels from viewport top), e.g. to clear a status
|
|
44
|
+
* bar. Defaults to 36. */
|
|
45
|
+
topMargin?: number;
|
|
46
|
+
/** Minimum card dimensions while resizing. Defaults to 80×45 (16:9). */
|
|
47
|
+
minWidth?: number;
|
|
48
|
+
minHeight?: number;
|
|
49
|
+
/** Called whenever the rect or collapsed state changes (drag, resize,
|
|
50
|
+
* collapse, expand, window resize re-anchor). */
|
|
51
|
+
onChange?: (state: OverlayState) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface DragState {
|
|
55
|
+
startCard: Rect;
|
|
56
|
+
startPointer: { x: number; y: number };
|
|
57
|
+
handle: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ImageOverlay {
|
|
61
|
+
readonly element: HTMLElement;
|
|
62
|
+
private readonly mount: HTMLElement;
|
|
63
|
+
private readonly className: string;
|
|
64
|
+
private readonly thumbW: number;
|
|
65
|
+
private readonly thumbH: number;
|
|
66
|
+
private readonly thumbMargin = 16;
|
|
67
|
+
private readonly statusBarH: number;
|
|
68
|
+
private readonly minVisible: number;
|
|
69
|
+
private readonly minWidth: number;
|
|
70
|
+
private readonly minHeight: number;
|
|
71
|
+
private readonly onChange?: (state: OverlayState) => void;
|
|
72
|
+
|
|
73
|
+
private _rect: Rect;
|
|
74
|
+
private _collapsed = false;
|
|
75
|
+
private uncollapsedRect: Rect | null = null;
|
|
76
|
+
private drag: DragState | null = null;
|
|
77
|
+
private destroyed = false;
|
|
78
|
+
|
|
79
|
+
constructor(opts: ImageOverlayOptions = {}) {
|
|
80
|
+
this.mount = opts.mount ?? document.body;
|
|
81
|
+
this.className = opts.className ?? "prism-overlay";
|
|
82
|
+
this.thumbW = opts.thumbWidth ?? 200;
|
|
83
|
+
this.thumbH = this.thumbW * (9 / 16);
|
|
84
|
+
this.minVisible = opts.minVisible ?? 60;
|
|
85
|
+
this.statusBarH = opts.topMargin ?? 36;
|
|
86
|
+
this.minWidth = opts.minWidth ?? 80;
|
|
87
|
+
this.minHeight = opts.minHeight ?? 45;
|
|
88
|
+
this.onChange = opts.onChange;
|
|
89
|
+
this._rect = opts.initialRect ?? this.defaultRect();
|
|
90
|
+
|
|
91
|
+
this.element = this.buildDom();
|
|
92
|
+
this.mount.appendChild(this.element);
|
|
93
|
+
this.applyRectToDom();
|
|
94
|
+
|
|
95
|
+
window.addEventListener("resize", this.onWindowResize, { passive: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Current card rect (live; do not mutate — call setRect instead). */
|
|
99
|
+
get rect(): Rect {
|
|
100
|
+
return { ...this._rect };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
isCollapsed(): boolean {
|
|
104
|
+
return this._collapsed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Programmatically set the rect. Silent: does NOT call onChange.
|
|
108
|
+
* Use this to push externally-managed state into the overlay
|
|
109
|
+
* without echoing back into your own change handler. The internal
|
|
110
|
+
* drag/resize handlers emit onChange themselves — that's the only
|
|
111
|
+
* source of change notifications. */
|
|
112
|
+
setRect(next: Rect): void {
|
|
113
|
+
this._rect = { ...next };
|
|
114
|
+
this.applyRectToDom();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Programmatically collapse. Silent — see setRect. */
|
|
118
|
+
collapse(): void {
|
|
119
|
+
if (this._collapsed) return;
|
|
120
|
+
this.uncollapsedRect = { ...this._rect };
|
|
121
|
+
this._rect = this.thumbRect();
|
|
122
|
+
this._collapsed = true;
|
|
123
|
+
this.element.setAttribute("data-collapsed", "");
|
|
124
|
+
this.applyRectToDom();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Programmatically expand. Silent — see setRect. */
|
|
128
|
+
expand(): void {
|
|
129
|
+
if (!this._collapsed) return;
|
|
130
|
+
this._rect = this.uncollapsedRect ?? this.defaultRect();
|
|
131
|
+
this._collapsed = false;
|
|
132
|
+
this.element.removeAttribute("data-collapsed");
|
|
133
|
+
this.applyRectToDom();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
show(): void {
|
|
137
|
+
this.element.removeAttribute("data-hidden");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
hide(): void {
|
|
141
|
+
this.element.setAttribute("data-hidden", "");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Whether the card is currently visible (DOM attribute is the
|
|
145
|
+
* source of truth, so this stays accurate if anyone toggles it
|
|
146
|
+
* externally for testing). */
|
|
147
|
+
isVisible(): boolean {
|
|
148
|
+
return !this.element.hasAttribute("data-hidden");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
destroy(): void {
|
|
152
|
+
if (this.destroyed) return;
|
|
153
|
+
this.destroyed = true;
|
|
154
|
+
window.removeEventListener("resize", this.onWindowResize);
|
|
155
|
+
this.element.remove();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── internals ──────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
private buildDom(): HTMLElement {
|
|
161
|
+
const card = document.createElement("div");
|
|
162
|
+
card.className = this.className;
|
|
163
|
+
card.setAttribute("data-hidden", "");
|
|
164
|
+
|
|
165
|
+
const collapseBtn = document.createElement("button");
|
|
166
|
+
collapseBtn.type = "button";
|
|
167
|
+
collapseBtn.className = `${this.className}__collapse`;
|
|
168
|
+
collapseBtn.title = "Collapse to thumbnail";
|
|
169
|
+
collapseBtn.setAttribute("aria-label", "Collapse");
|
|
170
|
+
const collapseIcon = document.createElement("span");
|
|
171
|
+
collapseIcon.setAttribute("aria-hidden", "true");
|
|
172
|
+
collapseIcon.textContent = "−";
|
|
173
|
+
collapseBtn.appendChild(collapseIcon);
|
|
174
|
+
collapseBtn.addEventListener("click", (e) => {
|
|
175
|
+
e.stopPropagation();
|
|
176
|
+
this.collapse();
|
|
177
|
+
this.emit();
|
|
178
|
+
});
|
|
179
|
+
card.appendChild(collapseBtn);
|
|
180
|
+
|
|
181
|
+
for (const corner of ["tl", "tr", "bl", "br"] as const) {
|
|
182
|
+
const handle = document.createElement("span");
|
|
183
|
+
handle.className = `${this.className}__handle ${this.className}__handle--${corner}`;
|
|
184
|
+
handle.dataset.handle = corner;
|
|
185
|
+
handle.setAttribute("aria-hidden", "true");
|
|
186
|
+
card.appendChild(handle);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
card.addEventListener("pointerdown", this.onPointerDown);
|
|
190
|
+
card.addEventListener("pointermove", this.onPointerMove);
|
|
191
|
+
card.addEventListener("pointerup", this.onPointerEnd);
|
|
192
|
+
card.addEventListener("pointercancel", this.onPointerEnd);
|
|
193
|
+
|
|
194
|
+
return card;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private applyRectToDom(): void {
|
|
198
|
+
const r = this._rect;
|
|
199
|
+
this.element.style.left = `${r.x}px`;
|
|
200
|
+
this.element.style.top = `${r.y}px`;
|
|
201
|
+
this.element.style.width = `${r.w}px`;
|
|
202
|
+
this.element.style.height = `${r.h}px`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private emit(): void {
|
|
206
|
+
this.onChange?.({ rect: { ...this._rect }, collapsed: this._collapsed });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private defaultRect(): Rect {
|
|
210
|
+
// Centered, ~58% viewport width, 16:9 unless taller fits.
|
|
211
|
+
const W = window.innerWidth;
|
|
212
|
+
const H = window.innerHeight;
|
|
213
|
+
const rw = W * 0.58;
|
|
214
|
+
const rh = Math.min(rw * (9 / 16), H * 0.54);
|
|
215
|
+
return { x: (W - rw) / 2, y: H * 0.16, w: rw, h: rh };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private thumbRect(): Rect {
|
|
219
|
+
return {
|
|
220
|
+
x: window.innerWidth - this.thumbW - this.thumbMargin,
|
|
221
|
+
y: this.statusBarH + this.thumbMargin,
|
|
222
|
+
w: this.thumbW,
|
|
223
|
+
h: this.thumbH,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private clampRect(r: Rect): Rect {
|
|
228
|
+
return {
|
|
229
|
+
x: Math.max(-(r.w - this.minVisible), Math.min(window.innerWidth - this.minVisible, r.x)),
|
|
230
|
+
y: Math.max(this.statusBarH, Math.min(window.innerHeight - this.minVisible, r.y)),
|
|
231
|
+
w: r.w,
|
|
232
|
+
h: r.h,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private readonly onWindowResize = (): void => {
|
|
237
|
+
if (this._collapsed) {
|
|
238
|
+
// Re-anchor the thumb to the new upper-right corner.
|
|
239
|
+
this._rect = this.thumbRect();
|
|
240
|
+
this.applyRectToDom();
|
|
241
|
+
this.emit();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
private readonly onPointerDown = (e: PointerEvent): void => {
|
|
246
|
+
const target = e.target as HTMLElement;
|
|
247
|
+
if (target.closest(`.${this.className}__collapse`)) return; // collapse btn handles itself
|
|
248
|
+
|
|
249
|
+
if (this._collapsed) {
|
|
250
|
+
// Clicking the thumbnail expands.
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
this.expand();
|
|
253
|
+
this.emit();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
this.element.setPointerCapture(e.pointerId);
|
|
259
|
+
this.drag = {
|
|
260
|
+
startCard: { ...this._rect },
|
|
261
|
+
startPointer: { x: e.clientX, y: e.clientY },
|
|
262
|
+
handle: target.classList.contains(`${this.className}__handle`)
|
|
263
|
+
? (target.dataset.handle ?? null)
|
|
264
|
+
: null,
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
private readonly onPointerMove = (e: PointerEvent): void => {
|
|
269
|
+
if (!this.drag) return;
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
const dx = e.clientX - this.drag.startPointer.x;
|
|
272
|
+
const dy = e.clientY - this.drag.startPointer.y;
|
|
273
|
+
const s = this.drag.startCard;
|
|
274
|
+
const h = this.drag.handle;
|
|
275
|
+
|
|
276
|
+
if (h === null) {
|
|
277
|
+
// Whole-card drag.
|
|
278
|
+
this._rect = this.clampRect({ x: s.x + dx, y: s.y + dy, w: s.w, h: s.h });
|
|
279
|
+
} else {
|
|
280
|
+
// Corner resize. l/r/t/b combinations resize from the matching edge.
|
|
281
|
+
let nx = s.x, ny = s.y, nw = s.w, nh = s.h;
|
|
282
|
+
if (h.includes("r")) nw = Math.max(this.minWidth, s.w + dx);
|
|
283
|
+
if (h.includes("l")) { nw = Math.max(this.minWidth, s.w - dx); nx = s.x + (s.w - nw); }
|
|
284
|
+
if (h.includes("b")) nh = Math.max(this.minHeight, s.h + dy);
|
|
285
|
+
if (h.includes("t")) { nh = Math.max(this.minHeight, s.h - dy); ny = s.y + (s.h - nh); }
|
|
286
|
+
this._rect = { x: nx, y: ny, w: nw, h: nh };
|
|
287
|
+
}
|
|
288
|
+
this.applyRectToDom();
|
|
289
|
+
this.emit();
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
private readonly onPointerEnd = (e: PointerEvent): void => {
|
|
293
|
+
if (!this.drag) return;
|
|
294
|
+
try {
|
|
295
|
+
this.element.releasePointerCapture(e.pointerId);
|
|
296
|
+
} catch {
|
|
297
|
+
/* ignore */
|
|
298
|
+
}
|
|
299
|
+
this.drag = null;
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Public entry point for /prism.
|
|
2
|
+
export {
|
|
3
|
+
PrismPlayer,
|
|
4
|
+
type PrismPlayerOptions,
|
|
5
|
+
type GraphInput,
|
|
6
|
+
type ImageSource,
|
|
7
|
+
} from "./player";
|
|
8
|
+
export { HeadlessSlideshow, type SlideshowOptions } from "./image-feed";
|
|
9
|
+
export {
|
|
10
|
+
ImageOverlay,
|
|
11
|
+
type ImageOverlayOptions,
|
|
12
|
+
type OverlayState,
|
|
13
|
+
type Rect,
|
|
14
|
+
} from "./image-overlay";
|
|
15
|
+
export { GraphRuntime, type ApplyResult, type RuntimeContext } from "./runtime";
|
|
16
|
+
export { SyntheticSignal } from "./synth";
|
|
17
|
+
export {
|
|
18
|
+
createMilkdropBackground,
|
|
19
|
+
type MilkdropBg,
|
|
20
|
+
type MilkdropBgOptions,
|
|
21
|
+
} from "./backends/milkdrop";
|
|
22
|
+
export {
|
|
23
|
+
createShadertoyBackground,
|
|
24
|
+
type ShadertoyBg,
|
|
25
|
+
} from "./backends/shadertoy";
|
|
26
|
+
export { lookup, shortIds, shortIdToGraph, type RegistryEntry } from "./registry";
|
|
27
|
+
export * from "./types";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Ambient declarations for the three peer-dep packages that ship without
|
|
2
|
+
// types. We treat their default exports as `unknown` here and refine
|
|
3
|
+
// inside the backend wrappers via structural casts — that keeps the
|
|
4
|
+
// emitted .d.ts files clean (no implicit any) without pulling in the
|
|
5
|
+
// upstream JS shapes we don't actually want to depend on.
|
|
6
|
+
declare module "butterchurn" {
|
|
7
|
+
const m: unknown;
|
|
8
|
+
export default m;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module "butterchurn-presets" {
|
|
12
|
+
const m: unknown;
|
|
13
|
+
export default m;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module "milkdrop-preset-converter" {
|
|
17
|
+
export const convertPreset: (text: string) => Promise<unknown>;
|
|
18
|
+
}
|