Summary
Refactor the camera's hardcoded effect system (_shake, _fadeIn, _fadeOut) into an extensible effect list architecture. This enables effect accumulation (multiple effects running simultaneously), custom user effects, and scene transitions — all through a single unified mechanism.
Current Limitations
Camera2d has hardcoded state objects for shake and fade (_shake, _fadeIn, _fadeOut)
- Effects are processed in a fixed order in
update() and drawFX()
- Only one shake and one fade can run at a time
- Adding a new effect type requires modifying Camera2d internals
state.transition() only supports "fade" (hardcoded string check)
- Scene transitions are coupled to
viewport.fadeIn/fadeOut in the state manager
- No way for users to create custom camera effects
Proposed Architecture
Base class
class CameraEffect {
constructor(camera, options) {
this.camera = camera;
this.isComplete = false;
}
// called each frame — modify camera state (offset, zoom, etc.)
update(dt) { }
// called after scene renders — draw overlays (color rects, masks, etc.)
draw(renderer, width, height) { }
// called when the effect is removed
destroy() { }
}
Camera holds an effect list
// in Camera2d
this.effects = [];
// in update(dt) — run all active effects
for (const fx of this.effects) fx.update(dt);
// auto-remove completed effects
this.effects = this.effects.filter(fx => !fx.isComplete);
// in drawFX(renderer) — draw all active effects
for (const fx of this.effects) fx.draw(renderer, this.width, this.height);
Built-in effects as subclasses
Migrate existing functionality into effect classes:
FadeEffect — color overlay with alpha tween (replaces _fadeIn / _fadeOut)
ShakeEffect — random position offset (replaces _shake)
FlashEffect — quick color burst (convenience, could be a short FadeEffect)
TransitionEffect — mask-based scene transition using a Polygon, Ellipse, or texture (see below)
Effect accumulation
Multiple effects of any type can coexist naturally:
// shake + fade at the same time
camera.addEffect(new ShakeEffect({ intensity: 10, duration: 300 }));
camera.addEffect(new FadeEffect({ color: "#000", duration: 500 }));
// two overlapping fades
camera.addEffect(new FadeEffect({ color: "#f00", duration: 200 }));
camera.addEffect(new FadeEffect({ color: "#000", duration: 500 }));
// user-defined custom effect
camera.addEffect(new MyCustomEffect());
Backward-compatible convenience methods
Existing camera methods become thin wrappers that create and add effects:
// these stay, unchanged API
camera.fadeIn(color, duration, onComplete); // → addEffect(new FadeEffect(...))
camera.fadeOut(color, duration, onComplete); // → addEffect(new FadeEffect(...))
camera.shake(intensity, duration, axis); // → addEffect(new ShakeEffect(...))
Scene Transitions
TransitionEffect
A CameraEffect subclass that uses a shape or texture as a clip mask, scaled by a tweened progress value (0→1):
- Phase 1 (hide): mask shrinks from full-screen to zero → scene is covered by transition color
- Phase 2 (reveal): mask grows from zero to full-screen → new scene is revealed
The mask can be:
- A
Polygon or Ellipse — defined in code, no asset needed
- An image/texture — for complex shapes (star, heart, game logo)
Built-in presets are just pre-defined shapes:
- iris:
Ellipse centered on viewport
- diamond: 4-point
Polygon
- Any custom shape the developer provides
Rendering the mask transition
Both renderers support clipping:
- Canvas:
ctx.clip() with evenodd fill rule (outer rect path + inner shape path → fills outside the shape)
- WebGL: stencil buffer (already used for masking in melonJS)
The shape is centered on the viewport and scaled based on progress. At progress=0 the shape covers nothing (screen filled with color). At progress=1 the shape covers the full viewport diagonal (screen fully visible).
Updated state.transition() API
// backward compatible — fade still works
state.transition("fade", "#000", 500);
// new: pass a shape for mask-based transitions
state.transition({
shape: new Ellipse(0, 0, 1, 1), // iris
color: "#000",
duration: 500
});
// polygon shape
state.transition({
shape: new Polygon(0, 0, [
{ x: 0, y: -1 }, { x: 1, y: 0 },
{ x: 0, y: 1 }, { x: -1, y: 0 }
]), // diamond
color: "#000",
duration: 400
});
// texture mask
state.transition({
texture: assets.getImage("star_silhouette"),
color: "#000",
duration: 500
});
State manager changes
Minimal — state.change() creates a TransitionEffect (or FadeEffect for backward compat) instead of directly calling viewport.fadeIn/fadeOut. The transition lifecycle (hide → switch stage → reveal) is driven by the effect's onComplete callback, same pattern as today.
Implementation Steps
- Create
CameraEffect base class
- Implement
FadeEffect and ShakeEffect, migrating existing logic from Camera2d
- Add
effects array and addEffect()/removeEffect() to Camera2d
- Refactor
update() and drawFX() to iterate the effect list
- Keep
fadeIn()/fadeOut()/shake() as convenience wrappers
- Implement
TransitionEffect with polygon/texture mask support
- Update
state.transition() to accept shape/texture configuration
- Ship built-in presets (iris, diamond) as pre-defined shapes
References
Camera2d (shake, fade, drawFX): src/camera/camera2d.ts
state.transition(): src/state/state.ts:423
state.change() transition flow: src/state/state.ts:464-478
- Polygon / Ellipse:
src/geometries/
Summary
Refactor the camera's hardcoded effect system (
_shake,_fadeIn,_fadeOut) into an extensible effect list architecture. This enables effect accumulation (multiple effects running simultaneously), custom user effects, and scene transitions — all through a single unified mechanism.Current Limitations
Camera2dhas hardcoded state objects for shake and fade (_shake,_fadeIn,_fadeOut)update()anddrawFX()state.transition()only supports"fade"(hardcoded string check)viewport.fadeIn/fadeOutin the state managerProposed Architecture
Base class
Camera holds an effect list
Built-in effects as subclasses
Migrate existing functionality into effect classes:
FadeEffect— color overlay with alpha tween (replaces_fadeIn/_fadeOut)ShakeEffect— random position offset (replaces_shake)FlashEffect— quick color burst (convenience, could be a short FadeEffect)TransitionEffect— mask-based scene transition using aPolygon,Ellipse, or texture (see below)Effect accumulation
Multiple effects of any type can coexist naturally:
Backward-compatible convenience methods
Existing camera methods become thin wrappers that create and add effects:
Scene Transitions
TransitionEffect
A
CameraEffectsubclass that uses a shape or texture as a clip mask, scaled by a tweened progress value (0→1):The mask can be:
PolygonorEllipse— defined in code, no asset neededBuilt-in presets are just pre-defined shapes:
Ellipsecentered on viewportPolygonRendering the mask transition
Both renderers support clipping:
ctx.clip()withevenoddfill rule (outer rect path + inner shape path → fills outside the shape)The shape is centered on the viewport and scaled based on
progress. At progress=0 the shape covers nothing (screen filled with color). At progress=1 the shape covers the full viewport diagonal (screen fully visible).Updated state.transition() API
State manager changes
Minimal —
state.change()creates aTransitionEffect(orFadeEffectfor backward compat) instead of directly callingviewport.fadeIn/fadeOut. The transition lifecycle (hide → switch stage → reveal) is driven by the effect'sonCompletecallback, same pattern as today.Implementation Steps
CameraEffectbase classFadeEffectandShakeEffect, migrating existing logic from Camera2deffectsarray andaddEffect()/removeEffect()to Camera2dupdate()anddrawFX()to iterate the effect listfadeIn()/fadeOut()/shake()as convenience wrappersTransitionEffectwith polygon/texture mask supportstate.transition()to accept shape/texture configurationReferences
Camera2d(shake, fade, drawFX):src/camera/camera2d.tsstate.transition():src/state/state.ts:423state.change()transition flow:src/state/state.ts:464-478src/geometries/