Skip to content

Visual Effects & Masks

YAGE wraps PixiJS filters and masks behind a consistent handle-based API that works identically at four scopes — per-component, per-layer, per-scene, per-screen. Pre-built effect presets ship in @yagejs/effects. Wrap a custom shader with rawFilter(...).

Every fx.addEffect(...) call returns a typed EffectHandle you keep around to control the effect later. Components, layers, scenes, and the renderer all expose effects through the same .fx property:

import { hitFlash } from "@yagejs/effects";
import { SpriteComponent } from "@yagejs/renderer";
const sprite = entity.get(SpriteComponent);
const flash = sprite.fx.addEffect(hitFlash({ color: 0xffffff, duration: 0.1 }));
flash.trigger(); // one-shot ramp up + down
flash.setIntensity(0.5); // set half strength immediately
flash.fadeOut(0.2); // returns a Process
flash.setEnabled(false); // toggle without removing
flash.remove(); // detach + destroy

The base handle is shared across every preset:

MethodDescription
remove()Detach + destroy. Idempotent.
setEnabled(on)Toggle the underlying filter’s enabled flag without removing.
enabledCurrent enabled state (live).
setIntensity(value)Set primary intensity immediately. Values are clamped to 0–1.
fadeIn(seconds)Tween primary intensity 0 → 1. Returns a Process.
fadeOut(seconds)Tween primary intensity → 0. Returns a Process.
run(p)Schedule a Process scoped to this effect — pauses with the owning scene, time-scales with it, auto-cancels on remove(). Use for custom uniform tweens.

@yagejs/effects exports EffectHandle as a type, so a field or a list that holds handles from several presets has a name to declare:

import { Component } from "@yagejs/core";
import type { EffectHandle } from "@yagejs/effects";
class Hazard extends Component {
private active: EffectHandle[] = [];
}

Each preset extends the base with its own knobs — OutlineHandle.setThickness(n), HitFlashHandle.trigger(), ColorGradeHandle.setPreset("sepia"), etc. The common setIntensity method changes the current 0–1 strength. A preset setter such as setThickness or setBloomScale changes what full strength means while preserving the current intensity ratio.

import { bloom, crt, hitFlash, vignette } from "@yagejs/effects";
import {
RendererKey,
SceneRenderTreeKey,
SpriteComponent,
} from "@yagejs/renderer";
const sprite = entity.get(SpriteComponent);
// 1. Component scope — sprite / graphics / text / animatedSprite.
sprite.fx.addEffect(hitFlash({ /* ... */ }));
// 2. Layer scope — every entity in this layer.
this.use(SceneRenderTreeKey).get("world").fx.addEffect(bloom({ /* ... */ }));
// 3. Scene scope — the entire per-scene root.
this.use(SceneRenderTreeKey).fx.addEffect(crt({ /* ... */ }));
// 4. Screen scope — cross-scene, on app.stage.
this.use(RendererKey).fx.addEffect(vignette({ /* ... */ }));

Pixi processes filters bottom-up the display tree, so an outer scope sees the previous scope’s rasterized output. A screen-scope pixelate will pixelate already-bloomed gameplay, not the raw pre-bloom pixels.

Blur and halo sizes use the host’s local pixels and follow its scale. For example, doubling the world layer’s scale also doubles its bloom radius. See Effect sizes and padding for the unit reference.

Covering some layers, or all but one visual

Section titled “Covering some layers, or all but one visual”

A screen-wide look usually belongs on the world and not on the HUD. addLayerEffect names the layers it covers and hands back one handle for all of them:

import { colorGrade } from "@yagejs/effects";
import { SceneRenderTreeKey } from "@yagejs/renderer";
const tree = this.use(SceneRenderTreeKey);
const grade = tree.addLayerEffect(colorGrade({ preset: "night" }), [
"background",
"world",
"props",
]);
grade.fadeOut(1); // fades on all three
grade.remove(); // removes all three

The factory runs once per layer, so this is one filter pass per listed layer: three fullscreen layers cost three fullscreen passes per frame. For a single layer use tree.get("world").fx.addEffect(...) instead.

Going the other way, renderAboveEffects lifts one visual out of every layer- and scene-scope effect — a cursor, a boss-fight portrait, a tutorial arrow that should stay crisp under a full-screen grade:

import { SpriteComponent } from "@yagejs/renderer";
const cursor = entity.add(
new SpriteComponent({ texture: "cursor", renderAboveEffects: true }),
);
// or at runtime
cursor.renderAboveEffects = false;

The visual draws after the scene’s layers while its logical parent still drives position, alpha, visibility, and camera. It escapes layer and scene filters, layer.setMask, tree.setMask, and the irisReveal / chessboard transition masks — during those reveals it shows at once. A screen-scope effect still covers it. Hit testing follows the logical tree, so a UI element drawn under it still receives the pointer first.

On a SortGroupComponent the flag applies to the component’s own render object; the group’s container is not lifted. A layer declared with isRenderGroup: true is unsupported for lifted visuals (a Pixi restriction).

ScopeCleaned up whenFades pause with
ComponentThe owning component is destroyed (or its entity)The entity’s scene
LayerThe owning scene exitsThe owning scene
SceneThe owning scene exitsThe owning scene
ScreenRendererPlugin.onDestroy (engine teardown)Never — fades run in engine time

You don’t have to manually remove() effects on scene exit; the renderer tears down each scope’s EffectStack for you.

Twenty-three presets ship in @yagejs/effects, all using the same handle protocol:

import {
hitFlash, bloom, outline, dropShadow, pixelate,
glow, crt, chromaticAberration, vignette, colorGrade,
godRay, shockwave, motionBlur, oldFilm, bulgePinch,
halftone, wave, colorize,
glitch, zoomBlur, axisBlur, implosion, dissolve,
} from "@yagejs/effects";
PresetOne-linePrimary intensity
hitFlashDamage flash with trigger()additive tint amount
bloomSoft glow on bright pixelsbloom scale
outlineHard-edge outlinethickness
dropShadowDrop shadow with offset + blurshadow alpha
pixelateBlock pixelationpixel size (clamped to ≥1)
glowOuter + inner glow haloscales BOTH strengths together
crtScanlines, curvature, noisefilter alpha (whole effect)
chromaticAberrationRGB channel separationsymmetric separation
vignetteEdge darkeningvignette alpha
colorGradeSepia / grayscale / negative / night / warm / coolfilter alpha
godRayAnimated volumetric light rays (self-scheduling time)gain (rays scale 0 → full)
shockwaveOne-shot ripple at (x, y) via trigger(), expanding or contractingamplitude × brightness (parked off until trigger)
motionBlurDirectional smear along a velocity vectorvelocity magnitude
oldFilmSepia + grain + scratches + animated seedfilter alpha
bulgePinchLens distortion — sign-preserving (negative pinches, positive bulges)strength (sign preserved)
halftoneComic-print dot grid via custom WebGL+WGSL shader pairamount (cross-fades back to source)
waveSinusoidal x-displacement via custom shader; scene-time animatorconfigured amplitude
colorizeLuminance-to-colour recolour via custom WebGL+WGSL shader pair — the replace-style alternative to sprite.tint’s multiplystrength (cross-fades back to source)
glitchDeterministic horizontal slices with RGB separationband displacement and RGB offsets
zoomBlurRadial speed blur, including inward pullsigned blur strength
axisBlurSymmetric horizontal or vertical Gaussian blurmain and perpendicular strengths
implosionInward pull with center darkening and swirlpull, darkness, and swirl
dissolveNoise-driven removal with a colored edgedissolve progress from intact to transparent

The handle controls an effect’s strength at runtime. setIntensity(value) sets the primary intensity immediately and clamps it to 0–1. fadeIn(seconds) and fadeOut(seconds) tween the same value. The set* setters that change a preset’s “full” value (bloom.setBloomScale, glow.setOuterStrength, outline.setThickness, godRay.setGain, motionBlur.setVelocity, bulgePinch.setStrength, halftone.setAmount, wave.setAmplitude, colorize.setStrength, etc.) rebase that ceiling while preserving the current intensity ratio, so adjusting it mid-pulse raises the pulse height instead of snapping the visible effect back to 1. For gameplay-driven strength (HP-linked tinting) or looping animation (heartbeat glow, breathing vignette), drive the preset’s set* setter from a scheduled tween — vignette.run(Tween.custom((v) => vignette.setStrength(v), from, to, seconds)) — which is scoped to the effect and auto-cancels on remove().

Most presets wrap either a built-in ColorMatrixFilter or a pixi-filters filter and expose typed extras specific to it. Five — halftone, wave, colorize, implosion, and dissolve — ship as custom WebGL + WGSL shader pairs with no pixi-filters dependency.

sprite.tint is the cheap built-in way to tint a sprite, but it multiplies the source RGB by the tint colour. That works for darken/desaturate/multiply-with-a-colour effects, but on saturated source art it produces muddy mid-axis colours — a blue mushroom × a yellow tint reads as olive, not yellow. colorize is the replace-style answer: black stays black, white reaches the target colour, midtones blend proportionally, source alpha is preserved unchanged.

import { colorize } from "@yagejs/effects";
import { SpriteComponent } from "@yagejs/renderer";
const sprite = entity.get(SpriteComponent);
const recolour = sprite.fx.addEffect(colorize({ color: 0xf2c14e }));
recolour.setColor("#d94a4a"); // numbers or strings ("#…", "red", "rgb(…)")
recolour.setStrength(0.6); // rebases full ceiling; preserves ratio
recolour.fadeOut(0.2); // strength → 0 cross-fades back to the source

Output per pixel is mix(sourceRGB, tintColor * L, strength) where L is Rec. 601 luminance (0.299·R + 0.587·G + 0.114·B). Pick colorize over sprite.tint whenever the source art has saturated colours and you want the recolour to read as the target hue rather than the multiply of the two.

A few presets behave differently from the steady-state pattern:

import { Transform } from "@yagejs/core";
import {
axisBlur,
bulgePinch,
dissolve,
glitch,
godRay,
implosion,
motionBlur,
oldFilm,
shockwave,
wave,
zoomBlur,
} from "@yagejs/effects";
import { SceneRenderTreeKey, SpriteComponent } from "@yagejs/renderer";
const tree = this.use(SceneRenderTreeKey);
const layer = tree.get("world");
const sprite = entity.get(SpriteComponent);
const playerPosition = entity.get(Transform).position;
const { x: heroX, y: heroY } = playerPosition;
const [drainX, drainY] = [640, 200];
// shockwave — parks at "off" until you call trigger(); re-trigger cancels
// any in-flight ramp.
const sw = tree.fx.addEffect(shockwave({ amplitude: 40 }));
sw.trigger(heroX, heroY);
// direction: "in" reverses the travel — the ring starts speed × duration
// host-local pixels out and contracts onto the trigger point.
const vortex = tree.fx.addEffect(
shockwave({ direction: "in", speed: 400, duration: 1.5 }),
);
vortex.trigger(drainX, drainY);
// godRay / oldFilm / wave self-advance their internal time uniform from
// scene time, so they keep animating without any caller wiring. Pause
// with the scene; time-scale with it.
tree.fx.addEffect(godRay({ angle: 30, gain: 0.5 }));
tree.fx.addEffect(oldFilm({ sepia: 0.4, noise: 0.4 }));
layer.fx.addEffect(wave({ amplitude: 6, speed: 2 }));
// motionBlur — directional. Coerces invalid kernel sizes (must be odd
// and ≥ 5) up to the nearest valid value with a one-shot warning.
const mb = sprite.fx.addEffect(motionBlur({ velocity: { x: 30, y: 0 } }));
mb.setVelocity(50, 12);
// bulgePinch takes its center and radius in host-local pixels.
const bp = tree.fx.addEffect(
bulgePinch({ strength: 1, radius: 200, center: { x: 640, y: 360 } }),
);
bp.setStrength(-0.8); // flips bulge → pinch; intensity ratio preserved
bp.setCenter(heroX, heroY);
bp.useHostCenter(); // back to the middle of the filtered region
// glitch keeps one deterministic pattern until refresh() is called.
const digital = sprite.fx.addEffect(glitch({ slices: 8, offset: 24 }));
digital.refresh(42);
// zoomBlur is signed. Negative strength pulls the image inward.
layer.fx.addEffect(
zoomBlur({
center: playerPosition,
strength: -0.14,
radius: 180,
expandFromCenter: true,
}),
);
// axisBlur is symmetric; motionBlur remains the trailing directional blur.
sprite.fx.addEffect(axisBlur({ axis: "horizontal", strength: 12 }));
// implosion is one focused visual. Add particles or camera movement separately.
layer.fx.addEffect(implosion({ center: playerPosition, radius: 180 }));
// dissolve intensity is progress: 0 is intact and 1 is transparent.
const vanish = sprite.fx.addEffect(
dissolve({ edgeColor: 0x67e8f9, noiseScale: 10 }),
);
vanish.setIntensity(0.5);

These five presets check the numbers you give them. Pass a NaN, an Infinity, or a value outside a documented range — as an option, to setIntensity, or to any of the setters — and the call throws right there, naming the input and the constraint:

implosion: radius must be >= 1, got 0.

The alternative is worse than an error. A NaN reaching a shader uniform produces a blank or garbled sprite on a later frame, with nothing left to point at the line that caused it.

The ranges: implosion needs a radius of at least 1 and darkness from 0 to 1; zoomBlur needs an innerRadius of at least 0; glitch needs whole slices and sampleSize of at least 1; axisBlur needs a whole quality of at least 1; dissolve needs edgeWidth from 0.001 to 0.5, noiseScale of at least 1, and softness from 0.001 to 0.25. Every other number these presets accept just has to be finite.

Scope decides how much of the screen a filter has to work with. A filter runs over its host container’s own content bounds, which pixi computes from whatever the container holds. Nothing sets filterArea, so the input frame is neither the canvas nor the virtual play rect: it tracks the content, and it moves and resizes with the camera and with the letterbox bars the responsive fit produces.

A layer holding three sprites therefore gives a small frame that changes size as those sprites move, and a vignette or a full-screen colour grade on it covers the three sprites and the gaps between them rather than the screen. A layer that fills the viewport gives a frame close to the visible area, which is what a full-screen look needs. A wider scope takes in more content and the frame grows to hold it — app.stage, the screen-scope host, has no filterArea either — so widening helps only as far as the added content reaches. The frame includes padding from child effects and resizes with the camera and the bars. Host-local effect sizes follow that scale automatically.

Four presets read best at scene scope (or higher) rather than on a single component:

  • godRay — its alpha-aware fragment shader treats fully transparent host pixels as black, so on a per-component sprite the rays render against a black box. At scene scope the layer rasterizes alpha=1 across the visible area and the rays blend into the world as intended.
  • bulgePinch — distortion samples outside the host’s bounding rect, so a sprite-scoped bulge clips at the sprite edges. Apply at scene/layer scope so the lens has room to bend pixels around its radius.
  • shockwave — the ring travels between center and the host’s bounds and is naturally clipped there, so a component-scoped shockwave on a small sprite reads as a tiny “bump” rather than a ring. Scene scope makes trigger(heroX, heroY) line up with the entity’s transform.
  • implosion — inward displacement samples beyond the output pixel. A small component host clips the warped image at its own bounds, while a layer or scene gives the distortion room around its center.

examples/src/effects-showcase/controls.ts wires godRay, bulgePinch, and shockwave at scene scope — copy that as the worked-out reference.

Most blur and halo sizes use host-local pixels. They follow the effect host’s scale, including camera zoom and responsive fit, on every render. Do not multiply these options by the canvas-to-virtual-size ratio yourself.

PresetOptions in host-local pixels
bloomblur
outlinethickness
dropShadowoffset, blur
glowdistance
chromaticAberrationseparation
motionBlurvelocity, offset
axisBlurstrength, perpendicularStrength
bulgePinch, implosioncenter, radius
zoomBlurcenter, innerRadius, radius
shockwavetrigger coordinates, speed, amplitude, wavelength, radius
glitchoffset, red, green, blue
dissolvenoiseScale

Scalar lengths use the mean of the host’s two axis scales when those scales differ. Axis blur strengths, channel separation, and vector components instead use the corresponding axis scale magnitude. Vector screen directions do not rotate with the host. Shockwave speed is measured in local pixels per second and uses the host’s x-axis scale. motionBlur.kernelSize and quality options count samples or passes; they are not lengths.

pixelate.size, crt.lineWidth, halftone.size, and wave.amplitude / wave.wavelength remain in post-transform logical pixels. Renderer resolution controls raster density separately from these units.

The seven blur and halo presets at the top of the table reserve padding from their current sampling reach. Padding updates with scale and intensity before the filter’s input is allocated. A parent effect also includes its children’s effect padding, so a scene or layer filter does not crop a sprite’s halo at the sprite’s original bounds. Consecutive filters add their padding.

Filter input buffers include these margins. Ordinary getBounds() queries and local layout bounds continue to measure content. Larger blur radii and more blur passes can require larger intermediate textures. Explicit masks, filterArea, viewport clipping, and render-target size limits still constrain output. axisBlur({ repeatEdgePixels: true }) deliberately keeps edge pixels and does not expand its input. With rawFilter, set the underlying filter’s padding to cover its shader’s sampling reach; parent effects preserve that declared margin.

For one-off shaders or pixi-filters imports the engine doesn’t wrap, use rawFilter:

import { BlurFilter } from "pixi.js";
import { rawFilter, SpriteComponent } from "@yagejs/renderer";
const sprite = entity.get(SpriteComponent);
const blur = new BlurFilter({ strength: 8 });
const handle = sprite.fx.addEffect(
rawFilter(blur, {
intensity: {
get: () => blur.strength,
set: (v) => { blur.strength = v * 8; },
},
}),
);
handle.fadeIn(0.2);

Without the intensity accessor, fadeIn / fadeOut no-op and warn once. The renderer treats both raw filters and named effects as runtime resources. Use defineEffect when you want a reusable named factory with typed options.

import { defineEffect } from "@yagejs/renderer";
import type { EffectHandle } from "@yagejs/renderer";
import { ShockwaveFilter } from "pixi-filters";
interface ShockOptions { strength: number; }
export const shock = defineEffect<EffectHandle, ShockOptions>({
name: "myGame:shockwave",
factory: (opts) => {
const filter = new ShockwaveFilter({ amplitude: opts.strength });
return {
filter,
getIntensity: () => filter.amplitude / opts.strength,
setIntensity: (v) => { filter.amplitude = opts.strength * v; },
};
},
});

The returned shock is callable like any preset: sprite.fx.addEffect(shock({ strength: 30 })).

Masks are exclusive (one per container) and use a parallel API:

import {
graphicsMask,
rectMask,
SceneRenderTreeKey,
SpriteComponent,
} from "@yagejs/renderer";
const sprite = entity.get(SpriteComponent);
const tree = this.use(SceneRenderTreeKey);
const layout = { x: 320, y: 180 }; // your layout code updates this object
const handle = sprite.setMask(rectMask({ x: 0, y: 0, width: 200, height: 200 }));
handle.setInverse(true);
sprite.clearMask();
// Layer + scene scope share the same shape.
tree.get("hud").setMask(graphicsMask((g) => {
g.clear(); // pixi commands accumulate
g.circle(layout.x, layout.y, 100).fill(0xffffff); // read live state, don't snapshot
}));
// After layout changes, call .redraw() to re-run the closure.

Two gotchas inside a graphicsMask closure:

  1. g.clear() first. Pixi Graphics commands accumulate; without clear(), every redraw() layers another shape on top of the previous one.
  2. Read live state, don’t snapshot it. JS closures capture variable bindings, not values. const w = panel.width followed by g.rect(0, 0, w, ...) will keep using the original w after redraw(). Reference the live source (panel.width, this.layout.width, a getter) inside the closure so each call picks up the current value.

Three mask factories are built in: rectMask, spriteMask, and graphicsMask. spriteMask uses a caller-owned sprite, while graphicsMask redraws through your callback. Use defineMask for a reusable named factory.

Every visual component takes a blendMode option deciding how its pixels combine with what is already drawn beneath it. It is also a live accessor, so it can change at runtime.

The default is "inherit" — the mode of the nearest ancestor that sets one, which is "normal" unless a parent changed it. Set "normal" explicitly on a child inside a group that uses another mode.

import { SpriteComponent } from "@yagejs/renderer";
// A muzzle flash that brightens whatever it lands on.
const sprite = entity.add(
new SpriteComponent({ texture: "flash", blendMode: "add" }),
);
// Later, back to plain compositing.
sprite.blendMode = "normal";

"normal", "add", "multiply", "screen", "erase", "min", "max", "none" and the -npm variants run directly on the GPU and work with no setup. The photoshop-style rest — "darken", "lighten", "overlay", "color-dodge", "soft-light" and the rest — are drawn with filters that PixiJS only registers when you ask for them. Add one import to your entry file:

import "pixi.js/advanced-blend-modes";

Without it, PixiJS logs a warning naming the mode and draws the object normally.

createRenderTarget draws a container into a texture your game owns, on a schedule you control. Reach for it when several objects need to composite against each other before they reach the screen, or when redrawing expensive static content every frame is wasteful.

import { Component } from "@yagejs/core";
import { Container, Graphics } from "pixi.js";
import { RendererKey, registerTexture, unregisterTexture } from "@yagejs/renderer";
import type { RenderTargetHandle } from "@yagejs/renderer";
export class LightBuffer extends Component {
private readonly source = new Container();
private readonly lamp = new Graphics().circle(0, 0, 120).fill(0xffffff);
private target!: RenderTargetHandle;
/** Registered keys are engine-global, so give each buffer its own. */
constructor(private readonly key: string) {
super();
}
onAdd(): void {
const darkness = new Graphics()
.rect(0, 0, 1280, 720)
.fill({ color: 0x05060a, alpha: 0.85 });
// `erase` only reaches `darkness`, because both live in the buffer.
this.lamp.blendMode = "erase";
this.source.addChild(darkness, this.lamp);
this.target = this.use(RendererKey).createRenderTarget(this.source, {
width: 1280,
height: 720,
resolutionScale: 0.5, // a quarter of the texels — invisible on a soft gradient
});
registerTexture(this.key, this.target.texture);
}
/** Move the lit spot. Coordinates are buffer pixels, not world pixels. */
moveLamp(x: number, y: number): void {
this.lamp.position.set(x, y);
this.target.invalidate(); // the content changed
}
update(): void {
this.target.renderIfNeeded(); // draws only when a redraw is pending
}
onDestroy(): void {
unregisterTexture(this.key);
this.target.destroy();
this.source.destroy({ children: true });
}
}

The buffer starts out needing a draw and is redrawn only when you ask. moveLamp calls invalidate() from the event that changed the content and update leaves the drawing to renderIfNeeded() — the right shape for a baked background or a fog-of-war map revealed on discovery. For content that moves every frame, call render() directly and use resolutionScale to keep the cost down.

Add the owner before anything that shows the texture, so the key already resolves:

import { Transform } from "@yagejs/core";
import { SpriteComponent } from "@yagejs/renderer";
import { LightBuffer } from "./light-buffer.js";
const LIGHTING = "arena:lighting";
const lights = this.spawn("lights");
lights.add(new Transform());
lights.add(new LightBuffer(LIGHTING));
lights.add(new SpriteComponent({ texture: LIGHTING, layer: "overlay" }));

A render target is three resources, and one object should free all three: the texture (handle.destroy()), the source container (source.destroy({ children: true }), which handle.destroy() does not touch) and the registered key (unregisterTexture(key), or the key keeps resolving a destroyed texture). A component’s onDestroy runs when its entity is destroyed or when the scene tears down, so it covers both exits. Own the buffer on a scene’s onEnter / onExit pair instead when its lifetime is exactly the scene’s.

Content is drawn in the source container’s own coordinate space. A child at local (100, 50) lands at texture pixel (100, 50). Ancestor transforms never reach the buffer, so neither the camera nor the responsive fit scale moves or resizes what you draw. Two consequences:

  • To follow the camera, move the children yourself. Run the world point through camera.worldToScreen(x, y) and place the child at the result — reading camera.position alone drifts as soon as zoom, rotation, or shake are in play. Or size the buffer to renderer.visibleVirtualRect and place content in that rectangle.
  • The source’s own transform still applies. Setting source.position shifts everything inside the texture. Leave the source untransformed unless you want that.
  • Keep the source out of the scene render tree. PixiJS promotes a rendered container to a render group, changing how it batches wherever it is parented, and buffer content is normally shown through the texture rather than drawn twice.
  • A hidden source draws nothing. PixiJS skips a container whose visible is false. The pending draw is kept — and set, if you forced one — so the buffer catches up the first time the source is shown.
  • A destroyed source throws. Once the source container is destroyed the buffer can never draw again, so render() and renderIfNeeded() raise a named error instead of leaving a permanently stale texture. Destroy the target alongside its source.
  • resolutionScale costs sharpness, not layout. Only the texel count changes. The exception is rounding: PixiJS stores whole texels, so a size that lands between them is rounded up and the measured size grows to match. Good for gradients and glows, bad for text and pixel art.
  • resize() keeps the density you asked for. Omit the third argument and the buffer re-derives its resolution from the renderer’s current one using the configured resolutionScale; pass one to replace that scale.
  • destroy() frees the texture only. The source container and any registered key are still yours to release — see the owner component above. Nothing is reclaimed automatically.
  • WebGPU is unverified. PixiJS tries WebGL first unless you pass pixi: { preference: "webgpu" }, and blend behaviour inside a render target has only been checked on WebGL.

Effects, masks, display objects, and in-flight fades are runtime resources. They are not included in @yagejs/save state roots. Save the durable game fact that selects an effect or mask, then recreate the visual during normal scene or component setup after loading that state.

Coding agents: fetch https://yage.dev/llms.txt first and prefer the Markdown references it links over these HTML pages. This page's Markdown counterpart is /llms/packages/renderer.md.