Effects

Typed timeline records the renderer reads as overlays, with normalized progress resolved per frame.

An effect is not a mutation. It is a record on the timeline that says "this actor is under this effect, at this progress, at this frame" — and a renderer decides what that looks like.

Contract status

Declaration, application and the withEffects decorator surface are settled as of v0.1.0. Backend handler internals may still move.

Define and apply

withEffects(Base, factories) is the single decorator. It attaches a typed factory map to the actor class and exposes each factory through .fx.

// Wrap the actor class with effect factories.
const Card = withEffects(BrowserPanel, { glow, blur });

// Equivalent sugar with the full DOM pack:
const Card2 = withDomEffects(BrowserPanel);

Pre-bound wrappers like withDomEffects are sugar over it, and renderer-level defineEffectRenderer bindings adapt into factories via effectFactories(bindings) (or backend.withEffects(Actor)).

Applying

const card = ctx.spawn(Card);

// .fx sugar — params first, { duration, easing } second.
yield* card.fx.glow({ color: "#22d3ee", spread: 18 }, { duration: 0.4 });

Fire-and-hold

Calling a factory without playing it returns a handle with signal accessors, so the effect can be held, retargeted and detached:

const h = card.fx.glow({ color: "#ff5ea8", spread: 24 });

yield* h.play({ duration: 0.4, easing: inOutCubic });
h.params.color.set("#34d399");
h.detach();

The underlying primitive

When the sugar does not fit:

const fx = ctx.apply(card, glow({ color: "#22d3ee", spread: 18 }));
yield* fx.progress.tween(1, 0.4);

At resolution time

compiled.at(frame) exposes the active effects on each ResolvedActor as actor.effects: ActiveEffect[]. Renderers read this array to apply overlays.

frame 0·0.00scard.effects → []

The card above is drawn from card.effects alone: ctx.apply records the glow, play drives its progress, a param tween widens it, and detach ends the record — the card's own state never changes.

const state = compiled.at(frame);
const card = state.findActor(Card);

for (const fx of card.effects) {
  fx.effectType; // "glow"
  fx.progress; // 0–1, normalized for this frame
  fx.params; // resolved params
}

Why effects are records, not state writes

A glow that wrote to appearance would be indistinguishable from authored state, so removing it would mean remembering what it overwrote. As a timeline record it composes — several effects can be active at once — and it stays a renderer concern, which is what lets the same scene render differently on DOM and Three.

On this page