addFrameTask
addFrameTask is an instance method on every ef-timegroup element. Register a callback and it runs when that group presents a frame during playback, seeking, or export.
Signature
timegroupElement.addFrameTask(callback: FrameTaskCallback): () => voidReturns a cleanup function that removes the callback when called.
interface FrameTaskCallback {
(info: {
ownCurrentTime: number; // seconds elapsed since this group's start
currentTime: number; // same local seconds as ownCurrentTime
duration: number; // total duration of this group in seconds
percentComplete: number; // ownCurrentTime / duration
element: EFTimegroupElement;
}): void | Promise<void>;
}The legacy ownCurrentTimeMs, currentTimeMs, and durationMs fields are also available in milliseconds. New code should use the seconds fields.
initializer
When rendering, Editframe creates isolated render clones of the root ef-timegroup. Any addFrameTask call in an inline <script> runs once on the live preview but never reaches those clones.
The initializer property solves this. Assign a synchronous callback to it and it runs on both the live timeline and every render clone. Put imperative addFrameTask setup inside an initializer so cloned compositions receive it too. In React, a TimelineRoot also remounts hooks such as useTimingInfo for each clone.
<ef-timegroup id="root" duration="3s" class="w-[1920px] h-[1080px] bg-black flex items-center justify-center">
<ef-text id="headline" class="text-white text-8xl font-bold" style="opacity: 0;">
Fade In
</ef-text>
</ef-timegroup>
<script type="module">
const root = document.getElementById("root");
root.initializer = (instance) => {
const headline = instance.querySelector("#headline");
instance.addFrameTask(({ percentComplete }) => {
headline.style.opacity = String(percentComplete);
});
};
</script>Query elements via the instance argument, rather than document.getElementById: render clones have their own element trees and can contain the same IDs as the live composition.
The initializer must be synchronous. Async initializers throw at runtime. Keep it brief: it registers work rather than running an animation loop.
Example: typewriter effect
<ef-timegroup id="root" duration="3s" class="w-[1920px] h-[1080px] bg-black flex items-center justify-center">
<ef-text id="typewriter" class="text-white text-6xl font-mono"></ef-text>
</ef-timegroup>
<script type="module">
const root = document.getElementById("root");
const fullText = "Hello, world.";
root.initializer = (instance) => {
const el = instance.querySelector("#typewriter");
instance.addFrameTask(({ percentComplete }) => {
el.textContent = fullText.slice(0, Math.floor(percentComplete * fullText.length));
});
};
</script>Example: counter
<ef-timegroup id="root" duration="2s" class="w-[1920px] h-[1080px] bg-black flex items-center justify-center">
<ef-text id="counter" class="text-white text-9xl font-bold tabular-nums">0</ef-text>
</ef-timegroup>
<script type="module">
const root = document.getElementById("root");
const target = 1000000;
function easeInOut(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
root.initializer = (instance) => {
const counter = instance.querySelector("#counter");
instance.addFrameTask(({ percentComplete }) => {
counter.textContent = Math.round(easeInOut(percentComplete) * target).toLocaleString();
});
};
</script>Targeting individual elements
ownCurrentTime and currentTime are both relative to the group's own start. A group starting at 2s in the composition reports 0 at that moment. The corresponding *Ms fields use the same local clock in milliseconds. Register a task on the root timegroup when you need root composition time.
React
In React, pass initializer as a function prop. Use TimelineRoot around the composition when exporting so its setup is remounted in each render clone:
import { TimelineRoot, Timegroup, Text } from "@editframe/react";
import type { EFTimegroupElement } from "@editframe/elements";
const initializeFade = (instance: EFTimegroupElement) => {
const headline = instance.querySelector<HTMLElement>("#headline");
instance.addFrameTask(({ percentComplete }) => {
if (headline) headline.style.opacity = String(percentComplete);
});
};
function FadeIn({ id }: { id: string }) {
return (
<Timegroup id={id} initializer={initializeFade} duration="3s" className="w-[1920px] h-[1080px] bg-black flex items-center justify-center">
<Text id="headline" className="text-white text-8xl font-bold" style={{ opacity: 0 }}>
Fade In
</Text>
</Timegroup>
);
}
export default function App() {
return <TimelineRoot id="root" component={FadeIn} />;
}