Headless
Build, render and export scenes without React.
Everything on this page runs without React.
Build and export
import { Scene, image, loadImage, rect } from "react-native-tgfx";
const photo = await loadImage("https://picsum.photos/id/237/300/200");
const scene = Scene.create({ width: 320, height: 420 });
scene.add(rect({ x: 0, y: 0, width: 320, height: 420, color: "#101725" }));
scene.add(rect({ x: 10, y: 10, width: 300, height: 60, color: "#ff2d55" }));
scene.add(
image({ image: photo, x: 10, y: 90, width: 300, height: 200, fit: "cover" })
);
await scene.save(`${dir}/card.png`, { pixelRatio: 2 });
// written natively; bytes never enter JSEvery component has a factory; see Imperative.
scene.save picks the format from the extension, or from format. toImage
returns a TgfxImage. toSvg and toPdf export vectors:
const still = await scene.toImage(); // draw it back, encode it, share it
const markup = await scene.toSvg();
await scene.toPdf(`${dir}/card.pdf`);Render size comes from width/height, else Scene.create, else one
attached surface. With none, it throws.
Mutate without re-encoding
add, insert, remove and replaceChildren batch per microtask. Set
values through handles:
const counter = createHandle();
scene.add(text({ ref: counter, x: 16, y: 40, text: "Day 1", fontSize: 18 }));
for (const n of [1, 2, 3]) {
counter.set({ text: `Day ${n}` });
const img = await renderToImage(scene, { width: 400, height: 400 });
await img.save(`${dir}/day-${n}.png`);
img.dispose();
}Use handles for tens of nodes. For pools, use atlas with
createSpriteBuffer or vertices with
createVertexBuffer.
Repeated renders: the retained surface
const surface = createOffscreenSurface({ width: 1024, height: 1024 });
scene.attach(surface); // a surface is a target, like a view
for (let f = 0; f < 120; f++) {
t.set({ x: f * 4 }); // slot write
surface.flush(); // render on the offscreen worker
if (f % 30 === 0) await surface.save(`${dir}/frame-${f}.png`);
}
surface.dispose();See Export for the full surface API.
Presses without a canvas
import { dispatchPress } from "react-native-tgfx";
dispatchPress(scene.handlesAt(surface.hitTest(x, y, false)), [
[box, () => select(box)],
]);Topmost wins, as with useCanvasPress.
Clocks, video and camera, callable
These hooks have callable forms:
const clock = createClock(); // seconds on the UI thread; clock.value → a uniform
clock.stop();
const player = await createVideoPlayer(require("./clip.mp4"), {
looping: true,
});
// currentFrame is null until the first decode and is read once at add time -
// build the node when the frame exists:
player.onFrame((frame) =>
scene.add(image({ image: frame, x: 0, y: 0, width: 1280, height: 720 }))
);
player.setOptions({ paused: true });
player.dispose();
const sink = createCameraFrameSink({ onImage: (img) => attachToScene(img) });
// VisionCamera side: useFrameProcessor((f) => { 'worklet'; sink.push(f) }, [sink])
sink.dispose();Worklets
Scenes also work on worklet runtimes:
const surface = createOffscreenSurface({ width: 1024, height: 1024 });
const boxed = NitroModules.box(surface);
scheduleOnUI(() => {
"worklet";
const scene = createScene();
scene.add(rect({ x: 0, y: 0, width: 512, height: 512, color: "#101725" }));
scene.attach(boxed.unbox());
scene.flush();
boxed.unbox().flush();
// The runtime that made the scene owns it; its global keeps it alive
// across worklet invocations. A SharedValue would clone it instead.
globalThis.myScene = scene;
});On a worklet:
- Use
createScene()/sceneFrom(), notScene.create. - Call
scene.flush()after structural changes. - Exports throw. Call
surface.save/surface.toImagefrom the JS thread. - Handles and mapper-driven animation stay on the JS thread. Use
surface.setValuesfor per-frame motion.
With Reanimated, handle.set() and SharedValue props repaint a JS-thread
Scene on their own. Box a surface with NitroModules.box() to use it in a
worklet, and don't destructure its methods.
Without Reanimated
Factories, Scene, exports, encode/save and the offscreen surface work
without Reanimated. createClock, createVideoPlayer and
createCameraFrameSink throw without it.