Canvas 2D Guide
This guide walks through building real-world Canvas 2D features: charts, drawing tools, and animations.
The <Canvas> component uses an imperative ref API — get a context object via ref.current, call drawing commands, then call flush() to submit the frame.
import { Canvas, type CanvasRef } from 'veloxkit'
import { useRef } from 'react'
const canvasRef = useRef<CanvasRef>(null)
const ctx = canvasRef.current // VeloxCanvasContextAvailable drawing commands:
| Method | Description |
|---|---|
ctx.fillRect(x, y, w, h) | Filled rectangle |
ctx.strokeRect(x, y, w, h) | Outlined rectangle |
ctx.fillCircle(x, y, r) | Filled circle |
ctx.strokeCircle(x, y, r) | Outlined circle |
ctx.strokeLine(x1, y1, x2, y2) | Line segment |
ctx.fillText(text, x, y) | Text string at position |
ctx.clear() | Erase entire canvas |
ctx.flush() | Submit the current frame |
Style properties (set before drawing commands):
ctx.fillStyle— fill color stringctx.strokeStyle— stroke color stringctx.lineWidth— stroke width
Line chart
import { Canvas, type CanvasRef } from 'veloxkit'
import { useRef, useEffect } from 'react'
interface DataPoint { x: number; y: number }
function LineChart({ data, width = 400, height = 200 }: {
data: DataPoint[]
width?: number
height?: number
}) {
const canvasRef = useRef<CanvasRef>(null)
const padding = 32
useEffect(() => {
const ctx = canvasRef.current
if (!ctx || data.length === 0) return
const w = width - padding * 2
const h = height - padding * 2
const minY = Math.min(...data.map(d => d.y))
const maxY = Math.max(...data.map(d => d.y))
const rangeY = maxY - minY || 1
ctx.clear()
// Background
ctx.fillStyle = '#14141A'
ctx.fillRect(0, 0, width, height)
// Grid lines
ctx.strokeStyle = '#2A2A3A'
ctx.lineWidth = 1
for (let i = 0; i <= 4; i++) {
const y = padding + (h / 4) * i
ctx.strokeLine(padding, y, padding + w, y)
}
// Line segments between data points
ctx.strokeStyle = '#00A878'
ctx.lineWidth = 2
for (let i = 0; i < data.length - 1; i++) {
const x1 = padding + (data[i].x / (data.length - 1)) * w
const y1 = padding + h - ((data[i].y - minY) / rangeY) * h
const x2 = padding + (data[i+1].x / (data.length - 1)) * w
const y2 = padding + h - ((data[i+1].y - minY) / rangeY) * h
ctx.strokeLine(x1, y1, x2, y2)
}
// Dots
ctx.fillStyle = '#00A878'
data.forEach(d => {
const x = padding + (d.x / (data.length - 1)) * w
const y = padding + h - ((d.y - minY) / rangeY) * h
ctx.fillCircle(x, y, 4)
})
ctx.flush()
}, [data, width, height])
return <Canvas ref={canvasRef} width={width} height={height} />
}Animated progress ring
Use useAnimationFrame to drive animations. Mutate a ref each frame, draw, then flush:
import { Canvas, useAnimationFrame, type CanvasRef } from 'veloxkit'
import { useRef } from 'react'
function ProgressRing({ progress, size = 80 }: { progress: number; size?: number }) {
const canvasRef = useRef<CanvasRef>(null)
const displayRef = useRef(0)
useAnimationFrame(() => {
const ctx = canvasRef.current
if (!ctx) return
// Ease towards target
displayRef.current += (progress - displayRef.current) * 0.1
const cx = size / 2
const cy = size / 2
const r = (size / 2) - 6
const pct = displayRef.current
ctx.clear()
// Track ring
ctx.strokeStyle = '#2A2A3A'
ctx.lineWidth = 5
ctx.strokeCircle(cx, cy, r)
// Progress arc — approximate with short line segments
const steps = Math.ceil(pct * 60)
ctx.strokeStyle = '#00A878'
ctx.lineWidth = 5
for (let i = 0; i < steps; i++) {
const a1 = -Math.PI / 2 + (i / 60) * Math.PI * 2
const a2 = -Math.PI / 2 + ((i + 1) / 60) * Math.PI * 2
ctx.strokeLine(
cx + Math.cos(a1) * r, cy + Math.sin(a1) * r,
cx + Math.cos(a2) * r, cy + Math.sin(a2) * r,
)
}
// Percentage label
ctx.fillStyle = '#F0F0F2'
ctx.fillText(`${Math.round(pct * 100)}%`, cx - 12, cy - 8)
ctx.flush()
})
return <Canvas ref={canvasRef} width={size} height={size} />
}Interactive drawing canvas
Handle pointer events in React, re-draw on each move:
import { Canvas, View, type CanvasRef } from 'veloxkit'
import { useRef, useCallback } from 'react'
function DrawingCanvas() {
const canvasRef = useRef<CanvasRef>(null)
const drawing = useRef(false)
const strokes = useRef<Array<Array<{x: number; y: number}>>>([])
const current = useRef<Array<{x: number; y: number}>>([])
const redraw = useCallback(() => {
const ctx = canvasRef.current
if (!ctx) return
ctx.clear()
ctx.strokeStyle = '#00A878'
ctx.lineWidth = 2
for (const stroke of [...strokes.current, current.current]) {
for (let i = 0; i < stroke.length - 1; i++) {
ctx.strokeLine(stroke[i].x, stroke[i].y, stroke[i+1].x, stroke[i+1].y)
}
}
ctx.flush()
}, [])
return (
<View
style={{ width: 600, height: 400, borderRadius: 8, borderWidth: 1, borderColor: '#2A2A3A' }}
onPointerDown={(e) => {
drawing.current = true
current.current = [{ x: e.x, y: e.y }]
}}
onPointerMove={(e) => {
if (!drawing.current) return
current.current.push({ x: e.x, y: e.y })
redraw()
}}
onPointerUp={() => {
drawing.current = false
strokes.current.push([...current.current])
current.current = []
}}
>
<Canvas ref={canvasRef} width={600} height={400} />
</View>
)
}