@editframe/elements 0.15.0-beta.1 → 0.15.0-beta.3
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.js +0 -2
- package/dist/elements/EFAudio.d.ts +0 -1
- package/dist/elements/EFAudio.js +1 -5
- package/dist/elements/EFCaptions.js +1 -1
- package/dist/elements/EFMedia.d.ts +1 -1
- package/dist/elements/EFMedia.js +37 -14
- package/dist/elements/EFTemporal.d.ts +3 -3
- package/dist/elements/EFTemporal.js +6 -2
- package/dist/elements/EFTimegroup.d.ts +1 -5
- package/dist/elements/EFTimegroup.js +4 -5
- package/dist/elements/EFWaveform.d.ts +13 -6
- package/dist/elements/EFWaveform.js +122 -50
- package/dist/elements/TargetController.d.ts +25 -0
- package/dist/elements/TargetController.js +164 -0
- package/dist/elements/TargetController.test.d.ts +19 -0
- package/dist/gui/EFPreview.d.ts +1 -1
- package/dist/gui/EFPreview.js +1 -0
- package/package.json +2 -2
- package/src/elements/EFAudio.ts +1 -4
- package/src/elements/EFCaptions.ts +1 -1
- package/src/elements/EFMedia.ts +41 -22
- package/src/elements/EFTemporal.ts +10 -10
- package/src/elements/EFTimegroup.ts +4 -9
- package/src/elements/EFWaveform.ts +161 -66
- package/src/elements/TargetController.test.ts +229 -0
- package/src/elements/TargetController.ts +219 -0
- package/src/gui/EFPreview.ts +10 -9
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { EFAudio } from "./EFAudio.js";
|
|
2
|
-
|
|
3
1
|
import { CSSStyleObserver } from "@bramus/style-observer";
|
|
4
2
|
import { Task } from "@lit/task";
|
|
5
3
|
import { LitElement, type PropertyValueMap, css, html } from "lit";
|
|
6
|
-
import { customElement, property } from "lit/decorators.js";
|
|
4
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
7
5
|
import { type Ref, createRef, ref } from "lit/directives/ref.js";
|
|
8
6
|
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
|
|
9
7
|
import { EF_RENDERING } from "../EF_RENDERING.js";
|
|
10
8
|
import { TWMixin } from "../gui/TWMixin.js";
|
|
11
9
|
import { CrossUpdateController } from "./CrossUpdateController.js";
|
|
10
|
+
import type { EFAudio } from "./EFAudio.js";
|
|
12
11
|
import { EFTemporal } from "./EFTemporal.js";
|
|
13
|
-
import { EFVideo } from "./EFVideo.js";
|
|
12
|
+
import type { EFVideo } from "./EFVideo.js";
|
|
13
|
+
import { TargetController } from "./TargetController.ts";
|
|
14
14
|
|
|
15
15
|
@customElement("ef-waveform")
|
|
16
16
|
export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
@@ -45,20 +45,22 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
45
45
|
type: String,
|
|
46
46
|
attribute: "mode",
|
|
47
47
|
})
|
|
48
|
-
mode: "roundBars" | "bars" | "bricks" | "line" | "pixel" | "wave"
|
|
48
|
+
mode: "roundBars" | "bars" | "bricks" | "line" | "pixel" | "wave" | "spikes" =
|
|
49
|
+
"bars";
|
|
49
50
|
|
|
50
51
|
@property({ type: String })
|
|
51
52
|
color = "currentColor";
|
|
52
53
|
|
|
53
|
-
@property({ type: String,
|
|
54
|
-
|
|
54
|
+
@property({ type: String, reflect: true })
|
|
55
|
+
target = "";
|
|
56
|
+
|
|
57
|
+
@state()
|
|
58
|
+
targetElement: EFAudio | EFVideo | null = null;
|
|
55
59
|
|
|
56
60
|
@property({ type: Number, attribute: "line-width" })
|
|
57
61
|
lineWidth = 4;
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
this.targetSelector = value;
|
|
61
|
-
}
|
|
63
|
+
targetController: TargetController = new TargetController(this);
|
|
62
64
|
|
|
63
65
|
connectedCallback() {
|
|
64
66
|
super.connectedCallback();
|
|
@@ -136,8 +138,6 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
136
138
|
ctx.reset();
|
|
137
139
|
|
|
138
140
|
// Scale all drawing operations by dpr
|
|
139
|
-
ctx.scale(dpr, dpr);
|
|
140
|
-
|
|
141
141
|
return ctx;
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -145,31 +145,25 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
145
145
|
const canvas = ctx.canvas;
|
|
146
146
|
const waveWidth = canvas.width;
|
|
147
147
|
const waveHeight = canvas.height;
|
|
148
|
-
const baseline = waveHeight / 4;
|
|
149
148
|
|
|
150
|
-
// Calculate bar width with padding
|
|
151
149
|
const totalBars = frequencyData.length;
|
|
152
|
-
const paddingInner = 0.5;
|
|
153
|
-
const paddingOuter = 0.01;
|
|
154
|
-
const availableWidth = waveWidth
|
|
150
|
+
const paddingInner = 0.5;
|
|
151
|
+
const paddingOuter = 0.01;
|
|
152
|
+
const availableWidth = waveWidth;
|
|
155
153
|
const barWidth =
|
|
156
154
|
availableWidth / (totalBars + (totalBars - 1) * paddingInner);
|
|
157
155
|
|
|
158
156
|
ctx.clearRect(0, 0, waveWidth, waveHeight);
|
|
159
|
-
|
|
160
|
-
// Create a single Path2D object for all bars
|
|
161
157
|
const path = new Path2D();
|
|
162
158
|
|
|
163
159
|
frequencyData.forEach((value, i) => {
|
|
164
|
-
const normalizedValue = value / 255;
|
|
165
|
-
const
|
|
160
|
+
const normalizedValue = Math.min((value / 255) * 2, 1);
|
|
161
|
+
const barHeight = normalizedValue * waveHeight;
|
|
162
|
+
const y = (waveHeight - barHeight) / 2;
|
|
166
163
|
const x = waveWidth * paddingOuter + i * (barWidth * (1 + paddingInner));
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
path.rect(x, y, barWidth, height * 2);
|
|
164
|
+
path.rect(x, y, barWidth, barHeight);
|
|
170
165
|
});
|
|
171
166
|
|
|
172
|
-
// Single fill operation for all bars
|
|
173
167
|
ctx.fill(path);
|
|
174
168
|
}
|
|
175
169
|
|
|
@@ -185,11 +179,16 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
185
179
|
|
|
186
180
|
const columnWidth = waveWidth / frequencyData.length;
|
|
187
181
|
const boxSize = columnWidth * 0.9;
|
|
182
|
+
const verticalGap = boxSize * 0.2; // Add spacing between bricks
|
|
183
|
+
const maxBricks = Math.floor(waveHeight / (boxSize + verticalGap)); // Account for gaps in height calculation
|
|
184
|
+
|
|
188
185
|
frequencyData.forEach((value, i) => {
|
|
189
|
-
const
|
|
190
|
-
|
|
186
|
+
const normalizedValue = Math.min((value / 255) * 2, 1);
|
|
187
|
+
const brickCount = Math.floor(normalizedValue * maxBricks);
|
|
188
|
+
|
|
189
|
+
for (let j = 0; j < brickCount; j++) {
|
|
191
190
|
const x = columnWidth * i;
|
|
192
|
-
const y = waveHeight - (j *
|
|
191
|
+
const y = waveHeight - (j + 1) * (boxSize + verticalGap); // Include gap in position calculation
|
|
193
192
|
path.rect(x, y, boxSize, boxSize);
|
|
194
193
|
}
|
|
195
194
|
});
|
|
@@ -204,13 +203,12 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
204
203
|
const canvas = ctx.canvas;
|
|
205
204
|
const waveWidth = canvas.width;
|
|
206
205
|
const waveHeight = canvas.height;
|
|
207
|
-
const baseline = waveHeight / 4;
|
|
208
206
|
|
|
209
207
|
// Similar padding calculation as drawBars
|
|
210
208
|
const totalBars = frequencyData.length;
|
|
211
209
|
const paddingInner = 0.5;
|
|
212
210
|
const paddingOuter = 0.01;
|
|
213
|
-
const availableWidth = waveWidth
|
|
211
|
+
const availableWidth = waveWidth;
|
|
214
212
|
const barWidth =
|
|
215
213
|
availableWidth / (totalBars + (totalBars - 1) * paddingInner);
|
|
216
214
|
|
|
@@ -220,13 +218,13 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
220
218
|
const path = new Path2D();
|
|
221
219
|
|
|
222
220
|
frequencyData.forEach((value, i) => {
|
|
223
|
-
const normalizedValue = value / 255;
|
|
224
|
-
const height = normalizedValue *
|
|
221
|
+
const normalizedValue = Math.min((value / 255) * 2, 1);
|
|
222
|
+
const height = normalizedValue * waveHeight; // Use full wave height like in drawBars
|
|
225
223
|
const x = waveWidth * paddingOuter + i * (barWidth * (1 + paddingInner));
|
|
226
|
-
const y =
|
|
224
|
+
const y = (waveHeight - height) / 2; // Center vertically
|
|
227
225
|
|
|
228
226
|
// Add rounded rectangle to path
|
|
229
|
-
path.roundRect(x, y, barWidth, height
|
|
227
|
+
path.roundRect(x, y, barWidth, height, barWidth / 2);
|
|
230
228
|
});
|
|
231
229
|
|
|
232
230
|
// Single fill operation for all bars
|
|
@@ -239,9 +237,9 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
239
237
|
) {
|
|
240
238
|
const canvas = ctx.canvas;
|
|
241
239
|
const waveWidth = canvas.width;
|
|
242
|
-
const waveHeight = canvas.height
|
|
243
|
-
const barWidth = (waveWidth / frequencyData.length) * 0.8;
|
|
240
|
+
const waveHeight = canvas.height;
|
|
244
241
|
const baseline = waveHeight / 2;
|
|
242
|
+
const barWidth = (waveWidth / frequencyData.length) * 0.8;
|
|
245
243
|
|
|
246
244
|
ctx.clearRect(0, 0, waveWidth, waveHeight);
|
|
247
245
|
|
|
@@ -272,7 +270,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
272
270
|
protected drawLine(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array) {
|
|
273
271
|
const canvas = ctx.canvas;
|
|
274
272
|
const waveWidth = canvas.width;
|
|
275
|
-
const waveHeight = canvas.height
|
|
273
|
+
const waveHeight = canvas.height;
|
|
276
274
|
|
|
277
275
|
ctx.clearRect(0, 0, waveWidth, waveHeight);
|
|
278
276
|
|
|
@@ -300,24 +298,21 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
300
298
|
) {
|
|
301
299
|
const canvas = ctx.canvas;
|
|
302
300
|
const waveWidth = canvas.width;
|
|
303
|
-
const waveHeight = canvas.height
|
|
301
|
+
const waveHeight = canvas.height;
|
|
304
302
|
const baseline = waveHeight / 2;
|
|
305
303
|
const barWidth = waveWidth / frequencyData.length;
|
|
306
304
|
|
|
307
305
|
ctx.clearRect(0, 0, waveWidth, waveHeight);
|
|
308
|
-
|
|
309
|
-
// Create a single Path2D object for all pixels
|
|
310
306
|
const path = new Path2D();
|
|
311
307
|
|
|
312
308
|
frequencyData.forEach((value, i) => {
|
|
309
|
+
const normalizedValue = Math.min((value / 255) * 2, 1); // Updated normalization
|
|
313
310
|
const x = i * (waveWidth / frequencyData.length);
|
|
314
|
-
const barHeight = (
|
|
311
|
+
const barHeight = normalizedValue * (waveHeight / 2); // Half height since we extend both ways
|
|
315
312
|
const y = baseline - barHeight;
|
|
316
|
-
|
|
317
|
-
path.rect(x, y, barWidth, barHeight * 2);
|
|
313
|
+
path.rect(x, y, barWidth, barHeight * 2); // Double height to extend both ways
|
|
318
314
|
});
|
|
319
315
|
|
|
320
|
-
// Single fill operation for all pixels
|
|
321
316
|
ctx.fill(path);
|
|
322
317
|
}
|
|
323
318
|
|
|
@@ -325,54 +320,147 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
325
320
|
const canvas = ctx.canvas;
|
|
326
321
|
const waveWidth = canvas.width;
|
|
327
322
|
const waveHeight = canvas.height;
|
|
328
|
-
const
|
|
323
|
+
const paddingOuter = 0.01;
|
|
324
|
+
const availableWidth = waveWidth * (1 - 2 * paddingOuter);
|
|
325
|
+
const startX = waveWidth * paddingOuter;
|
|
326
|
+
|
|
327
|
+
ctx.clearRect(0, 0, waveWidth, waveHeight);
|
|
328
|
+
const path = new Path2D();
|
|
329
|
+
|
|
330
|
+
// Draw top curve
|
|
331
|
+
const firstValue = Math.min(((frequencyData[0] ?? 0) / 255) * 2, 1);
|
|
332
|
+
const firstY = (waveHeight - firstValue * waveHeight) / 2;
|
|
333
|
+
path.moveTo(startX, firstY);
|
|
334
|
+
|
|
335
|
+
// Draw top half
|
|
336
|
+
frequencyData.forEach((value, i) => {
|
|
337
|
+
const normalizedValue = Math.min((value / 255) * 2, 1);
|
|
338
|
+
const x = startX + (i / (frequencyData.length - 1)) * availableWidth;
|
|
339
|
+
const barHeight = normalizedValue * waveHeight;
|
|
340
|
+
const y = (waveHeight - barHeight) / 2;
|
|
341
|
+
|
|
342
|
+
if (i === 0) {
|
|
343
|
+
path.moveTo(x, y);
|
|
344
|
+
} else {
|
|
345
|
+
const prevX =
|
|
346
|
+
startX + ((i - 1) / (frequencyData.length - 1)) * availableWidth;
|
|
347
|
+
const prevValue = Math.min(((frequencyData[i - 1] ?? 0) / 255) * 2, 1);
|
|
348
|
+
const prevBarHeight = prevValue * waveHeight;
|
|
349
|
+
const prevY = (waveHeight - prevBarHeight) / 2;
|
|
350
|
+
const xc = (prevX + x) / 2;
|
|
351
|
+
const yc = (prevY + y) / 2;
|
|
352
|
+
path.quadraticCurveTo(prevX, prevY, xc, yc);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Draw bottom half
|
|
357
|
+
for (let i = frequencyData.length - 1; i >= 0; i--) {
|
|
358
|
+
const normalizedValue = Math.min(((frequencyData[i] ?? 0) / 255) * 2, 1);
|
|
359
|
+
const x = startX + (i / (frequencyData.length - 1)) * availableWidth;
|
|
360
|
+
const barHeight = normalizedValue * waveHeight;
|
|
361
|
+
const y = (waveHeight + barHeight) / 2;
|
|
362
|
+
|
|
363
|
+
if (i === frequencyData.length - 1) {
|
|
364
|
+
path.lineTo(x, y);
|
|
365
|
+
} else {
|
|
366
|
+
const nextX =
|
|
367
|
+
startX + ((i + 1) / (frequencyData.length - 1)) * availableWidth;
|
|
368
|
+
const nextValue = Math.min(((frequencyData[i + 1] ?? 0) / 255) * 2, 1);
|
|
369
|
+
const nextBarHeight = nextValue * waveHeight;
|
|
370
|
+
const nextY = (waveHeight + nextBarHeight) / 2;
|
|
371
|
+
const xc = (nextX + x) / 2;
|
|
372
|
+
const yc = (nextY + y) / 2;
|
|
373
|
+
path.quadraticCurveTo(nextX, nextY, xc, yc);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Close the path with a smooth curve back to start
|
|
378
|
+
const lastY = (waveHeight + firstValue * waveHeight) / 2;
|
|
379
|
+
const controlX = startX;
|
|
380
|
+
const controlY = (lastY + firstY) / 2;
|
|
381
|
+
path.quadraticCurveTo(controlX, controlY, startX, firstY);
|
|
382
|
+
|
|
383
|
+
ctx.fill(path);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
protected drawSpikes(
|
|
387
|
+
ctx: CanvasRenderingContext2D,
|
|
388
|
+
frequencyData: Uint8Array,
|
|
389
|
+
) {
|
|
390
|
+
const canvas = ctx.canvas;
|
|
391
|
+
const waveWidth = canvas.width;
|
|
392
|
+
const waveHeight = canvas.height;
|
|
393
|
+
const paddingOuter = 0.01;
|
|
394
|
+
const availableWidth = waveWidth * (1 - 2 * paddingOuter);
|
|
395
|
+
const startX = waveWidth * paddingOuter;
|
|
329
396
|
|
|
330
397
|
ctx.clearRect(0, 0, waveWidth, waveHeight);
|
|
331
398
|
const path = new Path2D();
|
|
332
|
-
path.moveTo(0, baseline);
|
|
333
399
|
|
|
334
400
|
// Draw top curve
|
|
401
|
+
const firstValue = (frequencyData[0] ?? 0) / 255;
|
|
402
|
+
const firstY = (waveHeight - firstValue * waveHeight) / 2;
|
|
403
|
+
path.moveTo(startX, firstY);
|
|
404
|
+
|
|
405
|
+
// Draw top half
|
|
335
406
|
frequencyData.forEach((value, i) => {
|
|
336
|
-
const normalizedValue = value / 255;
|
|
337
|
-
const x = (i / (frequencyData.length - 1)) *
|
|
338
|
-
const
|
|
407
|
+
const normalizedValue = Math.min((value / 255) * 2, 1);
|
|
408
|
+
const x = startX + (i / (frequencyData.length - 1)) * availableWidth;
|
|
409
|
+
const barHeight = normalizedValue * (waveHeight / 2);
|
|
410
|
+
const y = (waveHeight - barHeight * 2) / 2;
|
|
339
411
|
|
|
340
412
|
if (i === 0) {
|
|
341
413
|
path.moveTo(x, y);
|
|
342
414
|
} else {
|
|
343
|
-
const prevX =
|
|
415
|
+
const prevX =
|
|
416
|
+
startX + ((i - 1) / (frequencyData.length - 1)) * availableWidth;
|
|
344
417
|
const prevValue = (frequencyData[i - 1] ?? 0) / 255;
|
|
345
|
-
const
|
|
418
|
+
const prevBarHeight = prevValue * (waveHeight / 2);
|
|
419
|
+
const prevY = (waveHeight - prevBarHeight * 2) / 2;
|
|
346
420
|
const xc = (prevX + x) / 2;
|
|
347
421
|
const yc = (prevY + y) / 2;
|
|
348
422
|
path.quadraticCurveTo(prevX, prevY, xc, yc);
|
|
349
423
|
}
|
|
350
424
|
});
|
|
351
425
|
|
|
352
|
-
// Draw bottom
|
|
426
|
+
// Draw bottom half
|
|
353
427
|
for (let i = frequencyData.length - 1; i >= 0; i--) {
|
|
354
|
-
const normalizedValue = (frequencyData[i] ?? 0) / 255;
|
|
355
|
-
const x = (i / (frequencyData.length - 1)) *
|
|
356
|
-
const
|
|
428
|
+
const normalizedValue = Math.min(((frequencyData[i] ?? 0) / 255) * 2, 1);
|
|
429
|
+
const x = startX + (i / (frequencyData.length - 1)) * availableWidth;
|
|
430
|
+
const barHeight = normalizedValue * (waveHeight / 2);
|
|
431
|
+
const y = (waveHeight + barHeight * 2) / 2;
|
|
357
432
|
|
|
358
433
|
if (i === frequencyData.length - 1) {
|
|
359
434
|
path.lineTo(x, y);
|
|
360
435
|
} else {
|
|
361
|
-
const nextX =
|
|
436
|
+
const nextX =
|
|
437
|
+
startX + ((i + 1) / (frequencyData.length - 1)) * availableWidth;
|
|
362
438
|
const nextValue = (frequencyData[i + 1] ?? 0) / 255;
|
|
363
|
-
const
|
|
439
|
+
const nextBarHeight = nextValue * (waveHeight / 2);
|
|
440
|
+
const nextY = (waveHeight + nextBarHeight * 2) / 2;
|
|
364
441
|
const xc = (nextX + x) / 2;
|
|
365
442
|
const yc = (nextY + y) / 2;
|
|
366
443
|
path.quadraticCurveTo(nextX, nextY, xc, yc);
|
|
367
444
|
}
|
|
368
445
|
}
|
|
369
446
|
|
|
447
|
+
// Close the path with a smooth curve
|
|
448
|
+
const lastY = (waveHeight + firstValue * waveHeight) / 2;
|
|
449
|
+
const controlX = startX;
|
|
450
|
+
const controlY = (lastY + firstY) / 2;
|
|
451
|
+
path.quadraticCurveTo(controlX, controlY, startX, firstY);
|
|
452
|
+
|
|
370
453
|
ctx.fill(path);
|
|
371
454
|
}
|
|
372
455
|
|
|
373
456
|
frameTask = new Task(this, {
|
|
374
457
|
autoRun: EF_INTERACTIVE,
|
|
375
|
-
args: () =>
|
|
458
|
+
args: () => {
|
|
459
|
+
return [
|
|
460
|
+
this.targetElement,
|
|
461
|
+
this.targetElement?.frequencyDataTask.value,
|
|
462
|
+
] as const;
|
|
463
|
+
},
|
|
376
464
|
task: async () => {
|
|
377
465
|
if (!this.targetElement) return;
|
|
378
466
|
await this.targetElement.frequencyDataTask.taskComplete;
|
|
@@ -383,11 +471,15 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
383
471
|
const frequencyData = this.targetElement.frequencyDataTask.value;
|
|
384
472
|
if (!frequencyData) return;
|
|
385
473
|
|
|
474
|
+
ctx.save();
|
|
386
475
|
if (this.color === "currentColor") {
|
|
387
476
|
const computedStyle = getComputedStyle(this);
|
|
388
477
|
const currentColor = computedStyle.color;
|
|
389
478
|
ctx.strokeStyle = currentColor;
|
|
390
479
|
ctx.fillStyle = currentColor;
|
|
480
|
+
} else {
|
|
481
|
+
ctx.strokeStyle = this.color;
|
|
482
|
+
ctx.fillStyle = this.color;
|
|
391
483
|
}
|
|
392
484
|
|
|
393
485
|
switch (this.mode) {
|
|
@@ -406,10 +498,15 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
406
498
|
case "wave":
|
|
407
499
|
this.drawWave(ctx, frequencyData);
|
|
408
500
|
break;
|
|
501
|
+
case "spikes":
|
|
502
|
+
this.drawSpikes(ctx, frequencyData);
|
|
503
|
+
break;
|
|
409
504
|
case "roundBars":
|
|
410
505
|
this.drawRoundBars(ctx, frequencyData);
|
|
411
506
|
break;
|
|
412
507
|
}
|
|
508
|
+
|
|
509
|
+
ctx.restore();
|
|
413
510
|
},
|
|
414
511
|
});
|
|
415
512
|
|
|
@@ -418,14 +515,6 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
418
515
|
return this.targetElement.durationMs;
|
|
419
516
|
}
|
|
420
517
|
|
|
421
|
-
get targetElement() {
|
|
422
|
-
const target = document.getElementById(this.targetSelector ?? "");
|
|
423
|
-
if (target instanceof EFAudio || target instanceof EFVideo) {
|
|
424
|
-
return target;
|
|
425
|
-
}
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
518
|
protected updated(changedProperties: PropertyValueMap<this>): void {
|
|
430
519
|
super.updated(changedProperties);
|
|
431
520
|
|
|
@@ -436,3 +525,9 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
|
|
|
436
525
|
}
|
|
437
526
|
}
|
|
438
527
|
}
|
|
528
|
+
|
|
529
|
+
declare global {
|
|
530
|
+
interface HTMLElementTagNameMap {
|
|
531
|
+
"ef-waveform": EFWaveform & Element;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { LitElement, html } from "lit";
|
|
2
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
4
|
+
import { EFTargetable, TargetController } from "./TargetController.ts";
|
|
5
|
+
|
|
6
|
+
let id = 0;
|
|
7
|
+
|
|
8
|
+
const nextId = () => {
|
|
9
|
+
return `targetable-test-${id++}`;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
@customElement("targetable-test")
|
|
13
|
+
class TargetableTest extends EFTargetable(LitElement) {
|
|
14
|
+
@property()
|
|
15
|
+
value = "initial";
|
|
16
|
+
|
|
17
|
+
render() {
|
|
18
|
+
return html`<div>${this.value}</div>`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@customElement("targeter-test")
|
|
23
|
+
class TargeterTest extends LitElement {
|
|
24
|
+
// @ts-expect-error this controller is needed, but never referenced
|
|
25
|
+
private targetController: TargetController = new TargetController(this);
|
|
26
|
+
|
|
27
|
+
@state()
|
|
28
|
+
targetElement: Element | null = null;
|
|
29
|
+
|
|
30
|
+
@property()
|
|
31
|
+
target = "";
|
|
32
|
+
|
|
33
|
+
render() {
|
|
34
|
+
const target = this.targetElement;
|
|
35
|
+
return html`
|
|
36
|
+
<div>
|
|
37
|
+
${target ? html`Found: ${target.tagName}` : html`Finding target...`}
|
|
38
|
+
</div>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("target", () => {
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
// Clean up all test elements from the document body
|
|
46
|
+
document.body.innerHTML = "";
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("should be able to get the target element", async () => {
|
|
50
|
+
const target = document.createElement("targetable-test");
|
|
51
|
+
const element = document.createElement("targeter-test");
|
|
52
|
+
document.body.appendChild(target);
|
|
53
|
+
document.body.appendChild(element);
|
|
54
|
+
|
|
55
|
+
const id = nextId();
|
|
56
|
+
target.id = id;
|
|
57
|
+
element.target = id;
|
|
58
|
+
|
|
59
|
+
await element.updateComplete;
|
|
60
|
+
expect(element.targetElement).toBe(target);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("should update when document changes", async () => {
|
|
64
|
+
const target = document.createElement("targetable-test");
|
|
65
|
+
const element = document.createElement("targeter-test");
|
|
66
|
+
document.body.appendChild(element);
|
|
67
|
+
|
|
68
|
+
const id = nextId();
|
|
69
|
+
element.target = id;
|
|
70
|
+
|
|
71
|
+
expect(element.targetElement).toBe(null);
|
|
72
|
+
|
|
73
|
+
target.id = id;
|
|
74
|
+
document.body.appendChild(target);
|
|
75
|
+
await element.updateComplete;
|
|
76
|
+
expect(element.targetElement).toBe(target);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("should update when attribute changes", async () => {
|
|
80
|
+
const target = document.createElement("targetable-test");
|
|
81
|
+
const element = document.createElement("targeter-test");
|
|
82
|
+
document.body.appendChild(element);
|
|
83
|
+
document.body.appendChild(target);
|
|
84
|
+
|
|
85
|
+
const id = nextId();
|
|
86
|
+
target.id = id;
|
|
87
|
+
element.target = id;
|
|
88
|
+
|
|
89
|
+
await element.updateComplete;
|
|
90
|
+
expect(element.targetElement).toBe(target);
|
|
91
|
+
|
|
92
|
+
target.id = nextId();
|
|
93
|
+
await element.updateComplete;
|
|
94
|
+
expect(element.targetElement).toBe(null);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("should update when target is set before id exists", async () => {
|
|
98
|
+
const target = document.createElement("targetable-test");
|
|
99
|
+
const element = document.createElement("targeter-test");
|
|
100
|
+
document.body.appendChild(target);
|
|
101
|
+
document.body.appendChild(element);
|
|
102
|
+
|
|
103
|
+
const id = nextId();
|
|
104
|
+
element.target = id;
|
|
105
|
+
expect(element.targetElement).toBe(null);
|
|
106
|
+
|
|
107
|
+
target.id = id;
|
|
108
|
+
await element.updateComplete;
|
|
109
|
+
expect(element.targetElement).toBe(target);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("should update when target changes to match existing id", async () => {
|
|
113
|
+
const target = document.createElement("targetable-test");
|
|
114
|
+
const element = document.createElement("targeter-test");
|
|
115
|
+
document.body.appendChild(target);
|
|
116
|
+
document.body.appendChild(element);
|
|
117
|
+
|
|
118
|
+
const id = nextId();
|
|
119
|
+
target.id = id;
|
|
120
|
+
expect(element.targetElement).toBe(null);
|
|
121
|
+
|
|
122
|
+
element.target = id;
|
|
123
|
+
await element.updateComplete;
|
|
124
|
+
expect(element.targetElement).toBe(target);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("should handle target being cleared", async () => {
|
|
128
|
+
const target = document.createElement("targetable-test");
|
|
129
|
+
const element = document.createElement("targeter-test");
|
|
130
|
+
document.body.appendChild(target);
|
|
131
|
+
document.body.appendChild(element);
|
|
132
|
+
|
|
133
|
+
const id = nextId();
|
|
134
|
+
target.id = id;
|
|
135
|
+
element.target = id;
|
|
136
|
+
|
|
137
|
+
await element.updateComplete;
|
|
138
|
+
expect(element.targetElement).toBe(target);
|
|
139
|
+
|
|
140
|
+
element.target = "";
|
|
141
|
+
await element.updateComplete;
|
|
142
|
+
expect(element.targetElement).toBe(null);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("should handle multiple elements targeting the same id", async () => {
|
|
146
|
+
const target = document.createElement("targetable-test");
|
|
147
|
+
const element1 = document.createElement("targeter-test");
|
|
148
|
+
const element2 = document.createElement("targeter-test");
|
|
149
|
+
document.body.appendChild(target);
|
|
150
|
+
document.body.appendChild(element1);
|
|
151
|
+
document.body.appendChild(element2);
|
|
152
|
+
|
|
153
|
+
const id = nextId();
|
|
154
|
+
target.id = id;
|
|
155
|
+
element1.target = id;
|
|
156
|
+
element2.target = id;
|
|
157
|
+
|
|
158
|
+
await Promise.all([element1.updateComplete, element2.updateComplete]);
|
|
159
|
+
expect(element1.targetElement).toBe(target);
|
|
160
|
+
expect(element2.targetElement).toBe(target);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("should handle element removal from DOM", async () => {
|
|
164
|
+
const target = document.createElement("targetable-test");
|
|
165
|
+
const element = document.createElement("targeter-test");
|
|
166
|
+
document.body.appendChild(target);
|
|
167
|
+
document.body.appendChild(element);
|
|
168
|
+
|
|
169
|
+
const id = nextId();
|
|
170
|
+
target.id = id;
|
|
171
|
+
element.target = id;
|
|
172
|
+
|
|
173
|
+
await element.updateComplete;
|
|
174
|
+
expect(element.targetElement).toBe(target);
|
|
175
|
+
|
|
176
|
+
document.body.removeChild(target);
|
|
177
|
+
await element.updateComplete;
|
|
178
|
+
expect(element.targetElement).toBe(null);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("should handle rapid target id changes", async () => {
|
|
182
|
+
const target = document.createElement("targetable-test");
|
|
183
|
+
const element = document.createElement("targeter-test");
|
|
184
|
+
document.body.appendChild(target);
|
|
185
|
+
document.body.appendChild(element);
|
|
186
|
+
|
|
187
|
+
const id1 = nextId();
|
|
188
|
+
const id2 = nextId();
|
|
189
|
+
const id3 = nextId();
|
|
190
|
+
|
|
191
|
+
target.id = id1;
|
|
192
|
+
element.target = id1;
|
|
193
|
+
await element.updateComplete;
|
|
194
|
+
expect(element.targetElement).toBe(target);
|
|
195
|
+
|
|
196
|
+
target.id = id2;
|
|
197
|
+
target.id = id3; // Immediately change again
|
|
198
|
+
await element.updateComplete;
|
|
199
|
+
expect(element.targetElement).toBe(null);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("should not trigger unnecessary updates when setting same id multiple times", async () => {
|
|
203
|
+
const target = document.createElement("targetable-test");
|
|
204
|
+
const element = document.createElement("targeter-test");
|
|
205
|
+
document.body.appendChild(target);
|
|
206
|
+
document.body.appendChild(element);
|
|
207
|
+
|
|
208
|
+
const id = nextId();
|
|
209
|
+
target.id = id;
|
|
210
|
+
element.target = id;
|
|
211
|
+
|
|
212
|
+
await element.updateComplete;
|
|
213
|
+
expect(element.targetElement).toBe(target);
|
|
214
|
+
|
|
215
|
+
// Set the same ID again
|
|
216
|
+
target.id = id;
|
|
217
|
+
await element.updateComplete;
|
|
218
|
+
|
|
219
|
+
// The target element should remain stable
|
|
220
|
+
expect(element.targetElement).toBe(target);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
declare global {
|
|
225
|
+
interface HTMLElementTagNameMap {
|
|
226
|
+
"targetable-test": TargetableTest & Element;
|
|
227
|
+
"targeter-test": TargeterTest & Element;
|
|
228
|
+
}
|
|
229
|
+
}
|