Save Expand icon

Ron Valstar
front-end developer

Experiment: flow field

A flow field is a vector field used to move particles. In this case the field is provided by a 3D Simplex field (2D and time).
The number of particles is dynamic; particles are added and removed to reach an optimal framerate.

import experiment from './base'
import perlin from '../math/perlin'
// import pool from '../pattern/pool'
import color from '../math/color'

let inst = experiment('flowfield',{
    init: init
    ,handleAnimate: handleAnimate
    ,handleResize: handleResize
  })
  ,zuper = inst.zuper
	//
	,target
	,imageData, pixels, i
	,scale = 1
	,w
	,h
	,particlesNum = 0
	,particles = []
	,fieldScale = 0.01*scale
	,timeScale = 0.0009
	,particleSpeed = 0.25
	,flowfieldScale = 2
	,flowfieldInstances = []
	,canvas,context
	,canvasTemp = document.createElement('canvas')
	,contextTemp = canvasTemp.getContext("2d")
	,aFPos = [[-1,-1],[0,-1],[1,-1],[1,0],[1,1],[0,1],[-1,1],[-1,0]]
	,point = /*pool*/(point_)
;

function init(_target) {
  target = _target;
  canvas = zuper.init(_target);
  context = inst.context;
  //
  handleResize();
  //
  perlin.noiseDetail(1);
  //
  if (particlesNum===0) for (i = 0; i<3000; i++) addParticle();
  //
  return canvas;
}

// protected methods
function handleResize(){
	canvasTemp.width = canvas.width;
	canvasTemp.height = canvas.height;
	contextTemp.drawImage(canvas,0,0,canvas.width,canvas.height);
	//
	w = target.clientWidth/scale<<0;
	h = target.clientHeight/scale<<0;
	canvas.width = w;
	canvas.height = h;
	imageData = context.getImageData(0,0,w,h);
	pixels = imageData.data;
	context.fillStyle = color('#402').rgba(0.1);
}

function handleAnimate(deltaT,millis,frame){
	// adjust number of particles fps 24 ~ 1000/24=41
	if (deltaT<30) {
		let num = 10;
		while (num--) addParticle();
	} else if (deltaT>40) {
		removeParticle();
	}
	//
	// blur a bit
	let iPos = frame%8
		,iPos2 = (iPos+4)%8
		,aFFPos = aFPos[iPos]
		,aFFPos2 = aFPos[iPos2];
	context.drawImage(canvas,aFFPos[0],aFFPos[1]);
	context.drawImage(canvas,aFFPos2[0],aFFPos2[1]);
	//
	context.fillRect(0, 0, w, h);
	//
	imageData = context.getImageData(0,0,w,h);
	pixels = imageData.data;
	//
	let aCheck = []
		,oPoint, iAge, n, x, y, xy, fSpeed, iModMillis;
	flowfieldInstances.length = 0;
	i = particlesNum;
	while (--i) {
		oPoint = particles[i];
		oPoint.run(deltaT,millis,flowfieldInstances);
		iAge = oPoint.getAge();
		x = oPoint.getX()<<0;
		y = oPoint.getY()<<0;
		xy = y*w+x;
		if (aCheck[xy]) {
			oPoint.reset();
		} else {
			aCheck[xy] = true;
			n = 4*(y*w+x);
			fSpeed = oPoint.getSpeed()/particleSpeed*10;
			iModMillis = ((millis+oPoint.id)*0.1)%511<<0;
			pixels[n]   = fSpeed*255;
			pixels[n+1] = iModMillis<=255?iModMillis:511-iModMillis;
			pixels[n+2] = 255-fSpeed*255;
			pixels[n+3] = 255;
		}
	}
	context.putImageData(imageData, 0, 0);
}

function addParticle() {
	particlesNum = particles.push(point(w*Math.random(),h*Math.random()));
}

function removeParticle() {
	// particles.pop().drop();
	particlesNum = particles.length;
}

function point_(_x,_y) {
	if (point.id===undefined) point.id = 0;
	let x = _x
		,y = _y
		,vx = 0.81 * (Math.random() - 0.5)
		,vy = 0.81 * (Math.random() - 0.5)
		,fOff = 0.1
		,fP1, fP2, fP3
		,scaleX, scaleY
		,iSx, iSy
		,birth = Math.random()*1E9<<0
		,age = 0
		,id = point.id++
	;
	function run(deltaT,millis,flowfield){
		scaleX = fieldScale * x;
		scaleY = fieldScale * y;
		iSx = x>>flowfieldScale;
		iSy = y>>flowfieldScale;
		//
		let iFlowfieldIndex = iSx + iSy*(w>>flowfieldScale);
		if (flowfield.length>iFlowfieldIndex&&flowfield[iFlowfieldIndex]!==undefined) {
			let aFlowfieldVector = flowfield[iFlowfieldIndex];
			vx = aFlowfieldVector[0];
			vy = aFlowfieldVector[1];
		} else {
			fP1 = perlin.noise(timeScale*millis,scaleX,scaleY);
			fP2 = perlin.noise(timeScale*millis,scaleX + fOff,scaleY);
			fP3 = perlin.noise(timeScale*millis,scaleX,scaleY + fOff);
			vx = fP2 - fP1;
			vy = fP3 - fP1;
			flowfield[iFlowfieldIndex] = [vx,vy];
		}
		//
		x = x + deltaT*particleSpeed*vx;
		y = y + deltaT*particleSpeed*vy;
		//
		age = millis-birth;
		if (x<0 || x>w || y<0 || y>h) {
			reset(millis);
		}
	}

	function reset(millis){
		birth = millis;
		x = w * Math.random();
		y = w * Math.random();
		vx = 0;
		vy = 0;
	}

	function getSpeed(){
		return Math.sqrt(vx * vx + vy * vy);
	}

	return {
		toString: function(){return '[object point '+id+']';}
		,getX: function() {
			return x;
		}
		,getY: function() {
			return y;
		}
		,getAge: function() {
			return age;
		}
		,id: id
		,run:run
		,reset:reset
		,getSpeed:getSpeed
	};
}

export const flowfield = inst.expose