🚧 VeloxKit is pre-release software. APIs may change before v1.0. Get started →
Documentation
Guides
Canvas 2D

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   // VeloxCanvasContext

Available drawing commands:

MethodDescription
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 string
  • ctx.strokeStyle — stroke color string
  • ctx.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>
  )
}