Drop any .js file into the /plugins folder next to the executable. It will be picked up automatically on launch.
Your file must export an object with the following fields:
id - string, required, unique identifier such as "my-cool-tool"name - string, required, display name shown in the panel header and Plugins menuwidth - number, optional, default panel width in px, default 360height - number, optional, default panel height in px, default 420, including the headerfitWidth / fitHeight - number, optional, size used when the user clicks the fit buttonmount(ctx) - function, required, called once when the panel is first openedmount() receives a context object:
ctx.el - the DOM element you should build your UI insidectx.api - helper methods and app hooksIf mount() returns a function, it will be used as cleanup when the plugin is destroyed.
These are the safe helper methods passed to your plugin through ctx.api:
api.toast(msg) - show a toast notificationapi.getTool() - returns current tool nameapi.setTool(name) - switch to a tool by nameapi.requestRender() - redraw the canvas after changing pixelsapi.canvas() - returns the main pixel canvas elementapi.overlays() - returns { cursorCanvas, gridCanvas, refCanvas }External plugins are loaded as CommonJS modules, so use ctx.api instead of relying on free globals like currentColor.
api.getFG() - get primary foreground color hex stringapi.getBG() - get secondary background color hex stringapi.setFG(hex) - set foreground color and update UIapi.setBG(hex) - set background color and update UIapi.TRANSPARENT - the string "transparent" for erased pixels, if exposedtilesWidth, tilesHeight - canvas size in tilestileSize - pixels per tilepixelSize - zoom level in screen px per canvas pxzoomLevel - current zoom multipliercanvasOffset - pan offset object { x, y }canvasData - 2D array [y][x] of hex strings or nulllayers - array of layer objectscurrentLayerId - active layer idEach layer object has { id, name, visible, opacity, blendMode, data, offsetX, offsetY, clippingMask, styles, tlLocked }.
createLayer(name) - create a new layeraddLayer() - add a layer and update UIdeleteLayer() - delete the active layerselectLayer(id) - switch active layermergeDown(id) - merge layer with the one belowtoggleLayerVisibility(id) - show or hide a layerrenderLayersPanel() - refresh the layers panel UIdrawPixel(x, y, color) - draw one pixel on the active layerfloodFill(x, y, color) - flood fill from a pointdrawCanvas() - full canvas redraw across layerscurrentTool - current tool name stringselectTool(name) - switch tool and update UIbrushSize - pencil brush sizeeraserSize - eraser sizeTool names include pencil, eraser, fill, eyedropper, move, marquee, shape, line, gradient, dither, zoom, and pan.
Use these helpers to read, switch, render, or create palettes from an external plugin:
api.getPalettes() - returns { key: { name, colors[] } }api.getCurrentPalette() - returns the active palette keyapi.changePalette(key) - switch palette and re-renderapi.renderPalette() - refresh palette displayapi.updatePaletteDeleteBtn() - update delete button stateapi.savePalette(name, colors) - create a new palette, store it, and switch to itSystem palettes that cannot be deleted: canvas, gb, gbc, gba, snes.
const colors = ["#ff0000", "#00ff00", "#0000ff"];
ctx.api.savePalette("My Ramp", colors);
saveHistory(label) - push current state to the undo stackundo() - undo last actionredo() - redo last undone actionhistory - array of history state objectshistoryStep - current position in the history stackselection - current selection object or nullmarqueeMode - selection mode such as "rect"Use these instead of alert, confirm, or prompt so the UI matches the app theme.
themedAlert(msg, title?) - themed alert dialog, returns a promisethemedConfirm(msg, title?) - themed confirm dialog, resolves to true or falsethemedPrompt(msg, default?, title?) - themed input prompt, resolves to a string or nullresetView() - reset zoom and panfitToScreen() - fit the canvas to the viewportsetZoomAbsolute(z) - set zoom to a specific levelshowGrid - boolean for grid visibilityimportPng() - open PNG import dialogimportPalette() - open palette import dialogexportAnimatedGif() - open GIF export dialogThese custom properties are defined on :root and update with the theme.
--bg-main: #535353 Main background --bg-dark: #333333 Darker bg areas --bg-darker: #282828 Darkest bg --panel-bg: #3d3d3d Panel background --panel-header: #2e2e2e Panel header bar --panel-border: #1a1a1a Borders between panels --accent: #1473e6 Accent / active highlights --accent-hover: #0d66d0 Accent on hover --text: #d4d4d4 Primary text --text-dim: #a0a0a0 Secondary / dimmed text --hover: #505050 Hover state bg --active: #5a5a5a Active or pressed bg --button-bg: #4a4a4a Button backgrounds --input-bg: #2a2a2a Input field backgrounds --border-light: #505050 Lighter borders on inputs
These values are shown for the dark theme and change automatically when the theme changes.
Use these classes to match the app look and theme behavior.
.option-btn - standard button.seg-btn - segmented control button.segmented - wrapper for grouped segment buttons.seg-btn.active - active segment state.size-slider - styled range input.size-number - small number input.tool-select - styled select dropdown.option-group - flex row with gap for controls.option-label - dimmed label text.checkbox-wrapper - checkbox and label row.checkbox-label - checkbox label text.tl-btn - compact timeline buttonButton
const btn = document.createElement("button");
btn.className = "option-btn";
btn.textContent = "Do Thing";
Slider and number input
const row = document.createElement("div");
row.className = "option-group";
const label = document.createElement("span");
label.className = "option-label";
label.textContent = "Size";
const slider = document.createElement("input");
slider.type = "range";
slider.className = "size-slider";
slider.min = "1";
slider.max = "32";
const num = document.createElement("input");
num.type = "number";
num.className = "size-number";
row.append(label, slider, num);
Dropdown select
const sel = document.createElement("select");
sel.className = "tool-select";
["Option A", "Option B"].forEach(t => {
const o = document.createElement("option");
o.value = t;
o.textContent = t;
sel.appendChild(o);
});
Segmented control
const seg = document.createElement("div");
seg.className = "segmented";
["1x", "2x", "4x"].forEach((label, i) => {
const btn = document.createElement("button");
btn.className = "seg-btn" + (i === 0 ? " active" : "");
btn.textContent = label;
btn.onclick = () => {
seg.querySelectorAll(".seg-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
};
seg.appendChild(btn);
});
Checkbox
const wrap = document.createElement("label");
wrap.className = "checkbox-wrapper";
const cb = document.createElement("input");
cb.type = "checkbox";
const lbl = document.createElement("span");
lbl.className = "checkbox-label";
lbl.textContent = "Enable thing";
wrap.append(cb, lbl);
module.exports = {
id: "my-plugin",
name: "My Plugin",
width: 300,
height: 200,
mount(ctx) {
const btn = document.createElement("button");
btn.className = "option-btn";
btn.textContent = "Hello!";
btn.onclick = () => ctx.api.toast("It works!");
ctx.el.appendChild(btn);
return () => {};
}
};
ctx.api.saveHistory("label") and then drawCanvas() to commit and display changes.name export, so you only build the content inside ctx.el.