Mastering Skia with CanvasKit: From Surfaces to Perspective Transforms
This guide explains how to use the open‑source Skia 2D graphics library via the canvaskit‑wasm WebAssembly package to create surfaces, draw shapes, apply paints, shaders, blend modes, and perform 2D matrix transformations—including translation, scaling, rotation, skew, and perspective—culminating in a complete procedural design workflow with reusable JSX components.
1. Skia Overview
Skia is an open‑source 2D graphics library that powers Chrome, Android, Flutter and many other products. It offers language bindings for C++, C#, Java, Python, Rust, WebAssembly, etc. For both browser‑side and server‑side rendering, the canvaskit-wasm NPM package provides a WebAssembly build that can be called from JavaScript.
import CanvasKitInit from 'canvaskit-wasm'
const loadLib = CanvasKitInit({
locateFile(file) { return 'https://unpkg.com/[email protected]/bin/' + file }
})
loadLib.then(lib => {
const surface = lib.MakeSurface(500, 500) // create a 500×500 surface
const canvas = surface.getCanvas() // obtain the drawing canvas
})Common Drawing APIs
Surface manages the memory for a drawing canvas and can be converted to an image.
Canvas is the drawing context that provides methods such as:
drawRect()
drawCircle()
drawLine()
drawPath()
drawArc()
drawText()
Path builds vector shapes. Typical methods include:
moveTo(x, y)
lineTo(x, y)
arcTo(...)
cubicTo(...)
quadTo(...)
addRect(...)
addCircle(...)
addOval(...)
addRoundedRect(...)
addPath(...)
// draw a triangle and a cubic Bézier curve
const path = new CanvasKit.Path()
path.moveTo(10, 10)
path.lineTo(100, 10)
path.lineTo(10, 100)
path.close()
const arcPath = new CanvasKit.Path()
arcPath.moveTo(55, 55)
arcPath.cubicTo(120, 150, 130, 180, 200, 200)
path.addPath(arcPath)
canvas.drawPath(path, paint)Paint stores style information for drawing. Important methods are:
setColor()
setAlphaf()
setAntiAlias()
setBlendMode()
setStyle()
setStrokeWidth()
setColorFilter()
setImageFilter()
setMaskFilter()
setShader()
const { Path, parseColorString } = CanvasKit
const paint = new Paint()
paint.setStyle(PaintStyle.Stroke)
paint.setColor(parseColorString('#000000'))
canvas.drawPath(path, paint)Shader creates gradient, noise, or pattern effects. Common constructors are:
MakeColor()
MakeLinearGradient()
MakeRadialGradient()
MakeSweepGradient()
MakeTwoPointConicalGradient()
MakeFractalNoise()
MakeTurbulence()
MakeBlend()
const shader = Shader.MakeLinearGradient(
[0, 0], [50, 50],
[parseColorString('#ff0000'), parseColorString('#ffff00'), parseColorString('#0000ff')],
[0, 0.5, 1], TileMode.Clamp)
paint.setShader(shader)Blendmode defines how overlapping graphics are composited. The most frequently used Porter‑Duff modes include Clear, Src, Dst, SrcOver, DstOver, SrcIn, DstIn, SrcOut, DstOut, SrcATop, DstATop, Xor, Plus, etc.
Skia’s coordinate system differs from the classic Cartesian system: the y‑axis points downwards, so transformation matrices must account for this orientation.
2. Matrix Transformations
All 2D transformations are expressed as point coordinate changes using a 3×3 matrix. Skia provides helper methods to build these matrices.
(x, y) → (x', y')
[x', y', 1] = [x, y, 1] × mat2.1 Translation
Moves an object by (X, Y). The matrix is:
│1 0 0│
|x y 1| × │0 1 0│ = |x' y' 1|
│X Y 1│Skia call:
Matrix.translated(X, Y)2.2 Scaling
Scales width by W and height by H:
│W 0 0│
|x y 1| × │0 H 0│ = |x' y' 1|
│0 0 1│Skia call:
Matrix.scaled(W, H)2.3 Rotation
Rotates around a point (default origin) by θ degrees (converted to radians):
│ cosθ sinθ 0│
|x y 1| × │-sinθ cosθ 0│ = |x' y' 1|
│ 0 0 1│Skia call:
Matrix.rotated(toRadians(θ), 0, 0)2.4 Skew
Shears the shape horizontally by α and vertically by θ:
│1 tanα 0│
|x y 1| × │tanθ 1 0│ = |x' y' 1|
│0 0 1│Skia call:
Matrix.skewed(tan(α), tan(θ), 0, 0)2.5 Perspective
Creates a projective transform that maps a rectangle to an arbitrary quadrilateral. The full 3×3 matrix contains scale, skew, translation and perspective components (Persp0, Persp1, Persp2). A TypeScript helper createPerspectiveMatrixFromPoints computes these values from four corner points and the original width/height.
export type Point = { x: number; y: number }
export function createPerspectiveMatrixFromPoints(
topLeft: Point, topRight: Point, botRight: Point, botLeft: Point,
w: number, h: number) {
// ... lengthy algebra omitted for brevity ...
return [scaleX, skewX, transX, skewY, scaleY, transY, persp0, persp1, persp2]
}3. Drawing Examples
3.1 Layer Analysis
The final 2.5D background consists of two stacked sections, each with a gradient background layer and a grid overlay. The lower section adds a perspective transform to the grid.
3.2 Background Layer
const backgroundPaint = new Paint()
backgroundPaint.setStyle(PaintStyle.Fill)
const points = { begin: [0, height], end: [0, 0] }
const colors = [parseColorString(beginColor), parseColorString(endColor)]
const shader = Shader.MakeLinearGradient(points.begin, points.end, colors, [0, 1], TileMode.Clamp)
backgroundPaint.setShader(shader)
canvas.drawRect(Rect.makeXYWH(0, 0, width, height).toArray(), backgroundPaint)3.3 Grid Layer
const rectsPath = new Path()
for (let i = 0; i < lineNum + 1; i++) {
for (let j = 0; j < yLineNum + 1; j++) {
if (i % 2 === 0 && j % 2 === 0 || i % 2 === 1 && j % 2 === 1) {
const rect = Rect.makeXYWH(rectSize * i, rectSize * j, rectSize, rectSize)
rectsPath.addRect(rect.toArray())
}
}
}
const overlayShader = Shader.MakeLinearGradient(points.begin, points.end, overlayColors, [0, 1], TileMode.Clamp)
const rectsPaint = new Paint()
rectsPaint.setAntiAlias(true)
rectsPaint.setStyle(PaintStyle.Stroke)
rectsPaint.setShader(overlayShader)
canvas.drawPath(rectsPath, rectsPaint)3.4 Chessboard Grid
rectsPaint.setStyle(PaintStyle.Fill)
canvas.drawPath(rectsPath, rectsPaint)3.5 Perspective Grid
// obtain a perspective matrix (e.g., from the helper above)
const m = getPerspectiveMatrix(width, height)
rectsPath.transform(m)
canvas.drawPath(rectsPath, rectsPaint)3.6 Composition
By combining the background, grid, chessboard, and perspective grid components into reusable JSX components, different parameter sets produce varied visual results, fulfilling the goal of procedural design.
Summary
Using Skia via canvaskit‑wasm enables developers to programmatically generate complex 2.5D graphics. The tutorial covers surface creation, drawing primitives, styling with paints and shaders, applying blend modes, and performing all essential matrix transformations. With these building blocks, a set of JSX‑based graphic components can be assembled to output a rich library of background patterns for the Linglong design system.
Aotu Lab
Aotu Lab, founded in October 2015, is a front-end engineering team serving multi-platform products. The articles in this public account are intended to share and discuss technology, reflecting only the personal views of Aotu Lab members and not the official stance of JD.com Technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
