Drawing Board

✹ Apr 8Rio de Janeiro, BR000 view(s)

Canvas Screenshot 1

A draggable pen component built in Vue that lets visitors draw directly on any page. Strokes are stored in Supabase and synced across all visitors in real time via a broadcast channel, so drawings are shared and persistent.

Try it on the dedicated canvas, or the home page for a non persistent, local variant.

Features

  • Drag and pick up the pen, then draw freely on the page
  • Multiple pen types: pencil, crayon, brush, eraser
  • Configurable stroke color, width, and tip offsets per instance
  • Undo / redo with Ctrl+Z / Ctrl+Y
  • Local storage mode for single-device persistence
  • Cloud storage mode via Supabase realtime broadcast — strokes are shared across all open sessions on the same canvas ID
  • Scales to page height, with a configurable maxCanvasHeight
  • dragAndDraw mode for a fixed pen that draws while scrolling

AD Canvas Screenshot

Usage

<DrawablePen
  :cloudStorage="true"
  penEmoji="🖉"
  strokeColor="#111111"
  :strokeWidth="3"
/>

Multiple pens can be placed on the same page and will share the same canvas:

<DrawablePen :cloudStorage="true" penEmoji="🖉" />

<DrawablePen :cloudStorage="true" penEmoji="🖉" strokeColor="green" />

<DrawablePen :cloudStorage="true" penEmoji="🖍️" strokeColor="red" :tipOffsetX="5" :tipOffsetY="43" />

<DrawablePen :cloudStorage="true" penEmoji="🖌️" strokeColor="rgba(0,255,255,0.5)" :strokeWidth="25" />

<DrawablePen :cloudStorage="true" penEmoji="🧹" :eraserMode="true" :strokeWidth="80" />

The canvas ID defaults to the current page path, so each page gets its own shared drawing by default. Pass canvasId to override.

Canvas Screenshot 2

Pages using it

Canvas Screenshot 3

How it works

Each completed stroke is saved to a shared allStrokes array and broadcast to other sessions via a Supabase realtime channel. Incoming strokes from other users are applied immediately and the canvas is redrawn:

function setupSupabaseSync() {
  broadcastChannel = supabase
    .channel(`drawing:${effectiveCloudStorageId}:strokes`, {
      config: { broadcast: { self: false } },
    })
    .on('broadcast', { event: 'stroke_added' }, ({ payload }) => {
      if (payload.stroke) {
        allStrokes.push(payload.stroke)
        redrawAll()
      }
    })
    .on('broadcast', { event: 'stroke_removed' }, ({ payload }) => {
      const index = allStrokes.findIndex(s =>
        s.userId === payload.stroke.userId
        && s.timestamp === payload.stroke.timestamp
        && JSON.stringify(s.points) === JSON.stringify(payload.stroke.points),
      )
      if (index !== -1) {
        allStrokes.splice(index, 1)
        redrawAll()
      }
    })
    .on('broadcast', { event: 'clear' }, () => {
      allStrokes.length = 0
      redrawAll()
    })
    .subscribe()
}

When the mouse is released, the collected path points are packaged into a stroke object and broadcast to all connected clients:

function endDrawing() {
  if (isDrawing.value && currentPath.length > 0) {
    saveStroke({
      points: [...currentPath],
      color: currentStrokeColor.value,
      width: currentStrokeWidth.value,
      isEraser: props.eraserMode,
      userId: currentUserId,
      timestamp: Date.now(),
    })
  }
  isDrawing.value = false
  currentPath = []
}

Source

The full source is part of this site’s repository: DrawablePen.vue.


>