Skip to main content

Animation

Animated image content (GIFs and other animated sources) is handled by a pluggable adapter registry in @oh-just-another/renderer-core. An adapter decodes frames and answers the stateless "what does this frame look like at time t?" question; live playback ticking is the host's job. <Editor> ships a GIF adapter and installs it by default.

Installing the GIF adapterโ€‹

@oh-just-another/editor exports installGifAnimationAdapter. The <Editor> component installs it automatically, but you can call it yourself for headless use or to opt in without the component.

import { installGifAnimationAdapter } from "@oh-just-another/editor";

installGifAnimationAdapter();

Registering a custom adapterโ€‹

Register your own adapter with registerAnimationAdapter. The registry is indexed by the adapter's kind ("gif", "lottie", "video", โ€ฆ); an ImageElement carrying animationKind + animationData resolves its frames through the matching adapter, otherwise its static src is used.

import {
registerAnimationAdapter,
type AnimatedSourceAdapter,
} from "@oh-just-another/renderer-core";

const myAdapter: AnimatedSourceAdapter<MyData> = {
kind: "my-format",
getFrameAt(data, timestampMs) {
return decodedFrameFor(data, timestampMs); // passed to target.drawImage
},
totalDurationMs: (data) => data.durationMs, // optional; unset = tick forever
};
registerAnimationAdapter(myAdapter);

registerAnimationAdapter is also re-exported from @oh-just-another/editor so the umbrella package is a single import for extending the editor.

Driving the clockโ€‹

Playback time comes from a pluggable AnimationClock โ€” a function (shape) => number (ms), defaulting to performance.now(). setAnimationClock / resetAnimationClock replace or restore the process-global fallback clock โ€” useful for deterministic tests and headless export of a specific frame. Per-render, prefer RenderSceneOptions.clock, which overrides the global clock for that renderScene call.

Adapters decode asynchronously; notifyAnimationContentReady / onAnimationContentReady let the host schedule one redraw once frames are decoded, so a paused shape paints instead of staying blank.

import {
setAnimationClock,
resetAnimationClock,
onAnimationContentReady,
} from "@oh-just-another/renderer-core";

const off = onAnimationContentReady(() => requestRedraw());
setAnimationClock(() => fixedTimeMs);
// later
resetAnimationClock();
off();

Use resolveImageSource and listAnimationKinds to inspect which kinds have registered adapters at runtime.