Plugin Authoring
Plugins are the primary extension mechanism in Supermouse. The core runner exists solely to coordinate them. You can write plugins in two ways: using the Raw Interface (for full control and logic-only utilities) or the definePlugin Helper (recommended for single-element visual layers).
A The Raw Interface (The Source of Truth)
At its simplest, a plugin is just an object with a unique name and standard lifecycle hooks. You don't need any helper functions to author a plugin. Understanding this raw structure is crucial for advanced use cases, such as plugins that manage multiple DOM elements, coordinate dynamic canvas contexts, or handle logic-only operations.
// A "Raw" Visual Plugin
// No helpers. Just the lifecycle methods.
export const RawSquare = () => {
let el: HTMLElement;
return {
name: 'raw-square',
install(app) {
// 1. Create DOM
el = document.createElement('div');
Object.assign(el.style, {
width: '20px',
height: '20px',
backgroundColor: 'red',
position: 'absolute',
pointerEvents: 'none' // Critical so it doesn't block clicks
});
// 2. Mount
app.container.appendChild(el);
},
update(app) {
// 3. Render Loop
const { x, y } = app.state.smooth;
// Use translate3d for GPU acceleration
el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
},
destroy(app) {
// 4. Cleanup
el.remove();
}
};
};B The Helper Strategy (definePlugin)
For 90% of visual plugins, you want to create a single DOM element, dynamically style it based on options, and center it on the cursor. The definePlugin utility provides a standard wrapper that handles the lifecycle, DOM mounting, and configuration watch boilerplate for you.
import { definePlugin, normalize, dom } from '@supermousejs/utils';
export const SmartSquare = (options = {}) => {
// Normalize allows users to pass static values ('blue')
// OR reactive getters (state => state.isHover ? 'red' : 'blue')
const getSize = normalize(options.size, 20);
return definePlugin({
name: 'smart-square',
// 1. Setup: Create and return the primary element.
// The helper handles appending to app.container and cleanup.
create: (app) => {
// dom.createActor creates a div with absolute position & pointer-events: none
return dom.createActor('div');
},
// 2. Auto-Binding: Map options keys to CSS properties.
// The helper watches 'options.color' and updates 'el.style.backgroundColor'.
// If 'options.color' is a function, it re-evaluates it every frame.
styles: {
color: 'backgroundColor',
opacity: 'opacity'
},
// 3. Loop: 'el' is passed in automatically.
update: (app, el, dt) => {
// Manual updates for things that aren't simple CSS mappings
const size = getSize(app.state);
dom.setStyle(el, 'width', `${size}px`);
dom.setStyle(el, 'height', `${size}px`);
const { x, y } = app.state.smooth;
// dom.setTransform handles centering (-50%, -50%) automatically
dom.setTransform(el, x, y);
}
}, options);
};The styles property in definePlugin is a declarative map. It tells the runtime: "Take the value of options.key and assign it to element.style.property every frame."
It is optimized internally to perform dirty-checking, ensuring style changes only touch the DOM if their evaluated value changes.
C Logic Plugins
Logic plugins manipulate the cursor's intent (its destination or bounds) rather than its rendering. They typically run before visual plugins (using negative priority) to modify state.target. Because they rarely create DOM nodes, they should be written using the Raw Interface.
export const Gravity = (intensity = 5) => ({
name: 'gravity',
// Critical: Run BEFORE visual plugins (which are usually priority 0)
// Logic modifies the 'target'. Visuals read the 'target' (indirectly via smooth).
priority: -10,
update(app, dt) {
// Pull the target down every frame
app.state.target.y += intensity;
}
});Handling Interaction
Supermouse enforces a strict **DOM Firewall** for rendering performance. You should **never** query the DOM or read layout properties (such as getBoundingClientRect() or getComputedStyle()) inside your plugin's update() loop. Doing so causes synchronous layout calculations, known as **Layout Thrashing**, which destroys 120fps+ smoothness.
Instead, read scraped attributes from state.interaction. The core input layer listens for hover changes and pre-scrapes any data-supermouse-* attributes on the hovered target, making them instantly available to your plugin update cycles in an O(1) dictionary.
// ❌ BAD: Layout Thrashing (DOM Firewall Violation)
// Reading DOM properties forces the browser to recalculate layout mid-frame.
// const data = el.getAttribute('data-color');
// ✅ GOOD: State Cache (DOM Firewall Adhered)
// The Input system pre-scrapes attributes on mouseover.
// Access is O(1).
const color = app.state.interaction.color;
if (color) {
dom.setStyle(el, 'backgroundColor', color);
}Lifecycle Hooks
Runs once when a plugin is registered via app.use(). Create your DOM structures and bind global event listeners here.
Runs every frame inside the animation loop. Keep this function lean and optimized for hot-path rendering.
Runs during runtime teardown or route shifts. Always clean up your created elements and unbind any event listeners to prevent memory leaks.
Called when a disabled plugin is re-enabled. Use this hook to restore element opacity or visibility.
Called when a plugin is disabled. Hide your visual elements but keep them in the DOM to avoid costly re-creation.
The Performance Contract
- 01.Do not create/destroy DOM elements in
update(). Pre-allocate or reuse elements. - 02.Do not read layouts (e.g. `getBoundingClientRect`) in update cycles. Cache values on target changes.
- 03.Use
dom.setTransform()for hardware-accelerated 3D transforms. Avoid CPU-heavy `top/left` styles. - 04.Respect
app.state.reducedMotion. When true, disable animation springiness and trails to support screen readability.