Klint Canvas Library
A React-first 2D canvas library for creative coding, inspired by p5.js but built with modern React patterns.
Quick Start
import { Klint, useKlint, useStorage } from "@shopify/klint";
function MySketch() {
const { context } = useKlint();
const draw = (K) => {
K.background("#1a1a2e");
K.fillColor("#e94560");
K.circle(K.width / 2, K.height / 2, 100);
};
return <Klint context={context} draw={draw} />;
}
Core Concepts
Lifecycle Functions
| Function |
When Called |
Purpose |
preload |
Once, before setup |
Async loading (images, fonts) |
setup |
Once, after preload |
Initialize state, create offscreens |
draw |
Every frame |
Main render loop |
The K Context
All drawing happens through the K context passed to lifecycle functions. K is a convention but you can use whatever you want. :
const draw = (K) => {
K.width; // Canvas width (scaled by DPR)
K.height; // Canvas height
K.time; // Seconds since start
K.frame; // Current frame number
K.deltaTime; // Time since last frame
};
State Management with useStorage
const store = useStorage({ particles: [], count: 0 });
const setup = (K) => {
store.set("particles", [{ x: 0, y: 0 }]);
};
const draw = (K) => {
const particles = store.get("particles");
// Mutate directly - no setState needed
particles.forEach((p) => (p.x += 1));
};
Complete API Reference
Core Functions (KlintCoreFunctions)
| Method |
Signature |
Description |
saveCanvas |
() |
Download canvas as PNG |
fullscreen |
() |
Request fullscreen mode |
play |
() |
Resume animation |
pause |
() |
Pause animation |
redraw |
() |
Force redraw (WIP) |
extend |
(name, data, enforceReplace?) |
Add custom property to context |
passImage |
(element) |
Validate image is loaded |
passImages |
(elements[]) |
Validate images array |
saveConfig |
(from?) |
Save context configuration |
restoreConfig |
(config) |
Restore context configuration |
describe |
(description) |
Set canvas accessibility description |
createOffscreen |
(id, width, height, options?, callback?) |
Create offscreen canvas |
getOffscreen |
(id) |
Get offscreen canvas by ID |
Drawing Functions (KlintFunctions)
Canvas & Background
| Method |
Signature |
Description |
background |
(color?) |
Clear canvas with color |
reset |
() |
Clear canvas and reset transform |
clear |
() |
Clear canvas (transparent) |
Styling
| Method |
Signature |
Description |
fillColor |
(color) |
Set fill style |
strokeColor |
(color) |
Set stroke style |
noFill |
() |
Disable fill |
noStroke |
() |
Disable stroke |
strokeWidth |
(width) |
Set line width |
strokeJoin |
(join) |
Set line join: miter|round|bevel |
strokeCap |
(cap) |
Set line cap: butt|round|square |
fillRule |
(rule) |
Set fill rule: nonzero|evenodd |
opacity |
(value) |
Set global alpha (0-1) |
blend |
(mode) |
Set composite operation |
Shapes
| Method |
Signature |
Description |
point |
(x, y) |
Draw single point |
line |
(x1, y1, x2, y2) |
Draw line |
circle |
(x, y, radius, radius2?) |
Draw circle/ellipse |
disk |
(x, y, radius, startAngle?, endAngle?, closed?) |
Draw arc/pie |
rectangle |
(x, y, width, height?) |
Draw rectangle |
roundedRectangle |
(x, y, width, radius, height?) |
Draw rounded rectangle |
polygon |
(x, y, radius, sides, radius2?, rotation?) |
Draw regular polygon |
Paths
| Method |
Signature |
Description |
beginShape |
() |
Start custom shape |
vertex |
(x, y) |
Add vertex point |
bezierVertex |
(cp1x, cp1y, cp2x, cp2y, x, y) |
Add cubic bezier |
quadraticVertex |
(cpx, cpy, x, y) |
Add quadratic curve |
arcVertex |
(x1, y1, x2, y2, radius) |
Add arc |
beginContour |
() |
Start cutout contour |
endContour |
(forceRevert?) |
End cutout contour |
endShape |
(close?) |
Finish and draw shape |
clipTo |
(callback, fillRule?) |
Create clipping region |
Gradients
| Method |
Signature |
Description |
gradient |
(x1?, y1?, x2?, y2?) |
Create linear gradient |
radialGradient |
(x1?, y1?, r1?, x2?, y2?, r2?) |
Create radial gradient |
conicGradient |
(angle?, x?, y?) |
Create conic gradient |
addColorStop |
(gradient, offset, color) |
Add color stop |
Text
| Method |
Signature |
Description |
textFont |
(font) |
Set font family |
textSize |
(size) |
Set font size |
textStyle |
(style) |
Set font style: normal|italic |
textWeight |
(weight) |
Set font weight |
textQuality |
(quality) |
Set rendering: speed|auto|legibility|precision |
textSpacing |
(kind, value) |
Set letter/word spacing |
textLeading |
(spacing) |
Set line height |
alignText |
(horizontal, vertical?) |
Set text alignment |
textWidth |
(text) |
Measure text width |
text |
(text, x, y, maxWidth?) |
Draw text |
paragraph |
(text, x, y, width, options?) |
Draw paragraph |
Transforms
| Method |
Signature |
Description |
push |
() |
Save canvas state |
pop |
() |
Restore canvas state |
Images & Pixels
| Method |
Signature |
Description |
image |
(image, x, y, ...) |
Draw image (3/5/9 arg forms) |
loadPixels |
() |
Get ImageData |
updatePixels |
(pixels) |
Put pixel array |
readPixels |
(x, y, w?, h?) |
Read pixels at position |
scaleTo |
(originW, originH, destW, destH, cover?) |
Calculate scale ratio |
toBase64 |
(type?, quality?) |
Export canvas as base64 |
Origin Settings
| Method |
Signature |
Description |
setCanvasOrigin |
(type) |
Set canvas origin: center|corner |
setImageOrigin |
(type) |
Set image origin: center|corner |
setRectOrigin |
(type) |
Set rectangle origin: center|corner |
Filters
| Method |
Signature |
Description |
canIuseFilter |
() |
Check filter support |
blur |
(radius) |
Apply blur filter |
grayscale |
(amount) |
Apply grayscale (0-1) |
hue |
(angle) |
Rotate hue (radians) |
invert |
(amount) |
Invert colors (0-1) |
filterOpacity |
(value) |
Filter opacity (0-1) |
dropShadow |
(x, y, blur, color) |
Apply drop shadow |
SVGfilter |
(url) |
Apply SVG filter |
Math Utilities
| Method |
Signature |
Description |
PI |
|
π constant |
TWO_PI |
|
2π constant |
TAU |
|
2π constant |
constrain |
(val, floor, ceil) |
Clamp value |
lerp |
(A, B, mix, bounded?) |
Linear interpolation |
fract |
(n, mod, mode?) |
Modulo (handles negatives) |
distance |
(x1, y1, x2, y2, mode?) |
Distance between points |
squareDistance |
(x1, y1, x2, y2) |
Squared distance |
dot |
(x1, y1, x2, y2) |
Dot product |
remap |
(n, A, B, C, D, bounded?) |
Map value between ranges |
bezierLerp |
(a, b, c, d, t) |
Cubic bezier interpolation |
bezierTangent |
(a, b, c, d, t) |
Cubic bezier tangent |
Elements
K.Color
K.Color.rgb(255, 100, 50); // RGB (0-255)
K.Color.rgba(255, 100, 50, 0.5); // RGBA with alpha
K.Color.hsl(180, 70, 50); // HSL (h: 0-360, s/l: 0-100)
K.Color.hsla(180, 70, 50, 0.5); // HSLA
K.Color.gray(128); // Grayscale
K.Color.hex("#ff6b6b"); // Hex
K.Color.oklch(0.7, 0.15, 180); // OKLCH (perceptually uniform)
K.Color.blendColors(c1, c2, 0.5); // Blend two colors
K.Color.complementary(color); // Opposite on wheel
K.Color.analogous(color, 30); // Adjacent colors
K.Color.triadic(color); // Three evenly spaced
K.Color.saturate(color, 0.2); // Increase saturation
K.Color.lighten(color, 0.2); // Mix with white
K.Color.darken(color, 0.2); // Mix with black
Named colors: coral, brown, mustard, crimson, navy, sky, olive, charcoal, peach, rose, plum, sage, drab, taupe, midnight, golden, orange, slate
K.Vector
const v = K.createVector(100, 200);
v.add(other); // Add vector
v.sub(other); // Subtract
v.mult(2); // Scale
v.normalize(); // Unit vector
v.mag(); // Magnitude
v.rotate(Math.PI/4); // Rotate
v.dist(other); // Distance
v.dot(other); // Dot product
v.copy(); // Clone
// Static
K.Vector.fromAngle(center, angle, radius);
K.Easing
All take t (0-1), return eased value:
K.Easing.linear(t);
K.Easing.easeInQuad(t);
K.Easing.easeOutQuad(t);
K.Easing.easeInOutQuad(t);
K.Easing.easeInCubic(t);
K.Easing.easeOutCubic(t);
K.Easing.easeInOutCubic(t);
K.Easing.easeInElastic(t);
K.Easing.easeOutElastic(t);
K.Easing.easeInOutElastic(t);
K.Easing.easeInBounce(t);
K.Easing.easeOutBounce(t);
K.Easing.easeInOutBounce(t);
// ... and more (Quart, Quint, Sine, Expo, Circ, Back)
Hooks
useKlint
const {
context, // Pass to <Klint>
KlintMouse, // Mouse input hook
KlintScroll, // Scroll input hook
KlintGesture, // Touch gestures hook
KlintKeyboard, // Keyboard input hook
KlintWindow, // Resize, focus, visibility
KlintImage, // Image loading hook
KlintTimeline, // Animation timeline hook
KlintPerformance, // Performance monitoring
togglePlay, // Play/pause control
useDev, // HMR support
} = useKlint();
KlintMouse
const { mouse, onClick, onMouseDown, onMouseUp, onMove, onDrag, onMouseIn, onMouseOut } = KlintMouse();
// mouse object:
mouse.x; // Current X position
mouse.y; // Current Y position
mouse.px; // Previous X
mouse.py; // Previous Y
mouse.vx; // Velocity X (x - px)
mouse.vy; // Velocity Y (y - py)
mouse.angle; // Angle of movement (Math.atan2)
mouse.isPressed; // Is button down (NOTE: published 0.0.98 uses isPressed, not pressed)
mouse.isHover; // Is pointer over the canvas
mouse.button; // Which button (0/1/2)
onClick((K, e) => { /* handle click */ });
onMouseDown((K, e) => { /* handle mouse down */ });
onMouseUp((K, e) => { /* handle mouse up */ });
onMouseIn((K, e) => { /* handle mouse enter canvas */ });
onMouseOut((K, e) => { /* handle mouse leave canvas */ });
KlintWindow
const { onResize, onFocus, onBlur, onVisibilityChange } = KlintWindow();
onResize((K) => { /* window resized */ });
onFocus((K) => { /* window gained focus */ });
onBlur((K) => { /* window lost focus */ });
onVisibilityChange((K, isVisible) => { /* tab visibility changed */ });
KlintImage
const { loadImage, loadImages, getImage, hasImage } = KlintImage();
// In preload:
await loadImage("avatar", "/images/avatar.png");
await loadImages({ bg: "/bg.jpg", sprite: "/sprite.png" });
// In draw:
const img = getImage("avatar");
if (img) K.image(img, 0, 0);
KlintTimeline
const { Timeline } = KlintTimeline();
const anim = Timeline.create((t) => ({
x: t.track((kf) => {
kf.start(0)
.then(100, 0.5, K.Easing.easeOutCubic)
.then(0, 0.5);
}),
scale: t.track((kf) => kf.start(1).then(2, 1)),
}));
// In draw:
anim.update(K.time % 1);
K.circle(anim.x.current, 100, anim.scale.current * 20);
Canvas Options
<Klint
context={context}
draw={draw}
options={{
alpha: "true", // Transparent background
willreadfrequently: "true", // Optimize for pixel reads
autoplay: "true", // Auto start animation
noloop: "true", // Single frame only
static: "true", // Render once, convert to image
fps: 60, // Target framerate
dpr: 2, // Device pixel ratio
origin: "center", // Coordinate origin
}}
/>
Code Examples
Animated Gradient
const draw = (K) => {
const g = K.gradient(0, 0, K.width, K.height);
const hue = (K.time * 30) % 360;
K.addColorStop(g, 0, K.Color.hsl(hue, 70, 50));
K.addColorStop(g, 1, K.Color.hsl((hue + 60) % 360, 70, 50));
K.fillColor(g);
K.rectangle(0, 0, K.width, K.height);
};
Circular Motion
const draw = (K) => {
K.background("#111");
const cx = K.width / 2;
const cy = K.height / 2;
for (let i = 0; i < 12; i++) {
const angle = K.time + (i * Math.PI * 2) / 12;
const x = cx + Math.cos(angle) * 150;
const y = cy + Math.sin(angle) * 150;
K.fillColor(K.Color.hsl(i * 30, 70, 60));
K.circle(x, y, 20);
}
};
Mouse Trail
const { context, KlintMouse } = useKlint();
const { mouse } = KlintMouse();
const store = useStorage({ trail: [] });
const draw = (K) => {
K.background("rgba(0, 0, 0, 0.05)");
const trail = store.get("trail");
trail.push({ x: mouse.x, y: mouse.y });
if (trail.length > 50) trail.shift();
trail.forEach((p, i) => {
const alpha = i / trail.length;
K.fillColor(K.Color.rgba(255, 100, 100, alpha));
K.circle(p.x, p.y, alpha * 20);
});
};
Grid Pattern
const draw = (K) => {
K.background("#1a1a2e");
const cols = 20,
rows = 20;
const cellW = K.width / cols,
cellH = K.height / rows;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const x = i * cellW + cellW / 2;
const y = j * cellH + cellH / 2;
const d = K.distance(x, y, K.width / 2, K.height / 2);
const size = K.remap(
Math.sin(d * 0.02 - K.time * 2),
-1,
1,
2,
cellW * 0.4
);
K.fillColor(K.Color.hsl(d * 0.5, 70, 60));
K.circle(x, y, size);
}
}
};
Offscreen Rendering
const setup = (K) => {
K.createOffscreen("bg", K.width, K.height, {}, (ctx) => {
for (let i = 0; i < 100; i++) {
ctx.fillColor(K.Color.rgba(255, 255, 255, 0.1));
ctx.circle(Math.random() * ctx.width, Math.random() * ctx.height, Math.random() * 50);
}
});
};
const draw = (K) => {
K.background("#111");
K.image(K.getOffscreen("bg"), 0, 0);
K.fillColor("#ff6b6b");
K.circle(K.width / 2 + Math.sin(K.time) * 100, K.height / 2, 30);
};
Custom Bezier Shape
const draw = (K) => {
K.background("#1a1a2e");
K.translate(K.width / 2, K.height / 2);
K.fillColor("#e94560");
K.noStroke();
K.beginShape();
K.vertex(-100, 0);
K.bezierVertex(-100, -55, -55, -100, 0, -100);
K.bezierVertex(55, -100, 100, -55, 100, 0);
K.bezierVertex(100, 55, 55, 100, 0, 100);
K.bezierVertex(-55, 100, -100, 55, -100, 0);
K.endShape(true);
};
Plugins
FontParser
Parse OpenType fonts for glyph-level access:
import { FontParser } from "@shopify/klint/plugins";
const preload = async () => {
await FontParser.load("myFont", "/fonts/MyFont.otf");
};
const draw = (K) => {
const paths = FontParser.getTextPaths("myFont", "Hello", 100);
paths.forEach((glyph) => {
K.beginShape();
glyph.commands.forEach((cmd) => {
if (cmd.type === "M") K.vertex(cmd.x, cmd.y);
else if (cmd.type === "L") K.vertex(cmd.x, cmd.y);
else if (cmd.type === "C") K.bezierVertex(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y);
});
K.endShape(true);
});
};
CatmullRom
Smooth curves through points:
import { CatmullRom } from "@shopify/klint/plugins";
const points = [
{ x: 100, y: 100 },
{ x: 200, y: 50 },
{ x: 300, y: 150 },
];
const smoothed = CatmullRom.getPoints(points, { tension: 0.5, segments: 20 });
K.beginShape();
smoothed.forEach((p) => K.vertex(p.x, p.y));
K.endShape();
Delaunay
Triangulation and Voronoi diagrams:
import { Delaunay } from "@shopify/klint/plugins";
const points = Array(50)
.fill(0)
.map(() => [Math.random() * K.width, Math.random() * K.height]);
const delaunay = Delaunay.from(points);
const voronoi = delaunay.voronoi([0, 0, K.width, K.height]);
// Draw Voronoi cells
for (const cell of voronoi.cellPolygons()) {
K.beginShape();
cell.forEach(([x, y]) => K.vertex(x, y));
K.endShape(true);
}
Sprites
Sprite sheet animations:
import { Sprites } from "@shopify/klint/plugins";
const sprite = Sprites.create(image, {
frameWidth: 64,
frameHeight: 64,
frameCount: 8,
fps: 12,
});
const draw = (K) => {
sprite.update(K.deltaTime);
sprite.draw(K, x, y);
};
Things
Entity management system:
import { Things } from "@shopify/klint/plugins";
const things = Things.create();
things.add({
x: 100,
y: 100,
update(dt) {
this.x += Math.sin(Date.now() / 1000);
},
draw(K) {
K.circle(this.x, this.y, 20);
},
});
const draw = (K) => {
things.update(K.deltaTime);
things.draw(K);
};
Blend Modes
K.blend("source-over"); // Default
K.blend("multiply");
K.blend("screen");
K.blend("overlay");
K.blend("darken");
K.blend("lighten");
K.blend("color-dodge");
K.blend("color-burn");
K.blend("hard-light");
K.blend("soft-light");
K.blend("difference");
K.blend("exclusion");
K.blend("hue");
K.blend("saturation");
K.blend("color");
K.blend("luminosity");
Performance Tips
- Avoid setup in draw - Initialize in
setup, not draw
- Use offscreen buffers - Pre-render static content
- Batch similar operations - Group by fill/stroke color
- Use
push/pop sparingly - State changes have cost
- Prefer
noStroke/noFill - When not needed
- Use integer coordinates - Avoids sub-pixel rendering
- Text is expensive - Cache text on offscreen canvas