jpweeks/particulate-js

View on GitHub
src/systems/ParticleSystem.js

Summary

Maintainability
A
45 mins
Test Coverage
import { removeAll } from '../utils/Collection'
import { inherit } from '../utils/Creator'
import { Vec3 } from '../math/Vec3'

// ..................................................
// ParticleSystem
// ..................................................

export { ParticleSystem }

/**
  @module systems
*/

/**
  Manages particle state as well as the forces and constraints that act on its particles.

  @class ParticleSystem
  @constructor
  @param {Int|Array} particles   Number of particles or array of initial particle positions
  @param {Int}       iterations  Number of constraint iterations per system tick
*/
function ParticleSystem(particles, iterations) {
  var isCount = typeof particles === 'number'
  var length = isCount ? particles * 3 : particles.length
  var count = length / 3
  var positions = isCount ? length : particles

  /**
    Current particle positions

    @property positions
    @type Float32Array (Vec3)
  */
  this.positions = new Float32Array(positions)

  /**
    Previous particle positions

    @property positionsPrev
    @type Float32Array (Vec3)
  */
  this.positionsPrev = new Float32Array(positions)

  /**
    Accumulated forces currently acting on particles

    @property accumulatedForces
    @type Float32Array (Vec3)
  */
  this.accumulatedForces = new Float32Array(length)

  /**
    Particle mass

    @property weights
    @type Float32Array (Float)
  */
  this.weights = new Float32Array(count)
  this.setWeights(1)

  /**
    Number of constraint relaxation loop iterations

    @property _iterations
    @type Int
    @private
  */
  this._iterations = iterations || 1

  /**
    Number of particles in system

    @property _count
    @type Int
    @private
  */
  this._count = count

  this._globalConstraints = []
  this._localConstraints = []
  this._pinConstraints = []
  this._forces = []
}

/**
  Create instance, accepts constructor arguments.

  @method create
  @static
*/
inherit(ParticleSystem, Object)

/**
  Alias for `Vec3.set`. Sets vector of `positions` and `positionsPrev`.

  @method setPosition
  @param {Int}   i  Particle index
  @param {Float} x
  @param {Float} y
  @param {Float} z
*/
ParticleSystem.prototype.setPosition = function (i, x, y, z) {
  Vec3.set(this.positions, i, x, y, z)
  Vec3.set(this.positionsPrev, i, x, y, z)
}

/**
  Alias for `Vec3.copy`. Copys vector from `positions`.

  @method getPosition
  @param  {Int}  i    Particle index
  @param  {Vec3} out
  @return {Vec3} out
*/
ParticleSystem.prototype.getPosition = function (i, out) {
  return Vec3.copy(this.positions, i, out)
}

/**
  Alias for `Vec3.getDistance`. Calculates distance from `positions`.

  @method getDistance
  @param  {Int}   a  Particle index
  @param  {Int}   b  Particle index
  @return {Float}    Distance
*/
ParticleSystem.prototype.getDistance = function (a, b) {
  return Vec3.distance(this.positions, a, b)
}

/**
  Alias for `Vec3.angle`. Calculates angle from `positions`.

  @method getAngle
  @param  {Int}   a  Particle index
  @param  {Int}   b  Particle index
  @param  {Int}   c  Particle index
  @return {Float}    Angle in radians
*/
ParticleSystem.prototype.getAngle = function (a, b, c) {
  return Vec3.angle(this.positions, a, b, c)
}

/**
  Set a particle's mass

  @method setWeight
  @param {Int}   i  Particle index
  @param {Float} w  Weight
*/
ParticleSystem.prototype.setWeight = function (i, w) {
  this.weights[i] = w
}

ParticleSystem.prototype.setWeights = function (w) {
  var weights = this.weights
  for (var i = 0, il = weights.length; i < il; i ++) {
    weights[i] = w
  }
}

ParticleSystem.prototype.each = function (iterator, context) {
  context = context || this
  for (var i = 0, il = this._count; i < il; i ++) {
    iterator.call(context, i, this)
  }
}

ParticleSystem.prototype.perturb = function (scale) {
  var positions = this.positions
  var positionsPrev = this.positionsPrev
  var dist

  for (var i = 0, il = positions.length; i < il; i ++) {
    dist = Math.random() * scale
    positions[i] += dist
    positionsPrev[i] += dist
  }
}

// ..................................................
// Verlet Integration
//

function ps_integrateParticle(i, p0, p1, f0, weight, d2) {
  var pt = p0[i]
  p0[i] += pt - p1[i] + f0[i] * weight * d2
  p1[i] = pt
}

/**
  Calculate particle's next position through Verlet integration.
  Called as part of `tick`.

  @method integrate
  @param {Float} delta  Time step
  @private
*/
ParticleSystem.prototype.integrate = function (delta) {
  var d2 = delta * delta
  var p0 = this.positions
  var p1 = this.positionsPrev
  var f0 = this.accumulatedForces
  var w0 = this.weights
  var ix, weight

  for (var i = 0, il = this._count; i < il; i ++) {
    weight = w0[i]
    ix = i * 3

    ps_integrateParticle(ix,     p0, p1, f0, weight, d2)
    ps_integrateParticle(ix + 1, p0, p1, f0, weight, d2)
    ps_integrateParticle(ix + 2, p0, p1, f0, weight, d2)
  }
}

// ..................................................
// Constraints
//

ParticleSystem.prototype._getConstraintBuffer = function (constraint) {
  return constraint._isGlobal ? this._globalConstraints : this._localConstraints
}

/**
  Add a constraint

  @method addConstraint
  @param {Constraint} constraint
*/
ParticleSystem.prototype.addConstraint = function (constraint) {
  this._getConstraintBuffer(constraint).push(constraint)
}

/**
  Alias for `Collection.removeAll`. Remove all references to a constraint.

  @method removeConstraint
  @param {Constraint} constraint
*/
ParticleSystem.prototype.removeConstraint = function (constraint) {
  removeAll(this._getConstraintBuffer(constraint), constraint)
}

/**
  Add a pin constraint.
  Although intended for instances of `PointConstraint`, this can be any
  type of constraint and will be resolved last in the relaxation loop.

  @method addPinConstraint
  @param {Constraint} constraint
*/
ParticleSystem.prototype.addPinConstraint = function (constraint) {
  this._pinConstraints.push(constraint)
}

/**
  Alias for `Collection.removeAll`. Remove all references to a pin constraint.

  @method removePinConstraint
  @param {Constraint} constraint
*/
ParticleSystem.prototype.removePinConstraint = function (constraint) {
  removeAll(this._pinConstraints, constraint)
}

/**
  Run relaxation loop, resolving constraints per defined iterations.
  Constraints are resolved in order by type: global, local, pin.

  @method satisfyConstraints
  @private
*/
ParticleSystem.prototype.satisfyConstraints = function () {
  var iterations = this._iterations
  var global = this._globalConstraints
  var local = this._localConstraints
  var pins = this._pinConstraints
  var globalCount = this._count
  var globalItemSize = 3

  for (var i = 0; i < iterations; i ++) {
    this.satisfyConstraintGroup(global, globalCount, globalItemSize)
    this.satisfyConstraintGroup(local)

    if (!pins.length) { continue; }
    this.satisfyConstraintGroup(pins)
  }
}

/**
  Resolve a group of constraints.

  @method satisfyConstraintGroup
  @param {Array} group       List of constraints
  @param {Int}   [count]     Override for number of particles a constraint affects
  @param {Int}   [itemSize]  Override for particle index stride
  @private
*/
ParticleSystem.prototype.satisfyConstraintGroup = function (group, count, itemSize) {
  var p0 = this.positions
  var p1 = this.positionsPrev
  var hasUniqueCount = !count
  var constraint

  for (var i = 0, il = group.length; i < il; i ++) {
    constraint = group[i]

    if (hasUniqueCount) {
      count = constraint._count
      itemSize = constraint._itemSize
    }

    for (var j = 0; j < count; j ++) {
      constraint.applyConstraint(j * itemSize, p0, p1)
    }
  }
}

// ..................................................
// Forces
//

/**
  Add a force

  @method addForce
  @param {Force} force
*/
ParticleSystem.prototype.addForce = function (force) {
  this._forces.push(force)
}

/**
  Alias for `Collection.removeAll`. Remove all references to a force.

  @method removeForce
  @param {Force} force
*/
ParticleSystem.prototype.removeForce = function (force) {
  removeAll(this._forces, force)
}

/**
  Accumulate forces acting on particles.

  @method accumulateForces
  @param {Float} delta  Time step
  @private
*/
ParticleSystem.prototype.accumulateForces = function (delta) {
  var forces = this._forces
  var f0 = this.accumulatedForces
  var p0 = this.positions
  var p1 = this.positionsPrev
  var ix

  for (var i = 0, il = this._count; i < il; i ++) {
    ix = i * 3
    f0[ix] = f0[ix + 1] = f0[ix + 2] = 0

    for (var j = 0, jl = forces.length; j < jl; j ++) {
      forces[j].applyForce(ix, f0, p0, p1)
    }
  }
}

/**
  Step simulation forward one frame.
  Applies forces, calculates particle positions, and resolves constraints.

  @method tick
  @param {Float} delta  Time step
*/
ParticleSystem.prototype.tick = function (delta) {
  this.accumulateForces(delta)
  this.integrate(delta)
  this.satisfyConstraints()
}