Advanced

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.

RawPlugin.ts
// 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.

SmartSquare.ts
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);
};
What is the 'styles' mapping object?

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."

styles: { opacity: 'opacity' } // maps options.opacity -> style.opacity

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.

Gravity.ts
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.

Snippet
// ❌ 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

install(app)

Runs once when a plugin is registered via app.use(). Create your DOM structures and bind global event listeners here.

update(app, dt)

Runs every frame inside the animation loop. Keep this function lean and optimized for hot-path rendering.

destroy(app)

Runs during runtime teardown or route shifts. Always clean up your created elements and unbind any event listeners to prevent memory leaks.

onEnable(app)

Called when a disabled plugin is re-enabled. Use this hook to restore element opacity or visibility.

onDisable(app)

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.
supermouse
js
| Copyright © 2024-2026 Stud.io Inc.