@fusion.dev/helix 0.217.0 → 0.218.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/index.d.ts CHANGED
@@ -126,3 +126,4 @@ export * from './trend/index.js';
126
126
  export * from './video/index.js';
127
127
  export * from './video-playlist/index.js';
128
128
  export * from './virtual-list/index.js';
129
+ export * from './voice-level/index.js';
package/dist/index.js CHANGED
@@ -121,3 +121,4 @@ export * from './trend/index.js';
121
121
  export * from './video/index.js';
122
122
  export * from './video-playlist/index.js';
123
123
  export * from './virtual-list/index.js';
124
+ export * from './voice-level/index.js';
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The arithmetic behind a voice level's two drawings, with no canvas in it.
3
+ *
4
+ * Every function takes plain numbers and returns plain numbers, so the shapes
5
+ * are held by a pure spec and the element is left with measuring a box and
6
+ * filling paths.
7
+ */
8
+ /** What a voice level draws. */
9
+ export type VoiceLevelVariant = 'spectrum' | 'waves';
10
+ /** The bar count the spectrum draws where `--voice-level-bars` says nothing. */
11
+ export declare const DEFAULT_BARS = 24;
12
+ /** The fewest and the most bars a spectrum draws, whatever the hook asks for. */
13
+ export declare const MIN_BARS = 3;
14
+ export declare const MAX_BARS = 96;
15
+ /**
16
+ * How fast a bar or a wave follows a louder reading and a quieter one, as a
17
+ * rate per second.
18
+ *
19
+ * **Up fast and down slower**, as a meter's needle moves: a syllable shows the
20
+ * frame it starts, and the fall that follows is slow enough to read.
21
+ */
22
+ export declare const RISE = 22;
23
+ export declare const FALL = 6;
24
+ /**
25
+ * How long the drawing has to rest at zero before its loop stops, in seconds,
26
+ * counted from the end of the fall, so the last frame drawn is the resting
27
+ * shape.
28
+ */
29
+ export declare const QUIET_STOP = 0.5;
30
+ /** Below this a reading is silence. */
31
+ export declare const SILENT = 0.002;
32
+ /** A reading clamped to the range a level is written in. */
33
+ export declare const unit: (value: number) => number;
34
+ /**
35
+ * The bar count, from what the hook computed to.
36
+ *
37
+ * Anything that is not a whole number in the range keeps the default or is
38
+ * brought into it.
39
+ */
40
+ export declare const barCount: (written: string) => number;
41
+ /**
42
+ * How tall bar `index` of `count` may stand, from about a quarter at either end
43
+ * to the whole box in the middle.
44
+ *
45
+ * **Center weighted**, so the row reads as one shape that swells with the
46
+ * voice. Without it, the loud low bands sit at one end and the drawing leans.
47
+ */
48
+ export declare const envelope: (index: number, count: number) => number;
49
+ /**
50
+ * Whether bar `index` of `count` is one of the center bars, which take the
51
+ * stronger color. The middle half of the row is.
52
+ */
53
+ export declare const centered: (index: number, count: number) => boolean;
54
+ /**
55
+ * The reading bar `index` of `count` follows: its band, where `bands` holds a
56
+ * different number of bands than there are bars read between the two nearest,
57
+ * and the level where there are no bands at all.
58
+ */
59
+ export declare const bandFor: (bands: readonly number[], level: number, index: number, count: number) => number;
60
+ /**
61
+ * One step of `current` toward `target` over `delta` seconds, rising at
62
+ * `RISE` and falling at `FALL`. Exponential, so the step is the same however
63
+ * the frames are spaced.
64
+ */
65
+ export declare const ease: (current: number, target: number, delta: number) => number;
66
+ /** One of the three lines the waves draw. */
67
+ export interface WaveLine {
68
+ /** How many whole waves fit across the box. */
69
+ cycles: number;
70
+ /** Which color the line takes: the main color, the accent, or the main color faded. */
71
+ ink: 'accent' | 'faint' | 'main';
72
+ /** Where the line starts in its cycle, in radians. */
73
+ phase: number;
74
+ /** How fast it travels, in radians a second. A negative speed travels the other way. */
75
+ speed: number;
76
+ /** Its share of the amplitude, `1` for the main line. */
77
+ weight: number;
78
+ /** Its stroke, in CSS pixels. */
79
+ width: number;
80
+ }
81
+ /**
82
+ * The three lines, in the order they are drawn, the main line last so it lies
83
+ * on top. **Different speeds, phases, and weights**, so the three cross and
84
+ * part and never move as one.
85
+ */
86
+ export declare const WAVE_LINES: readonly WaveLine[];
87
+ /**
88
+ * The moment the waves are drawn at when the element is `still`, in seconds.
89
+ * Fixed, so a still picture is the same on every load.
90
+ */
91
+ export declare const STILL_TIME = 0;
92
+ /**
93
+ * How far a wave may leave the center line at `across`, from `0` at the start
94
+ * to `1` at the end: nothing at either end and all of it in the middle, so
95
+ * every line is pinned to the center at both ends.
96
+ */
97
+ export declare const pinned: (across: number) => number;
98
+ /**
99
+ * How far `line` stands from the center line at `across`, at `time` seconds,
100
+ * as a share of the amplitude: from `-1` to `1`.
101
+ */
102
+ export declare const waveAt: (line: WaveLine, across: number, time: number) => number;
103
+ /**
104
+ * The length of the still bar drawn under `prefers-reduced-motion`: the level's
105
+ * share of `span`, and never shorter than its own `thickness`, so silence is a
106
+ * dot.
107
+ */
108
+ export declare const stillLength: (level: number, span: number, thickness: number) => number;
@@ -0,0 +1,53 @@
1
+ export const DEFAULT_BARS = 24;
2
+ export const MIN_BARS = 3;
3
+ export const MAX_BARS = 96;
4
+ export const RISE = 22;
5
+ export const FALL = 6;
6
+ export const QUIET_STOP = 0.5;
7
+ export const SILENT = 0.002;
8
+ const SETTLED = 0.004;
9
+ export const unit = (value) => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
10
+ export const barCount = (written) => {
11
+ const read = Number.parseInt(written, 10);
12
+ if (!Number.isFinite(read)) {
13
+ return DEFAULT_BARS;
14
+ }
15
+ return Math.min(MAX_BARS, Math.max(MIN_BARS, read));
16
+ };
17
+ export const envelope = (index, count) => {
18
+ if (count <= 1) {
19
+ return 1;
20
+ }
21
+ const across = Math.sin((Math.PI * (index + 0.5)) / count);
22
+ return 0.25 + 0.75 * across ** 1.2;
23
+ };
24
+ export const centered = (index, count) => Math.abs(index + 0.5 - count / 2) < count / 4;
25
+ export const bandFor = (bands, level, index, count) => {
26
+ if (bands.length === 0) {
27
+ return unit(level);
28
+ }
29
+ if (bands.length === 1) {
30
+ return unit(bands[0] ?? 0);
31
+ }
32
+ const at = ((index + 0.5) / count) * bands.length - 0.5;
33
+ const low = Math.max(0, Math.min(bands.length - 1, Math.floor(at)));
34
+ const high = Math.min(bands.length - 1, low + 1);
35
+ const mix = Math.min(1, Math.max(0, at - low));
36
+ return unit((bands[low] ?? 0) * (1 - mix) + (bands[high] ?? 0) * mix);
37
+ };
38
+ export const ease = (current, target, delta) => {
39
+ const rate = target > current ? RISE : FALL;
40
+ const next = current + (target - current) * (1 - Math.exp(-rate * delta));
41
+ return Math.abs(next - target) < SETTLED ? target : next;
42
+ };
43
+ export const WAVE_LINES = [
44
+ { cycles: 2.6, ink: 'faint', phase: 2.4, speed: 3.4, weight: 0.5, width: 1 },
45
+ { cycles: 1.9, ink: 'accent', phase: 1.1, speed: -2.3, weight: 0.75, width: 1.5 },
46
+ { cycles: 1.3, ink: 'main', phase: 0, speed: 1.8, weight: 1, width: 2 },
47
+ ];
48
+ export const STILL_TIME = 0;
49
+ export const pinned = (across) => Math.sin(Math.PI * unit(across)) ** 2;
50
+ export const waveAt = (line, across, time) => line.weight *
51
+ pinned(across) *
52
+ Math.sin(Math.PI * 2 * line.cycles * across + line.phase + line.speed * time);
53
+ export const stillLength = (level, span, thickness) => Math.max(thickness, unit(level) * span);
@@ -0,0 +1,2 @@
1
+ export type { VoiceLevelVariant } from './draw.js';
2
+ export * from './root/voice-level-root.js';
@@ -0,0 +1 @@
1
+ export * from './root/voice-level-root.js';
@@ -0,0 +1 @@
1
+ export declare const styles: import("lit").CSSResult;
@@ -0,0 +1,2 @@
1
+ import { createSheet } from '@fusion.dev/helix-core';
2
+ export const styles = createSheet(`:host {--_voice-level-block-size: var(--voice-level-block-size, var(--helix-size-icon));--_voice-level-ratio: 4;--_voice-level-color: var(--voice-level-color, var(--helix-color-primary));block-size: var(--_voice-level-block-size);display: inline-block;flex-shrink: 0;inline-size: var(--voice-level-inline-size, calc(var(--_voice-level-block-size) * var(--_voice-level-ratio)));vertical-align: middle}:host([variant="waves"]) {--_voice-level-block-size: var(--voice-level-block-size, var(--helix-size-control));--_voice-level-ratio: 3}[part="canvas"] {animation-iteration-count: var(--voice-level-bars, 24);block-size: 100%;color: var(--_voice-level-color);column-rule-color: var(--voice-level-accent-color, var(--helix-color-accent));display: block;inline-size: 100%;text-decoration-color: var(--voice-level-edge-color, color-mix(in oklab, var(--_voice-level-color) 55%, transparent));transition-duration: var(--helix-duration-fast);transition-property: color, column-rule-color, text-decoration-color}[part="canvas"][hidden] {display: none}@media print {:host {display: none}}`);
@@ -0,0 +1,186 @@
1
+ import { HelixElement } from '@fusion.dev/helix-core';
2
+ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
3
+ import { type VoiceLevelVariant } from '../draw.js';
4
+ /**
5
+ * @summary A live level of a voice, drawn as a row of bars or as moving waves.
6
+ *
7
+ * @description
8
+ * Two drawings, chosen with `variant`:
9
+ *
10
+ * - `spectrum`: a row of thin rounded bars, one for each frequency band in
11
+ * `bands`, tallest in the middle. The middle half of the row takes the
12
+ * stronger color. A quiet bar rests as a dot.
13
+ * - `waves`: three sine lines with different speeds, phases, and weights, held
14
+ * to the center line at both ends. `level` sets how far they swing, and a
15
+ * quiet level rests as a flat line.
16
+ *
17
+ * Set `level` and `bands` from your audio as often as you have readings, about
18
+ * 30 times a second. Each bar and wave eases toward its reading: up fast and
19
+ * down slower. A spectrum given no `bands` draws every bar from `level`.
20
+ *
21
+ * The drawing stops when it is off screen, in a background tab, or after the
22
+ * level has been zero for under a second, and starts again with the next
23
+ * reading. With `prefers-reduced-motion`, it draws one bar from the middle
24
+ * over a faint track, whose length is the level, and redraws it on each
25
+ * reading. With `still`, it draws either drawing as a still picture of its
26
+ * readings, whatever the motion preference.
27
+ *
28
+ * The level is an image named by `label`. A meter's value would be announced
29
+ * on every change, and a level changes many times a second.
30
+ *
31
+ * Voice Level is styled only and has behavior, which the tier split does not
32
+ * otherwise allow. Its role and name are set on the host, and the rest is
33
+ * drawing to a canvas.
34
+ *
35
+ * @since 0.215.0
36
+ * @status experimental
37
+ * @category feedback
38
+ * @aliases Audio Level, Waveform, Microphone Level, Equalizer, VU Meter
39
+ * @documentation https://fusion.dev/helix/components/voice-level
40
+ *
41
+ * @csspart canvas - The canvas the level is drawn on.
42
+ *
43
+ * @cssproperty --voice-level-accent-color - The color of the second wave. Defaults to the accent color.
44
+ * @cssproperty --voice-level-bars - How many bars the spectrum draws, from 3 to 96. Defaults to `24`.
45
+ * @cssproperty --voice-level-block-size - The height. Defaults to the icon size for `spectrum` and the control size for `waves`.
46
+ * @cssproperty --voice-level-color - The color of the center bars and the main wave. Defaults to the primary color.
47
+ * @cssproperty --voice-level-edge-color - The color of the bars at either end. Defaults to `--voice-level-color` at 55 percent.
48
+ * @cssproperty --voice-level-inline-size - The width. Defaults to four times the height for `spectrum` and three times for `waves`.
49
+ */
50
+ export declare class HelixVoiceLevel extends HelixElement {
51
+ /**
52
+ * The key this family's words are registered under, its tag:
53
+ * `registerWords('de', { 'helix-voice-level': { fallbackLabel: 'Sprachpegel' } })`.
54
+ */
55
+ static words: string;
56
+ static styles: CSSResultGroup;
57
+ /**
58
+ * How loud each frequency band is, from `0` to `1`, lowest band first.
59
+ *
60
+ * Each bar of the spectrum follows one band. Where there are more or fewer
61
+ * bands than bars, each bar reads between the two nearest bands. Empty draws
62
+ * every bar from `level`. Written as an attribute, the numbers are separated
63
+ * by spaces or commas: `bands="0.1 0.4 0.6 0.3"`. Assign an array to the
64
+ * property for readings that change many times a second.
65
+ */
66
+ bands: string | readonly number[];
67
+ /**
68
+ * The name where `label` is empty. Translate it with the rest of your
69
+ * interface.
70
+ */
71
+ fallbackLabel: string;
72
+ /**
73
+ * The name read by screen readers, such as "Your voice". Defaults to
74
+ * `fallbackLabel`.
75
+ */
76
+ label: string;
77
+ /**
78
+ * How loud the voice is, from `0` for silence to `1`.
79
+ *
80
+ * It sets how far the waves swing, the length of the still bar under
81
+ * `prefers-reduced-motion`, and every bar of a spectrum that has no
82
+ * `bands`. A value outside the range is drawn at the nearer end.
83
+ */
84
+ level: number;
85
+ /**
86
+ * Draws the readings as a still picture: each bar at its band and the waves
87
+ * at one fixed moment, swung as far as `level`.
88
+ *
89
+ * For a level that holds, such as the loudness of a recorded clip or a
90
+ * picture of the drawing. No frame loop runs, and each new reading is drawn
91
+ * at once with no easing. Because nothing moves, the drawing is the same
92
+ * under `prefers-reduced-motion`. Clearing it starts the motion from the
93
+ * readings as they are.
94
+ */
95
+ still: boolean;
96
+ /**
97
+ * The drawing: a row of bars, one for each band, or three moving waves.
98
+ *
99
+ * Reflected because the stylesheet sizes each drawing differently. Absent
100
+ * is `spectrum`, the resting value, which selects nothing.
101
+ */
102
+ variant?: VoiceLevelVariant;
103
+ private canvas;
104
+ private context;
105
+ /** Whether the browser has taken the canvas, as its own two events say. */
106
+ private lost;
107
+ /** The box the canvas was last measured at, in CSS pixels. */
108
+ private width;
109
+ private height;
110
+ /** Each bar as it is drawn now, eased toward its band. */
111
+ private shown;
112
+ /** The waves' amplitude as it is drawn now, eased toward the level. */
113
+ private swing;
114
+ /** The waves' clock, in seconds. */
115
+ private time;
116
+ /** How long everything drawn has been at rest, in seconds. */
117
+ private quiet;
118
+ /**
119
+ * Whether the loop has stopped for a level at rest. A reading above zero
120
+ * clears it. Held here so the gate's own paths to a frame, a tab coming back
121
+ * or the element scrolling into view, do not start a loop with nothing to
122
+ * draw.
123
+ */
124
+ private resting;
125
+ private readonly gate;
126
+ protected willUpdate(changed: PropertyValues): void;
127
+ protected render(): TemplateResult;
128
+ protected firstUpdated(changed: PropertyValues): void;
129
+ connectedCallback(): void;
130
+ disconnectedCallback(): void;
131
+ /** Readings as numbers, each brought into `0` to `1`. */
132
+ private get readings();
133
+ /** Whether anything is to be drawn above rest: a level or a band above silence. */
134
+ private get sounding();
135
+ /**
136
+ * A new reading. Drawn at once under reduced motion, and otherwise handed to
137
+ * the loop, which is started where it had stopped for a level at rest.
138
+ */
139
+ private wake;
140
+ /**
141
+ * One frame for a still level, once a color the canvas carries has moved,
142
+ * so a theme changed while nothing moves is drawn in its own colors.
143
+ */
144
+ private readonly handleRestyle;
145
+ /**
146
+ * Everything the canvas needs, in one place both lifecycles reach, so an
147
+ * element moved by a framework keeps its observers.
148
+ */
149
+ private start;
150
+ /**
151
+ * Matches the drawing buffer to the box, at the device's pixel ratio capped
152
+ * at 2, and reports whether it changed.
153
+ */
154
+ private measure;
155
+ /** A 2D context comes back on its own, so a loss only stops the loop and hides the canvas. */
156
+ private readonly handleLost;
157
+ /** Sized again, because a restored canvas comes back in its default state. */
158
+ private readonly handleRestored;
159
+ /** Moves every bar and the waves one frame toward their readings. */
160
+ private step;
161
+ /**
162
+ * Every bar and the waves set to their readings with no easing, and the
163
+ * clock to the still moment, which is what `still` draws. The moving drawing
164
+ * starts from here when it is cleared.
165
+ */
166
+ private settle;
167
+ /** The bar count the hook asks for. */
168
+ private get bars();
169
+ /** One frame of whichever drawing `variant` names, or the still one. */
170
+ private paint;
171
+ /** The bars, the center ones in `main` and the rest in `edge`. */
172
+ private paintSpectrum;
173
+ /** The three waves at `time` seconds, the main one on top. */
174
+ private paintWaves;
175
+ /**
176
+ * Under reduced motion: one bar from the middle, as long as the level, in
177
+ * the main color, over a faint track the width of the box. As thick as a
178
+ * spectrum's bar or the main wave.
179
+ */
180
+ private paintStill;
181
+ }
182
+ declare global {
183
+ interface HTMLElementTagNameMap {
184
+ 'helix-voice-level': HelixVoiceLevel;
185
+ }
186
+ }