Skip to content

Graphics

GraphicsComponent is the right primitive when you need to draw shapes procedurally — debug overlays, prototype art, gradient bars, custom UI chrome that doesn’t fit a flexbox model. For laid-out widgets with text and backgrounds, prefer UISurface instead.

import { GraphicsComponent } from "@yagejs/renderer";
entity.add(
new GraphicsComponent({ layer: "world", tint: 0x88ccff }).draw((g) => {
g.circle(0, 0, 50).fill({ color: 0x38bdf8 });
}),
);

Call .draw() again at any time to redraw. The callback receives a PixiJS Graphics object, so all standard drawing methods are available — rect, roundRect, poly, moveTo/lineTo, stroke, and fill.

GraphicsComponent takes the same visible / tint / alpha / blendMode / interactive options as SpriteComponent, plus setMask() / fx for masking and filters. Graphics are runtime resources and are not part of an @yagejs/save state root. Save the game facts that determine the drawing, then let normal scene setup call .draw() when the scene is constructed.

Escape hatch: .graphics (and the g passed to .draw(fn)) is a raw pixi Graphics instance. The fluent API mirrors Canvas 2D vocabulary — moveTo, lineTo, arc, bezierCurveTo, plus shape shortcuts like rect and circle. arc continues the current path: moveTo() to its start point first for a standalone arc, otherwise a line connects it from the previous point. Fill and stroke take options bags ({ color, alpha, width, ... }). See the pixi Graphics docs.

The drawing’s (0, 0) sits on the entity’s position, and the entity’s rotation turns the drawing about that point. A barrel drawn upwards from (0, 0) therefore tips from side to side about its base and never rolls.

pivot names a different point of the drawing to put on the entity position, in the drawing’s own pixels. Rotation and scale then act about that point:

import { GraphicsComponent } from "@yagejs/renderer";
// The circle's centre sits on the entity, so the barrel rolls in place.
entity.add(
new GraphicsComponent({ layer: "world", pivot: { x: 0, y: -24 } }).draw(
(g) => {
g.circle(0, -24, 24).fill({ color: 0x8b5a2b });
},
),
);

pivot is the GraphicsComponent counterpart of the anchor that sprites and text take. It is in pixels rather than a 0–1 fraction, because a drawing has no texture size to take a fraction of. Both numbers must be finite; a NaN throws naming the component and the coordinate.

Drawing the shape around (0, 0) in the first place reaches the same result, and is the simpler choice when you control the callback. pivot is for the drawings whose coordinates are fixed by something else — a callback shared by entities that turn about different points, or a shape whose numbers come from data.

Neither route turns a drawing about a point outside its own entity. For that, parent the entity under another entity placed at the turning point and rotate the parent: an entity’s rotation carries every descendant around its own position.

The context carries a transform stack, so a part that repeats draws in its own coordinates and the callback works out no per-copy offsets. translateTransform(x, y?), scaleTransform(x, y?) and rotateTransform(angle) multiply into the current transform. setTransform(matrix) — or six numbers — replaces it, and resetTransform() returns it to identity. save() pushes the current transform and restore() pops it. Each of them returns the context, so calls chain:

import { GraphicsComponent } from "@yagejs/renderer";
entity.add(
new GraphicsComponent({ layer: "world" }).draw((g) => {
for (let i = 0; i < 6; i++) {
g.save();
g.translateTransform(i * 24, 0).rotateTransform(i * 0.2);
g.rect(-6, -6, 12, 12).fill(0xffcc00); // square drawn around its origin
g.restore();
}
}),
);

Each square is described once, at the origin, and the transform places and turns it. The save() / restore() pair is what keeps the six copies independent: without it each transform would build on the one before, so the squares would spread further apart and turn further with every step.

For linear and radial gradients, use linearGradient / radialGradient instead of importing FillGradient from pixi.js. Stops use yage’s numeric color + alpha pairs (no CSS color strings), and the result plugs straight into .fill():

import {
GraphicsComponent,
linearGradient,
radialGradient,
} from "@yagejs/renderer";
const fade = linearGradient({
axis: "vertical", // or "horizontal", or explicit start/end points
stops: [
{ offset: 0, color: 0x000000, alpha: 0.8 },
{ offset: 1, color: 0x000000, alpha: 0 },
],
});
entity.add(
new GraphicsComponent({ layer: "fog" }).draw((g) => {
g.rect(0, 0, 200, 40).fill(fade);
}),
);

By default, coordinates and radii are in local space (the 0-1 range of the filled shape), so one gradient instance scales to any rect you apply it to. Pass space: "global" to treat them as absolute pixels instead.

radialGradient additionally accepts an outerCenter for non-concentric radial effects (rim lighting, directional glows).

Gradients own a GPU texture — call .destroy() in a component’s onDestroy() when you’re done with them.

Re-export: GradientFill IS pixi FillGradient. The factories convert yage’s numeric stops to pixi color stops and forward the rest as-is. See the pixi FillGradient docs.

A fill also takes a texture, and the texture tiles across the shape. A small baked strip covers a shape of any size with a repeating pattern — you do not need one texture per band, and you do not need a tiling sprite:

createTexture hands back a GPU texture the caller owns, so bake it inside a component that destroys it again:

import { Component } from "@yagejs/core";
import { GraphicsComponent, RendererKey } from "@yagejs/renderer";
import type { TextureResource } from "@yagejs/renderer";
class SkyBand extends Component {
private band!: TextureResource;
onAdd(): void {
// Bake the tile once, then fill a much larger shape with it.
this.band = this.use(RendererKey).createTexture(
(g) => {
g.rect(0, 0, 64, 16).fill(0x1b3a5c);
g.rect(0, 16, 64, 16).fill(0x24507d);
},
{ width: 64, height: 32 },
);
this.entity.add(
new GraphicsComponent({ layer: "sky" }).draw((g) => {
g.rect(0, 0, 1200, 400).fill({
texture: this.band,
textureSpace: "global",
});
}),
);
}
onDestroy(): void {
this.band.destroy(); // nothing draws it after the owner goes
}
}

textureSpace: "global" measures the texture in the shape’s own coordinates, so the pattern repeats every texture.source.width pixels however large the shape is. A texture from createTexture is its own source, so that is texture.width. A frame cut from a spritesheet fills with the whole sheet, not the frame. The default, "local", stretches exactly one copy over the shape’s bounds — useful when you want the texture to fit the shape rather than tile inside it.

The size passed to createTexture names the region it bakes, measured from (0, 0). Without a size the texture is the drawn bounds, so its top-left corner is the first pixel drawn: a drawing that starts at (10, 4) bakes shifted by that much and tiles at the smaller size.

To scroll the pattern, pass a matrix and redraw: new Matrix().translate(-offset, 0) moves it sideways. Matrix comes from pixi.js, like the Graphics object .draw() hands you.

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.