Custom Relations
Create your own relation types to define custom behaviors between items.
Registering a Custom Relation
Use app.registerRelationRule() to define a new relation type:
app.registerRelationRule('repels', {
description: 'Item moves away from target',
// Parameter schema with defaults
params: {
force: { type: 'number', default: 50, description: 'Repulsion strength' },
maxDistance: { type: 'number', default: 200, description: 'Max effect range' }
},
// Compute function - runs in Web Worker (pure math only)
compute: (ctx) => {
const { fromPosition, toPosition, params, delta } = ctx;
const dx = fromPosition.x - toPosition.x;
const dy = fromPosition.y - toPosition.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > params.maxDistance || dist < 0.001) {
return { x: fromPosition.x, y: fromPosition.y };
}
const force = (params.maxDistance - dist) / params.maxDistance * params.force;
const nx = dx / dist;
const ny = dy / dist;
return {
x: fromPosition.x + nx * force * delta,
y: fromPosition.y + ny * force * delta
};
},
// Apply function - runs on main thread (can use Paper.js)
apply: (item, target, computed, params) => {
if (computed) {
item.position.x = computed.x;
item.position.y = computed.y;
}
},
// Optional: natural language templates for training data
templates: [
'{item} repels from {target}',
'make {item} move away from {target}'
]
});
Using Your Custom Relation
Once registered, use it like any built-in relation:
// Create items
const particle = app.create('circle', { x: 300, y: 300, radius: 10, color: '#ef4444' });
const obstacle = app.create('circle', { x: 400, y: 300, radius: 30, color: '#3b82f6' });
// Get their IDs
const particleId = particle.data.registryId;
const obstacleId = obstacle.data.registryId;
// Apply the custom relation
app.addRelation(particleId, obstacleId, 'repels', {
force: 100,
maxDistance: 150
});
Definition Properties
| Property | Required | Description |
|---|---|---|
description |
No | Human-readable description |
params |
No | Parameter schema with defaults |
compute |
Yes | Math function (runs in Worker) |
apply |
Yes | Update function (runs on main thread) |
templates |
No | Natural language templates |
continuous |
No | Whether to update every frame (default: true) |
priority |
No | Execution order (lower = first, default: 0) |
Parameter Schema
Define parameters with types, defaults, and constraints:
params: {
speed: {
type: 'number',
default: 1,
description: 'Movement speed'
},
direction: {
type: 'string',
default: 'clockwise',
options: ['clockwise', 'counterclockwise']
},
offset: {
type: 'array',
default: [0, 0],
description: 'Position offset [x, y]'
},
enabled: {
type: 'boolean',
default: true
}
}
Compute Context
The compute function receives a context object:
| Property | Type | Description |
|---|---|---|
fromPosition |
{x, y} |
Source item position |
toPosition |
{x, y} |
Target item position |
params |
object |
Merged parameters (defaults + overrides) |
delta |
number |
Frame delta time in seconds |
time |
number |
Total elapsed time in seconds |
Important: The compute function runs in a Web Worker for performance. It must be:
- Pure JavaScript (no DOM access, no Paper.js)
- Only use serializable inputs/outputs
- Stateless (no external variables)
Apply Function
The apply function updates the actual Paper.js items:
| Parameter | Type | Description |
|---|---|---|
item |
paper.Item |
Source item to update |
target |
paper.Item |
Target item (reference) |
computed |
object |
Result from compute function |
params |
object |
Relation parameters |
apply: (item, target, computed, params) => {
// Update position
if (computed.x !== undefined) item.position.x = computed.x;
if (computed.y !== undefined) item.position.y = computed.y;
// Update rotation
if (computed.rotation !== undefined) item.rotation = computed.rotation;
// Update scale
if (computed.scale !== undefined) item.scaling = computed.scale;
// Update opacity
if (computed.opacity !== undefined) item.opacity = computed.opacity;
}
Example: Magnetic Attraction
app.registerRelationRule('attracts', {
description: 'Item is attracted toward target',
params: {
strength: { type: 'number', default: 50 },
minDistance: { type: 'number', default: 20 },
maxDistance: { type: 'number', default: 300 }
},
compute: (ctx) => {
const { fromPosition, toPosition, params, delta } = ctx;
const dx = toPosition.x - fromPosition.x;
const dy = toPosition.y - fromPosition.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Too close or too far - no effect
if (dist < params.minDistance || dist > params.maxDistance) {
return fromPosition;
}
// Attraction force (inverse square law)
const force = params.strength / (dist * dist) * 1000;
const nx = dx / dist;
const ny = dy / dist;
return {
x: fromPosition.x + nx * force * delta,
y: fromPosition.y + ny * force * delta
};
},
apply: (item, target, computed, params) => {
if (computed) {
item.position.x = computed.x;
item.position.y = computed.y;
}
}
});
// Usage
app.addRelation(ironId, magnetId, 'attracts', { strength: 100 });
Example: Wobble Effect
app.registerRelationRule('wobbles_with', {
description: 'Item wobbles in sync with target',
params: {
amplitude: { type: 'number', default: 10 },
frequency: { type: 'number', default: 2 },
phase: { type: 'number', default: 0 }
},
compute: (ctx) => {
const { fromPosition, params, time } = ctx;
const angle = (time * params.frequency + params.phase) * Math.PI * 2;
return {
x: fromPosition.x,
y: fromPosition.y,
offsetX: Math.sin(angle) * params.amplitude,
offsetY: Math.cos(angle) * params.amplitude * 0.5
};
},
apply: (item, target, computed, params) => {
if (computed) {
// Apply wobble as visual offset
item.position.x += computed.offsetX;
item.position.y += computed.offsetY;
}
}
});
Checking if a Relation Exists
// Check if your custom relation was registered
const rule = app.relationRegistry.getRule('repels');
if (rule) {
console.log('Repels relation is available');
}
// Get all registered relations
const allRules = app.relationRegistry.getAllRules();
console.log('Available relations:', allRules.map(r => r.name));
Training Data Templates
Templates help generate instruction/code pairs for LLM fine-tuning:
templates: [
'{item} repels from {target}',
'push {item} away from {target}',
'make {item} avoid {target} with force {force}'
]
Export training data:
const trainingData = app.exportRelationTrainingData();
// Returns array of { instruction, code, relation, params }