What you'll learn
- How to initialize Skia, mount a
SkiaCanvasin texture or overlay mode, and render shapes - How to load fonts and apply custom paints (gradients, blend modes, blur, drop shadow)
- How to perform programmatic path operations —
union,intersect,difference,xor
@three-flatland/skia brings Skia's GPU-accelerated vector rendering to Three.js via WASM, built with Zig. It auto-detects WebGPU or WebGL from your renderer and supports both Three.js and React Three Fiber.
You probably don't need this page yet. Skia earns its WASM weight when you need real vector graphics in 3D — UI overlays, dynamic text, path operations, gradients. For static images and animated sprites, plain Sprite2D is the right answer.
Includes a native WebGPU backend (Graphite/Dawn) — CanvasKit's npm package ships WebGL only. At 857 KB (WebGPU) / 1 MB (WebGL) brotli vs CanvasKit's 2.2 MB.
Installation
Section titled “Installation”npm install @three-flatland/skiapnpm add @three-flatland/skiayarn add @three-flatland/skia threebun add @three-flatland/skiaPaste this into your agent to add Skia drawing support:
Add @three-flatland/skia to the project for GPU-accelerated 2D vectorgraphics (shapes, text, gradients, path ops) rendered via Skia WASM.
Read the API reference: https://thejustinwalsh.com/three-flatland/llms-full.txt
Install: npm install @three-flatland/skiaRequires: three >= 0.183.1, auto-detects WebGPU or WebGL from rendererImport from: "@three-flatland/skia" (core), "@three-flatland/skia/three" (scene nodes), "@three-flatland/skia/react" (R3F integration)
Key APIs:- Skia.init(renderer) — initialize WASM, returns SkiaContext- SkiaCanvas — root render target (texture mode or overlay mode)- Drawing nodes: SkiaRect, SkiaCircle, SkiaLine, SkiaPathNode, SkiaTextNode, SkiaGroup- SkiaPaint — custom gradients, shaders, effects (assign to node.paint)- SkiaPath — programmatic paths, boolean ops (difference, intersect, union, xor)- SkiaFontLoader — loads SkiaTypeface (useLoader compatible, cached by URL)- SkiaTypeface.atSize(n) — derive sized SkiaFont from a loaded typeface
React patterns:- <SkiaCanvas> wrapper provides context to children via useSkiaContext()- useLoader(SkiaFontLoader, url) returns SkiaTypeface, call .atSize(n) for sized fonts- attachSkiaTexture — R3F attach helper for texture-on-material- useState(() => new SkiaPaint(skia)) for WASM objects (survives strict mode)- useFrame with { before: 'render' } / { after: 'render' } for render pipeline
WASM setup: works zero-config with Vite dev server.For production, copy WASM to public: npx skia-wasm public/skiaOverride URL: Skia.init(renderer, { wasmUrl: '/skia/skia-gl.wasm' })Peer dependencies: three >= 0.183.1. For React: @react-three/fiber >= 10.0.0-alpha.2 and react >= 19.0.0.
WASM Setup
Section titled “WASM Setup”Skia ships ~1 MB WASM binaries (one for WebGL, one for WebGPU). Vite handles this automatically in dev — no config needed. For production builds or other bundlers, copy the WASM to your public directory:
npx skia-wasm public/skiaThis copies skia-gl.wasm and skia-wgpu.wasm to public/skia/. Then tell Skia where to find them:
// Option 1: pass the URL directlyconst skia = await Skia.init(renderer, { wasmUrl: '/skia/skia-gl.wasm' })
// Option 2: set env vars (replaced at build time by your bundler)// webpack.config.jsconst { DefinePlugin } = require('webpack')module.exports = { plugins: [ new DefinePlugin({ 'process.env.SKIA_WASM_URL_GL': JSON.stringify('/skia/skia-gl.wasm'), 'process.env.SKIA_WASM_URL_WGPU': JSON.stringify('/skia/skia-wgpu.wasm'), }), ],}Initialization
Section titled “Initialization”import { Skia } from '@three-flatland/skia'import { SkiaCanvas, SkiaRect } from '@three-flatland/skia/three'
// Auto-detects WebGL or WebGPU from the rendererconst skia = await Skia.init(renderer)
const canvas = new SkiaCanvas({ renderer, width: 512, height: 512 })canvas.add(new SkiaRect())
// In your animation loop:canvas.invalidate()canvas.render()import { useThree } from '@react-three/fiber/webgpu'import { SkiaCanvas, SkiaRect, useSkiaContext } from '@three-flatland/skia/react'
// SkiaCanvas initializes Skia and provides context to children.// Child components access it via useSkiaContext().function MyScene() { const renderer = useThree((s) => s.gl) return ( <SkiaCanvas renderer={renderer} width={512} height={512}> <skiaRect x={10} y={10} width={100} height={50} fill={[1, 0, 0, 1]} /> </SkiaCanvas> )}SkiaCanvas Alpha
Section titled “SkiaCanvas ”AlphaThe root node that owns the render target and walks children to draw. Two modes:
| Mode | Description | Use Case |
|------|-------------|----------|
| Texture | Renders to an off-screen texture (canvas.texture) | Apply Skia output to 3D mesh materials |
| Overlay | Renders to the screen framebuffer | HUD, debug text, UI on top of 3D |
// Texture mode (default)const shapes = new SkiaCanvas({ renderer, width: 1024, height: 880 })
// Overlay modeconst overlay = new SkiaCanvas({ renderer, width: window.innerWidth * dpr, height: window.innerHeight * dpr, overlay: true,})// Texture mode — use attachSkiaTexture to apply to a material<meshBasicMaterial transparent premultipliedAlpha> <SkiaCanvas attach={attachSkiaTexture} renderer={renderer} width={1024} height={880}> {/* shapes */} </SkiaCanvas></meshBasicMaterial>
// Overlay mode<SkiaCanvas ref={overlayRef} renderer={renderer} width={pw} height={ph} overlay> {/* HUD elements */}</SkiaCanvas>Render Pipeline
Section titled “Render Pipeline”SkiaCanvas does not auto-render. Call render() each frame — it automatically marks the canvas dirty and draws:
function animate() { // 1. Render Skia shapes to texture shapesCanvas.render()
// 2. Apply texture to mesh material.map = shapesCanvas.texture
// 3. Render 3D scene renderer.render(scene, camera)
// 4. Overlay on top overlay.render()}// Shapes texture — render before Three.js sceneuseFrame(() => { canvasRef.current?.render()}, { before: 'render' })
// Overlay — render after Three.js sceneuseFrame(() => { overlayRef.current?.render()}, { after: 'render' })Drawing Nodes Alpha
Section titled “Drawing Nodes ”AlphaAll nodes extend SkiaNode (which extends Three.js Object3D) and accept inline paint props.
Common Paint Props
Section titled “Common Paint Props”| Prop | Type | Description |
|------|------|-------------|
| fill | [r, g, b, a] | Fill color (0-1 floats) |
| stroke | [r, g, b, a] | Stroke color |
| strokeWidth | number | Stroke width in pixels |
| opacity | number | 0-1 opacity |
| blur | number | Gaussian blur sigma |
| blendMode | BlendMode | Compositing blend mode |
| paint | SkiaPaint | Explicit paint (overrides all above) |
Shape Nodes
Section titled “Shape Nodes”{/* Rectangle with rounded corners */}<skiaRect x={10} y={10} width={200} height={100} cornerRadius={12} fill={[0.2, 0.4, 1, 1]} />
{/* Circle */}<skiaCircle cx={150} cy={150} r={60} fill={[1, 0.3, 0.3, 1]} />
{/* Ellipse */}<skiaOval x={100} y={100} width={200} height={120} stroke={[1, 0.8, 0.2, 1]} strokeWidth={3} />
{/* Line */}<skiaLine x1={10} y1={10} x2={200} y2={100} stroke={[1, 1, 1, 0.5]} strokeWidth={2} />Path Node
Section titled “Path Node”Accepts SVG path data or an explicit SkiaPath reference:
{/* From SVG data string */}<skiaPathNode d="M10 80 Q95 10 180 80" fill={[1, 1, 1, 1]} />
{/* From SkiaPath reference (for dynamic paths, boolean ops) */}<skiaPathNode path={myPath} fill={[0.9, 0.3, 0.3, 0.9]} />Text Node
Section titled “Text Node”Requires a SkiaFont loaded via SkiaFontLoader:
<skiaTextNode text="Hello Skia" font={font} fill={[1, 1, 1, 1]} x={20} y={40} />SkiaGroup Alpha
Section titled “SkiaGroup ”AlphaGroups apply 2D transforms, clipping, and layer effects to all children:
<skiaGroup position={[100, 50, 0]} scale={[2, 2, 1]}> <skiaRect fill={[1, 0, 0, 1]} width={50} height={50} /></skiaGroup>Uses standard Object3D position, scale, and rotation.z (radians). The degrees prop is a convenience shorthand.
| Prop | Description |
|------|-------------|
| position | Translation (Object3D — [x, y, z]) |
| rotation | Rotation (Object3D — rotation.z in radians) |
| degrees | Rotation in degrees (convenience, overrides rotation.z) |
| scale | Scale (Object3D — [x, y, z]) |
| skewX, skewY | Skew (Skia-specific, no Object3D equivalent) |
| opacity | Layer opacity |
| blur | Gaussian blur on group |
| shadow | Drop shadow ({ dx, dy, blur, color }) |
| clipRect | [x, y, w, h] clip rectangle |
| backdropBlur | Frosted glass effect |
Font Loading
Section titled “Font Loading”SkiaFontLoader loads a SkiaTypeface — the font data independent of size. Call .atSize(n) to get a sized SkiaFont.
import { SkiaFontLoader } from '@three-flatland/skia/three'
const typeface = await SkiaFontLoader.load('/fonts/Inter.ttf')const titleFont = typeface.atSize(32)const bodyFont = typeface.atSize(14)textNode.font = titleFontimport { useLoader } from '@react-three/fiber/webgpu'import { SkiaFontLoader } from '@three-flatland/skia/react'
function TextComponent() { // useLoader caches the typeface by URL — one fetch, multiple sizes const typeface = useLoader(SkiaFontLoader, '/fonts/Inter.ttf') const titleFont = typeface.atSize(32) const bodyFont = typeface.atSize(14)
return <> <skiaTextNode text="Title" font={titleFont} fill={[1, 1, 1, 1]} /> <skiaTextNode text="Body" font={bodyFont} fill={[0.8, 0.8, 0.8, 1]} /> </>}The typeface resolves the SkiaContext lazily — if no context is available when atSize() is called, it falls back to the global SkiaContext.instance singleton. Inside a <SkiaCanvas> subtree, this is always available.
Custom Paints
Section titled “Custom Paints”For gradients, shaders, and effects beyond flat fill/stroke, create a SkiaPaint:
import { SkiaPaint } from '@three-flatland/skia'
const paint = new SkiaPaint(skia) .setFill() .setLinearGradient(0, 0, 200, 0, [0xFFFF0000, 0xFF0000FF], [0, 1])
rect.paint = paint // overrides inline fill/stroke propsIn React, use useState initializer to create paints that survive strict mode:
const skia = useSkiaContext()!const [paint] = useState(() => new SkiaPaint(skia).setFill())
useFrame(({ elapsed }) => { // Animate the gradient each frame paint.setLinearGradient(...)})
return <skiaRect paint={paint} x={0} y={0} width={200} height={50} />Path Operations
Section titled “Path Operations”Create paths programmatically and perform boolean operations:
import { SkiaPath } from '@three-flatland/skia'
const a = new SkiaPath(skia).addCircle(100, 100, 40)const b = new SkiaPath(skia).addCircle(130, 100, 40)const result = new SkiaPath(skia)
a.opInto(b, 'intersect', result) // 'difference' | 'intersect' | 'union' | 'xor'pathNode.path = resultOperations: difference, intersect, union, xor, reverse-difference.
Texture Attachment (React)
Section titled “Texture Attachment (React)”attachSkiaTexture is an R3F attach helper that grabs .texture from a SkiaCanvas and assigns it to the parent material's map:
import { SkiaCanvas, attachSkiaTexture } from '@three-flatland/skia/react'
<meshBasicMaterial transparent premultipliedAlpha> <SkiaCanvas attach={attachSkiaTexture} renderer={renderer} width={1024} height={880}> {/* Skia shapes rendered to texture */} </SkiaCanvas></meshBasicMaterial>Next Steps
Section titled “Next Steps”- Skia Example — Full interactive demo
- Sprites Guide — Three-flatland 2D sprites