@joewinke/jatui 0.11.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/dist/components/VideoPlayer.svelte +504 -8
- package/dist/components/VideoPlayer.svelte.d.ts +42 -1
- package/dist/index.d.ts +1 -1
- package/dist/video/README.md +1 -1
- 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 +1 -1
|
@@ -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. */
|