@editframe/elements 0.11.0-beta.9 → 0.12.0-beta.2
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.
- package/dist/EF_FRAMEGEN.d.ts +8 -15
- package/dist/assets/src/MP4File.js +5 -3
- package/dist/elements/EFCaptions.d.ts +50 -6
- package/dist/elements/EFMedia.d.ts +1 -1
- package/dist/elements/EFTimegroup.browsertest.d.ts +4 -0
- package/dist/elements/EFTimegroup.d.ts +23 -2
- package/dist/elements/EFWaveform.d.ts +15 -11
- package/dist/elements/src/EF_FRAMEGEN.js +24 -26
- package/dist/elements/src/elements/EFCaptions.js +295 -42
- package/dist/elements/src/elements/EFImage.js +0 -6
- package/dist/elements/src/elements/EFMedia.js +0 -5
- package/dist/elements/src/elements/EFTemporal.js +13 -10
- package/dist/elements/src/elements/EFTimegroup.js +37 -12
- package/dist/elements/src/elements/EFVideo.js +1 -4
- package/dist/elements/src/elements/EFWaveform.js +250 -143
- package/dist/elements/src/gui/ContextMixin.js +36 -7
- package/dist/elements/src/gui/EFScrubber.js +142 -0
- package/dist/elements/src/gui/EFTimeDisplay.js +81 -0
- package/dist/elements/src/gui/EFTogglePlay.js +14 -14
- package/dist/elements/src/gui/EFWorkbench.js +1 -24
- package/dist/elements/src/gui/TWMixin.css.js +1 -1
- package/dist/elements/src/index.js +8 -1
- package/dist/gui/ContextMixin.d.ts +2 -1
- package/dist/gui/EFScrubber.d.ts +23 -0
- package/dist/gui/EFTimeDisplay.d.ts +17 -0
- package/dist/gui/EFTogglePlay.d.ts +1 -1
- package/dist/gui/EFWorkbench.d.ts +0 -1
- package/dist/index.d.ts +3 -1
- package/dist/style.css +6 -801
- package/package.json +2 -2
- package/src/elements/EFCaptions.browsertest.ts +6 -6
- package/src/elements/EFCaptions.ts +325 -56
- package/src/elements/EFImage.browsertest.ts +4 -17
- package/src/elements/EFImage.ts +0 -6
- package/src/elements/EFMedia.browsertest.ts +8 -19
- package/src/elements/EFMedia.ts +1 -6
- package/src/elements/EFTemporal.browsertest.ts +14 -0
- package/src/elements/EFTemporal.ts +14 -0
- package/src/elements/EFTimegroup.browsertest.ts +37 -0
- package/src/elements/EFTimegroup.ts +42 -17
- package/src/elements/EFVideo.ts +1 -4
- package/src/elements/EFWaveform.ts +339 -314
- package/src/gui/ContextMixin.browsertest.ts +28 -2
- package/src/gui/ContextMixin.ts +41 -9
- package/src/gui/EFScrubber.ts +145 -0
- package/src/gui/EFTimeDisplay.ts +81 -0
- package/src/gui/EFTogglePlay.ts +21 -21
- package/src/gui/EFWorkbench.ts +3 -36
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { EFAudio } from "./EFAudio.ts";
|
|
2
2
|
|
|
3
3
|
import { Task } from "@lit/task";
|
|
4
|
-
import
|
|
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
|
-
|
|
17
|
+
:host {
|
|
18
|
+
display: block;
|
|
19
19
|
all: inherit;
|
|
20
20
|
}
|
|
21
21
|
`,
|
|
22
22
|
];
|
|
23
|
-
|
|
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
|
|
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
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
75
|
-
|
|
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
|
-
|
|
82
|
-
const minBarHeight = 2;
|
|
99
|
+
ctx.clearRect(0, 0, waveWidth, waveHeight);
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.
|
|
87
|
-
|
|
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
|
-
|
|
106
|
+
ctx.fillRect(x, y, barWidth, Math.max(height * 2, 2));
|
|
107
|
+
});
|
|
95
108
|
}
|
|
96
109
|
|
|
97
|
-
protected drawBricks(
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
|
|
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(
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
159
|
-
const waveWidth =
|
|
160
|
-
const waveHeight =
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
172
|
-
.
|
|
173
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.
|
|
183
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
.
|
|
225
|
-
|
|
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(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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(
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
ctx.fillRect(x, y, barWidth, barHeight * 2);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
287
285
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
.
|
|
321
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
}
|