Scripting

Use addFrameTask to animate elements programmatically on a per-frame basis.

addFrameTask

addFrameTask is an instance method on every ef-timegroup element. Register a callback and it runs on every captured frame.

Signature

timegroupElement.addFrameTask(callback: FrameTaskCallback): () => void

Returns a cleanup function that removes the callback when called.

interface FrameTaskCallback {
  (info: {
    ownCurrentTimeMs: number;  // ms elapsed since this group's own start
    currentTimeMs: number;     // ms elapsed in the global composition
    durationMs: number;        // total duration of this group
    percentComplete: number;   // ownCurrentTimeMs / durationMs, clamped 0–1
    element: EFTimegroup;      // the timegroup element itself
  }): void | Promise<void>;
}

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 prime timeline and every render clone. All addFrameTask calls must live inside an initializer.

<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 = percentComplete;
    });
  };
</script>

Query elements via the instance argument — not document.getElementById — since render clones are independent documents.

The initializer must be synchronous. Async initializers throw at runtime.

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

ownCurrentTimeMs is always relative to the group's own start time, not the global composition clock. A group starting at 2s into the composition reports ownCurrentTimeMs = 0 at that moment. Use currentTimeMs if you need the global clock.

Frame task callbacks can be synchronous or async. In preview, slow tasks cause frames to be dropped to maintain playback. In rendered output, every frame waits for all tasks to complete — no frames are dropped — but tasks that take longer than 5 seconds will time out.

React

In React, pass setup logic as a function prop — there is no initializer property to set directly. Use a useEffect that runs once after the element connects:

import { useRef, useEffect } from "react";
import { Timegroup, Text } from "@editframe/react";
import type { EFTimegroup } from "@editframe/elements";

function FadeIn() {
  const rootRef = useRef<EFTimegroup>(null);

  useEffect(() => {
    const root = rootRef.current;
    if (!root) return;

    root.initializer = (instance) => {
      const headline = instance.querySelector<HTMLElement>("#headline");
      instance.addFrameTask(({ percentComplete }) => {
        if (headline) headline.style.opacity = String(percentComplete);
      });
    };
  }, []);

  return (
    <Timegroup ref={rootRef} 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>
  );
}