Custom Generators
PinePaper allows you to create custom generators for backgrounds and general-purpose graphics.
Two Types of Generators
| Type | Purpose | Output Layer |
|---|---|---|
| Background Generator | Full-canvas patterns (like sunburst, grid) | patternGroup (background) |
| Function Generator | Create any graphics anywhere | textItemGroup (interactive) or custom |
Custom Background Generator
Creates full-canvas background patterns like the built-in sunburst, sunset, and grid generators.
Basic Example
app.generatorRegistry.register('myBackground', {
displayName: 'My Background',
description: 'A custom background pattern',
category: 'background',
params: {
bgColor: { type: 'color', default: '#1e293b' },
patternColor: { type: 'color', default: '#3b82f6' },
density: { type: 'number', default: 20, min: 5, max: 50 }
},
fn: function(ctx) {
const { params, patternGroup, view, register } = ctx;
// 1. Create background fill
new paper.Path.Rectangle({
point: [0, 0],
size: view.size,
fillColor: params.bgColor,
parent: patternGroup
});
// 2. Create pattern elements
const spacing = view.size.width / params.density;
for (let x = 0; x < view.size.width; x += spacing) {
for (let y = 0; y < view.size.height; y += spacing) {
new paper.Path.Circle({
center: [x, y],
radius: 3,
fillColor: params.patternColor,
parent: patternGroup
});
}
}
// 3. Register for tracking
register(patternGroup, 'myBackground', params);
}
});
// Use it
app.executeGenerator('myBackground', {
bgColor: '#0f172a',
patternColor: '#6366f1',
density: 30
});
Animated Background
app.generatorRegistry.register('rotatingRays', {
displayName: 'Rotating Rays',
category: 'background',
params: {
rayCount: { type: 'number', default: 12, min: 4, max: 24 },
colors: { type: 'array', default: ['#ef4444', '#f97316', '#fbbf24'] },
speed: { type: 'number', default: 1, min: 0.1, max: 5 }
},
fn: function(ctx) {
const { params, patternGroup, view, register } = ctx;
const center = view.center;
// Create rays
const rayGroup = new paper.Group({ parent: patternGroup });
for (let i = 0; i < params.rayCount; i++) {
const angle = (360 / params.rayCount) * i;
const color = params.colors[i % params.colors.length];
const ray = new paper.Path({
segments: [
center,
center.add(new paper.Point({
angle: angle,
length: Math.max(view.size.width, view.size.height)
})),
center.add(new paper.Point({
angle: angle + (360 / params.rayCount / 2),
length: Math.max(view.size.width, view.size.height)
}))
],
fillColor: color,
closed: true,
parent: rayGroup
});
}
register(rayGroup, 'rotatingRays', params);
},
// Animation callback - called every frame
onAnimate: function(event, elements, params) {
elements.forEach(el => {
el.rotate(event.delta * params.speed * 10);
});
}
});
Custom Function Generator
Creates graphics that can be placed anywhere - not tied to background. Use this for reusable graphic components.
Basic Example
app.generatorRegistry.register('starField', {
displayName: 'Star Field',
description: 'Creates a field of stars',
category: 'function', // Not a background
params: {
count: { type: 'number', default: 50, min: 10, max: 200 },
minSize: { type: 'number', default: 2, min: 1, max: 10 },
maxSize: { type: 'number', default: 8, min: 2, max: 20 },
color: { type: 'color', default: '#fbbf24' },
area: { type: 'object', default: null } // Optional bounding area
},
fn: function(ctx) {
const { params, view, app } = ctx;
// Determine area (full canvas or specified)
const area = params.area || {
x: 0, y: 0,
width: view.size.width,
height: view.size.height
};
const stars = [];
for (let i = 0; i < params.count; i++) {
const x = area.x + Math.random() * area.width;
const y = area.y + Math.random() * area.height;
const size = params.minSize + Math.random() * (params.maxSize - params.minSize);
const star = new paper.Path.Star({
center: [x, y],
points: 5,
radius1: size / 2,
radius2: size,
fillColor: params.color,
parent: app.textItemGroup // Interactive layer
});
// Register each star as interactive item
app.itemRegistry.register(star, 'star', {
source: 'generator',
generator: 'starField'
});
stars.push(star);
}
return stars; // Return created items
}
});
// Use it
const stars = app.executeGenerator('starField', {
count: 100,
color: '#fef08a',
area: { x: 100, y: 100, width: 600, height: 400 }
});
Reusable Component Generator
app.generatorRegistry.register('socialBadge', {
displayName: 'Social Badge',
description: 'Creates a social media style badge',
category: 'function',
params: {
position: { type: 'point', default: [400, 300] },
text: { type: 'string', default: 'FOLLOW' },
bgColor: { type: 'color', default: '#ec4899' },
textColor: { type: 'color', default: '#ffffff' },
size: { type: 'number', default: 1, min: 0.5, max: 3 }
},
fn: function(ctx) {
const { params, app } = ctx;
const pos = new paper.Point(params.position);
// Create badge group
const badge = new paper.Group();
// Background pill
const bg = new paper.Path.Rectangle({
point: [0, 0],
size: [120, 40],
radius: 20,
fillColor: params.bgColor
});
bg.position = pos;
badge.addChild(bg);
// Text
const text = new paper.PointText({
point: pos,
content: params.text,
fontSize: 16,
fontWeight: 'bold',
fillColor: params.textColor,
justification: 'center'
});
text.position = pos;
badge.addChild(text);
// Scale
badge.scale(params.size);
// Add to interactive layer
app.textItemGroup.addChild(badge);
// Register
app.itemRegistry.register(badge, 'badge', {
source: 'generator',
generator: 'socialBadge'
});
return badge;
}
});
// Use it
const badge = app.executeGenerator('socialBadge', {
position: [200, 150],
text: 'NEW',
bgColor: '#22c55e',
size: 1.5
});
Data-Driven Generator
app.generatorRegistry.register('barChart', {
displayName: 'Bar Chart',
category: 'function',
params: {
position: { type: 'point', default: [400, 300] },
data: { type: 'array', default: [30, 50, 80, 60, 40] },
colors: { type: 'array', default: ['#3b82f6', '#8b5cf6', '#ec4899', '#f97316', '#22c55e'] },
barWidth: { type: 'number', default: 40 },
maxHeight: { type: 'number', default: 200 },
gap: { type: 'number', default: 10 }
},
fn: function(ctx) {
const { params, app } = ctx;
const chart = new paper.Group();
const maxValue = Math.max(...params.data);
const startX = params.position[0] - (params.data.length * (params.barWidth + params.gap)) / 2;
const baseY = params.position[1];
params.data.forEach((value, i) => {
const height = (value / maxValue) * params.maxHeight;
const x = startX + i * (params.barWidth + params.gap);
const color = params.colors[i % params.colors.length];
const bar = new paper.Path.Rectangle({
point: [x, baseY - height],
size: [params.barWidth, height],
fillColor: color,
radius: 4
});
chart.addChild(bar);
});
app.textItemGroup.addChild(chart);
app.itemRegistry.register(chart, 'chart', { source: 'generator' });
return chart;
}
});
// Use it
app.executeGenerator('barChart', {
position: [400, 400],
data: [25, 60, 45, 80, 35, 70],
barWidth: 50,
maxHeight: 250
});
Parameter Types Reference
| Type | Properties | Example |
|---|---|---|
number |
default, min, max, step |
{ type: 'number', default: 10, min: 1, max: 100 } |
color |
default |
{ type: 'color', default: '#3b82f6' } |
boolean |
default |
{ type: 'boolean', default: true } |
string |
default |
{ type: 'string', default: 'Hello' } |
select |
options, default |
{ type: 'select', options: ['a', 'b', 'c'], default: 'a' } |
array |
default |
{ type: 'array', default: [1, 2, 3] } |
point |
default |
{ type: 'point', default: [400, 300] } |
object |
default |
{ type: 'object', default: { x: 0, y: 0 } } |
Context Object
The generator function receives:
| Property | Description |
|---|---|
params |
Resolved parameters with defaults applied |
patternGroup |
Background layer group (for background generators) |
view |
Paper.js view with size, center, bounds |
app |
PinePaper instance (access all APIs) |
register |
Function to register decorative elements |
async |
Async helpers for heavy computations (async generators only) |
Async Generators (Heavy Computation)
For generators that need heavy math, use async with Web Workers:
app.generatorRegistry.register('poissonField', {
displayName: 'Poisson Field',
async: true, // Enable async mode
params: {
minDistance: { type: 'number', default: 30 },
color: { type: 'color', default: '#8b5cf6' }
},
fn: async function(ctx) {
const { params, async: asyncHelpers, patternGroup, view } = ctx;
// Heavy computation runs in Web Worker (non-blocking)
const points = await asyncHelpers.poissonDiskSampling(
view.size.width,
view.size.height,
params.minDistance
);
// Rendering happens on main thread
points.forEach(pt => {
new paper.Path.Circle({
center: [pt.x, pt.y],
radius: params.minDistance / 4,
fillColor: params.color,
parent: patternGroup
});
});
}
});
// Execute async generator
await app.generatorRegistry.executeAsync('poissonField', {
minDistance: 40
});
Available Async Helpers
| Helper | Description |
|---|---|
poissonDiskSampling(w, h, dist) |
Even random distribution |
goldenRatioDistribution(count, w, h) |
Spiral distribution |
simplifyPath(points, tolerance) |
Reduce path complexity |