@editframe/elements 0.19.4-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 +6 -1
- package/dist/elements/EFTimegroup.js +46 -10
- 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 +70 -11
- package/src/elements/updateAnimations.browsertest.ts +333 -11
- 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
|
@@ -420,13 +420,57 @@ describe("setting currentTime", () => {
|
|
|
420
420
|
await timegroup.waitForMediaDurations();
|
|
421
421
|
|
|
422
422
|
timegroup.currentTime = 5_000; // 5000 seconds, should clamp to 10s
|
|
423
|
-
await timegroup.seekTask.
|
|
423
|
+
await timegroup.seekTask.run();
|
|
424
424
|
|
|
425
425
|
const storedValue = localStorage.getItem(timegroup.storageKey);
|
|
426
426
|
assert.equal(storedValue, "10"); // Should store 10 (clamped from 5000 to duration)
|
|
427
427
|
timegroup.remove();
|
|
428
428
|
});
|
|
429
429
|
|
|
430
|
+
test("root timegroup remains visible when currentTime equals duration exactly", async () => {
|
|
431
|
+
const timegroup = renderTimegroup(
|
|
432
|
+
html`<ef-timegroup id="end-time-test" mode="fixed" duration="10s"></ef-timegroup>`,
|
|
433
|
+
);
|
|
434
|
+
await timegroup.waitForMediaDurations();
|
|
435
|
+
|
|
436
|
+
// Set currentTime to exactly the duration
|
|
437
|
+
timegroup.currentTime = 10; // 10 seconds
|
|
438
|
+
await timegroup.seekTask.taskComplete;
|
|
439
|
+
await timegroup.frameTask.taskComplete;
|
|
440
|
+
|
|
441
|
+
// The root timegroup should still be visible at the exact end time
|
|
442
|
+
assert.notEqual(
|
|
443
|
+
timegroup.style.display,
|
|
444
|
+
"none",
|
|
445
|
+
"Root timegroup should be visible at exact end time",
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("root timegroup becomes hidden only after currentTime exceeds duration", async () => {
|
|
450
|
+
const timegroup = renderTimegroup(
|
|
451
|
+
html`<ef-timegroup id="beyond-end-test" mode="fixed" duration="10s"></ef-timegroup>`,
|
|
452
|
+
);
|
|
453
|
+
await timegroup.waitForMediaDurations();
|
|
454
|
+
|
|
455
|
+
// Set currentTime beyond the duration (should be clamped to duration)
|
|
456
|
+
timegroup.currentTime = 15; // 15 seconds, should clamp to 10s
|
|
457
|
+
await timegroup.seekTask.taskComplete;
|
|
458
|
+
await timegroup.frameTask.taskComplete;
|
|
459
|
+
|
|
460
|
+
// Even when clamped, it should still be visible at the end
|
|
461
|
+
assert.notEqual(
|
|
462
|
+
timegroup.style.display,
|
|
463
|
+
"none",
|
|
464
|
+
"Root timegroup should be visible even when time is clamped to duration",
|
|
465
|
+
);
|
|
466
|
+
// Verify that the time was actually clamped
|
|
467
|
+
assert.equal(
|
|
468
|
+
timegroup.currentTime,
|
|
469
|
+
10,
|
|
470
|
+
"Time should be clamped to duration",
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
430
474
|
test("does not persist in localStorage if the timegroup has no id", async () => {
|
|
431
475
|
const timegroup = renderTimegroup(
|
|
432
476
|
html`<ef-timegroup mode="fixed" duration="10s"></ef-timegroup>`,
|
|
@@ -572,19 +616,23 @@ describe("Dynamic content updates", () => {
|
|
|
572
616
|
const frameTaskB = timegroup.querySelector("test-frame-task-b")!;
|
|
573
617
|
const frameTaskC = timegroup.querySelector("test-frame-task-c")!;
|
|
574
618
|
|
|
575
|
-
|
|
619
|
+
// following the initial update, the first frame tasks have run once.
|
|
620
|
+
await timegroup.updateComplete;
|
|
621
|
+
|
|
622
|
+
assert.equal(frameTaskA.frameTaskCount, 1);
|
|
576
623
|
assert.equal(frameTaskB.frameTaskCount, 0);
|
|
577
|
-
assert.equal(frameTaskC.frameTaskCount,
|
|
624
|
+
assert.equal(frameTaskC.frameTaskCount, 1);
|
|
578
625
|
|
|
626
|
+
// Then we run them manually.
|
|
579
627
|
await timegroup.frameTask.run();
|
|
580
628
|
|
|
581
629
|
// At timeline time 0ms:
|
|
582
630
|
// - frameTaskA (0-1000ms) should run
|
|
583
631
|
// - frameTaskB (1000-2000ms) should NOT run
|
|
584
632
|
// - frameTaskC (0-1000ms) should run (inherits root positioning)
|
|
585
|
-
assert.equal(frameTaskA.frameTaskCount,
|
|
633
|
+
assert.equal(frameTaskA.frameTaskCount, 2);
|
|
586
634
|
assert.equal(frameTaskB.frameTaskCount, 0); // Not visible at time 0
|
|
587
|
-
assert.equal(frameTaskC.frameTaskCount,
|
|
635
|
+
assert.equal(frameTaskC.frameTaskCount, 2); // Nested in B but inherits root positioning
|
|
588
636
|
});
|
|
589
637
|
});
|
|
590
638
|
|
|
@@ -599,9 +647,10 @@ describe("Dynamic content updates", () => {
|
|
|
599
647
|
);
|
|
600
648
|
const nonRootTimegroup = timegroup.querySelector("ef-timegroup")!;
|
|
601
649
|
const frameTaskA = timegroup.querySelector("test-frame-task-a")!;
|
|
602
|
-
|
|
650
|
+
await timegroup.updateComplete;
|
|
651
|
+
assert.equal(frameTaskA.frameTaskCount, 1);
|
|
603
652
|
await nonRootTimegroup.seekTask.run();
|
|
604
|
-
assert.equal(frameTaskA.frameTaskCount,
|
|
653
|
+
assert.equal(frameTaskA.frameTaskCount, 1);
|
|
605
654
|
});
|
|
606
655
|
|
|
607
656
|
test("waits for media durations", async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { provide } from "@lit/context";
|
|
2
2
|
import { Task, TaskStatus } from "@lit/task";
|
|
3
3
|
import debug from "debug";
|
|
4
|
-
import { css, html, LitElement } from "lit";
|
|
4
|
+
import { css, html, LitElement, type PropertyValues } from "lit";
|
|
5
5
|
import { customElement, property } from "lit/decorators.js";
|
|
6
6
|
|
|
7
7
|
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
timegroupContext,
|
|
18
18
|
} from "./EFTemporal.js";
|
|
19
19
|
import { TimegroupController } from "./TimegroupController.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
evaluateTemporalStateForAnimation,
|
|
22
|
+
updateAnimations,
|
|
23
|
+
} from "./updateAnimations.ts";
|
|
21
24
|
|
|
22
25
|
const log = debug("ef:elements:EFTimegroup");
|
|
23
26
|
|
|
@@ -104,6 +107,46 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
104
107
|
|
|
105
108
|
#processingPendingSeek = false;
|
|
106
109
|
|
|
110
|
+
#frameTaskInProgress = false;
|
|
111
|
+
|
|
112
|
+
#pendingFrameTaskRun = false;
|
|
113
|
+
|
|
114
|
+
#processingPendingFrameTask = false;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Throttles frameTask execution to ensure only one runs at a time while preserving the last request
|
|
118
|
+
*/
|
|
119
|
+
private async runThrottledFrameTask(): Promise<void> {
|
|
120
|
+
if (this.#frameTaskInProgress) {
|
|
121
|
+
this.#pendingFrameTaskRun = true;
|
|
122
|
+
// Wait for the current frame task to complete
|
|
123
|
+
while (this.#frameTaskInProgress) {
|
|
124
|
+
await this.frameTask.taskComplete;
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.#frameTaskInProgress = true;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await this.frameTask.run();
|
|
133
|
+
} finally {
|
|
134
|
+
this.#frameTaskInProgress = false;
|
|
135
|
+
|
|
136
|
+
if (this.#pendingFrameTaskRun && !this.#processingPendingFrameTask) {
|
|
137
|
+
this.#pendingFrameTaskRun = false;
|
|
138
|
+
this.#processingPendingFrameTask = true;
|
|
139
|
+
try {
|
|
140
|
+
await this.runThrottledFrameTask();
|
|
141
|
+
} finally {
|
|
142
|
+
this.#processingPendingFrameTask = false;
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
this.#pendingFrameTaskRun = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
107
150
|
@property({ type: Number, attribute: "currenttime" })
|
|
108
151
|
set currentTime(time: number) {
|
|
109
152
|
time = Math.max(0, time);
|
|
@@ -127,7 +170,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
127
170
|
}
|
|
128
171
|
|
|
129
172
|
this.#currentTime = time;
|
|
130
|
-
// This will be set to false in the seekTask
|
|
131
173
|
this.#seekInProgress = true;
|
|
132
174
|
|
|
133
175
|
this.seekTask.run().finally(() => {
|
|
@@ -232,6 +274,15 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
232
274
|
}
|
|
233
275
|
}
|
|
234
276
|
|
|
277
|
+
#previousDurationMs = 0;
|
|
278
|
+
|
|
279
|
+
protected updated(_changedProperties: PropertyValues): void {
|
|
280
|
+
if (this.#previousDurationMs !== this.durationMs) {
|
|
281
|
+
this.#previousDurationMs = this.durationMs;
|
|
282
|
+
this.runThrottledFrameTask();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
235
286
|
disconnectedCallback() {
|
|
236
287
|
super.disconnectedCallback();
|
|
237
288
|
this.#resizeObserver?.disconnect();
|
|
@@ -333,7 +384,14 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
333
384
|
const startTimeMs = (temporal as any).startTimeMs as number;
|
|
334
385
|
const endTimeMs = (temporal as any).endTimeMs as number;
|
|
335
386
|
const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
|
|
336
|
-
|
|
387
|
+
// Root timegroups should remain visible at exact end time, but other elements use exclusive end for clean transitions
|
|
388
|
+
const isRootTimegroup =
|
|
389
|
+
temporal.tagName.toLowerCase() === "ef-timegroup" &&
|
|
390
|
+
!(temporal as any).parentTimegroup;
|
|
391
|
+
const useInclusiveEnd = isRootTimegroup;
|
|
392
|
+
const elementEndsAfterStart = useInclusiveEnd
|
|
393
|
+
? endTimeMs >= timelineTimeMs
|
|
394
|
+
: endTimeMs > timelineTimeMs;
|
|
337
395
|
return elementStartsBeforeEnd && elementEndsAfterStart;
|
|
338
396
|
});
|
|
339
397
|
|
|
@@ -366,15 +424,14 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
366
424
|
const temporalElements = deepGetElementsWithFrameTasks(this);
|
|
367
425
|
|
|
368
426
|
// Filter to only include temporally visible elements for frame processing
|
|
427
|
+
// Use animation-friendly visibility to prevent animation jumps at exact boundaries
|
|
369
428
|
const visibleElements = temporalElements.filter((element) => {
|
|
370
|
-
const
|
|
371
|
-
return
|
|
429
|
+
const animationState = evaluateTemporalStateForAnimation(element);
|
|
430
|
+
return animationState.isVisible;
|
|
372
431
|
});
|
|
373
432
|
|
|
374
433
|
await Promise.all(
|
|
375
|
-
visibleElements.map((element) =>
|
|
376
|
-
return element.frameTask.run();
|
|
377
|
-
}),
|
|
434
|
+
visibleElements.map((element) => element.frameTask.run()),
|
|
378
435
|
);
|
|
379
436
|
}
|
|
380
437
|
|
|
@@ -679,9 +736,11 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
679
736
|
0,
|
|
680
737
|
Math.min(targetTime ?? 0, this.durationMs / 1000),
|
|
681
738
|
);
|
|
739
|
+
// Apply the clamped time back to currentTime
|
|
740
|
+
this.#currentTime = newTime;
|
|
682
741
|
this.requestUpdate("currentTime");
|
|
683
|
-
await this.
|
|
684
|
-
this.#saveTimeToLocalStorage(
|
|
742
|
+
await this.runThrottledFrameTask();
|
|
743
|
+
this.#saveTimeToLocalStorage(this.#currentTime);
|
|
685
744
|
// This has to be set false here so any following seeks are not treated as pending
|
|
686
745
|
this.#seekInProgress = false;
|
|
687
746
|
return newTime;
|
|
@@ -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,17 +10,31 @@ import {
|
|
|
7
10
|
|
|
8
11
|
import "./EFTimegroup.js";
|
|
9
12
|
|
|
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
|
+
}
|
|
31
|
+
|
|
10
32
|
beforeEach(() => {
|
|
11
33
|
// Clean up DOM
|
|
12
34
|
while (document.body.children.length) {
|
|
13
35
|
document.body.children[0]?.remove();
|
|
14
36
|
}
|
|
15
|
-
|
|
16
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
17
|
-
const key = localStorage.key(i);
|
|
18
|
-
if (typeof key !== "string") continue;
|
|
19
|
-
localStorage.removeItem(key);
|
|
20
|
-
}
|
|
37
|
+
window.localStorage.clear();
|
|
21
38
|
});
|
|
22
39
|
|
|
23
40
|
function createTestElement(
|
|
@@ -49,6 +66,10 @@ function createTestElement(
|
|
|
49
66
|
value: props.parentTimegroup,
|
|
50
67
|
writable: true,
|
|
51
68
|
});
|
|
69
|
+
Object.defineProperty(element, "ownCurrentTimeMs", {
|
|
70
|
+
value: props.ownCurrentTimeMs ?? 0,
|
|
71
|
+
writable: true,
|
|
72
|
+
});
|
|
52
73
|
document.body.appendChild(element);
|
|
53
74
|
return element;
|
|
54
75
|
}
|
|
@@ -212,6 +233,75 @@ describe("Timeline Element Synchronizer", () => {
|
|
|
212
233
|
assert.equal(element.style.display, "");
|
|
213
234
|
});
|
|
214
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
|
+
|
|
215
305
|
test("uses element currentTimeMs when no rootTimegroup", () => {
|
|
216
306
|
const element = createTestElement({
|
|
217
307
|
currentTimeMs: 500,
|
|
@@ -297,8 +387,6 @@ describe("Timeline Element Synchronizer", () => {
|
|
|
297
387
|
});
|
|
298
388
|
animation.play();
|
|
299
389
|
|
|
300
|
-
assert.equal(animation.playState, "running");
|
|
301
|
-
|
|
302
390
|
updateAnimations(element);
|
|
303
391
|
|
|
304
392
|
assert.equal(animation.playState, "paused");
|
|
@@ -365,9 +453,6 @@ describe("Timeline Element Synchronizer", () => {
|
|
|
365
453
|
animation1.play();
|
|
366
454
|
animation2.play();
|
|
367
455
|
|
|
368
|
-
assert.equal(animation1.playState, "running");
|
|
369
|
-
assert.equal(animation2.playState, "running");
|
|
370
|
-
|
|
371
456
|
updateAnimations(element);
|
|
372
457
|
|
|
373
458
|
// Both animations should be paused
|
|
@@ -444,6 +529,243 @@ describe("Timeline Element Synchronizer", () => {
|
|
|
444
529
|
});
|
|
445
530
|
});
|
|
446
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
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
447
769
|
describe("edge cases", () => {
|
|
448
770
|
test("handles zero duration gracefully", () => {
|
|
449
771
|
const element = createTestElement({
|