Extensions
The extension system allows composers to add custom JavaScript functionality to their scores without modifying the Oscilla core codebase. Extensions are score-specific ES modules that can react to Oscilla events, integrate external libraries, and create custom visuals or behaviors.
Directory Structure
Extensions live in your score's extensions/ folder:
scores/
myproject/
score.svg
extensions/
myextension.js
visuals.js
The ext() Cue
The ext() cue connects SVG elements to extension functions.
Syntax
ext(fn:moduleName.functionName, on:eventType)
Parameters
| Parameter | Description |
|---|---|
fn: |
Function path: moduleName.functionName (required) |
on: |
Event trigger: load, select, click, play, stop, or custom event name (default: load) |
Example Usage in SVG
<!-- Call onSelect when polygon selection changes -->
<g id="ext(fn:myextension.onSelect, on:select)">
<rect x="0" y="0" width="0" height="0" opacity="0" />
</g>
<!-- Call setup once when score loads -->
<g id="ext(fn:visuals.setup, on:load)" />
<!-- Call onClick when this element is clicked -->
<g id="ext(fn:myextension.onClick, on:click)">
<rect x="0" y="0" width="100" height="100" fill="red" />
</g>
Writing Extensions
Extensions are ES modules that export functions. Each function receives a context object with useful data.
Basic Extension
// extensions/myextension.js
export function onSelect(ctx) {
const { selected, selectedSet, wasActivation, wasDeactivation } = ctx;
console.log('Selected groups:', selected);
// Your custom logic here
}
export function onClick(ctx) {
const { element, clickEvent } = ctx;
console.log('Clicked:', element.id);
}
console.log('[myextension] Extension loaded');
Context Object
The context object passed to extension functions includes:
| Property | Description |
|---|---|
event |
Event type that triggered the call |
element |
The SVG element containing the ext() cue |
paramBus |
Access to Oscilla's control signal system |
fnPath |
The function path that was called |
For on:select events:
| Property | Description |
|---|---|
selected |
Array of currently selected group IDs |
selectedSet |
Set of selected group IDs |
groupId |
The group that was just toggled |
wasActivation |
True if this was a selection |
wasDeactivation |
True if this was a deselection |
all |
Array of all selectable elements |
selectors |
All path selector objects |
For on:click events:
| Property | Description |
|---|---|
clickEvent |
The native click event |
target |
The clicked element |
For on:play / on:stop events:
| Property | Description |
|---|---|
playing |
Boolean playback state |
playheadX |
Current playhead position |
Importing External Libraries
Extensions can import libraries from CDNs using ES module imports:
// extensions/visuals.js
// Import from CDN
import confetti from 'https://cdn.skypack.dev/canvas-confetti';
export function onSelect(ctx) {
if (ctx.wasActivation) {
// Fire confetti when something is selected!
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 }
});
}
}
p5.js Example
// extensions/p5sketch.js
import p5 from 'https://cdn.jsdelivr.net/npm/p5@1.9.0/+esm';
let sketch = null;
let container = null;
export function setup(ctx) {
// Create container for p5 canvas
container = document.createElement('div');
container.id = 'p5-container';
container.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 1000;
`;
document.body.appendChild(container);
// Create p5 instance
sketch = new p5((p) => {
p.setup = () => {
p.createCanvas(400, 400);
p.noFill();
};
p.draw = () => {
p.clear();
p.stroke(255, 100);
p.strokeWeight(2);
// Draw animated circles
const t = p.millis() / 1000;
for (let i = 0; i < 5; i++) {
const size = 50 + i * 40 + Math.sin(t + i) * 20;
p.circle(200, 200, size);
}
};
}, container);
}
export function onSelect(ctx) {
// React to selection changes
if (sketch && ctx.selected.length > 0) {
// Could modify sketch parameters based on selection
}
}
console.log('[p5sketch] Extension loaded');
Using ParamBus
Extensions can read and subscribe to Oscilla's control signals:
// extensions/reactive.js
let unsubscribe = null;
export function setup(ctx) {
const { paramBus } = ctx;
// Subscribe to a fader value
unsubscribe = paramBus.subscribe('myFader.t', (value) => {
console.log('Fader value:', value);
// Update visuals based on fader
});
}
export function cleanup(ctx) {
if (unsubscribe) {
unsubscribe();
}
}
Custom Events
You can listen for any custom event by specifying its name:
<g id="ext(fn:myext.onCustom, on:mycustomevent)" />
Then dispatch from anywhere:
window.dispatchEvent(new CustomEvent('mycustomevent', {
detail: { foo: 'bar' }
}));
Your extension receives the event detail:
export function onCustom(ctx) {
const { detail } = ctx;
console.log('Custom event data:', detail);
}
Tips
-
Module naming: Name your extension after your score (e.g.,
polygonfield.js) or its purpose (e.g.,visuals.js) -
Initialization: Use
on:loadfor setup code that should run once when the score loads -
Cleanup: Store references to intervals, subscriptions, or DOM elements so you can clean them up
-
Debugging: Extensions log to the browser console with
[ext:modulename]prefixes -
Performance: For frequent updates, consider using
requestAnimationFramerather than tight loops
Complete Example
Here's a complete extension that dims unselected polygons:
// extensions/polygonfield.js
const POLYGON_GROUP_IDS = [
'g_triangleA', 'g_triangleB', 'g_triangleC',
'g_square', 'g_pentagon', 'g_hexagon', 'g_heptagon', 'g_octagon'
];
const SELECTED_OPACITY = 1;
const DIMMED_OPACITY = 0.4;
function getGroupId(selected) {
return `g_${selected}`;
}
export function onSelect(ctx) {
const { selected } = ctx;
// Build set of selected group IDs
const selectedGroups = new Set(
Array.isArray(selected) ? selected.map(getGroupId) : []
);
const hasSelection = selectedGroups.size > 0;
for (const groupId of POLYGON_GROUP_IDS) {
const el = document.getElementById(groupId);
if (!el) continue;
// Set transition for smooth animation
el.style.transition = 'opacity 0.2s ease-in-out';
// Determine opacity
const opacity = !hasSelection ? SELECTED_OPACITY
: selectedGroups.has(groupId) ? SELECTED_OPACITY
: DIMMED_OPACITY;
el.setAttribute('opacity', opacity);
}
}
export function reset() {
for (const groupId of POLYGON_GROUP_IDS) {
const el = document.getElementById(groupId);
if (el) {
el.setAttribute('opacity', SELECTED_OPACITY);
}
}
}
console.log('[ext:polygonfield] Extension loaded');
<!-- In score.svg -->
<g id="ext(fn:polygonfield.onSelect, on:select)">
<rect x="0" y="0" width="0" height="0" opacity="0" />
</g>
Tip: use ← → or ↑ ↓ to navigate the docs