Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                
              
            
!

CSS

              
                body, html {
  margin: 0;
  overflow: hidden;
}
              
            
!

JS

              
                
let loader = PIXI.loader;
let sprites = [];
let field;
let config = {
  debug: false,
  particals: 100,
  animating: false,
  friction: 50,
  minDistance: 150,
  forceMultiplier: 250,
  degenerate: true
};
let container = document.createElement('div');
document.body.appendChild(container);

let init = function() {
  initiated = true;
  field = new Particlefield(sprites, container);
  field.behaviour = ParticalFieldBehaviour.SPAWNING;
  // field.addGravitywell();

  window.addEventListener('resize', function() {
    config.particals = field.particalsOnStageAtOnce;
  });
  
  // Setting up the dat gui configuration object
  if( config.debug === true ) field.debug = true;
  else config.debug = field.debug;
  if( config.particals == 0 ) config.particals = field.particalsOnStageAtOnce;
  else field.particalsOnStageAtOnce = config.particals;
  if( config.friction == 0 ) config.friction = (1 - field.airfriction) * 1000;
  else field.airfriction = config.friction / 100;
  if( config.minDistance == 0 ) config.minDistance = field.minDistance;
  else field.minDistance = config.minDistance;
  if( config.forceMultiplier == 0 ) config.forceMultiplier = field.accelerationFactor;
  else field.accelerationFactor = config.forceMultiplier;
  field.degenerate = config.degenerate;
}
let i = 1;
while(i <= 18) {
  loader.add('shape'+i, `https://s3-us-west-2.amazonaws.com/s.cdpn.io/982762/Pattern-${('0' + i).substr(-2)}_2x.png`);
  i++;
}
loader.load((loader, resources) => {
  // Create sprites from our resources. These sprites are going to be used as prototypes onlyps -Al
  //
  for(i in resources) {
    sprites.push(resources[i].texture);
  }

  init();
});

















const PPP = 0.00008;
const airfriction = 0.98;
const minDistance = 100;
const accelerationFactor = 5000;

const ParticalFieldBehaviour = {
  LOOPING: 0,
  SPAWNING: 1
}

class Particlefield {
  constructor(sprites, container) {
    this.dimensions = new Vector(window.innerWidth, window.innerHeight);

    // this.renderer = PIXI.autoDetectRenderer(window.innerWidth, window.innerHeight, {
    //   antialias: true,
    //   transparent: true,
    //   autoResize: true
    // });
    this.app = new PIXI.Application(window.innerWidth, window.innerHeight, {
      // antialias: true,
      transparent: true,
      autoResize: true
    });
    this.renderer = this.app.renderer;
    // this.renderer.plugins.interaction.autoPreventDefault = false;
    this.stage = this.app.stage;
    this.forcesContainer = new PIXI.Container();
    this.stage.addChild(this.forcesContainer);

    this.sprites = sprites;

    this.container = container;

    this.renderer.view.id = 'particalfield'
    this.container.appendChild(this.app.view);

    this.particals = [];

    window.addEventListener('resize', this.resize.bind(this));

    this.running = true;

    MouseBehaviour.init(this);

    this.initialise();
  }

  initialise() {
    let particalnum = this.particalsOnStageAtOnce;

    while(particalnum > 0) {

      this.newParticle();

      particalnum--;
    }
  }

  mouseStateSpinner() {
    MouseBehaviour.state = MouseBehaviourStates.STATE_SPINNER;
  }
  mouseStateExploder() {
    MouseBehaviour.state = MouseBehaviourStates.STATE_EXPLODER;
  }

  removeForces() {
    if( this.forcesContainer ) this.stage.removeChild( this.forcesContainer );
    this.forcesContainer = new PIXI.Container();
    this.stage.addChild(this.forcesContainer);
  }
  addForce(force) {
    if(force instanceof BasicForce) {
      force.debug = this.debug;
      this.forcesContainer.addChild(force);
    }
  }
  addAttractor() {
    let force = new Attractor();
    this.addForce(force);
  }
  addRepeller() {
    let force = new Repeller();
    this.addForce(force);
  }
  addGravitywell() {
    let force = new Gravitywell();
    this.addForce(force);
  }
  addSpincycle() {
    let force = new Spincycle();
    this.addForce(force);
  }

  addVelocityField() {
    let vfield = new VelocityField(this);
    this.addForce(vfield);
  }

  newParticle() {
    let p = this.createPartical();
    this.addParticle(p);
    return p;
  }

  createPartical() {
    let partical = new Particle(this);
    partical.sprite = this.sprites[Math.round(Math.random() * this.sprites.length)];
    partical.position = new Vector(Math.random() * this.dimensions.width, Math.random() * this.dimensions.height);
    // partical.position = new Vector(this.dimensions.width / 2, this.dimensions.height / 2);
    let diff = 2;
    partical.velocity = new Vector(-diff + Math.random() * (diff * 2), -diff + Math.random() * (diff * 2));
    // partical.velocity = new Vector(2,2);

    return partical;
  }

  updateParticalField() {
    let particalDifference = this.particalsOnStageAtOnce - this.particlesOnStageNow;

    if(particalDifference > 0) {
      while(particalDifference > 0) {
        this.newParticle();
        particalDifference--;
      }
    } else {
      if( this.degenerate ) {
        while(particalDifference < 0) {
          let randomIndex = Math.floor(Math.random() * this.particlesOnStageNow);
          let partical = this.particals[randomIndex];
          this.removeParticalByIndex(randomIndex, false);
          particalDifference++;
        }
      }
    }

  }

  render() {
    this.renderparticals();

    this.renderer.render(this.stage);

    if(this.running) {
      requestAnimationFrame(this.render.bind(this));
    }
  }

  renderparticals() {
    this.particals.forEach((p) => {
      p.render();

      this.particals.forEach((q) => {
        p.applyOther(q);
      });
    });
  }

  resize() {
    this.dimensions = new Vector(window.innerWidth, window.innerHeight);
    this.renderer.resize(this.dimensions.width,this.dimensions.height);
    this.updateParticalField();
  }

  addParticle(partical) {
    if(partical instanceof Particle) {
      this.particals.push(partical);
      setTimeout(() => {
        this.stage.addChild(partical.sprite);
      }, 1);
    }
  }
  removePartical(partical) {
    try {
      this.particals.forEach((_partical, i) => {
        if(_partical === partical) {
          this.removeParticalByIndex(i);
          throw {}; // equivalent of break
        }
      });
    } catch (e) {
      // null
    }
  }
  removeParticalByIndex(index, spawn = true) {

    let partical = this.particals[index];
    partical.die();
    // this.stage.removeChild(partical.sprite);
    this.particals.splice(index, 1);

    if( spawn ) {
      let difference = this.particals.length - this.particalsOnStageAtOnce;
      if( difference < 0 ) {
        this.newParticle();
      }
    }
  }
  removeAllParticals() {
    this.particals.forEach((partical, i) => {
      this.stage.removeChild(partical.sprite);
    });
    this.particals = [];
  }

  set running(running) {
    this._running = running === true;

    if(this._running) {
      this.render();
    }
  }
  get running() {
    return this._running || false;
  }

  set minDistance(value) {
    if( value > 0 ) this._minDistance = value;
  }
  get minDistance() {
    return this._minDistance || minDistance;
  }

  set accelerationFactor(value) {
    if( value > 0 ) this._accelerationFactor = value;
  }
  get accelerationFactor() {
    return this._accelerationFactor || accelerationFactor;
  }

  get particlesOnStageNow() {
    return this.particals.length;
  }

  set particalsOnStageAtOnce(POSAO) {
    this.particalDensity = POSAO / this.dimensions.area;
  }
  get particalsOnStageAtOnce() {
    return Math.round(this.dimensions.area * this.particalDensity);
  }

  set particalDensity(density) {
    if(density > 0) {
      this._oldDensity = this.particalDensity;
      this._density = density;
      this.updateParticalField();
    }
  }
  get particalDensity() {
    return this._density || PPP;
  }

  set airfriction(friction) {
    if(friction > 0) this._friction = friction;
  }
  get airfriction() {
    return this._friction || airfriction;
  }

  set behaviour(value) {
    this._behaviour = value;
  }
  get behaviour() {
    return this._behaviour || 0;
  }

  set degenerate(value) {
    this._degenerate = (value === true);
  }
  get degenerate() {
    return this._degenerate !== false;
  }

  get forces() {
    let _forces = [];
    this.forcesContainer.children.forEach((force)=> {
      _forces.push(force);
    });
    return _forces;
  }

  set debug(value) {
    this._debug = value === true;

    this.forcesContainer.children.forEach((child)=> {
      child.debug = this.debug;
    });
  }
  get debug() {
    return this._debug === true;
  }
}




const ParticalStates = {
  STATE_NEW: 0,
  STATE_ALIVE: 1,
  STATE_DYING: 2,
  STATE_DEAD: 4
}

const offscreenBuffer = 200;

// Decorate vector with a new inverse function
const inverseStrengthNew = function(v, st) {
  let x = Math.abs(v.x);
  let y = Math.abs(v.y);
  let strX = st - x;
  let strY = st - y;
  if(v.x < 0) {
    strX = 0 - strX;
  }

  if(v.y < 0) {
    strY = 0 - strY;
  }

  return new Vector(strX, strY);
}

class Particle {
  constructor(field) {
    this.field = field

    this.velocity = new Vector(0, 0);
    this.spin = 0;
    this.state = ParticalStates.STATE_NEW;
    this.mass = 10 / 1000;

    setTimeout(() => {
      this.init();
    }, 0);
  }

  init() {
    if(this.state !== ParticalStates.STATE_NEW) return; // return if, for some reason, this partical has been killed before this method is run.

    let alpha = this.sprite.alpha;
    let width = this.sprite.width;
    let height = this.sprite.height;

    this.sprite.alpha = 0;
    this.sprite.width = 1;
    this.sprite.height = 1;

    this.tweenUp = TweenMax.to(this.sprite, 5.5, {
      alpha: alpha,
      width: width,
      height: height,
      onComplete: () => {
        this.state = ParticalStates.STATE_ALIVE;
      }
    });
  }

  render() {
    let forces = this.forces;

    if(this.field.behaviour === ParticalFieldBehaviour.SPAWNING) {
      if( this.state < ParticalStates.STATE_DYING ) {
        if(this.position.x < -offscreenBuffer
        || this.position.x > this.field.dimensions.x + offscreenBuffer
        || this.position.y < -offscreenBuffer
        || this.position.y > this.field.dimensions.y + offscreenBuffer) {
          this.field.removePartical(this);
        }
      } else {
        return;
      }
    } else {

      if( !this.field.degenerate && this.field.particlesOnStageNow > this.field.particalsOnStageAtOnce )
      {
        if( this.state < ParticalStates.STATE_DYING ) {
          if(this.position.x < -offscreenBuffer
          || this.position.x > this.field.dimensions.x + offscreenBuffer
          || this.position.y < -offscreenBuffer
          || this.position.y > this.field.dimensions.y + offscreenBuffer) {
            this.field.removePartical(this);
          }
        } else {
          return;
        }
      }

      if(this.position.x < -offscreenBuffer) {
        this.position.x = this.field.dimensions.x + offscreenBuffer/2;
      } else if(this.position.x > this.field.dimensions.x + offscreenBuffer) {
        this.position.x = -offscreenBuffer/2;
      }
      if(this.position.y < -offscreenBuffer) {
        this.position.y = this.field.dimensions.y + offscreenBuffer/2;
      } else if(this.position.y > this.field.dimensions.y + offscreenBuffer) {
        this.position.y = -offscreenBuffer/2;
      }
    }

    this.position = this.position.add(forces);
    this.spin += forces.angle / 10000;
    this.spin *= this.frictionalConclusion;
    this.rotation += this.spin;
  }

  applyOther(partical) {
    let distance = partical.position.subtractNew(this.position);
    if(distance.length <= this.minDistance) {
      // Inverting the strength of the vector so that the pull is stronger if the vectors are closer together
      let acceleration = inverseStrengthNew(distance, this.minDistance).divideScalar(distance.length).divideScalar(this.accelerationFactor);
      // let acceleration = inverseStrengthNew(distance, this.minDistance);
      // acceleration = acceleration.divide(acceleration);
      this.velocity.subtract(acceleration);
      partical.velocity.add(acceleration);
    }
  }

  die() {
    if( this.tweenUp ) this.tweenUp.kill(); // cleaning up the old tween, if it exists

    this.state = ParticalStates.STATE_DYING;

    TweenMax.to(this.sprite, 0.5, {
      alpha: 0,
      width: 1,
      height: 1 ,
      onComplete: () => {
        this.state = ParticalStates.STATE_DEAD;
        this.sprite.parent.removeChild(this.sprite);
      }
    });
  }

  set sprite(sprite) {
    if(sprite instanceof PIXI.Texture) {
      this._sprite = new PIXI.Container();
      let __sprite = new PIXI.Sprite(sprite);
      this._sprite.addChild(__sprite);
      __sprite.position.x = -25;
      __sprite.position.y = -25;
      __sprite.height = 50;
      __sprite.width = 50;
      __sprite.alpha = 0.2;
    }
  }
  get sprite() {
    if(!this._sprite || !(this._sprite instanceof PIXI.Sprite || this._sprite instanceof PIXI.Container)) {
      let p = new PIXI.Graphics();
      p.beginFill(0x000000);
      p.lineStyle(0);
      p.drawCircle(5, 5, 5);
      p.endFill();
      let t = PIXI.RenderTexture.create(p.width, p.height);
      this.field.renderer.render(p, t);

      this._sprite = new PIXI.Sprite(t);
      this._sprite.alpha = 0.4;
    }

    return this._sprite;
  }

  set minDistance(value) {
    if( value > 0 ) this._minDistance = value;
  }
  get minDistance() {
    return this._minDistance || this.field.minDistance;
  }

  set accelerationFactor(value) {
    if( value > 0 ) this._accelerationFactor = value;
  }
  get accelerationFactor() {
    return this._accelerationFactor || this.field.accelerationFactor;
  }

  get forces() {
    let forces = new Vector(0, 0);

    let _forces = this.field.forces;
    _forces.forEach((force)=> {
      this.velocity.add(force.calculateForce(this));
    });

    this.velocity = this.velocity.multiplyScalar(this.frictionalConclusion);

    forces.add(this.velocity);

    return forces;
  }

  get frictionalConclusion() {
    let friction = 1 - this.field.airfriction * this.mass;
    if( friction > 1 ) friction = 1;
    if( friction < 0 ) friction = 0;
    return friction;
  }

  set position(pos) {
    if(pos instanceof Vector) {
      this._position = pos;
      this.sprite.position = this._position;
    }
  }
  get position() {
    return this._position || new Vector(0,0);
  }

  set velocity(vel) {
    if(vel instanceof Vector) {
      if( vel.length > 8 ) vel.normalise().length = 8; // maximum velocity of 2
      this._velocity = vel;
    }
  }
  get velocity() {
    return this._velocity || new Vector(0,0);
  }

  set rotation(rotation) {
    this.sprite.rotation = rotation;
  }
  get rotation() {
    return this.sprite.rotation;
  }
}




const MouseBehaviourStates = {
  STATE_IDLE: 0,
  STATE_EXPLODER: 1,
  STATE_SPINNER: 2,
  STATE_SUCKER: 4
}

const onMouseMove = function(e) {

  switch (this.state) {
    case MouseBehaviourStates.STATE_EXPLODER:
      for(let i in this.forces) {
        this.forces[i].position.x = e.clientX;
        this.forces[i].position.y = e.clientY;
      }
      break;
    case MouseBehaviourStates.STATE_SPINNER:
      for(let i in this.forces) {
        this.forces[i].position.x = e.clientX;
        this.forces[i].position.y = e.clientY;
      }
      break;
    case MouseBehaviourStates.STATE_SUCKER:
      break;
  }
}
const onMouseDown = function(e) {

  switch (this.state) {
    case MouseBehaviourStates.STATE_EXPLODER:
      break;
    case MouseBehaviourStates.STATE_SPINNER:
      for(let i in this.forces) {
        this.forces[i].isOn = true;
      }
      break;
    case MouseBehaviourStates.STATE_SUCKER:
      break;
  }

}
const onMouseUp = function(e) {

  switch (this.state) {
    case MouseBehaviourStates.STATE_EXPLODER:
      for(let i in this.forces) {
        this.forces[i].isOn = true;
      }
      this.tweens.forEach((tween)=> {
        tween.play(0);
      });
      break;
    case MouseBehaviourStates.STATE_SPINNER:
      for(let i in this.forces) {
        this.forces[i].isOn = false;
      }
      break;
    case MouseBehaviourStates.STATE_SUCKER:
      break;
  }
}

class MouseBehaviour {
  static init(field) {
    this.state = MouseBehaviourStates.STATE_IDLE;
    if( field instanceof Particlefield ) {
      this.field = field;
      this.initialised = true;
      this._onMouseMove = onMouseMove.bind(this);
      this._onMouseDown = onMouseDown.bind(this);
      this._onMouseUp = onMouseUp.bind(this);
    }
  }

  static setupState() {
    if( !this.initialised ) return;

    this._removeListeners();
    if( this.state === MouseBehaviourStates.STATE_IDLE ) {
    } else {
      this._addListeners();

      this.forces = {};
      this.tweens = [];

      switch (this.state) {
        case MouseBehaviourStates.STATE_EXPLODER:

          this.forces.repeller = new Repeller();
          this.forces.repeller.effectRadius = 1000;
          this.forces.repeller.strength = 0.001;
          this.forces.repeller.isOn = false;

          this.tweens.push(new TweenMax.from(this.forces.repeller, 0.3, { effectRadius: 10, strength: 0.05, onComplete: ()=> { this.forces.repeller.isOn = false; } }));

          break;
        case MouseBehaviourStates.STATE_SPINNER:

          this.forces.attractor = new Attractor();
          this.forces.attractor.effectRadius = 700;
          this.forces.attractor.strength = 0.002;
          this.forces.attractor.isOn = false;

          this.forces.repeller = new Repeller();
          this.forces.repeller.effectRadius = 150;
          this.forces.repeller.strength = 0.03;
          this.forces.repeller.isOn = false;

          this.tweens.push(new TweenMax(this.forces.repeller, 1, { effectRadius: 50, repeat: -1, yoyo: true }));

          this.forces.spinner = new Spincycle();
          this.forces.spinner.effectRadius = 600;
          this.forces.spinner.strength = 0.0002;
          this.forces.spinner.isOn = false;

          break;
        case MouseBehaviourStates.STATE_SUCKER:
          break;
      }

      for(let i in this.forces) {
        this.field.addForce(this.forces[i]);
      }
    }
  }

  static _removeListeners() {
    // window.removeEventListener('pointermove', this._onMouseMove);
    // window.removeEventListener('pointerdown', this._onMouseDown);
    // window.removeEventListener('pointerup', this._onMouseUp);
  }
  static _addListeners() {
    if( !this.initialised ) return;

    // window.addEventListener('pointermove', this._onMouseMove);
    // window.addEventListener('pointerdown', this._onMouseDown);
    // window.addEventListener('pointerup', this._onMouseUp);
  }

  static set state(value) {
    if( !this.initialised ) return;

    this.field.removeForces(); // Remove all existing forces first

    switch (value) {
      case MouseBehaviourStates.STATE_IDLE:
        break;
      case MouseBehaviourStates.STATE_EXPLODER:
        break;
      case MouseBehaviourStates.STATE_SPINNER:
        break;
      case MouseBehaviourStates.STATE_SUCKER:
        break;
      default:
        console.warn('MouseBehaviour.state provided ('+value+') is invalid.');
        return;
    }
    this._state = value;
    this.setupState();
  }
  static get state() {
    return this._state || 0;
  }

  static set tweens(value) {
    if(value instanceof Array) {
      if(this._tweens) {
        this._tweens.forEach((tween)=> {
          tween.kill();
        });
      }
      this._tweens = [];
    }
  }
  static get tweens() {
    return this._tweens;
  }
}




class BasicForce extends PIXI.Container {

  constructor() {
    super();
  }

  /**
   * emitEvent - Takes a custom event name and data and emits it to the window.
   *
   * @private
   * @param  {String} eventname The name of the event to emit
   * @param  {Object} data      The data to emit to event.detail
   * @return {undefined}
   */
  emitEvent(eventname, data) {
    if(typeof eventname != 'string')  {
      return;
    }

    var event = null;

    if (window.CustomEvent) {
      event = new CustomEvent(eventname, {detail: data});
    } else {
      event = document.createEvent('CustomEvent');
      event.initCustomEvent(eventname, true, true, data);
    }

    window.dispatchEvent(event);
  }

  calculateForce(particle) {
    if(this.isOn === false) {
      return new Vector(0,0);
    }

    return new Vector(0,0);
  }

  set debug(value) {
    this._debug = value === true;
  }
  get debug() {
    return this._debug === true;
  }

  set isOn(value) {
    this._emitting = value === true;
  }
  get isOn() {
    return this._emitting !== false;
  }
}




class Force extends BasicForce {

  constructor() {
    super();

    this.drawAreaDisplay();
    this.drawControls();

    this._onDragStart = this.onDragStart.bind(this);
    this._onDragEnd = this.onDragEnd.bind(this);
    this._onDragMove = this.onDragMove.bind(this);

    window.addEventListener('forceSelected', (e)=> {
      if( e.detail.force !== this ) {
        this.selected = false;
      }
    })

    this.pos = new Vector(window.innerWidth / 2, window.innerHeight / 2);

    this.dragable = true;
    this.interactive = true;
    this.buttonMode = true;
    setTimeout(()=> {
      this.selected = true;
    }, 0);
  }

  drawAreaDisplay() {
    if( this.areaDisplay ) {
      this.removeChild(this.areaDisplay);
      this.areaDisplay = null;
    }

    if( this.debug ) {
      this.areaDisplay = new PIXI.Graphics();
      this.areaDisplay.beginFill(this.forceColour);
      this.areaDisplay.alpha = 0.1;
      this.areaDisplay.drawCircle(0,0,this.effectRadius);

      this.addChild(this.areaDisplay);
    }
  }

  drawControls() {

  }

  onDragStart(e) {
    let originalEvent = e.data.originalEvent;
    this.dragging = false;
    this.mouseDown = true;
    this.dragStart = new Vector(originalEvent.clientX, originalEvent.clientY);
    this.draggingOffset = new Vector(originalEvent.clientX - this.pos.x, originalEvent.clientY - this.pos.y);
    this.dragData = e.data;

    this.selected = true;
  }
  onDragEnd(e) {
    this.dragging = false;
    this.mouseDown = false;
  }
  onDragMove(e) {
    if( this.mouseDown ) {
      let originalEvent = e.data.originalEvent;
      let dragNow = new Vector(originalEvent.clientX, originalEvent.clientY);
      let distanceV = dragNow.subtractNew(this.dragStart);
      let distance = distanceV.length;

      if(distance > 3) {
        this.dragging = true;
      }

      if(this.dragging) {
        this.pos = new Vector(dragNow.x - this.draggingOffset.x, dragNow.y - this.draggingOffset.y);
      }
    }
  }

  set effectRadius(value) {
    if(typeof value == 'number') {
      this._effectRadius = value;
      this.drawAreaDisplay();
    }
  }
  get effectRadius() {
    return this._effectRadius || 300;
  }

  set dragable(value) {
    this._isDragable = value === true;

    if(this._isDragable === true) {
      this.on('pointerdown', this._onDragStart)
          .on('pointerup', this._onDragEnd)
          .on('pointerupoutside', this._onDragEnd)
          .on('pointermove', this._onDragMove);
    } else {
      this.off('pointerdown', this.onDragStart)
          .off('pointerup', this.onDragEnd)
          .off('pointerupoutside', this.onDragEnd)
          .off('pointermove', this.onDragMove);
    }
  }
  get dragable() {
    return this._isDragable !== false;
  }

  set pos(value) {
    if(value instanceof Vector) {
      this.position.x = value.x;
      this.position.y = value.y;
    }
  }
  get pos() {
    return new Vector(this.position.x, this.position.y);
  }

  get forceColour() {
    return 0x333333;
  }

  set selected(value) {
    let oldSelected = this._selected;
    this._selected = value === true;

    if(oldSelected !== this._selected) {
      if(this._selected === true) {
        if( this.debug ) this.areaDisplay.alpha = 0.3;
        this.emitEvent('forceSelected', { force: this });
      } else {
        if( this.debug ) this.areaDisplay.alpha = 0.04;
        this.emitEvent('forceDeselected', { force: this });
      }
    }
  }
  get selected() {
    return this._selected !== false;
  }

  set strength(value) {
    if(typeof value === 'number') this._strength = value;
  }
  get strength() {
    return this._strength || 0.00001;
  }

  set debug(value) {
    this._debug = value === true;
    this.drawAreaDisplay();
  }
  get debug() {
    return this._debug === true;
  }

  set isOn(value) {
    this._emitting = value === true;
    this.drawAreaDisplay();
  }
  get isOn() {
    return this._emitting !== false;
  }
}




class Gravitywell extends Force {
  constructor() {
    super();

    this.strength = 1.8;
  }

  calculateForce(particle) {
    if(this.isOn === false) {
      return new Vector(0,0);
    }

    let distanceV = this.pos.subtractNew(particle.position);
    let distance = distanceV.length;
    if( distance < this.effectRadius ) {
      // let strength = new Vector(0, 0);
      // // This inverts the strength, making the effect stronger near the centre
      // strength.x = -(this.effectRadius / -distanceV.x);
      // strength.y = -(this.effectRadius / -distanceV.y);

      let strength = distanceV.divideScalar(this.effectRadius).multiplyScalar(this.strength);
      return strength;
    }

    return new Vector(0,0);
  }

  get forceColour() {
    if(this.isOn === false) {
      return 0x666666;
    }
    return 0x0088AA;
  }
}
              
            
!
999px

Console