@editframe/elements 0.14.0-beta.3 → 0.15.0-beta.3

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.
@@ -1,14 +1,16 @@
1
- import { EFAudio } from "./EFAudio.js";
2
-
1
+ import { CSSStyleObserver } from "@bramus/style-observer";
3
2
  import { Task } from "@lit/task";
4
3
  import { LitElement, type PropertyValueMap, css, html } from "lit";
5
- import { customElement, property } from "lit/decorators.js";
4
+ import { customElement, property, state } from "lit/decorators.js";
6
5
  import { type Ref, createRef, ref } from "lit/directives/ref.js";
7
6
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
7
+ import { EF_RENDERING } from "../EF_RENDERING.js";
8
8
  import { TWMixin } from "../gui/TWMixin.js";
9
9
  import { CrossUpdateController } from "./CrossUpdateController.js";
10
+ import type { EFAudio } from "./EFAudio.js";
10
11
  import { EFTemporal } from "./EFTemporal.js";
11
- import { EFVideo } from "./EFVideo.js";
12
+ import type { EFVideo } from "./EFVideo.js";
13
+ import { TargetController } from "./TargetController.ts";
12
14
 
13
15
  @customElement("ef-waveform")
14
16
  export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
@@ -30,6 +32,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
30
32
 
31
33
  canvasRef: Ref<HTMLCanvasElement> = createRef();
32
34
  private ctx: CanvasRenderingContext2D | null = null;
35
+ private styleObserver: CSSStyleObserver | null = null;
33
36
 
34
37
  private resizeObserver?: ResizeObserver;
35
38
  private mutationObserver?: MutationObserver;
@@ -42,30 +45,33 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
42
45
  type: String,
43
46
  attribute: "mode",
44
47
  })
45
- mode:
46
- | "roundBars"
47
- | "bars"
48
- | "bricks"
49
- | "equalizer"
50
- | "curve"
51
- | "line"
52
- | "pixel"
53
- | "wave" = "bars";
48
+ mode: "roundBars" | "bars" | "bricks" | "line" | "pixel" | "wave" | "spikes" =
49
+ "bars";
54
50
 
55
51
  @property({ type: String })
56
52
  color = "currentColor";
57
53
 
58
- @property({ type: String, attribute: "target", reflect: true })
59
- targetSelector = "";
54
+ @property({ type: String, reflect: true })
55
+ target = "";
60
56
 
61
- set target(value: string) {
62
- this.targetSelector = value;
63
- }
57
+ @state()
58
+ targetElement: EFAudio | EFVideo | null = null;
59
+
60
+ @property({ type: Number, attribute: "line-width" })
61
+ lineWidth = 4;
62
+
63
+ targetController: TargetController = new TargetController(this);
64
64
 
65
65
  connectedCallback() {
66
66
  super.connectedCallback();
67
- if (this.targetElement) {
68
- new CrossUpdateController(this.targetElement, this);
67
+ try {
68
+ if (this.targetElement) {
69
+ new CrossUpdateController(this.targetElement, this);
70
+ }
71
+ } catch (e) {
72
+ // TODO: determine if this is a critical error, or if we should just ignore it
73
+ // currenty evidence suggests everything still works
74
+ // no target element, no cross update controller
69
75
  }
70
76
 
71
77
  // Initialize ResizeObserver
@@ -87,6 +93,13 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
87
93
 
88
94
  // Observe attribute changes on the element
89
95
  this.mutationObserver.observe(this, { attributes: true });
96
+
97
+ if (!EF_RENDERING()) {
98
+ this.styleObserver = new CSSStyleObserver(["color"], () => {
99
+ this.frameTask.run();
100
+ });
101
+ this.styleObserver.attach(this);
102
+ }
90
103
  }
91
104
 
92
105
  disconnectedCallback() {
@@ -94,6 +107,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
94
107
  // Disconnect the observers when the element is removed from the DOM
95
108
  this.resizeObserver?.disconnect();
96
109
  this.mutationObserver?.disconnect();
110
+ this.styleObserver?.detach();
97
111
  }
98
112
 
99
113
  private resizeCanvas() {
@@ -107,7 +121,10 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
107
121
  const canvas = this.canvasRef.value;
108
122
  if (!canvas) return null;
109
123
 
110
- const rect = this.getBoundingClientRect();
124
+ const rect = {
125
+ width: this.offsetWidth,
126
+ height: this.offsetHeight,
127
+ };
111
128
  const dpr = window.devicePixelRatio;
112
129
 
113
130
  canvas.style.width = `${rect.width}px`;
@@ -121,27 +138,33 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
121
138
  ctx.reset();
122
139
 
123
140
  // Scale all drawing operations by dpr
124
- ctx.scale(dpr, dpr);
125
-
126
141
  return ctx;
127
142
  }
128
143
 
129
144
  protected drawBars(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array) {
130
145
  const canvas = ctx.canvas;
131
- const waveWidth = canvas.width / devicePixelRatio;
132
- const waveHeight = canvas.height / devicePixelRatio;
133
- const barWidth = (waveWidth / frequencyData.length) * 0.8; // 0.8 for padding
134
- const baseline = waveHeight / 2;
146
+ const waveWidth = canvas.width;
147
+ const waveHeight = canvas.height;
148
+
149
+ const totalBars = frequencyData.length;
150
+ const paddingInner = 0.5;
151
+ const paddingOuter = 0.01;
152
+ const availableWidth = waveWidth;
153
+ const barWidth =
154
+ availableWidth / (totalBars + (totalBars - 1) * paddingInner);
135
155
 
136
156
  ctx.clearRect(0, 0, waveWidth, waveHeight);
157
+ const path = new Path2D();
137
158
 
138
159
  frequencyData.forEach((value, i) => {
139
- const height = (value / 255) * (waveHeight / 2);
140
- const x = i * (waveWidth / frequencyData.length);
141
- const y = baseline - height;
142
-
143
- ctx.fillRect(x, y, barWidth, Math.max(height * 2, 2));
160
+ const normalizedValue = Math.min((value / 255) * 2, 1);
161
+ const barHeight = normalizedValue * waveHeight;
162
+ const y = (waveHeight - barHeight) / 2;
163
+ const x = waveWidth * paddingOuter + i * (barWidth * (1 + paddingInner));
164
+ path.rect(x, y, barWidth, barHeight);
144
165
  });
166
+
167
+ ctx.fill(path);
145
168
  }
146
169
 
147
170
  protected drawBricks(
@@ -149,55 +172,28 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
149
172
  frequencyData: Uint8Array,
150
173
  ) {
151
174
  const canvas = ctx.canvas;
152
- const waveWidth = canvas.width / devicePixelRatio;
153
- const waveHeight = canvas.height / devicePixelRatio;
154
- const brickWidth = waveWidth / frequencyData.length;
155
- const brickHeightFactor = waveHeight / 255 / 2;
156
- const brickPadding = 2;
157
- const midHeight = waveHeight / 2;
158
-
175
+ const waveWidth = canvas.width;
176
+ const waveHeight = canvas.height;
159
177
  ctx.clearRect(0, 0, waveWidth, waveHeight);
178
+ const path = new Path2D();
160
179
 
161
- // Draw baseline
162
- ctx.beginPath();
163
- ctx.setLineDash([2]);
164
- ctx.lineWidth = 4;
165
- ctx.moveTo(0, midHeight);
166
- ctx.lineTo(waveWidth, midHeight);
167
- ctx.stroke();
168
- ctx.setLineDash([]); // Reset dash
169
-
170
- // Draw bricks
171
- frequencyData.forEach((value, i) => {
172
- const x = i * brickWidth;
173
- const height = value * brickHeightFactor * 2;
174
- const y = midHeight - height / 2;
175
-
176
- ctx.fillRect(x, y, brickWidth - brickPadding, height);
177
- });
178
- }
179
-
180
- protected drawLine(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array) {
181
- const canvas = ctx.canvas;
182
- const waveWidth = canvas.width / devicePixelRatio;
183
- const waveHeight = canvas.height / devicePixelRatio;
184
-
185
- ctx.clearRect(0, 0, waveWidth, waveHeight);
186
- ctx.beginPath();
180
+ const columnWidth = waveWidth / frequencyData.length;
181
+ const boxSize = columnWidth * 0.9;
182
+ const verticalGap = boxSize * 0.2; // Add spacing between bricks
183
+ const maxBricks = Math.floor(waveHeight / (boxSize + verticalGap)); // Account for gaps in height calculation
187
184
 
188
185
  frequencyData.forEach((value, i) => {
189
- const x = (i / frequencyData.length) * waveWidth;
190
- const y = (1 - value / 255) * waveHeight;
186
+ const normalizedValue = Math.min((value / 255) * 2, 1);
187
+ const brickCount = Math.floor(normalizedValue * maxBricks);
191
188
 
192
- if (i === 0) {
193
- ctx.moveTo(x, y);
194
- } else {
195
- ctx.lineTo(x, y);
189
+ for (let j = 0; j < brickCount; j++) {
190
+ const x = columnWidth * i;
191
+ const y = waveHeight - (j + 1) * (boxSize + verticalGap); // Include gap in position calculation
192
+ path.rect(x, y, boxSize, boxSize);
196
193
  }
197
194
  });
198
195
 
199
- ctx.lineWidth = 4;
200
- ctx.stroke();
196
+ ctx.fill(path);
201
197
  }
202
198
 
203
199
  protected drawRoundBars(
@@ -205,25 +201,34 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
205
201
  frequencyData: Uint8Array,
206
202
  ) {
207
203
  const canvas = ctx.canvas;
208
- const waveWidth = canvas.width / devicePixelRatio;
209
- const waveHeight = canvas.height / devicePixelRatio;
210
- const barWidth = (waveWidth / frequencyData.length) * 0.5; // Add padding
211
- const baseline = waveHeight / 2;
212
- const maxHeight = waveHeight / 2;
204
+ const waveWidth = canvas.width;
205
+ const waveHeight = canvas.height;
206
+
207
+ // Similar padding calculation as drawBars
208
+ const totalBars = frequencyData.length;
209
+ const paddingInner = 0.5;
210
+ const paddingOuter = 0.01;
211
+ const availableWidth = waveWidth;
212
+ const barWidth =
213
+ availableWidth / (totalBars + (totalBars - 1) * paddingInner);
213
214
 
214
215
  ctx.clearRect(0, 0, waveWidth, waveHeight);
215
216
 
217
+ // Create a single Path2D object for all rounded bars
218
+ const path = new Path2D();
219
+
216
220
  frequencyData.forEach((value, i) => {
217
- const height = (value / 255) * maxHeight;
218
- const x = i * (waveWidth / frequencyData.length);
219
- const y = baseline - height;
221
+ const normalizedValue = Math.min((value / 255) * 2, 1);
222
+ const height = normalizedValue * waveHeight; // Use full wave height like in drawBars
223
+ const x = waveWidth * paddingOuter + i * (barWidth * (1 + paddingInner));
224
+ const y = (waveHeight - height) / 2; // Center vertically
220
225
 
221
- // Draw rounded rectangle
222
- const radius = barWidth / 2;
223
- ctx.beginPath();
224
- ctx.roundRect(x, y, barWidth, Math.max(height * 2, 2), radius);
225
- ctx.fill();
226
+ // Add rounded rectangle to path
227
+ path.roundRect(x, y, barWidth, height, barWidth / 2);
226
228
  });
229
+
230
+ // Single fill operation for all bars
231
+ ctx.fill(path);
227
232
  }
228
233
 
229
234
  protected drawEqualizer(
@@ -231,54 +236,60 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
231
236
  frequencyData: Uint8Array,
232
237
  ) {
233
238
  const canvas = ctx.canvas;
234
- const waveWidth = canvas.width / devicePixelRatio;
235
- const waveHeight = canvas.height / devicePixelRatio;
236
- const barWidth = (waveWidth / frequencyData.length) * 0.8;
239
+ const waveWidth = canvas.width;
240
+ const waveHeight = canvas.height;
237
241
  const baseline = waveHeight / 2;
242
+ const barWidth = (waveWidth / frequencyData.length) * 0.8;
238
243
 
239
244
  ctx.clearRect(0, 0, waveWidth, waveHeight);
240
245
 
246
+ // Create paths for baseline and bars
247
+ const baselinePath = new Path2D();
248
+ const barsPath = new Path2D();
249
+
241
250
  // Draw baseline
242
- ctx.beginPath();
243
- ctx.lineWidth = 2;
244
- ctx.moveTo(0, baseline);
245
- ctx.lineTo(waveWidth, baseline);
246
- ctx.stroke();
251
+ baselinePath.moveTo(0, baseline);
252
+ baselinePath.lineTo(waveWidth, baseline);
247
253
 
248
254
  // Draw bars
249
255
  frequencyData.forEach((value, i) => {
250
256
  const height = (value / 255) * (waveHeight / 2);
251
257
  const x = i * (waveWidth / frequencyData.length);
252
258
  const y = baseline - height;
253
-
254
- ctx.fillRect(x, y, barWidth, Math.max(height * 2, 1));
259
+ barsPath.rect(x, y, barWidth, Math.max(height * 2, 1));
255
260
  });
261
+
262
+ // Render baseline
263
+ ctx.lineWidth = 2;
264
+ ctx.stroke(baselinePath);
265
+
266
+ // Render bars
267
+ ctx.fill(barsPath);
256
268
  }
257
269
 
258
- protected drawCurve(
259
- ctx: CanvasRenderingContext2D,
260
- frequencyData: Uint8Array,
261
- ) {
270
+ protected drawLine(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array) {
262
271
  const canvas = ctx.canvas;
263
- const waveWidth = canvas.width / devicePixelRatio;
264
- const waveHeight = canvas.height / devicePixelRatio;
272
+ const waveWidth = canvas.width;
273
+ const waveHeight = canvas.height;
265
274
 
266
275
  ctx.clearRect(0, 0, waveWidth, waveHeight);
267
- ctx.beginPath();
276
+
277
+ // Create a single Path2D object for the curve
278
+ const path = new Path2D();
268
279
 
269
280
  frequencyData.forEach((value, i) => {
270
281
  const x = (i / frequencyData.length) * waveWidth;
271
282
  const y = (1 - value / 255) * waveHeight;
272
283
 
273
284
  if (i === 0) {
274
- ctx.moveTo(x, y);
285
+ path.moveTo(x, y);
275
286
  } else {
276
- ctx.lineTo(x, y);
287
+ path.lineTo(x, y);
277
288
  }
278
289
  });
279
290
 
280
- ctx.lineWidth = 4;
281
- ctx.stroke();
291
+ ctx.lineWidth = this.lineWidth;
292
+ ctx.stroke(path);
282
293
  }
283
294
 
284
295
  protected drawPixel(
@@ -286,143 +297,224 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
286
297
  frequencyData: Uint8Array,
287
298
  ) {
288
299
  const canvas = ctx.canvas;
289
- const waveWidth = canvas.width / devicePixelRatio;
290
- const waveHeight = canvas.height / devicePixelRatio;
300
+ const waveWidth = canvas.width;
301
+ const waveHeight = canvas.height;
291
302
  const baseline = waveHeight / 2;
292
- const barWidth = (waveWidth / frequencyData.length) * 0.97; // Account for padding
303
+ const barWidth = waveWidth / frequencyData.length;
293
304
 
294
305
  ctx.clearRect(0, 0, waveWidth, waveHeight);
306
+ const path = new Path2D();
295
307
 
296
308
  frequencyData.forEach((value, i) => {
309
+ const normalizedValue = Math.min((value / 255) * 2, 1); // Updated normalization
297
310
  const x = i * (waveWidth / frequencyData.length);
298
- const barHeight = (value / 255) * baseline;
311
+ const barHeight = normalizedValue * (waveHeight / 2); // Half height since we extend both ways
299
312
  const y = baseline - barHeight;
300
-
301
- ctx.fillRect(x, y, barWidth, barHeight * 2);
313
+ path.rect(x, y, barWidth, barHeight * 2); // Double height to extend both ways
302
314
  });
315
+
316
+ ctx.fill(path);
303
317
  }
304
318
 
305
319
  protected drawWave(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array) {
306
320
  const canvas = ctx.canvas;
307
- const waveWidth = canvas.width / devicePixelRatio;
308
- const waveHeight = canvas.height / devicePixelRatio;
309
- const baseline = waveHeight / 2;
310
- const barWidth = (waveWidth / frequencyData.length) * 0.97; // Account for padding
321
+ const waveWidth = canvas.width;
322
+ const waveHeight = canvas.height;
323
+ const paddingOuter = 0.01;
324
+ const availableWidth = waveWidth * (1 - 2 * paddingOuter);
325
+ const startX = waveWidth * paddingOuter;
311
326
 
312
327
  ctx.clearRect(0, 0, waveWidth, waveHeight);
328
+ const path = new Path2D();
313
329
 
314
- // Draw baseline
315
- ctx.beginPath();
316
- ctx.moveTo(0, baseline);
317
- ctx.lineTo(waveWidth, baseline);
318
- ctx.strokeStyle = this.color;
319
- ctx.lineWidth = 1;
320
- ctx.stroke();
330
+ // Draw top curve
331
+ const firstValue = Math.min(((frequencyData[0] ?? 0) / 255) * 2, 1);
332
+ const firstY = (waveHeight - firstValue * waveHeight) / 2;
333
+ path.moveTo(startX, firstY);
321
334
 
322
- // Draw bars
335
+ // Draw top half
323
336
  frequencyData.forEach((value, i) => {
324
- const x = i * (waveWidth / frequencyData.length);
325
- const barHeight = (value / 255) * (waveHeight / 2);
326
- const y = baseline - barHeight;
337
+ const normalizedValue = Math.min((value / 255) * 2, 1);
338
+ const x = startX + (i / (frequencyData.length - 1)) * availableWidth;
339
+ const barHeight = normalizedValue * waveHeight;
340
+ const y = (waveHeight - barHeight) / 2;
327
341
 
328
- ctx.fillRect(x, y, barWidth, barHeight * 2);
342
+ if (i === 0) {
343
+ path.moveTo(x, y);
344
+ } else {
345
+ const prevX =
346
+ startX + ((i - 1) / (frequencyData.length - 1)) * availableWidth;
347
+ const prevValue = Math.min(((frequencyData[i - 1] ?? 0) / 255) * 2, 1);
348
+ const prevBarHeight = prevValue * waveHeight;
349
+ const prevY = (waveHeight - prevBarHeight) / 2;
350
+ const xc = (prevX + x) / 2;
351
+ const yc = (prevY + y) / 2;
352
+ path.quadraticCurveTo(prevX, prevY, xc, yc);
353
+ }
329
354
  });
355
+
356
+ // Draw bottom half
357
+ for (let i = frequencyData.length - 1; i >= 0; i--) {
358
+ const normalizedValue = Math.min(((frequencyData[i] ?? 0) / 255) * 2, 1);
359
+ const x = startX + (i / (frequencyData.length - 1)) * availableWidth;
360
+ const barHeight = normalizedValue * waveHeight;
361
+ const y = (waveHeight + barHeight) / 2;
362
+
363
+ if (i === frequencyData.length - 1) {
364
+ path.lineTo(x, y);
365
+ } else {
366
+ const nextX =
367
+ startX + ((i + 1) / (frequencyData.length - 1)) * availableWidth;
368
+ const nextValue = Math.min(((frequencyData[i + 1] ?? 0) / 255) * 2, 1);
369
+ const nextBarHeight = nextValue * waveHeight;
370
+ const nextY = (waveHeight + nextBarHeight) / 2;
371
+ const xc = (nextX + x) / 2;
372
+ const yc = (nextY + y) / 2;
373
+ path.quadraticCurveTo(nextX, nextY, xc, yc);
374
+ }
375
+ }
376
+
377
+ // Close the path with a smooth curve back to start
378
+ const lastY = (waveHeight + firstValue * waveHeight) / 2;
379
+ const controlX = startX;
380
+ const controlY = (lastY + firstY) / 2;
381
+ path.quadraticCurveTo(controlX, controlY, startX, firstY);
382
+
383
+ ctx.fill(path);
330
384
  }
331
385
 
332
- frameTask = new Task(this, {
333
- autoRun: EF_INTERACTIVE,
334
- args: () => [this.targetElement.audioBufferTask.status] as const,
335
- task: async () => {
336
- await this.targetElement.audioBufferTask.taskComplete;
337
- // Lazy initialize canvas, if we don't stash it, we re-init
338
- // every frame, which blanks the canvas, causing flicker.
339
- this.ctx ||= this.initCanvas();
340
- const ctx = this.ctx;
341
- if (!ctx) return;
386
+ protected drawSpikes(
387
+ ctx: CanvasRenderingContext2D,
388
+ frequencyData: Uint8Array,
389
+ ) {
390
+ const canvas = ctx.canvas;
391
+ const waveWidth = canvas.width;
392
+ const waveHeight = canvas.height;
393
+ const paddingOuter = 0.01;
394
+ const availableWidth = waveWidth * (1 - 2 * paddingOuter);
395
+ const startX = waveWidth * paddingOuter;
342
396
 
343
- if (!this.targetElement.audioBufferTask.value) return;
397
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
398
+ const path = new Path2D();
399
+
400
+ // Draw top curve
401
+ const firstValue = (frequencyData[0] ?? 0) / 255;
402
+ const firstY = (waveHeight - firstValue * waveHeight) / 2;
403
+ path.moveTo(startX, firstY);
344
404
 
345
- if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
346
- const FRAME_DURATION_MS = 48000 / 1000;
347
- const FRAME_SMEAR_MS = FRAME_DURATION_MS * 1;
348
- const FRAME_SMEAR_S = FRAME_SMEAR_MS / 1000;
405
+ // Draw top half
406
+ frequencyData.forEach((value, i) => {
407
+ const normalizedValue = Math.min((value / 255) * 2, 1);
408
+ const x = startX + (i / (frequencyData.length - 1)) * availableWidth;
409
+ const barHeight = normalizedValue * (waveHeight / 2);
410
+ const y = (waveHeight - barHeight * 2) / 2;
349
411
 
350
- const audioContext = new OfflineAudioContext(2, 48000 / 25, 48000);
351
- const audioBufferSource = audioContext.createBufferSource();
352
- audioBufferSource.buffer =
353
- this.targetElement.audioBufferTask.value.buffer;
354
- const analyser = audioContext.createAnalyser();
412
+ if (i === 0) {
413
+ path.moveTo(x, y);
414
+ } else {
415
+ const prevX =
416
+ startX + ((i - 1) / (frequencyData.length - 1)) * availableWidth;
417
+ const prevValue = (frequencyData[i - 1] ?? 0) / 255;
418
+ const prevBarHeight = prevValue * (waveHeight / 2);
419
+ const prevY = (waveHeight - prevBarHeight * 2) / 2;
420
+ const xc = (prevX + x) / 2;
421
+ const yc = (prevY + y) / 2;
422
+ path.quadraticCurveTo(prevX, prevY, xc, yc);
423
+ }
424
+ });
355
425
 
356
- // Adjust FFT size for better frequency resolution
357
- analyser.fftSize = 128 * 8;
426
+ // Draw bottom half
427
+ for (let i = frequencyData.length - 1; i >= 0; i--) {
428
+ const normalizedValue = Math.min(((frequencyData[i] ?? 0) / 255) * 2, 1);
429
+ const x = startX + (i / (frequencyData.length - 1)) * availableWidth;
430
+ const barHeight = normalizedValue * (waveHeight / 2);
431
+ const y = (waveHeight + barHeight * 2) / 2;
358
432
 
359
- audioBufferSource.connect(analyser);
433
+ if (i === frequencyData.length - 1) {
434
+ path.lineTo(x, y);
435
+ } else {
436
+ const nextX =
437
+ startX + ((i + 1) / (frequencyData.length - 1)) * availableWidth;
438
+ const nextValue = (frequencyData[i + 1] ?? 0) / 255;
439
+ const nextBarHeight = nextValue * (waveHeight / 2);
440
+ const nextY = (waveHeight + nextBarHeight * 2) / 2;
441
+ const xc = (nextX + x) / 2;
442
+ const yc = (nextY + y) / 2;
443
+ path.quadraticCurveTo(nextX, nextY, xc, yc);
444
+ }
445
+ }
360
446
 
361
- const startTime = Math.max(
362
- 0,
363
- (this.targetElement.trimAdjustedOwnCurrentTimeMs -
364
- this.targetElement.audioBufferTask.value.startOffsetMs) /
365
- 1000,
366
- );
447
+ // Close the path with a smooth curve
448
+ const lastY = (waveHeight + firstValue * waveHeight) / 2;
449
+ const controlX = startX;
450
+ const controlY = (lastY + firstY) / 2;
451
+ path.quadraticCurveTo(controlX, controlY, startX, firstY);
367
452
 
368
- audioBufferSource.start(0, startTime, FRAME_SMEAR_S);
369
- await audioContext.startRendering();
453
+ ctx.fill(path);
454
+ }
370
455
 
371
- const frameData = new Uint8Array(analyser.frequencyBinCount);
372
- analyser.getByteFrequencyData(frameData);
456
+ frameTask = new Task(this, {
457
+ autoRun: EF_INTERACTIVE,
458
+ args: () => {
459
+ return [
460
+ this.targetElement,
461
+ this.targetElement?.frequencyDataTask.value,
462
+ ] as const;
463
+ },
464
+ task: async () => {
465
+ if (!this.targetElement) return;
466
+ await this.targetElement.frequencyDataTask.taskComplete;
467
+ this.ctx ||= this.initCanvas();
468
+ const ctx = this.ctx;
469
+ if (!ctx) return;
373
470
 
374
- const smoothedData = frameData.slice(0, frameData.length / 2);
471
+ const frequencyData = this.targetElement.frequencyDataTask.value;
472
+ if (!frequencyData) return;
375
473
 
376
- if (this.color === "currentColor") {
377
- const computedStyle = getComputedStyle(this);
378
- const currentColor = computedStyle.color;
379
- ctx.strokeStyle = currentColor;
380
- ctx.fillStyle = currentColor;
381
- } else {
382
- }
474
+ ctx.save();
475
+ if (this.color === "currentColor") {
476
+ const computedStyle = getComputedStyle(this);
477
+ const currentColor = computedStyle.color;
478
+ ctx.strokeStyle = currentColor;
479
+ ctx.fillStyle = currentColor;
480
+ } else {
481
+ ctx.strokeStyle = this.color;
482
+ ctx.fillStyle = this.color;
483
+ }
383
484
 
384
- switch (this.mode) {
385
- case "bars":
386
- this.drawBars(ctx, smoothedData);
387
- break;
388
- case "bricks":
389
- this.drawBricks(ctx, smoothedData);
390
- break;
391
- case "curve":
392
- this.drawCurve(ctx, smoothedData);
393
- break;
394
- case "line":
395
- this.drawLine(ctx, smoothedData);
396
- break;
397
- case "pixel":
398
- this.drawPixel(ctx, smoothedData);
399
- break;
400
- case "wave":
401
- this.drawWave(ctx, smoothedData);
402
- break;
403
- case "roundBars":
404
- this.drawRoundBars(ctx, smoothedData);
405
- break;
406
- case "equalizer":
407
- this.drawEqualizer(ctx, smoothedData);
408
- break;
409
- }
485
+ switch (this.mode) {
486
+ case "bars":
487
+ this.drawBars(ctx, frequencyData);
488
+ break;
489
+ case "bricks":
490
+ this.drawBricks(ctx, frequencyData);
491
+ break;
492
+ case "line":
493
+ this.drawLine(ctx, frequencyData);
494
+ break;
495
+ case "pixel":
496
+ this.drawPixel(ctx, frequencyData);
497
+ break;
498
+ case "wave":
499
+ this.drawWave(ctx, frequencyData);
500
+ break;
501
+ case "spikes":
502
+ this.drawSpikes(ctx, frequencyData);
503
+ break;
504
+ case "roundBars":
505
+ this.drawRoundBars(ctx, frequencyData);
506
+ break;
410
507
  }
508
+
509
+ ctx.restore();
411
510
  },
412
511
  });
413
512
 
414
513
  get durationMs() {
514
+ if (!this.targetElement) return 0;
415
515
  return this.targetElement.durationMs;
416
516
  }
417
517
 
418
- get targetElement() {
419
- const target = document.getElementById(this.targetSelector ?? "");
420
- if (target instanceof EFAudio || target instanceof EFVideo) {
421
- return target;
422
- }
423
- throw new Error("Invalid target, must be an EFAudio or EFVideo element");
424
- }
425
-
426
518
  protected updated(changedProperties: PropertyValueMap<this>): void {
427
519
  super.updated(changedProperties);
428
520
 
@@ -433,3 +525,9 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
433
525
  }
434
526
  }
435
527
  }
528
+
529
+ declare global {
530
+ interface HTMLElementTagNameMap {
531
+ "ef-waveform": EFWaveform & Element;
532
+ }
533
+ }