Developer

Functional effects guide (.mjs)

Edit

Build per-LED .mjs effects for RGBJunkie without a canvas: export meta and sampleLed, with audio and color-profile support.

Published Jul 1, 2026

This guide is for anyone who wants to build JavaScript module effects that run when the user selects Effect lighting mode — including beginners who are comfortable with basic JavaScript but may not want a full <canvas> pipeline.

Start below with what an .mjs effect is, then copy the minimal example. For the formal API rules (Worker readiness, enumeration order), see EFFECT-FUNCTIONAL-MJS-CONTRACT.md.

HTML canvas effects (iframe, requestAnimationFrame, engine, <meta property>) are documented in EFFECT-DEVELOPER-GUIDE.md.

For downloads, Effect Builder, and gallery publishing, see rgbjunkie.com.


Making functional effects (start here)

What you are writing

A single ES module file (extension .mjs) that exports exactly two named bindings:

ExportRole
metaPlain object: human-readable name, stable id, optional description / publisher, and a params array describing sidebar controls.
sampleLedA function the host calls once per logical LED (and for preview tiles). It returns { r, g, b } with channels in 0…255.

There is no canvas and no animation loop inside your module. The host drives time by passing args.tSec and args.frameIndex. Your job is pure math: “given this LED, this time, and the user’s settings, what RGB?”

Why the host does it this way

The app needs colors for many LEDs per frame. A functional effect is easy for the host to run in lockstep with hardware sampling and workspace previews: it is just a function. That also keeps effects small and testable compared to a full HTML scene graph.

HTML vs functional — quick choice

TopicHTML effect (.html)Functional effect (.mjs)
OutputYou draw to a <canvas>; the host samples pixels to LEDs.You return RGB per LED from sampleLed.
TimeYou use requestAnimationFrame and performance.now().The host passes args.tSec (seconds since the effect was mounted).
Settings<meta property="…"> in <head> → globals on window.meta.params → values in args.params.
Audiowindow.engine.audioargs.context.audio (same shape).
Color profilesGlobals foo + fooStops, plus window.__rgbjColorProfiles.Declare type: "colorProfile" in meta.params, then call args.context.sampleColorProfile(id, t) (see Color profiles).
DOM / networkAllowed in the iframe (still read-only toward the host).Not allowed inside sampleLed — no window, document, or fetches (see contract).

Use HTML when you want layouts, images, screen capture, or rich UI. Use .mjs when your effect is pure lighting math and you want a single file with predictable performance.


Minimal example (copy and adapt)

Save as something like effects/MyFirstSweep.mjs (or your user effects folder — same discovery rules as HTML files). The basename should be unique vs any .html in the same folder.

What to notice

  • meta.id must stay stable (profiles and workspace tabs reference it). Use lowercase, digits, and underscores.
  • stripU is 0…1 along the strip when the host can derive it; otherwise fall back to ledIndex / (ledCount - 1).
  • args.params.speed is always the types you declared (number → coerced number in the host).

Discovery metadata (meta)

These fields feed the effect browser (search, cards) and the Effects sidebar.

FieldRequiredPurpose
idYesStable machine id, unique in the catalog.
nameYesShort title in the UI.
paramsNo (but usual)Sidebar controls → args.params.
descriptionNoCard / tooltip text.
publisherNoAttribution (e.g. "RGBJunkie").
contractVersionNoDefaults to 1. Bump only if you target a newer host contract (see contract doc).

Each control is an object with at least key and type. Optional label, default, min, max, values, allowNone.

typeSidebar widgetValue in args.params[key]
numberSlider + numeric fieldnumber
booleanCheckboxboolean
colorColor pickerstring (#rrggbb)
stringText inputstring
listDropdownstring (must be one of values)
colorProfileSame modal + stripe picker as HTML effectsstring (profile id, e.g. "Rainbow" or a user profile id)

For colorProfile, optional allowNone: true matches HTML’s allowNone="true" on <meta type="colorProfile"> (adds None to the list). Omit allowNone for the default list (Custom, Rainbow, built-ins, user profiles).


The sampleLed function

Arguments (args)

PropertyMeaning
tSecSeconds since the effect was mounted (monotonic for that run).
frameIndexHost tick counter (often 0 each frame in current builds — still useful for discrete stepping).
ledIndex, ledCountWhich LED in host enumeration order for this workspace (0 … ledCount - 1).
layout{ x, y } in workspace/stage coordinates (same space as HTML sampling).
stripU0…1 along the logical strip when known; otherwise null — then derive position from ledIndex.
paramsCurrent control values keyed by meta.params[].key.
contextHost-injected object (see below). Do not rely on arbitrary keys without checking — extras may appear in future versions.

Return value

Return { r, g, b }. The host clamps to 0…255 and floors to integers; still prefer clean values.

Performance

The host may call sampleLed very often (every LED × preview grid). Keep inner loops cheap; prefer closed-form math over allocations.


Host context (args.context)

Audio (args.context.audio)

Same information as window.engine.audio for HTML effects:

FieldMeaning
freqnumber[], length 200 — mono downmix spectrum bins 0…255 (unchanged for existing effects).
freqL, freqRSame shape — left/right bins when stereo is true.
stereoboolean — true when capture has separate L/R channels.
levelOverall loudness from the mono mix, roughly −100…0 (dB-style).
levelL, levelRPer-channel loudness when stereo is available.
density0…1 spectral energy from the mono mix.
densityL, densityRPer-channel spectral energy.
pan−1…1 balance hint (left vs right) when stereo is true; 0 when mono.

Mono fallback (recommended):

JavaScript
const a = args.context && args.context.audio;
const stereo =
  a &&
  a.stereo &&
  Array.isArray(a.freqL) &&
  Array.isArray(a.freqR) &&
  a.freqL.length === a.freq?.length &&
  a.freqR.length === a.freq?.length;
const left = stereo ? a.freqL : a?.freq;
const right = stereo ? a.freqR : a?.freq;

When stereo is false, drive both halves of a split layout from freq so mono music stays balanced. HTML canvas authors: full reference in EFFECT-DEVELOPER-GUIDE.md Section 6.2 (published Section 6.2).

Always guard: const a = args.context && args.context.audio; if (!a || typeof a !== "object") ….

Color profiles (sampleColorProfile + colorProfiles)

When the host runs functional effects, it injects:

MemberTypePurpose
context.colorProfilesObject mapSerialized catalog (advanced; usually you only need sampleColorProfile).
context.sampleColorProfile(profileId: string, t: number) => { r, g, b }profileId is the current param string (e.g. args.params.colorProfile). t wraps on \0, 1) along the resolved closed gradient (same rules as HTML’s <name>Stops).

Recommended pattern: compute your effect’s RGB with your usual math, then tint using the profile so hues stay inside the user’s palette. The built-in *effects/.mjs effects use luminance scaling: compute Rec. 601 luma y from your RGB, then tone = sampleColorProfile(id, paletteT01) and output { r: tone.r y, … }* clamped. That avoids “green × orange still looks green” that you get from naive per-channel multiply.

Declare a control:

JavaScript
{ key: "colorProfile", label: "Color profile", type: "colorProfile", default: "Rainbow" },

…and read String(args.params.colorProfile ?? "Rainbow") when calling sampleColorProfile.


Rules that matter

  1. sampleLed must stay pure — no window, document, fetch, localStorage, timers, or DOM. The host may evaluate calls from different code paths; side effects break previews and future Worker offloading.
    Formal list: [EFFECT-FUNCTIONAL-MJS-CONTRACT.md Section 4.2.
  1. One module file — no import of other project files unless your bundler supplies them; stock RGBJunkie loads a single .mjs source string. Pure math helpers in the same file are ideal.
  1. Multi-workspace tabs use the same snapshot model as HTML effects for params and lighting mode. Overview: EFFECT-MULTI-CANVAS-CONTRACT.md.

Learn more in this repo

ResourceUse
EFFECT-FUNCTIONAL-MJS-CONTRACT.mdNormative contract, versioning, workspace notes.
*effects/.mjs**Shipped examples (rainbow sweep, plasma, spectrum bands, VU meter, …).
EFFECT-DEVELOPER-GUIDE.mdFull HTML / canvas / engine reference.
docs/examples/effects/Small HTML demos (audio-bars-demo.html, color profile gradient, …) — audio and color-profile ideas port straight to sampleLed.

Document history

VersionSummary
1Initial newcomer guide: minimal .mjs example, meta.params, context, color profiles, links to HTML guide and formal contract.