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 }