Skip to main content
The Atlas component is used for efficient rendering of multiple instances of the same texture or image. It’s especially useful for drawing a large number of similar objects, like sprites or tiles, with varying transformations. Atlas transforms can be animated with near-zero cost using worklets, making it ideal for tile-based maps, sprite animations, and scenarios with many instances of similar textures.

Properties

NameTypeDescription
imageSkImage or nullImage containing the sprites
spritesSkRect[]Locations of sprites in the atlas
transformsRSXform[]Rotation/scale transforms for each sprite
colors?SkColor[]Optional colors to blend with sprites
blendMode?BlendModeBlend mode for sprites and colors
sampling?SamplingSampling method for the image

RSXform

The RSXform object is a compressed transformation matrix: [fSCos -fSSin fTx, fSSin fSCos fTy, 0, 0, 1]. Useful transformations:
import { Skia } from "@shopify/react-native-skia";

// 1. Identity (no transformation)
let rsxForm = Skia.RSXform(1, 0, 0, 0);

// 2. Scale by 2 and translate by (50, 100)
rsxForm = Skia.RSXform(2, 0, 50, 100);

// 3. Rotate by PI/4, translate by (50, 100)
const r = Math.PI/4;
rsxForm = Skia.RSXform(Math.cos(r), Math.sin(r), 50, 100);

// 4. Scale by 2, rotate by PI/4 with pivot (25, 25)
rsxForm = Skia.RSXformFromRadians(2, r, 0, 0, 25, 25);

// 5. Translate by (125, 0), rotate by PI/4 with pivot (125, 25)
rsxForm = Skia.RSXformFromRadians(1, r, 100, 0, 125, 25);

Hello World

In this example, we draw a rectangle as an image, then display it 150 times with transformations:
import { 
  Skia, 
  drawAsImage, 
  Group, 
  Rect, 
  Canvas, 
  Atlas, 
  rect 
} from "@shopify/react-native-skia";

const size = { width: 25, height: 11.25 };
const strokeWidth = 2;
const imageSize = {
  width: size.width + strokeWidth,
  height: size.height + strokeWidth,
};

const image = await drawAsImage(
  <Group>
    <Rect
      rect={rect(
        strokeWidth / 2, 
        strokeWidth / 2, 
        size.width, 
        size.height
      )}
      color="cyan"
    />
    <Rect
      rect={rect(
        strokeWidth / 2, 
        strokeWidth / 2, 
        size.width, 
        size.height
      )}
      color="blue"
      style="stroke"
      strokeWidth={strokeWidth}
    />
  </Group>,
  imageSize
);

export const Demo = () => {
  const numberOfBoxes = 150;
  const pos = { x: 128, y: 128 };
  const width = 256;
  
  const sprites = new Array(numberOfBoxes)
    .fill(0)
    .map(() => rect(0, 0, imageSize.width, imageSize.height));
    
  const transforms = new Array(numberOfBoxes).fill(0).map((_, i) => {
    const tx = 5 + ((i * size.width) % width);
    const ty = 25 + Math.floor(i / (width / size.width)) * size.width;
    const r = Math.atan2(pos.y - ty, pos.x - tx);
    return Skia.RSXform(Math.cos(r), Math.sin(r), tx, ty);
  });

  return (
    <Canvas style={{ flex: 1 }}>
      <Atlas image={image} sprites={sprites} transforms={transforms} />
    </Canvas>
  );
};

Animations

The Atlas component should typically be used with Reanimated. Use useTexture to create textures on the UI thread and hooks like useRectBuffer and useRSXformBuffer to efficiently animate sprites and transformations.
import {
  Skia,
  drawAsImage,
  Group,
  Rect,
  Canvas,
  Atlas,
  rect,
  useTexture,
  useRSXformBuffer,
} from "@shopify/react-native-skia";
import { useSharedValue } from "react-native-reanimated";
import { GestureDetector, Gesture } from "react-native-gesture-handler";

const size = { width: 25, height: 11.25 };
const strokeWidth = 2;
const textureSize = {
  width: size.width + strokeWidth,
  height: size.height + strokeWidth,
};

export const Demo = () => {
  const pos = useSharedValue({ x: 0, y: 0 });
  
  const texture = useTexture(
    <Group>
      <Rect
        rect={rect(
          strokeWidth / 2,
          strokeWidth / 2,
          size.width,
          size.height
        )}
        color="cyan"
      />
      <Rect
        rect={rect(
          strokeWidth / 2,
          strokeWidth / 2,
          size.width,
          size.height
        )}
        color="blue"
        style="stroke"
        strokeWidth={strokeWidth}
      />
    </Group>,
    textureSize
  );
  
  const gesture = Gesture.Pan().onChange((e) => (pos.value = e));
  
  const numberOfBoxes = 150;
  const width = 256;
  const sprites = new Array(numberOfBoxes)
    .fill(0)
    .map(() => rect(0, 0, textureSize.width, textureSize.height));

  const transforms = useRSXformBuffer(numberOfBoxes, (val, i) => {
    "worklet";
    const tx = 5 + ((i * size.width) % width);
    const ty = 25 + Math.floor(i / (width / size.width)) * size.width;
    const r = Math.atan2(pos.value.y - ty, pos.value.x - tx);
    val.set(Math.cos(r), Math.sin(r), tx, ty);
  });

  return (
    <GestureDetector gesture={gesture}>
      <Canvas style={{ flex: 1 }}>
        <Atlas image={texture} sprites={sprites} transforms={transforms} />
      </Canvas>
    </GestureDetector>
  );
};

Performance Benefits

  • Single draw call - Renders all instances in one pass
  • GPU acceleration - Takes full advantage of hardware
  • Minimal FFI cost - Worklet-based animations run entirely on UI thread
  • Efficient memory - Shares texture memory across instances

Use Cases

  • Tile-based maps
  • Sprite animations for games
  • Particle systems
  • Repeated UI elements
  • Icon grids

Build docs developers (and LLMs) love