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(...).
The handle pattern
Section titled “The handle pattern”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 + downflash.setIntensity(0.5); // set half strength immediatelyflash.fadeOut(0.2); // returns a Processflash.setEnabled(false); // toggle without removingflash.remove(); // detach + destroyThe base handle is shared across every preset:
| Method | Description |
|---|---|
remove() | Detach + destroy. Idempotent. |
setEnabled(on) | Toggle the underlying filter’s enabled flag without removing. |
enabled | Current 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.
Four scopes
Section titled “Four scopes”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 threegrade.remove(); // removes all threeThe 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 runtimecursor.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).
Lifecycle
Section titled “Lifecycle”| Scope | Cleaned up when | Fades pause with |
|---|---|---|
| Component | The owning component is destroyed (or its entity) | The entity’s scene |
| Layer | The owning scene exits | The owning scene |
| Scene | The owning scene exits | The owning scene |
| Screen | RendererPlugin.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.
Hero presets
Section titled “Hero presets”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";| Preset | One-line | Primary intensity |
|---|---|---|
hitFlash | Damage flash with trigger() | additive tint amount |
bloom | Soft glow on bright pixels | bloom scale |
outline | Hard-edge outline | thickness |
dropShadow | Drop shadow with offset + blur | shadow alpha |
pixelate | Block pixelation | pixel size (clamped to ≥1) |
glow | Outer + inner glow halo | scales BOTH strengths together |
crt | Scanlines, curvature, noise | filter alpha (whole effect) |
chromaticAberration | RGB channel separation | symmetric separation |
vignette | Edge darkening | vignette alpha |
colorGrade | Sepia / grayscale / negative / night / warm / cool | filter alpha |
godRay | Animated volumetric light rays (self-scheduling time) | gain (rays scale 0 → full) |
shockwave | One-shot ripple at (x, y) via trigger(), expanding or contracting | amplitude × brightness (parked off until trigger) |
motionBlur | Directional smear along a velocity vector | velocity magnitude |
oldFilm | Sepia + grain + scratches + animated seed | filter alpha |
bulgePinch | Lens distortion — sign-preserving (negative pinches, positive bulges) | strength (sign preserved) |
halftone | Comic-print dot grid via custom WebGL+WGSL shader pair | amount (cross-fades back to source) |
wave | Sinusoidal x-displacement via custom shader; scene-time animator | configured amplitude |
colorize | Luminance-to-colour recolour via custom WebGL+WGSL shader pair — the replace-style alternative to sprite.tint’s multiply | strength (cross-fades back to source) |
glitch | Deterministic horizontal slices with RGB separation | band displacement and RGB offsets |
zoomBlur | Radial speed blur, including inward pull | signed blur strength |
axisBlur | Symmetric horizontal or vertical Gaussian blur | main and perpendicular strengths |
implosion | Inward pull with center darkening and swirl | pull, darkness, and swirl |
dissolve | Noise-driven removal with a colored edge | dissolve 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.
Replace-style recolour (colorize)
Section titled “Replace-style recolour (colorize)”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 ratiorecolour.fadeOut(0.2); // strength → 0 cross-fades back to the sourceOutput 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.
Triggerable + animated presets
Section titled “Triggerable + animated presets”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 preservedbp.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.
Pick the right scope for each preset
Section titled “Pick the right scope for each preset”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 itsradius.shockwave— the ring travels betweencenterand 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 makestrigger(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.
Effect sizes and padding
Section titled “Effect sizes and padding”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.
| Preset | Options in host-local pixels |
|---|---|
bloom | blur |
outline | thickness |
dropShadow | offset, blur |
glow | distance |
chromaticAberration | separation |
motionBlur | velocity, offset |
axisBlur | strength, perpendicularStrength |
bulgePinch, implosion | center, radius |
zoomBlur | center, innerRadius, radius |
shockwave | trigger coordinates, speed, amplitude, wavelength, radius |
glitch | offset, red, green, blue |
dissolve | noiseScale |
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.
Custom filters via rawFilter
Section titled “Custom filters via rawFilter”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.
Custom presets via defineEffect
Section titled “Custom presets via defineEffect”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:
g.clear()first. PixiGraphicscommands accumulate; withoutclear(), everyredraw()layers another shape on top of the previous one.- Read live state, don’t snapshot it. JS closures capture variable
bindings, not values.
const w = panel.widthfollowed byg.rect(0, 0, w, ...)will keep using the originalwafterredraw(). 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.
Blend modes
Section titled “Blend modes”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.
Offscreen render targets
Section titled “Offscreen render targets”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.
Where things land inside the buffer
Section titled “Where things land inside the buffer”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 — readingcamera.positionalone drifts as soon as zoom, rotation, or shake are in play. Or size the buffer torenderer.visibleVirtualRectand place content in that rectangle. - The source’s own transform still applies. Setting
source.positionshifts everything inside the texture. Leave the source untransformed unless you want that.
Rules worth knowing
Section titled “Rules worth knowing”- 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
visibleisfalse. 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()andrenderIfNeeded()raise a named error instead of leaving a permanently stale texture. Destroy the target alongside its source. resolutionScalecosts 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 configuredresolutionScale; 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.
Save / load
Section titled “Save / load”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.