API reference
Build plugins to extend Acme Design Studio.
Getting Started
Acme plugins are written in JavaScript and have access to the Acme API.
Creating a Plugin
Create a folder with this structure:
my-plugin/
├── manifest.json
├── code.js
└── ui.html (optional)
manifest.json
{
"name": "My Plugin",
"id": "com.example.myplugin",
"version": "1.0.0",
"description": "A description of what your plugin does",
"author": "Your Name",
"main": "code.js",
"ui": "ui.html",
"permissions": ["read", "write"],
"menu": [
{
"name": "My Plugin",
"command": "show-ui"
}
]
}
code.js
// Show plugin UI
acme.ui.onMessage(msg => {
if (msg.type === 'create-rectangles') {
const count = msg.count
for (let i = 0; i < count; i++) {
const rect = acme.createRectangle()
rect.x = i * 150
rect.y = 100
rect.width = 100
rect.height = 100
rect.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
}
acme.ui.postMessage({ type: 'complete' })
}
})
acme.showUI(__html__, { width: 300, height: 200 })
ui.html
<!DOCTYPE html>
<html>
<body>
<h2>Rectangle Generator</h2>
<label>Count: <input id="count" type="number" value="5"></label>
<button id="create">Create</button>
<script>
document.getElementById('create').onclick = () => {
const count = document.getElementById('count').value
parent.postMessage({
pluginMessage: { type: 'create-rectangles', count: parseInt(count) }
}, '*')
}
window.onmessage = (event) => {
const msg = event.data.pluginMessage
if (msg.type === 'complete') {
console.log('Rectangles created!')
}
}
</script>
</body>
</html>
Core API
Selection
Get and manipulate selected layers:
// Get current selection
const selection = acme.currentPage.selection
// Select specific nodes
acme.currentPage.selection = [node1, node2]
// Clear selection
acme.currentPage.selection = []
// Listen for selection changes
acme.on('selectionchange', () => {
console.log('Selection changed:', acme.currentPage.selection)
})
Creating Nodes
Create design elements programmatically:
// Rectangle
const rect = acme.createRectangle()
rect.x = 100
rect.y = 100
rect.width = 200
rect.height = 150
rect.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.5, b: 1 } }]
// Circle
const circle = acme.createEllipse()
circle.x = 50
circle.y = 50
circle.width = 100
circle.height = 100
// Text
const text = acme.createText()
text.characters = "Hello World"
text.fontSize = 24
text.fontName = { family: "Inter", style: "Bold" }
// Frame
const frame = acme.createFrame()
frame.name = "Container"
frame.resize(400, 300)
// Add children to frame
frame.appendChild(rect)
frame.appendChild(text)
Modifying Properties
Change properties of existing nodes:
const node = acme.currentPage.selection[0]
// Position and size
node.x = 100
node.y = 200
node.resize(300, 200)
// Fills
node.fills = [
{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } },
{ type: 'GRADIENT_LINEAR', gradientStops: [...] }
]
// Strokes
node.strokes = [
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }
]
node.strokeWeight = 2
// Effects
node.effects = [
{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.25 },
offset: { x: 0, y: 4 },
radius: 8
}
]
// Opacity
node.opacity = 0.5
// Corner radius (for rectangles)
if (node.type === 'RECTANGLE') {
node.cornerRadius = 12
}
Traversing the Tree
Navigate the layer hierarchy:
// Get current page
const page = acme.currentPage
// Iterate through all children
for (const node of page.children) {
console.log(node.name, node.type)
}
// Recursive traversal
function traverse(node) {
console.log(node.name)
if ('children' in node) {
for (const child of node.children) {
traverse(child)
}
}
}
traverse(page)
// Find nodes by name
function findByName(name) {
return page.findOne(node => node.name === name)
}
// Find all text nodes
function findAllText() {
return page.findAll(node => node.type === 'TEXT')
}
Text API
Work with text layers:
const text = acme.createText()
// Set content
text.characters = "Hello World"
// Load font before setting
await acme.loadFontAsync({ family: "Inter", style: "Bold" })
text.fontName = { family: "Inter", style: "Bold" }
// Text properties
text.fontSize = 24
text.letterSpacing = { value: 0, unit: 'PIXELS' }
text.lineHeight = { value: 32, unit: 'PIXELS' }
text.textAlignHorizontal = 'LEFT' // 'LEFT', 'CENTER', 'RIGHT', 'JUSTIFIED'
text.textAlignVertical = 'TOP' // 'TOP', 'CENTER', 'BOTTOM'
// Text case
text.textCase = 'UPPER' // 'UPPER', 'LOWER', 'TITLE', 'ORIGINAL'
// Get text length
console.log(text.characters.length)
// Styled text (mixed formatting)
text.setRangeFontSize(0, 5, 32) // Make first 5 chars size 32
text.setRangeFillStyleId(0, 5, styleId) // Apply color style
Component API
Create and manage components:
// Create component from selection
const selection = acme.currentPage.selection[0]
const component = acme.createComponent()
component.appendChild(selection.clone())
// Create instance
const instance = component.createInstance()
instance.x = 200
instance.y = 200
// Check if node is component or instance
if (node.type === 'COMPONENT') {
console.log('This is a main component')
}
if (node.type === 'INSTANCE') {
console.log('This is an instance')
console.log('Main component:', node.mainComponent)
}
// Detach instance (convert to regular frame)
const instance = acme.currentPage.selection[0]
if (instance.type === 'INSTANCE') {
instance.detachInstance()
}
Styles API
Work with color, text, and effect styles:
// Get all styles
const colorStyles = acme.getLocalPaintStyles()
const textStyles = acme.getLocalTextStyles()
// Create color style
const style = acme.createPaintStyle()
style.name = "Primary Blue"
style.paints = [{ type: 'SOLID', color: { r: 0, g: 0.5, b: 1 } }]
// Apply style to node
node.fillStyleId = style.id
// Create text style
const textStyle = acme.createTextStyle()
textStyle.name = "Heading 1"
textStyle.fontSize = 32
textStyle.fontName = { family: "Inter", style: "Bold" }
// Apply to text node
await acme.loadFontAsync({ family: "Inter", style: "Bold" })
textNode.textStyleId = textStyle.id
Export API
Export nodes as images:
// Export as PNG
const node = acme.currentPage.selection[0]
const bytes = await node.exportAsync({
format: 'PNG',
constraint: { type: 'SCALE', value: 2 } // 2x scale
})
// Export as SVG
const svgBytes = await node.exportAsync({
format: 'SVG'
})
// Export as JPEG
const jpegBytes = await node.exportAsync({
format: 'JPG',
quality: 0.8 // 0-1
})
// Save to file system (with user permission)
acme.ui.postMessage({
type: 'export',
bytes: bytes,
filename: 'export.png'
})
Plugin UI API
Communication between plugin code and UI:
// code.js - Show UI
acme.showUI(__html__, {
width: 400,
height: 300,
title: "My Plugin"
})
// code.js - Receive messages from UI
acme.ui.onMessage(msg => {
if (msg.type === 'create-shape') {
const rect = acme.createRectangle()
// ... configure rect
acme.ui.postMessage({ type: 'done' })
}
if (msg.type === 'close') {
acme.closePlugin()
}
})
// ui.html - Send message to plugin
parent.postMessage({
pluginMessage: { type: 'create-shape', color: 'red' }
}, '*')
// ui.html - Receive messages from plugin
window.onmessage = (event) => {
const msg = event.data.pluginMessage
if (msg.type === 'done') {
console.log('Shape created!')
}
}
Network API
Make HTTP requests from plugins:
// GET request
const response = await acme.fetch('https://api.example.com/data')
const data = await response.json()
// POST request
const response = await acme.fetch('https://api.example.com/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'New Item' })
})
Storage API
Persist plugin data:
// Save data
await acme.clientStorage.setAsync('myKey', { foo: 'bar', count: 42 })
// Load data
const data = await acme.clientStorage.getAsync('myKey')
console.log(data) // { foo: 'bar', count: 42 }
// Delete data
await acme.clientStorage.deleteAsync('myKey')
// Get all keys
const keys = await acme.clientStorage.keysAsync()
Notifications
Show messages to the user:
// Success notification
acme.notify('Operation completed!', { timeout: 3000 })
// Error notification
acme.notify('Something went wrong', { error: true })
// Persistent notification (must be dismissed manually)
acme.notify('Processing...', { timeout: Infinity })
Best Practices
- Always check types before modifying nodes
- Load fonts before setting fontName on text
- Use async/await for asynchronous operations
- Handle errors gracefully
- Clean up when plugin closes
- Minimize expensive operations in loops
- Cache frequently accessed nodes and styles
Example Plugins
Batch Rename
const selection = acme.currentPage.selection
acme.ui.onMessage(msg => {
if (msg.type === 'rename') {
const { find, replace } = msg
let count = 0
for (const node of selection) {
if (node.name.includes(find)) {
node.name = node.name.replace(find, replace)
count++
}
}
acme.notify(`Renamed ${count} layers`)
acme.ui.postMessage({ type: 'complete', count })
}
})
Color Palette Generator
// Generate color palette from selected image
const node = acme.currentPage.selection[0]
if (node.type === 'RECTANGLE' && node.fills[0].type === 'IMAGE') {
const bytes = await node.exportAsync({ format: 'PNG' })
// Extract colors (simplified)
const colors = extractColors(bytes) // Your color extraction logic
// Create color styles
for (const color of colors) {
const style = acme.createPaintStyle()
style.name = `Palette ${color.name}`
style.paints = [{ type: 'SOLID', color }]
}
acme.notify(`Created ${colors.length} color styles`)
}