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 JS

Every 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(), not Scene.create.
  • Call scene.flush() after structural changes.
  • Exports throw. Call surface.save / surface.toImage from the JS thread.
  • Handles and mapper-driven animation stay on the JS thread. Use surface.setValues for 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.