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

  1. Module naming: Name your extension after your score (e.g., polygonfield.js) or its purpose (e.g., visuals.js)

  2. Initialization: Use on:load for setup code that should run once when the score loads

  3. Cleanup: Store references to intervals, subscriptions, or DOM elements so you can clean them up

  4. Debugging: Extensions log to the browser console with [ext:modulename] prefixes

  5. Performance: For frequent updates, consider using requestAnimationFrame rather 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