Skills/Editor Toolkit/Canvas Elements

ef-canvas

Attributes

data-element-id-attribute
string

Attribute name for element identification

Default:data-element-id
enable-transform-handles
boolean

Show transform handles for selected elements

Default:true

Properties

selectionContext
SelectionContext

Selection state and methods (provided via Lit context)

highlightedElement
HTMLElement | null

Currently highlighted (hovered) element

activeRootTemporal
TemporalMixinInterface & HTMLElement | null

Root temporal element containing current selection

Methods

registerElement(element: HTMLElement, id?: string): string

Register an element for canvas management

Returns: Element ID
unregisterElement(element: HTMLElement | string): void

Unregister an element

tryRegisterElement(element: HTMLElement): void

Try to register element, auto-generating ID if needed

updateElementPosition(elementId: string, x: number, y: number): void

Update element position in canvas coordinates

getElementData(elementId: string): CanvasElementData | null

Get element metadata

Returns: CanvasElementData | null
getAllElementsData(): CanvasElementData[]

Get metadata for all elements

Returns: CanvasElementData[]
screenToCanvasCoords(screenX: number, screenY: number): { x: number, y: number }

Convert screen coordinates to canvas coordinates

canvasToScreenCoords(canvasX: number, canvasY: number): { x: number, y: number }

Convert canvas coordinates to screen coordinates

setHighlightedElement(element: HTMLElement | null): void

Set the highlighted (hovered) element

Interactive canvas for managing, selecting, and manipulating elements with drag-and-drop and transform handles.

Basic Usage

Canvas automatically manages any child elements:

<div class="relative w-full h-[400px] border border-gray-300 rounded overflow-hidden">
<ef-pan-zoom class="w-full h-full">
<ef-canvas class="w-[1200px] h-[800px] bg-gray-100">
<div id="box-1" class="absolute top-8 left-8 w-32 h-32 bg-blue-500 text-white flex items-center justify-center rounded">
Box 1
</div>
<div id="box-2" class="absolute top-48 left-48 w-32 h-32 bg-green-500 text-white flex items-center justify-center rounded">
Box 2
</div>
<div id="box-3" class="absolute top-8 left-48 w-32 h-32 bg-red-500 text-white flex items-center justify-center rounded">
Box 3
</div>
</ef-canvas>
</ef-pan-zoom>
</div>

Click to select, drag to move, use handles to resize and rotate.

Selection

Canvas provides multiple selection modes:

Single Selection

Click an element to select it:

const canvas = document.querySelector('ef-canvas');
// Listen for selection changes
canvas.selectionContext.addEventListener('selectionchange', () => {
const selected = Array.from(canvas.selectionContext.selectedIds);
console.log('Selected:', selected);
});

Multi-Selection

Hold Shift to add to selection:

  • Click: Select single element (clear others)
  • Shift + Click: Add element to selection
  • Ctrl/Cmd + Click: Toggle element selection

Box Selection

Click and drag on empty space to select multiple elements:

<div class="relative w-full h-[400px] border border-gray-300 rounded overflow-hidden">
<ef-pan-zoom class="w-full h-full">
<ef-canvas class="w-[1200px] h-[800px] bg-gray-100">
<div id="item-1" class="absolute top-12 left-12 w-24 h-24 bg-blue-500 rounded"></div>
<div id="item-2" class="absolute top-12 left-48 w-24 h-24 bg-green-500 rounded"></div>
<div id="item-3" class="absolute top-12 left-84 w-24 h-24 bg-red-500 rounded"></div>
<div id="item-4" class="absolute top-48 left-12 w-24 h-24 bg-purple-500 rounded"></div>
<div id="item-5" class="absolute top-48 left-48 w-24 h-24 bg-yellow-500 rounded"></div>
<div id="item-6" class="absolute top-48 left-84 w-24 h-24 bg-pink-500 rounded"></div>
</ef-canvas>
</ef-pan-zoom>
</div>

Drag on empty space to draw selection box. Hold Shift to add to existing selection.

Drag and Drop

Canvas supports dragging selected elements:

Single Element Drag

Click and drag an element to move it:

// Element position updates automatically
// No event handlers needed

Multi-Element Drag

Select multiple elements and drag any selected element to move all:

const canvas = document.querySelector('ef-canvas');
// Select multiple elements
canvas.selectionContext.select('box-1');
canvas.selectionContext.addToSelection('box-2');
canvas.selectionContext.addToSelection('box-3');
// Drag any selected element - all move together

Drag Threshold

Canvas uses a 5-pixel drag threshold to distinguish clicks from drags.

Transform Handles

Canvas shows transform handles for selected elements:

Single Selection

Handles for resize and rotate:

  • Corner handles: Resize proportionally
  • Edge handles: Resize in one dimension
  • Rotation handle: Rotate around center

Multi-Selection

Handles for the bounding box:

  • Corner handles: Scale all elements proportionally
  • Rotation handle: Rotate all elements around group center

Disable Handles

<ef-canvas enable-transform-handles="false">
<!-- No transform handles shown -->
</ef-canvas>

Programmatic Control

Control canvas selection via JavaScript:

const canvas = document.querySelector('ef-canvas');
const { selectionContext } = canvas;
// Select single element
selectionContext.select('element-id');
// Add to selection
selectionContext.addToSelection('another-id');
// Toggle selection
selectionContext.toggle('element-id');
// Clear selection
selectionContext.clear();
// Get selected IDs
const selected = Array.from(selectionContext.selectedIds);

Element Registration

Canvas automatically registers child elements:

Automatic Registration

All child elements are auto-registered with auto-generated IDs:

<ef-canvas>
<div>Auto-registered with generated ID</div>
</ef-canvas>

Manual Registration

Provide explicit IDs for stable references:

<ef-canvas>
<div id="my-element">Registered as 'my-element'</div>
</ef-canvas>

Programmatic Registration

Register elements via JavaScript:

const canvas = document.querySelector('ef-canvas');
const element = document.createElement('div');
// Register with auto-generated ID
canvas.tryRegisterElement(element);
// Register with explicit ID
const id = canvas.registerElement(element, 'my-custom-id');
// Unregister
canvas.unregisterElement('my-custom-id');

Position Management

Update element positions in canvas coordinates:

const canvas = document.querySelector('ef-canvas');
// Update position (canvas coordinates)
canvas.updateElementPosition('element-id', 100, 200);
// Get element metadata
const data = canvas.getElementData('element-id');
console.log('Position:', data.x, data.y);
console.log('Size:', data.width, data.height);
console.log('Rotation:', data.rotation);
// Get all elements
const allElements = canvas.getAllElementsData();

Coordinate Conversion

Convert between screen and canvas coordinates:

const canvas = document.querySelector('ef-canvas');
// Screen to canvas
const canvasPos = canvas.screenToCanvasCoords(clientX, clientY);
// Canvas to screen
const screenPos = canvas.canvasToScreenCoords(canvasX, canvasY);

Coordinates account for pan/zoom transforms.

Hover Highlighting

Canvas tracks hovered elements:

const canvas = document.querySelector('ef-canvas');
// Get highlighted element
console.log(canvas.highlightedElement);
// Programmatically highlight
const element = document.getElementById('my-element');
canvas.setHighlightedElement(element);
// Clear highlight
canvas.setHighlightedElement(null);

Highlighted elements get data-highlighted attribute for styling:

[data-highlighted] {
outline: 2px solid blue;
}

Selection Styling

Selected elements get data-selected attribute:

[data-selected] {
outline: 2px solid #2196f3;
}

Pan/Zoom Integration

Canvas works seamlessly with ef-pan-zoom:

<ef-pan-zoom>
<ef-canvas>
<!-- Elements stay correctly positioned during pan/zoom -->
</ef-canvas>
</ef-pan-zoom>

Canvas automatically:

  • Converts coordinates accounting for zoom
  • Updates transform handles
  • Maintains correct hit testing

Active Root Temporal

Canvas tracks the root temporal element containing selection:

const canvas = document.querySelector('ef-canvas');
// Get active root temporal
console.log(canvas.activeRootTemporal);
// Listen for changes
canvas.addEventListener('activeroottemporalchange', (e) => {
console.log('Active root:', e.detail.activeRootTemporal);
});

Useful for timeline scrubbing and playback controls.

Nested Elements

Canvas supports nested element hierarchies:

<ef-canvas>
<div id="parent" class="absolute top-0 left-0 w-64 h-48">
<div id="child" class="absolute top-4 left-4 w-32 h-24">
<!-- Nested element -->
</div>
</div>
</ef-canvas>

Both parent and child are selectable and draggable.

Element Metadata

Canvas tracks element bounds and transforms:

interface CanvasElementData {
id: string;
element: HTMLElement;
x: number; // Top-left x in canvas coordinates
y: number; // Top-left y in canvas coordinates
width: number; // Width in canvas units
height: number; // Height in canvas units
rotation?: number; // Rotation in degrees
}

Metadata updates automatically when elements change.

Coordinate System

Canvas uses a unified coordinate calculation:

  • Dimensions: From offsetWidth/offsetHeight (unaffected by transforms)
  • Center: From getBoundingClientRect() center (rotation-invariant)
  • Top-left: Calculated from center minus half dimensions

This approach works correctly for rotated, scaled, and nested elements.

Performance

Canvas optimizes for interactive editing:

  • Single RAF loop for overlays and handles
  • Pointer capture during drag operations
  • Efficient hit testing with z-order respect
  • No change detection overhead

Deprecated: ef-canvas-item

The ef-canvas-item wrapper is deprecated. Use plain HTML elements:

<!-- Old (deprecated) -->
<ef-canvas>
<ef-canvas-item id="item-1">
<div>Content</div>
</ef-canvas-item>
</ef-canvas>
<!-- New (recommended) -->
<ef-canvas>
<div id="item-1">Content</div>
</ef-canvas>

All DOM nodes in canvas are now automatically selectable.

Events

activeroottemporalchange

Fired when active root temporal changes. See the Active Root Temporal section above.