@editframe/elements 0.11.0-beta.8 → 0.12.0-beta.1

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