This skill should be used when working with the Foundry canvas, PIXI.js rendering, canvas layers, placeable objects (tokens, tiles, drawings), render flags for performance, or canvas lifecycle hooks like canvasReady.
Adds Foundry VTT canvas and PIXI.js rendering capabilities for custom visual elements. Use when creating sprites, graphics, custom layers, or extending token/tile rendering on the canvas.
/plugin marketplace add ImproperSubset/hh-agentics/plugin install fvtt-dev@hh-agenticsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-05
The Foundry canvas is a WebGL-powered HTML5 canvas using PIXI.js for rendering. Understanding the layer architecture and PIXI basics is essential for visual customizations.
Primary Group:
Interface Group:
canvas.tokens // TokenLayer
canvas.tiles // TileLayer
canvas.drawings // DrawingsLayer
canvas.templates // TemplatesLayer
canvas.walls // WallsLayer
canvas.lighting // LightingLayer
canvas.sounds // SoundsLayer
canvas.notes // NotesLayer
canvas.grid // GridLayer
canvas.primary // PrimaryCanvasGroup
canvas.interface // InterfaceCanvasGroup
canvas.environment // EnvironmentCanvasGroup
Group objects together:
const group = new PIXI.Container();
group.addChild(sprite1);
group.addChild(sprite2);
// Position the group (moves all children)
group.position.set(100, 100);
group.rotation = Math.PI / 4;
canvas.tokens.addChild(group);
Display images:
const sprite = PIXI.Sprite.from("path/to/image.png");
// Use anchor for rotation center (0-1 percentage)
sprite.anchor.set(0.5); // Center
sprite.position.set(100, 100);
sprite.width = 50;
sprite.height = 50;
sprite.rotation = Math.PI / 4; // Radians
sprite.alpha = 0.8;
sprite.tint = 0xFF0000; // Red tint
Draw shapes programmatically:
const graphics = new PIXI.Graphics();
// Filled rectangle
graphics.beginFill(0x0000FF, 0.5); // Blue, 50% alpha
graphics.drawRect(0, 0, 100, 100);
graphics.endFill();
// Stroked circle
graphics.lineStyle(2, 0xFF0000); // 2px red line
graphics.drawCircle(50, 50, 25);
// Polygon
graphics.beginFill(0x00FF00);
graphics.drawPolygon([0, 0, 50, 100, 100, 0]);
graphics.endFill();
canvas.drawings.addChild(graphics);
// Transparency
sprite.alpha = 0.5;
// Color tint (multiply)
sprite.tint = 0xFF0000; // Red
sprite.tint = 0xFFFFFF; // No change (white)
// Blend modes
sprite.blendMode = PIXI.BLEND_MODES.ADD; // Glow effect
sprite.blendMode = PIXI.BLEND_MODES.MULTIPLY; // Darken
sprite.blendMode = PIXI.BLEND_MODES.SCREEN; // Lighten
// Filters (use sparingly - performance impact)
const blur = new PIXI.BlurFilter();
blur.blur = 10;
sprite.filters = [blur];
// Graphics mask
const mask = new PIXI.Graphics();
mask.beginFill(0xFFFFFF);
mask.drawCircle(50, 50, 50);
mask.endFill();
container.mask = mask;
container.addChild(contentToMask);
const token = canvas.tokens.get(tokenId);
token.document // TokenDocument
token.scene // Parent Scene
token.isOwner // Ownership check
// Permission checks
token.can("update")
token.can("delete")
token.can("control")
// Control
token.control(); // Select
token.release(); // Deselect
await token.rotate(45); // Rotate by degrees
// Rendering
await token.draw(); // Full redraw
token.refresh(); // Incremental update
Optimize updates by specifying what changed:
token.renderFlags.set({
refreshPosition: true, // X/Y changed
refreshSize: true, // Width/height changed
refreshRotation: true, // Angle changed
refreshBars: true, // HP bars changed
refreshEffects: true, // Status icons changed
refreshBorder: true, // Selection border
refreshVisibility: true, // Vision state
refreshElevation: true, // Z-axis display
refreshNameplate: true, // Name display
redraw: true // Complete redraw
});
// BAD - full redraw every time
token.draw();
token.draw();
token.draw();
// GOOD - batch incremental updates
token.renderFlags.set({ refreshPosition: true });
token.renderFlags.set({ refreshBars: true });
// Updates happen efficiently in next render cycle
init
→ setup
→ canvasConfig
→ canvasInit
→ ready
→ canvasReady
// Canvas fully ready - safe to access all layers
Hooks.on("canvasReady", (canvas) => {
console.log("Scene:", canvas.scene.name);
console.log("Tokens:", canvas.tokens.placeables.length);
});
// Canvas being torn down
Hooks.on("canvasTearDown", (canvas) => {
// Clean up custom elements
});
// Canvas panned/zoomed
Hooks.on("canvasPan", (canvas, position) => {
console.log("New center:", position.x, position.y);
console.log("Scale:", position.scale);
});
// Promise-based
await canvas.ready;
// Hook-based
Hooks.once("canvasReady", () => {
// Safe to interact
});
Hooks.on("canvasReady", () => {
const marker = new PIXI.Graphics();
marker.beginFill(0xFF0000);
marker.drawCircle(0, 0, 20);
marker.endFill();
marker.position.set(500, 500);
canvas.interface.addChild(marker);
});
class CustomToken extends Token {
async _draw() {
await super._draw();
// Add custom aura
const aura = new PIXI.Graphics();
aura.beginFill(0x00FF00, 0.2);
aura.drawCircle(0, 0, this.w);
aura.endFill();
this.addChildAt(aura, 0); // Behind token
}
}
// Register
CONFIG.Token.objectClass = CustomToken;
// Client (viewport) to canvas coordinates
const canvasCoords = canvas.canvasCoordinatesFromClient({
x: event.clientX,
y: event.clientY
});
// Canvas to client coordinates
const clientCoords = canvas.clientCoordinatesFromCanvas({
x: 500,
y: 500
});
// Instant pan
await canvas.pan({ x: 1000, y: 1000 });
// Animated pan
await canvas.animatePan({
x: 1000,
y: 1000,
scale: 1.5,
duration: 1000
});
// Center on controlled token
await canvas.recenter();
// WRONG - canvas not initialized
Hooks.on("init", () => {
canvas.tokens.placeables; // undefined!
});
// CORRECT - wait for canvasReady
Hooks.on("canvasReady", () => {
canvas.tokens.placeables; // works
});
// WRONG - breaks PIXI rendering
element.style.transform = "rotate(45deg)";
// CORRECT - use PIXI properties
sprite.angle = 45; // degrees
sprite.rotation = Math.PI / 4; // radians
// WRONG - bypasses layer hierarchy
canvas.app.stage.addChild(myGraphics);
// CORRECT - add to appropriate group
canvas.interface.addChild(myGraphics);
// BAD - major performance hit
object.filters = [blur, color, displacement, glow];
// BETTER - minimal filters, combine effects
object.filters = [combinedEffect];
// Remember to remove custom elements
Hooks.on("canvasTearDown", () => {
myCustomElement.destroy();
});
// Rotation around top-left (default)
sprite.rotation = Math.PI / 4;
// Rotation around center (usually desired)
sprite.anchor.set(0.5);
sprite.rotation = Math.PI / 4;
canvasReady before canvas accesscanvasTearDownLast Updated: 2026-01-05 Status: Production-Ready Maintainer: ImproperSubset
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.