Canvas Drawing and Annotation Implementation for Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1215
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1043
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Canvas Drawing and Annotation Implementation

Canvas drawing tools are needed in document verification systems (PDF annotation), educational platforms (teacher's whiteboard), design tools, and project management systems with visual diagrams. Technically, this involves working with 2D Context or WebGL over <canvas>, handling pointer events, and serializing state.

Architecture: immediate mode vs retained mode

Immediate mode (pure Canvas 2D) — we draw pixels directly. Fast, no overhead. Difficult: no object model, hit-testing has to be written manually.

Retained mode (Fabric.js, Konva.js) — object model over canvas. Each shape is an object that can be selected, moved, modified. Higher memory cost, but much more convenient for editors.

For annotation with shape selection and editing — use retained mode (Konva or Fabric). For freehand drawing with brushes — immediate mode with pressure sensitivity.

Freehand drawing (Canvas 2D)

function DrawingCanvas() {
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const isDrawingRef = useRef(false)
  const lastPointRef = useRef<{ x: number; y: number } | null>(null)

  const [tool, setTool] = useState<'pen' | 'eraser'>('pen')
  const [color, setColor] = useState('#2563eb')
  const [lineWidth, setLineWidth] = useState(3)

  function getPoint(e: PointerEvent): { x: number; y: number } {
    const rect = canvasRef.current!.getBoundingClientRect()
    const scaleX = canvasRef.current!.width / rect.width
    const scaleY = canvasRef.current!.height / rect.height
    return {
      x: (e.clientX - rect.left) * scaleX,
      y: (e.clientY - rect.top) * scaleY,
    }
  }

  useEffect(() => {
    const canvas = canvasRef.current!
    const ctx = canvas.getContext('2d')!

    function onPointerDown(e: PointerEvent) {
      canvas.setPointerCapture(e.pointerId)
      isDrawingRef.current = true
      lastPointRef.current = getPoint(e)

      // Point on click without movement
      ctx.beginPath()
      ctx.arc(lastPointRef.current.x, lastPointRef.current.y, lineWidth / 2, 0, Math.PI * 2)
      ctx.fillStyle = tool === 'eraser' ? '#ffffff' : color
      ctx.fill()
    }

    function onPointerMove(e: PointerEvent) {
      if (!isDrawingRef.current || !lastPointRef.current) return

      const point = getPoint(e)

      ctx.globalCompositeOperation = tool === 'eraser' ? 'destination-out' : 'source-over'
      ctx.strokeStyle = color
      ctx.lineWidth = tool === 'eraser' ? lineWidth * 4 : lineWidth
      ctx.lineCap = 'round'
      ctx.lineJoin = 'round'

      // Quadratic bezier for smooth lines
      ctx.beginPath()
      ctx.moveTo(lastPointRef.current.x, lastPointRef.current.y)
      ctx.quadraticCurveTo(
        lastPointRef.current.x,
        lastPointRef.current.y,
        (lastPointRef.current.x + point.x) / 2,
        (lastPointRef.current.y + point.y) / 2
      )
      ctx.stroke()

      lastPointRef.current = point
    }

    function onPointerUp() {
      isDrawingRef.current = false
      lastPointRef.current = null
    }

    canvas.addEventListener('pointerdown', onPointerDown)
    canvas.addEventListener('pointermove', onPointerMove)
    canvas.addEventListener('pointerup', onPointerUp)
    canvas.addEventListener('pointercancel', onPointerUp)

    return () => {
      canvas.removeEventListener('pointerdown', onPointerDown)
      canvas.removeEventListener('pointermove', onPointerMove)
      canvas.removeEventListener('pointerup', onPointerUp)
      canvas.removeEventListener('pointercancel', onPointerUp)
    }
  }, [tool, color, lineWidth])

  return (
    <div>
      <canvas
        ref={canvasRef}
        width={1200}
        height={800}
        style={{ width: '100%', touchAction: 'none', cursor: 'crosshair' }}
        className="border rounded bg-white"
      />
    </div>
  )
}

touchAction: none is mandatory, otherwise the browser intercepts touch for scrolling.

Annotation with Konva.js

npm install konva react-konva
import { Stage, Layer, Line, Rect, Circle, Text, Transformer } from 'react-konva'
import Konva from 'konva'

type AnnotationType = 'line' | 'rect' | 'circle' | 'arrow' | 'text'

interface Annotation {
  id: string
  type: AnnotationType
  points?: number[]
  x?: number
  y?: number
  width?: number
  height?: number
  text?: string
  color: string
}

function AnnotationTool({ backgroundImage }: { backgroundImage: string }) {
  const [annotations, setAnnotations] = useState<Annotation[]>([])
  const [selectedId, setSelectedId] = useState<string | null>(null)
  const [activeTool, setActiveTool] = useState<AnnotationType>('rect')
  const [isDrawing, setIsDrawing] = useState(false)
  const stageRef = useRef<Konva.Stage>(null)

  function getRelativePosition() {
    const stage = stageRef.current!
    const pos = stage.getPointerPosition()!
    return { x: pos.x, y: pos.y }
  }

  function handleMouseDown() {
    setSelectedId(null)
    const pos = getRelativePosition()
    const newAnnotation: Annotation = {
      id: crypto.randomUUID(),
      type: activeTool,
      color: '#ef4444',
      x: pos.x,
      y: pos.y,
      width: 0,
      height: 0,
    }

    if (activeTool === 'line') {
      newAnnotation.points = [pos.x, pos.y, pos.x, pos.y]
    }

    setAnnotations((prev) => [...prev, newAnnotation])
    setIsDrawing(true)
  }

  function handleMouseMove() {
    if (!isDrawing) return
    const pos = getRelativePosition()
    const lastIndex = annotations.length - 1
    const last = annotations[lastIndex]

    const updated = { ...last }
    if (activeTool === 'line') {
      updated.points = [last.points![0], last.points![1], pos.x, pos.y]
    } else {
      updated.width = pos.x - last.x!
      updated.height = pos.y - last.y!
    }

    setAnnotations((prev) => [...prev.slice(0, lastIndex), updated])
  }

  function handleMouseUp() {
    setIsDrawing(false)
  }

  return (
    <Stage
      ref={stageRef}
      width={800}
      height={600}
      onMouseDown={handleMouseDown}
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
    >
      <Layer>
        {annotations.map((ann) => {
          if (ann.type === 'rect') {
            return (
              <Rect
                key={ann.id}
                x={ann.x}
                y={ann.y}
                width={ann.width}
                height={ann.height}
                stroke={ann.color}
                strokeWidth={2}
                fill="transparent"
                onClick={() => setSelectedId(ann.id)}
                draggable={selectedId === ann.id}
              />
            )
          }
          if (ann.type === 'line') {
            return (
              <Line
                key={ann.id}
                points={ann.points}
                stroke={ann.color}
                strokeWidth={2}
                lineCap="round"
              />
            )
          }
          return null
        })}
      </Layer>
    </Stage>
  )
}

Undo/Redo with state stack

function useUndoRedo<T>(initialState: T) {
  const [history, setHistory] = useState<T[]>([initialState])
  const [cursor, setCursor] = useState(0)

  const current = history[cursor]

  function push(newState: T) {
    // Trim history after current position (branching)
    const newHistory = [...history.slice(0, cursor + 1), newState]
    setHistory(newHistory)
    setCursor(newHistory.length - 1)
  }

  function undo() {
    if (cursor > 0) setCursor((c) => c - 1)
  }

  function redo() {
    if (cursor < history.length - 1) setCursor((c) => c + 1)
  }

  return { current, push, undo, redo, canUndo: cursor > 0, canRedo: cursor < history.length - 1 }
}

Export annotated image

function exportAnnotated(stage: Konva.Stage, bgImage: HTMLImageElement) {
  const canvas = document.createElement('canvas')
  canvas.width = stage.width()
  canvas.height = stage.height()
  const ctx = canvas.getContext('2d')!

  // Background first
  ctx.drawImage(bgImage, 0, 0)

  // Annotations on top from Konva
  const stageCanvas = stage.toCanvas()
  ctx.drawImage(stageCanvas, 0, 0)

  canvas.toBlob((blob) => {
    const url = URL.createObjectURL(blob!)
    const a = document.createElement('a')
    a.href = url
    a.download = 'annotated.png'
    a.click()
    URL.revokeObjectURL(url)
  })
}

What we do

Analyze the scenario: freehand drawing, document annotation, schematic diagrams. Choose an approach (immediate/retained), implement tools (brush, shapes, text, arrows), undo/redo, export. If needed, add synchronization via WebSocket for collaborative drawing.

Timeline: basic canvas with brush and shapes — 2–3 days. PDF annotation with saving — 5–7 days.