HTML Canvas Utilities

Demo Published on npm

Abstract canvas controller that can take a set of drawable children and render them to the canvas while hooking up events for pan, zoom, move and select.

Online Demo

  • ✅ No Dependencies
  • ✅ ES Modules
  • ✅ Full Browser Support
  • ✅ 100% Typescript
demo.mov

Features

  • Mouse wheel events
  • Keyboard navigation
  • Mobile pinch / zoom
  • Zoom at cursor
  • Mobile tap to select
  • Cursor changes
  • Theme API
  • Double click to select nested child
  • Relative children
  • Multi selection
  • Double click to edit text
  • Gesture events (Safari)

Theme

Light Theme

--canvas-background-color: #fafafa;
--canvas-grid-color: #ccc;
--canvas-selected-color: #f00;
--canvas-hovered-color: #0f0;
--canvas-text-color: #000;

Dark Theme

--canvas-background-color: #333;
--canvas-grid-color: #666;
--canvas-selected-color: #bd0303;
--canvas-hovered-color: #04a104;
--canvas-text-color: #fff;

Example

// Get the canvas
const canvas = document.querySelector('canvas');

// Attach the controller
const controller = new CanvasController(canvas);

// (Optional) Add a listener
controller.addListener(() => {
    const { offset, scale } = controller.info;
    console.debug(`offset: ${offset.x}, ${offset.y}; scale: ${scale}`);
});

// Add a child to control and render
controller.addChild({
    rect: new DOMRect(0, 0, 100, 100),
    draw: (ctx: CanvasRenderingContext2D) => {
        ctx.save();
        ctx.fillStyle = 'red';
        ctx.fillRect(0, 0, 100, 100);
        ctx.restore();
    },
});

// Add a child with nested children relative to the parent
controller.addChild({
    rect: new DOMRect(100, 100, 100, 100),
    draw: (ctx, size) => {
      ctx.fillStyle = "purple";
      ctx.fillRect(0, 0, size.width, size.height);
    },
    children: [
      {
        rect: new DOMRect(50, 50, 50, 50),
        draw: (ctx, size) => {
          ctx.fillStyle = "yellow";
          ctx.fillRect(0, 0, size.width, size.height);
        },
        children: [
          {
            rect: new DOMRect(12.5, 12.5, 25, 25),
            draw: (ctx, size) => {
              ctx.fillStyle = "magenta";
              ctx.fillRect(0, 0, size.width, size.height);
            },
          },
        ],
      },
    ],
});

// Start the animation loop
controller.init();

Lit Example

import { html, css, LitElement } from "lit";
import { customElement, query } from "lit/decorators.js";
import { CanvasController } from "./controller";
import { addRandomShapes } from "./shapes";

@customElement("canvas-editor")
export class CanvasEditor extends LitElement {
  static styles = css`
    canvas {
      --canvas-background-color: #fafafa;
      --canvas-grid-color: #ccc;
      --canvas-selected-color: #f00;
      --canvas-hovered-color: #0f0;
      width: 100%;
      height: 100%;
    }
    @media (prefers-color-scheme: dark) {
      canvas {
        --canvas-background-color: #333;
        --canvas-grid-color: #666;
        --canvas-selected-color: #bd0303;
        --canvas-hovered-color: #04a104;
      }
    }
  `;

  @query("#canvas") canvas!: HTMLCanvasElement;

  render() {
    return html` <canvas id="canvas"></canvas> `;
  }

  firstUpdated() {
    const controller = new CanvasController(this.canvas);
    controller.addListener(() => {
      const { offset, scale } = controller.info;
      console.debug(`offset: ${offset.x}, ${offset.y}; scale: ${scale}`);
    });  
    controller.addChild({
        rect: new DOMRect(0, 0, 100, 100),
        draw: (ctx: CanvasRenderingContext2D) => {
            ctx.save();
            ctx.fillStyle = 'red';
            ctx.fillRect(0, 0, 100, 100);
            ctx.restore();
        },
    });
    controller.init();
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "canvas-editor": CanvasEditor;
  }
}