@fusion.dev/helix 0.216.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.
@@ -0,0 +1,324 @@
1
+ import { __decorate } from "tslib";
2
+ import { customElement, HelixElement, hostAria, numberConverter, PaintGateController, readColorAlpha, words, writtenNumbers, } from '@fusion.dev/helix-core';
3
+ import { html } from 'lit';
4
+ import { property } from 'lit/decorators.js';
5
+ import { bandFor, barCount, centered, ease, envelope, QUIET_STOP, SILENT, STILL_TIME, stillLength, unit, WAVE_LINES, waveAt, } from '../draw.js';
6
+ import { styles } from './voice-level-root.css.js';
7
+ const GRAY = 'rgba(128,128,128,1)';
8
+ const FAINT = 0.45;
9
+ const TRACK = 0.18;
10
+ const WAVE_STEP = 2;
11
+ const ink = (value, alpha = 1) => {
12
+ const read = readColorAlpha(value);
13
+ return read ? `rgba(${read[0]},${read[1]},${read[2]},${read[3] * alpha})` : GRAY;
14
+ };
15
+ let HelixVoiceLevel = class HelixVoiceLevel extends HelixElement {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.bands = '';
19
+ this.fallbackLabel = 'Voice level';
20
+ this.label = '';
21
+ this.level = 0;
22
+ this.still = false;
23
+ this.context = null;
24
+ this.lost = false;
25
+ this.width = 0;
26
+ this.height = 0;
27
+ this.shown = [];
28
+ this.swing = 0;
29
+ this.time = 0;
30
+ this.quiet = 0;
31
+ this.resting = false;
32
+ this.gate = new PaintGateController(this, {
33
+ canRun: () => !!this.context && !this.lost && !this.resting && !this.still,
34
+ onFrame: (delta) => {
35
+ this.step(delta);
36
+ this.paint();
37
+ if (this.quiet >= QUIET_STOP) {
38
+ this.resting = true;
39
+ this.gate.stop();
40
+ }
41
+ },
42
+ onMotion: () => {
43
+ this.paint();
44
+ },
45
+ onResize: () => {
46
+ this.measure();
47
+ this.paint();
48
+ },
49
+ });
50
+ this.handleRestyle = () => {
51
+ if (!this.gate.running) {
52
+ this.paint();
53
+ }
54
+ };
55
+ this.handleLost = () => {
56
+ this.lost = true;
57
+ this.gate.stop();
58
+ this.canvas?.toggleAttribute('hidden', true);
59
+ };
60
+ this.handleRestored = () => {
61
+ this.lost = false;
62
+ this.canvas?.toggleAttribute('hidden', false);
63
+ this.width = 0;
64
+ this.height = 0;
65
+ this.measure();
66
+ this.paint();
67
+ this.gate.play();
68
+ };
69
+ }
70
+ static { this.words = 'helix-voice-level'; }
71
+ static { this.styles = styles; }
72
+ willUpdate(changed) {
73
+ super.willUpdate(changed);
74
+ hostAria(this.internals, { label: this.label || this.fallbackLabel, role: 'img' });
75
+ if (changed.has('level') ||
76
+ changed.has('bands') ||
77
+ changed.has('still') ||
78
+ changed.has('variant')) {
79
+ this.wake();
80
+ }
81
+ }
82
+ render() {
83
+ return html `<canvas part="canvas" aria-hidden="true"></canvas>`;
84
+ }
85
+ firstUpdated(changed) {
86
+ super.firstUpdated(changed);
87
+ this.start();
88
+ }
89
+ connectedCallback() {
90
+ super.connectedCallback();
91
+ if (this.hasUpdated) {
92
+ this.start();
93
+ }
94
+ }
95
+ disconnectedCallback() {
96
+ super.disconnectedCallback();
97
+ this.width = 0;
98
+ this.height = 0;
99
+ this.canvas?.removeEventListener('contextlost', this.handleLost);
100
+ this.canvas?.removeEventListener('contextrestored', this.handleRestored);
101
+ this.canvas?.removeEventListener('transitionend', this.handleRestyle);
102
+ }
103
+ get readings() {
104
+ return writtenNumbers(this.bands).map(unit);
105
+ }
106
+ get sounding() {
107
+ return unit(this.level) > SILENT || this.readings.some((band) => band > SILENT);
108
+ }
109
+ wake() {
110
+ if (!this.context) {
111
+ return;
112
+ }
113
+ if (this.still) {
114
+ this.gate.stop();
115
+ this.paint();
116
+ return;
117
+ }
118
+ if (this.gate.reduced) {
119
+ this.paint();
120
+ return;
121
+ }
122
+ if (this.sounding) {
123
+ this.resting = false;
124
+ this.quiet = 0;
125
+ this.gate.play();
126
+ }
127
+ else if (!this.gate.running) {
128
+ this.paint();
129
+ }
130
+ }
131
+ start() {
132
+ if (!this.isConnected) {
133
+ return;
134
+ }
135
+ const canvas = this.renderRoot.querySelector('canvas');
136
+ if (!canvas) {
137
+ return;
138
+ }
139
+ this.canvas = canvas;
140
+ this.context ??= canvas.getContext('2d');
141
+ if (!this.context) {
142
+ return;
143
+ }
144
+ canvas.addEventListener('contextlost', this.handleLost);
145
+ canvas.addEventListener('contextrestored', this.handleRestored);
146
+ canvas.addEventListener('transitionend', this.handleRestyle);
147
+ if (this.lost && !this.context.isContextLost()) {
148
+ this.lost = false;
149
+ canvas.toggleAttribute('hidden', false);
150
+ }
151
+ this.gate.watch();
152
+ this.measure();
153
+ this.paint();
154
+ this.resting = !this.sounding;
155
+ this.gate.play();
156
+ }
157
+ measure() {
158
+ const canvas = this.canvas;
159
+ if (!canvas) {
160
+ return false;
161
+ }
162
+ const box = this.getBoundingClientRect();
163
+ const width = Math.round(box.width);
164
+ const height = Math.round(box.height);
165
+ if (width <= 0 || height <= 0 || (width === this.width && height === this.height)) {
166
+ return false;
167
+ }
168
+ this.width = width;
169
+ this.height = height;
170
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
171
+ canvas.width = Math.round(width * dpr);
172
+ canvas.height = Math.round(height * dpr);
173
+ this.context?.setTransform(dpr, 0, 0, dpr, 0, 0);
174
+ return true;
175
+ }
176
+ step(delta) {
177
+ const count = this.bars;
178
+ const readings = this.readings;
179
+ if (this.shown.length !== count) {
180
+ this.shown = Array.from({ length: count }, (_, at) => this.shown[at] ?? 0);
181
+ }
182
+ let top = 0;
183
+ for (let at = 0; at < count; at++) {
184
+ const target = bandFor(readings, this.level, at, count) * envelope(at, count);
185
+ const next = ease(this.shown[at] ?? 0, target, delta);
186
+ this.shown[at] = next;
187
+ top = Math.max(top, next, target);
188
+ }
189
+ const level = unit(this.level);
190
+ this.swing = ease(this.swing, level, delta);
191
+ this.time += delta;
192
+ top = Math.max(top, this.swing, level);
193
+ this.quiet = top > SILENT ? 0 : this.quiet + delta;
194
+ }
195
+ settle() {
196
+ const count = this.bars;
197
+ const readings = this.readings;
198
+ this.shown = Array.from({ length: count }, (_, at) => bandFor(readings, this.level, at, count) * envelope(at, count));
199
+ this.swing = unit(this.level);
200
+ this.time = STILL_TIME;
201
+ }
202
+ get bars() {
203
+ return this.canvas ? barCount(getComputedStyle(this.canvas).animationIterationCount) : 0;
204
+ }
205
+ paint() {
206
+ const ctx = this.context;
207
+ const canvas = this.canvas;
208
+ if (!ctx || !canvas || this.width <= 0 || this.height <= 0) {
209
+ return;
210
+ }
211
+ const painted = getComputedStyle(canvas);
212
+ const main = ink(painted.color);
213
+ ctx.clearRect(0, 0, this.width, this.height);
214
+ if (this.still) {
215
+ this.settle();
216
+ }
217
+ if (this.gate.reduced && !this.still) {
218
+ this.paintStill(ctx, main, ink(painted.color, TRACK));
219
+ }
220
+ else if (this.variant === 'waves') {
221
+ this.paintWaves(ctx, main, painted, this.still ? STILL_TIME : this.time);
222
+ }
223
+ else {
224
+ this.paintSpectrum(ctx, main, ink(painted.textDecorationColor));
225
+ }
226
+ }
227
+ paintSpectrum(ctx, main, edge) {
228
+ const count = this.bars;
229
+ if (this.shown.length !== count) {
230
+ this.step(0);
231
+ }
232
+ const slot = this.width / count;
233
+ const thickness = Math.min(4, Math.max(1.5, slot * 0.5));
234
+ const middle = this.height / 2;
235
+ const center = new Path2D();
236
+ const ends = new Path2D();
237
+ for (let at = 0; at < count; at++) {
238
+ const tall = thickness + (this.shown[at] ?? 0) * (this.height - thickness);
239
+ const path = centered(at, count) ? center : ends;
240
+ rounded(path, slot * (at + 0.5) - thickness / 2, middle - tall / 2, thickness, tall);
241
+ }
242
+ ctx.fillStyle = edge;
243
+ ctx.fill(ends);
244
+ ctx.fillStyle = main;
245
+ ctx.fill(center);
246
+ }
247
+ paintWaves(ctx, main, painted, time) {
248
+ const middle = this.height / 2;
249
+ const colors = {
250
+ accent: ink(painted.columnRuleColor),
251
+ faint: ink(painted.color, FAINT),
252
+ main,
253
+ };
254
+ ctx.lineCap = 'round';
255
+ ctx.lineJoin = 'round';
256
+ for (const line of WAVE_LINES) {
257
+ const reach = Math.max(0, middle - line.width) * this.swing;
258
+ ctx.beginPath();
259
+ for (let x = 0; x <= this.width; x += WAVE_STEP) {
260
+ const y = middle - reach * waveAt(line, x / this.width, time);
261
+ if (x === 0) {
262
+ ctx.moveTo(x, y);
263
+ }
264
+ else {
265
+ ctx.lineTo(x, y);
266
+ }
267
+ }
268
+ ctx.lineTo(this.width, middle);
269
+ ctx.lineWidth = line.width;
270
+ ctx.strokeStyle = colors[line.ink];
271
+ ctx.stroke();
272
+ }
273
+ }
274
+ paintStill(ctx, main, faint) {
275
+ const thickness = this.variant === 'waves'
276
+ ? 2
277
+ : Math.min(4, Math.max(1.5, (this.width / (this.bars || 1)) * 0.5));
278
+ const readings = this.readings;
279
+ const level = this.level > 0 || readings.length === 0
280
+ ? this.level
281
+ : Math.max(...readings.map((band, at) => band * envelope(at, readings.length)));
282
+ const length = stillLength(level, this.width, thickness);
283
+ const top = (this.height - thickness) / 2;
284
+ const track = new Path2D();
285
+ rounded(track, 0, top, this.width, thickness);
286
+ ctx.fillStyle = faint;
287
+ ctx.fill(track);
288
+ const path = new Path2D();
289
+ rounded(path, (this.width - length) / 2, top, length, thickness);
290
+ ctx.fillStyle = main;
291
+ ctx.fill(path);
292
+ }
293
+ };
294
+ __decorate([
295
+ property()
296
+ ], HelixVoiceLevel.prototype, "bands", void 0);
297
+ __decorate([
298
+ words,
299
+ property({ attribute: 'fallback-label' })
300
+ ], HelixVoiceLevel.prototype, "fallbackLabel", void 0);
301
+ __decorate([
302
+ property()
303
+ ], HelixVoiceLevel.prototype, "label", void 0);
304
+ __decorate([
305
+ property({ converter: numberConverter('level', 0) })
306
+ ], HelixVoiceLevel.prototype, "level", void 0);
307
+ __decorate([
308
+ property({ type: Boolean })
309
+ ], HelixVoiceLevel.prototype, "still", void 0);
310
+ __decorate([
311
+ property({ reflect: true })
312
+ ], HelixVoiceLevel.prototype, "variant", void 0);
313
+ HelixVoiceLevel = __decorate([
314
+ customElement('helix-voice-level')
315
+ ], HelixVoiceLevel);
316
+ export { HelixVoiceLevel };
317
+ const rounded = (path, x, y, width, height) => {
318
+ if (typeof path.roundRect === 'function') {
319
+ path.roundRect(x, y, width, height, Math.min(width, height) / 2);
320
+ }
321
+ else {
322
+ path.rect(x, y, width, height);
323
+ }
324
+ };