| name | power2d |
| description | GPU-accelerated 2D graphics rendering library built on Babylon.js. Provides styled shapes with custom shaders, stroke outlines, and thin instancing for 10,000+ shapes. Use when writing code that uses @avtools/power2d for 2D rendering, shape creation, materials, instancing, or canvas textures. |
| metadata | {"author":"avtools","version":"0.0.0"} |
| compatibility | Requires Babylon.js and earcut. WebGPU engine required for BatchedStyledShape. Deno-native with browser support via bundlers. |
@avtools/power2d
Summary
@avtools/power2d is a 2D graphics rendering library built on Babylon.js that provides efficient GPU-accelerated styled 2D shapes. It works in pixel-space coordinates (top-left origin, y-down) and handles the pixel-to-NDC conversion internally via shaders. The library supports custom WGSL/GLSL shaders through code-generated material definitions, optional stroke outlines with miter joins, and thin instancing for rendering 10,000+ shapes in a single draw call.
Package: @avtools/power2d (version 0.0.0)
Entry point: mod.ts
Dependencies: babylonjs@^8.20.0, earcut@^3.0.0
Runtime: Deno (uses .ts extensions and deno.json for config)
Architecture
The package is split into two layers:
power2d/
core/ # Pure geometry, no Babylon dependency
types.ts - Point2D, StrokeMeshData
point_generators.ts - Shape point generators (Rect, Circle, etc.)
stroke_mesh_generator.ts - Generates triangle-strip stroke meshes
mod.ts
babylon/ # Babylon.js integration
types.ts - MaterialDef, MaterialInstance, BatchMaterialDef, etc.
styled_shape.ts - StyledShape class (single shape with body + stroke)
batched_styled_shape.ts - BatchedStyledShape class (thin instancing)
scene_helpers.ts - createPower2DScene, CanvasTexture
material_names.ts - Unique material name generation
mod.ts
generated/ # Code-generated material definitions (from power2d-codegen)
mod.ts # Re-exports core/ and babylon/
Core is pure TypeScript with no rendering dependency. It generates point arrays and stroke mesh geometry.
Babylon consumes core geometry and wraps it in Babylon.js meshes with shader materials. All rendering uses an orthographic camera at [-1,1] NDC range, with shaders converting pixel coordinates to NDC.
Core Concepts
Coordinate System
Power2D uses pixel-space coordinates with origin at (0, 0) top-left. The y-axis points downward. Points are specified in pixels, and the shader pipeline applies a power2d_pixelToNDC transform to convert to clip space:
fn power2d_pixelToNDC(pixel: vec2f) -> vec4f {
let ndcX = (pixel.x / uniforms.power2d_canvasWidth) * 2.0 - 1.0;
let ndcY = -((pixel.y / uniforms.power2d_canvasHeight) * 2.0 - 1.0);
return vec4f(ndcX, ndcY, 0.0, 1.0);
}
Materials Are Code-Generated
Materials are not authored at runtime. They are defined as .wgsl or .glsl source files and processed by @avtools/power2d-codegen into TypeScript modules that export a MaterialDef (or BatchMaterialDef for instanced materials). Each generated module provides:
- Vertex and fragment shader source strings
- A
createMaterial(scene, name?) factory function
- Typed
setUniforms() with the specific uniform interface
- Typed
setTexture() with named texture slots
- Uniform defaults and metadata
You do not write BABYLON.ShaderMaterial code manually. Instead, import the generated material definition and pass it to StyledShape or BatchedStyledShape.
Shape Transform Pipeline
Every shape has a shader-level transform applied in the vertex shader:
- User's
vertShader() adjusts the pixel position (optional)
power2d_applyShapeTransform() applies scale, rotation, then translation
power2d_pixelToNDC() converts to NDC
This means x, y, rotation, scaleX, scaleY on StyledShape/BatchedStyledShape are handled entirely in the GPU via uniforms (power2d_shapeTranslate, power2d_shapeRotation, power2d_shapeScale).
Core API
Types
type Point2D = readonly [number, number];
interface StrokeMeshData {
positions: Float32Array;
uvs: Float32Array;
normals: Float32Array;
sides: Float32Array;
arcLengths: Float32Array;
normalizedArcs: Float32Array;
miterFactors: Float32Array;
indices: Uint32Array;
totalArcLength: number;
}
Point Generators
All generators return Point2D[]. Points define the outline of a closed polygon.
function RectPts(opts: {
x: number;
y: number;
width: number;
height: number;
}): Point2D[];
function CirclePts(opts: {
cx: number;
cy: number;
radius: number;
segments?: number; // default: 32
}): Point2D[];
function EllipsePts(opts: {
cx: number;
cy: number;
radiusX: number;
radiusY: number;
segments?: number; // default: 32
}): Point2D[];
function RegularPolygonPts(opts: {
cx: number;
cy: number;
radius: number;
sides: number;
rotation?: number; // radians, default: 0
}): Point2D[];
function PolygonPts(points: Array<{ x: number; y: number }>): Point2D[];
Stroke Mesh Generator
function generateStrokeMesh(
points: readonly Point2D[],
thickness: number,
closed: boolean,
): StrokeMeshData;
The generated mesh uses miter joins at corners (clamped to 4x thickness to prevent spikes). For closed paths, a seam-duplicate vertex is added to avoid UV interpolation artifacts across the closing edge. The thickness parameter is stored but the actual extrusion is done in the vertex shader using strokeNormal * side * thickness * miterFactor.
Babylon API
Material Type System
interface MaterialInstance<U, T extends string> {
material: BABYLON.ShaderMaterial;
setUniforms(uniforms: Partial<U>): void;
setTexture(name: T, texture: BABYLON.BaseTexture): void;
setCanvasSize(width: number, height: number): void;
dispose(): void;
setTextureSampler?(name: T, sampler: BABYLON.TextureSampler): void;
}
interface MaterialDef<U, T extends string> {
readonly createMaterial: (scene: BABYLON.Scene, name?: string) => MaterialInstance<U, T>;
readonly uniformDefaults: U;
readonly textureNames: readonly T[];
}
interface InstanceAttrLayout<I> {
size: number;
members: Array<{
name: keyof I;
offset: number;
floatCount: number;
}>;
}
interface BatchMaterialDef<U, T extends string, I> extends MaterialDef<U, T> {
readonly instanceAttrLayout: InstanceAttrLayout<I>;
}
type MaterialUniforms<M> = M extends MaterialDef<infer U, infer _T> ? U : never;
type MaterialTextureNames<M> = M extends MaterialDef<unknown, infer T> ? T : never;
type BatchInstanceAttrs<M> = M extends { instanceAttrLayout: InstanceAttrLayout<infer I> } ? I : never;
TextureSource
Accepted texture sources for setTexture():
type TextureSource =
| BABYLON.BaseTexture
| BABYLON.RenderTargetTexture
| { output: BABYLON.RenderTargetTexture };
StyledShape
A single shape with a body fill and an optional stroke outline. Both body and stroke have independent materials.
class StyledShape<BodyMat, StrokeMat?> {
constructor(options: {
scene: BABYLON.Scene;
points: readonly Point2D[];
bodyMaterial: BodyMat; // MaterialDef for the fill
strokeMaterial?: StrokeMat; // MaterialDef for the stroke (optional)
strokeThickness?: number; // default: 1
closed?: boolean; // default: true
canvasWidth: number;
canvasHeight: number;
});
body: {
setUniforms(uniforms: Partial<BodyUniforms>): void;
setTexture(name: BodyTextureName, source: TextureSource): void;
setTextureSampler(name: BodyTextureName, sampler: BABYLON.TextureSampler): void;
readonly mesh: BABYLON.Mesh;
};
stroke: {
setUniforms(uniforms: Partial<StrokeUniforms>): void;
setTexture(name: StrokeTextureName, source: TextureSource): void;
setTextureSampler(name: StrokeTextureName, sampler: BABYLON.TextureSampler): void;
thickness: number;
readonly mesh: BABYLON.Mesh;
} | null;
x: number;
y: number;
rotation: number;
scaleX: number;
scaleY: number;
position: BABYLON.Vector3;
scaling: BABYLON.Vector3;
alphaIndex: number;
setPoints(points: readonly Point2D[], closed?: boolean): void;
setCanvasSize(width: number, height: number): void;
dispose(): void;
}
Key details:
- Body mesh is triangulated using
earcut (polygon ear-clipping).
- UVs are computed as normalized bounding-box coordinates
(x - minX) / width.
- Setting
stroke.thickness triggers a full stroke mesh rebuild.
setPoints() disposes and recreates both body and stroke meshes.
- All materials get
disableDepthWrite = true, depthFunction = ALWAYS, backFaceCulling = false, and alphaMode = ALPHA_COMBINE (painter's algorithm for 2D).
BatchedStyledShape
Thin instancing for rendering many copies of the same shape with per-instance attributes. Uses WebGPU StorageBuffer for instance data.
class BatchedStyledShape<M extends BatchMaterialDef<...>> {
constructor(options: {
scene: BABYLON.Scene;
points: readonly Point2D[];
material: M; // BatchMaterialDef with instance layout
instanceCount: number;
canvasWidth: number;
canvasHeight: number;
closed?: boolean; // default: true
});
setUniforms(uniforms: Partial<MaterialUniforms<M>>): void;
setTexture(name: MaterialTextureNames<M>, source: TextureSource): void;
setTextureSampler(name: MaterialTextureNames<M>, sampler: BABYLON.TextureSampler): void;
writeInstanceAttr(index: number, values: Partial<BatchInstanceAttrs<M>>): void;
updateInstanceBuffer(): void;
beforeRender(): void;
setInstancingBuffer(buffer: BABYLON.StorageBuffer | null): void;
setExternalBufferMode(enabled: boolean): void;
getInstanceBuffer(): BABYLON.StorageBuffer;
x: number;
y: number;
rotation: number;
scaleX: number;
scaleY: number;
position: BABYLON.Vector3;
scaling: BABYLON.Vector3;
setCanvasSize(width: number, height: number): void;
dispose(): void;
}
Key details:
- Internally uses
thinInstanceSetBuffer('matrix', null, 16) and forcedInstanceCount for thin instancing.
- Instance data is stored in a
Float32Array of size instanceLayout.size * instanceCount.
writeInstanceAttr writes to the CPU-side buffer. Call updateInstanceBuffer() (or beforeRender()) to upload to the GPU.
- When using
setInstancingBuffer(), writeInstanceAttr is disabled (warns on use). This is for compute-shader-driven instance data.
- Instance attributes are exposed as
VertexBuffer objects with instanced divisor, named inst_<memberName> in shaders.
- Requires WebGPU engine (
BABYLON.WebGPUEngine), not the WebGL engine.
CanvasTexture
Helper for uploading HTML Canvas content to a GPU texture each frame.
class CanvasTexture {
constructor(options: {
engine: BABYLON.WebGPUEngine;
scene: BABYLON.Scene;
width?: number; // initial width, default: 1
height?: number; // initial height, default: 1
samplingMode?: number; // default: BILINEAR
});
readonly texture: BABYLON.BaseTexture;
readonly width: number;
readonly height: number;
update(canvas: HTMLCanvasElement | OffscreenCanvas): void;
dispose(): void;
}
Key details:
- Creates a
DynamicTexture internally with CLAMP addressing.
- On first creation, uploads a transparent canvas to make the texture "ready" for WebGPU.
- When canvas dimensions change, creates a new internal texture and defers disposal of the old one to the next frame to avoid "destroyed texture" errors.
- Pass
canvasTexture.texture to shape.body.setTexture(...).
createPower2DScene
Sets up a Babylon.js scene configured for 2D rendering.
function createPower2DScene(options: {
engine: BABYLON.WebGPUEngine;
canvasWidth: number;
canvasHeight: number;
clearColor?: BABYLON.Color4; // default: black (0,0,0,1)
}): {
scene: BABYLON.Scene;
camera: BABYLON.FreeCamera;
canvasWidth: number;
canvasHeight: number;
resize(width: number, height: number): void;
};
Key details:
- Creates an orthographic camera at
z = -1, looking at z = 0, with ortho bounds [-1, 1] in both axes.
autoClear = true, autoClearDepthAndStencil = true.
- The
resize() function is currently a no-op because the shaders handle pixel-to-NDC conversion via canvas size uniforms.
Code-Generated Material Structure
Materials are generated by @avtools/power2d-codegen from WGSL/GLSL source files. Each generated .generated.ts file exports:
For Body Materials
export const <Name>VertexSource: string;
export const <Name>FragmentSource: string;
export interface <Name>Uniforms {
time: number;
color: BABYLON.Vector3 | readonly [number, number, number];
}
export const <Name>UniformDefaults: <Name>Uniforms;
export const <Name>UniformMeta: readonly [...];
export function set<Name>Uniforms(material: BABYLON.ShaderMaterial, uniforms: Partial<...>): void;
export type <Name>TextureName = 'webcamTex' | ...;
export interface <Name>MaterialInstance { ... }
export function create<Name>Material(scene: BABYLON.Scene, name?: string): <Name>MaterialInstance;
export const <Name>Material: <Name>MaterialDef;
export default <Name>Material;
For Stroke Materials
Same structure but the vertex shader receives stroke-specific attributes (strokeNormal, strokeSide, strokeArcLength, strokeNormalizedArc, strokeMiterFactor) and the power2d_strokeThickness uniform.
The strokeVertShader function signature is:
fn strokeVertShader(
centerPos: vec2f,
normal: vec2f,
side: f32,
arcLength: f32,
normalizedArc: f32,
miterFactor: f32,
thickness: f32,
uniforms: <Name>Uniforms,
) -> vec2f
The strokeFragShader function signature is:
fn strokeFragShader(
uv: vec2f,
arcLength: f32,
normalizedArc: f32,
uniforms: <Name>Uniforms,
) -> vec4f
For Instanced (Batch) Materials
Extends the body material pattern with:
export interface <Name>Instance {
offset: readonly [number, number];
scale: number;
rotation: number;
tint: readonly [number, number, number];
instanceIndex: number;
}
export const <Name>InstanceAttrLayout: InstanceAttrLayout<...>;
export const <Name>Material: <Name>MaterialDef;
The vertShader and fragShader receive an additional inst: <Name>Instance parameter.
Usage Patterns
Basic Shape with Body Material
import { createPower2DScene, StyledShape, CirclePts } from '@avtools/power2d';
import { BasicMaterial } from './basic.material.wgsl.generated.ts';
const { scene, canvasWidth, canvasHeight } = createPower2DScene({
engine,
canvasWidth: 800,
canvasHeight: 600,
});
const circle = new StyledShape({
scene,
points: CirclePts({ cx: 400, cy: 300, radius: 100 }),
bodyMaterial: BasicMaterial,
canvasWidth,
canvasHeight,
});
circle.body.setUniforms({ time: 0, color: [1, 0.5, 0.2] });
circle.x = 400;
circle.y = 300;
circle.rotation += 0.01;
Shape with Body and Stroke
import { StyledShape, RectPts } from '@avtools/power2d';
import { BasicMaterial } from './basic.material.wgsl.generated.ts';
import { BasicStrokeMaterial } from './basic.strokeMaterial.wgsl.generated.ts';
const rect = new StyledShape({
scene,
points: RectPts({ x: 100, y: 100, width: 200, height: 150 }),
bodyMaterial: BasicMaterial,
strokeMaterial: BasicStrokeMaterial,
strokeThickness: 4,
canvasWidth,
canvasHeight,
});
rect.body.setUniforms({ color: [0.2, 0.4, 0.8] });
rect.stroke!.setUniforms({ color: [1, 1, 1] });
rect.stroke!.thickness = 8;
Batched Instancing (10,000+ shapes)
import { BatchedStyledShape, CirclePts } from '@avtools/power2d';
import { InstancedBasicMaterial } from './instancedBasic.material.wgsl.generated.ts';
const COUNT = 10000;
const batch = new BatchedStyledShape({
scene,
points: CirclePts({ cx: 0, cy: 0, radius: 5 }),
material: InstancedBasicMaterial,
instanceCount: COUNT,
canvasWidth,
canvasHeight,
});
batch.setUniforms({ time: 0, color: [1, 1, 1] });
for (let i = 0; i < COUNT; i++) {
batch.writeInstanceAttr(i, {
offset: [Math.random() * 800, Math.random() * 600],
scale: 0.5 + Math.random() * 2,
rotation: Math.random() * Math.PI * 2,
tint: [Math.random(), Math.random(), Math.random()],
instanceIndex: i,
});
}
batch.beforeRender();
Canvas Texture Integration
import { CanvasTexture, StyledShape, RectPts } from '@avtools/power2d';
import { WebcamPixelMaterial } from './webcamPixel.material.wgsl.generated.ts';
const canvasTex = new CanvasTexture({ engine, scene, width: 640, height: 480 });
const quad = new StyledShape({
scene,
points: RectPts({ x: 0, y: 0, width: 640, height: 480 }),
bodyMaterial: WebcamPixelMaterial,
canvasWidth: 800,
canvasHeight: 600,
});
const offscreenCanvas = getVideoFrame();
canvasTex.update(offscreenCanvas);
quad.body.setTexture('webcamTex', canvasTex.texture);
Dynamic Point Updates
const shape = new StyledShape({
scene,
points: CirclePts({ cx: 0, cy: 0, radius: 50 }),
bodyMaterial: BasicMaterial,
canvasWidth,
canvasHeight,
});
shape.setPoints(
RegularPolygonPts({ cx: 0, cy: 0, radius: 50, sides: 6 }),
true,
);
External Instance Buffer (Compute Shader Driven)
const computeBuffer = new BABYLON.StorageBuffer(engine, totalFloats * 4, ...);
batch.setInstancingBuffer(computeBuffer);
batch.setInstancingBuffer(null);
Render Pipeline Details
All materials created by the generated code have these settings:
disableDepthWrite = true -- 2D shapes do not write to depth buffer
depthFunction = BABYLON.Constants.ALWAYS -- always pass depth test
backFaceCulling = false -- both faces visible
alphaMode = BABYLON.Engine.ALPHA_COMBINE -- standard alpha blending
Render order is controlled by alphaIndex on StyledShape. Lower values render first (behind), higher values render on top.
Reserved Shader Uniforms
These uniforms are managed internally by power2d and must not be set manually:
| Uniform | Type | Purpose |
|---|
power2d_shapeTranslate | vec2f | Shape translation (x, y) |
power2d_shapeRotation | f32 | Shape rotation in radians |
power2d_shapeScale | vec2f | Shape scale (scaleX, scaleY) |
power2d_canvasWidth | f32 | Canvas width in pixels |
power2d_canvasHeight | f32 | Canvas height in pixels |
power2d_strokeThickness | f32 | Stroke thickness (stroke materials only) |
Stroke Vertex Attributes
Stroke meshes provide these custom vertex attributes:
| Attribute | Type | Description |
|---|
strokeNormal | vec2<f32> | Miter normal direction at vertex |
strokeSide | f32 | -1 for left edge, +1 for right edge |
strokeArcLength | f32 | Cumulative arc length in pixels |
strokeNormalizedArc | f32 | Arc length normalized to [0, 1] |
strokeMiterFactor | f32 | Miter join scale factor (clamped [-4, 4]) |
Caveats and Important Notes
-
WebGPU Required for BatchedStyledShape: The BatchedStyledShape class casts the engine to BABYLON.WebGPUEngine and uses StorageBuffer. It will not work with WebGL.
-
StyledShape works with both WebGL and WebGPU: The StyledShape class does not use StorageBuffer and can work with either engine, but the generated materials may be WGSL-only (WebGPU) or GLSL-only (WebGL) depending on which codegen path was used.
-
Materials must be code-generated: You cannot create a MaterialDef by hand in a practical way. Use @avtools/power2d-codegen to generate material TypeScript from .wgsl or .glsl source files.
-
setPoints() is expensive: It disposes and recreates the body mesh (and stroke mesh if present). Do not call it every frame. For animation, use transform properties or shader uniforms instead.
-
stroke.thickness setter rebuilds the mesh: Changing stroke.thickness at runtime triggers rebuildStrokeMesh(). Avoid rapid changes.
-
Closed shapes by default: StyledShape defaults to closed: true. Open paths must explicitly set closed: false.
-
Miter join limits: Stroke miter factors are clamped to [-4, 4] to prevent spike artifacts at sharp corners. Very acute angles will have clipped miters.
-
CanvasTexture deferred disposal: When the canvas size changes, the old InternalTexture is disposed on the next update() call, not immediately. This prevents WebGPU "destroyed texture" errors.
-
Instance attribute naming convention: In shaders, instance attributes are prefixed with inst_ (e.g., inst_offset, inst_scale). The corresponding varyings are prefixed with vInst_.
-
earcut triangulation: Body meshes use the earcut algorithm for polygon triangulation. This works well for simple and convex polygons but may produce suboptimal results for highly concave or self-intersecting polygons.
-
No z-ordering in geometry: All shapes are rendered at z = 0. Render order is determined by alphaIndex and Babylon.js mesh registration order.
-
Uniform vectors accept tuples: Generated uniform setters accept both BABYLON.Vector3 and readonly [number, number, number] tuples. Tuples are converted to Vector3 internally via Vector3.FromArray().
Monorepo Relationships
@avtools/power2d -- This package. The runtime library.
@avtools/power2d-codegen -- The code generator that processes .wgsl and .glsl shader files into TypeScript material definitions. It is a build-time dependency, not a runtime dependency. Its output goes into the generated/ directory (or a project-specific output path).
- Generated material files import from
babylonjs directly and conform to the MaterialDef / BatchMaterialDef interfaces defined in @avtools/power2d.
- The codegen supports both WGSL (for WebGPU) and GLSL (for WebGL) backends, producing separate
.wgsl.generated.ts and .gl.generated.ts files respectively.
File Reference
| File | Purpose |
|---|
core/types.ts | Point2D, StrokeMeshData type definitions |
core/point_generators.ts | RectPts, CirclePts, EllipsePts, RegularPolygonPts, PolygonPts |
core/stroke_mesh_generator.ts | generateStrokeMesh() function |
babylon/types.ts | MaterialDef, MaterialInstance, BatchMaterialDef, InstanceAttrLayout, TextureSource |
babylon/styled_shape.ts | StyledShape class |
babylon/batched_styled_shape.ts | BatchedStyledShape class |
babylon/scene_helpers.ts | createPower2DScene(), CanvasTexture class |
babylon/material_names.ts | createMaterialInstanceName() -- auto-incrementing unique names |
mod.ts | Re-exports everything from core/mod.ts and babylon/mod.ts |