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.
- ✅ No Dependencies
- ✅ ES Modules
- ✅ Full Browser Support
- ✅ 100% Typescript
demo.mov
- 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)
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;
// 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();
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;
}
}