Plugins
Create a plugin when you need to:
- Add custom ECS components with specific data types
- Run systems that execute logic every frame
- Define new block types with custom behavior
- Package functionality for reuse across projects
For a deep dive into the Entity Component System architecture, see the woven-ecs documentation.
Plugin Structure
Section titled “Plugin Structure”A plugin is an object that implements the EditorPlugin interface:
import type { EditorPlugin } from "@woven-canvas/core";
export function PotionPlugin(options = {}): EditorPlugin { return { name: "potions",
// ECS components to register components: [],
// Singleton components (one per world) singletons: [],
// Block type definitions blockDefs: [],
// Systems that run each frame systems: [],
// Keyboard shortcut bindings keybinds: [],
// Custom cursor definitions cursors: {},
// Plugin configuration (accessible at runtime) resources: {},
// Plugin dependencies dependencies: [], };}Defining Components
Section titled “Defining Components”Components store data on entities. Use defineCanvasComponent to create typed components:
import { defineCanvasComponent, field } from "@woven-canvas/core";
export const HslColor = defineCanvasComponent("hsl-color", { hue: field.number().default(0), saturation: field.number().default(100), lightness: field.number().default(50),});
export const Potion = defineCanvasComponent("potion", { name: field.string().default("Mystery Potion"), effect: field.string().default(""), brewed: field.boolean().default(false), potency: field.enum(["weak", "standard", "potent"]).default("standard"),});Field Types
Section titled “Field Types”| Field Type | Description |
|---|---|
field.string().max(n) | String value (max length required) |
field.boolean() | Boolean value |
field.enum({...}) | Enumerated string values |
field.ref() | Reference to another entity |
field.array(type, maxLength) | Fixed-size array |
field.tuple(type, length) | Fixed-size tuple (e.g., coordinates) |
field.buffer(type).size(n) | Typed buffer for zero-allocation views |
field.binary() | Binary data (Uint8Array) |
field.float64() | 64-bit floating point |
field.float32() | 32-bit floating point |
field.uint8() | Unsigned 8-bit integer (0-255) |
field.uint16() | Unsigned 16-bit integer |
field.uint32() | Unsigned 32-bit integer |
field.int8() | Signed 8-bit integer |
field.int16() | Signed 16-bit integer |
field.int32() | Signed 32-bit integer |
All field types support .default(value) to set a default value.
Components can also specify a sync behavior to control how data is persisted and synced in multiplayer. See Sync Behaviors for details.
Defining Block Definitions
Section titled “Defining Block Definitions”Block definitions configure how blocks behave:
import { ResizeMode } from "@woven-canvas/core";
blockDefs: [ { tag: "potion-card", // Block type identifier resizeMode: ResizeMode.Free, // Free transform resizing canRotate: true, // Allow rotation canScale: true, // Allow scaling stratum: "content", // 'background', 'content', or 'overlay' components: [HslColor, Potion], // Additional components for this block type connectors: { enabled: true }, // Arrow connection support },];Block Options
Section titled “Block Options”| Option | Type | Default | Description |
|---|---|---|---|
tag | string | required | Unique block type identifier |
resizeMode | ResizeMode | ResizeMode.Scale | Scale (maintain aspect), Free (stretch) |
canRotate | boolean | true | Allow rotation |
canScale | boolean | true | Allow scaling |
stratum | string | 'content' | Render layer |
components | array | [] | Components added to new blocks |
connectors | object | { enabled: true } | Arrow connection config |
The Block component is always included automatically on every block entity.
Defining Commands
Section titled “Defining Commands”Commands are typed actions that can be dispatched and consumed:
import { defineCommand } from "@woven-canvas/core";
// Command with no payloadexport const BrewPotion = defineCommand<void>("brew-potion");
// Command with typed payloadexport const SetPotency = defineCommand<{ entityId: number; potency: "weak" | "standard" | "potent";}>("set-potency");Defining Systems
Section titled “Defining Systems”Systems run each frame to process commands and update state:
import { defineEditorSystem, defineQuery, Selected } from "@woven-canvas/core";
// Query for entities matching criteriaconst selectedPotions = defineQuery((q) => q.with(Potion, Selected));
// System that runs in the 'update' phaseconst handlePotencySystem = defineEditorSystem({ phase: "update" }, (ctx) => { // Consume commands for (const cmd of SetPotency.consume(ctx)) { const potion = Potion.write(ctx, cmd.entityId); potion.potency = cmd.potency; }});The Tick Loop
Section titled “The Tick Loop”The editor runs a continuous loop that processes input and updates state:
┌─────────────────────────────────────────┐│ ││ Input → Capture → Update → Render ││ ↑ │ ││ └───────────────────────────┘ ││ │└─────────────────────────────────────────┘Each system is assigned a phase, and each phase has a specific purpose:
| Phase | Description |
|---|---|
input | Convert raw DOM events to ECS state (keyboard, mouse, pointer) |
capture | Detect targets, compute intersections, process keybinds |
update | Modify document state, process commands |
render | Sync ECS state to output (DOM, canvas) |
Defining Keybinds
Section titled “Defining Keybinds”Map keyboard shortcuts to commands:
import { Key } from "@woven-canvas/core";
keybinds: [ { command: BrewPotion.name, key: Key.Enter, mod: true, // Require Ctrl/Cmd shift: false, // Require Shift alt: false, // Require Alt },];Defining Cursors
Section titled “Defining Cursors”Define custom SVG cursors for your tools:
import type { CursorDef } from "@woven-canvas/core";
const POTION_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">...</svg>`;
const potionCursor: CursorDef = { makeSvg: () => POTION_SVG, hotspot: [2, 22], // Click point [x, y] in pixels rotationOffset: 0, // Base rotation in radians};
// In plugin definitioncursors: { potion: potionCursor,}Use the cursor name in your toolbar button:
<ToolbarButton name="potion-tool" cursor="potion" :placement-snapshot="snapshot"/>Plugin Resources
Section titled “Plugin Resources”Resources store plugin configuration accessible at runtime:
import { getPluginResources } from '@woven-canvas/core'
// In plugin definitionresources: { defaultPotency: options.defaultPotency ?? 'standard', maxPotions: options.maxPotions ?? 100,}
// Accessing in a systemconst mySystem = defineEditorSystem({ phase: 'update' }, (ctx) => { const resources = getPluginResources<PotionPluginResources>(ctx, 'potions') console.log(resources.defaultPotency)})Plugin Dependencies
Section titled “Plugin Dependencies”Declare plugins your plugin depends on:
export function PotionPlugin(): EditorPlugin { return { name: "potions", dependencies: ["canvas-controls"], // Requires CanvasControlsPlugin // ... };}Dependencies are automatically loaded before your plugin.
Complete Example
Section titled “Complete Example”Here’s a complete plugin that adds potion cards:
import type { CursorDef, EditorPlugin } from "@woven-canvas/core";import { defineCanvasComponent, defineCommand, defineEditorSystem, defineQuery, field, Key, Selected,} from "@woven-canvas/core";
// Componentsexport const HslColor = defineCanvasComponent("hsl-color", { hue: field.number().default(280), saturation: field.number().default(80), lightness: field.number().default(50),});
export const Potion = defineCanvasComponent("potion", { name: field.string().default("Mystery Potion"), brewed: field.boolean().default(false),});
// Commandexport const BrewPotion = defineCommand<void>("brew-potion");
// Queryconst selectedPotions = defineQuery((q) => q.with(Potion, Selected));
// Systemconst brewPotionSystem = defineEditorSystem({ phase: "update" }, (ctx) => { for (const _cmd of BrewPotion.consume(ctx)) { for (const entityId of selectedPotions.current(ctx)) { const potion = Potion.write(ctx, entityId); potion.brewed = !potion.brewed; } }});
// Cursorconst POTION_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">...</svg>`;
const potionCursor: CursorDef = { makeSvg: () => POTION_SVG, hotspot: [12, 20], rotationOffset: 0,};
// Plugin factoryexport function PotionPlugin(): EditorPlugin { return { name: "potions", components: [HslColor, Potion], systems: [brewPotionSystem], keybinds: [{ command: BrewPotion.name, key: Key.Enter, mod: true }], cursors: { potion: potionCursor }, blockDefs: [ { tag: "potion-card", resizeMode: ResizeMode.Free, canRotate: false, components: [HslColor, Potion], }, ], };}Using Your Plugin
Section titled “Using Your Plugin”<script setup lang="ts">import { WovenCanvas, SelectTool, HandTool } from "@woven-canvas/vue";import { PotionPlugin } from "./PotionPlugin";import PotionCard from "./PotionCard.vue";import PotionTool from "./PotionTool.vue";</script>
<template> <WovenCanvas :editor="{ plugins: [PotionPlugin()] }"> <template #toolbar> <div class="toolbar"> <SelectTool /> <HandTool /> <PotionTool /> </div> </template> <template #block:potion-card="props"> <PotionCard v-bind="props" /> </template> </WovenCanvas></template>See the Create a Custom Block and Create a Plugin examples for more practical patterns.