๐Ÿ‡ฎ๐Ÿ‡ณ
๐Ÿ‡ฎ๐Ÿ‡ณ
Republic Day Special Offer!Get 20% OFF on all courses
Enroll Now
P
Prakalpana
๐Ÿ“šLearn
โ€ขCode Your Future
Interview Prepโฑ๏ธ 25 min read๐Ÿ“… Dec 22

UI Interview: Design Figma/Whiteboard Canvas (Complete Mock)

AK
Arjun Krishnanโ€ขStaff Engineer at Figma
๐Ÿ“‘ Contents (13 sections)

๐Ÿ“ŒThe Interview Setting

Company: Figma Round: Frontend System Design Duration: 60 minutes

๐Ÿ“Œ๐ŸŽค Interviewer: "Design a collaborative whiteboard canvas."

Candidate's Clarifications:

  • 1What shapes? (Rectangle, circle, text, freehand?)
  • 2Collaboration - how many concurrent users?
  • 3Do we need undo/redo?
  • 4Zoom and pan support?
  • Interviewer: "Basic shapes, up to 10 users real-time, yes to undo/redo and zoom/pan."

    ๐Ÿ“Œ๐Ÿ“ Core Data Structures

    // Shape base type
    interface Shape {
    id: string;
    type: 'rectangle' | 'circle' | 'text' | 'line';
    x: number;
    y: number;
    width: number;
    height: number;
    rotation: number;
    fill: string;
    stroke: string;
    strokeWidth: number;
    zIndex: number;
    createdBy: string;
    createdAt: number;
    }
    // Canvas state
    interface CanvasState {
    shapes: Map<string, Shape>;
    selectedIds: Set<string>;
    viewport: { x: number; y: number; zoom: number };
    history: HistoryStack;
    }

    ๐Ÿ“Œ๐ŸŽค Interviewer: "How do you render thousands of shapes efficiently?"

    Candidate: "Use Canvas API with dirty rectangle optimization..."

    class CanvasRenderer {
    constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.shapes = new Map();
    this.dirtyRegions = [];
    this.rafId = null;
    }
    // Only redraw changed areas
    markDirty(region) {
    this.dirtyRegions.push(region);
    this.scheduleRender();
    }
    scheduleRender() {
    if (this.rafId) return;
    this.rafId = requestAnimationFrame(() => {
    this.render();
    this.rafId = null;
    });
    }
    render() {
    const { ctx, shapes, dirtyRegions } = this;
    if (dirtyRegions.length === 0) {
    // Full redraw
    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.renderAllShapes();
    } else {
    // Partial redraw - only dirty regions
    for (const region of dirtyRegions) {
    ctx.save();
    ctx.beginPath();
    ctx.rect(region.x, region.y, region.width, region.height);
    ctx.clip();
    ctx.clearRect(region.x, region.y, region.width, region.height);
    this.renderShapesInRegion(region);
    ctx.restore();
    }
    }
    this.dirtyRegions = [];
    }
    renderAllShapes() {
    // Sort by zIndex
    const sorted = [...this.shapes.values()].sort((a, b) => a.zIndex - b.zIndex);
    for (const shape of sorted) {
    this.renderShape(shape);
    }
    }
    renderShape(shape) {
    const { ctx } = this;
    ctx.save();
    // Apply transformations
    ctx.translate(shape.x + shape.width / 2, shape.y + shape.height / 2);
    ctx.rotate((shape.rotation * Math.PI) / 180);
    ctx.translate(-shape.width / 2, -shape.height / 2);
    // Draw based on type
    switch (shape.type) {
    case 'rectangle':
    ctx.fillStyle = shape.fill;
    ctx.strokeStyle = shape.stroke;
    ctx.lineWidth = shape.strokeWidth;
    ctx.fillRect(0, 0, shape.width, shape.height);
    ctx.strokeRect(0, 0, shape.width, shape.height);
    break;
    case 'circle':
    ctx.beginPath();
    ctx.ellipse(
    shape.width / 2, shape.height / 2,
    shape.width / 2, shape.height / 2,
    0, 0, Math.PI * 2
    );
    ctx.fillStyle = shape.fill;
    ctx.fill();
    ctx.strokeStyle = shape.stroke;
    ctx.stroke();
    break;
    }
    ctx.restore();
    }
    }

    ๐Ÿ“Œ๐ŸŽค Interviewer: "How do you implement undo/redo?"

    Candidate: "Command pattern with operation-based history..."

    class HistoryManager {
    constructor() {
    this.undoStack = [];
    this.redoStack = [];
    this.maxSize = 100;
    }
    execute(command) {
    command.execute();
    this.undoStack.push(command);
    this.redoStack = []; // Clear redo on new action
    // Limit history size
    if (this.undoStack.length > this.maxSize) {
    this.undoStack.shift();
    }
    }
    undo() {
    if (this.undoStack.length === 0) return;
    const command = this.undoStack.pop();
    command.undo();
    this.redoStack.push(command);
    }
    redo() {
    if (this.redoStack.length === 0) return;
    const command = this.redoStack.pop();
    command.execute();
    this.undoStack.push(command);
    }
    }
    // Command implementations
    class MoveCommand {
    constructor(shapeId, fromPos, toPos, canvas) {
    this.shapeId = shapeId;
    this.fromPos = fromPos;
    this.toPos = toPos;
    this.canvas = canvas;
    }
    execute() {
    this.canvas.updateShape(this.shapeId, this.toPos);
    }
    undo() {
    this.canvas.updateShape(this.shapeId, this.fromPos);
    }
    }
    class CreateCommand {
    constructor(shape, canvas) {
    this.shape = shape;
    this.canvas = canvas;
    }
    execute() {
    this.canvas.addShape(this.shape);
    }
    undo() {
    this.canvas.removeShape(this.shape.id);
    }
    }

    ๐Ÿ“Œ๐ŸŽค Interviewer: "How do you handle real-time collaboration?"

    Candidate: "CRDT or Operational Transform for conflict resolution..."

    // Using Yjs for CRDT
    import * as Y from 'yjs';
    import { WebsocketProvider } from 'y-websocket';
    const setupCollaboration = (roomId) => {
    const ydoc = new Y.Doc();
    const provider = new WebsocketProvider(
    'wss://collab.figma.com',
    roomId,
    ydoc
    );
    // Shared shapes map
    const yShapes = ydoc.getMap('shapes');
    // Local to remote
    const updateShape = (shapeId, updates) => {
    const shape = yShapes.get(shapeId);
    yShapes.set(shapeId, { ...shape, ...updates });
    };
    // Remote to local
    yShapes.observe((event) => {
    event.changes.keys.forEach((change, key) => {
    if (change.action === 'add') {
    renderer.addShape(yShapes.get(key));
    } else if (change.action === 'update') {
    renderer.updateShape(key, yShapes.get(key));
    } else if (change.action === 'delete') {
    renderer.removeShape(key);
    }
    });
    });
    // Cursor awareness
    const awareness = provider.awareness;
    awareness.setLocalState({
    user: { name: currentUser.name, color: currentUser.color },
    cursor: null
    });
    // Track other users' cursors
    awareness.on('change', () => {
    const states = awareness.getStates();
    renderCursors(states);
    });
    return { ydoc, yShapes, awareness };
    };

    ๐Ÿ“Œ๐ŸŽค Interviewer: "What about zoom and pan?"

    Candidate:

    const useViewport = () => {
    const [viewport, setViewport] = useState({ x: 0, y: 0, zoom: 1 });
    // Pan with middle mouse or space + drag
    const handlePan = (deltaX, deltaY) => {
    setViewport(v => ({
    ...v,
    x: v.x - deltaX / v.zoom,
    y: v.y - deltaY / v.zoom
    }));
    };
    // Zoom toward cursor position
    const handleZoom = (delta, cursorX, cursorY) => {
    setViewport(v => {
    const zoomFactor = delta > 0 ? 1.1 : 0.9;
    const newZoom = Math.min(Math.max(v.zoom * zoomFactor, 0.1), 10);
    // Zoom toward cursor
    const worldX = (cursorX - v.x) / v.zoom;
    const worldY = (cursorY - v.y) / v.zoom;
    return {
    x: cursorX - worldX * newZoom,
    y: cursorY - worldY * newZoom,
    zoom: newZoom
    };
    });
    };
    // Convert screen coords to world coords
    const screenToWorld = (screenX, screenY) => ({
    x: (screenX - viewport.x) / viewport.zoom,
    y: (screenY - viewport.y) / viewport.zoom
    });
    return { viewport, handlePan, handleZoom, screenToWorld };
    };

    ๐Ÿ“Œ๐Ÿ“Š Interview Scoring

    CriteriaScore

    Data Structuresโœ… Canvas Renderingโœ… Undo/Redoโœ… Real-time Syncโœ… Zoom/Pan Mathโœ…

    Result: Strong Hire ๐ŸŽ‰

    AK

    Written by

    Arjun Krishnan

    Staff Engineer at Figma

    ๐Ÿš€ Master Interview Prep

    Join 500+ developers

    Explore Courses โ†’
    Chat on WhatsApp