Custom Decorators
Decorators enhance stage elements with reusable visual effects and badges using .decorate().
What is a Decorator?
A decorator is a function with the ElementDecorator signature:
type ElementDecorator = (element: DOMElement) => void;
When you call element.decorate(myDecorator()), StageRoutine passes the target element into your function. Your decorator can modify styles, append child DOM/SVG elements, or hook into the element's lifecycle.
Visual Decorator
A decorator can append decorative DOM nodes and apply styling:
import type { DOMElement, ElementDecorator } from "stageroutine";
export interface CornerTagOptions {
text: string;
color?: string;
}
export function cornerTag(options: CornerTagOptions): ElementDecorator {
return (element: DOMElement) => {
const badge = document.createElement("span");
badge.innerText = options.text;
badge.style.position = "absolute";
badge.style.top = "-0.6rem";
badge.style.right = "-0.6rem";
badge.style.fontSize = "0.75rem";
badge.style.padding = "0.2rem 0.5rem";
badge.style.borderRadius = "9999px";
badge.style.background = options.color ?? "var(--sr-primary)";
badge.style.color = "var(--sr-surface)";
element.domElement.style.position = "relative";
element.domElement.appendChild(badge);
};
}
Use it in your presentation:
import { Card } from "stageroutine";
import { cornerTag } from "./cornerTag";
const card = Card("System Architecture").decorate(
cornerTag({ text: "v2.0", color: "#38bdf8" }),
);
Lifecycle-Aware Decorator
If your decorator manages active resources (like an interval or stream), hook into the element's lifecycle:
import type { DOMElement, ElementDecorator } from "stageroutine";
export function pulseBorder(): ElementDecorator {
return (element: DOMElement) => {
let intervalId: number | null = null;
let lit = false;
element.onActivate(() => {
intervalId = window.setInterval(() => {
lit = !lit;
element.domElement.style.borderColor = lit
? "var(--sr-primary)"
: "var(--sr-surface-border)";
}, 500);
});
element.onDeactivate(() => {
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
});
};
}
This guarantees that all timers stop when the element is not visible, preserving the zero GPU at rest guarantee.
Transition-Aware Decorator
Decorators can also animate custom visual properties during scene transitions. By hooking into element.onUpdate(), the decorator animates on StageRoutine's central clock without running independent animation frames:
import type { DOMElement, ElementDecorator } from "stageroutine";
export interface BlurEntranceOptions {
/** Entrance animation duration in seconds (default: 1.0). */
duration?: number;
/** Maximum blur radius in pixels at the start of entrance (default: 16). */
maxBlur?: number;
}
export function blurEntrance(options: BlurEntranceOptions = {}): ElementDecorator {
const { duration = 1.0, maxBlur = 16 } = options;
return (element: DOMElement) => {
element.enterDuration = duration;
element.onUpdate((progress) => {
// Clear filters when fully visible or hidden to preserve system resources
if (progress <= 0 || progress >= 1) {
element.domElement.style.filter = "";
return;
}
// Smoothly resolve blur as entrance progresses
const currentBlur = maxBlur * (1 - progress);
element.domElement.style.filter = `blur(${currentBlur.toFixed(1)}px)`;
});
element.onDeactivate(() => {
element.domElement.style.filter = "";
});
};
}
Because element.onUpdate() runs synchronously with the stage transition, the effect automatically rests when the step completes and restores previous states cleanly when navigating backward.