@joewinke/jatui 0.10.0 → 0.12.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 +504 -8
- package/dist/components/VideoPlayer.svelte.d.ts +42 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +10 -0
- package/dist/video/README.md +18 -4
- 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/keys.d.ts +4 -4
- package/dist/video/keys.js +4 -3
- package/dist/video/night/clock.d.ts +31 -0
- package/dist/video/night/clock.js +58 -0
- package/dist/video/night/levels.d.ts +78 -0
- package/dist/video/night/levels.js +117 -0
- package/dist/video/night/meter.d.ts +53 -0
- package/dist/video/night/meter.js +115 -0
- package/dist/video/night/track.d.ts +105 -0
- package/dist/video/night/track.js +138 -0
- package/dist/video/night.d.ts +25 -20
- package/dist/video/night.js +25 -20
- package/dist/video/settings.js +10 -1
- package/package.json +3 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The night meter + bias-light sampler (mos-5lzns.33) — pixel math only.
|
|
3
|
+
*
|
|
4
|
+
* extension-library FEATURES.md §Night dimming / §Bias light. The component
|
|
5
|
+
* draws the current frame into a NIGHT_W × NIGHT_H canvas (576 px — an
|
|
6
|
+
* average does not get truer with more pixels, it just gets hotter) and hands
|
|
7
|
+
* the RGBA bytes here. This module never sees the <video> or the canvas: it
|
|
8
|
+
* takes an array of bytes and returns numbers, so it is node-testable and a
|
|
9
|
+
* tainted canvas is the COMPONENT's problem to escalate (getImageData throws
|
|
10
|
+
* there, before anything reaches this file).
|
|
11
|
+
*
|
|
12
|
+
* Bias light reads the SAME bytes the meter just read. That is the
|
|
13
|
+
* extension's hard-won rule: the sampler's canvas is only meaningful right
|
|
14
|
+
* after a fresh draw — read it any other time and it is empty, and an empty
|
|
15
|
+
* canvas reads as pitch black, which kills the glow.
|
|
16
|
+
*/
|
|
17
|
+
export declare const NIGHT_W = 32;
|
|
18
|
+
export declare const NIGHT_H = 18;
|
|
19
|
+
/** A pixel brighter than this counts as blown out. */
|
|
20
|
+
export declare const BLOWN_AT = 0.82;
|
|
21
|
+
/** How much say the blown-out share gets on top of the mean. */
|
|
22
|
+
export declare const BLOWN_WEIGHT = 0.25;
|
|
23
|
+
/** Smoothing: bright arrives fast (it is the thing that hurts)… */
|
|
24
|
+
export declare const METER_ATTACK = 0.5;
|
|
25
|
+
/** …and releases slowly, so a dark cut inside a bright video does not flash the level back up. */
|
|
26
|
+
export declare const METER_RELEASE = 0.12;
|
|
27
|
+
/** The meter's resting value before the first reading (extension parity). */
|
|
28
|
+
export declare const METER_START = 0.55;
|
|
29
|
+
/** Colour push for bias cells: an average is always muddier than the scene it came from. */
|
|
30
|
+
export declare const BIAS_SATURATE = 1.6;
|
|
31
|
+
type Bytes = ArrayLike<number>;
|
|
32
|
+
/**
|
|
33
|
+
* Rec.709 mean luma of an RGBA byte array, plus a term for the blown-out
|
|
34
|
+
* share: `min(1, mean + blownShare × 0.25)`. The mean alone under-reads the
|
|
35
|
+
* case that actually hurts — a dark page with one white pane in it.
|
|
36
|
+
* Returns null for anything that is not a whole number of RGBA pixels.
|
|
37
|
+
*/
|
|
38
|
+
export declare function frameLuma(px: Bytes | null | undefined): number | null;
|
|
39
|
+
/** One asymmetric smoothing step. A null/garbage reading leaves the value where it was. */
|
|
40
|
+
export declare function smoothMeter(prev: number, reading: number | null | undefined): number;
|
|
41
|
+
/**
|
|
42
|
+
* Nine cells (3×3, row-major) of the frame's own colours, each pushed toward
|
|
43
|
+
* saturation: `mean + (channel − mean) × 1.6`. A grey halo behind a video
|
|
44
|
+
* reads as fog, not light. Returns null on a buffer that does not match w×h.
|
|
45
|
+
*/
|
|
46
|
+
export declare function biasCells(px: Bytes | null | undefined, w?: number, h?: number): Array<[number, number, number]> | null;
|
|
47
|
+
/**
|
|
48
|
+
* The glow as one CSS background-image: eight radial gradients (every cell
|
|
49
|
+
* but the centre, which the video itself covers). These rgb() values are the
|
|
50
|
+
* FRAME's colours — data, like a speaker hue — not theme colours.
|
|
51
|
+
*/
|
|
52
|
+
export declare function biasGradient(cells: Array<[number, number, number]> | null | undefined): string;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The night meter + bias-light sampler (mos-5lzns.33) — pixel math only.
|
|
3
|
+
*
|
|
4
|
+
* extension-library FEATURES.md §Night dimming / §Bias light. The component
|
|
5
|
+
* draws the current frame into a NIGHT_W × NIGHT_H canvas (576 px — an
|
|
6
|
+
* average does not get truer with more pixels, it just gets hotter) and hands
|
|
7
|
+
* the RGBA bytes here. This module never sees the <video> or the canvas: it
|
|
8
|
+
* takes an array of bytes and returns numbers, so it is node-testable and a
|
|
9
|
+
* tainted canvas is the COMPONENT's problem to escalate (getImageData throws
|
|
10
|
+
* there, before anything reaches this file).
|
|
11
|
+
*
|
|
12
|
+
* Bias light reads the SAME bytes the meter just read. That is the
|
|
13
|
+
* extension's hard-won rule: the sampler's canvas is only meaningful right
|
|
14
|
+
* after a fresh draw — read it any other time and it is empty, and an empty
|
|
15
|
+
* canvas reads as pitch black, which kills the glow.
|
|
16
|
+
*/
|
|
17
|
+
export const NIGHT_W = 32;
|
|
18
|
+
export const NIGHT_H = 18;
|
|
19
|
+
/** A pixel brighter than this counts as blown out. */
|
|
20
|
+
export const BLOWN_AT = 0.82;
|
|
21
|
+
/** How much say the blown-out share gets on top of the mean. */
|
|
22
|
+
export const BLOWN_WEIGHT = 0.25;
|
|
23
|
+
/** Smoothing: bright arrives fast (it is the thing that hurts)… */
|
|
24
|
+
export const METER_ATTACK = 0.5;
|
|
25
|
+
/** …and releases slowly, so a dark cut inside a bright video does not flash the level back up. */
|
|
26
|
+
export const METER_RELEASE = 0.12;
|
|
27
|
+
/** The meter's resting value before the first reading (extension parity). */
|
|
28
|
+
export const METER_START = 0.55;
|
|
29
|
+
/** Colour push for bias cells: an average is always muddier than the scene it came from. */
|
|
30
|
+
export const BIAS_SATURATE = 1.6;
|
|
31
|
+
/**
|
|
32
|
+
* Rec.709 mean luma of an RGBA byte array, plus a term for the blown-out
|
|
33
|
+
* share: `min(1, mean + blownShare × 0.25)`. The mean alone under-reads the
|
|
34
|
+
* case that actually hurts — a dark page with one white pane in it.
|
|
35
|
+
* Returns null for anything that is not a whole number of RGBA pixels.
|
|
36
|
+
*/
|
|
37
|
+
export function frameLuma(px) {
|
|
38
|
+
if (!px || typeof px.length !== 'number' || px.length < 4 || px.length % 4 !== 0)
|
|
39
|
+
return null;
|
|
40
|
+
let sum = 0;
|
|
41
|
+
let blown = 0;
|
|
42
|
+
const n = px.length / 4;
|
|
43
|
+
for (let i = 0; i < px.length; i += 4) {
|
|
44
|
+
const r = px[i], g = px[i + 1], b = px[i + 2];
|
|
45
|
+
if (!Number.isFinite(r) || !Number.isFinite(g) || !Number.isFinite(b))
|
|
46
|
+
return null;
|
|
47
|
+
const l = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
48
|
+
sum += l;
|
|
49
|
+
if (l > BLOWN_AT)
|
|
50
|
+
blown++;
|
|
51
|
+
}
|
|
52
|
+
return clamp01(sum / n + (blown / n) * BLOWN_WEIGHT);
|
|
53
|
+
}
|
|
54
|
+
/** One asymmetric smoothing step. A null/garbage reading leaves the value where it was. */
|
|
55
|
+
export function smoothMeter(prev, reading) {
|
|
56
|
+
const p = Number.isFinite(prev) ? clamp01(prev) : METER_START;
|
|
57
|
+
if (typeof reading !== 'number' || !Number.isFinite(reading))
|
|
58
|
+
return p;
|
|
59
|
+
const l = clamp01(reading);
|
|
60
|
+
return p + (l - p) * (l > p ? METER_ATTACK : METER_RELEASE);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Nine cells (3×3, row-major) of the frame's own colours, each pushed toward
|
|
64
|
+
* saturation: `mean + (channel − mean) × 1.6`. A grey halo behind a video
|
|
65
|
+
* reads as fog, not light. Returns null on a buffer that does not match w×h.
|
|
66
|
+
*/
|
|
67
|
+
export function biasCells(px, w = NIGHT_W, h = NIGHT_H) {
|
|
68
|
+
if (!px || !(w >= 3) || !(h >= 3) || px.length !== w * h * 4)
|
|
69
|
+
return null;
|
|
70
|
+
const cells = [];
|
|
71
|
+
for (let cy = 0; cy < 3; cy++) {
|
|
72
|
+
for (let cx = 0; cx < 3; cx++) {
|
|
73
|
+
let r = 0, g = 0, b = 0, n = 0;
|
|
74
|
+
const x0 = Math.floor((cx * w) / 3), x1 = Math.floor(((cx + 1) * w) / 3);
|
|
75
|
+
const y0 = Math.floor((cy * h) / 3), y1 = Math.floor(((cy + 1) * h) / 3);
|
|
76
|
+
for (let y = y0; y < y1; y++) {
|
|
77
|
+
for (let x = x0; x < x1; x++) {
|
|
78
|
+
const i = (y * w + x) * 4;
|
|
79
|
+
r += px[i];
|
|
80
|
+
g += px[i + 1];
|
|
81
|
+
b += px[i + 2];
|
|
82
|
+
n++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!n)
|
|
86
|
+
return null;
|
|
87
|
+
const avg = [r / n, g / n, b / n];
|
|
88
|
+
const mean = (avg[0] + avg[1] + avg[2]) / 3;
|
|
89
|
+
const push = avg.map((c) => Math.round(Math.max(0, Math.min(255, mean + (c - mean) * BIAS_SATURATE))));
|
|
90
|
+
cells.push([push[0], push[1], push[2]]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return cells;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The glow as one CSS background-image: eight radial gradients (every cell
|
|
97
|
+
* but the centre, which the video itself covers). These rgb() values are the
|
|
98
|
+
* FRAME's colours — data, like a speaker hue — not theme colours.
|
|
99
|
+
*/
|
|
100
|
+
export function biasGradient(cells) {
|
|
101
|
+
if (!cells || cells.length !== 9)
|
|
102
|
+
return '';
|
|
103
|
+
const at = ['0% 0%', '50% 0%', '100% 0%', '0% 50%', '', '100% 50%', '0% 100%', '50% 100%', '100% 100%'];
|
|
104
|
+
const parts = [];
|
|
105
|
+
for (let i = 0; i < 9; i++) {
|
|
106
|
+
if (i === 4)
|
|
107
|
+
continue;
|
|
108
|
+
const [r, g, b] = cells[i];
|
|
109
|
+
parts.push(`radial-gradient(60% 60% at ${at[i]}, rgb(${r} ${g} ${b}) 0%, transparent 70%)`);
|
|
110
|
+
}
|
|
111
|
+
return parts.join(', ');
|
|
112
|
+
}
|
|
113
|
+
function clamp01(v) {
|
|
114
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
115
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Predictive dimming + the strobe guard + the composer (mos-5lzns.33).
|
|
3
|
+
*
|
|
4
|
+
* extension-library FEATURES.md §The light track. The live meter is honest but
|
|
5
|
+
* always a beat late: the bright frame reaches your eye, THEN the level comes
|
|
6
|
+
* down. With the host's pre-scanned light track (the same `luma` prop the
|
|
7
|
+
* ribbon paints — band/luma.ts) the player reads ahead and starts easing
|
|
8
|
+
* BEFORE the cut lands.
|
|
9
|
+
*
|
|
10
|
+
* Two rules carry the whole design:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Prediction can only ever ADD dimming.** `composeNight` takes the max of
|
|
13
|
+
* the meter and the prediction, so a stale or wrong track degrades to the
|
|
14
|
+
* live meter alone — never to a brighter picture than the meter asked for.
|
|
15
|
+
* 2. **Nothing here ever brightens in a jump.** Darkening lands at once (a
|
|
16
|
+
* sudden drop in light is never the photosensitivity hazard; the CSS ramp
|
|
17
|
+
* eases it anyway). Brightening is released a fraction per tick, so the end
|
|
18
|
+
* of a predicted cut or a flash interval can't snap the level back up —
|
|
19
|
+
* even under `prefers-reduced-motion`, where the CSS ramp shrinks to a
|
|
20
|
+
* short luminance fade.
|
|
21
|
+
*
|
|
22
|
+
* The strobe guard widens each detected flash interval by a lead and a tail
|
|
23
|
+
* and pins its own level to FLASH_LUMA at a FIXED depth of 1 across it —
|
|
24
|
+
* independent of the night dial and of whether night is on at all. The lead
|
|
25
|
+
* also covers one tick of look-ahead (`horizonSec`), because a guard that
|
|
26
|
+
* arrives after the strobe is commentary rather than protection. It REDUCES a
|
|
27
|
+
* flash; it does not make content WCAG 2.3.1-safe, and detecting the flashes
|
|
28
|
+
* is the host's job (the sidecar's `flashes` intervals).
|
|
29
|
+
*/
|
|
30
|
+
import { type LumaSeries, type VideoLumaTrack } from '../band/luma';
|
|
31
|
+
import { type NightLevels } from './levels';
|
|
32
|
+
/** Seconds of look-ahead: long enough to ease, short enough to still be about the moment you are in. */
|
|
33
|
+
export declare const LUMA_LOOKAHEAD = 0.9;
|
|
34
|
+
/** The guard is already down this long before the first flash… */
|
|
35
|
+
export declare const FLASH_LEAD = 0.4;
|
|
36
|
+
/** …and stays down this long after the last. */
|
|
37
|
+
export declare const FLASH_TAIL = 0.3;
|
|
38
|
+
/** The level the guard pins to across a flash interval. */
|
|
39
|
+
export declare const FLASH_LUMA = 0.92;
|
|
40
|
+
/** Fraction of the gap closed per tick when the level is coming back UP. */
|
|
41
|
+
export declare const OUTPUT_RELEASE = 0.25;
|
|
42
|
+
/**
|
|
43
|
+
* Peak brightness due over the next LUMA_LOOKAHEAD (+ one tick's horizon),
|
|
44
|
+
* 0–1, or null without a track. Reuses the ribbon's own max-per-interval
|
|
45
|
+
* reader so the ribbon and the prediction can never disagree.
|
|
46
|
+
*/
|
|
47
|
+
export declare function lumaAhead(series: LumaSeries | null | undefined, t: number, horizonSec?: number): number | null;
|
|
48
|
+
/**
|
|
49
|
+
* Validate the sidecar's flash intervals: finite, ordered pairs, sorted by
|
|
50
|
+
* start. Garbage entries are dropped (never guessed at) — a missing guard is
|
|
51
|
+
* degraded, a guard in the wrong place is wrong.
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizeFlashes(track: VideoLumaTrack | null | undefined): Array<[number, number]>;
|
|
54
|
+
/**
|
|
55
|
+
* Is the guard down at `t`? True inside `[start − lead, end + tail]` of any
|
|
56
|
+
* interval, where the lead is widened by `horizonSec` (the gap until the next
|
|
57
|
+
* evaluation) so a coarse tick can't let the first flash through.
|
|
58
|
+
*/
|
|
59
|
+
export declare function inFlash(flashes: ReadonlyArray<[number, number]>, t: number, horizonSec?: number): boolean;
|
|
60
|
+
export interface NightInput {
|
|
61
|
+
/** Night dimming is armed right now (mode on, or auto inside the window). */
|
|
62
|
+
armed: boolean;
|
|
63
|
+
/** Frame readback was refused — run on NIGHT_BLIND_LUMA instead of the meter. */
|
|
64
|
+
blind: boolean;
|
|
65
|
+
/** The smoothed live meter, 0–1. */
|
|
66
|
+
meter: number;
|
|
67
|
+
/** Predicted peak ahead (null = no track / prediction off). */
|
|
68
|
+
ahead: number | null;
|
|
69
|
+
/** The strobe guard is down (host-detected flash interval, strobeGuard on). */
|
|
70
|
+
flash: boolean;
|
|
71
|
+
/** The user's night depth — night only; the guard never reads it. */
|
|
72
|
+
depth: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The composer's running state — two independent levels. Night's and the
|
|
76
|
+
* guard's are kept apart so each releases on its own terms: a guard ending
|
|
77
|
+
* inside a lighter night must not hand the picture straight back to night's
|
|
78
|
+
* (shallower) level in one step.
|
|
79
|
+
*/
|
|
80
|
+
export interface NightState {
|
|
81
|
+
/** Night's applied luma. */
|
|
82
|
+
luma: number;
|
|
83
|
+
/** The guard's applied luma (FLASH_LUMA while down, released after). */
|
|
84
|
+
guardLuma: number;
|
|
85
|
+
}
|
|
86
|
+
export declare const NIGHT_IDLE: Readonly<NightState>;
|
|
87
|
+
export interface NightFrame extends NightState {
|
|
88
|
+
levels: NightLevels;
|
|
89
|
+
/** Which surface is acting: night (all four layers), the guard alone (level drop only), or nothing. */
|
|
90
|
+
mode: 'night' | 'guard' | 'none';
|
|
91
|
+
/** The guard is down right now — the component switches to the fast ramp and freezes the bias light. */
|
|
92
|
+
guard: boolean;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* One tick of the composer. The component owns the state (feed the frame
|
|
96
|
+
* back as `prev`) and the application; this decides. `mode: 'none'` ⇒ remove
|
|
97
|
+
* every trace.
|
|
98
|
+
*
|
|
99
|
+
* Night: target = max(meter or blind level, prediction) at the user's depth.
|
|
100
|
+
* Guard: target = FLASH_LUMA at GUARD_DEPTH (1) while down, whatever night is
|
|
101
|
+
* doing — armed, disarmed or off. The two stack per channel, the deeper wins.
|
|
102
|
+
* Each level lands darker at once and releases brighter by OUTPUT_RELEASE per
|
|
103
|
+
* tick.
|
|
104
|
+
*/
|
|
105
|
+
export declare function composeNight(prev: Partial<NightState> | null | undefined, input: NightInput): NightFrame;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Predictive dimming + the strobe guard + the composer (mos-5lzns.33).
|
|
3
|
+
*
|
|
4
|
+
* extension-library FEATURES.md §The light track. The live meter is honest but
|
|
5
|
+
* always a beat late: the bright frame reaches your eye, THEN the level comes
|
|
6
|
+
* down. With the host's pre-scanned light track (the same `luma` prop the
|
|
7
|
+
* ribbon paints — band/luma.ts) the player reads ahead and starts easing
|
|
8
|
+
* BEFORE the cut lands.
|
|
9
|
+
*
|
|
10
|
+
* Two rules carry the whole design:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Prediction can only ever ADD dimming.** `composeNight` takes the max of
|
|
13
|
+
* the meter and the prediction, so a stale or wrong track degrades to the
|
|
14
|
+
* live meter alone — never to a brighter picture than the meter asked for.
|
|
15
|
+
* 2. **Nothing here ever brightens in a jump.** Darkening lands at once (a
|
|
16
|
+
* sudden drop in light is never the photosensitivity hazard; the CSS ramp
|
|
17
|
+
* eases it anyway). Brightening is released a fraction per tick, so the end
|
|
18
|
+
* of a predicted cut or a flash interval can't snap the level back up —
|
|
19
|
+
* even under `prefers-reduced-motion`, where the CSS ramp shrinks to a
|
|
20
|
+
* short luminance fade.
|
|
21
|
+
*
|
|
22
|
+
* The strobe guard widens each detected flash interval by a lead and a tail
|
|
23
|
+
* and pins its own level to FLASH_LUMA at a FIXED depth of 1 across it —
|
|
24
|
+
* independent of the night dial and of whether night is on at all. The lead
|
|
25
|
+
* also covers one tick of look-ahead (`horizonSec`), because a guard that
|
|
26
|
+
* arrives after the strobe is commentary rather than protection. It REDUCES a
|
|
27
|
+
* flash; it does not make content WCAG 2.3.1-safe, and detecting the flashes
|
|
28
|
+
* is the host's job (the sidecar's `flashes` intervals).
|
|
29
|
+
*/
|
|
30
|
+
import { lumaPeak } from '../band/luma';
|
|
31
|
+
import { deeperLevels, guardLevels, isNeutral, nightLevels, NIGHT_BLIND_LUMA, NIGHT_NEUTRAL_LEVELS } from './levels';
|
|
32
|
+
/** Seconds of look-ahead: long enough to ease, short enough to still be about the moment you are in. */
|
|
33
|
+
export const LUMA_LOOKAHEAD = 0.9;
|
|
34
|
+
/** The guard is already down this long before the first flash… */
|
|
35
|
+
export const FLASH_LEAD = 0.4;
|
|
36
|
+
/** …and stays down this long after the last. */
|
|
37
|
+
export const FLASH_TAIL = 0.3;
|
|
38
|
+
/** The level the guard pins to across a flash interval. */
|
|
39
|
+
export const FLASH_LUMA = 0.92;
|
|
40
|
+
/** Fraction of the gap closed per tick when the level is coming back UP. */
|
|
41
|
+
export const OUTPUT_RELEASE = 0.25;
|
|
42
|
+
/**
|
|
43
|
+
* Peak brightness due over the next LUMA_LOOKAHEAD (+ one tick's horizon),
|
|
44
|
+
* 0–1, or null without a track. Reuses the ribbon's own max-per-interval
|
|
45
|
+
* reader so the ribbon and the prediction can never disagree.
|
|
46
|
+
*/
|
|
47
|
+
export function lumaAhead(series, t, horizonSec = 0) {
|
|
48
|
+
if (!series || !Number.isFinite(t))
|
|
49
|
+
return null;
|
|
50
|
+
const h = Number.isFinite(horizonSec) && horizonSec > 0 ? horizonSec : 0;
|
|
51
|
+
if (t > series.durationSec)
|
|
52
|
+
return null;
|
|
53
|
+
return lumaPeak(series, Math.max(0, t), Math.max(0, t) + LUMA_LOOKAHEAD + h);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate the sidecar's flash intervals: finite, ordered pairs, sorted by
|
|
57
|
+
* start. Garbage entries are dropped (never guessed at) — a missing guard is
|
|
58
|
+
* degraded, a guard in the wrong place is wrong.
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeFlashes(track) {
|
|
61
|
+
const raw = track?.flashes;
|
|
62
|
+
if (!Array.isArray(raw))
|
|
63
|
+
return [];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const f of raw) {
|
|
66
|
+
if (!Array.isArray(f) || f.length < 2)
|
|
67
|
+
continue;
|
|
68
|
+
const s = f[0], e = f[1];
|
|
69
|
+
if (typeof s !== 'number' || typeof e !== 'number' || !Number.isFinite(s) || !Number.isFinite(e))
|
|
70
|
+
continue;
|
|
71
|
+
out.push(s <= e ? [s, e] : [e, s]);
|
|
72
|
+
}
|
|
73
|
+
return out.sort((a, b) => a[0] - b[0]);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Is the guard down at `t`? True inside `[start − lead, end + tail]` of any
|
|
77
|
+
* interval, where the lead is widened by `horizonSec` (the gap until the next
|
|
78
|
+
* evaluation) so a coarse tick can't let the first flash through.
|
|
79
|
+
*/
|
|
80
|
+
export function inFlash(flashes, t, horizonSec = 0) {
|
|
81
|
+
if (!flashes.length || !Number.isFinite(t))
|
|
82
|
+
return false;
|
|
83
|
+
const h = Number.isFinite(horizonSec) && horizonSec > 0 ? horizonSec : 0;
|
|
84
|
+
for (const [s, e] of flashes) {
|
|
85
|
+
if (s - FLASH_LEAD > t + h)
|
|
86
|
+
return false; // sorted: nothing later can match
|
|
87
|
+
if (t <= e + FLASH_TAIL)
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
export const NIGHT_IDLE = Object.freeze({ luma: 0, guardLuma: 0 });
|
|
93
|
+
/**
|
|
94
|
+
* One tick of the composer. The component owns the state (feed the frame
|
|
95
|
+
* back as `prev`) and the application; this decides. `mode: 'none'` ⇒ remove
|
|
96
|
+
* every trace.
|
|
97
|
+
*
|
|
98
|
+
* Night: target = max(meter or blind level, prediction) at the user's depth.
|
|
99
|
+
* Guard: target = FLASH_LUMA at GUARD_DEPTH (1) while down, whatever night is
|
|
100
|
+
* doing — armed, disarmed or off. The two stack per channel, the deeper wins.
|
|
101
|
+
* Each level lands darker at once and releases brighter by OUTPUT_RELEASE per
|
|
102
|
+
* tick.
|
|
103
|
+
*/
|
|
104
|
+
export function composeNight(prev, input) {
|
|
105
|
+
const pNight = finite01(prev?.luma ?? 0);
|
|
106
|
+
const pGuard = finite01(prev?.guardLuma ?? 0);
|
|
107
|
+
const flash = !!input.flash;
|
|
108
|
+
let nightTarget = 0;
|
|
109
|
+
if (input.armed) {
|
|
110
|
+
const base = input.blind ? NIGHT_BLIND_LUMA : finite01(input.meter);
|
|
111
|
+
nightTarget = Math.max(base, input.ahead == null ? 0 : finite01(input.ahead));
|
|
112
|
+
}
|
|
113
|
+
// Disarmed (the auto window just ended) ⇒ night's level releases per tick
|
|
114
|
+
// like any other brightening rather than dropping to 0 in one step. An
|
|
115
|
+
// explicit `off` is the component's to clear at once — that is a statement.
|
|
116
|
+
const luma = step(pNight, nightTarget);
|
|
117
|
+
const guardLuma = step(pGuard, flash ? FLASH_LUMA : 0);
|
|
118
|
+
const guardL = guardLevels(guardLuma);
|
|
119
|
+
const nightL = nightLevels(luma, input.depth);
|
|
120
|
+
// While releasing after disarm only the level drop carries over; the
|
|
121
|
+
// warm/vignette veils fade out on their own CSS ramp.
|
|
122
|
+
const levels = input.armed
|
|
123
|
+
? deeperLevels(nightL, guardL)
|
|
124
|
+
: deeperLevels({ bright: nightL.bright, sat: nightL.sat, warm: 0, vig: 0 }, guardL);
|
|
125
|
+
if (!input.armed && isNeutral(levels)) {
|
|
126
|
+
return { luma: 0, guardLuma: 0, levels: { ...NIGHT_NEUTRAL_LEVELS }, mode: 'none', guard: false };
|
|
127
|
+
}
|
|
128
|
+
// Settle a released guard to exactly 0 once it no longer changes a pixel.
|
|
129
|
+
const g = isNeutral(guardL) && !flash ? 0 : guardLuma;
|
|
130
|
+
return { luma, guardLuma: g, levels, mode: input.armed ? 'night' : 'guard', guard: flash };
|
|
131
|
+
}
|
|
132
|
+
/** Darker lands at once; brighter is released a fraction per tick. */
|
|
133
|
+
function step(prev, target) {
|
|
134
|
+
return target >= prev ? target : prev + (target - prev) * OUTPUT_RELEASE;
|
|
135
|
+
}
|
|
136
|
+
function finite01(v) {
|
|
137
|
+
return typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0;
|
|
138
|
+
}
|
package/dist/video/night.d.ts
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Night module
|
|
3
|
-
*
|
|
2
|
+
* Night module (mos-5lzns.33) — night dimming, bias light, predictive
|
|
3
|
+
* dimming (the luma track), strobe guard. Ported from extension-library
|
|
4
|
+
* FEATURES.md §Night dimming, §Bias light, §The light track.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* §The light track), as ADVISORS + paint helpers:
|
|
6
|
+
* Layout (every file pure and node-testable — `npm run test:unit`):
|
|
7
7
|
*
|
|
8
|
-
* · night
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
8
|
+
* · night/meter.ts — 32×18 frame bytes → Rec.709 luma (+ blown-out share),
|
|
9
|
+
* asymmetric smoothing (attack 0.5, release 0.12), and the 3×3 bias-light
|
|
10
|
+
* cells read from the SAME bytes.
|
|
11
|
+
* · night/levels.ts — luma → brightness / saturation / warmth / vignette off
|
|
12
|
+
* NIGHT_FLOOR 0.34; the blind fixed level; the surround level that re-tunes
|
|
13
|
+
* night for LIGHT themes.
|
|
14
|
+
* · night/clock.ts — what arms it: local clock (prefers-color-scheme dark only
|
|
15
|
+
* widens the window) + the manual auto/on/off cycle (`N`). No hyprsunset.
|
|
16
|
+
* · night/track.ts — prediction (can only ADD dimming), flash intervals, and
|
|
17
|
+
* `composeNight`, the one-tick decision: darker lands at once, brighter is
|
|
18
|
+
* released per tick, so nothing ever brightens in a jump.
|
|
19
19
|
*
|
|
20
|
-
* The
|
|
21
|
-
* <video>,
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* The advisor rule (README): nothing here reads a setting, touches the
|
|
21
|
+
* <video>, or draws. The component samples the frame (crossorigin="anonymous"
|
|
22
|
+
* is the host's call), escalates a tainted canvas via `onNightBlind`, and
|
|
23
|
+
* applies the numbers as CSS variables that exist only while night acts — off
|
|
24
|
+
* leaves no trace. prefers-reduced-motion drops the ramps, never the
|
|
25
|
+
* correction: the dimming is the feature, the ramp is only how it arrives.
|
|
25
26
|
*/
|
|
26
27
|
export declare const NIGHT_NEUTRAL = 1;
|
|
28
|
+
export * from './night/meter';
|
|
29
|
+
export * from './night/levels';
|
|
30
|
+
export * from './night/clock';
|
|
31
|
+
export * from './night/track';
|
package/dist/video/night.js
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Night module
|
|
3
|
-
*
|
|
2
|
+
* Night module (mos-5lzns.33) — night dimming, bias light, predictive
|
|
3
|
+
* dimming (the luma track), strobe guard. Ported from extension-library
|
|
4
|
+
* FEATURES.md §Night dimming, §Bias light, §The light track.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* §The light track), as ADVISORS + paint helpers:
|
|
6
|
+
* Layout (every file pure and node-testable — `npm run test:unit`):
|
|
7
7
|
*
|
|
8
|
-
* · night
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
8
|
+
* · night/meter.ts — 32×18 frame bytes → Rec.709 luma (+ blown-out share),
|
|
9
|
+
* asymmetric smoothing (attack 0.5, release 0.12), and the 3×3 bias-light
|
|
10
|
+
* cells read from the SAME bytes.
|
|
11
|
+
* · night/levels.ts — luma → brightness / saturation / warmth / vignette off
|
|
12
|
+
* NIGHT_FLOOR 0.34; the blind fixed level; the surround level that re-tunes
|
|
13
|
+
* night for LIGHT themes.
|
|
14
|
+
* · night/clock.ts — what arms it: local clock (prefers-color-scheme dark only
|
|
15
|
+
* widens the window) + the manual auto/on/off cycle (`N`). No hyprsunset.
|
|
16
|
+
* · night/track.ts — prediction (can only ADD dimming), flash intervals, and
|
|
17
|
+
* `composeNight`, the one-tick decision: darker lands at once, brighter is
|
|
18
|
+
* released per tick, so nothing ever brightens in a jump.
|
|
19
19
|
*
|
|
20
|
-
* The
|
|
21
|
-
* <video>,
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* The advisor rule (README): nothing here reads a setting, touches the
|
|
21
|
+
* <video>, or draws. The component samples the frame (crossorigin="anonymous"
|
|
22
|
+
* is the host's call), escalates a tainted canvas via `onNightBlind`, and
|
|
23
|
+
* applies the numbers as CSS variables that exist only while night acts — off
|
|
24
|
+
* leaves no trace. prefers-reduced-motion drops the ramps, never the
|
|
25
|
+
* correction: the dimming is the feature, the ramp is only how it arrives.
|
|
25
26
|
*/
|
|
26
27
|
export const NIGHT_NEUTRAL = 1;
|
|
28
|
+
export * from './night/meter';
|
|
29
|
+
export * from './night/levels';
|
|
30
|
+
export * from './night/clock';
|
|
31
|
+
export * from './night/track';
|
package/dist/video/settings.js
CHANGED
|
@@ -34,7 +34,16 @@ export const VIDEO_SETTINGS = [
|
|
|
34
34
|
{ id: 'player.oneAxis', type: 'bool', default: true, owner: 'mos-5lzns.30', wired: true },
|
|
35
35
|
// { id: 'audio.level', type: 'bool', default: true, owner: 'mos-5lzns.31' },
|
|
36
36
|
// { id: 'player.speakerRate', type: 'bool', default: false, owner: 'mos-5lzns.32' },
|
|
37
|
-
//
|
|
37
|
+
// Read by VideoPlayer's `night` / `nightDepth` / `biasLight` / `predictive` /
|
|
38
|
+
// `strobeGuard` props → video/night/* (clock.nightArmed, levels.nightLevels,
|
|
39
|
+
// meter.biasCells, track.lumaAhead / inFlash / composeNight). Unlike the
|
|
40
|
+
// extension, night.mode's DEFAULT here is the host's opt-in: an absent `night`
|
|
41
|
+
// prop means the feature does not exist on that player.
|
|
42
|
+
{ id: 'night.mode', type: 'enum', default: 'auto', options: ['auto', 'on', 'off'], owner: 'mos-5lzns.33', wired: true },
|
|
43
|
+
{ id: 'night.depth', type: 'range', default: 0.7, min: 0.2, max: 1, step: 0.1, owner: 'mos-5lzns.33', wired: true },
|
|
44
|
+
{ id: 'night.bias', type: 'bool', default: true, owner: 'mos-5lzns.33', wired: true },
|
|
45
|
+
{ id: 'player.predictive', type: 'bool', default: true, owner: 'mos-5lzns.33', wired: true },
|
|
46
|
+
{ id: 'player.strobeGuard', type: 'bool', default: true, owner: 'mos-5lzns.33', wired: true },
|
|
38
47
|
// { id: 'sleep.enabled', type: 'bool', default: true, owner: 'mos-5lzns.34' },
|
|
39
48
|
];
|
|
40
49
|
/** Coerce a raw stored value against a spec — never throws, never returns junk. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joewinke/jatui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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": {
|