Exotic Clips
Beyond the browser
When most people think of MotorCortex, they think of HTML, CSS, and animations in the browser. But the engine underneath is far more general than that.
At its core, MotorCortex is a timeline engine for entities. An entity can be anything — a DOM element, an SVG shape, a 3D mesh, a drone, a robotic arm, an audio synthesizer, a data point on a chart. The engine doesn't care what the entity is. It cares that it can be:
- Identified — by an id or class
- Selected — via a selector string
- Affected — by an Incident that changes its properties over time
HTMLClip is MotorCortex's built-in implementation of this idea for the DOM. BrowserClip extends it for canvas, WebGL, and other browser-native contexts. But the real foundation — the classes that make all of this possible — are ExtendableClip and ExtendableContextHandler.
When you extend these directly, you leave the browser behind entirely. You can build Clips for domains that have nothing to do with HTML.
ExtendableContextHandler — what is a context, really?
A context in MotorCortex is a registry of entities that can be addressed by selectors. That's it. No DOM required. No rendering assumptions.
ExtendableContextHandler is the abstract base that defines this contract. Every context handler must answer four questions:
- getElements(selector) — given a selector string, return the matching entities
- getMCID(element) — given an entity, return its unique MC identifier
- setMCID(element, mcid) — assign an MC identifier to an entity
- getElementSelectorByMCID(mcid) — given an MC identifier, return a selector string that would find it
That's the full interface. Implement these four methods and MotorCortex can target, track, and animate your entities — whatever they are.
import { ExtendableContextHandler } from "@donkeyclip/motorcortex";
class DroneContextHandler extends ExtendableContextHandler {
constructor(drones) {
super();
this.drones = {};
for (const drone of drones) {
this.drones[drone.id] = {
mcid: drone.id,
id: drone.id,
classes: drone.classes || [],
position: { x: 0, y: 0, z: 0 },
speed: 0,
heading: 0,
};
this.elementsByMCID[drone.id] = this.drones[drone.id];
}
this.setContext({ contextLoaded: true });
}
getElements(selector) {
if (selector.startsWith("#")) {
const id = selector.substring(1);
return this.drones[id] ? [this.drones[id]] : [];
}
if (selector.startsWith(".")) {
const cls = selector.substring(1);
return Object.values(this.drones).filter(d => d.classes.includes(cls));
}
return [];
}
getMCID(element) { return element.mcid; }
setMCID(element, mcid) { element.mcid = mcid; }
getElementSelectorByMCID(mcid) { return `[data-mcid="${mcid}"]`; }
}
Notice: no DOM, no canvas, no rendering. Just entities with properties, addressable by selectors. The drones could be real hardware accessible via WebSockets, simulated objects in a physics engine, or entries in a database. MotorCortex doesn't need to know.
In an exotic Clip, your entities are first-class citizens of the context — not "custom" entities living alongside DOM elements. Selectors are plain #id and .class, just like CSS selectors on an HTMLClip. No ! prefix needed. Your context handler's getElements receives these selectors directly and resolves them however you choose.
ExtendableClip — a timeline that owns a context
ExtendableClip is the base class for all Clips in MotorCortex. HTMLClip, BrowserClip, AudioClip — they all extend it. When you extend it directly, you get the full power of the MC timeline without any browser-specific assumptions:
- Channels — the system that checks, adds, and executes Incidents without conflicts
- Seek and flash — jump to any millisecond and have the full state reconstructed
- CAsI — your Clip can be nested inside other Clips as an Incident
- Export and reproduce — your Clip's definition can be serialized and recreated
- Duration management — automatic duration calculation from child Incidents
The only thing ExtendableClip needs from you is the ownContext — an instance of your context handler:
import { ExtendableClip } from "@donkeyclip/motorcortex";
class DroneClip extends ExtendableClip {
constructor(attrs, props) {
super(attrs, props);
const handler = new DroneContextHandler(props.audioSources);
handler.context.initParams = props.initParams;
this.ownContext = handler.context;
}
get volume() { return 1; }
set volume(v) { /* control master volume if applicable */ }
}
Once ownContext is set, MotorCortex takes over. Your Clip now has a working timeline. Incidents can be added, the Clip can be seeked, played, paused, nested inside other Clips — everything works.
Custom Effects — animating anything
Effects work identically on exotic Clips as they do on HTMLClip. Extend Effect, implement onProgress and getScratchValue, and animate whatever property your entity has:
import { Effect } from "@donkeyclip/motorcortex";
class DronePosition extends Effect {
getScratchValue() {
// Return the current value of the attribute on the entity
if (this.attributeKey === "x") return this.element.position.x;
if (this.attributeKey === "y") return this.element.position.y;
if (this.attributeKey === "z") return this.element.position.z;
return 0;
}
onProgress(ms) {
const fraction = this.getFraction(ms);
const value = (this.targetValue - this.initialValue) * fraction + this.initialValue;
this.element.position[this.attributeKey] = value;
// Send the command to the actual hardware
DroneAPI.setPosition(this.element.id, this.element.position);
}
}
Then use it like any other MC Effect:
// Move drone "alpha" from its current position to x:500, y:300, z:100 over 5 seconds
droneClip.addIncident(
new DronePosition(
{ animatedAttrs: { x: 500, y: 300, z: 100 } },
{ selector: "#alpha", duration: 5000, easing: "easeInOutQuad" }
),
0
);
// Move all drones in the "formation-a" class
droneClip.addIncident(
new DronePosition(
{ animatedAttrs: { z: 50 } },
{ selector: ".formation-a", duration: 3000 }
),
2000
);
All of MC's features apply: easing, delay, hiatus, repeats, dynamic values (@stagger, @expression), Combos, Groups, conflict checking. None of this code knows or cares that it's controlling hardware instead of pixels.
The plugin manifest
Exotic Clips are packaged as plugins just like any other:
export default {
npm_name: "@my-org/motorcortex-drones",
version: "1.0.0",
incidents: [
{ exportable: DronePosition, name: "DronePosition" },
{ exportable: DroneRotation, name: "DroneRotation" },
],
Clip: {
exportable: DroneClip,
},
audio: "custom", // or "off" if no audio routing needed
};
The audio option determines how MC handles the Clip:
| Value | Use when |
|---|---|
"off" | Your exotic Clip has no audio concerns |
"custom" | Your Clip manages its own audio graph (e.g. synthesis, spatial audio). MC uses your Clip class as the audio clip and creates no DOM |
Real-world example
mc-tone is an exotic Clip plugin that integrates Tone.js for real-time music synthesis. Its entities are synthesizer instruments. Its Effects animate gain and pan. Its Playback Incident controls the Tone.js transport. It runs entirely outside the DOM — yet it fully participates in MC's timeline, can be nested as CAsI inside an HTMLClip, and all of MC's Incident machinery works on it.
When to use an Exotic Clip
Use ExtendableClip directly when your domain:
- Has entities that aren't DOM elements — hardware, audio sources, data points, 3D objects managed outside the browser's rendering pipeline
- Needs the full MC timeline — seek, flash, CAsI nesting, export/reproduce, conflict-free Incident management
- Benefits from MC's selector system — addressing entities by id and class, targeting groups of entities with a single Incident
- Wants to leverage dynamic values —
@staggeracross a fleet of drones,@expression(random())for procedural animations,@initParamsfor parametric configurations
If your entities live in the browser and have DOM nodes, use BrowserClip instead — it handles rendering, shadow DOM isolation, and CSSEffect support for you. Exotic Clips are for everything else.