@editframe/elements 0.19.2-beta.0 → 0.20.0-beta.0
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/elements/ContextProxiesController.d.ts +40 -0
- package/dist/elements/ContextProxiesController.js +69 -0
- package/dist/elements/EFCaptions.d.ts +45 -6
- package/dist/elements/EFCaptions.js +220 -26
- package/dist/elements/EFImage.js +4 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +2 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +9 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +11 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +13 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +9 -0
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +7 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +24 -0
- package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
- package/dist/elements/EFMedia.js +25 -1
- package/dist/elements/EFSurface.browsertest.d.ts +0 -0
- package/dist/elements/EFSurface.d.ts +30 -0
- package/dist/elements/EFSurface.js +96 -0
- package/dist/elements/EFTemporal.js +7 -6
- package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
- package/dist/elements/EFThumbnailStrip.d.ts +86 -0
- package/dist/elements/EFThumbnailStrip.js +490 -0
- package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
- package/dist/elements/EFTimegroup.d.ts +7 -7
- package/dist/elements/EFTimegroup.js +59 -16
- package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
- package/dist/elements/updateAnimations.d.ts +5 -0
- package/dist/elements/updateAnimations.js +37 -13
- package/dist/getRenderInfo.js +1 -1
- package/dist/gui/ContextMixin.js +27 -14
- package/dist/gui/EFControls.browsertest.d.ts +0 -0
- package/dist/gui/EFControls.d.ts +38 -0
- package/dist/gui/EFControls.js +51 -0
- package/dist/gui/EFFilmstrip.d.ts +40 -1
- package/dist/gui/EFFilmstrip.js +240 -3
- package/dist/gui/EFPreview.js +2 -1
- package/dist/gui/EFScrubber.d.ts +6 -5
- package/dist/gui/EFScrubber.js +31 -21
- package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
- package/dist/gui/EFTimeDisplay.d.ts +2 -6
- package/dist/gui/EFTimeDisplay.js +13 -23
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/currentTimeContext.d.ts +3 -0
- package/dist/gui/currentTimeContext.js +3 -0
- package/dist/gui/durationContext.d.ts +3 -0
- package/dist/gui/durationContext.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/dist/style.css +1 -1
- package/dist/transcoding/types/index.d.ts +11 -0
- package/dist/utils/LRUCache.d.ts +46 -0
- package/dist/utils/LRUCache.js +382 -1
- package/dist/utils/LRUCache.test.d.ts +1 -0
- package/package.json +2 -2
- package/src/elements/ContextProxiesController.ts +123 -0
- package/src/elements/EFCaptions.browsertest.ts +1820 -0
- package/src/elements/EFCaptions.ts +373 -36
- package/src/elements/EFImage.ts +4 -1
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
- package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
- package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +48 -0
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
- package/src/elements/EFMedia.ts +38 -1
- package/src/elements/EFSurface.browsertest.ts +155 -0
- package/src/elements/EFSurface.ts +141 -0
- package/src/elements/EFTemporal.ts +14 -8
- package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
- package/src/elements/EFThumbnailStrip.ts +905 -0
- package/src/elements/EFTimegroup.browsertest.ts +56 -7
- package/src/elements/EFTimegroup.ts +88 -18
- package/src/elements/updateAnimations.browsertest.ts +361 -12
- package/src/elements/updateAnimations.ts +68 -19
- package/src/gui/ContextMixin.browsertest.ts +0 -25
- package/src/gui/ContextMixin.ts +44 -20
- package/src/gui/EFControls.browsertest.ts +175 -0
- package/src/gui/EFControls.ts +84 -0
- package/src/gui/EFFilmstrip.ts +323 -4
- package/src/gui/EFPreview.ts +2 -1
- package/src/gui/EFScrubber.ts +29 -25
- package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
- package/src/gui/EFTimeDisplay.ts +12 -40
- package/src/gui/currentTimeContext.ts +5 -0
- package/src/gui/durationContext.ts +3 -0
- package/src/transcoding/types/index.ts +13 -0
- package/src/utils/LRUCache.test.ts +272 -0
- package/src/utils/LRUCache.ts +543 -0
- package/types.json +1 -1
- package/dist/transcoding/cache/CacheManager.d.ts +0 -73
- package/src/transcoding/cache/CacheManager.ts +0 -208
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
import { customElement } from "lit/decorators.js";
|
|
1
3
|
import { assert, beforeEach, describe, test } from "vitest";
|
|
4
|
+
import { EFTemporal } from "./EFTemporal.js";
|
|
2
5
|
import type { EFTimegroup } from "./EFTimegroup.js";
|
|
3
6
|
import {
|
|
4
7
|
type AnimatableElement,
|
|
@@ -7,19 +10,31 @@ import {
|
|
|
7
10
|
|
|
8
11
|
import "./EFTimegroup.js";
|
|
9
12
|
|
|
10
|
-
//
|
|
13
|
+
// Create proper temporal test elements
|
|
14
|
+
@customElement("test-temporal-element")
|
|
15
|
+
class TestTemporalElement extends EFTemporal(LitElement) {
|
|
16
|
+
get intrinsicDurationMs() {
|
|
17
|
+
return this._durationMs;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private _durationMs = 1000;
|
|
21
|
+
setDuration(duration: number) {
|
|
22
|
+
this._durationMs = duration;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare global {
|
|
27
|
+
interface HTMLElementTagNameMap {
|
|
28
|
+
"test-temporal-element": TestTemporalElement;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
11
31
|
|
|
12
32
|
beforeEach(() => {
|
|
13
33
|
// Clean up DOM
|
|
14
34
|
while (document.body.children.length) {
|
|
15
35
|
document.body.children[0]?.remove();
|
|
16
36
|
}
|
|
17
|
-
|
|
18
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
19
|
-
const key = localStorage.key(i);
|
|
20
|
-
if (typeof key !== "string") continue;
|
|
21
|
-
localStorage.removeItem(key);
|
|
22
|
-
}
|
|
37
|
+
window.localStorage.clear();
|
|
23
38
|
});
|
|
24
39
|
|
|
25
40
|
function createTestElement(
|
|
@@ -51,6 +66,10 @@ function createTestElement(
|
|
|
51
66
|
value: props.parentTimegroup,
|
|
52
67
|
writable: true,
|
|
53
68
|
});
|
|
69
|
+
Object.defineProperty(element, "ownCurrentTimeMs", {
|
|
70
|
+
value: props.ownCurrentTimeMs ?? 0,
|
|
71
|
+
writable: true,
|
|
72
|
+
});
|
|
54
73
|
document.body.appendChild(element);
|
|
55
74
|
return element;
|
|
56
75
|
}
|
|
@@ -214,6 +233,75 @@ describe("Timeline Element Synchronizer", () => {
|
|
|
214
233
|
assert.equal(element.style.display, "");
|
|
215
234
|
});
|
|
216
235
|
|
|
236
|
+
test("sequence elements remain coordinated at exact end boundary", () => {
|
|
237
|
+
// Create a root timegroup mock
|
|
238
|
+
const rootTimegroup = {
|
|
239
|
+
currentTimeMs: 3000,
|
|
240
|
+
durationMs: 3000,
|
|
241
|
+
startTimeMs: 0,
|
|
242
|
+
endTimeMs: 3000,
|
|
243
|
+
tagName: "EF-TIMEGROUP",
|
|
244
|
+
} as any;
|
|
245
|
+
|
|
246
|
+
// Create a child element in sequence that spans 2000-3000ms
|
|
247
|
+
const element = createTestElement({
|
|
248
|
+
startTimeMs: 2000,
|
|
249
|
+
endTimeMs: 3000,
|
|
250
|
+
durationMs: 1000,
|
|
251
|
+
ownCurrentTimeMs: 1000, // At exact end of its own duration
|
|
252
|
+
rootTimegroup: rootTimegroup,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Create REAL animations using the Web Animations API
|
|
256
|
+
const animation1 = element.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
257
|
+
duration: 1000,
|
|
258
|
+
delay: 0,
|
|
259
|
+
iterations: 1,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const animation2 = element.animate(
|
|
263
|
+
[{ transform: "scale(1)" }, { transform: "scale(1.5)" }],
|
|
264
|
+
{
|
|
265
|
+
duration: 1000,
|
|
266
|
+
delay: 0,
|
|
267
|
+
iterations: 1,
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Start with animations running
|
|
272
|
+
animation1.play();
|
|
273
|
+
animation2.play();
|
|
274
|
+
|
|
275
|
+
// Verify we have real animations
|
|
276
|
+
const animations = element.getAnimations({ subtree: true });
|
|
277
|
+
assert.equal(animations.length, 2, "Should have 2 real animations");
|
|
278
|
+
|
|
279
|
+
updateAnimations(element);
|
|
280
|
+
|
|
281
|
+
// The element should be hidden due to exclusive end condition (3000 > 3000 = false)
|
|
282
|
+
assert.equal(
|
|
283
|
+
element.style.display,
|
|
284
|
+
"none",
|
|
285
|
+
"Element should be hidden at exact end boundary due to exclusive end",
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// BUT animations should still be coordinated to prevent jarring visual jumps
|
|
289
|
+
// This is the fix we want: animations coordinated even when element is hidden at exact boundary
|
|
290
|
+
animations.forEach((animation, index) => {
|
|
291
|
+
assert.approximately(
|
|
292
|
+
animation.currentTime as number,
|
|
293
|
+
999,
|
|
294
|
+
1,
|
|
295
|
+
`Animation ${index + 1} should be coordinated at exact end boundary to prevent visual jumps`,
|
|
296
|
+
);
|
|
297
|
+
assert.equal(
|
|
298
|
+
animation.playState,
|
|
299
|
+
"paused",
|
|
300
|
+
`Animation ${index + 1} should be paused after coordination`,
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
217
305
|
test("uses element currentTimeMs when no rootTimegroup", () => {
|
|
218
306
|
const element = createTestElement({
|
|
219
307
|
currentTimeMs: 500,
|
|
@@ -299,8 +387,6 @@ describe("Timeline Element Synchronizer", () => {
|
|
|
299
387
|
});
|
|
300
388
|
animation.play();
|
|
301
389
|
|
|
302
|
-
assert.equal(animation.playState, "running");
|
|
303
|
-
|
|
304
390
|
updateAnimations(element);
|
|
305
391
|
|
|
306
392
|
assert.equal(animation.playState, "paused");
|
|
@@ -367,9 +453,6 @@ describe("Timeline Element Synchronizer", () => {
|
|
|
367
453
|
animation1.play();
|
|
368
454
|
animation2.play();
|
|
369
455
|
|
|
370
|
-
assert.equal(animation1.playState, "running");
|
|
371
|
-
assert.equal(animation2.playState, "running");
|
|
372
|
-
|
|
373
456
|
updateAnimations(element);
|
|
374
457
|
|
|
375
458
|
// Both animations should be paused
|
|
@@ -415,6 +498,272 @@ describe("Timeline Element Synchronizer", () => {
|
|
|
415
498
|
|
|
416
499
|
assert.equal(animation.playState, "paused");
|
|
417
500
|
});
|
|
501
|
+
|
|
502
|
+
test("keeps completed animations available for scrubbing", async () => {
|
|
503
|
+
// Create a timegroup with 10s duration
|
|
504
|
+
const timegroup = document.createElement("ef-timegroup") as EFTimegroup;
|
|
505
|
+
timegroup.setAttribute("mode", "fixed");
|
|
506
|
+
timegroup.setAttribute("duration", "10000ms");
|
|
507
|
+
document.body.appendChild(timegroup);
|
|
508
|
+
|
|
509
|
+
// Create a child element with a 5s animation
|
|
510
|
+
const child = document.createElement("div");
|
|
511
|
+
timegroup.appendChild(child);
|
|
512
|
+
|
|
513
|
+
child.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
514
|
+
duration: 5000, // 5s animation
|
|
515
|
+
iterations: 1,
|
|
516
|
+
delay: 0,
|
|
517
|
+
});
|
|
518
|
+
timegroup.currentTime = 6;
|
|
519
|
+
await timegroup.seekTask.run();
|
|
520
|
+
|
|
521
|
+
// Animation should still be available even though timeline (6s) > animation duration (5s)
|
|
522
|
+
// This prevents animations from being removed, enabling scrubbing backwards
|
|
523
|
+
const animations = timegroup.getAnimations({ subtree: true });
|
|
524
|
+
assert.equal(
|
|
525
|
+
animations.length,
|
|
526
|
+
1,
|
|
527
|
+
"REGRESSION TEST: Animation should remain available for scrubbing. This would fail with Number.EPSILON due to insufficient precision offset.",
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
describe("child element animation coordination", () => {
|
|
533
|
+
test("coordinates animations on non-temporal child elements", async () => {
|
|
534
|
+
// Create root timegroup
|
|
535
|
+
const rootTimegroup = document.createElement(
|
|
536
|
+
"ef-timegroup",
|
|
537
|
+
) as EFTimegroup;
|
|
538
|
+
rootTimegroup.currentTimeMs = 150; // Timeline at 150ms
|
|
539
|
+
document.body.appendChild(rootTimegroup);
|
|
540
|
+
|
|
541
|
+
// Create parent temporal element
|
|
542
|
+
const parentElement = document.createElement(
|
|
543
|
+
"test-temporal-element",
|
|
544
|
+
) as TestTemporalElement;
|
|
545
|
+
parentElement.setDuration(300); // 300ms duration
|
|
546
|
+
parentElement.setAttribute("offset", "100ms"); // Start at 100ms in root timeline
|
|
547
|
+
rootTimegroup.appendChild(parentElement);
|
|
548
|
+
|
|
549
|
+
// Create a regular NON-temporal HTML element inside the temporal element
|
|
550
|
+
const nonTemporalDiv = document.createElement("div");
|
|
551
|
+
parentElement.appendChild(nonTemporalDiv);
|
|
552
|
+
|
|
553
|
+
// Wait for elements to be connected and updated
|
|
554
|
+
await rootTimegroup.updateComplete;
|
|
555
|
+
await parentElement.updateComplete;
|
|
556
|
+
|
|
557
|
+
// Create animation on the NON-temporal child element
|
|
558
|
+
const nonTemporalAnimation = nonTemporalDiv.animate(
|
|
559
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
560
|
+
{
|
|
561
|
+
duration: 1000,
|
|
562
|
+
},
|
|
563
|
+
);
|
|
564
|
+
nonTemporalAnimation.play();
|
|
565
|
+
|
|
566
|
+
// Call updateAnimations on root timegroup
|
|
567
|
+
updateAnimations(rootTimegroup);
|
|
568
|
+
|
|
569
|
+
// Parent should be visible at current timeline position (150ms is between 100ms-400ms)
|
|
570
|
+
assert.notEqual(
|
|
571
|
+
parentElement.style.display,
|
|
572
|
+
"none",
|
|
573
|
+
"Parent should be visible at current timeline time",
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
// FIXED: Non-temporal child animation should be paused and coordinated
|
|
577
|
+
assert.equal(
|
|
578
|
+
nonTemporalAnimation.playState,
|
|
579
|
+
"paused",
|
|
580
|
+
"Non-temporal child element animation should be paused and coordinated with timeline",
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("coordinates animations on deeply nested non-temporal elements", async () => {
|
|
585
|
+
// Create root timegroup
|
|
586
|
+
const rootTimegroup = document.createElement(
|
|
587
|
+
"ef-timegroup",
|
|
588
|
+
) as EFTimegroup;
|
|
589
|
+
rootTimegroup.currentTimeMs = 150; // Timeline at 150ms
|
|
590
|
+
document.body.appendChild(rootTimegroup);
|
|
591
|
+
|
|
592
|
+
// Create parent temporal element
|
|
593
|
+
const parentElement = document.createElement(
|
|
594
|
+
"test-temporal-element",
|
|
595
|
+
) as TestTemporalElement;
|
|
596
|
+
parentElement.setDuration(300); // 300ms duration
|
|
597
|
+
parentElement.setAttribute("offset", "100ms"); // Start at 100ms in root timeline
|
|
598
|
+
rootTimegroup.appendChild(parentElement);
|
|
599
|
+
|
|
600
|
+
// Create nested non-temporal structure: temporal > div > div > span
|
|
601
|
+
const outerDiv = document.createElement("div");
|
|
602
|
+
const innerDiv = document.createElement("div");
|
|
603
|
+
const span = document.createElement("span");
|
|
604
|
+
|
|
605
|
+
parentElement.appendChild(outerDiv);
|
|
606
|
+
outerDiv.appendChild(innerDiv);
|
|
607
|
+
innerDiv.appendChild(span);
|
|
608
|
+
|
|
609
|
+
// Wait for elements to be connected and updated
|
|
610
|
+
await rootTimegroup.updateComplete;
|
|
611
|
+
await parentElement.updateComplete;
|
|
612
|
+
|
|
613
|
+
// Create animations on different levels of nesting
|
|
614
|
+
const outerAnimation = outerDiv.animate(
|
|
615
|
+
[{ transform: "scale(1)" }, { transform: "scale(1.1)" }],
|
|
616
|
+
{
|
|
617
|
+
duration: 800,
|
|
618
|
+
},
|
|
619
|
+
);
|
|
620
|
+
const innerAnimation = innerDiv.animate(
|
|
621
|
+
[{ opacity: 0.5 }, { opacity: 1 }],
|
|
622
|
+
{
|
|
623
|
+
duration: 1200,
|
|
624
|
+
},
|
|
625
|
+
);
|
|
626
|
+
const spanAnimation = span.animate(
|
|
627
|
+
[{ color: "red" }, { color: "blue" }],
|
|
628
|
+
{
|
|
629
|
+
duration: 600,
|
|
630
|
+
},
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
outerAnimation.play();
|
|
634
|
+
innerAnimation.play();
|
|
635
|
+
spanAnimation.play();
|
|
636
|
+
|
|
637
|
+
// Call updateAnimations on root timegroup
|
|
638
|
+
updateAnimations(rootTimegroup);
|
|
639
|
+
|
|
640
|
+
// All nested non-temporal animations should be coordinated
|
|
641
|
+
assert.equal(
|
|
642
|
+
outerAnimation.playState,
|
|
643
|
+
"paused",
|
|
644
|
+
"Outer div animation should be coordinated",
|
|
645
|
+
);
|
|
646
|
+
assert.equal(
|
|
647
|
+
innerAnimation.playState,
|
|
648
|
+
"paused",
|
|
649
|
+
"Inner div animation should be coordinated",
|
|
650
|
+
);
|
|
651
|
+
assert.equal(
|
|
652
|
+
spanAnimation.playState,
|
|
653
|
+
"paused",
|
|
654
|
+
"Span animation should be coordinated",
|
|
655
|
+
);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("coordinates animations on child temporal elements when they are visible", async () => {
|
|
659
|
+
// Create root timegroup
|
|
660
|
+
const rootTimegroup = document.createElement(
|
|
661
|
+
"ef-timegroup",
|
|
662
|
+
) as EFTimegroup;
|
|
663
|
+
rootTimegroup.currentTimeMs = 150; // Timeline at 150ms
|
|
664
|
+
document.body.appendChild(rootTimegroup);
|
|
665
|
+
|
|
666
|
+
// Create parent element (timegroup acts as parent)
|
|
667
|
+
const parentTimegroup = document.createElement(
|
|
668
|
+
"ef-timegroup",
|
|
669
|
+
) as EFTimegroup;
|
|
670
|
+
parentTimegroup.setAttribute("duration", "1000ms");
|
|
671
|
+
rootTimegroup.appendChild(parentTimegroup);
|
|
672
|
+
|
|
673
|
+
// Create child temporal element that WILL be visible at timeline time 150ms
|
|
674
|
+
const childElement = document.createElement(
|
|
675
|
+
"test-temporal-element",
|
|
676
|
+
) as TestTemporalElement;
|
|
677
|
+
childElement.setDuration(300); // 300ms duration (from 100ms to 400ms in root timeline)
|
|
678
|
+
childElement.setAttribute("offset", "100ms"); // Start at 100ms in root timeline
|
|
679
|
+
parentTimegroup.appendChild(childElement);
|
|
680
|
+
|
|
681
|
+
// Wait for elements to be connected and updated
|
|
682
|
+
await rootTimegroup.updateComplete;
|
|
683
|
+
await parentTimegroup.updateComplete;
|
|
684
|
+
await childElement.updateComplete;
|
|
685
|
+
|
|
686
|
+
// Create animation on child element
|
|
687
|
+
const childAnimation = childElement.animate(
|
|
688
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
689
|
+
{
|
|
690
|
+
duration: 1000,
|
|
691
|
+
},
|
|
692
|
+
);
|
|
693
|
+
childAnimation.play();
|
|
694
|
+
|
|
695
|
+
// Call updateAnimations on parent timegroup - this should coordinate child animations too
|
|
696
|
+
updateAnimations(parentTimegroup);
|
|
697
|
+
|
|
698
|
+
// Child should be visible at current timeline position (150ms is between 100ms-400ms)
|
|
699
|
+
assert.notEqual(
|
|
700
|
+
childElement.style.display,
|
|
701
|
+
"none",
|
|
702
|
+
"Child should be visible at current timeline time",
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
// FIXED: Child animation should be paused and coordinated
|
|
706
|
+
assert.equal(
|
|
707
|
+
childAnimation.playState,
|
|
708
|
+
"paused",
|
|
709
|
+
"Child element animation should be paused and coordinated with timeline",
|
|
710
|
+
);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test("does not coordinate animations on child temporal elements when they are not visible", async () => {
|
|
714
|
+
// Create root timegroup
|
|
715
|
+
const rootTimegroup = document.createElement(
|
|
716
|
+
"ef-timegroup",
|
|
717
|
+
) as EFTimegroup;
|
|
718
|
+
rootTimegroup.currentTimeMs = 100; // Timeline at 100ms
|
|
719
|
+
document.body.appendChild(rootTimegroup);
|
|
720
|
+
|
|
721
|
+
// Create parent element (timegroup acts as parent)
|
|
722
|
+
const parentTimegroup = document.createElement(
|
|
723
|
+
"ef-timegroup",
|
|
724
|
+
) as EFTimegroup;
|
|
725
|
+
parentTimegroup.setAttribute("duration", "1000ms");
|
|
726
|
+
rootTimegroup.appendChild(parentTimegroup);
|
|
727
|
+
|
|
728
|
+
// Create child temporal element that will NOT be visible at timeline time 100ms
|
|
729
|
+
const childElement = document.createElement(
|
|
730
|
+
"test-temporal-element",
|
|
731
|
+
) as TestTemporalElement;
|
|
732
|
+
childElement.setDuration(200); // 200ms duration
|
|
733
|
+
childElement.setAttribute("offset", "500ms"); // Start at 500ms in root timeline (way after current time)
|
|
734
|
+
parentTimegroup.appendChild(childElement);
|
|
735
|
+
|
|
736
|
+
// Wait for elements to be connected and updated
|
|
737
|
+
await rootTimegroup.updateComplete;
|
|
738
|
+
await parentTimegroup.updateComplete;
|
|
739
|
+
await childElement.updateComplete;
|
|
740
|
+
|
|
741
|
+
// Create animation on child element
|
|
742
|
+
const childAnimation = childElement.animate(
|
|
743
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
744
|
+
{
|
|
745
|
+
duration: 1000,
|
|
746
|
+
},
|
|
747
|
+
);
|
|
748
|
+
childAnimation.play();
|
|
749
|
+
|
|
750
|
+
// Call updateAnimations on parent timegroup
|
|
751
|
+
updateAnimations(parentTimegroup);
|
|
752
|
+
|
|
753
|
+
// Child should be hidden (display: none)
|
|
754
|
+
assert.equal(
|
|
755
|
+
childElement.style.display,
|
|
756
|
+
"none",
|
|
757
|
+
"Child should be hidden when not in visible time range",
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
// Child animation should still be running (not coordinated since child is not visible)
|
|
761
|
+
assert.equal(
|
|
762
|
+
childAnimation.playState,
|
|
763
|
+
"paused",
|
|
764
|
+
"Child animation should remain running when child element is not visible",
|
|
765
|
+
);
|
|
766
|
+
});
|
|
418
767
|
});
|
|
419
768
|
|
|
420
769
|
describe("edge cases", () => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deepGetTemporalElements,
|
|
3
|
-
isEFTemporal,
|
|
4
3
|
type TemporalMixinInterface,
|
|
5
4
|
} from "./EFTemporal.ts";
|
|
6
5
|
|
|
@@ -8,7 +7,7 @@ import {
|
|
|
8
7
|
export type AnimatableElement = TemporalMixinInterface & HTMLElement;
|
|
9
8
|
|
|
10
9
|
// Constants
|
|
11
|
-
const ANIMATION_PRECISION_OFFSET =
|
|
10
|
+
const ANIMATION_PRECISION_OFFSET = 0.1; // Use 0.1ms to safely avoid completion threshold
|
|
12
11
|
const DEFAULT_ANIMATION_ITERATIONS = 1;
|
|
13
12
|
const PROGRESS_PROPERTY = "--ef-progress";
|
|
14
13
|
const DURATION_PROPERTY = "--ef-duration";
|
|
@@ -39,8 +38,40 @@ export const evaluateTemporalState = (
|
|
|
39
38
|
? 1
|
|
40
39
|
: Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));
|
|
41
40
|
|
|
41
|
+
// Root timegroups should remain visible at exact end time, but other elements use exclusive end for clean transitions
|
|
42
|
+
const isRootTimegroup =
|
|
43
|
+
element.tagName.toLowerCase() === TIMEGROUP_TAGNAME &&
|
|
44
|
+
!(element as any).parentTimegroup;
|
|
45
|
+
const useInclusiveEnd = isRootTimegroup;
|
|
46
|
+
|
|
42
47
|
const isVisible =
|
|
43
|
-
element.startTimeMs <= timelineTimeMs &&
|
|
48
|
+
element.startTimeMs <= timelineTimeMs &&
|
|
49
|
+
(useInclusiveEnd
|
|
50
|
+
? element.endTimeMs >= timelineTimeMs
|
|
51
|
+
: element.endTimeMs > timelineTimeMs);
|
|
52
|
+
|
|
53
|
+
return { progress, isVisible, timelineTimeMs };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Evaluates element visibility specifically for animation coordination
|
|
58
|
+
* Uses inclusive end boundaries to prevent animation jumps at exact boundaries
|
|
59
|
+
*/
|
|
60
|
+
export const evaluateTemporalStateForAnimation = (
|
|
61
|
+
element: AnimatableElement,
|
|
62
|
+
): TemporalState => {
|
|
63
|
+
// Get timeline time from root timegroup, or use element's own time if it IS a timegroup
|
|
64
|
+
const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;
|
|
65
|
+
|
|
66
|
+
const progress =
|
|
67
|
+
element.durationMs <= 0
|
|
68
|
+
? 1
|
|
69
|
+
: Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));
|
|
70
|
+
|
|
71
|
+
// For animation coordination, use inclusive end for ALL elements to prevent visual jumps
|
|
72
|
+
const isVisible =
|
|
73
|
+
element.startTimeMs <= timelineTimeMs &&
|
|
74
|
+
element.endTimeMs >= timelineTimeMs;
|
|
44
75
|
|
|
45
76
|
return { progress, isVisible, timelineTimeMs };
|
|
46
77
|
};
|
|
@@ -80,9 +111,11 @@ const updateVisualState = (
|
|
|
80
111
|
};
|
|
81
112
|
|
|
82
113
|
/**
|
|
83
|
-
* Coordinates animations
|
|
114
|
+
* Coordinates animations for a single element and its subtree, using the element as the time source
|
|
84
115
|
*/
|
|
85
|
-
const
|
|
116
|
+
const coordinateAnimationsForSingleElement = (
|
|
117
|
+
element: AnimatableElement,
|
|
118
|
+
): void => {
|
|
86
119
|
const animations = element.getAnimations({ subtree: true });
|
|
87
120
|
|
|
88
121
|
for (const animation of animations) {
|
|
@@ -96,17 +129,12 @@ const coordinateAnimations = (element: AnimatableElement): void => {
|
|
|
96
129
|
}
|
|
97
130
|
|
|
98
131
|
const target = effect.target;
|
|
99
|
-
if (!target
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const timeTarget = isEFTemporal(target)
|
|
104
|
-
? target
|
|
105
|
-
: target.closest(TIMEGROUP_TAGNAME);
|
|
106
|
-
if (!timeTarget) {
|
|
132
|
+
if (!target) {
|
|
107
133
|
continue;
|
|
108
134
|
}
|
|
109
135
|
|
|
136
|
+
// For animations in this element's subtree, always use this element as the time source
|
|
137
|
+
// This handles both animations directly on the temporal element and on its non-temporal children
|
|
110
138
|
const timing = effect.getTiming();
|
|
111
139
|
const duration = Number(timing.duration) || 0;
|
|
112
140
|
const delay = Number(timing.delay) || 0;
|
|
@@ -118,8 +146,8 @@ const coordinateAnimations = (element: AnimatableElement): void => {
|
|
|
118
146
|
continue;
|
|
119
147
|
}
|
|
120
148
|
|
|
121
|
-
//
|
|
122
|
-
const currentTime =
|
|
149
|
+
// Use the element itself as the time source (it's guaranteed to be temporal)
|
|
150
|
+
const currentTime = element.ownCurrentTimeMs ?? 0;
|
|
123
151
|
|
|
124
152
|
if (currentTime < delay) {
|
|
125
153
|
animation.currentTime = 0;
|
|
@@ -130,12 +158,22 @@ const coordinateAnimations = (element: AnimatableElement): void => {
|
|
|
130
158
|
const currentIteration = Math.floor(adjustedTime / duration);
|
|
131
159
|
const currentIterationTime = adjustedTime % duration;
|
|
132
160
|
|
|
161
|
+
// Calculate the total animation timeline length (delay + duration * iterations)
|
|
162
|
+
const totalAnimationLength = delay + duration * iterations;
|
|
163
|
+
|
|
164
|
+
// CRITICAL: Always keep currentTime below totalAnimationLength to prevent completion
|
|
165
|
+
const maxSafeCurrentTime =
|
|
166
|
+
totalAnimationLength - ANIMATION_PRECISION_OFFSET;
|
|
167
|
+
|
|
133
168
|
if (currentIteration >= iterations) {
|
|
134
|
-
|
|
169
|
+
// Animation would be complete - clamp to just before completion
|
|
170
|
+
animation.currentTime = maxSafeCurrentTime;
|
|
135
171
|
} else {
|
|
136
|
-
|
|
172
|
+
// Animation in progress - clamp to safe value within current iteration
|
|
173
|
+
const proposedCurrentTime =
|
|
137
174
|
Math.min(currentIterationTime, duration - ANIMATION_PRECISION_OFFSET) +
|
|
138
175
|
delay;
|
|
176
|
+
animation.currentTime = Math.min(proposedCurrentTime, maxSafeCurrentTime);
|
|
139
177
|
}
|
|
140
178
|
}
|
|
141
179
|
};
|
|
@@ -151,7 +189,18 @@ export const updateAnimations = (element: AnimatableElement): void => {
|
|
|
151
189
|
});
|
|
152
190
|
updateVisualState(element, temporalState);
|
|
153
191
|
|
|
154
|
-
|
|
155
|
-
|
|
192
|
+
// Coordinate animations - use animation-specific visibility to prevent jumps at exact boundaries
|
|
193
|
+
const animationState = evaluateTemporalStateForAnimation(element);
|
|
194
|
+
if (animationState.isVisible) {
|
|
195
|
+
coordinateAnimationsForSingleElement(element);
|
|
156
196
|
}
|
|
197
|
+
|
|
198
|
+
// Coordinate animations for child elements using animation-specific visibility
|
|
199
|
+
deepGetTemporalElements(element).forEach((temporalElement) => {
|
|
200
|
+
const childAnimationState =
|
|
201
|
+
evaluateTemporalStateForAnimation(temporalElement);
|
|
202
|
+
if (childAnimationState.isVisible) {
|
|
203
|
+
coordinateAnimationsForSingleElement(temporalElement);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
157
206
|
};
|
|
@@ -4,7 +4,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
4
4
|
|
|
5
5
|
import { ContextMixin } from "./ContextMixin.js";
|
|
6
6
|
|
|
7
|
-
// Required to test timeupdate event, we need a duration, and timegroups are a quick way to do that
|
|
8
7
|
import "../elements/EFTimegroup.js";
|
|
9
8
|
|
|
10
9
|
@customElement("test-context")
|
|
@@ -529,30 +528,6 @@ describe("ContextMixin", () => {
|
|
|
529
528
|
});
|
|
530
529
|
});
|
|
531
530
|
|
|
532
|
-
test("Time update event when the currentTimeMs changed", async () => {
|
|
533
|
-
const timegroup = document.createElement("ef-timegroup");
|
|
534
|
-
timegroup.mode = "fixed";
|
|
535
|
-
timegroup.duration = "10s";
|
|
536
|
-
|
|
537
|
-
const preview = document.createElement("test-context");
|
|
538
|
-
preview.append(timegroup);
|
|
539
|
-
document.body.append(preview);
|
|
540
|
-
|
|
541
|
-
type CurrentTimeEvent = CustomEvent<{ currentTimeMs: number }>;
|
|
542
|
-
|
|
543
|
-
// Expect the timeupdate event to be dispatched
|
|
544
|
-
const timeupdatePromise = new Promise<CurrentTimeEvent>((resolve) => {
|
|
545
|
-
preview.addEventListener(
|
|
546
|
-
"timeupdate",
|
|
547
|
-
(event: Event) => resolve(event as CurrentTimeEvent),
|
|
548
|
-
{ once: true },
|
|
549
|
-
);
|
|
550
|
-
});
|
|
551
|
-
preview.currentTimeMs = 1000;
|
|
552
|
-
const event = await timeupdatePromise;
|
|
553
|
-
expect(event.detail.currentTimeMs).toBe(1000);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
531
|
describe("Reactivity", () => {
|
|
557
532
|
test("should update durationMs when child tree changes", async () => {
|
|
558
533
|
const element = document.createElement("test-context-reactivity");
|