Imperative

Node handles, slot writes and buffers.

App.tsx
1import { Canvas, Circle, Line, useNode } from 'react-native-tgfx'
2
3export function Tether() {
4 const ball = useNode()
5 const cord = useNode()
6 const pan = Gesture.Pan().onUpdate((e) => {
7 'worklet'
8 ball.set({ cx: e.x, cy: e.y })
9 cord.set({ x2: e.x, y2: e.y })
10 })
11
12 return (
13 <GestureDetector gesture={pan}>
14 <Canvas style={{ flex: 1 }}>
15 <Line ref={cord} x1={201} y1={0}
16 x2={201} y2={170} strokeWidth={4}
17 color="#a1a1aa" />
18 <Circle ref={ball} cx={201} cy={170}
19 r={44} color="#7c3aed" />
20 </Canvas>
21 </GestureDetector>
22 )
23}
9:41

useNode returns a NodeHandle. set() updates a mounted node without a React render.

import { Canvas, RoundedRect, useNode } from 'react-native-tgfx'

function Box() {
  const box = useNode()

  return (
    <>
      <Canvas style={{ width: 300, height: 200 }}>
        <RoundedRect ref={box} x={20} y={20} width={70} height={50} r={12} color="#4F8EF7" />
      </Canvas>
      <Button title="move" onPress={() => box.set({ x: 120 })} />
    </>
  )
}

Colour from a worklet

box.set({ color: '#4F8EF7' })         // JS thread only - the parser lives there
box.set({ color: 0xff4f8ef7 })        // packed AARRGGBB, worklet-safe
box.set({ color: [0.3, 0.5, 1, 1] })  // r, g, b, a in 0..1, worklet-safe

Gestures

const pan = Gesture.Pan().onUpdate((e) => {
  'worklet'
  dot.set({ cx: e.x, cy: e.y })
})

To drive the canvas viewport from a gesture, pass it a SharedValue<Viewport>:

const viewport = useSharedValue({ zoom: 1, x: 0, y: 0 })
const zoom = Gesture.Pinch().onChange((e) => {
  const at = viewport.value
  viewport.value = { ...at, zoom: at.zoom * e.scaleChange }
})
// <Canvas viewport={viewport}>

CanvasHandle.setViewport() does the same from JS.

What has slots

Geometry, transforms, paint, images, uniforms, gradients, blur, shadow, alpha threshold, backdrop, film grain, text, paths and visible patch in place. Other props re-encode. Max 16 gradient stops and 32 uniform floats.

fx.set({ uniforms: { uFace: [face.x, face.y], uRadius: face.width / 2 } })
label.set({ text: String(Math.round(fps.value)) })
sprite.set({ rect: { x: frame * 32, y: 0, width: 32, height: 32 } })
fade.set({ end: { x: drag.x, y: 0 }, positions: [0, t] })
glow.set({ blur: 4 + press.value * 12, dy: 2 + press.value * 6 })

Pools

createHandle is useNode without the hook. visible={false} skips drawing and hit testing.

const [dots] = useState(() => Array.from({ length: 64 }, () => createHandle()))

<Canvas>
  {dots.map((ref, i) => (
    <Circle key={i} ref={ref} cx={0} cy={0} r={4} visible={false} color="#fff" />
  ))}
</Canvas>

dots[next].set({ visible: true, cx: x, cy: y })

Scenes without React

Each component has a factory (rect, circle, group, linearGradient, mask and so on). A Scene draws a tree of them without React. Both forms compile the same program:

import { Canvas, Circle, Group, LinearGradient, Rect, useNode } from 'react-native-tgfx'

function Scene() {
  const box = useNode()
  return (
    <Canvas style={{ flex: 1 }}>
      <Rect ref={box} x={0} y={0} width={40} height={40} color="tomato" />
      <Circle cx={100} cy={100} r={20}>
        <LinearGradient start={{ x: 80, y: 80 }} end={{ x: 120, y: 120 }} colors={['#000', '#fff']} />
      </Circle>
      <Group transform={[{ rotate: 0.3 }]}>
        <Rect x={-20} y={-20} width={40} height={40} color="gold" />
      </Group>
    </Canvas>
  )
}

// later, from JS or a worklet - no React render
box.set({ x: 120 })

add, insert, remove and replaceChildren batch into one compile per microtask; flush() commits now. Update values through handles. One scene can drive several targets.

Where slot writes go

set() writes shared native memory and allocates nothing. Last write wins until the next React commit.

Build a scene in a worklet with createScene() (Worklets).

Buffers

useSpriteBuffer and useVertexBuffer share a typed array with the renderer (SpriteBuffer, VertexBuffer).

const sprites = useSpriteBuffer(512)   // SPRITE_FLOATS per sprite
const count = useSharedValue(0)

<Atlas image={sheet} buffer={sprites} count={count} />

// per frame, in a worklet: srcX, srcY, srcW, srcH, scale, centreX, centreY
sprites.set(i, 0, 0, 16, 16, p.scale, p.x, p.y)
count.value = alive                    // set the count after filling entries

The canvas redraws only when a bound shared value changes. For a constant count, publish count.value = N + (frame.timestamp % 1000) * 1e-6 (truncated natively).

For speed, write the Float32Array from spriteFloats:

const f = spriteFloats(sprites)        // SPRITE_FLOATS per sprite
for (let i = 0; i < alive; i++) {
  const o = i * SPRITE_FLOATS
  f[o + 4] = p[i].scale
  f[o + 5] = p[i].x
  f[o + 6] = p[i].y
}

useVertexBuffer takes a VertexBufferOptions. Write its positions/textures/colors/indices views from JS:

const mesh = useVertexBuffer({ maxVertices: 4, maxIndices: 6 })
mesh.positions.set([0, 0, 200, 0, 0, 200, 200, 200])
mesh.indices.set([0, 1, 2, 1, 2, 3])

<Vertices buffer={mesh} vertexCount={4} indexCount={6} />

In a worklet, write through meshViews:

const drag = Gesture.Pan().onUpdate((e) => {
  'worklet'
  meshViews(mesh).positions[0] = e.x
})

On this page