Animation

Driving props from the UI thread with shared values.

App.tsx
1import { useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'
2import { Canvas, Circle } from 'react-native-tgfx'
3
4export function Pulse({ rows }) {
5 const r = useSharedValue(48)
6 useEffect(() => {
7 const grow = withTiming(96, { duration: 800 })
8 r.value = withRepeat(grow, -1, true)
9 sortAll(rows) // blocks the JS thread
10 }, [rows])
11
12 return (
13 <Canvas style={{ flex: 1 }}>
14 <Circle cx={201} cy={236} r={r}
15 color="#7c3aed" />
16 </Canvas>
17 )
18}
9:41

Props typed Animated<T> accept a Reanimated SharedValue (SharedValueLike) and update on the UI thread, with no re-render.

import { useEffect } from 'react'
import { useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'
import { Canvas, Circle } from 'react-native-tgfx'

function Pulse() {
  const r = useSharedValue(20)

  useEffect(() => {
    r.value = withRepeat(withTiming(60, { duration: 900 }), -1, true)
  }, [r])

  return (
    <Canvas style={{ width: 200, height: 200 }}>
      <Circle cx={100} cy={100} r={r} color="#d98a4a" />
    </Canvas>
  )
}

Animated colour

Animate colour channels inside a ColorComponents tuple. A SharedValue holding a colour string or number does not animate.

<Rect x={0} y={0} width={40} height={40} color={[red, 0.2, 0.4, 1]} />

Clock

const t = useClock()
<Shader source={source} uniforms={{ uTime: t, uSize: [300, 300] }} />

One opacity per chain

Animate one opacity per chain. Two opacities multiply and freeze:

[tgfx] two opacities in one chain, at least one animated: a draw command has a
single alpha slot, so their product is frozen at the last commit. Animate one
opacity per chain.

Zoom and pan

Pass a SharedValue<Viewport> as viewport to zoom and pan from the UI thread, with no React render or scene re-upload.

import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import { useSharedValue } from 'react-native-reanimated'
import { Canvas, Circle } from 'react-native-tgfx'

function Map() {
  const viewport = useSharedValue({ zoom: 1, x: 0, y: 0 })

  // A worklet end to end: no runOnJS, no React render, no scene re-encode.
  const pan = Gesture.Pan().onChange(e => {
    const at = viewport.value
    viewport.value = {
      zoom: at.zoom,
      x: at.x + e.changeX,
      y: at.y + e.changeY,
    }
  })

  return (
    <GestureDetector gesture={pan}>
      <View style={{ height: 200 }} collapsable={false}>
        <Canvas viewport={viewport} style={{ flex: 1 }}>
          <Circle cx={100} cy={100} r={40} color="#d98a4a" />
        </Canvas>
      </View>
    </GestureDetector>
  )
}

Write the whole object at once. zoom scales about the origin, then x/y pan. To pinch about the focal point:

const pinch = Gesture.Pinch().onChange(e => {
  const at = viewport.value
  const next = Math.max(0.5, Math.min(8, at.zoom * e.scaleChange))
  const sceneX = (e.focalX - at.x) / at.zoom
  const sceneY = (e.focalY - at.y) / at.zoom
  viewport.value = {
    zoom: next,
    x: e.focalX - next * sceneX,
    y: e.focalY - next * sceneY,
  }
})

Without Reanimated, call CanvasHandle.setViewport() from a JS gesture callback. Headless, use surface.setViewport([zoom, x, y]).

hitTest and snapshot() follow the viewport. <CameraFrame> does not; wrap it in a <Group transform> to zoom it. Use the viewport prop or setViewport, not both. Invalid values are ignored.

Without Reanimated

useNode works from plain JS handlers. With Reanimated, writes also run in worklets (Imperative), and createScene builds scenes on a worklet runtime.

On this page