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