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.