@joewinke/jatui 0.9.1 → 0.11.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/CLAUDE.md +10 -0
- package/dist/components/VideoPlayer.svelte +371 -1
- package/dist/components/VideoPlayer.svelte.d.ts +44 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +10 -0
- package/dist/video/README.md +18 -3
- package/dist/video/advisors/README.md +20 -0
- package/dist/video/advisors/qr.d.ts +95 -0
- package/dist/video/advisors/qr.js +546 -0
- package/dist/video/advisors/quiet-listening.d.ts +73 -0
- package/dist/video/advisors/quiet-listening.js +292 -0
- package/dist/video/advisors/resume-point.d.ts +71 -0
- package/dist/video/advisors/resume-point.js +302 -0
- package/dist/video/advisors/smart-speed.d.ts +82 -0
- package/dist/video/advisors/smart-speed.js +314 -0
- package/dist/video/advisors/speaker-rate.d.ts +74 -0
- package/dist/video/advisors/speaker-rate.js +322 -0
- package/dist/video/band/axis.d.ts +81 -0
- package/dist/video/band/axis.js +106 -0
- package/dist/video/band/drag.d.ts +38 -0
- package/dist/video/band/drag.js +45 -0
- package/dist/video/band/luma.d.ts +62 -0
- package/dist/video/band/luma.js +97 -0
- package/dist/video/band/markers.d.ts +73 -0
- package/dist/video/band/markers.js +146 -0
- package/dist/video/band/memo.d.ts +21 -0
- package/dist/video/band/memo.js +15 -0
- package/dist/video/band/window.d.ts +35 -0
- package/dist/video/band/window.js +61 -0
- package/dist/video/band.d.ts +35 -29
- package/dist/video/band.js +77 -56
- package/dist/video/keys.d.ts +1 -1
- package/dist/video/keys.js +3 -0
- package/dist/video/settings.js +6 -5
- package/package.json +3 -2
package/dist/video/band.js
CHANGED
|
@@ -22,61 +22,13 @@
|
|
|
22
22
|
* re-render of this module.
|
|
23
23
|
*/
|
|
24
24
|
import { ALL_KEY, filterDims, speakerKeyOf } from './model';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
export function isZoomed(w, duration) {
|
|
34
|
-
return w.end > 0 && spanSec(w, duration) < duration - 0.01;
|
|
35
|
-
}
|
|
36
|
-
export function resetWindow(duration) {
|
|
37
|
-
return { start: 0, end: duration };
|
|
38
|
-
}
|
|
39
|
-
export function secToFrac(w, duration, sec) {
|
|
40
|
-
const span = spanSec(w, duration);
|
|
41
|
-
return span > 0 ? (sec - w.start) / span : 0;
|
|
42
|
-
}
|
|
43
|
-
export function fracToSec(w, duration, frac) {
|
|
44
|
-
return w.start + frac * spanSec(w, duration);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Scroll to zoom, centred on the cursor — the DAW/mpv convention: the point
|
|
48
|
-
* under the pointer stays fixed while the window resizes around it, so
|
|
49
|
-
* scrolling repeatedly walks you inward on exactly the spot you're pointing
|
|
50
|
-
* at. `deltaY < 0` zooms in.
|
|
51
|
-
*/
|
|
52
|
-
export const ZOOM_FACTOR = 1.3;
|
|
53
|
-
export const MIN_WINDOW_SEC = 3;
|
|
54
|
-
export function zoomAt(w, duration, cursorSec, deltaY) {
|
|
55
|
-
if (!duration)
|
|
56
|
-
return w;
|
|
57
|
-
if (w.end <= 0)
|
|
58
|
-
w = resetWindow(duration);
|
|
59
|
-
const span = spanSec(w, duration);
|
|
60
|
-
let newSpan = deltaY < 0 ? span / ZOOM_FACTOR : span * ZOOM_FACTOR;
|
|
61
|
-
newSpan = Math.min(duration, Math.max(MIN_WINDOW_SEC, newSpan));
|
|
62
|
-
const frac = span > 0 ? (cursorSec - w.start) / span : 0.5;
|
|
63
|
-
const start = Math.max(0, Math.min(cursorSec - frac * newSpan, duration - newSpan));
|
|
64
|
-
return { start, end: start + newSpan };
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Zoomed in and playback walked past the edge of the window — follow it, or
|
|
68
|
-
* you'd watch a frozen slice of waveform while the video plays on. Returns
|
|
69
|
-
* null when no follow is needed.
|
|
70
|
-
*/
|
|
71
|
-
export function followPlayhead(w, duration, t) {
|
|
72
|
-
if (!isZoomed(w, duration))
|
|
73
|
-
return null;
|
|
74
|
-
if (t >= w.start && t <= w.end)
|
|
75
|
-
return null;
|
|
76
|
-
const span = spanSec(w, duration);
|
|
77
|
-
const start = Math.max(0, Math.min(t - span / 2, Math.max(0, duration - span)));
|
|
78
|
-
return { start, end: start + span };
|
|
79
|
-
}
|
|
25
|
+
// ── Zoom window: lives in band/window.ts (shared with the band/ layers) ─────
|
|
26
|
+
import { fracToSec, secToFrac } from './band/window';
|
|
27
|
+
import { PLAYHEAD_W, playheadX } from './band/axis';
|
|
28
|
+
import { ribbonColumns } from './band/luma';
|
|
29
|
+
import { commentClusters, HEAT_BIN_PX, likeHeat } from './band/markers';
|
|
30
|
+
import { memo } from './band/memo';
|
|
31
|
+
export { fracToSec, followPlayhead, isZoomed, MIN_WINDOW_SEC, resetWindow, secToFrac, spanSec, ZOOM_FACTOR, zoomAt } from './band/window';
|
|
80
32
|
// ── Hit tests / position helpers ────────────────────────────────────────────
|
|
81
33
|
/**
|
|
82
34
|
* Click-to-seek biases toward the START of the spoken line under the click —
|
|
@@ -141,6 +93,11 @@ export function clampCardX(x, width) {
|
|
|
141
93
|
}
|
|
142
94
|
// ── Canvas paint ────────────────────────────────────────────────────────────
|
|
143
95
|
export const RAIL_H = 10; // no-waveform fallback: a flat rail instead of an envelope
|
|
96
|
+
/** Like heat strip height, px, along the band's bottom edge (under the bookmark diamonds). */
|
|
97
|
+
export const HEAT_H = 3;
|
|
98
|
+
/** Comment dot radius and centre line (just under the chapter flags, which occupy y 0–4). */
|
|
99
|
+
export const COMMENT_R = 2.5;
|
|
100
|
+
export const COMMENT_Y = 7.5;
|
|
144
101
|
/**
|
|
145
102
|
* Size the canvas backing store for devicePixelRatio and return its 2d
|
|
146
103
|
* context with the transform applied — or null when there is nothing honest
|
|
@@ -183,6 +140,27 @@ export function paintBand(ctx, W, H, s) {
|
|
|
183
140
|
ctx.globalAlpha = 1;
|
|
184
141
|
return;
|
|
185
142
|
}
|
|
143
|
+
// The light ribbon: a wash under the envelope, through the same window as
|
|
144
|
+
// the peaks so it zooms with everything else. Painted FIRST so every
|
|
145
|
+
// other layer sits on top of it — it is a property of the timeline, not
|
|
146
|
+
// a fourth bar (FEATURES.md §The light track).
|
|
147
|
+
if (s.luma) {
|
|
148
|
+
const RIBBON_COL_PX = 2;
|
|
149
|
+
const cols = Math.max(1, Math.ceil(W / RIBBON_COL_PX));
|
|
150
|
+
const luma = s.luma;
|
|
151
|
+
const compute = () => ribbonColumns(luma, s.window, dur, cols);
|
|
152
|
+
const alphas = s.cache
|
|
153
|
+
? memo(s.cache.ribbon, [luma, s.window.start, s.window.end, dur, cols], compute)
|
|
154
|
+
: compute();
|
|
155
|
+
ctx.fillStyle = s.colors.ribbon ?? marker;
|
|
156
|
+
for (let i = 0; i < alphas.length; i++) {
|
|
157
|
+
if (alphas[i] <= 0.004)
|
|
158
|
+
continue;
|
|
159
|
+
ctx.globalAlpha = alphas[i];
|
|
160
|
+
ctx.fillRect((i * W) / cols, 0, W / cols + 0.5, H);
|
|
161
|
+
}
|
|
162
|
+
ctx.globalAlpha = 1;
|
|
163
|
+
}
|
|
186
164
|
const p = s.peaks;
|
|
187
165
|
const withWave = !!p;
|
|
188
166
|
const n = p?.length ?? 0;
|
|
@@ -277,6 +255,38 @@ export function paintBand(ctx, W, H, s) {
|
|
|
277
255
|
ctx.closePath();
|
|
278
256
|
ctx.fill();
|
|
279
257
|
}
|
|
258
|
+
// Likes: one heat strip along the bottom edge, never individual glyphs.
|
|
259
|
+
if (s.likes && s.likes.length) {
|
|
260
|
+
const bins = Math.max(1, Math.ceil(W / HEAT_BIN_PX));
|
|
261
|
+
const likes = s.likes;
|
|
262
|
+
const compute = () => likeHeat(likes, w, dur, bins);
|
|
263
|
+
const heat = s.cache ? memo(s.cache.heat, [likes, w.start, w.end, dur, bins], compute) : compute();
|
|
264
|
+
ctx.fillStyle = s.colors.like ?? accent;
|
|
265
|
+
for (let i = 0; i < heat.length; i++) {
|
|
266
|
+
if (heat[i] <= 0)
|
|
267
|
+
continue;
|
|
268
|
+
ctx.globalAlpha = 0.15 + 0.8 * heat[i];
|
|
269
|
+
ctx.fillRect((i * W) / bins, H - HEAT_H, W / bins + 0.5, HEAT_H);
|
|
270
|
+
}
|
|
271
|
+
ctx.globalAlpha = 1;
|
|
272
|
+
}
|
|
273
|
+
// Comments: round dots just under the chapter flags, merged when close.
|
|
274
|
+
if (s.comments && s.comments.length) {
|
|
275
|
+
const surface = s.colors.surface;
|
|
276
|
+
for (const c of bandClusters(s.comments, w, dur, W, s.cache)) {
|
|
277
|
+
ctx.beginPath();
|
|
278
|
+
ctx.arc(c.x, COMMENT_Y, COMMENT_R + (c.markers.length > 1 ? 0.75 : 0), 0, Math.PI * 2);
|
|
279
|
+
if (surface) {
|
|
280
|
+
ctx.globalAlpha = 0.9;
|
|
281
|
+
ctx.lineWidth = 1.5;
|
|
282
|
+
ctx.strokeStyle = surface;
|
|
283
|
+
ctx.stroke();
|
|
284
|
+
}
|
|
285
|
+
ctx.globalAlpha = 1;
|
|
286
|
+
ctx.fillStyle = s.colors.comment ?? accent;
|
|
287
|
+
ctx.fill();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
280
290
|
// Bookmarks: diamonds pointing up from the BOTTOM edge — the mirror image
|
|
281
291
|
// of the chapter flags on top, so yours and the model's never blur together.
|
|
282
292
|
ctx.fillStyle = marker;
|
|
@@ -295,7 +305,9 @@ export function paintBand(ctx, W, H, s) {
|
|
|
295
305
|
// Playhead
|
|
296
306
|
ctx.globalAlpha = 0.9;
|
|
297
307
|
ctx.fillStyle = neutral;
|
|
298
|
-
|
|
308
|
+
// Centred on playheadX — the one function every "where you are" widget
|
|
309
|
+
// uses (band/axis.ts), so the overview rail and the band agree to ≤0.2px.
|
|
310
|
+
ctx.fillRect(playheadX(s.currentTime, w, dur, W) - PLAYHEAD_W / 2, 0, PLAYHEAD_W, H);
|
|
299
311
|
ctx.globalAlpha = 1;
|
|
300
312
|
}
|
|
301
313
|
/** Turn → BandTurn for the paint state (resolved key, ad flag). */
|
|
@@ -309,3 +321,12 @@ export function toBandTurns(turns) {
|
|
|
309
321
|
}
|
|
310
322
|
// Re-exports so the component (and sibling modules) have one band import site.
|
|
311
323
|
export { ALL_KEY };
|
|
324
|
+
/**
|
|
325
|
+
* Comment clusters for this window/width — memoised when a cache is given.
|
|
326
|
+
* The component's hit test calls this with the SAME cache as the paint, so a
|
|
327
|
+
* mousemove never re-clusters what the last repaint already did.
|
|
328
|
+
*/
|
|
329
|
+
export function bandClusters(comments, w, duration, width, cache) {
|
|
330
|
+
const compute = () => commentClusters(comments, w, duration, width);
|
|
331
|
+
return cache ? memo(cache.clusters, [comments, w.start, w.end, duration, width], compute) : compute();
|
|
332
|
+
}
|
package/dist/video/keys.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface PlayerKeyRow {
|
|
|
20
20
|
* Availability gate: the row is only advertised (and the key only bound)
|
|
21
21
|
* when the feature's data exists. Undefined ⇒ always active.
|
|
22
22
|
*/
|
|
23
|
-
when?: 'chapters' | 'bookmarks' | 'transcript';
|
|
23
|
+
when?: 'chapters' | 'bookmarks' | 'transcript' | 'comments';
|
|
24
24
|
}
|
|
25
25
|
/** Order follows the help overlay: transport, seek, volume, speed, structure. */
|
|
26
26
|
export declare const PLAYER_KEYS: PlayerKeyRow[];
|
package/dist/video/keys.js
CHANGED
|
@@ -23,6 +23,9 @@ export const PLAYER_KEYS = [
|
|
|
23
23
|
{ keys: 'Backspace', description: 'Reset speed to 1×' },
|
|
24
24
|
{ keys: '< / >', description: 'Frame step (pauses; ~1/30s per press)' },
|
|
25
25
|
{ keys: '[ / ]', description: 'Previous / next chapter', when: 'chapters' },
|
|
26
|
+
// Shift+[ / Shift+] — the chapter keys' shifted twins, so "structure"
|
|
27
|
+
// and "people's reactions to it" sit under one hand (mos-5lzns.30).
|
|
28
|
+
{ keys: '{ / }', description: 'Previous / next comment', when: 'comments' },
|
|
26
29
|
{ keys: 'B', description: 'Bookmark at the current time', when: 'bookmarks' },
|
|
27
30
|
{ keys: 'T', description: 'Show / hide the transcript', when: 'transcript' },
|
|
28
31
|
{ keys: 'Home / End', description: 'Jump to start / end' },
|
package/dist/video/settings.js
CHANGED
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
* the honest state while every behaviour is prop-driven.
|
|
31
31
|
*/
|
|
32
32
|
export const VIDEO_SETTINGS = [
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
// { id: '
|
|
36
|
-
// { id: '
|
|
37
|
-
// { id: '
|
|
33
|
+
// Read by VideoPlayer's `oneAxis` prop → video/band/axis.ts resolveAxis().
|
|
34
|
+
{ id: 'player.oneAxis', type: 'bool', default: true, owner: 'mos-5lzns.30', wired: true },
|
|
35
|
+
// { id: 'audio.level', type: 'bool', default: true, owner: 'mos-5lzns.31' },
|
|
36
|
+
// { id: 'player.speakerRate', type: 'bool', default: false, owner: 'mos-5lzns.32' },
|
|
37
|
+
// { id: 'night.mode', type: 'enum', default: 'auto', options: ['auto','on','off'], owner: 'mos-5lzns.33' },
|
|
38
|
+
// { id: 'sleep.enabled', type: 'bool', default: true, owner: 'mos-5lzns.34' },
|
|
38
39
|
];
|
|
39
40
|
/** Coerce a raw stored value against a spec — never throws, never returns junk. */
|
|
40
41
|
export function coerce(spec, raw) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joewinke/jatui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared Svelte 5 component library for JAT projects",
|
|
6
6
|
"type": "module",
|
|
@@ -76,7 +76,8 @@
|
|
|
76
76
|
"check:demos": "node scripts/check-demo-coverage.mjs",
|
|
77
77
|
"guard": "node scripts/check-demo-coverage.mjs",
|
|
78
78
|
"test:dist": "svelte-kit sync && vitest run --config vitest.dist.config.ts",
|
|
79
|
-
"test:unit": "node --import ./tests/unit/register.mjs --test tests/unit
|
|
79
|
+
"test:unit": "node --import ./tests/unit/register.mjs --test tests/unit/*.test.ts",
|
|
80
|
+
"test:advisors": "svelte-kit sync && vitest run --config vitest.unit.config.ts",
|
|
80
81
|
"prepublishOnly": "node scripts/guard-ci-publish.mjs && npm run package"
|
|
81
82
|
},
|
|
82
83
|
"peerDependencies": {
|