Graphics
Draw procedural shapes and mesh-based geometry.
Graphics
Graphics is a Container that builds vector shapes and filled meshes procedurally at runtime. Each call to drawCircle, drawRectangle, drawLine, or any of the path commands appends a new colored Mesh child. Graphics inherits full filter, blend, tint, and mask support from Container, so every shape you draw participates in the scene graph the same way a Sprite would.
Use Graphics when you need shapes that are defined in code rather than loaded from an image — UI frames, debug overlays, particle decals, health bars, level-editor annotations, or any geometry you assemble from primitives.
Fill, stroke, and the active color
Graphics holds a fillColor and a lineColor, both Color instances. When you call a fill shape method (drawRectangle, drawCircle, drawPolygon, drawStar, drawEllipse), the shape is filled with the current fillColor. When lineWidth is greater than zero, the shape is also outlined with the current lineColor.
import { Color, Graphics } from '@codexo/exojs';
const g = new Graphics();
g.fillColor = new Color(0xff6347);
g.lineColor = new Color(0x8b0000);
g.lineWidth = 3;
g.drawRectangle(0, 0, 120, 80);
g.drawCircle(200, 40, 40);
Each draw call creates a separate Mesh. Changing fillColor between calls produces multi-colored output without creating a new Graphics instance:
import { Color, Graphics } from '@codexo/exojs';
const g = new Graphics();
g.fillColor = new Color(0xff6347);
g.drawCircle(0, 0, 30);
g.fillColor = new Color(0x4682b4);
g.drawRectangle(40, -20, 60, 40);
Writing a color
Color takes four spellings, and which one to reach for follows from whether alpha belongs in the literal:
import { Color } from '@codexo/exojs';
new Color(0xff6347); // packed 0xRRGGBB, opaque
new Color(0xff6347, 0.5); // packed, with alpha as its own argument
new Color(255, 99, 71, 0.5); // channel by channel: RGB 0..255, alpha 0..1
Color.fromHex('#ff6347cc'); // #RGB, #RGBA, #RRGGBB or #RRGGBBAA, alpha last
A number is always 0xRRGGBB and never carries alpha. That is not a limitation of the parser but of the number: JavaScript keeps no leading zeros, so 0x00FF00FF (opaque green, read as RGBA) and 0xFF00FF (magenta, read as RGB) are the same value at runtime and cannot be told apart. A string still carries its own length, so '#00ff00ff' is unambiguous — put alpha in a string, or pass it separately.
Color.from(value, alpha?) accepts any of those plus another Color or a plain { r, g, b, a }, which is what a config or a serialized document usually holds. In a loop, prefer color.setHex(...) — it overwrites in place instead of allocating.
The named constants are the eight corners of the RGB cube plus the two transparent ends:
Color.black, Color.red, Color.green, Color.blue, Color.cyan, Color.magenta, Color.yellow, Color.white, Color.transparentBlack, Color.transparentWhite, and Color.transparent (CSS transparent, the same value as transparentBlack).
Anything else is a value rather than a name — write new Color(0x6495ed), not a lookup. These instances are shared, so clone() before mutating one; a development build freezes them and throws on the write, a production build does not.
Going the other way, toRgb() gives back the 0xRRGGBB the constructor takes, and toHex(alpha?) the string form. toRgba8() is a different thing despite the similar name: one RGBA8 texel as a little-endian Uint32 for GPU upload, written 0xAABBGGRR. Do not feed it back into Color.from.
Built-in shapes
Seven shape methods cover the common cases. All coordinates are in the Graphics object’s local space and participate in its transform:
import { Graphics } from '@codexo/exojs';
const g = new Graphics();
g.drawRectangle(10, 20, 120, 80);
g.drawCircle(200, 60, 30);
g.drawEllipse(300, 60, 50, 25);
g.drawPolygon([0, -30, 30, 30, -30, 30]);
g.drawStar(400, 60, 5, 40, 20, Math.PI / 2);
g.drawLine(0, 150, 120, 180);
g.drawPath([150, 150, 200, 180, 250, 150]);
drawLine uses two explicit endpoints and does not affect the pen position. drawPath accepts a flat [x0, y0, x1, y1, ...] array. drawStar takes center, point count, outer radius, inner radius, and rotation in radians; the inner radius defaults to half the outer radius.
Path and pen commands
Graphics tracks a cursor position. Path commands move the cursor and emit stroked line segments:
import { Graphics } from '@codexo/exojs';
const g = new Graphics();
g.moveTo(50, 50);
g.lineTo(150, 100);
g.lineTo(100, 200);
// Quadratic Bézier curve: one control point
g.moveTo(20, 20);
g.quadraticCurveTo(80, 20, 120, 80);
// Cubic Bézier curve: two control points
g.bezierCurveTo(140, 20, 180, 120, 220, 60);
// Arc tangent to two lines through (x1,y1)
g.arcTo(220, 60, 260, 100, 20);
// Explicit arc
g.drawArc(320, 80, 30, 0, Math.PI, false);
Each of these emits a drawPath call behind the scenes. The currentPoint property reflects where the pen currently sits.
Reusable paths
The pen commands above draw as you call them: each segment becomes its own mesh, which is why a pen-drawn outline cannot be filled and why its segments overlap at the corners instead of joining. GraphicsPath is the other half of the story — a path you assemble first and draw afterwards, as one shape:
import { Color, Graphics, GraphicsPath } from '@codexo/exojs';
const shield = new GraphicsPath()
.moveTo(0, -40)
.quadraticCurveTo(35, -30, 35, 0)
.quadraticCurveTo(35, 35, 0, 50)
.quadraticCurveTo(-35, 35, -35, 0)
.quadraticCurveTo(-35, -30, 0, -40)
.closePath();
const g = new Graphics();
g.fillColor = new Color(60, 120, 220);
g.lineColor = Color.white;
g.lineWidth = 3;
g.drawShape(shield);
A path is a plain value. It owns no GPU resources, needs no disposal, and one path can be drawn into any number of Graphics objects — an icon reused across a HUD costs one path and one drawShape per place it appears.
It also stores commands rather than vertices, which is what lets it be drawn at any size. Pass the size of one path unit in device pixels as the second argument to drawShape and the curves are flattened to suit:
import { Graphics, GraphicsPath } from '@codexo/exojs';
const path = new GraphicsPath().circle(0, 0, 50);
const g = new Graphics();
g.scale.set(8, 8);
// Without this the circle is flattened for its unscaled size and shows facets.
g.drawShape(path, 1 / 8);
Filling closes each subpath implicitly, as a canvas fill does; stroking follows what the path declares, so only a subpath ended with closePath() is stroked all the way round.
Clearing and reusing
clear() removes all child meshes and resets the pen state (position, colors, line width). The Graphics object itself stays alive. This is the right pattern for per-frame redrawing:
override update(delta: Seconds): void {
this.g.clear();
this.g.fillColor = new Color(0xff6347);
for (let i = 0; i < this.values.length; i++) {
const h = this.values[i] * 200;
this.g.drawRectangle(i * 30, -h, 26, h);
}
}For shapes that do not change, create the Graphics once in init and never call clear. The meshes live as children and render identically each frame.
Graphics in the scene graph
Because Graphics extends Container, it inherits position, rotation, scale, and origin. A single Graphics object with several drawn shapes behaves as a group — rotating the Graphics rotates all its shape children together:
override init(): void {
this.group = new Graphics();
this.group.fillColor = new Color(0xdaa520);
this.group.drawCircle(-40, 0, 20);
this.group.drawCircle(40, 0, 20);
this.group.drawRectangle(-10, -15, 20, 30);
this.group.setPosition(this.app.width / 2, this.app.height / 2);
this.addChild(this.group);
}
override update(delta: Seconds): void {
this.group.rotate(60 * delta);
}Graphics vs. Mesh
Graphics is a convenience builder on top of Mesh. If you need direct control over vertex indices, UVs, per-vertex colors, textures, or custom shaders, construct a Mesh directly. Graphics is the right choice when the geometry is simple enough to describe with shape primitives and you want the immediate-mode API.
Performance
Each draw method on Graphics creates a new Mesh child. A tight loop that calls drawCircle 500 times per frame creates 500 meshes per frame. For static shapes this is fine — create once, never clear. For per-frame redrawing of many shapes, consider whether a single Mesh with a custom vertex buffer, a ParticleSystem, or a pre-built Graphics with transform-only animation is a better fit.
Examples
All seven shape primitives on one canvas, each animated differently.
Animated vertex deformation on a Mesh — the lower-level primitive Graphics builds on.
Try it
Playground
Where to go next
The next chapter, Sprites, covers the primary textured drawable — loading images, positioning, sizing, and the relationship between textures and sprites.