@editframe/elements 0.15.0-beta.1 → 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.
@@ -1,16 +1,16 @@
1
- import { EFAudio } from "./EFAudio.js";
2
-
3
1
  import { CSSStyleObserver } from "@bramus/style-observer";
4
2
  import { Task } from "@lit/task";
5
3
  import { LitElement, type PropertyValueMap, css, html } from "lit";
6
- import { customElement, property } from "lit/decorators.js";
4
+ import { customElement, property, state } from "lit/decorators.js";
7
5
  import { type Ref, createRef, ref } from "lit/directives/ref.js";
8
6
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
9
7
  import { EF_RENDERING } from "../EF_RENDERING.js";
10
8
  import { TWMixin } from "../gui/TWMixin.js";
11
9
  import { CrossUpdateController } from "./CrossUpdateController.js";
10
+ import type { EFAudio } from "./EFAudio.js";
12
11
  import { EFTemporal } from "./EFTemporal.js";
13
- import { EFVideo } from "./EFVideo.js";
12
+ import type { EFVideo } from "./EFVideo.js";
13
+ import { TargetController } from "./TargetController.ts";
14
14
 
15
15
  @customElement("ef-waveform")
16
16
  export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
@@ -45,20 +45,29 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
45
45
  type: String,
46
46
  attribute: "mode",
47
47
  })
48
- mode: "roundBars" | "bars" | "bricks" | "line" | "pixel" | "wave" = "bars";
48
+ mode:
49
+ | "roundBars"
50
+ | "bars"
51
+ | "bricks"
52
+ | "line"
53
+ | "curve"
54
+ | "pixel"
55
+ | "wave"
56
+ | "spikes" = "bars";
49
57
 
50
58
  @property({ type: String })
51
59
  color = "currentColor";
52
60
 
53
- @property({ type: String, attribute: "target", reflect: true })
54
- targetSelector = "";
61
+ @property({ type: String, reflect: true })
62
+ target = "";
63
+
64
+ @state()
65
+ targetElement: EFAudio | EFVideo | null = null;
55
66
 
56
67
  @property({ type: Number, attribute: "line-width" })
57
68
  lineWidth = 4;
58
69
 
59
- set target(value: string) {
60
- this.targetSelector = value;
61
- }
70
+ targetController: TargetController = new TargetController(this);
62
71
 
63
72
  connectedCallback() {
64
73
  super.connectedCallback();
@@ -136,8 +145,6 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
136
145
  ctx.reset();
137
146
 
138
147
  // Scale all drawing operations by dpr
139
- ctx.scale(dpr, dpr);
140
-
141
148
  return ctx;
142
149
  }
143
150
 
@@ -145,31 +152,25 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
145
152
  const canvas = ctx.canvas;
146
153
  const waveWidth = canvas.width;
147
154
  const waveHeight = canvas.height;
148
- const baseline = waveHeight / 4;
149
155
 
150
- // Calculate bar width with padding
151
156
  const totalBars = frequencyData.length;
152
- const paddingInner = 0.5; // 50% padding between bars
153
- const paddingOuter = 0.01; // 1% padding on edges
154
- const availableWidth = waveWidth * (1 - 2 * paddingOuter);
157
+ const paddingInner = 0.5;
158
+ const paddingOuter = 0.01;
159
+ const availableWidth = waveWidth;
155
160
  const barWidth =
156
161
  availableWidth / (totalBars + (totalBars - 1) * paddingInner);
157
162
 
158
163
  ctx.clearRect(0, 0, waveWidth, waveHeight);
159
-
160
- // Create a single Path2D object for all bars
161
164
  const path = new Path2D();
162
165
 
163
166
  frequencyData.forEach((value, i) => {
164
- const normalizedValue = value / 255;
165
- const height = normalizedValue * (waveHeight / 2);
167
+ const normalizedValue = Math.min((value / 255) * 2, 1);
168
+ const barHeight = normalizedValue * waveHeight;
169
+ const y = (waveHeight - barHeight) / 2;
166
170
  const x = waveWidth * paddingOuter + i * (barWidth * (1 + paddingInner));
167
- const y = baseline - height;
168
-
169
- path.rect(x, y, barWidth, height * 2);
171
+ path.rect(x, y, barWidth, barHeight);
170
172
  });
171
173
 
172
- // Single fill operation for all bars
173
174
  ctx.fill(path);
174
175
  }
175
176
 
@@ -185,11 +186,16 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
185
186
 
186
187
  const columnWidth = waveWidth / frequencyData.length;
187
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
191
+
188
192
  frequencyData.forEach((value, i) => {
189
- const brickHeight = (value / 255) * waveHeight;
190
- for (let j = 0; j <= brickHeight; j++) {
193
+ const normalizedValue = Math.min((value / 255) * 2, 1);
194
+ const brickCount = Math.floor(normalizedValue * maxBricks);
195
+
196
+ for (let j = 0; j < brickCount; j++) {
191
197
  const x = columnWidth * i;
192
- const y = waveHeight - (j * columnWidth + boxSize);
198
+ const y = waveHeight - (j + 1) * (boxSize + verticalGap); // Include gap in position calculation
193
199
  path.rect(x, y, boxSize, boxSize);
194
200
  }
195
201
  });
@@ -204,13 +210,12 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
204
210
  const canvas = ctx.canvas;
205
211
  const waveWidth = canvas.width;
206
212
  const waveHeight = canvas.height;
207
- const baseline = waveHeight / 4;
208
213
 
209
214
  // Similar padding calculation as drawBars
210
215
  const totalBars = frequencyData.length;
211
216
  const paddingInner = 0.5;
212
217
  const paddingOuter = 0.01;
213
- const availableWidth = waveWidth * (1 - 2 * paddingOuter);
218
+ const availableWidth = waveWidth;
214
219
  const barWidth =
215
220
  availableWidth / (totalBars + (totalBars - 1) * paddingInner);
216
221
 
@@ -220,13 +225,13 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
220
225
  const path = new Path2D();
221
226
 
222
227
  frequencyData.forEach((value, i) => {
223
- const normalizedValue = value / 255;
224
- const height = normalizedValue * (waveHeight / 2);
228
+ const normalizedValue = Math.min((value / 255) * 2, 1);
229
+ const height = normalizedValue * waveHeight; // Use full wave height like in drawBars
225
230
  const x = waveWidth * paddingOuter + i * (barWidth * (1 + paddingInner));
226
- const y = baseline - height;
231
+ const y = (waveHeight - height) / 2; // Center vertically
227
232
 
228
233
  // Add rounded rectangle to path
229
- path.roundRect(x, y, barWidth, height * 2, barWidth / 2);
234
+ path.roundRect(x, y, barWidth, height, barWidth / 2);
230
235
  });
231
236
 
232
237
  // Single fill operation for all bars
@@ -239,9 +244,9 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
239
244
  ) {
240
245
  const canvas = ctx.canvas;
241
246
  const waveWidth = canvas.width;
242
- const waveHeight = canvas.height / 2;
243
- const barWidth = (waveWidth / frequencyData.length) * 0.8;
247
+ const waveHeight = canvas.height;
244
248
  const baseline = waveHeight / 2;
249
+ const barWidth = (waveWidth / frequencyData.length) * 0.8;
245
250
 
246
251
  ctx.clearRect(0, 0, waveWidth, waveHeight);
247
252
 
@@ -272,13 +277,47 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
272
277
  protected drawLine(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array) {
273
278
  const canvas = ctx.canvas;
274
279
  const waveWidth = canvas.width;
275
- const waveHeight = canvas.height / 2;
280
+ const waveHeight = canvas.height;
276
281
 
277
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
278
287
 
279
- // Create a single Path2D object for the curve
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);
307
+ }
308
+
309
+ protected drawCurve(
310
+ ctx: CanvasRenderingContext2D,
311
+ frequencyData: Uint8Array,
312
+ ) {
313
+ const canvas = ctx.canvas;
314
+ const waveWidth = canvas.width;
315
+ const waveHeight = canvas.height;
316
+
317
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
280
318
  const path = new Path2D();
281
319
 
320
+ // Draw smooth curves between points using quadratic curves
282
321
  frequencyData.forEach((value, i) => {
283
322
  const x = (i / frequencyData.length) * waveWidth;
284
323
  const y = (1 - value / 255) * waveHeight;
@@ -286,7 +325,11 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
286
325
  if (i === 0) {
287
326
  path.moveTo(x, y);
288
327
  } else {
289
- path.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);
290
333
  }
291
334
  });
292
335
 
@@ -300,24 +343,21 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
300
343
  ) {
301
344
  const canvas = ctx.canvas;
302
345
  const waveWidth = canvas.width;
303
- const waveHeight = canvas.height / 2;
346
+ const waveHeight = canvas.height;
304
347
  const baseline = waveHeight / 2;
305
348
  const barWidth = waveWidth / frequencyData.length;
306
349
 
307
350
  ctx.clearRect(0, 0, waveWidth, waveHeight);
308
-
309
- // Create a single Path2D object for all pixels
310
351
  const path = new Path2D();
311
352
 
312
353
  frequencyData.forEach((value, i) => {
354
+ const normalizedValue = Math.min((value / 255) * 2, 1); // Updated normalization
313
355
  const x = i * (waveWidth / frequencyData.length);
314
- const barHeight = (value / 255) * baseline;
356
+ const barHeight = normalizedValue * (waveHeight / 2); // Half height since we extend both ways
315
357
  const y = baseline - barHeight;
316
-
317
- path.rect(x, y, barWidth, barHeight * 2);
358
+ path.rect(x, y, barWidth, barHeight * 2); // Double height to extend both ways
318
359
  });
319
360
 
320
- // Single fill operation for all pixels
321
361
  ctx.fill(path);
322
362
  }
323
363
 
@@ -325,54 +365,147 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
325
365
  const canvas = ctx.canvas;
326
366
  const waveWidth = canvas.width;
327
367
  const waveHeight = canvas.height;
328
- const baseline = canvas.height / 4;
368
+ const paddingOuter = 0.01;
369
+ const availableWidth = waveWidth * (1 - 2 * paddingOuter);
370
+ const startX = waveWidth * paddingOuter;
329
371
 
330
372
  ctx.clearRect(0, 0, waveWidth, waveHeight);
331
373
  const path = new Path2D();
332
- path.moveTo(0, baseline);
333
374
 
334
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);
379
+
380
+ // Draw top half
335
381
  frequencyData.forEach((value, i) => {
336
- const normalizedValue = value / 255;
337
- const x = (i / (frequencyData.length - 1)) * waveWidth;
338
- const y = baseline - normalizedValue * (waveHeight / 2);
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;
339
386
 
340
387
  if (i === 0) {
341
388
  path.moveTo(x, y);
342
389
  } else {
343
- const prevX = ((i - 1) / (frequencyData.length - 1)) * waveWidth;
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
+ }
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);
429
+ }
430
+
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;
441
+
442
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
443
+ const path = new Path2D();
444
+
445
+ // Draw top curve
446
+ const firstValue = (frequencyData[0] ?? 0) / 255;
447
+ const firstY = (waveHeight - firstValue * waveHeight) / 2;
448
+ path.moveTo(startX, firstY);
449
+
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;
456
+
457
+ if (i === 0) {
458
+ path.moveTo(x, y);
459
+ } else {
460
+ const prevX =
461
+ startX + ((i - 1) / (frequencyData.length - 1)) * availableWidth;
344
462
  const prevValue = (frequencyData[i - 1] ?? 0) / 255;
345
- const prevY = baseline - prevValue * (waveHeight / 2);
463
+ const prevBarHeight = prevValue * (waveHeight / 2);
464
+ const prevY = (waveHeight - prevBarHeight * 2) / 2;
346
465
  const xc = (prevX + x) / 2;
347
466
  const yc = (prevY + y) / 2;
348
467
  path.quadraticCurveTo(prevX, prevY, xc, yc);
349
468
  }
350
469
  });
351
470
 
352
- // Draw bottom curve
471
+ // Draw bottom half
353
472
  for (let i = frequencyData.length - 1; i >= 0; i--) {
354
- const normalizedValue = (frequencyData[i] ?? 0) / 255;
355
- const x = (i / (frequencyData.length - 1)) * waveWidth;
356
- const y = baseline + normalizedValue * (waveHeight / 2);
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;
357
477
 
358
478
  if (i === frequencyData.length - 1) {
359
479
  path.lineTo(x, y);
360
480
  } else {
361
- const nextX = ((i + 1) / (frequencyData.length - 1)) * waveWidth;
481
+ const nextX =
482
+ startX + ((i + 1) / (frequencyData.length - 1)) * availableWidth;
362
483
  const nextValue = (frequencyData[i + 1] ?? 0) / 255;
363
- const nextY = baseline + nextValue * (waveHeight / 2);
484
+ const nextBarHeight = nextValue * (waveHeight / 2);
485
+ const nextY = (waveHeight + nextBarHeight * 2) / 2;
364
486
  const xc = (nextX + x) / 2;
365
487
  const yc = (nextY + y) / 2;
366
488
  path.quadraticCurveTo(nextX, nextY, xc, yc);
367
489
  }
368
490
  }
369
491
 
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);
497
+
370
498
  ctx.fill(path);
371
499
  }
372
500
 
373
501
  frameTask = new Task(this, {
374
502
  autoRun: EF_INTERACTIVE,
375
- args: () => [this.targetElement?.frequencyDataTask.status] as const,
503
+ args: () => {
504
+ return [
505
+ this.targetElement,
506
+ this.targetElement?.frequencyDataTask.value,
507
+ ] as const;
508
+ },
376
509
  task: async () => {
377
510
  if (!this.targetElement) return;
378
511
  await this.targetElement.frequencyDataTask.taskComplete;
@@ -381,13 +514,18 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
381
514
  if (!ctx) return;
382
515
 
383
516
  const frequencyData = this.targetElement.frequencyDataTask.value;
384
- if (!frequencyData) return;
517
+ const byteTimeData = this.targetElement.byteTimeDomainTask.value;
518
+ if (!frequencyData || !byteTimeData) return;
385
519
 
520
+ ctx.save();
386
521
  if (this.color === "currentColor") {
387
522
  const computedStyle = getComputedStyle(this);
388
523
  const currentColor = computedStyle.color;
389
524
  ctx.strokeStyle = currentColor;
390
525
  ctx.fillStyle = currentColor;
526
+ } else {
527
+ ctx.strokeStyle = this.color;
528
+ ctx.fillStyle = this.color;
391
529
  }
392
530
 
393
531
  switch (this.mode) {
@@ -398,7 +536,10 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
398
536
  this.drawBricks(ctx, frequencyData);
399
537
  break;
400
538
  case "line":
401
- this.drawLine(ctx, frequencyData);
539
+ this.drawLine(ctx, byteTimeData);
540
+ break;
541
+ case "curve":
542
+ this.drawCurve(ctx, byteTimeData);
402
543
  break;
403
544
  case "pixel":
404
545
  this.drawPixel(ctx, frequencyData);
@@ -406,10 +547,15 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
406
547
  case "wave":
407
548
  this.drawWave(ctx, frequencyData);
408
549
  break;
550
+ case "spikes":
551
+ this.drawSpikes(ctx, frequencyData);
552
+ break;
409
553
  case "roundBars":
410
554
  this.drawRoundBars(ctx, frequencyData);
411
555
  break;
412
556
  }
557
+
558
+ ctx.restore();
413
559
  },
414
560
  });
415
561
 
@@ -418,14 +564,6 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
418
564
  return this.targetElement.durationMs;
419
565
  }
420
566
 
421
- get targetElement() {
422
- const target = document.getElementById(this.targetSelector ?? "");
423
- if (target instanceof EFAudio || target instanceof EFVideo) {
424
- return target;
425
- }
426
- return null;
427
- }
428
-
429
567
  protected updated(changedProperties: PropertyValueMap<this>): void {
430
568
  super.updated(changedProperties);
431
569
 
@@ -436,3 +574,9 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
436
574
  }
437
575
  }
438
576
  }
577
+
578
+ declare global {
579
+ interface HTMLElementTagNameMap {
580
+ "ef-waveform": EFWaveform & Element;
581
+ }
582
+ }