@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.
- package/dist/EF_FRAMEGEN.d.ts +8 -15
- 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 +17 -13
- 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 +3 -13
- 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 +7 -7
- package/dist/elements/src/elements/EFWaveform.js +262 -149
- package/dist/elements/src/gui/ContextMixin.js +36 -7
- package/dist/elements/src/gui/EFFilmstrip.js +16 -3
- 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 +4 -14
- 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 +7 -8
- package/src/elements/EFWaveform.ts +349 -319
- package/src/gui/ContextMixin.browsertest.ts +28 -2
- package/src/gui/ContextMixin.ts +41 -9
- package/src/gui/EFFilmstrip.ts +16 -3
- 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,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
|
|
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
|
-
|
|
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
|
|
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
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
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
|
|
63
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
77
|
-
const minBarHeight = 2;
|
|
99
|
+
ctx.clearRect(0, 0, waveWidth, waveHeight);
|
|
78
100
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.
|
|
82
|
-
|
|
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
|
-
|
|
106
|
+
ctx.fillRect(x, y, barWidth, Math.max(height * 2, 2));
|
|
107
|
+
});
|
|
90
108
|
}
|
|
91
109
|
|
|
92
|
-
protected drawBricks(
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
.
|
|
118
|
-
|
|
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(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
160
|
-
.
|
|
161
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
.
|
|
178
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
(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(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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(
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
ctx.fillRect(x, y, barWidth, barHeight * 2);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
282
285
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
.
|
|
316
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
}
|