@editframe/elements 0.11.0-beta.9 → 0.12.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 (51) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +8 -15
  2. package/dist/assets/src/MP4File.js +73 -20
  3. package/dist/elements/EFCaptions.d.ts +50 -6
  4. package/dist/elements/EFMedia.d.ts +1 -2
  5. package/dist/elements/EFTimegroup.browsertest.d.ts +4 -0
  6. package/dist/elements/EFTimegroup.d.ts +23 -2
  7. package/dist/elements/EFWaveform.d.ts +15 -11
  8. package/dist/elements/src/EF_FRAMEGEN.js +24 -26
  9. package/dist/elements/src/elements/EFCaptions.js +295 -42
  10. package/dist/elements/src/elements/EFImage.js +0 -6
  11. package/dist/elements/src/elements/EFMedia.js +70 -18
  12. package/dist/elements/src/elements/EFTemporal.js +13 -10
  13. package/dist/elements/src/elements/EFTimegroup.js +37 -12
  14. package/dist/elements/src/elements/EFVideo.js +1 -4
  15. package/dist/elements/src/elements/EFWaveform.js +250 -143
  16. package/dist/elements/src/gui/ContextMixin.js +44 -11
  17. package/dist/elements/src/gui/EFPreview.js +3 -1
  18. package/dist/elements/src/gui/EFScrubber.js +142 -0
  19. package/dist/elements/src/gui/EFTimeDisplay.js +81 -0
  20. package/dist/elements/src/gui/EFTogglePlay.js +11 -19
  21. package/dist/elements/src/gui/EFWorkbench.js +1 -24
  22. package/dist/elements/src/gui/TWMixin.css.js +1 -1
  23. package/dist/elements/src/index.js +8 -1
  24. package/dist/gui/ContextMixin.d.ts +2 -1
  25. package/dist/gui/EFScrubber.d.ts +23 -0
  26. package/dist/gui/EFTimeDisplay.d.ts +17 -0
  27. package/dist/gui/EFTogglePlay.d.ts +0 -2
  28. package/dist/gui/EFWorkbench.d.ts +0 -1
  29. package/dist/index.d.ts +3 -1
  30. package/dist/style.css +6 -801
  31. package/package.json +2 -2
  32. package/src/elements/EFCaptions.browsertest.ts +6 -6
  33. package/src/elements/EFCaptions.ts +325 -56
  34. package/src/elements/EFImage.browsertest.ts +4 -17
  35. package/src/elements/EFImage.ts +0 -6
  36. package/src/elements/EFMedia.browsertest.ts +10 -19
  37. package/src/elements/EFMedia.ts +87 -20
  38. package/src/elements/EFTemporal.browsertest.ts +14 -0
  39. package/src/elements/EFTemporal.ts +14 -0
  40. package/src/elements/EFTimegroup.browsertest.ts +37 -0
  41. package/src/elements/EFTimegroup.ts +42 -17
  42. package/src/elements/EFVideo.ts +1 -4
  43. package/src/elements/EFWaveform.ts +339 -314
  44. package/src/gui/ContextMixin.browsertest.ts +28 -2
  45. package/src/gui/ContextMixin.ts +52 -14
  46. package/src/gui/EFPreview.ts +4 -2
  47. package/src/gui/EFScrubber.ts +145 -0
  48. package/src/gui/EFTimeDisplay.ts +81 -0
  49. package/src/gui/EFTogglePlay.ts +19 -25
  50. package/src/gui/EFWorkbench.ts +3 -36
  51. package/dist/elements/src/elements/util.js +0 -11
@@ -1,8 +1,7 @@
1
1
  import { EFAudio } from "./EFAudio.ts";
2
2
 
3
3
  import { Task } from "@lit/task";
4
- import * as d3 from "d3";
5
- import { LitElement, css, html } from "lit";
4
+ import { LitElement, type PropertyValueMap, css, html } from "lit";
6
5
  import { customElement, property } from "lit/decorators.js";
7
6
  import { type Ref, createRef, ref } from "lit/directives/ref.js";
8
7
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
@@ -15,18 +14,24 @@ import { EFVideo } from "./EFVideo.ts";
15
14
  export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
16
15
  static styles = [
17
16
  css`
18
- svg {
17
+ :host {
18
+ display: block;
19
19
  all: inherit;
20
20
  }
21
21
  `,
22
22
  ];
23
- svgRef: Ref<SVGElement> = createRef();
23
+
24
+ canvasRef: Ref<HTMLCanvasElement> = createRef();
25
+ private ctx: CanvasRenderingContext2D | null = null;
26
+
24
27
  createRenderRoot() {
25
28
  return this;
26
29
  }
30
+
27
31
  render() {
28
- return html` <svg ${ref(this.svgRef)} store></svg> `;
32
+ return html`<canvas ${ref(this.canvasRef)}></canvas>`;
29
33
  }
34
+
30
35
  @property({
31
36
  type: String,
32
37
  attribute: "mode",
@@ -58,358 +63,368 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
58
63
  }
59
64
  }
60
65
 
61
- protected drawBars(svg: SVGElement, frequencyData: Uint8Array) {
62
- const waveWidth = svg.clientWidth * devicePixelRatio;
63
- const waveHeight = svg.clientHeight;
64
- const waveLeft = 0;
65
- const waveRight = waveWidth;
66
+ protected initCanvas() {
67
+ console.count("initCanvas");
68
+ const canvas = this.canvasRef.value;
69
+ if (!canvas) return null;
70
+
71
+ const rect = this.getBoundingClientRect();
72
+ const dpr = window.devicePixelRatio;
73
+
74
+ canvas.style.width = `${rect.width}px`;
75
+ canvas.style.height = `${rect.height}px`;
66
76
 
67
- const barX = d3
68
- .scaleBand()
69
- .paddingInner(0.5)
70
- .paddingOuter(0.01)
71
- .domain(d3.range(frequencyData.length).map((n) => String(n)))
72
- .rangeRound([waveLeft, waveRight]);
77
+ canvas.width = rect.width * dpr;
78
+ canvas.height = rect.height * dpr;
73
79
 
74
- const height = d3
75
- .scaleLinear()
76
- .domain([0, 255])
77
- .range([0, waveHeight / 2]);
80
+ const ctx = canvas.getContext("2d");
81
+ if (!ctx) return null;
78
82
 
83
+ // Scale all drawing operations by dpr
84
+ ctx.scale(dpr, dpr);
85
+
86
+ return ctx;
87
+ }
88
+
89
+ protected drawBars(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array) {
90
+ ctx.strokeStyle = this.color;
91
+ ctx.fillStyle = this.color;
92
+
93
+ const canvas = ctx.canvas;
94
+ const waveWidth = canvas.width / devicePixelRatio;
95
+ const waveHeight = canvas.height / devicePixelRatio;
96
+ const barWidth = (waveWidth / frequencyData.length) * 0.8; // 0.8 for padding
79
97
  const baseline = waveHeight / 2;
80
98
 
81
- const bars = d3.select(svg).selectAll("rect").data(frequencyData);
82
- const minBarHeight = 2;
99
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
83
100
 
84
- bars
85
- .enter()
86
- .append("rect")
87
- // @ts-ignore Not sure why this doesn't pass typechecks.
88
- .merge(bars)
89
- .attr("x", (_, i) => barX(String(i)) || 0)
90
- .attr("y", (value) => baseline - height(value))
91
- .attr("width", barX.bandwidth() / 1.2)
92
- .attr("height", (value) => Math.max(height(value) * 2, minBarHeight));
101
+ frequencyData.forEach((value, i) => {
102
+ const height = (value / 255) * (waveHeight / 2);
103
+ const x = i * (waveWidth / frequencyData.length);
104
+ const y = baseline - height;
93
105
 
94
- bars.exit().remove();
106
+ ctx.fillRect(x, y, barWidth, Math.max(height * 2, 2));
107
+ });
95
108
  }
96
109
 
97
- protected drawBricks(svg: SVGElement, frequencyData: Uint8Array) {
98
- const waveWidth = svg.clientWidth * devicePixelRatio;
99
- const waveHeight = svg.clientHeight * devicePixelRatio;
110
+ protected drawBricks(
111
+ ctx: CanvasRenderingContext2D,
112
+ frequencyData: Uint8Array,
113
+ ) {
114
+ ctx.strokeStyle = this.color;
115
+ ctx.fillStyle = this.color;
116
+
117
+ const canvas = ctx.canvas;
118
+ const waveWidth = canvas.width / devicePixelRatio;
119
+ const waveHeight = canvas.height / devicePixelRatio;
100
120
  const brickWidth = waveWidth / frequencyData.length;
101
121
  const brickHeightFactor = waveHeight / 255 / 2;
102
122
  const brickPadding = 2;
103
123
  const midHeight = waveHeight / 2;
104
124
 
105
- d3.select(svg)
106
- .selectAll("line.brickBaseLine")
107
- .data([0])
108
- .join("line")
109
- .attr("class", "brickBaseLine")
110
- .attr("x1", 0)
111
- .attr("x2", waveWidth)
112
- .attr("y1", midHeight)
113
- .attr("y2", midHeight)
114
- .attr("stroke", "currentColor")
115
- .attr("stroke-width", 4)
116
- .attr("stroke-dasharray", "2");
117
-
118
- d3.select(svg)
119
- .selectAll("rect.brick")
120
- .data(frequencyData)
121
- .join("rect")
122
- .attr("class", "brick")
123
- .attr("x", (_d, i) => i * brickWidth)
124
- .attr("y", (d) => midHeight - d * brickHeightFactor)
125
- .attr("width", brickWidth - brickPadding)
126
- .attr("height", (d) => d * brickHeightFactor * 2);
125
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
126
+
127
+ // Draw baseline
128
+ ctx.beginPath();
129
+ ctx.setLineDash([2]);
130
+ ctx.lineWidth = 4;
131
+ ctx.moveTo(0, midHeight);
132
+ ctx.lineTo(waveWidth, midHeight);
133
+ ctx.stroke();
134
+ ctx.setLineDash([]); // Reset dash
135
+
136
+ // Draw bricks
137
+ frequencyData.forEach((value, i) => {
138
+ const x = i * brickWidth;
139
+ const height = value * brickHeightFactor * 2;
140
+ const y = midHeight - height / 2;
141
+
142
+ ctx.fillRect(x, y, brickWidth - brickPadding, height);
143
+ });
127
144
  }
128
145
 
129
- protected drawLine(svg: SVGElement, frequencyData: Uint8Array) {
130
- const waveWidth = svg.clientWidth * devicePixelRatio;
131
- const waveHeight = svg.clientHeight * devicePixelRatio;
132
-
133
- const xScale = d3
134
- .scaleLinear()
135
- .domain([0, frequencyData.length - 1])
136
- .range([0, waveWidth]);
137
-
138
- const yScale = d3.scaleLinear().domain([0, 255]).range([waveHeight, 0]);
139
-
140
- const lineGenerator = d3
141
- .line<Uint8Array[number]>()
142
- .x((_, i) => xScale(i))
143
- .y((d) => yScale(d));
144
-
145
- const pathData = lineGenerator(frequencyData);
146
-
147
- d3.select(svg)
148
- .selectAll("path.line")
149
- .data([frequencyData])
150
- .join("path")
151
- .attr("class", "line")
152
- .attr("d", pathData)
153
- .attr("fill", "none")
154
- .attr("stroke", "currentColor")
155
- .attr("stroke-width", 4);
156
- }
146
+ protected drawLine(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array) {
147
+ ctx.strokeStyle = this.color;
148
+ ctx.fillStyle = this.color;
157
149
 
158
- protected drawRoundBars(svg: SVGElement, frequencyData: Uint8Array) {
159
- const waveWidth = svg.clientWidth * devicePixelRatio;
160
- const waveHeight = svg.clientHeight;
161
- const waveLeft = 0;
162
- const waveRight = waveWidth;
150
+ const canvas = ctx.canvas;
151
+ const waveWidth = canvas.width / devicePixelRatio;
152
+ const waveHeight = canvas.height / devicePixelRatio;
163
153
 
164
- const barX = d3
165
- .scaleBand()
166
- .paddingInner(0.5)
167
- .paddingOuter(0.01)
168
- .domain(d3.range(frequencyData.length).map((n) => String(n)))
169
- .rangeRound([waveLeft, waveRight]);
154
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
155
+ ctx.beginPath();
170
156
 
171
- const height = d3
172
- .scaleLinear()
173
- .domain([0, 255])
174
- .range([0, waveHeight / 2]);
157
+ frequencyData.forEach((value, i) => {
158
+ const x = (i / frequencyData.length) * waveWidth;
159
+ const y = (1 - value / 255) * waveHeight;
175
160
 
161
+ if (i === 0) {
162
+ ctx.moveTo(x, y);
163
+ } else {
164
+ ctx.lineTo(x, y);
165
+ }
166
+ });
167
+
168
+ ctx.lineWidth = 4;
169
+ ctx.stroke();
170
+ }
171
+
172
+ protected drawRoundBars(
173
+ ctx: CanvasRenderingContext2D,
174
+ frequencyData: Uint8Array,
175
+ ) {
176
+ ctx.strokeStyle = this.color;
177
+ ctx.fillStyle = this.color;
178
+
179
+ const canvas = ctx.canvas;
180
+ const waveWidth = canvas.width / devicePixelRatio;
181
+ const waveHeight = canvas.height / devicePixelRatio;
182
+ const barWidth = (waveWidth / frequencyData.length) * 0.5; // Add padding
176
183
  const baseline = waveHeight / 2;
184
+ const maxHeight = waveHeight / 2;
185
+
186
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
187
+
188
+ frequencyData.forEach((value, i) => {
189
+ const height = (value / 255) * maxHeight;
190
+ const x = i * (waveWidth / frequencyData.length);
191
+ const y = baseline - height;
177
192
 
178
- const bars = d3.select(svg).selectAll("rect").data(frequencyData);
179
- const minBarHeight = 2;
180
-
181
- bars
182
- .enter()
183
- .append("rect")
184
- // @ts-ignore Not sure why this doesn't pass typechecks.
185
- .merge(bars)
186
- .attr("x", (_, i) => barX(String(i)) || 0)
187
- .attr("y", (value) => baseline - height(value))
188
- .attr("width", barX.bandwidth() / 1.2)
189
- .attr("height", (value) => Math.max(height(value) * 2, minBarHeight))
190
- .attr("rx", barX.bandwidth())
191
- .attr("ry", barX.bandwidth());
192
-
193
- bars.exit().remove();
193
+ // Draw rounded rectangle
194
+ const radius = barWidth / 2;
195
+ ctx.beginPath();
196
+ ctx.roundRect(x, y, barWidth, Math.max(height * 2, 2), radius);
197
+ ctx.fill();
198
+ });
194
199
  }
195
- protected drawEqualizer(svg: SVGElement, frequencyData: Uint8Array) {
196
- const waveWidth = svg.clientWidth * devicePixelRatio;
197
- const waveHeight = svg.clientHeight * devicePixelRatio;
198
- const barWidth = waveWidth / frequencyData.length;
199
- const barPadding = 1;
200
- const minHeight = 1;
201
-
202
- const heightScale = d3
203
- .scaleLinear()
204
- .domain([0, 255])
205
- .range([0, waveHeight / 2]);
206
-
207
- d3.select(svg)
208
- .selectAll("line.equalizerBaseLine")
209
- .data([0])
210
- .join("line")
211
- .attr("class", "equalizerBaseLine")
212
- .attr("x1", 0)
213
- .attr("x2", waveWidth)
214
- .attr("y1", waveHeight / 2)
215
- .attr("y2", waveHeight / 2)
216
- .attr("stroke-width", 2);
217
-
218
- d3.select(svg)
219
- .selectAll("rect.equalizerBar")
220
- .data(frequencyData)
221
- .join("rect")
222
- .attr("class", "equalizerBar")
223
- .attr("x", (_d, i) => i * barWidth + barPadding / 2)
224
- .attr(
225
- "y",
226
- (d) => waveHeight / 2 - Math.max(heightScale(d) / 2, minHeight),
227
- )
228
- .attr("width", barWidth - barPadding)
229
- .attr("height", (d) => Math.max(heightScale(d), minHeight));
230
-
231
- d3.select(svg)
232
- .selectAll("rect.equalizerBar")
233
- .transition()
234
- .duration(100)
235
- .attr(
236
- "y",
237
- (d) => waveHeight / 2 - Math.max(heightScale(Number(d)) / 2, minHeight),
238
- )
239
- .attr("height", (d) => Math.max(heightScale(Number(d)), minHeight));
200
+
201
+ protected drawEqualizer(
202
+ ctx: CanvasRenderingContext2D,
203
+ frequencyData: Uint8Array,
204
+ ) {
205
+ ctx.strokeStyle = this.color;
206
+ ctx.fillStyle = this.color;
207
+
208
+ const canvas = ctx.canvas;
209
+ const waveWidth = canvas.width / devicePixelRatio;
210
+ const waveHeight = canvas.height / devicePixelRatio;
211
+ const barWidth = (waveWidth / frequencyData.length) * 0.8;
212
+ const baseline = waveHeight / 2;
213
+
214
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
215
+
216
+ // Draw baseline
217
+ ctx.beginPath();
218
+ ctx.lineWidth = 2;
219
+ ctx.moveTo(0, baseline);
220
+ ctx.lineTo(waveWidth, baseline);
221
+ ctx.stroke();
222
+
223
+ // Draw bars
224
+ frequencyData.forEach((value, i) => {
225
+ const height = (value / 255) * (waveHeight / 2);
226
+ const x = i * (waveWidth / frequencyData.length);
227
+ const y = baseline - height;
228
+
229
+ ctx.fillRect(x, y, barWidth, Math.max(height * 2, 1));
230
+ });
240
231
  }
241
232
 
242
- protected drawCurve(svg: SVGElement, frequencyData: Uint8Array) {
243
- const waveWidth = svg.clientWidth * devicePixelRatio;
244
- const waveHeight = svg.clientHeight * devicePixelRatio;
245
-
246
- const xScale = d3
247
- .scaleLinear()
248
- .domain([0, frequencyData.length])
249
- .range([0, waveWidth]);
250
-
251
- const yScale = d3.scaleLinear().domain([0, 255]).range([waveHeight, 0]);
252
-
253
- const curveGenerator = d3
254
- .line<Uint8Array[number]>()
255
- .x((_, i) => xScale(i))
256
- .y((d) => yScale(d))
257
- .curve(d3.curveNatural);
258
-
259
- const pathData = curveGenerator(frequencyData);
260
-
261
- d3.select(svg)
262
- .selectAll("path.curve")
263
- .data([frequencyData])
264
- .join("path")
265
- .attr("class", "curve")
266
- .attr("d", pathData)
267
- .attr("fill", "none")
268
- .attr("stroke", "currentColor")
269
- .attr("stroke-width", 4);
233
+ protected drawCurve(
234
+ ctx: CanvasRenderingContext2D,
235
+ frequencyData: Uint8Array,
236
+ ) {
237
+ ctx.strokeStyle = this.color;
238
+ ctx.fillStyle = this.color;
239
+
240
+ const canvas = ctx.canvas;
241
+ const waveWidth = canvas.width / devicePixelRatio;
242
+ const waveHeight = canvas.height / devicePixelRatio;
243
+
244
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
245
+ ctx.beginPath();
246
+
247
+ frequencyData.forEach((value, i) => {
248
+ const x = (i / frequencyData.length) * waveWidth;
249
+ const y = (1 - value / 255) * waveHeight;
250
+
251
+ if (i === 0) {
252
+ ctx.moveTo(x, y);
253
+ } else {
254
+ ctx.lineTo(x, y);
255
+ }
256
+ });
257
+
258
+ ctx.lineWidth = 4;
259
+ ctx.stroke();
270
260
  }
271
261
 
272
- protected drawPixel(svg: SVGElement, frequencyData: Uint8Array) {
273
- const waveWidth = svg.clientWidth * devicePixelRatio;
274
- const waveHeight = svg.clientHeight;
262
+ protected drawPixel(
263
+ ctx: CanvasRenderingContext2D,
264
+ frequencyData: Uint8Array,
265
+ ) {
266
+ ctx.strokeStyle = this.color;
267
+ ctx.fillStyle = this.color;
268
+
269
+ const canvas = ctx.canvas;
270
+ const waveWidth = canvas.width / devicePixelRatio;
271
+ const waveHeight = canvas.height / devicePixelRatio;
275
272
  const baseline = waveHeight / 2;
273
+ const barWidth = (waveWidth / frequencyData.length) * 0.97; // Account for padding
276
274
 
277
- const barX = d3
278
- .scaleBand()
279
- .domain(d3.range(frequencyData.length).map(String))
280
- .rangeRound([0, waveWidth])
281
- .paddingInner(0.03)
282
- .paddingOuter(0.02);
275
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
283
276
 
284
- const height = d3.scaleLinear().domain([0, 255]).range([0, baseline]);
277
+ frequencyData.forEach((value, i) => {
278
+ const x = i * (waveWidth / frequencyData.length);
279
+ const barHeight = (value / 255) * baseline;
280
+ const y = baseline - barHeight;
285
281
 
286
- const bars = d3.select(svg).selectAll("rect").data(frequencyData);
282
+ ctx.fillRect(x, y, barWidth, barHeight * 2);
283
+ });
284
+ }
287
285
 
288
- bars
289
- .enter()
290
- .append("rect")
291
- // @ts-ignore Not sure why this doesn't pass typechecks.
292
- .merge(bars)
293
- .attr("x", (_, i) => barX(String(i)) || 0)
294
- .attr("y", (value) => baseline - height(value))
295
- .attr("width", barX.bandwidth())
296
- .attr("height", (value) => height(value) * 2);
286
+ protected drawWave(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array) {
287
+ ctx.strokeStyle = this.color;
288
+ ctx.fillStyle = this.color;
297
289
 
298
- bars.exit().remove();
299
- }
300
- protected drawWave(svg: SVGElement, frequencyData: Uint8Array) {
301
- const waveWidth = svg.clientWidth * devicePixelRatio;
302
- const waveHeight = svg.clientHeight;
303
-
304
- const barX = d3
305
- .scaleBand()
306
- .domain(d3.range(frequencyData.length).map(String))
307
- .rangeRound([0, waveWidth])
308
- .paddingInner(0.03)
309
- .paddingOuter(0.02);
310
-
311
- const height = d3
312
- .scaleLinear()
313
- .domain([0, 255])
314
- .range([0, waveHeight / 2]);
315
-
316
- d3.select(svg)
317
- .selectAll("line.baseline")
318
- .data([0])
319
- .join("line")
320
- .attr("class", "baseline")
321
- .attr("x1", (_, i) => barX(String(i)) || 0)
322
- .attr("x2", waveWidth)
323
- .attr("y1", waveHeight / 2)
324
- .attr("y2", waveHeight / 2)
325
- .attr("stroke", "currentColor")
326
- .attr("stroke-width", 2);
327
-
328
- const bars = d3.select(svg).selectAll("rect").data(frequencyData);
329
-
330
- bars
331
- .enter()
332
- .append("rect")
333
- // @ts-ignore Not sure why this doesn't pass typechecks.
334
- .merge(bars)
335
- .attr("x", (_, i) => barX(String(i)) || 0)
336
- .attr("y", (value) => waveHeight / 2 - height(value))
337
- .attr("width", barX.bandwidth())
338
- .attr("height", (value) => height(value) * 2);
339
-
340
- bars.exit().remove();
290
+ const canvas = ctx.canvas;
291
+ const waveWidth = canvas.width / devicePixelRatio;
292
+ const waveHeight = canvas.height / devicePixelRatio;
293
+ const baseline = waveHeight / 2;
294
+ const barWidth = (waveWidth / frequencyData.length) * 0.97; // Account for padding
295
+
296
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
297
+
298
+ // Draw baseline
299
+ ctx.beginPath();
300
+ ctx.moveTo(0, baseline);
301
+ ctx.lineTo(waveWidth, baseline);
302
+ ctx.strokeStyle = this.color;
303
+ ctx.lineWidth = 2;
304
+ ctx.stroke();
305
+
306
+ // Draw bars
307
+ frequencyData.forEach((value, i) => {
308
+ const x = i * (waveWidth / frequencyData.length);
309
+ const barHeight = (value / 255) * (waveHeight / 2);
310
+ const y = baseline - barHeight;
311
+
312
+ ctx.fillRect(x, y, barWidth, barHeight * 2);
313
+ });
341
314
  }
342
315
 
316
+ // Add this property to store the last frame timestamp
317
+ private lastFrameTime = 0;
318
+
343
319
  frameTask = new Task(this, {
344
320
  autoRun: EF_INTERACTIVE,
345
321
  args: () => [this.targetElement.audioBufferTask.status] as const,
346
322
  task: async () => {
323
+ // Calculate and log time since last frame
324
+ const currentTime = performance.now();
325
+ const timeSinceLastFrame = this.lastFrameTime
326
+ ? currentTime - this.lastFrameTime
327
+ : 0;
328
+ console.log(`Time since last frame: ${timeSinceLastFrame.toFixed(2)}ms`);
329
+ this.lastFrameTime = currentTime;
330
+
347
331
  await this.targetElement.audioBufferTask.taskComplete;
332
+ // Lazy initialize canvas, if we don't stash it, we re-init
333
+ // every frame, which blanks the canvas, causing flicker.
334
+ this.ctx ||= this.initCanvas();
335
+ const ctx = this.ctx;
336
+ if (!ctx) return;
337
+
338
+ if (!this.targetElement.audioBufferTask.value) return;
339
+
340
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
341
+ const FRAMES_TO_ANALYZE = 4;
342
+ const FRAME_DURATION_MS = 48000 / 100;
343
+
344
+ const multiFrameData: Uint8Array[] = [];
345
+
346
+ for (let i = 0; i < FRAMES_TO_ANALYZE; i++) {
347
+ const frameOffset = i - Math.floor(FRAMES_TO_ANALYZE / 2);
348
+ const audioContext = new OfflineAudioContext(2, 48000 / 25, 48000);
349
+ const audioBufferSource = audioContext.createBufferSource();
350
+ audioBufferSource.buffer =
351
+ this.targetElement.audioBufferTask.value.buffer;
352
+ const analyser = audioContext.createAnalyser();
353
+
354
+ // Adjust FFT size for better frequency resolution
355
+ analyser.fftSize = 128 / 2;
356
+ // Adjust smoothing to make peaks more pronounced
357
+ analyser.smoothingTimeConstant = 0.1;
358
+
359
+ audioBufferSource.connect(analyser);
360
+
361
+ const startTime = Math.max(
362
+ 0,
363
+ (this.targetElement.trimAdjustedOwnCurrentTimeMs -
364
+ this.targetElement.audioBufferTask.value.startOffsetMs) /
365
+ 1000 +
366
+ (frameOffset * FRAME_DURATION_MS) / 1000,
367
+ );
368
+
369
+ audioBufferSource.start(0, startTime, FRAME_DURATION_MS / 1000);
370
+ await audioContext.startRendering();
371
+
372
+ const frameData = new Uint8Array(analyser.frequencyBinCount);
373
+ analyser.getByteFrequencyData(frameData);
374
+
375
+ multiFrameData.push(frameData);
376
+ }
377
+
378
+ const dataLength = multiFrameData[0]?.length ?? 0;
379
+
380
+ // Average and enhance dynamics
381
+ const smoothedData = new Uint8Array(dataLength);
382
+ for (let i = 0; i < smoothedData.length; i++) {
383
+ let sum = 0;
384
+ for (const frameData of multiFrameData) {
385
+ sum += frameData[i] ?? 0;
386
+ }
387
+ let avg = sum / FRAMES_TO_ANALYZE;
388
+
389
+ // Enhance dynamics by applying a subtle exponential curve
390
+ avg = (avg / 255) ** 1.2 * 255;
391
+
392
+ smoothedData[i] = Math.round(avg);
393
+ }
394
+
395
+ // Use smoothedData for rendering
396
+ switch (this.mode) {
397
+ case "bars":
398
+ this.drawBars(ctx, smoothedData);
399
+ break;
400
+ case "bricks":
401
+ this.drawBricks(ctx, smoothedData);
402
+ break;
403
+ case "curve":
404
+ this.drawCurve(ctx, smoothedData);
405
+ break;
406
+ case "line":
407
+ this.drawLine(ctx, smoothedData);
408
+ break;
409
+ case "pixel":
410
+ this.drawPixel(ctx, smoothedData);
411
+ break;
412
+ case "wave":
413
+ this.drawWave(ctx, smoothedData);
414
+ break;
415
+ case "roundBars":
416
+ this.drawRoundBars(ctx, smoothedData);
417
+ break;
418
+ case "equalizer":
419
+ this.drawEqualizer(ctx, smoothedData);
420
+ break;
421
+ }
422
+ }
348
423
  },
349
424
  });
350
425
 
351
- protected async updated() {
352
- const svg = this.svgRef.value;
353
- if (!svg) {
354
- return;
355
- }
356
- if (!this.targetElement.audioBufferTask.value) {
357
- return;
358
- }
359
- if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
360
- const audioContext = new OfflineAudioContext(2, 48000 / 25, 48000);
361
- const audioBufferSource = audioContext.createBufferSource();
362
- audioBufferSource.buffer =
363
- this.targetElement.audioBufferTask.value.buffer;
364
- const analyser = audioContext.createAnalyser();
365
- analyser.fftSize = 256;
366
- audioBufferSource.connect(analyser);
367
-
368
- audioBufferSource.start(
369
- 0,
370
- Math.max(
371
- 0,
372
- (this.targetElement.trimAdjustedOwnCurrentTimeMs -
373
- this.targetElement.audioBufferTask.value.startOffsetMs) /
374
- 1000,
375
- ),
376
- 48000 / 1000,
377
- );
378
- await audioContext.startRendering();
379
- const frequencyData = new Uint8Array(analyser.frequencyBinCount);
380
- analyser.getByteFrequencyData(frequencyData);
381
- const rect = this.getBoundingClientRect();
382
-
383
- svg.setAttribute("width", (rect.width * devicePixelRatio).toString());
384
- svg.setAttribute("height", (rect.height * devicePixelRatio).toString());
385
-
386
- switch (this.mode) {
387
- case "bars":
388
- this.drawBars(svg, frequencyData);
389
- break;
390
- case "bricks":
391
- this.drawBricks(svg, frequencyData);
392
- break;
393
- case "curve":
394
- this.drawCurve(svg, frequencyData);
395
- break;
396
- case "line":
397
- this.drawLine(svg, frequencyData);
398
- break;
399
- case "pixel":
400
- this.drawPixel(svg, frequencyData);
401
- break;
402
- case "wave":
403
- this.drawWave(svg, frequencyData);
404
- break;
405
- case "roundBars":
406
- this.drawRoundBars(svg, frequencyData);
407
- break;
408
- case "equalizer":
409
- this.drawEqualizer(svg, frequencyData);
410
- break;
411
- }
412
- }
426
+ get durationMs() {
427
+ return this.targetElement.durationMs;
413
428
  }
414
429
 
415
430
  get targetElement() {
@@ -419,4 +434,14 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
419
434
  }
420
435
  throw new Error("Invalid target, must be an EFAudio or EFVideo element");
421
436
  }
437
+
438
+ protected updated(changedProperties: PropertyValueMap<this>): void {
439
+ super.updated(changedProperties);
440
+
441
+ // Trigger a redraw if color or mode changes
442
+ if (changedProperties.has("color") || changedProperties.has("mode")) {
443
+ // Request a new frame
444
+ this.frameTask.run();
445
+ }
446
+ }
422
447
  }