Effing 0.38.0: cache me if you can
Effing 0.38.0 adds a persistent image cache for per-frame rendering, plus two fixes: user-space SVG gradients and jitter-free slide motion.
Effing 0.38.0 is out. Here’s what’s new and what got fixed.
The image cache
The headline is a new imageCache option on renderReactElement, the
@effing/canvas function that turns a JSX tree into pixels.
By default, every call to renderReactElement creates its own internal image
cache. That’s fine when you render one image. But an annie often renders the
same component once per frame, and if that component has an <img> or a
background-image: url(...), each frame re-fetches and re-decodes that source
from scratch. You can work around it by fetching the image yourself, generating
a data URL, and using that in the JSX, but that’s rather tedious.
Now you can pass in a cache you own, so each source loads once and every frame after the first reuses it:
import { renderReactElement, type ImageCache } from "@effing/canvas";
const imageCache: ImageCache = new Map();
for (let frame = 0; frame < frameCount; frame++) {
await renderReactElement(ctx, <Frame n={frame} />, { fonts, imageCache });
}
ImageCache is just a Map, exported for convenience. Two things worth
knowing:
- Concurrent renders share in-flight work. Cache entries are load promises, not resolved images, so two renders that ask for the same source at the same time share a single fetch instead of downloading it twice. You can keep one long-lived cache for your whole render pool.
- Failed loads don’t poison the cache. A transient network error used to get cached as a rejection, so one blip could break that source for the life of the cache. Now failures are evicted and the next frame tries again.
Sometimes the React tree isn’t the right place for an image, like a full-screen
photo background under animated text. For those, the manual’s “Creating Annies”
section describes the load-once pattern: pre-load with loadImage(), draw it
yourself with ctx.drawImage, and keep the per-frame JSX to text and vectors.
Gradients from your design tool
When rendering SVG, @effing/canvas was treating every gradient’s coordinates
as objectBoundingBox fractions, the 0-to-1 kind. But Figma and Illustrator export
gradients with gradientUnits="userSpaceOnUse", where the coordinates are real
positions in the SVG’s own space. Read as fractions, they clamped to the first
stop, so the gradient rendered as a single flat color.
0.38.0 honors userSpaceOnUse for both linear and radial gradients, on fills
and strokes, with percentages resolved against the viewport per the SVG spec.
Exported icons now render their gradients correctly.
No more slide jitter, FFS
In @effing/ffs, the FFmpeg render service, layers animated with slide motion
could pick up a ±2px wobble on individual frames.
It came from two things interacting. FFmpeg’s overlay filter snaps x/y down to
even values for yuv420p chroma alignment, and the unrounded position expressions
carried enough floating-point noise that a value like 719.9999999 and a clean
720 could land on opposite sides of that boundary from one frame to the next.
The motion coordinate expressions for slide, bounce, and shake are now wrapped
in round(), so positions hit exact integers before the snap.
Upgrading
Bump your @effing/* packages to 0.38.0. It’s a backward-compatible upgrade
and the imageCache option is additive. Per-package details are in the
canvas changelog
and the ffs changelog.