@hyperframes/core 0.8.213 → 0.8.216

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.
@@ -49,3 +49,9 @@ export declare function bandFramesOf(signal: AudioSignal): BandFrames;
49
49
  export declare function rhythm(signal: AudioSignal): Rhythm;
50
50
  /** `rhythm`, yielding between short pieces of work. */
51
51
  export declare function rhythmSteps(signal: AudioSignal): Steps<Rhythm>;
52
+ export interface BeatEvidence {
53
+ confidence: number;
54
+ lift: number;
55
+ }
56
+ /** Steady-beat confidence over six seconds around each time, and its onset's lift there, at any level. */
57
+ export declare function localBeatEvidence(signal: AudioSignal, times: readonly number[], beatSeconds?: number): BeatEvidence[];
@@ -422,3 +422,27 @@ export function* rhythmSteps(signal) {
422
422
  entry.rhythm ??= yield* rhythmOf(entry.frames);
423
423
  return entry.rhythm;
424
424
  }
425
+ const SPAN_SECONDS = 6;
426
+ /** Steady-beat confidence over six seconds around each time, and its onset's lift there, at any level. */
427
+ export function localBeatEvidence(signal, times, beatSeconds) {
428
+ const { broad, fps } = onsetCurves(bandFramesOf(signal));
429
+ const half = Math.round((SPAN_SECONDS / 2) * fps);
430
+ const whole = beatSeconds ?? medianInterval(times) ?? 0;
431
+ if (whole * fps < 2)
432
+ return times.map(() => ({ confidence: 0, lift: 0 }));
433
+ const local = smoothed(broad, whole * fps);
434
+ return times.map((t) => {
435
+ const centre = Math.round((t - ONSET_LAG_SECONDS) * fps);
436
+ const from = Math.max(0, centre - half);
437
+ const to = Math.min(broad.length, centre + half);
438
+ const near = times.filter((other) => Math.abs(other - t) * fps <= half);
439
+ const lag = Math.round((beatSeconds ?? medianInterval(near) ?? whole) * fps);
440
+ const ac = autocorrelation(broad.subarray(from, to), lag + 1);
441
+ const clarity = Math.max(ac[lag - 1] ?? 0, ac[lag] ?? 0, ac[lag + 1] ?? 0);
442
+ const span = local.subarray(from, to);
443
+ const mean = span.reduce((sum, v) => sum + v, 0) / (span.length || 1);
444
+ const peak = Math.max(...[-2, -1, 0, 1, 2].map((d) => local[centre + d] ?? 0));
445
+ const lift = mean > 0 ? peak / mean - 1 : 0;
446
+ return { confidence: beatConfidence(clarity, lift), lift };
447
+ });
448
+ }
@@ -0,0 +1,14 @@
1
+ import { type HfColorGrading, type ResolvedHfColorGrading } from "./colorGrading.js";
2
+ import { type CubeLut3D } from "./colorLuts.js";
3
+ export interface BakeHfColorGradingCubeOptions {
4
+ /** Points per axis. ffmpeg's lut3d reads 2 to 256. */
5
+ size?: number;
6
+ /** The parsed output LUT, required when the grading names one. */
7
+ lut?: CubeLut3D | null;
8
+ }
9
+ /**
10
+ * Bakes a colour grade into a `.cube` 3D LUT (red varying fastest) that matches the runtime's
11
+ * shader, for applying the grade outside a browser, e.g. with ffmpeg's lut3d. Throws for a grade
12
+ * with spatial stages (grain, vignette, blur, halftone and the other effects), which no LUT holds.
13
+ */
14
+ export declare function bakeHfColorGradingCube(grading: HfColorGrading | ResolvedHfColorGrading, options?: BakeHfColorGradingCubeOptions): string;
@@ -0,0 +1,299 @@
1
+ import { calculateHfColorGradingSecondaryMask, hasHfColorGradingHueCurveValues, hasHfColorGradingRgbCurveValues, hasHfColorGradingSecondaryValues, HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS, normalizeHfColorGrading, } from "./colorGrading.js";
2
+ import { compileHfColorCurve, compileHfHueCurve, HF_COLOR_CURVE_LUT_SIZE, } from "./colorGradingCurves.js";
3
+ import { packCubeLutToRgba8, unitFloatToByte } from "./colorLuts.js";
4
+ const LUMA_WEIGHTS = [0.2126, 0.7152, 0.0722];
5
+ const CURVE_MAX_INDEX = HF_COLOR_CURVE_LUT_SIZE - 1;
6
+ const luma = (c) => c[0] * LUMA_WEIGHTS[0] + c[1] * LUMA_WEIGHTS[1] + c[2] * LUMA_WEIGHTS[2];
7
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
8
+ const clampRgb = (c) => [clamp(c[0], 0, 1), clamp(c[1], 0, 1), clamp(c[2], 0, 1)];
9
+ const fract = (value) => value - Math.floor(value);
10
+ const mod = (value, divisor) => value - divisor * Math.floor(value / divisor);
11
+ const mix = (a, b, t) => a + (b - a) * t;
12
+ const mixRgb = (a, b, t) => [
13
+ mix(a[0], b[0], t),
14
+ mix(a[1], b[1], t),
15
+ mix(a[2], b[2], t),
16
+ ];
17
+ const smoothstep = (edge0, edge1, value) => {
18
+ const t = clamp((value - edge0) / (edge1 - edge0), 0, 1);
19
+ return t * t * (3 - 2 * t);
20
+ };
21
+ const towardLuma = (c, base, amount) => [
22
+ mix(base, c[0], amount),
23
+ mix(base, c[1], amount),
24
+ mix(base, c[2], amount),
25
+ ];
26
+ /**
27
+ * A value as the shader reads it back from an 8-bit texture channel. The runtime stages texture
28
+ * data in a Float32Array first, which moves rounding ties: 0.9 packs as 229, not 230.
29
+ */
30
+ const texel = (value) => unitFloatToByte(Math.fround(value)) / 255;
31
+ /** The runtime's encoding of a signed value into an 8-bit channel. */
32
+ const signedUnit = (value, range) => Math.min(1, Math.max(1 / 255, (value * (127 / range) + 128) / 255));
33
+ const decodeSigned = (value) => (value * 255 - 128) / 127;
34
+ const signedTexel = (value, range) => decodeSigned(texel(signedUnit(value, range)));
35
+ function rgbToHsv(c) {
36
+ const maximum = Math.max(c[0], c[1], c[2]);
37
+ const minimum = Math.min(c[0], c[1], c[2]);
38
+ const delta = maximum - minimum;
39
+ let hue = 0;
40
+ if (delta > 0.00001) {
41
+ if (maximum === c[0])
42
+ hue = mod((c[1] - c[2]) / delta, 6);
43
+ else if (maximum === c[1])
44
+ hue = (c[2] - c[0]) / delta + 2;
45
+ else
46
+ hue = (c[0] - c[1]) / delta + 4;
47
+ hue = fract(hue / 6);
48
+ }
49
+ return [hue, maximum > 0.00001 ? delta / maximum : 0, maximum];
50
+ }
51
+ function hsvToRgb([hue, saturation, value]) {
52
+ const channel = (offset) => {
53
+ const band = Math.abs(fract(hue + offset) * 6 - 3);
54
+ return value * mix(1, clamp(band - 1, 0, 1), saturation);
55
+ };
56
+ return [channel(0), channel(0.6666667), channel(0.3333333)];
57
+ }
58
+ function applyPrimaryGrade(input, adjust) {
59
+ const gain = 2 ** adjust.exposure;
60
+ let c = [input[0] * gain, input[1] * gain, input[2] * gain];
61
+ const y = luma(c);
62
+ const lift = adjust.shadows * 0.35 * (1 - smoothstep(0, 0.65, y)) +
63
+ adjust.highlights * 0.35 * smoothstep(0.35, 1, y);
64
+ const blackPoint = clamp(adjust.blacks * 0.18, -0.18, 0.18);
65
+ const whitePoint = clamp(1 - adjust.whites * 0.18, 0.82, 1.18);
66
+ const range = Math.max(whitePoint - blackPoint, 0.2);
67
+ c = c.map((value) => (value + lift - blackPoint) / range);
68
+ c[0] += adjust.temperature * 0.08 + adjust.tint * 0.04;
69
+ c[2] -= adjust.temperature * 0.08 - adjust.tint * 0.04;
70
+ c[1] -= adjust.tint * 0.08;
71
+ const contrast = Math.max(0, 1 + adjust.contrast);
72
+ c = c.map((value) => (value - 0.5) * contrast + 0.5);
73
+ const satLuma = luma(c);
74
+ const currentSat = clamp(Math.max(...c) - Math.min(...c), 0, 1);
75
+ const skinLike = smoothstep(0.02, 0.18, c[0] - c[1]) *
76
+ smoothstep(0, 0.16, c[1] - c[2]) *
77
+ smoothstep(0.18, 0.82, satLuma);
78
+ const vibranceWeight = (1 - currentSat * 0.72) * mix(1, 0.55, skinLike);
79
+ c = towardLuma(c, satLuma, Math.max(0, 1 + adjust.vibrance * vibranceWeight));
80
+ return towardLuma(c, satLuma, Math.max(0, 1 + adjust.saturation));
81
+ }
82
+ function wheelDirection(hue) {
83
+ const direction = hsvToRgb([fract(hue), 1, 1]);
84
+ const y = luma(direction);
85
+ const centred = direction.map((value) => value - y);
86
+ const scale = Math.max(Math.abs(centred[0]), Math.abs(centred[1]), Math.abs(centred[2]), 0.0001);
87
+ return centred.map((value) => value / scale);
88
+ }
89
+ function applyTonalWheels(color, wheels) {
90
+ const y = luma(color);
91
+ const shadows = 1 - smoothstep(0, 0.6, y);
92
+ const highlights = smoothstep(0.4, 1, y);
93
+ const midtones = Math.max(0, 1 - shadows - highlights);
94
+ const total = Math.max(shadows + midtones + highlights, 0.0001);
95
+ const c = [...color];
96
+ for (const [wheel, weight] of [
97
+ [wheels.shadows, shadows / total],
98
+ [wheels.midtones, midtones / total],
99
+ [wheels.highlights, highlights / total],
100
+ ]) {
101
+ const direction = wheelDirection(wheel.hue / 360);
102
+ for (let channel = 0; channel < 3; channel += 1) {
103
+ c[channel] =
104
+ (c[channel] ?? 0) +
105
+ (direction[channel] ?? 0) * wheel.amount * weight * 0.18 +
106
+ wheel.level * weight * 0.25;
107
+ }
108
+ }
109
+ return c;
110
+ }
111
+ function quantisedCurve(samples) {
112
+ return Float64Array.from(samples, texel);
113
+ }
114
+ function sampleCurve(samples, coordinate) {
115
+ const position = clamp(coordinate, 0, 1) * CURVE_MAX_INDEX;
116
+ const lower = Math.floor(position);
117
+ const upper = Math.min(lower + 1, CURVE_MAX_INDEX);
118
+ return mix(samples[lower] ?? 0, samples[upper] ?? 0, position - lower);
119
+ }
120
+ function applyRgbCurves(c, curves) {
121
+ return [
122
+ sampleCurve(curves.red, sampleCurve(curves.master, c[0])),
123
+ sampleCurve(curves.green, sampleCurve(curves.master, c[1])),
124
+ sampleCurve(curves.blue, sampleCurve(curves.master, c[2])),
125
+ ];
126
+ }
127
+ function signedHueCurve(points, outputMin, outputMax) {
128
+ const samples = points.length >= 3
129
+ ? compileHfHueCurve(points, outputMin, outputMax)
130
+ : new Float32Array(HF_COLOR_CURVE_LUT_SIZE);
131
+ return Float64Array.from(samples, (value) => signedTexel(value, outputMax));
132
+ }
133
+ function sampleHueCurve(samples, hue) {
134
+ const position = fract(hue) * HF_COLOR_CURVE_LUT_SIZE;
135
+ const lower = Math.floor(position);
136
+ const upper = (lower + 1) % HF_COLOR_CURVE_LUT_SIZE;
137
+ return mix(samples[lower] ?? 0, samples[upper] ?? 0, position - lower);
138
+ }
139
+ function applyHueCurves(c, curves) {
140
+ const hsv = rgbToHsv(clampRgb(c));
141
+ const originalLuma = luma(c);
142
+ const hueShift = sampleHueCurve(curves.hue, hsv[0]);
143
+ const saturation = sampleHueCurve(curves.saturation, hsv[0]);
144
+ const lumaShift = sampleHueCurve(curves.luma, hsv[0]);
145
+ const shifted = hsvToRgb([
146
+ fract(hsv[0] + hueShift * 0.5),
147
+ clamp(hsv[1] * Math.max(0, 1 + saturation), 0, 1),
148
+ hsv[2],
149
+ ]);
150
+ const offset = originalLuma - luma(shifted) + lumaShift;
151
+ return shifted.map((value) => value + offset);
152
+ }
153
+ function quantisedSecondary({ key, correction, }) {
154
+ const range = (soft) => ({
155
+ min: texel(soft.min),
156
+ max: texel(soft.max),
157
+ softness: texel(soft.softness / 0.5) * 0.5,
158
+ });
159
+ return {
160
+ key: {
161
+ hue: {
162
+ center: texel(key.hue.center / 360) * 360,
163
+ range: texel(key.hue.range / 180) * 180,
164
+ softness: texel(key.hue.softness / 180) * 180,
165
+ },
166
+ saturation: range(key.saturation),
167
+ luma: range(key.luma),
168
+ },
169
+ hueShift: signedTexel(correction.hueShift, 180),
170
+ saturation: signedTexel(correction.saturation, 1),
171
+ luma: signedTexel(correction.luma, 1),
172
+ temperature: signedTexel(correction.temperature, 1),
173
+ tint: signedTexel(correction.tint, 1),
174
+ };
175
+ }
176
+ function applySecondary(c, secondary) {
177
+ const hsv = rgbToHsv(clampRgb(c));
178
+ const mask = calculateHfColorGradingSecondaryMask(hsv[0] * 360, hsv[1], luma(c), secondary.key);
179
+ if (mask <= 0)
180
+ return c;
181
+ const rotated = hsvToRgb([fract(hsv[0] + secondary.hueShift * 0.5), hsv[1], hsv[2]]);
182
+ const corrected = towardLuma(rotated, luma(rotated), Math.max(0, 1 + secondary.saturation)).map((value) => value + secondary.luma);
183
+ corrected[0] += secondary.temperature * 0.08 + secondary.tint * 0.04;
184
+ corrected[2] -= secondary.temperature * 0.08 - secondary.tint * 0.04;
185
+ corrected[1] -= secondary.tint * 0.08;
186
+ return mixRgb(c, corrected, mask);
187
+ }
188
+ function sampleLut(lut, r, g, b) {
189
+ const index = (g * lut.width + b * lut.size + r) * 4;
190
+ return [
191
+ (lut.data[index] ?? 0) / 255,
192
+ (lut.data[index + 1] ?? 0) / 255,
193
+ (lut.data[index + 2] ?? 0) / 255,
194
+ ];
195
+ }
196
+ function applyLut(color, lut) {
197
+ const top = lut.size - 1;
198
+ const scaled = color.map((value, channel) => clamp((value - (lut.domainMin[channel] ?? 0)) / (lut.span[channel] ?? 1), 0, 1) * top);
199
+ const lo = scaled.map(Math.floor);
200
+ const hi = lo.map((value) => Math.min(value + 1, top));
201
+ const f = scaled.map((value, channel) => value - (lo[channel] ?? 0));
202
+ const c00 = mixRgb(sampleLut(lut, lo[0], lo[1], lo[2]), sampleLut(lut, hi[0], lo[1], lo[2]), f[0]);
203
+ const c10 = mixRgb(sampleLut(lut, lo[0], hi[1], lo[2]), sampleLut(lut, hi[0], hi[1], lo[2]), f[0]);
204
+ const c01 = mixRgb(sampleLut(lut, lo[0], lo[1], hi[2]), sampleLut(lut, hi[0], lo[1], hi[2]), f[0]);
205
+ const c11 = mixRgb(sampleLut(lut, lo[0], hi[1], hi[2]), sampleLut(lut, hi[0], hi[1], hi[2]), f[0]);
206
+ const lutColor = mixRgb(mixRgb(c00, c10, f[1]), mixRgb(c01, c11, f[1]), f[2]);
207
+ return mixRgb(color, lutColor, clamp(lut.intensity, 0, 1));
208
+ }
209
+ function spatialStages(grading) {
210
+ const details = ["vignette", "grain"]
211
+ .filter((key) => Math.abs(grading.details[key]) > 0.0001)
212
+ .map((key) => `details.${key}`);
213
+ const effects = HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS.filter((key) => Math.abs(grading.effects[key]) > 0.0001).map((key) => `effects.${key}`);
214
+ return [...details, ...effects];
215
+ }
216
+ function compileLut(grading, lut) {
217
+ if (!grading.lut || grading.lut.intensity === 0)
218
+ return null;
219
+ if (!lut) {
220
+ throw new Error(`The grading applies the LUT ${grading.lut.src}; pass its parsed .cube as options.lut to bake it.`);
221
+ }
222
+ const packed = packCubeLutToRgba8(lut);
223
+ return {
224
+ size: Math.max(lut.size, 2),
225
+ width: packed.width,
226
+ data: packed.data,
227
+ domainMin: [...lut.domainMin],
228
+ span: lut.domainMax.map((max, channel) => Math.max(max - (lut.domainMin[channel] ?? 0), 0.00001)),
229
+ intensity: grading.lut.intensity ?? 0,
230
+ };
231
+ }
232
+ function compileGrade(grading, lut) {
233
+ const spatial = spatialStages(grading);
234
+ if (spatial.length > 0) {
235
+ throw new Error(`A 3D LUT holds only colour; this grading also has spatial stages: ${spatial.join(", ")}.`);
236
+ }
237
+ const { curves, hueCurves, secondaries } = grading;
238
+ return {
239
+ grading,
240
+ curves: hasHfColorGradingRgbCurveValues(curves)
241
+ ? {
242
+ red: quantisedCurve(compileHfColorCurve(curves.red)),
243
+ green: quantisedCurve(compileHfColorCurve(curves.green)),
244
+ blue: quantisedCurve(compileHfColorCurve(curves.blue)),
245
+ master: quantisedCurve(compileHfColorCurve(curves.master)),
246
+ }
247
+ : null,
248
+ hueCurves: hasHfColorGradingHueCurveValues(hueCurves)
249
+ ? {
250
+ hue: signedHueCurve(hueCurves.hueVsHue, -180, 180),
251
+ saturation: signedHueCurve(hueCurves.hueVsSaturation, -1, 1),
252
+ luma: signedHueCurve(hueCurves.hueVsLuma, -1, 1),
253
+ }
254
+ : null,
255
+ secondaries: hasHfColorGradingSecondaryValues(secondaries)
256
+ ? secondaries.filter((secondary) => secondary.enabled).map(quantisedSecondary)
257
+ : [],
258
+ lut: compileLut(grading, lut),
259
+ };
260
+ }
261
+ function gradeColor(input, grade) {
262
+ let c = applyPrimaryGrade(input, grade.grading.adjust);
263
+ c = applyTonalWheels(c, grade.grading.wheels);
264
+ if (grade.curves)
265
+ c = applyRgbCurves(c, grade.curves);
266
+ if (grade.hueCurves)
267
+ c = applyHueCurves(c, grade.hueCurves);
268
+ for (const secondary of grade.secondaries)
269
+ c = applySecondary(c, secondary);
270
+ c = clampRgb(c);
271
+ if (grade.lut)
272
+ c = clampRgb(applyLut(c, grade.lut));
273
+ return clampRgb(mixRgb(input, c, grade.grading.intensity));
274
+ }
275
+ /**
276
+ * Bakes a colour grade into a `.cube` 3D LUT (red varying fastest) that matches the runtime's
277
+ * shader, for applying the grade outside a browser, e.g. with ffmpeg's lut3d. Throws for a grade
278
+ * with spatial stages (grain, vignette, blur, halftone and the other effects), which no LUT holds.
279
+ */
280
+ export function bakeHfColorGradingCube(grading, options = {}) {
281
+ const size = options.size ?? 33;
282
+ if (!Number.isInteger(size) || size < 2 || size > 256) {
283
+ throw new RangeError(`A .cube size must be a whole number from 2 to 256, not ${size}.`);
284
+ }
285
+ const resolved = normalizeHfColorGrading(grading);
286
+ const grade = resolved ? compileGrade(resolved, options.lut) : null;
287
+ const step = size - 1;
288
+ const lines = [`LUT_3D_SIZE ${size}`, "DOMAIN_MIN 0 0 0", "DOMAIN_MAX 1 1 1"];
289
+ for (let b = 0; b < size; b += 1) {
290
+ for (let g = 0; g < size; g += 1) {
291
+ for (let r = 0; r < size; r += 1) {
292
+ const input = [r / step, g / step, b / step];
293
+ const output = grade ? gradeColor(input, grade) : input;
294
+ lines.push(output.map((value) => value.toFixed(6)).join(" "));
295
+ }
296
+ }
297
+ }
298
+ return `${lines.join("\n")}\n`;
299
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Grade groups: one colour grade for a whole lane. An inert `<hf-color-group>`
3
+ * holds the grade in `data-color-grading`; each `<video>`, `<img>` or `<canvas>`
4
+ * joins with `data-color-group` naming the group's id. Membership is held by the
5
+ * member, as with audio groups, so a camera cut into hundreds of clips is graded
6
+ * by editing one attribute.
7
+ */
8
+ export declare const HF_COLOR_GROUP_TAG = "hf-color-group";
9
+ export declare const HF_COLOR_GROUP_ATTR = "data-color-group";
10
+ type GroupLookupRoot = Pick<Document, "getElementById"> | null | undefined;
11
+ /**
12
+ * The group a gradable element belongs to, or null. `data-color-group=""` is no
13
+ * group. Tolerant of partial `Element` doubles, as `audioGroupOf` is.
14
+ */
15
+ export declare function colorGroupOf(el: Element): string | null;
16
+ /**
17
+ * The `<hf-color-group>` for an id, or null. Ids are unique only per
18
+ * composition file, so a member's own composition is searched first, as audio
19
+ * groups pair per composition; then the document. Tag-checked so an unrelated
20
+ * element sharing the id is never read as a lane's grade.
21
+ */
22
+ export declare function resolveColorGroupElement(root: GroupLookupRoot, groupId: string, member?: Element): Element | null;
23
+ /**
24
+ * The grade that applies to `el`: its own `data-color-grading` when present,
25
+ * else its group element's, else null. A group id with no element is ungrouped.
26
+ */
27
+ export declare function effectiveColorGrading(el: Element, root: GroupLookupRoot): string | null;
28
+ /**
29
+ * Make `<hf-color-group>` inert, once per document: an unknown custom element
30
+ * still makes a box, and a grade must not shift authored layout. `contents`, so
31
+ * anything nested inside one still renders; `!important` because an author rule
32
+ * can outrank a bare type selector.
33
+ */
34
+ export declare function ensureColorGroupInertStyle(doc: Document): void;
35
+ export {};
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Grade groups: one colour grade for a whole lane. An inert `<hf-color-group>`
3
+ * holds the grade in `data-color-grading`; each `<video>`, `<img>` or `<canvas>`
4
+ * joins with `data-color-group` naming the group's id. Membership is held by the
5
+ * member, as with audio groups, so a camera cut into hundreds of clips is graded
6
+ * by editing one attribute.
7
+ */
8
+ import { COLOR_GROUP_ATTR, COLOR_GROUP_TAG } from "@hyperframes/parsers/color-grading-contract";
9
+ import { HF_COLOR_GRADING_ATTR, HF_COLOR_GRADING_SOURCE_TAGS } from "./colorGrading.js";
10
+ export const HF_COLOR_GROUP_TAG = COLOR_GROUP_TAG;
11
+ export const HF_COLOR_GROUP_ATTR = COLOR_GROUP_ATTR;
12
+ /**
13
+ * The group a gradable element belongs to, or null. `data-color-group=""` is no
14
+ * group. Tolerant of partial `Element` doubles, as `audioGroupOf` is.
15
+ */
16
+ export function colorGroupOf(el) {
17
+ if (typeof el.tagName !== "string" || typeof el.getAttribute !== "function")
18
+ return null;
19
+ if (!HF_COLOR_GRADING_SOURCE_TAGS.includes(el.tagName.toLowerCase())) {
20
+ return null;
21
+ }
22
+ return el.getAttribute(HF_COLOR_GROUP_ATTR) || null;
23
+ }
24
+ /**
25
+ * The `<hf-color-group>` for an id, or null. Ids are unique only per
26
+ * composition file, so a member's own composition is searched first, as audio
27
+ * groups pair per composition; then the document. Tag-checked so an unrelated
28
+ * element sharing the id is never read as a lane's grade.
29
+ */
30
+ export function resolveColorGroupElement(root, groupId, member) {
31
+ const scope = typeof member?.closest === "function" ? member.closest("[data-composition-id]") : null;
32
+ const scoped = scope
33
+ ? Array.from(scope.getElementsByTagName(HF_COLOR_GROUP_TAG)).find((g) => g.id === groupId)
34
+ : undefined;
35
+ if (scoped)
36
+ return scoped;
37
+ const el = root?.getElementById(groupId) ?? null;
38
+ return el?.tagName.toLowerCase() === HF_COLOR_GROUP_TAG ? el : null;
39
+ }
40
+ /**
41
+ * The grade that applies to `el`: its own `data-color-grading` when present,
42
+ * else its group element's, else null. A group id with no element is ungrouped.
43
+ */
44
+ export function effectiveColorGrading(el, root) {
45
+ const own = el.getAttribute(HF_COLOR_GRADING_ATTR);
46
+ if (own !== null)
47
+ return own;
48
+ const groupId = colorGroupOf(el);
49
+ if (!groupId)
50
+ return null;
51
+ return resolveColorGroupElement(root, groupId, el)?.getAttribute(HF_COLOR_GRADING_ATTR) ?? null;
52
+ }
53
+ /**
54
+ * Make `<hf-color-group>` inert, once per document: an unknown custom element
55
+ * still makes a box, and a grade must not shift authored layout. `contents`, so
56
+ * anything nested inside one still renders; `!important` because an author rule
57
+ * can outrank a bare type selector.
58
+ */
59
+ export function ensureColorGroupInertStyle(doc) {
60
+ const styleId = "__hf-color-group-inert";
61
+ if (!doc?.head || doc.getElementById(styleId))
62
+ return;
63
+ const style = doc.createElement("style");
64
+ style.id = styleId;
65
+ style.textContent = `${HF_COLOR_GROUP_TAG}{display:contents!important}`;
66
+ doc.head.appendChild(style);
67
+ }
@@ -0,0 +1,4 @@
1
+ /** Whether `fetch` follows the proxy variables (`NODE_USE_ENV_PROXY=1` or `--use-env-proxy`). */
2
+ export declare function envProxyEnabled(env?: NodeJS.ProcessEnv, execArgv?: readonly string[]): boolean;
3
+ /** Whether `fetch` reaches the network through `HTTPS_PROXY`, which then does the name resolution. */
4
+ export declare function fetchGoesThroughEnvProxy(env?: NodeJS.ProcessEnv, execArgv?: readonly string[]): boolean;
@@ -0,0 +1,9 @@
1
+ /** Whether `fetch` follows the proxy variables (`NODE_USE_ENV_PROXY=1` or `--use-env-proxy`). */
2
+ export function envProxyEnabled(env = process.env, execArgv = process.execArgv) {
3
+ const flags = [...execArgv, ...(env["NODE_OPTIONS"]?.split(/\s+/u) ?? [])];
4
+ return env["NODE_USE_ENV_PROXY"] === "1" || flags.includes("--use-env-proxy");
5
+ }
6
+ /** Whether `fetch` reaches the network through `HTTPS_PROXY`, which then does the name resolution. */
7
+ export function fetchGoesThroughEnvProxy(env = process.env, execArgv = process.execArgv) {
8
+ return (Boolean((env["HTTPS_PROXY"] ?? env["https_proxy"])?.trim()) && envProxyEnabled(env, execArgv));
9
+ }