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

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