Plugin API
Overview
Tokenizer 2 exposes a hook-based plugin system that allows other Foundry VTT modules to:
- Register toolbar tools with custom icons, panels, and consent dialogs
- Listen to lifecycle hooks fired during editor use
- Inject license information into the Settings → Licenses tab
Plugins are standard Foundry VTT modules that declare a dependency on tokenizer-2 and register themselves via the tokenizer-2.registerPlugins hook.
Quick Start
1. Create a Foundry module
my-plugin/
├── module.json
├── scripts/
│ └── index.js
└── lang/
└── en.json
module.json - declare a dependency on tokenizer-2:
{
"id": "my-tokenizer-plugin",
"title": "My Tokenizer Plugin",
"version": "1.0.0",
"compatibility": { "minimum": "13", "verified": "13" },
"relationships": {
"requires": [{ "id": "tokenizer-2", "type": "module" }]
},
"esmodules": ["scripts/index.js"],
"languages": [{ "lang": "en", "name": "English", "path": "lang/en.json" }]
}
2. Register your plugin
In scripts/index.js:
import { MyTool } from "./MyTool.js";
Hooks.once("tokenizer-2.registerPlugins", (registry) => {
registry.register({
id: "my-tool",
moduleId: "my-tokenizer-plugin",
name: "My Tool",
description: "Does something useful",
version: "1.0.0",
author: "Your Name",
tools: [{
id: "my-tool",
icon: "fa-solid fa-wand-magic-sparkles",
tooltip: "My Tool Tooltip",
toolClass: MyTool,
panel: renderMyPanel,
}],
});
});
function renderMyPanel(container, { app, tool }) {
const p = document.createElement("p");
p.textContent = "My tool is active!";
container.appendChild(p);
}
Plugin Descriptor
The object passed to registry.register():
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique plugin identifier |
moduleId | string | yes | The Foundry module ID (must match module.json) |
name | string | yes | Display name |
description | string | no | Short description |
version | string | no | Plugin version |
author | string | no | Author name |
license | object | no | License info shown in Settings → Licenses tab |
license.name | string | yes (if license) | Library/license name |
license.text | string | no | Description text |
license.url | string | no | Link to library/license |
license.copyright | string | no | Copyright notice |
tools | ToolDescriptor[] | no | Array of tools to add to the toolbar |
hooks | object | no | Map of hook names to callback functions |
Tool Descriptor
Each entry in the tools array:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique tool identifier (used as data-tool attribute) |
icon | string | yes | FontAwesome class (e.g. "fa-solid fa-scissors") |
tooltip | string | yes | Tooltip text or localization key (keys containing . are passed through game.i18n.localize()) |
toolClass | class | yes | ES class instantiated when the tool is activated |
panel | function | no | (container: HTMLElement, ctx: { app, tool }) => void - builds the left-panel UI |
consent | object | no | Consent dialog shown before first use |
Consent Descriptor
If your tool requires user consent (e.g. for AI/ML features):
| Field | Type | Required | Description |
|---|---|---|---|
moduleId | string | no | Module ID for the setting (defaults to plugin's moduleId) |
settingKey | string | yes | Setting key storing consent state ("" = not asked, "yes", "no") |
title | string | yes | Dialog title (or localization key) |
content | string | yes | Dialog body HTML (or localization key) |
yesLabel | string | yes | Accept button label (or localization key) |
noLabel | string | yes | Decline button label (or localization key) |
You must register the consent setting in a Hooks.once("init", ...) handler under your module's ID:
Hooks.once("init", () => {
game.settings.register("my-tokenizer-plugin", "my-consent", {
scope: "world",
config: false,
type: String,
default: "",
});
});
Tool Class Interface
Your tool class is instantiated when the user clicks the toolbar button and destroyed when they switch away. Implement any of these methods:
export class MyTool {
/**
* Called when the tool becomes active.
* @param {object} ctx
* @param {TokenImageEditor} ctx.app - The editor instance
* @param {LayerManager} ctx.layerManager
* @param {CanvasEngine} ctx.canvasEngine
* @param {Function} ctx.scheduleRender - Call to trigger a canvas re-render
* @param {Function} ctx.pushUndoSnapshot - Call before making changes for undo support
*/
activate(ctx) {}
/** Called when the tool is deactivated (user switches to another tool). */
deactivate() {}
/**
* Called when the user selects a different layer while this tool is active.
* @param {string} newLayerId
*/
onActiveLayerChange(newLayerId) {}
// Pointer event handlers - called by ToolManager with logical canvas coordinates
onPointerDown(e, logicalX, logicalY) {}
onPointerMove(e, logicalX, logicalY) {}
onPointerUp(e, logicalX, logicalY) {}
onWheel(e) {}
onKeyDown(e) {}
}
Panel Function
The panel function is called on every render cycle when your tool is the active tool. It receives:
container- an empty<div id="tie-plugin-panel">element. Build your UI by appending DOM nodes.ctx.app- theTokenImageEditorinstance (accesslayerManager,canvasEngine, etc.)ctx.tool- your tool class instance (access tool state like slider values)
The container is cleared (innerHTML = "") before each call, so build the full panel every time. Attach event listeners directly to your elements.
function renderMyPanel(container, { app, tool }) {
const heading = document.createElement("h3");
heading.className = "tie-panel__heading";
heading.textContent = "My Tool";
container.appendChild(heading);
// Add sliders, buttons, etc.
}
Lifecycle Hooks
Subscribe to these hooks via the standard Foundry Hooks.on() API, or via the hooks field in your plugin descriptor.
| Hook Name | Fired When | Data |
|---|---|---|
tokenizer-2.registerPlugins | Once at startup, before tokenizer-2.ready | registry - call registry.register({...}) |
tokenizer-2.ready | Once after all plugins registered | api - the Tokenizer 2 public API object |
tokenizer-2.editorOpen | Editor finishes first render | { editor, actor, token } |
tokenizer-2.editorClose | Editor begins closing | { editor, actor } |
tokenizer-2.preSave | Before token image is exported | { editor, actor, layers } |
tokenizer-2.postSave | After token image is saved | { editor, actor, path } |
tokenizer-2.toolActivated | A tool becomes active | { toolName, tool } |
tokenizer-2.toolDeactivated | A tool is deactivated | { toolName } |
tokenizer-2.layerAdded | A layer is added to the stack | { layer } |
tokenizer-2.layerRemoved | A layer is removed from the stack | { layerId } |
Using the hooks field
registry.register({
id: "my-plugin",
moduleId: "my-tokenizer-plugin",
name: "My Plugin",
hooks: {
"tokenizer-2.editorOpen": ({ editor, actor }) => {
console.log(`Editor opened for ${actor.name}`);
},
"tokenizer-2.layerAdded": ({ layer }) => {
console.log(`Layer added: ${layer.name}`);
},
},
});
Accessing Tokenizer 2 Internals
The Tokenizer 2 API is available after the tokenizer-2.ready hook:
// Via the module API
const api = game.modules.get("tokenizer-2").api;
// Via the global
const api = window.Tokenizer2;
The API includes pluginRegistry for inspecting registered plugins at runtime.
Within a tool's activate() method and the panel function, you receive the app (editor) instance which provides access to:
app.layerManager- layer CRUD, active layer, serializationapp.canvasEngine- rendering, image cache, exportapp.toolManager- active tool stateapp._scheduleRender()- trigger a canvas re-render
Tokenizer Lib
Tokenizer exposes it's libray through globalThis.Tokenizer2.lib
Example: Auto Subject Cutout
This example will be available on GitHub soon!