skills/power2d/SKILL.md
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.
npx skillsauth add avneeshsarwate/avtools power2dInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
@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)
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.
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 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:
createMaterial(scene, name?) factory functionsetUniforms() with the specific uniform interfacesetTexture() with named texture slotsYou do not write BABYLON.ShaderMaterial code manually. Instead, import the generated material definition and pass it to StyledShape or BatchedStyledShape.
Every shape has a shader-level transform applied in the vertex shader:
vertShader() adjusts the pixel position (optional)power2d_applyShapeTransform() applies scale, rotation, then translationpower2d_pixelToNDC() converts to NDCThis means x, y, rotation, scaleX, scaleY on StyledShape/BatchedStyledShape are handled entirely in the GPU via uniforms (power2d_shapeTranslate, power2d_shapeRotation, power2d_shapeScale).
// A 2D point as a readonly tuple
type Point2D = readonly [number, number];
// Triangle mesh data for a stroke outline
interface StrokeMeshData {
positions: Float32Array; // vec3 per vertex (z = 0)
uvs: Float32Array; // vec2 per vertex (u = 0|1 for side, v = normalizedArc)
normals: Float32Array; // vec2 per vertex (miter normal direction)
sides: Float32Array; // f32 per vertex (-1 = left, +1 = right)
arcLengths: Float32Array; // f32 per vertex (cumulative arc length in pixels)
normalizedArcs: Float32Array; // f32 per vertex (arc length / total length, 0..1)
miterFactors: Float32Array; // f32 per vertex (miter scaling, clamped to [-4, 4])
indices: Uint32Array; // triangle indices
totalArcLength: number; // total path length in pixels
}
All generators return Point2D[]. Points define the outline of a closed polygon.
// Rectangle from top-left corner
function RectPts(opts: {
x: number;
y: number;
width: number;
height: number;
}): Point2D[];
// Circle (default 32 segments)
function CirclePts(opts: {
cx: number;
cy: number;
radius: number;
segments?: number; // default: 32
}): Point2D[];
// Ellipse (default 32 segments)
function EllipsePts(opts: {
cx: number;
cy: number;
radiusX: number;
radiusY: number;
segments?: number; // default: 32
}): Point2D[];
// Regular polygon (triangle, pentagon, hexagon, etc.)
function RegularPolygonPts(opts: {
cx: number;
cy: number;
radius: number;
sides: number;
rotation?: number; // radians, default: 0
}): Point2D[];
// Arbitrary polygon from {x, y} objects
function PolygonPts(points: Array<{ x: number; y: number }>): Point2D[];
// Generates a triangle mesh for a thick stroke along a path
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.
// A material instance created from a definition
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;
}
// A material definition (factory pattern)
interface MaterialDef<U, T extends string> {
readonly createMaterial: (scene: BABYLON.Scene, name?: string) => MaterialInstance<U, T>;
readonly uniformDefaults: U;
readonly textureNames: readonly T[];
}
// Instance attribute layout for batched materials
interface InstanceAttrLayout<I> {
size: number; // total floats per instance
members: Array<{
name: keyof I;
offset: number; // float offset within instance stride
floatCount: number; // number of floats for this member
}>;
}
// A batched material definition extends MaterialDef with instance attributes
interface BatchMaterialDef<U, T extends string, I> extends MaterialDef<U, T> {
readonly instanceAttrLayout: InstanceAttrLayout<I>;
}
// Utility types to extract types from a MaterialDef
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;
Accepted texture sources for setTexture():
type TextureSource =
| BABYLON.BaseTexture
| BABYLON.RenderTargetTexture
| { output: BABYLON.RenderTargetTexture }; // e.g. ShaderEffect objects
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 material API
body: {
setUniforms(uniforms: Partial<BodyUniforms>): void;
setTexture(name: BodyTextureName, source: TextureSource): void;
setTextureSampler(name: BodyTextureName, sampler: BABYLON.TextureSampler): void;
readonly mesh: BABYLON.Mesh;
};
// Stroke material API (null if no stroke material provided)
stroke: {
setUniforms(uniforms: Partial<StrokeUniforms>): void;
setTexture(name: StrokeTextureName, source: TextureSource): void;
setTextureSampler(name: StrokeTextureName, sampler: BABYLON.TextureSampler): void;
thickness: number; // getter/setter; rebuilds stroke mesh on change
readonly mesh: BABYLON.Mesh;
} | null;
// Transform properties (shader-level, via uniforms)
x: number;
y: number;
rotation: number; // radians
scaleX: number;
scaleY: number;
position: BABYLON.Vector3; // convenience (reads/writes x, y)
scaling: BABYLON.Vector3; // convenience (reads/writes scaleX, scaleY)
alphaIndex: number; // render order for transparency
// Update shape geometry at runtime
setPoints(points: readonly Point2D[], closed?: boolean): void;
// Update canvas dimensions (affects pixel-to-NDC conversion)
setCanvasSize(width: number, height: number): void;
dispose(): void;
}
Key details:
earcut (polygon ear-clipping).(x - minX) / width.stroke.thickness triggers a full stroke mesh rebuild.setPoints() disposes and recreates both body and stroke meshes.disableDepthWrite = true, depthFunction = ALWAYS, backFaceCulling = false, and alphaMode = ALPHA_COMBINE (painter's algorithm for 2D).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
});
// Shared uniforms (same for all instances)
setUniforms(uniforms: Partial<MaterialUniforms<M>>): void;
setTexture(name: MaterialTextureNames<M>, source: TextureSource): void;
setTextureSampler(name: MaterialTextureNames<M>, sampler: BABYLON.TextureSampler): void;
// Per-instance attribute writing
writeInstanceAttr(index: number, values: Partial<BatchInstanceAttrs<M>>): void;
// Upload instance data to GPU (call once per frame after all writes)
updateInstanceBuffer(): void;
// Convenience: calls updateInstanceBuffer()
beforeRender(): void;
// Use an external StorageBuffer for instance data (e.g. from compute shader)
setInstancingBuffer(buffer: BABYLON.StorageBuffer | null): void;
setExternalBufferMode(enabled: boolean): void;
getInstanceBuffer(): BABYLON.StorageBuffer;
// Transform (applies to ALL instances as a group)
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:
thinInstanceSetBuffer('matrix', null, 16) and forcedInstanceCount for thin instancing.Float32Array of size instanceLayout.size * instanceCount.writeInstanceAttr writes to the CPU-side buffer. Call updateInstanceBuffer() (or beforeRender()) to upload to the GPU.setInstancingBuffer(), writeInstanceAttr is disabled (warns on use). This is for compute-shader-driven instance data.VertexBuffer objects with instanced divisor, named inst_<memberName> in shaders.BABYLON.WebGPUEngine), not the WebGL engine.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; // pass to setTexture()
readonly width: number;
readonly height: number;
// Upload canvas content; auto-resizes if canvas dimensions changed
update(canvas: HTMLCanvasElement | OffscreenCanvas): void;
dispose(): void;
}
Key details:
DynamicTexture internally with CLAMP addressing.canvasTexture.texture to shape.body.setTexture(...).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:
z = -1, looking at z = 0, with ortho bounds [-1, 1] in both axes.autoClear = true, autoClearDepthAndStencil = true.resize() function is currently a no-op because the shaders handle pixel-to-NDC conversion via canvas size uniforms.Materials are generated by @avtools/power2d-codegen from WGSL/GLSL source files. Each generated .generated.ts file exports:
// Shader source strings
export const <Name>VertexSource: string;
export const <Name>FragmentSource: string;
// Typed uniform interface
export interface <Name>Uniforms {
time: number;
color: BABYLON.Vector3 | readonly [number, number, number];
// ... per-material
}
// Defaults
export const <Name>UniformDefaults: <Name>Uniforms;
// Uniform metadata (binding names, WGSL types)
export const <Name>UniformMeta: readonly [...];
// Typed setter
export function set<Name>Uniforms(material: BABYLON.ShaderMaterial, uniforms: Partial<...>): void;
// Texture slot names (type-level, can be `never` if no textures)
export type <Name>TextureName = 'webcamTex' | ...;
// Material instance interface
export interface <Name>MaterialInstance { ... }
// Factory function
export function create<Name>Material(scene: BABYLON.Scene, name?: string): <Name>MaterialInstance;
// MaterialDef object (pass to StyledShape/BatchedStyledShape)
export const <Name>Material: <Name>MaterialDef;
export default <Name>Material;
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
Extends the body material pattern with:
// Per-instance attribute interface
export interface <Name>Instance {
offset: readonly [number, number];
scale: number;
rotation: number;
tint: readonly [number, number, number];
instanceIndex: number;
// ... per-material
}
// Instance attribute layout
export const <Name>InstanceAttrLayout: InstanceAttrLayout<...>;
// MaterialDef includes instanceAttrLayout
export const <Name>Material: <Name>MaterialDef; // satisfies BatchMaterialDef
The vertShader and fragShader receive an additional inst: <Name>Instance parameter.
import { createPower2DScene, StyledShape, CirclePts } from '@avtools/power2d';
import { BasicMaterial } from './basic.material.wgsl.generated.ts';
// Setup
const { scene, canvasWidth, canvasHeight } = createPower2DScene({
engine,
canvasWidth: 800,
canvasHeight: 600,
});
// Create a circle
const circle = new StyledShape({
scene,
points: CirclePts({ cx: 400, cy: 300, radius: 100 }),
bodyMaterial: BasicMaterial,
canvasWidth,
canvasHeight,
});
// Set uniforms
circle.body.setUniforms({ time: 0, color: [1, 0.5, 0.2] });
// Animate
circle.x = 400;
circle.y = 300;
circle.rotation += 0.01;
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,
});
// Body and stroke have independent uniforms
rect.body.setUniforms({ color: [0.2, 0.4, 0.8] });
rect.stroke!.setUniforms({ color: [1, 1, 1] });
// Change stroke thickness at runtime (rebuilds stroke mesh)
rect.stroke!.thickness = 8;
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,
});
// Set shared uniforms
batch.setUniforms({ time: 0, color: [1, 1, 1] });
// Write per-instance attributes
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,
});
}
// In render loop: upload instance data to GPU
batch.beforeRender(); // or batch.updateInstanceBuffer()
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,
});
// In render loop:
const offscreenCanvas = getVideoFrame(); // your canvas source
canvasTex.update(offscreenCanvas);
quad.body.setTexture('webcamTex', canvasTex.texture);
const shape = new StyledShape({
scene,
points: CirclePts({ cx: 0, cy: 0, radius: 50 }),
bodyMaterial: BasicMaterial,
canvasWidth,
canvasHeight,
});
// Later: change to a different shape entirely
shape.setPoints(
RegularPolygonPts({ cx: 0, cy: 0, radius: 50, sides: 6 }),
true, // closed
);
// Create a StorageBuffer from a compute shader
const computeBuffer = new BABYLON.StorageBuffer(engine, totalFloats * 4, ...);
// Attach to batched shape
batch.setInstancingBuffer(computeBuffer);
// Now writeInstanceAttr is disabled; the compute shader writes directly
// batch.writeInstanceAttr(i, ...) will warn
// To switch back to CPU-driven:
batch.setInstancingBuffer(null);
All materials created by the generated code have these settings:
disableDepthWrite = true -- 2D shapes do not write to depth bufferdepthFunction = BABYLON.Constants.ALWAYS -- always pass depth testbackFaceCulling = false -- both faces visiblealphaMode = BABYLON.Engine.ALPHA_COMBINE -- standard alpha blendingRender order is controlled by alphaIndex on StyledShape. Lower values render first (behind), higher values render on top.
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 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]) |
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().
@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).babylonjs directly and conform to the MaterialDef / BatchMaterialDef interfaces defined in @avtools/power2d..wgsl.generated.ts and .gl.generated.ts files respectively.| 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 |
tools
Deno Jupyter-to-iframe communication infrastructure for embedding interactive web components in notebooks. Provides HTTP/WebSocket bridge, session management, and adapter pattern. Use when writing code that uses @avtools/ui-bridge for notebook UI, WebSocket clients, or component adapters.
tools
GPU-accelerated shader effects framework built on Babylon.js. Provides composable graph-based post-processing, multi-pass rendering, feedback loops, and fluid simulation. Use when writing code that uses @avtools/shader-fx for shader effects, effect chains, or GPU computations.
tools
Shader code generation framework that transforms WGSL and GLSL shader source files into TypeScript companion modules with type-safe APIs for Babylon.js. Use when writing or modifying shader codegen, material generation, or working with @avtools/power2d-codegen.
tools
Music theory and MIDI note manipulation library. Provides type definitions, data structures, and utilities for Ableton Live clips, musical scales, Bezier curve envelopes, and MPE data. Use when writing code that uses @avtools/music-types for clips, notes, scales, curves, or MIDI operations.