Skip to main content
Surfaces provide a drawing context where you can render graphics offscreen. This is useful for creating complex effects, caching renders, or generating images programmatically.

Creating Surfaces

import { Skia } from "@shopify/react-native-skia";

// Create a surface
const surface = Skia.Surface.Make(300, 300);

if (surface) {
  const canvas = surface.getCanvas();
  
  // Draw on the canvas
  const paint = Skia.Paint();
  paint.setColor(Skia.Color("blue"));
  canvas.drawCircle(150, 150, 100, paint);
  
  // Get the result as an image
  const image = surface.makeImageSnapshot();
  
  // Use the image...
}

Surface Methods

getCanvas
() => SkCanvas
Returns the canvas for drawing on the surface
const canvas = surface.getCanvas();
makeImageSnapshot
(bounds?: SkRect) => SkImage
Captures the surface content as an image. Optionally specify a rectangle to capture only a portion.
const fullImage = surface.makeImageSnapshot();
const partialImage = surface.makeImageSnapshot(rect(0, 0, 100, 100));
flush
() => void
Ensures all queued draw operations are completed
surface.flush();
width
() => number
Returns the width of the surface
const w = surface.width();
height
() => number
Returns the height of the surface
const h = surface.height();

Use Cases

Caching Complex Renders

Pre-render complex graphics for better performance:
import { useMemo } from "react";
import { Skia } from "@shopify/react-native-skia";

const useCachedImage = (size: number) => {
  return useMemo(() => {
    const surface = Skia.Surface.Make(size, size);
    if (!surface) return null;
    
    const canvas = surface.getCanvas();
    const paint = Skia.Paint();
    
    // Complex drawing operations
    for (let i = 0; i < 100; i++) {
      paint.setColor(Skia.Color(`hsl(${i * 3.6}, 100%, 50%)`));
      canvas.drawCircle(
        size / 2,
        size / 2,
        (size / 2) * (1 - i / 100),
        paint
      );
    }
    
    return surface.makeImageSnapshot();
  }, [size]);
};

export default function CachedRender() {
  const image = useCachedImage(300);
  
  if (!image) return null;
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Image image={image} x={0} y={0} width={300} height={300} />
    </Canvas>
  );
}

Generating Textures

Create procedural textures:
const generateTexture = (width: number, height: number) => {
  const surface = Skia.Surface.Make(width, height);
  if (!surface) return null;
  
  const canvas = surface.getCanvas();
  const paint = Skia.Paint();
  
  // Generate noise texture
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const value = Math.random() * 255;
      paint.setColor(Skia.Color(`rgb(${value},${value},${value})`));
      canvas.drawRect(rect(x, y, 1, 1), paint);
    }
  }
  
  return surface.makeImageSnapshot();
};

Image Processing

Manipulate images:
const applyFilter = (sourceImage: SkImage) => {
  const surface = Skia.Surface.Make(
    sourceImage.width(),
    sourceImage.height()
  );
  
  if (!surface) return null;
  
  const canvas = surface.getCanvas();
  const paint = Skia.Paint();
  
  // Apply filter
  const blur = Skia.ImageFilter.MakeBlur(10, 10, TileMode.Decal, null);
  paint.setImageFilter(blur);
  
  canvas.drawImage(sourceImage, 0, 0, paint);
  
  return surface.makeImageSnapshot();
};

Creating Thumbnails

Generate smaller versions of images:
const createThumbnail = (sourceImage: SkImage, maxSize: number) => {
  const srcWidth = sourceImage.width();
  const srcHeight = sourceImage.height();
  
  const scale = Math.min(maxSize / srcWidth, maxSize / srcHeight);
  const thumbWidth = Math.floor(srcWidth * scale);
  const thumbHeight = Math.floor(srcHeight * scale);
  
  const surface = Skia.Surface.Make(thumbWidth, thumbHeight);
  if (!surface) return null;
  
  const canvas = surface.getCanvas();
  
  canvas.drawImageRect(
    sourceImage,
    rect(0, 0, srcWidth, srcHeight),
    rect(0, 0, thumbWidth, thumbHeight),
    Skia.Paint()
  );
  
  return surface.makeImageSnapshot();
};

Compositing Multiple Images

const compositeImages = (images: SkImage[]) => {
  const surface = Skia.Surface.Make(300, 300);
  if (!surface) return null;
  
  const canvas = surface.getCanvas();
  const paint = Skia.Paint();
  
  images.forEach((image, index) => {
    paint.setAlphaf(1 / (index + 1));
    canvas.drawImage(image, 0, 0, paint);
  });
  
  return surface.makeImageSnapshot();
};

Drawing on Canvas

The canvas obtained from a surface has all standard drawing methods:
const surface = Skia.Surface.Make(400, 400);
const canvas = surface.getCanvas();
const paint = Skia.Paint();

// Background
paint.setColor(Skia.Color("white"));
canvas.drawPaint(paint);

// Shapes
paint.setColor(Skia.Color("blue"));
canvas.drawCircle(200, 200, 100, paint);

paint.setColor(Skia.Color("red"));
paint.setStyle(PaintStyle.Stroke);
paint.setStrokeWidth(5);
canvas.drawRect(rect(100, 100, 200, 200), paint);

// Text
const font = Skia.Font(null, 24);
paint.setColor(Skia.Color("black"));
paint.setStyle(PaintStyle.Fill);
canvas.drawText("Hello", 150, 220, paint, font);

// Get result
const image = surface.makeImageSnapshot();

Saving Surface Output

import RNFS from "react-native-fs";
import { ImageFormat } from "@shopify/react-native-skia";

const saveSurface = async (surface: SkSurface, filename: string) => {
  const image = surface.makeImageSnapshot();
  const bytes = image.encodeToBytes(ImageFormat.PNG);
  
  const path = `${RNFS.DocumentDirectoryPath}/${filename}`;
  await RNFS.writeFile(path, bytes.buffer, "base64");
  
  console.log("Saved to:", path);
};

Performance Considerations

  • Surfaces allocate memory for offscreen buffers - use judiciously
  • Larger surfaces use more memory
  • Create surfaces once and reuse when possible
  • Call flush() to ensure rendering is complete before capturing
  • Dispose of surfaces when no longer needed (automatic in JavaScript)
  • For repeated renders, consider caching the result as an image

Examples

Chart Rendering

const renderChart = (data: number[]) => {
  const width = 400;
  const height = 300;
  const surface = Skia.Surface.Make(width, height);
  if (!surface) return null;
  
  const canvas = surface.getCanvas();
  const paint = Skia.Paint();
  
  // Background
  paint.setColor(Skia.Color("white"));
  canvas.drawPaint(paint);
  
  // Draw bars
  const barWidth = width / data.length;
  const maxValue = Math.max(...data);
  
  data.forEach((value, index) => {
    const barHeight = (value / maxValue) * (height - 40);
    const x = index * barWidth;
    const y = height - barHeight - 20;
    
    paint.setColor(Skia.Color("blue"));
    canvas.drawRect(rect(x + 5, y, barWidth - 10, barHeight), paint);
  });
  
  return surface.makeImageSnapshot();
};

QR Code Generator

const generateQR = (data: string, size: number) => {
  const surface = Skia.Surface.Make(size, size);
  if (!surface) return null;
  
  const canvas = surface.getCanvas();
  const paint = Skia.Paint();
  
  // Generate QR code data (simplified)
  const matrix = generateQRMatrix(data);
  const moduleSize = size / matrix.length;
  
  paint.setColor(Skia.Color("white"));
  canvas.drawPaint(paint);
  
  paint.setColor(Skia.Color("black"));
  matrix.forEach((row, y) => {
    row.forEach((module, x) => {
      if (module) {
        canvas.drawRect(
          rect(x * moduleSize, y * moduleSize, moduleSize, moduleSize),
          paint
        );
      }
    });
  });
  
  return surface.makeImageSnapshot();
};

Build docs developers (and LLMs) love