Custom Effects

Create your own effects by extending the Effect base class.

Effect Structure

class CustomEffect extends Effect {
  constructor(target, config) {
    super(target, config);

    // Find the appropriate layer
    let effectLayer = target.layer;
    if (!effectLayer) {
      let current = target;
      while (current && !effectLayer) {
        if (current.className === 'Layer') effectLayer = current;
        current = current.parent;
      }
    }
    if (!effectLayer) effectLayer = paper.project.activeLayer;

    // Create visual elements
    this.myVisual = new paper.Path.Circle({
      center: target.position,
      radius: config.size || 10,
      fillColor: config.color || 'white',
      parent: effectLayer
    });

    // Track for cleanup
    this.visuals.push(this.myVisual);
  }

  update(dt) {
    // Called every frame
    // dt = delta time in seconds

    if (!this.target || !this.target.parent) {
      this.isFinished = true;
      return;
    }

    // Update visual position, animation, etc.
    this.myVisual.position = this.target.position;
  }

  updateConfig(config) {
    super.updateConfig(config);
    // Apply new config to visuals
    if (config.color) {
      this.myVisual.fillColor = config.color;
    }
  }

  remove() {
    // Base class removes all items in this.visuals
    super.remove();
  }
}

Registering Your Effect

Add to the switch statement in EffectSystem.addEffect():

case 'custom':
  effect = new CustomEffect(item, config);
  break;

Effect Lifecycle

  1. Constructor: Create visual elements
  2. update(dt): Called every frame, animate visuals
  3. updateConfig(): Called when config changes
  4. remove(): Cleanup when effect is removed

Important Properties

Property Description
this.target The Paper.js item this effect is attached to
this.config Effect configuration object
this.visuals Array of visual elements (auto-cleaned)
this.isFinished Set true to remove effect

Example: Orbit Effect

class OrbitEffect extends Effect {
  constructor(target, config) {
    super(target, config);

    const layer = target.layer || paper.project.activeLayer;

    this.orbitDot = new paper.Path.Circle({
      center: target.position,
      radius: config.dotSize || 5,
      fillColor: config.color || '#60a5fa',
      parent: layer
    });

    this.visuals.push(this.orbitDot);
    this.angle = 0;
    this.radius = config.radius || 50;
    this.speed = config.speed || 2;
  }

  update(dt) {
    if (!this.target || !this.target.parent) {
      this.isFinished = true;
      return;
    }

    this.angle += this.speed * dt;

    const x = this.target.position.x + Math.cos(this.angle) * this.radius;
    const y = this.target.position.y + Math.sin(this.angle) * this.radius;

    this.orbitDot.position = new paper.Point(x, y);
  }
}

Tips

  • Always check this.target.parent in update() to handle deleted items
  • Use this.visuals.push() for automatic cleanup
  • Set this.isFinished = true to remove the effect
  • Access frame time via the dt parameter