@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,12 +1,13 @@
1
- import { html, css, LitElement } from "lit";
2
1
  import { provide } from "@lit/context";
3
2
  import { Task } from "@lit/task";
4
- import { property, customElement } from "lit/decorators.js";
5
3
  import debug from "debug";
6
- import { EFTemporal, shallowGetTemporalElements, isEFTemporal, timegroupContext } from "./EFTemporal.js";
7
- import { TimegroupController } from "./TimegroupController.js";
4
+ import { html, css, LitElement } from "lit";
5
+ import { property, customElement } from "lit/decorators.js";
8
6
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
7
+ import { isContextMixin } from "../gui/ContextMixin.js";
9
8
  import { deepGetMediaElements } from "./EFMedia.js";
9
+ import { EFTemporal, shallowGetTemporalElements, isEFTemporal, timegroupContext } from "./EFTemporal.js";
10
+ import { TimegroupController } from "./TimegroupController.js";
10
11
  import { durationConverter } from "./durationConverter.js";
11
12
  var __defProp = Object.defineProperty;
12
13
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -138,10 +139,16 @@ let EFTimegroup = class extends EFTemporal(LitElement) {
138
139
  throw new Error(`Invalid time mode: ${this.mode}`);
139
140
  }
140
141
  }
142
+ /**
143
+ * Wait for all media elements to load their initial segments.
144
+ * Ideally we would only need the extracted index json data, but
145
+ * that caused issues with constructing audio data. We had negative durations
146
+ * in calculations and it was not clear why.
147
+ */
141
148
  async waitForMediaDurations() {
142
149
  return await Promise.all(
143
150
  deepGetMediaElements(this).map(
144
- (media) => media.trackFragmentIndexLoader.taskComplete
151
+ (media) => media.initSegmentsLoader.taskComplete
145
152
  )
146
153
  );
147
154
  }
@@ -212,8 +219,28 @@ let EFTimegroup = class extends EFTemporal(LitElement) {
212
219
  }
213
220
  }
214
221
  }
222
+ get contextProvider() {
223
+ let parent = this.parentNode;
224
+ while (parent) {
225
+ if (isContextMixin(parent)) {
226
+ return parent;
227
+ }
228
+ parent = parent.parentNode;
229
+ }
230
+ return null;
231
+ }
232
+ /**
233
+ * Returns true if the timegroup should be wrapped with a workbench.
234
+ *
235
+ * A timegroup should be wrapped with a workbench if it is the root-most timegroup
236
+ * and EF_INTERACTIVE is true.
237
+ *
238
+ * If the timegroup is already wrappedin a context provider like ef-preview,
239
+ * it should NOT be wrapped in a workbench.
240
+ *
241
+ */
215
242
  shouldWrapWithWorkbench() {
216
- return EF_INTERACTIVE && this.closest("ef-timegroup") === this && this.closest("ef-workbench") === null && this.closest("ef-preview") === null;
243
+ return EF_INTERACTIVE && this.closest("ef-timegroup") === this && this.closest("ef-preview") === null && this.closest("ef-workbench") === null && this.closest("test-context") === null;
217
244
  }
218
245
  wrapWithWorkbench() {
219
246
  const workbench = document.createElement("ef-workbench");
@@ -270,10 +297,8 @@ _currentTime = /* @__PURE__ */ new WeakMap();
270
297
  _EFTimegroup_instances = /* @__PURE__ */ new WeakSet();
271
298
  addAudioToContext_fn = async function(audioContext, fromMs, toMs) {
272
299
  await this.waitForMediaDurations();
273
- const durationMs = toMs - fromMs;
274
300
  await Promise.all(
275
301
  deepGetMediaElements(this).map(async (mediaElement) => {
276
- await mediaElement.trackFragmentIndexLoader.taskComplete;
277
302
  const mediaStartsBeforeEnd = mediaElement.startTimeMs <= toMs;
278
303
  const mediaEndsAfterStart = mediaElement.endTimeMs >= fromMs;
279
304
  const mediaOverlaps = mediaStartsBeforeEnd && mediaEndsAfterStart;
@@ -284,15 +309,15 @@ addAudioToContext_fn = async function(audioContext, fromMs, toMs) {
284
309
  if (!audio) {
285
310
  throw new Error("Failed to fetch audio");
286
311
  }
287
- const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
288
- const ctxEndMs = Math.min(durationMs, mediaElement.endTimeMs - fromMs);
289
- const ctxDurationMs = ctxEndMs - ctxStartMs;
290
- const offset = Math.max(0, fromMs - mediaElement.startTimeMs) - audio.startMs;
291
312
  const bufferSource = audioContext.createBufferSource();
292
313
  bufferSource.buffer = await audioContext.decodeAudioData(
293
314
  await audio.blob.arrayBuffer()
294
315
  );
295
316
  bufferSource.connect(audioContext.destination);
317
+ const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
318
+ const ctxEndMs = mediaElement.endTimeMs - fromMs;
319
+ const ctxDurationMs = ctxEndMs - ctxStartMs;
320
+ const offset = Math.max(0, fromMs - mediaElement.startTimeMs) - audio.startMs;
296
321
  bufferSource.start(
297
322
  ctxStartMs / 1e3,
298
323
  offset / 1e3,
@@ -85,10 +85,7 @@ let EFVideo = class extends TWMixin(EFMedia) {
85
85
  });
86
86
  }
87
87
  render() {
88
- return html` <canvas
89
- class="h-full w-full object-fill"
90
- ${ref(this.canvasRef)}
91
- ></canvas>`;
88
+ return html` <canvas ${ref(this.canvasRef)}></canvas>`;
92
89
  }
93
90
  get canvasElement() {
94
91
  return this.canvasRef.value;
@@ -1,6 +1,5 @@
1
1
  import { EFAudio } from "./EFAudio.js";
2
2
  import { Task } from "@lit/task";
3
- import * as d3 from "d3";
4
3
  import { html, css, LitElement } from "lit";
5
4
  import { property, customElement } from "lit/decorators.js";
6
5
  import { createRef, ref } from "lit/directives/ref.js";
@@ -22,15 +21,86 @@ var __decorateClass = (decorators, target, key, kind) => {
22
21
  let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
23
22
  constructor() {
24
23
  super(...arguments);
25
- this.svgRef = createRef();
24
+ this.canvasRef = createRef();
25
+ this.ctx = null;
26
26
  this.mode = "bars";
27
27
  this.color = "currentColor";
28
28
  this.targetSelector = "";
29
+ this.lastFrameTime = 0;
29
30
  this.frameTask = new Task(this, {
30
31
  autoRun: EF_INTERACTIVE,
31
32
  args: () => [this.targetElement.audioBufferTask.status],
32
33
  task: async () => {
34
+ const currentTime = performance.now();
35
+ const timeSinceLastFrame = this.lastFrameTime ? currentTime - this.lastFrameTime : 0;
36
+ console.log(`Time since last frame: ${timeSinceLastFrame.toFixed(2)}ms`);
37
+ this.lastFrameTime = currentTime;
33
38
  await this.targetElement.audioBufferTask.taskComplete;
39
+ this.ctx ||= this.initCanvas();
40
+ const ctx = this.ctx;
41
+ if (!ctx) return;
42
+ if (!this.targetElement.audioBufferTask.value) return;
43
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
44
+ const FRAMES_TO_ANALYZE = 4;
45
+ const FRAME_DURATION_MS = 48e3 / 100;
46
+ const multiFrameData = [];
47
+ for (let i = 0; i < FRAMES_TO_ANALYZE; i++) {
48
+ const frameOffset = i - Math.floor(FRAMES_TO_ANALYZE / 2);
49
+ const audioContext = new OfflineAudioContext(2, 48e3 / 25, 48e3);
50
+ const audioBufferSource = audioContext.createBufferSource();
51
+ audioBufferSource.buffer = this.targetElement.audioBufferTask.value.buffer;
52
+ const analyser = audioContext.createAnalyser();
53
+ analyser.fftSize = 128 / 2;
54
+ analyser.smoothingTimeConstant = 0.1;
55
+ audioBufferSource.connect(analyser);
56
+ const startTime = Math.max(
57
+ 0,
58
+ (this.targetElement.trimAdjustedOwnCurrentTimeMs - this.targetElement.audioBufferTask.value.startOffsetMs) / 1e3 + frameOffset * FRAME_DURATION_MS / 1e3
59
+ );
60
+ audioBufferSource.start(0, startTime, FRAME_DURATION_MS / 1e3);
61
+ await audioContext.startRendering();
62
+ const frameData = new Uint8Array(analyser.frequencyBinCount);
63
+ analyser.getByteFrequencyData(frameData);
64
+ multiFrameData.push(frameData);
65
+ }
66
+ const dataLength = multiFrameData[0]?.length ?? 0;
67
+ const smoothedData = new Uint8Array(dataLength);
68
+ for (let i = 0; i < smoothedData.length; i++) {
69
+ let sum = 0;
70
+ for (const frameData of multiFrameData) {
71
+ sum += frameData[i] ?? 0;
72
+ }
73
+ let avg = sum / FRAMES_TO_ANALYZE;
74
+ avg = (avg / 255) ** 1.2 * 255;
75
+ smoothedData[i] = Math.round(avg);
76
+ }
77
+ switch (this.mode) {
78
+ case "bars":
79
+ this.drawBars(ctx, smoothedData);
80
+ break;
81
+ case "bricks":
82
+ this.drawBricks(ctx, smoothedData);
83
+ break;
84
+ case "curve":
85
+ this.drawCurve(ctx, smoothedData);
86
+ break;
87
+ case "line":
88
+ this.drawLine(ctx, smoothedData);
89
+ break;
90
+ case "pixel":
91
+ this.drawPixel(ctx, smoothedData);
92
+ break;
93
+ case "wave":
94
+ this.drawWave(ctx, smoothedData);
95
+ break;
96
+ case "roundBars":
97
+ this.drawRoundBars(ctx, smoothedData);
98
+ break;
99
+ case "equalizer":
100
+ this.drawEqualizer(ctx, smoothedData);
101
+ break;
102
+ }
103
+ }
34
104
  }
35
105
  });
36
106
  }
@@ -38,7 +108,7 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
38
108
  return this;
39
109
  }
40
110
  render() {
41
- return html` <svg ${ref(this.svgRef)} store></svg> `;
111
+ return html`<canvas ${ref(this.canvasRef)}></canvas>`;
42
112
  }
43
113
  set target(value) {
44
114
  this.targetSelector = value;
@@ -49,153 +119,183 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
49
119
  new CrossUpdateController(this.targetElement, this);
50
120
  }
51
121
  }
52
- drawBars(svg, frequencyData) {
53
- const waveWidth = svg.clientWidth * devicePixelRatio;
54
- const waveHeight = svg.clientHeight;
55
- const waveLeft = 0;
56
- const waveRight = waveWidth;
57
- const barX = d3.scaleBand().paddingInner(0.5).paddingOuter(0.01).domain(d3.range(frequencyData.length).map((n) => String(n))).rangeRound([waveLeft, waveRight]);
58
- const height = d3.scaleLinear().domain([0, 255]).range([0, waveHeight / 2]);
122
+ initCanvas() {
123
+ console.count("initCanvas");
124
+ const canvas = this.canvasRef.value;
125
+ if (!canvas) return null;
126
+ const rect = this.getBoundingClientRect();
127
+ const dpr = window.devicePixelRatio;
128
+ canvas.style.width = `${rect.width}px`;
129
+ canvas.style.height = `${rect.height}px`;
130
+ canvas.width = rect.width * dpr;
131
+ canvas.height = rect.height * dpr;
132
+ const ctx = canvas.getContext("2d");
133
+ if (!ctx) return null;
134
+ ctx.scale(dpr, dpr);
135
+ return ctx;
136
+ }
137
+ drawBars(ctx, frequencyData) {
138
+ ctx.strokeStyle = this.color;
139
+ ctx.fillStyle = this.color;
140
+ const canvas = ctx.canvas;
141
+ const waveWidth = canvas.width / devicePixelRatio;
142
+ const waveHeight = canvas.height / devicePixelRatio;
143
+ const barWidth = waveWidth / frequencyData.length * 0.8;
59
144
  const baseline = waveHeight / 2;
60
- const bars = d3.select(svg).selectAll("rect").data(frequencyData);
61
- const minBarHeight = 2;
62
- bars.enter().append("rect").merge(bars).attr("x", (_, i) => barX(String(i)) || 0).attr("y", (value) => baseline - height(value)).attr("width", barX.bandwidth() / 1.2).attr("height", (value) => Math.max(height(value) * 2, minBarHeight));
63
- bars.exit().remove();
64
- }
65
- drawBricks(svg, frequencyData) {
66
- const waveWidth = svg.clientWidth * devicePixelRatio;
67
- const waveHeight = svg.clientHeight * devicePixelRatio;
145
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
146
+ frequencyData.forEach((value, i) => {
147
+ const height = value / 255 * (waveHeight / 2);
148
+ const x = i * (waveWidth / frequencyData.length);
149
+ const y = baseline - height;
150
+ ctx.fillRect(x, y, barWidth, Math.max(height * 2, 2));
151
+ });
152
+ }
153
+ drawBricks(ctx, frequencyData) {
154
+ ctx.strokeStyle = this.color;
155
+ ctx.fillStyle = this.color;
156
+ const canvas = ctx.canvas;
157
+ const waveWidth = canvas.width / devicePixelRatio;
158
+ const waveHeight = canvas.height / devicePixelRatio;
68
159
  const brickWidth = waveWidth / frequencyData.length;
69
160
  const brickHeightFactor = waveHeight / 255 / 2;
70
161
  const brickPadding = 2;
71
162
  const midHeight = waveHeight / 2;
72
- d3.select(svg).selectAll("line.brickBaseLine").data([0]).join("line").attr("class", "brickBaseLine").attr("x1", 0).attr("x2", waveWidth).attr("y1", midHeight).attr("y2", midHeight).attr("stroke", "currentColor").attr("stroke-width", 4).attr("stroke-dasharray", "2");
73
- d3.select(svg).selectAll("rect.brick").data(frequencyData).join("rect").attr("class", "brick").attr("x", (_d, i) => i * brickWidth).attr("y", (d) => midHeight - d * brickHeightFactor).attr("width", brickWidth - brickPadding).attr("height", (d) => d * brickHeightFactor * 2);
74
- }
75
- drawLine(svg, frequencyData) {
76
- const waveWidth = svg.clientWidth * devicePixelRatio;
77
- const waveHeight = svg.clientHeight * devicePixelRatio;
78
- const xScale = d3.scaleLinear().domain([0, frequencyData.length - 1]).range([0, waveWidth]);
79
- const yScale = d3.scaleLinear().domain([0, 255]).range([waveHeight, 0]);
80
- const lineGenerator = d3.line().x((_, i) => xScale(i)).y((d) => yScale(d));
81
- const pathData = lineGenerator(frequencyData);
82
- d3.select(svg).selectAll("path.line").data([frequencyData]).join("path").attr("class", "line").attr("d", pathData).attr("fill", "none").attr("stroke", "currentColor").attr("stroke-width", 4);
83
- }
84
- drawRoundBars(svg, frequencyData) {
85
- const waveWidth = svg.clientWidth * devicePixelRatio;
86
- const waveHeight = svg.clientHeight;
87
- const waveLeft = 0;
88
- const waveRight = waveWidth;
89
- const barX = d3.scaleBand().paddingInner(0.5).paddingOuter(0.01).domain(d3.range(frequencyData.length).map((n) => String(n))).rangeRound([waveLeft, waveRight]);
90
- const height = d3.scaleLinear().domain([0, 255]).range([0, waveHeight / 2]);
163
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
164
+ ctx.beginPath();
165
+ ctx.setLineDash([2]);
166
+ ctx.lineWidth = 4;
167
+ ctx.moveTo(0, midHeight);
168
+ ctx.lineTo(waveWidth, midHeight);
169
+ ctx.stroke();
170
+ ctx.setLineDash([]);
171
+ frequencyData.forEach((value, i) => {
172
+ const x = i * brickWidth;
173
+ const height = value * brickHeightFactor * 2;
174
+ const y = midHeight - height / 2;
175
+ ctx.fillRect(x, y, brickWidth - brickPadding, height);
176
+ });
177
+ }
178
+ drawLine(ctx, frequencyData) {
179
+ ctx.strokeStyle = this.color;
180
+ ctx.fillStyle = this.color;
181
+ const canvas = ctx.canvas;
182
+ const waveWidth = canvas.width / devicePixelRatio;
183
+ const waveHeight = canvas.height / devicePixelRatio;
184
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
185
+ ctx.beginPath();
186
+ frequencyData.forEach((value, i) => {
187
+ const x = i / frequencyData.length * waveWidth;
188
+ const y = (1 - value / 255) * waveHeight;
189
+ if (i === 0) {
190
+ ctx.moveTo(x, y);
191
+ } else {
192
+ ctx.lineTo(x, y);
193
+ }
194
+ });
195
+ ctx.lineWidth = 4;
196
+ ctx.stroke();
197
+ }
198
+ drawRoundBars(ctx, frequencyData) {
199
+ ctx.strokeStyle = this.color;
200
+ ctx.fillStyle = this.color;
201
+ const canvas = ctx.canvas;
202
+ const waveWidth = canvas.width / devicePixelRatio;
203
+ const waveHeight = canvas.height / devicePixelRatio;
204
+ const barWidth = waveWidth / frequencyData.length * 0.5;
91
205
  const baseline = waveHeight / 2;
92
- const bars = d3.select(svg).selectAll("rect").data(frequencyData);
93
- const minBarHeight = 2;
94
- bars.enter().append("rect").merge(bars).attr("x", (_, i) => barX(String(i)) || 0).attr("y", (value) => baseline - height(value)).attr("width", barX.bandwidth() / 1.2).attr("height", (value) => Math.max(height(value) * 2, minBarHeight)).attr("rx", barX.bandwidth()).attr("ry", barX.bandwidth());
95
- bars.exit().remove();
96
- }
97
- drawEqualizer(svg, frequencyData) {
98
- const waveWidth = svg.clientWidth * devicePixelRatio;
99
- const waveHeight = svg.clientHeight * devicePixelRatio;
100
- const barWidth = waveWidth / frequencyData.length;
101
- const barPadding = 1;
102
- const minHeight = 1;
103
- const heightScale = d3.scaleLinear().domain([0, 255]).range([0, waveHeight / 2]);
104
- d3.select(svg).selectAll("line.equalizerBaseLine").data([0]).join("line").attr("class", "equalizerBaseLine").attr("x1", 0).attr("x2", waveWidth).attr("y1", waveHeight / 2).attr("y2", waveHeight / 2).attr("stroke-width", 2);
105
- d3.select(svg).selectAll("rect.equalizerBar").data(frequencyData).join("rect").attr("class", "equalizerBar").attr("x", (_d, i) => i * barWidth + barPadding / 2).attr(
106
- "y",
107
- (d) => waveHeight / 2 - Math.max(heightScale(d) / 2, minHeight)
108
- ).attr("width", barWidth - barPadding).attr("height", (d) => Math.max(heightScale(d), minHeight));
109
- d3.select(svg).selectAll("rect.equalizerBar").transition().duration(100).attr(
110
- "y",
111
- (d) => waveHeight / 2 - Math.max(heightScale(Number(d)) / 2, minHeight)
112
- ).attr("height", (d) => Math.max(heightScale(Number(d)), minHeight));
113
- }
114
- drawCurve(svg, frequencyData) {
115
- const waveWidth = svg.clientWidth * devicePixelRatio;
116
- const waveHeight = svg.clientHeight * devicePixelRatio;
117
- const xScale = d3.scaleLinear().domain([0, frequencyData.length]).range([0, waveWidth]);
118
- const yScale = d3.scaleLinear().domain([0, 255]).range([waveHeight, 0]);
119
- const curveGenerator = d3.line().x((_, i) => xScale(i)).y((d) => yScale(d)).curve(d3.curveNatural);
120
- const pathData = curveGenerator(frequencyData);
121
- d3.select(svg).selectAll("path.curve").data([frequencyData]).join("path").attr("class", "curve").attr("d", pathData).attr("fill", "none").attr("stroke", "currentColor").attr("stroke-width", 4);
122
- }
123
- drawPixel(svg, frequencyData) {
124
- const waveWidth = svg.clientWidth * devicePixelRatio;
125
- const waveHeight = svg.clientHeight;
206
+ const maxHeight = waveHeight / 2;
207
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
208
+ frequencyData.forEach((value, i) => {
209
+ const height = value / 255 * maxHeight;
210
+ const x = i * (waveWidth / frequencyData.length);
211
+ const y = baseline - height;
212
+ const radius = barWidth / 2;
213
+ ctx.beginPath();
214
+ ctx.roundRect(x, y, barWidth, Math.max(height * 2, 2), radius);
215
+ ctx.fill();
216
+ });
217
+ }
218
+ drawEqualizer(ctx, frequencyData) {
219
+ ctx.strokeStyle = this.color;
220
+ ctx.fillStyle = this.color;
221
+ const canvas = ctx.canvas;
222
+ const waveWidth = canvas.width / devicePixelRatio;
223
+ const waveHeight = canvas.height / devicePixelRatio;
224
+ const barWidth = waveWidth / frequencyData.length * 0.8;
126
225
  const baseline = waveHeight / 2;
127
- const barX = d3.scaleBand().domain(d3.range(frequencyData.length).map(String)).rangeRound([0, waveWidth]).paddingInner(0.03).paddingOuter(0.02);
128
- const height = d3.scaleLinear().domain([0, 255]).range([0, baseline]);
129
- const bars = d3.select(svg).selectAll("rect").data(frequencyData);
130
- bars.enter().append("rect").merge(bars).attr("x", (_, i) => barX(String(i)) || 0).attr("y", (value) => baseline - height(value)).attr("width", barX.bandwidth()).attr("height", (value) => height(value) * 2);
131
- bars.exit().remove();
132
- }
133
- drawWave(svg, frequencyData) {
134
- const waveWidth = svg.clientWidth * devicePixelRatio;
135
- const waveHeight = svg.clientHeight;
136
- const barX = d3.scaleBand().domain(d3.range(frequencyData.length).map(String)).rangeRound([0, waveWidth]).paddingInner(0.03).paddingOuter(0.02);
137
- const height = d3.scaleLinear().domain([0, 255]).range([0, waveHeight / 2]);
138
- d3.select(svg).selectAll("line.baseline").data([0]).join("line").attr("class", "baseline").attr("x1", (_, i) => barX(String(i)) || 0).attr("x2", waveWidth).attr("y1", waveHeight / 2).attr("y2", waveHeight / 2).attr("stroke", "currentColor").attr("stroke-width", 2);
139
- const bars = d3.select(svg).selectAll("rect").data(frequencyData);
140
- bars.enter().append("rect").merge(bars).attr("x", (_, i) => barX(String(i)) || 0).attr("y", (value) => waveHeight / 2 - height(value)).attr("width", barX.bandwidth()).attr("height", (value) => height(value) * 2);
141
- bars.exit().remove();
142
- }
143
- async updated() {
144
- const svg = this.svgRef.value;
145
- if (!svg) {
146
- return;
147
- }
148
- if (!this.targetElement.audioBufferTask.value) {
149
- return;
150
- }
151
- if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
152
- const audioContext = new OfflineAudioContext(2, 48e3 / 25, 48e3);
153
- const audioBufferSource = audioContext.createBufferSource();
154
- audioBufferSource.buffer = this.targetElement.audioBufferTask.value.buffer;
155
- const analyser = audioContext.createAnalyser();
156
- analyser.fftSize = 256;
157
- audioBufferSource.connect(analyser);
158
- audioBufferSource.start(
159
- 0,
160
- Math.max(
161
- 0,
162
- (this.targetElement.trimAdjustedOwnCurrentTimeMs - this.targetElement.audioBufferTask.value.startOffsetMs) / 1e3
163
- ),
164
- 48e3 / 1e3
165
- );
166
- await audioContext.startRendering();
167
- const frequencyData = new Uint8Array(analyser.frequencyBinCount);
168
- analyser.getByteFrequencyData(frequencyData);
169
- const rect = this.getBoundingClientRect();
170
- svg.setAttribute("width", (rect.width * devicePixelRatio).toString());
171
- svg.setAttribute("height", (rect.height * devicePixelRatio).toString());
172
- switch (this.mode) {
173
- case "bars":
174
- this.drawBars(svg, frequencyData);
175
- break;
176
- case "bricks":
177
- this.drawBricks(svg, frequencyData);
178
- break;
179
- case "curve":
180
- this.drawCurve(svg, frequencyData);
181
- break;
182
- case "line":
183
- this.drawLine(svg, frequencyData);
184
- break;
185
- case "pixel":
186
- this.drawPixel(svg, frequencyData);
187
- break;
188
- case "wave":
189
- this.drawWave(svg, frequencyData);
190
- break;
191
- case "roundBars":
192
- this.drawRoundBars(svg, frequencyData);
193
- break;
194
- case "equalizer":
195
- this.drawEqualizer(svg, frequencyData);
196
- break;
226
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
227
+ ctx.beginPath();
228
+ ctx.lineWidth = 2;
229
+ ctx.moveTo(0, baseline);
230
+ ctx.lineTo(waveWidth, baseline);
231
+ ctx.stroke();
232
+ frequencyData.forEach((value, i) => {
233
+ const height = value / 255 * (waveHeight / 2);
234
+ const x = i * (waveWidth / frequencyData.length);
235
+ const y = baseline - height;
236
+ ctx.fillRect(x, y, barWidth, Math.max(height * 2, 1));
237
+ });
238
+ }
239
+ drawCurve(ctx, frequencyData) {
240
+ ctx.strokeStyle = this.color;
241
+ ctx.fillStyle = this.color;
242
+ const canvas = ctx.canvas;
243
+ const waveWidth = canvas.width / devicePixelRatio;
244
+ const waveHeight = canvas.height / devicePixelRatio;
245
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
246
+ ctx.beginPath();
247
+ frequencyData.forEach((value, i) => {
248
+ const x = i / frequencyData.length * waveWidth;
249
+ const y = (1 - value / 255) * waveHeight;
250
+ if (i === 0) {
251
+ ctx.moveTo(x, y);
252
+ } else {
253
+ ctx.lineTo(x, y);
197
254
  }
198
- }
255
+ });
256
+ ctx.lineWidth = 4;
257
+ ctx.stroke();
258
+ }
259
+ drawPixel(ctx, frequencyData) {
260
+ ctx.strokeStyle = this.color;
261
+ ctx.fillStyle = this.color;
262
+ const canvas = ctx.canvas;
263
+ const waveWidth = canvas.width / devicePixelRatio;
264
+ const waveHeight = canvas.height / devicePixelRatio;
265
+ const baseline = waveHeight / 2;
266
+ const barWidth = waveWidth / frequencyData.length * 0.97;
267
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
268
+ frequencyData.forEach((value, i) => {
269
+ const x = i * (waveWidth / frequencyData.length);
270
+ const barHeight = value / 255 * baseline;
271
+ const y = baseline - barHeight;
272
+ ctx.fillRect(x, y, barWidth, barHeight * 2);
273
+ });
274
+ }
275
+ drawWave(ctx, frequencyData) {
276
+ ctx.strokeStyle = this.color;
277
+ ctx.fillStyle = this.color;
278
+ const canvas = ctx.canvas;
279
+ const waveWidth = canvas.width / devicePixelRatio;
280
+ const waveHeight = canvas.height / devicePixelRatio;
281
+ const baseline = waveHeight / 2;
282
+ const barWidth = waveWidth / frequencyData.length * 0.97;
283
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
284
+ ctx.beginPath();
285
+ ctx.moveTo(0, baseline);
286
+ ctx.lineTo(waveWidth, baseline);
287
+ ctx.strokeStyle = this.color;
288
+ ctx.lineWidth = 2;
289
+ ctx.stroke();
290
+ frequencyData.forEach((value, i) => {
291
+ const x = i * (waveWidth / frequencyData.length);
292
+ const barHeight = value / 255 * (waveHeight / 2);
293
+ const y = baseline - barHeight;
294
+ ctx.fillRect(x, y, barWidth, barHeight * 2);
295
+ });
296
+ }
297
+ get durationMs() {
298
+ return this.targetElement.durationMs;
199
299
  }
200
300
  get targetElement() {
201
301
  const target = document.getElementById(this.targetSelector ?? "");
@@ -204,10 +304,17 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
204
304
  }
205
305
  throw new Error("Invalid target, must be an EFAudio or EFVideo element");
206
306
  }
307
+ updated(changedProperties) {
308
+ super.updated(changedProperties);
309
+ if (changedProperties.has("color") || changedProperties.has("mode")) {
310
+ this.frameTask.run();
311
+ }
312
+ }
207
313
  };
208
314
  EFWaveform.styles = [
209
315
  css`
210
- svg {
316
+ :host {
317
+ display: block;
211
318
  all: inherit;
212
319
  }
213
320
  `