Claude Code Plugins

Community-maintained marketplace

Feedback

drawing-canvas-implementation

@majiayu000/claude-skill-registry
3
0

Implement real-time collaborative drawing canvas with brush tools, color picker, undo/clear functionality, and canvas-to-image export. Use when building drawing UI, handling stroke synchronization, managing drawing state, or uploading drawings to storage.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name drawing-canvas-implementation
description Implement real-time collaborative drawing canvas with brush tools, color picker, undo/clear functionality, and canvas-to-image export. Use when building drawing UI, handling stroke synchronization, managing drawing state, or uploading drawings to storage.
compatibility HTML5 Canvas API, React hooks, real-time event handling, Convex file storage
metadata [object Object]

Drawing Canvas Implementation

Overview

This skill implements a full-featured Pictionary drawing canvas with real-time collaborative features, local drawing tools (brush, eraser, color picker), and integration with Convex file storage for drawing export and archival.

Core Architecture

Canvas State Management

interface DrawingState {
  strokes: Stroke[]; // Array of all strokes
  currentStroke: Point[]; // Current drawing in progress
  currentColor: string; // Hex color
  currentSize: number; // Brush size in pixels
  isErasing: boolean; // Eraser mode toggle
  canUndo: boolean; // Undo availability
}

interface Stroke {
  points: Point[]; // Array of coordinates
  color: string; // Stroke color
  size: number; // Brush size
  isEraser: boolean; // Is eraser stroke
  timestamp: number; // When drawn
}

interface Point {
  x: number;
  y: number;
  pressure?: number; // Pen pressure (0-1) for tablets
}

Component Structure

DrawingCanvas Component

Main canvas component with all drawing tools.

interface DrawingCanvasProps {
  gameId: Id<"games">;
  turnId: Id<"turns">;
  isDrawer: boolean; // Can draw if true
  onDrawingUpdate?: (strokes: Stroke[]) => void;
  onDrawingComplete?: (imageUrl: string) => void;
  timeLimit: number; // In seconds
}

Drawing Tools

Brush Tool

  • Mode: Normal drawing
  • Default Size: 3-20px adjustable
  • Color: Any hex color via picker
  • Pressure Sensitivity: Optional for tablet support

Eraser Tool

  • Mode: Removes strokes (or paints white)
  • Size: 3-50px adjustable
  • Option: Erase individual strokes or continuous area

Color Picker

  • Palette: Predefined colors + custom hex input
  • Current Color: Display + picker
  • Recent Colors: Last 5 used colors

Undo

  • Action: Remove last stroke
  • Limit: Full history (no limit)
  • Button State: Disabled when no strokes

Clear

  • Action: Clear entire canvas
  • Confirmation: Ask before clear (optional)

Event Handling

Mouse Events

// Mouse down - start stroke
canvas.addEventListener("mousedown", (e) => {
  const point = getCanvasPoint(e);
  currentStroke = [point];
  drawPoint(point);
});

// Mouse move - continue stroke
canvas.addEventListener("mousemove", (e) => {
  if (!isDrawing) return;
  const point = getCanvasPoint(e);
  currentStroke.push(point);
  drawLine(currentStroke[currentStroke.length - 2], point);
});

// Mouse up - finish stroke
canvas.addEventListener("mouseup", (e) => {
  if (currentStroke.length > 0) {
    strokes.push({
      points: currentStroke,
      color: currentColor,
      size: currentSize,
      isEraser: isErasing,
      timestamp: Date.now(),
    });
  }
  currentStroke = [];
  redrawCanvas();
});

Touch Events (Mobile)

canvas.addEventListener('touchstart', (e) => {
  const touch = e.touches[0];
  const point = getCanvasPoint(touch);
  currentStroke = [point];
});

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault();
  const touch = e.touches[0];
  const point = getCanvasPoint(touch);
  currentStroke.push(point);
  drawLine(currentStroke[currentStroke.length - 2], point);
});

canvas.addEventListener('touchend', (e) => {
  strokes.push({ points: currentStroke, ... });
  currentStroke = [];
  redrawCanvas();
});

Canvas Rendering

Line Drawing (Smooth)

function drawLine(from: Point, to: Point) {
  ctx.strokeStyle = currentColor;
  ctx.lineWidth = currentSize;
  ctx.lineJoin = "round";
  ctx.lineCap = "round";

  ctx.beginPath();
  ctx.moveTo(from.x, from.y);
  ctx.lineTo(to.x, to.y);
  ctx.stroke();
}

Full Canvas Redraw

function redrawCanvas() {
  // Clear canvas
  ctx.fillStyle = "#ffffff";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Redraw all strokes
  for (const stroke of strokes) {
    ctx.strokeStyle = stroke.color;
    ctx.lineWidth = stroke.size;

    ctx.beginPath();
    ctx.moveTo(stroke.points[0].x, stroke.points[0].y);

    for (let i = 1; i < stroke.points.length; i++) {
      ctx.lineTo(stroke.points[i].x, stroke.points[i].y);
    }
    ctx.stroke();
  }
}

Canvas Export & Upload

Capture Canvas as Image

function captureCanvas(): Promise<Blob> {
  return new Promise((resolve) => {
    canvas.toBlob(
      (blob) => {
        resolve(blob!);
      },
      "image/png",
      0.95
    );
  });
}

Upload to Convex Storage

const uploadDrawing = useAction(
  api.actions.uploadDrawing.uploadDrawingScreenshot
);

async function saveDrawing() {
  const blob = await captureCanvas();
  const storageId = await uploadDrawing({
    gameId,
    turnId,
    drawingBlob: await blob.arrayBuffer(), // Base64 or buffer
  });

  return storageId; // Can fetch later with getUrl()
}

Real-time Synchronization (Optional)

Broadcast Strokes

// On each stroke completion
const syncStrokes = useMutation(api.mutations.drawings.syncStrokes);

async function broadcastStroke(stroke: Stroke) {
  await syncStrokes({
    turn_id: turnId,
    stroke: stroke,
  });
}

Listen for Remote Strokes

const remoteStrokes = useQuery(api.queries.drawings.getTurnDrawing, {
  turn_id: turnId,
});

// Subscribe to updates
useEffect(() => {
  if (remoteStrokes?.strokes) {
    redrawCanvas(); // Redraw with remote strokes
  }
}, [remoteStrokes]);

State Management Pattern

export const DrawingCanvas = ({ isDrawer, ...props }: DrawingCanvasProps) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [strokes, setStrokes] = useState<Stroke[]>([]);
  const [currentColor, setCurrentColor] = useState("#000000");
  const [currentSize, setCurrentSize] = useState(5);
  const [isErasing, setIsErasing] = useState(false);

  useEffect(() => {
    if (!canvasRef.current) return;
    const canvas = canvasRef.current;
    const ctx = canvas.getContext("2d")!;

    // Event handlers...
    // Canvas drawing logic...
  }, [strokes, currentColor, currentSize, isErasing]);

  const handleUndo = () => {
    setStrokes(strokes.slice(0, -1));
  };

  const handleClear = () => {
    if (confirm("Clear canvas?")) {
      setStrokes([]);
    }
  };

  return (
    <div className="drawing-container">
      <canvas
        ref={canvasRef}
        width={800}
        height={600}
        className="border-2 border-gray-300"
        style={{ cursor: isDrawer ? "crosshair" : "default" }}
      />
      <ToolBar
        color={currentColor}
        onColorChange={setCurrentColor}
        size={currentSize}
        onSizeChange={setCurrentSize}
        isErasing={isErasing}
        onEraserToggle={setIsErasing}
        onUndo={handleUndo}
        onClear={handleClear}
        canUndo={strokes.length > 0}
      />
    </div>
  );
};

Canvas Sizing

Responsive Canvas

function resizeCanvas() {
  const container = canvas.parentElement!;
  const rect = container.getBoundingClientRect();

  canvas.width = rect.width;
  canvas.height = rect.height;

  redrawCanvas(); // Redraw at new size
}

window.addEventListener("resize", resizeCanvas);

Aspect Ratio Preservation

.drawing-container {
  aspect-ratio: 4 / 3;
  width: 100%;
  max-width: 800px;
}

canvas {
  width: 100%;
  height: 100%;
}

Performance Optimization

Stroke Batching

  • Buffer strokes locally, sync every 500ms
  • Reduces mutation calls
  • Improves responsiveness

Canvas Optimization

  • Use requestAnimationFrame for smooth drawing
  • Implement dirty rectangle invalidation
  • Clear only changed regions (advanced)

Memory Management

  • Limit stroke history to ~1000 strokes
  • Compress old strokes data if needed
  • Clear temporary Point arrays

Browser Compatibility

  • Chrome/Edge: Full support
  • Firefox: Full support
  • Safari: Full support (15+)
  • Mobile: Touch events supported

Common Patterns

Detect Drawing Changes

const hasDrawn = strokes.length > 0;

Save on Turn End

useEffect(() => {
  if (turnState === "completed") {
    saveDrawing();
  }
}, [turnState]);

Prevent Accidental Close

useEffect(() => {
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (strokes.length > 0) {
      e.preventDefault();
      e.returnValue = "";
    }
  };

  window.addEventListener("beforeunload", handleBeforeUnload);
  return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [strokes]);

See Also