@editframe/elements 0.15.0-beta.17 → 0.15.0-beta.19

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 (38) hide show
  1. package/dist/EF_FRAMEGEN.js +1 -1
  2. package/dist/elements/EFMedia.d.ts +7 -3
  3. package/dist/elements/EFMedia.js +45 -90
  4. package/dist/elements/EFTemporal.browsertest.d.ts +4 -3
  5. package/dist/elements/EFTemporal.d.ts +14 -11
  6. package/dist/elements/EFTemporal.js +63 -87
  7. package/dist/elements/EFTimegroup.d.ts +1 -3
  8. package/dist/elements/EFTimegroup.js +15 -103
  9. package/dist/elements/EFVideo.js +3 -1
  10. package/dist/elements/EFWaveform.d.ts +1 -0
  11. package/dist/elements/EFWaveform.js +6 -2
  12. package/dist/elements/durationConverter.d.ts +8 -8
  13. package/dist/elements/durationConverter.js +2 -2
  14. package/dist/elements/updateAnimations.d.ts +9 -0
  15. package/dist/elements/updateAnimations.js +62 -0
  16. package/dist/gui/EFFilmstrip.js +7 -16
  17. package/dist/gui/EFFitScale.d.ts +25 -0
  18. package/dist/gui/EFFitScale.js +123 -0
  19. package/dist/gui/EFWorkbench.d.ts +1 -5
  20. package/dist/gui/EFWorkbench.js +6 -55
  21. package/dist/gui/TWMixin.css.js +1 -1
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +2 -0
  24. package/dist/style.css +3 -3
  25. package/package.json +2 -2
  26. package/src/elements/EFMedia.browsertest.ts +10 -10
  27. package/src/elements/EFMedia.ts +56 -118
  28. package/src/elements/EFTemporal.browsertest.ts +64 -31
  29. package/src/elements/EFTemporal.ts +99 -119
  30. package/src/elements/EFTimegroup.ts +15 -133
  31. package/src/elements/EFVideo.ts +3 -1
  32. package/src/elements/EFWaveform.ts +5 -2
  33. package/src/elements/durationConverter.ts +9 -4
  34. package/src/elements/updateAnimations.ts +88 -0
  35. package/src/gui/EFFilmstrip.ts +7 -16
  36. package/src/gui/EFFitScale.ts +133 -0
  37. package/src/gui/EFWorkbench.ts +7 -64
  38. package/types.json +1 -1
@@ -18,6 +18,8 @@ export declare class TemporalMixinInterface {
18
18
  */
19
19
  get hasExplicitDuration(): boolean;
20
20
 
21
+ get sourceStartMs(): number;
22
+
21
23
  /**
22
24
  * Used to trim the start of the media.
23
25
  *
@@ -27,7 +29,7 @@ export declare class TemporalMixinInterface {
27
29
  *
28
30
  * @domAttribute "trimstart"
29
31
  */
30
- get trimStartMs(): number;
32
+ get trimStartMs(): number | undefined;
31
33
 
32
34
  /**
33
35
  * Used to trim the end of the media.
@@ -40,10 +42,10 @@ export declare class TemporalMixinInterface {
40
42
  */
41
43
  get trimEndMs(): number;
42
44
 
43
- set trimStartMs(value: number);
44
- set trimEndMs(value: number);
45
- set trimstart(value: string);
46
- set trimend(value: string);
45
+ set trimStartMs(value: number | undefined);
46
+ set trimEndMs(value: number | undefined);
47
+ set trimstart(value: string | undefined);
48
+ set trimend(value: string | undefined);
47
49
 
48
50
  /**
49
51
  * The source in time of the element.
@@ -64,7 +66,7 @@ export declare class TemporalMixinInterface {
64
66
  *
65
67
  * @domAttribute "sourcein"
66
68
  */
67
- get sourceInMs(): number;
69
+ get sourceInMs(): number | undefined;
68
70
 
69
71
  /**
70
72
  * The source out time of the element.
@@ -87,18 +89,22 @@ export declare class TemporalMixinInterface {
87
89
  *
88
90
  * @domAttribute "sourceout"
89
91
  */
90
- get sourceOutMs(): number;
92
+ get sourceOutMs(): number | undefined;
91
93
 
92
- set sourceInMs(value: number);
93
- set sourceOutMs(value: number);
94
- set sourcein(value: string);
95
- set sourceout(value: string);
94
+ set sourceInMs(value: number | undefined);
95
+ set sourceOutMs(value: number | undefined);
96
+ set sourcein(value: string | undefined);
97
+ set sourceout(value: string | undefined);
96
98
 
97
99
  /**
98
100
  * @domAttribute "duration"
99
101
  */
100
102
  get durationMs(): number;
101
103
 
104
+ get explicitDurationMs(): number | undefined;
105
+
106
+ get intrinsicDurationMs(): number | undefined;
107
+
102
108
  /**
103
109
  * The start time of the element within its root timegroup in milliseconds.
104
110
  *
@@ -338,109 +344,85 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
338
344
  }
339
345
  }
340
346
 
341
- private _trimStartMs = 0;
342
347
  @property({
343
348
  type: Number,
344
349
  attribute: "trimstart",
345
350
  converter: durationConverter,
346
351
  })
347
- public get trimStartMs(): number {
348
- return this._trimStartMs;
349
- }
350
- public set trimStartMs(value: number) {
351
- if (this._trimStartMs === value) {
352
- return;
352
+ _trimStartMs: number | undefined = undefined;
353
+
354
+ get trimStartMs() {
355
+ if (this._trimStartMs === undefined) {
356
+ return undefined;
353
357
  }
354
- this._trimStartMs = value;
355
- this.setAttribute(
356
- "trimstart",
357
- durationConverter.toAttribute(value / 1000),
358
+ return Math.min(
359
+ Math.max(this._trimStartMs, 0),
360
+ this.intrinsicDurationMs ?? 0,
358
361
  );
359
362
  }
360
- set trimstart(value: string | undefined) {
361
- if (value !== undefined) {
362
- this.setAttribute("trimstart", value);
363
- } else {
364
- this.removeAttribute("trimstart");
365
- }
363
+
364
+ set trimStartMs(value: number | undefined) {
365
+ this._trimStartMs = value;
366
366
  }
367
367
 
368
- private _trimEndMs = 0;
369
368
  @property({
370
369
  type: Number,
371
370
  attribute: "trimend",
372
371
  converter: durationConverter,
373
372
  })
374
- public get trimEndMs(): number {
375
- return this._trimEndMs;
376
- }
377
- public set trimEndMs(value: number) {
378
- if (this._trimEndMs === value) {
379
- return;
373
+ _trimEndMs: number | undefined = undefined;
374
+
375
+ get trimEndMs() {
376
+ if (this._trimEndMs === undefined) {
377
+ return undefined;
380
378
  }
381
- this._trimEndMs = value;
382
- this.setAttribute("trimend", durationConverter.toAttribute(value / 1000));
379
+ return Math.min(this._trimEndMs, this.intrinsicDurationMs ?? 0);
383
380
  }
384
- set trimend(value: string | undefined) {
385
- if (value !== undefined) {
386
- this.setAttribute("trimend", value);
387
- } else {
388
- this.removeAttribute("trimend");
389
- }
381
+
382
+ set trimEndMs(value: number | undefined) {
383
+ this._trimEndMs = value;
390
384
  }
391
385
 
392
- private _sourceInMs: number | undefined;
393
386
  @property({
394
387
  type: Number,
395
388
  attribute: "sourcein",
396
389
  converter: durationConverter,
397
- reflect: true,
398
390
  })
399
- get sourceInMs(): number | undefined {
400
- return this._sourceInMs;
391
+ _sourceInMs: number | undefined = undefined;
392
+
393
+ get sourceInMs() {
394
+ if (this._sourceInMs === undefined) {
395
+ return undefined;
396
+ }
397
+ return Math.max(this._sourceInMs, 0);
401
398
  }
399
+
402
400
  set sourceInMs(value: number | undefined) {
403
401
  this._sourceInMs = value;
404
- value !== undefined
405
- ? this.setAttribute(
406
- "sourcein",
407
- durationConverter.toAttribute(value / 1000),
408
- )
409
- : this.removeAttribute("sourcein");
410
- }
411
- set sourcein(value: string | undefined) {
412
- if (value !== undefined) {
413
- this.setAttribute("sourcein", value);
414
- } else {
415
- this.removeAttribute("sourcein");
416
- }
417
402
  }
418
403
 
419
- private _sourceOutMs: number | undefined;
420
404
  @property({
421
405
  type: Number,
422
406
  attribute: "sourceout",
423
407
  converter: durationConverter,
424
- reflect: true,
425
408
  })
426
- get sourceOutMs(): number | undefined {
427
- return this._sourceOutMs;
409
+ _sourceOutMs: number | undefined = undefined;
410
+
411
+ get sourceOutMs() {
412
+ if (this._sourceOutMs === undefined) {
413
+ return undefined;
414
+ }
415
+ if (
416
+ this.intrinsicDurationMs &&
417
+ this._sourceOutMs > this.intrinsicDurationMs
418
+ ) {
419
+ return this.intrinsicDurationMs;
420
+ }
421
+ return Math.max(this._sourceOutMs, 0);
428
422
  }
423
+
429
424
  set sourceOutMs(value: number | undefined) {
430
425
  this._sourceOutMs = value;
431
- value !== undefined
432
- ? this.setAttribute(
433
- "sourceout",
434
- durationConverter.toAttribute(value / 1000),
435
- )
436
- : this.removeAttribute("sourceout");
437
- }
438
- set sourceout(value: string | undefined) {
439
- if (value !== undefined) {
440
- this.setAttribute("sourceout", value);
441
- } else {
442
- this.removeAttribute("sourceout");
443
- }
444
426
  }
445
427
 
446
428
  @property({
@@ -469,28 +451,51 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
469
451
  return this._durationMs !== undefined;
470
452
  }
471
453
 
454
+ get explicitDurationMs() {
455
+ if (this.hasExplicitDuration) {
456
+ return this._durationMs;
457
+ }
458
+ return undefined;
459
+ }
460
+
472
461
  get hasOwnDuration() {
473
462
  return false;
474
463
  }
475
464
 
476
- // Defining this as a getter to a private property allows us to
477
- // override it classes that include this mixin.
465
+ get intrinsicDurationMs() {
466
+ return undefined;
467
+ }
468
+
478
469
  get durationMs() {
479
- if (this.sourceInMs) {
480
- return (
481
- this._durationMs ||
482
- this.parentTimegroup?.durationMs ||
483
- 0 - this.sourceInMs
484
- );
470
+ if (this.intrinsicDurationMs === undefined) {
471
+ return this._durationMs || this.parentTimegroup?.durationMs || 0;
485
472
  }
486
- if (this.sourceOutMs) {
487
- return (
488
- this._durationMs ||
489
- this.parentTimegroup?.durationMs ||
490
- 0 - this.sourceOutMs
491
- );
473
+
474
+ if (this.trimStartMs || this.trimEndMs) {
475
+ const trimmedDurationMs =
476
+ this.intrinsicDurationMs -
477
+ (this.trimStartMs ?? 0) -
478
+ (this.trimEndMs ?? 0);
479
+ if (trimmedDurationMs < 0) {
480
+ return 0;
481
+ }
482
+ return trimmedDurationMs;
483
+ }
484
+
485
+ if (this.sourceInMs || this.sourceOutMs) {
486
+ const sourceInMs = this.sourceInMs ?? 0;
487
+ const sourceOutMs = this.sourceOutMs ?? this.intrinsicDurationMs;
488
+ if (sourceInMs >= sourceOutMs) {
489
+ return 0;
490
+ }
491
+ return sourceOutMs - sourceInMs;
492
492
  }
493
- return this._durationMs || this.parentTimegroup?.durationMs || 0;
493
+
494
+ return this.intrinsicDurationMs;
495
+ }
496
+
497
+ get sourceStartMs() {
498
+ return this.trimStartMs ?? this.sourceInMs ?? 0;
494
499
  }
495
500
 
496
501
  get offsetMs() {
@@ -586,33 +591,8 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
586
591
  * for mapping to internal media time codes for audio/video elements.
587
592
  */
588
593
  get currentSourceTimeMs() {
589
- if (this.rootTimegroup) {
590
- if (this.sourceInMs && this.sourceOutMs) {
591
- return Math.min(
592
- Math.max(
593
- 0,
594
- this.rootTimegroup.currentTimeMs -
595
- this.startTimeMs +
596
- this.trimStartMs +
597
- this.sourceInMs,
598
- ),
599
- this.durationMs +
600
- Math.abs(this.startOffsetMs) +
601
- this.trimStartMs +
602
- this.sourceInMs,
603
- );
604
- }
605
- return Math.min(
606
- Math.max(
607
- 0,
608
- this.rootTimegroup.currentTimeMs -
609
- this.startTimeMs +
610
- this.trimStartMs,
611
- ),
612
- this.durationMs + Math.abs(this.startOffsetMs) + this.trimStartMs,
613
- );
614
- }
615
- return 0;
594
+ const leadingTrimMs = this.sourceInMs || this.trimStartMs || 0;
595
+ return this.ownCurrentTimeMs + leadingTrimMs;
616
596
  }
617
597
 
618
598
  frameTask = new Task(this, {
@@ -9,12 +9,12 @@ import { isContextMixin } from "../gui/ContextMixin.js";
9
9
  import { deepGetMediaElements } from "./EFMedia.js";
10
10
  import {
11
11
  EFTemporal,
12
- isEFTemporal,
13
12
  shallowGetTemporalElements,
14
13
  timegroupContext,
15
14
  } from "./EFTemporal.js";
16
15
  import { TimegroupController } from "./TimegroupController.js";
17
16
  import { durationConverter } from "./durationConverter.js";
17
+ import { updateAnimations } from "./updateAnimations.ts";
18
18
 
19
19
  const log = debug("ef:elements:EFTimegroup");
20
20
 
@@ -40,7 +40,8 @@ export class EFTimegroup extends EFTemporal(LitElement) {
40
40
  width: 100%;
41
41
  height: 100%;
42
42
  position: absolute;
43
- transform-origin: center center;
43
+ top: 0;
44
+ left: 0;
44
45
  }
45
46
  `;
46
47
 
@@ -121,16 +122,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
121
122
  this.wrapWithWorkbench();
122
123
  }
123
124
 
124
- // Create resize observer to handle scaling
125
- this.#resizeObserver = new ResizeObserver(() => this.updateScale());
126
- if (this.parentElement) {
127
- this.#resizeObserver.observe(this.parentElement);
128
- }
129
- this.updateScale();
130
-
131
- // Initialize animations when component is first connected
132
- // Regrettably, this doesn't work without the requestAnimationFrame
133
- requestAnimationFrame(() => this.updateAnimations());
125
+ requestAnimationFrame(() => {
126
+ this.updateAnimations();
127
+ });
134
128
  }
135
129
 
136
130
  disconnectedCallback() {
@@ -138,53 +132,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
138
132
  this.#resizeObserver?.disconnect();
139
133
  }
140
134
 
141
- private get displayedParent(): Element | null {
142
- let displayedParent = this.parentElement;
143
- while (
144
- displayedParent &&
145
- getComputedStyle(displayedParent).display === "contents"
146
- ) {
147
- displayedParent = displayedParent.parentElement;
148
- }
149
- return displayedParent;
150
- }
151
-
152
- private updateScale() {
153
- if (this.fit === "none") return;
154
-
155
- const displayedParent = this.displayedParent;
156
- if (!displayedParent) return;
157
-
158
- // Get the natural size of the content
159
- const contentWidth = this.clientWidth;
160
- const contentHeight = this.clientHeight;
161
-
162
- // Get the available space from displayed parent
163
- const containerWidth = displayedParent.clientWidth;
164
- const containerHeight = displayedParent.clientHeight;
165
-
166
- // Calculate scale ratios
167
- const widthRatio = containerWidth / contentWidth;
168
- const heightRatio = containerHeight / contentHeight;
169
-
170
- let scale: number;
171
- if (this.fit === "contain") {
172
- // Use height ratio for contain mode to ensure it fits vertically
173
- scale = heightRatio;
174
-
175
- // If width would overflow after scaling, use width ratio instead
176
- if (contentWidth * scale > containerWidth) {
177
- scale = widthRatio;
178
- }
179
- } else {
180
- // cover
181
- scale = Math.max(widthRatio, heightRatio);
182
- }
183
-
184
- // Apply transform with fixed center origin
185
- this.style.transform = `scale(${scale})`;
186
- }
187
-
188
135
  get storageKey() {
189
136
  if (!this.id) {
190
137
  throw new Error("Timegroup must have an id to use localStorage.");
@@ -192,6 +139,13 @@ export class EFTimegroup extends EFTemporal(LitElement) {
192
139
  return `ef-timegroup-${this.id}`;
193
140
  }
194
141
 
142
+ get intrinsicDurationMs() {
143
+ if (this.hasExplicitDuration) {
144
+ return this.explicitDurationMs;
145
+ }
146
+ return undefined;
147
+ }
148
+
195
149
  get durationMs() {
196
150
  switch (this.mode) {
197
151
  case "fixed":
@@ -209,7 +163,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
209
163
  case "contain": {
210
164
  let maxDuration = 0;
211
165
  for (const node of this.childTemporals) {
212
- if (node.hasOwnDuration) {
166
+ if (node.intrinsicDurationMs !== undefined) {
213
167
  maxDuration = Math.max(maxDuration, node.durationMs);
214
168
  }
215
169
  }
@@ -250,75 +204,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
250
204
  }
251
205
 
252
206
  private updateAnimations() {
253
- this.style.setProperty(
254
- "--ef-progress",
255
- `${Math.max(0, Math.min(1, this.currentTimeMs / this.durationMs)) * 100}%`,
256
- );
257
- const timelineTimeMs = (this.rootTimegroup ?? this).currentTimeMs;
258
- if (this.startTimeMs > timelineTimeMs || this.endTimeMs < timelineTimeMs) {
259
- this.style.display = "none";
260
- return;
261
- }
262
- this.style.display = "";
263
- const animations = this.getAnimations({ subtree: true });
264
- this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
265
- this.style.setProperty(
266
- "--ef-transition-duration",
267
- `${this.parentTimegroup?.overlapMs ?? 0}ms`,
268
- );
269
- this.style.setProperty(
270
- "--ef-transition-out-start",
271
- `${this.durationMs - (this.parentTimegroup?.overlapMs ?? 0)}ms`,
272
- );
273
-
274
- for (const animation of animations) {
275
- if (animation.playState === "running") {
276
- animation.pause();
277
- }
278
- const effect = animation.effect;
279
- if (!(effect && effect instanceof KeyframeEffect)) {
280
- return;
281
- }
282
- const target = effect.target;
283
- // TODO: better generalize work avoidance for temporal elements
284
- if (!target) {
285
- return;
286
- }
287
- if (target.closest("ef-timegroup") !== this) {
288
- return;
289
- }
290
-
291
- // Important to avoid going to the end of the animation
292
- // or it will reset awkwardly.
293
- if (isEFTemporal(target)) {
294
- const timing = effect.getTiming();
295
- const duration = Number(timing.duration) ?? 0;
296
- const delay = Number(timing.delay);
297
- const newTime = Math.floor(
298
- Math.min(target.ownCurrentTimeMs, duration - 1 + delay),
299
- );
300
- if (Number.isNaN(newTime)) {
301
- return;
302
- }
303
- animation.currentTime = newTime;
304
- } else if (target) {
305
- const nearestTimegroup = target.closest("ef-timegroup");
306
- if (!nearestTimegroup) {
307
- return;
308
- }
309
- const timing = effect.getTiming();
310
- const duration = Number(timing.duration) ?? 0;
311
- const delay = Number(timing.delay);
312
- const newTime = Math.floor(
313
- Math.min(nearestTimegroup.ownCurrentTimeMs, duration - 1 + delay),
314
- );
315
-
316
- if (Number.isNaN(newTime)) {
317
- return;
318
- }
319
- animation.currentTime = newTime;
320
- }
321
- }
207
+ updateAnimations(this);
322
208
  }
323
209
 
324
210
  get contextProvider() {
@@ -366,10 +252,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
366
252
  workbench.append(filmstrip);
367
253
  }
368
254
 
369
- get hasOwnDuration() {
370
- return true;
371
- }
372
-
373
255
  get efElements() {
374
256
  return Array.from(
375
257
  this.querySelectorAll(
@@ -20,7 +20,9 @@ export class EFVideo extends TWMixin(EFMedia) {
20
20
  ];
21
21
  canvasRef = createRef<HTMLCanvasElement>();
22
22
  render() {
23
- return html` <canvas ${ref(this.canvasRef)}></canvas>`;
23
+ return html`
24
+ <canvas ${ref(this.canvasRef)}></canvas>
25
+ `;
24
26
  }
25
27
 
26
28
  get canvasElement() {
@@ -61,6 +61,9 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
61
61
  @property({ type: String, reflect: true })
62
62
  target = "";
63
63
 
64
+ @property({ type: Number, attribute: "bar-spacing" })
65
+ barSpacing = 0.5;
66
+
64
67
  @state()
65
68
  targetElement: EFAudio | EFVideo | null = null;
66
69
 
@@ -154,7 +157,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
154
157
  const waveHeight = canvas.height;
155
158
 
156
159
  const totalBars = frequencyData.length;
157
- const paddingInner = 0.5;
160
+ const paddingInner = this.barSpacing;
158
161
  const paddingOuter = 0.01;
159
162
  const availableWidth = waveWidth;
160
163
  const barWidth =
@@ -213,7 +216,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
213
216
 
214
217
  // Similar padding calculation as drawBars
215
218
  const totalBars = frequencyData.length;
216
- const paddingInner = 0.5;
219
+ const paddingInner = this.barSpacing;
217
220
  const paddingOuter = 0.01;
218
221
  const availableWidth = waveWidth;
219
222
  const barWidth =
@@ -1,19 +1,24 @@
1
1
  import { parseTimeToMs } from "./parseTimeToMs.js";
2
2
 
3
3
  export const durationConverter = {
4
- fromAttribute: (value: string): number => parseTimeToMs(value),
5
- toAttribute: (value: number) => `${value}s`,
4
+ fromAttribute: (value: string | null) =>
5
+ value === null ? null : parseTimeToMs(value),
6
+ toAttribute: (value: number | null) => (value === null ? null : `${value}s`),
6
7
  };
7
8
 
8
9
  const positiveDurationConverter = (error: string) => {
9
10
  return {
10
- fromAttribute: (value: string): number => {
11
+ fromAttribute: (value: string | null): number | null => {
12
+ if (value === null) {
13
+ return null;
14
+ }
11
15
  if (value.startsWith("-")) {
12
16
  throw new Error(error);
13
17
  }
14
18
  return parseTimeToMs(value);
15
19
  },
16
- toAttribute: (value: number) => `${value}s`,
20
+ toAttribute: (value: number | null) =>
21
+ value === null ? null : `${value}s`,
17
22
  };
18
23
  };
19
24
 
@@ -0,0 +1,88 @@
1
+ import { isEFTemporal } from "./EFTemporal.ts";
2
+ import type { EFTimegroup } from "./EFTimegroup.ts";
3
+
4
+ export const updateAnimations = (
5
+ element: HTMLElement & {
6
+ currentTimeMs: number;
7
+ durationMs: number;
8
+ rootTimegroup?: EFTimegroup;
9
+ parentTimegroup?: EFTimegroup;
10
+ startTimeMs: number;
11
+ endTimeMs: number;
12
+ },
13
+ ) => {
14
+ element.style.setProperty(
15
+ "--ef-progress",
16
+ `${Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs)) * 100}%`,
17
+ );
18
+ const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;
19
+ if (
20
+ element.startTimeMs > timelineTimeMs ||
21
+ element.endTimeMs < timelineTimeMs
22
+ ) {
23
+ element.style.display = "none";
24
+ return;
25
+ }
26
+ element.style.display = "";
27
+ const animations = element.getAnimations({ subtree: true });
28
+ element.style.setProperty("--ef-duration", `${element.durationMs}ms`);
29
+ element.style.setProperty(
30
+ "--ef-transition-duration",
31
+ `${element.parentTimegroup?.overlapMs ?? 0}ms`,
32
+ );
33
+ element.style.setProperty(
34
+ "--ef-transition-out-start",
35
+ `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`,
36
+ );
37
+
38
+ for (const animation of animations) {
39
+ if (animation.playState === "running") {
40
+ animation.pause();
41
+ }
42
+ const effect = animation.effect;
43
+ if (!(effect && effect instanceof KeyframeEffect)) {
44
+ continue;
45
+ }
46
+ const target = effect.target;
47
+ // TODO: better generalize work avoidance for temporal elements
48
+ if (!target) {
49
+ continue;
50
+ }
51
+ if (target.closest("ef-timegroup") !== element) {
52
+ continue;
53
+ }
54
+
55
+ const timing = effect.getTiming();
56
+ const duration = Number(timing.duration) ?? 0;
57
+ const delay = Number(timing.delay) ?? 0;
58
+ const iterations = Number(timing.iterations) ?? 1;
59
+
60
+ const timeTarget = isEFTemporal(target)
61
+ ? target
62
+ : target.closest("ef-timegroup");
63
+ if (!timeTarget) {
64
+ continue;
65
+ }
66
+
67
+ const currentTime = timeTarget.ownCurrentTimeMs;
68
+
69
+ // Handle delay - don't start animation until delay is complete
70
+ if (currentTime < delay) {
71
+ animation.currentTime = 0;
72
+ continue;
73
+ }
74
+
75
+ const currentIteration = Math.floor((currentTime - delay) / duration);
76
+ const currentIterationTime = (currentTime - delay) % duration;
77
+
78
+ if (currentIteration >= iterations) {
79
+ // Stop just before the end to prevent DOM removal
80
+ animation.currentTime = duration - 0.01;
81
+ continue;
82
+ }
83
+
84
+ // Ensure we never reach exactly duration
85
+ animation.currentTime =
86
+ Math.min(currentIterationTime, duration - 0.01) + delay;
87
+ }
88
+ };