Three.js – FlashLight Panel

Just one more trial on Three.js. As I’m getting used to the API, just regained some more interests in the library. Initially, I tried to use more complex geometries and textures, but the final code resulted in a mixture of simple lines and particles. Mostly because I found that simple objects can provide beautiful pictures along with color gradations and opacities (the another reason is that I couldn’t make them work properly…).

The followings are some more additional info.

Screenshots

flashing_panels

Working Example

http://jsdo.it/parroty/s2Jq

Notes

ParticleSystem
The “ParticleSystem” class of Three.js handles drawing particle objects, and it supports so many objects on screen. Though it gets slower as I increase the active particles, more than 10,000 particles just worked fine on my laptop.

dat.GUI
I’ve used dat.GUI for providing the GUI to modify parameters. It’s not related to Three.js, but dat.GUI provides a very nice user interface to configure parameters dynamically. Also, it’s pretty much easy to use.

Source Code

flash.js

var stats, camera, scene, renderer;
var mousePos, windowHalfPos;

// Initialization Parameters
var INITIAL_CAMERA_POS = new Point(0, 100, 500);
var INITIAL_MOUSE_POS  = new Point(500, 500, 0);
var HIDDEN_POINT_POS   = new Point(100000, 100000, 100000);
var FRAME_RATE_WAIT    = 1000 / 30;
var TEXTURE_ARC_SIZE   = 50;
var PANEL_OPACITY_ON   = 0.8;
var PANEL_OPACITY_OFF  = 0.4;
var CAMERA_SPEED       = 0.05;
var PARTICLES_COUNT    = 15000; // limit of total maximum particles

//parameters
var DEFAULT_PANEL_NUM         = 200;
var DEFAULT_PANEL_SIZE        = 20;
var DEFAULT_PANEL_SPEED       = 100;
var DEFAULT_PANEL_FLASH       = 15;
var DEFAULT_LINE_NUM          = 12;
var DEFAULT_LINE_WIDTH        = 3;
var DEFAULT_LINE_SPEED        = 15;
var DEFAULT_PARTICLE_SIZE     = 5;
var DEFAULT_PARTICLE_AMOUNT   = 7; // number of particles for each splash
var DEFAULT_PARTICLE_SPEED    = 10;
var DEFAULT_PARTICLE_DURATION = 150;
var DEFAULT_COLOR_MODE        = "normal" // or "fast"

var Settings = function() {};
Settings.lineNum            = DEFAULT_LINE_NUM;
Settings.lineWidth          = DEFAULT_LINE_WIDTH;
Settings.lineSpeed          = DEFAULT_LINE_SPEED;
Settings.panelNum           = DEFAULT_PANEL_NUM;
Settings.panelSize          = DEFAULT_PANEL_SIZE;
Settings.panelSpeed         = DEFAULT_PANEL_SPEED;
Settings.panelFlashInterval = DEFAULT_PANEL_FLASH;
Settings.particleSize       = DEFAULT_PARTICLE_SIZE;
Settings.particleAmount     = DEFAULT_PARTICLE_AMOUNT;
Settings.particleSpeed      = DEFAULT_PARTICLE_SPEED;
Settings.particleDuration   = DEFAULT_PARTICLE_DURATION;
Settings.colorMode          = DEFAULT_COLOR_MODE;

var ParticleInfo = function() {
  this.direction = 0;
  this.speed     = 0;
  this.life      = 0;
};

var objects;
var lineManager;

init();
animate();

function initializeDrawObjects() {
  // container to store dispaly objects information
  objects = {
    planes: [],
    panels: [],
    lines: [],
    particles: [],
    particleSystem: null
  };

  //initialize scene
  scene = new THREE.Scene();
  scene.add(camera);

  //initialize light
  scene.add(new THREE.AmbientLight(0x333333));

  // generate panels
  for(var i = 0; i < Settings.panelNum; i++) {
    var point    = Generator.generatePoint(500, 500, 500);
    var color    = Generator.generateColor();
    var rotation = Generator.generateRotation(360);
    var speed    = Generator.generateSpeed(Settings.panelSpeed).applyScale(0.0005).applyOffset(0.0005);

    var panel = new Panel(point, rotation, speed, color, Settings.panelFlashInterval);
    var plane = new THREE.Mesh(
      new THREE.PlaneGeometry(Settings.panelSize, Settings.panelSize),
      new THREE.MeshBasicMaterial({
        color: color.to_rgb(),
        wireframe: false,
        opacity: 50,
        transparent: true,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
        depthTest: false
      })
    );

    plane.position.set(point.x, point.y, point.z);
    plane.rotation.set(rotation.x, rotation.y, rotation.z);
    plane.overdraw = true;
    plane.material.side = THREE.DoubleSide;

    objects.panels.push(panel);
    objects.planes.push(plane);

    scene.add(plane);
  }

  // generate line
  lineManager = new LineManager(objects.panels, Settings.lineSpeed, Settings.colorMode);

  for(var i = 0; i < Settings.lineNum; i++) {
    lineManager.pickPanel();

    var color = Generator.generateColor();
    var opacity = PANEL_OPACITY_ON;

    var m = new THREE.LineBasicMaterial({color: color.to_rgb(), opacity: opacity, linewidth: Settings.lineWidth});
    var g = new THREE.Geometry();

    g.vertices.push(new THREE.Vector3(0, 0, 0));  // for start point
    g.vertices.push(new THREE.Vector3(0, 0, 0));  // for end point

    var line = new THREE.Line(g, m);
    scene.add(line);

    objects.lines.push(line);
  }

  //generate particle
  generateParticles();
}

function init() {
  //initialize dom
  var container = document.createElement('div');
  document.body.appendChild(container);

  //initialize positions
  mousePos = INITIAL_MOUSE_POS;
  updateWindowPosition();

  //initialize camera
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
  camera.position.x = INITIAL_CAMERA_POS.x;
  camera.position.y = INITIAL_CAMERA_POS.y;
  camera.position.z = INITIAL_CAMERA_POS.z;

  //generate texture
  canvas = document.createElement('canvas');
  canvas.width  = TEXTURE_ARC_SIZE;
  canvas.height = TEXTURE_ARC_SIZE;
  context = canvas.getContext('2d');
  context.fillStyle = 'black';
  context.fillRect(0, 0, TEXTURE_ARC_SIZE, TEXTURE_ARC_SIZE);
  context.fillStyle = 'white';
  context.beginPath();
  context.arc(TEXTURE_ARC_SIZE / 2, TEXTURE_ARC_SIZE / 2, TEXTURE_ARC_SIZE / 2, 0, 2 * Math.PI, false);
  context.stroke();
  context.closePath();
  context.fill();
  texture = new THREE.Texture(canvas);
  texture.needsUpdate = true;

  // initialize objects
  initializeDrawObjects();

  // initialize GUI
  var controls = [];
  gui = new dat.GUI();
  gui.width = 300;
  var fLines     = gui.addFolder('Lines');
  var fParticles = gui.addFolder('Particles');
  var fPanels    = gui.addFolder('Panels');
  var fColors    = gui.addFolder('Color');
  controls.push(fLines.add(Settings, 'lineWidth', 1, 10).step(1).name('Size'));
  controls.push(fLines.add(Settings, 'lineSpeed', 1, 50).step(1).name('Speed'));
  controls.push(fLines.add(Settings, 'lineNum', 1, 50).step(1).name('Count'));
  controls.push(fParticles.add(Settings, 'particleSize', 1, 10).step(1).name('Size'));
  controls.push(fParticles.add(Settings, 'particleSpeed', 1, 50).step(1).name('Speed'));
  controls.push(fParticles.add(Settings, 'particleDuration', 10, 300).step(10).name('Duration'));
  controls.push(fParticles.add(Settings, 'particleAmount', 1, 20).step(1).name('Count'));
  controls.push(fPanels.add(Settings, 'panelSize', 5, 100).step(1).name('Size'));
  controls.push(fPanels.add(Settings, 'panelSpeed', 1, 500).step(10).name('Speed'));
  controls.push(fPanels.add(Settings, 'panelNum', 10, 500).step(1).name('Count'));
  controls.push(fPanels.add(Settings, 'panelFlashInterval', 5, 50).step(1).name('Flash Interval'));
  controls.push(fColors.add(Settings, 'colorMode', ['fast', 'normal']).name('Transition'));
  for(var i = 0; i < controls.length; i++) {
    controls[i].onChange(function(value) {
      initializeDrawObjects();
    });
  }
  [fLines, fParticles, fPanels, fColors].forEach(function(c) { c.open(); });
  gui.close();

  //renderer
  renderer = new THREE.WebGLRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement);

  stats = new Stats();
  stats.domElement.style.position = 'absolute';
  stats.domElement.style.top = '0px';
  container.appendChild(stats.domElement);

  document.addEventListener('mousemove', onDocumentMouseMove, false);
  window.addEventListener('resize', onWindowResize, false);
}

function updateWindowPosition() {
  windowHalf = new Point(window.innerWidth / 2, window.innerHeight / 2, 0);
}

function onWindowResize() {
  updateWindowPosition();

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

function onDocumentMouseMove(event) {
  mousePos.x = event.clientX - windowHalf.x;
  mousePos.y = event.clientY - windowHalf.y;
}

function convertToVertex(point) {
  return new THREE.Vector3(point.x, point.y, point.z);
}

function updateVertex(v1, v2) {
  v1.x = v2.x;
  v1.y = v2.y;
  v1.z = v2.z;
}

function updateVertexByPoint(vertex, point) {
  updateVertex(vertex, convertToVertex(point));
}

function generateParticles() {
  var geo = new THREE.Geometry();

  var particles = objects.particles;
  for(var i = 0; i < PARTICLES_COUNT; i++) {
    geo.vertices.push(new THREE.Vector3(HIDDEN_POINT_POS.x, HIDDEN_POINT_POS.y, HIDDEN_POINT_POS.z));

    var particle = new ParticleInfo();
    particle.direction = new THREE.Vector3(Math.random()*2 - 1, Math.random()*2 - 1, Math.random()*2 - 1);
    particle.speed = Math.random() * Settings.particleSpeed * 0.01;
    particle.life = 0;
    particles.push(particle);

    geo.colors.push(new THREE.Color());
  }

  var material = new THREE.ParticleBasicMaterial({
        map: texture,
        size: Settings.particleSize,
        blending: THREE.AdditiveBlending,
        depthTest: false,
        vertexColors: true,
        transparent: true });

  var ps = new THREE.ParticleSystem(geo, material);
  ps.sortParticles = true;
  objects.particleSystem = ps;
  scene.add(ps);
}

function activateParticles(item) {
  var point     = item.getCurrentOffset();
  var color     = item.getCurrentColor().applyScale(1 / 256);
  var direction = item.getDirection();
  var speedUnit = item.getSpeed() / Settings.particleAmount;

  var geometry = objects.particleSystem.geometry;
  var count    = geometry.vertices.length;

  var activeCount = 0;
  var index = 0;
  while(activeCount < Settings.particleAmount && index < PARTICLES_COUNT) {
    var position = geometry.vertices[index];

    if(position.x == HIDDEN_POINT_POS.x) {
      var offset = direction.multiply(activeCount * speedUnit);
      updateVertexByPoint(position, point.plus(offset));
      geometry.colors[index].setRGB(color.r, color.g, color.b);
      activeCount += 1;
    }

    index += 1;
  }

  geometry.colorsNeedUpdate = true;
}

function updateParticles() {
  var count = 0;
  var geometry    = objects.particleSystem.geometry;
  var particles   = objects.particles;

  for(var i = 0; i < PARTICLES_COUNT; i++) {
    var position = geometry.vertices[i];

    if(position.x != HIDDEN_POINT_POS.x) {
      var direction = particles[i].direction;
      var speed     = particles[i].speed;

      position.x += direction.x * speed;
      position.y += direction.y * speed;
      position.z += direction.z * speed;

      count += 1;
    }
  }
  geometry.__dirtyVertices = true;

  activeParticleCount = count;  //TODO : show in the screen
}


function animate() {
  setTimeout(function() {
    requestAnimationFrame(animate);
  }, FRAME_RATE_WAIT);
  render();
  stats.update();
}

function render() {
  //move camera
  camera.position.x += ( mousePos.x - camera.position.x) * CAMERA_SPEED;
  camera.position.y += (-mousePos.y - camera.position.y) * CAMERA_SPEED;
  camera.lookAt(scene.position);

  //move lines
  lineManager.move();
  var list = lineManager.getLines();
  for(var i = 0; i < list.length; i++) {
    var geometry = objects.lines[i].geometry;
    var material = objects.lines[i].material;

    updateVertexByPoint(geometry.vertices[0], list[i].getStartPoint());
    updateVertexByPoint(geometry.vertices[1], list[i].getEndPoint());

    // normalize color to 0.0-1.0 range and apply.
    var c = list[i].getCurrentColor().applyScale(1 / 256);
    material.color.setRGB(c.r, c.g, c.b);

    geometry.verticesNeedUpdate = true;
  }

  // activate and move particles
  var movingLines = lineManager.getLinesForPartcileSplash();
  for(var i = 0; i < movingLines.length; i++) {
    activateParticles(movingLines[i]);
  }
  var particles = objects.particles;
  var geometry  = objects.particleSystem.geometry;
  for(var i = 0; i < PARTICLES_COUNT; i++) {
    var position = geometry.vertices[i];
    var particle = particles[i];

    if(position.x != HIDDEN_POINT_POS.x) {
      particle.life += 1;
    }

    if(particle.life > Settings.particleDuration) {
      updateVertexByPoint(position, HIDDEN_POINT_POS);
      particle.life = 0;
    }
  }
  updateParticles();

  // rotate panels
  for(var i = 0; i < objects.planes.length; i++) {
    var plane = objects.planes[i];
    var panel = objects.panels[i];
    var rotation = panel.rotation;

    panel.move();
    plane.rotation.set(rotation.x, rotation.y, rotation.z);

    var opacity = panel.getFlashStatus() ? PANEL_OPACITY_OFF : PANEL_OPACITY_ON;
    plane.material.opacity = opacity;
  }

  renderer.render(scene, camera);
}

lib.js

//------- Random Class -------
var Random = function() {};
Random.generate = function(range) {
  return Math.floor(Math.random() * range);
}

//------- Color Class -------
var Color = function(r, g, b) {
  this.r = r;
  this.g = g;
  this.b = b;
}

Color.mix = function(c1, c2, rate) {
  var r = c1.r*rate + c2.r*(1 - rate);
  var g = c1.g*rate + c2.g*(1 - rate);
  var b = c1.b*rate + c2.b*(1 - rate);
  return new Color(r, g, b);
}

Color.prototype = {
  to_s: function() {
    return this.to_rgb();
  },

  to_rgb: function() {
    var r = Math.floor(this.r);
    var g = Math.floor(this.g);
    var b = Math.floor(this.b);
    return "rgb(" + r + "," + g + "," + b + ")";
  },

  applyScale: function(amount) {
    this.r *= amount;
    this.g *= amount;
    this.b *= amount;
    return this;
  },
}

//------- Generator Class -------
var Generator = function() {};
Generator.generateColor = function() {
  var c = new Color(
    Random.generate(256),
    Random.generate(256),
    Random.generate(256));
  return c;
}

// generate x, y, z so that its sum becomes fixed number
Generator.generateSpeed = function(max) {
  var r1 = Random.generate(max);
  var r2 = Random.generate(max);

  var numbers = [0, r1, r2, max];
  numbers.sort(function(a, b) { return a - b; });
  var results = [];
  for(var i = 0; i < (numbers.length - 1); i++) {
    results.push(numbers[i + 1] - numbers[i]);
  }

  return new Point(results[0], results[1], results[2]);
}

// generate random points located between (+/-)size
Generator.generatePoint = function(xsize, ysize, zsize) {
  return new Point(
    Random.generate(xsize * 2) - xsize,
    Random.generate(ysize * 2) - ysize,
    Random.generate(zsize * 2) - zsize);
}

Generator.generateRotation = function(max) {
  return new Point(
    Random.generate(max),
    Random.generate(max),
    Random.generate(max));
}

//------- Point Class -------
var Point = function(x, y, z) {
  this.x = x;
  this.y = y;
  this.z = z;
}

Point.prototype = {
  to_s: function() {
    return "Point : x = " + this.x + " y = " + this.y + " z = " + this.z;
  },

  clone: function() {
    return new Point(this.x, this.y, this.z);
  },

  applyScale: function(amount) {
    this.x *= amount;
    this.y *= amount;
    this.z *= amount;
    return this;
  },

  applyOffset: function(amount) {
    this.x += amount;
    this.y += amount;
    this.z += amount;
    return this;
  },

  move: function(dx, dy, dz) {
    this.x += dx;
    this.y += dy;
    this.z += dz;
  },

  plus: function(p) {
    return new Point(this.x + p.x, this.y + p.y, this.z + p.z);
  },

  minus: function(p) {
    return new Point(this.x - p.x, this.y - p.y, this.z - p.z);
  },

  multiply: function(n) {
    return new Point(this.x*n, this.y*n, this.z*n);
  },

  getLength: function() {
    return Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z);
  },

  getDistanceFrom: function(p) {
    return this.minus(p).getLength();
  }
}

//------- Panel Class -------
var Panel = function(point, rotation, speed, color, flashInterval) {
  this.position      = point;
  this.rotation      = rotation;
  this.speed         = speed;
  this.color         = color;
  this.flashInterval = flashInterval;
  this.flashCount    = 0;
  this.flashEnabled  = false;
  this.flashStatus   = false;
}

Panel.prototype = {
  move: function() {
    this.rotation.x += this.speed.x;
    this.rotation.y += this.speed.y;
    this.rotation.z += this.speed.z;

    this.flashCount += 1;
    if(this.flashCount % this.flashInterval == 0) {
      this.flashStatus = !this.flashStatus;
    }
  },

  setFlashStatus: function(flag) {
    this.flashEnabled = flag;
    this.flashCount = 0;
  },

  getFlashStatus: function() {
    if(this.flashEnabled) {
      return this.flashStatus;
    }
    else {
      return false;
    }
  }
}

//------- Line Class -------
var Line = function(startPoint, endPoint, colorMode) {
  this.reset(startPoint, endPoint);
  this.speed = 10;
  this.startColor = new Color(256, 256, 256);
  this.endColor   = new Color(256, 256, 256);
  this.colorMode  = colorMode;
}

Line.SCALE_SMALL_OFFSET = 0.0001;

Line.prototype = {
  reset: function(startPoint, endPoint) {
    this.startPoint  = startPoint;
    this.endPoint    = endPoint;
    this.barHeight   = endPoint.getDistanceFrom(startPoint);
    this.count       = 0;
    this.isMoving    = true;
  },

  to_s: function() {
    var ret = "\n";
    ret += "this.startPoint = " + this.startPoint.to_s() + "\n";
    ret += "this.endPoint = " + this.endPoint.to_s() + "\n";
    ret += "this.getCurrentOffset = " + this.getCurrentOffset().to_s() + "\n";
    ret += "this.count = " + this.count + "\n";
    ret += "this.speed = " + this.speed + "\n"
    return ret;
  },

  getSpeed: function() {
    return this.speed;
  },

  setSpeed: function(speed) {
    this.speed = speed;
    return this;
  },

  getDirection: function() {
    var p = this.endPoint.minus(this.startPoint);
    return p.applyScale(1 / p.getLength());
  },

  setColor: function(startColor, endColor) {
    this.startColor = startColor;
    this.endColor   = endColor;
    return this;
  },

  isSplashingParticle: function() {
    if(this.colorMode == 'normal') {
      return true;
    }
    else {
      var offset = this.count % (2 * this.barHeight);
      if(offset <= this.barHeight) {
        return true;
      }
      else {
        return false;
      }
    }
  },

  updatePosition: function(startPoint, endPoint) {
    this.reset(startPoint, endPoint);
  },

  move: function() {
    this.count += this.speed;

    if(this.count >= (2 * this.barHeight)) {
      this.isMoving = false;
    }
  },

  getStartPoint: function() {
    if(this.count >= this.barHeight) {
      return this.getPosition();
    }
    else {
      return this.startPoint;
    }
  },

  getEndPoint: function() {
    if(this.count >= this.barHeight) {
      return this.endPoint;
    }
    else {
      return this.getPosition();
    }
  },

  getPosition: function() {
    return this.getCurrentOffset();
  },

  getCurrentColor: function() {
    if(this.colorMode == 'fast') {
      var rate = 1 - (this.count / this.barHeight);
      return Color.mix(this.startColor, this.endColor, rate);
    }
    else {
      var rate = 1 - (this.count / (this.barHeight * 2));
      return Color.mix(this.startColor, this.endColor, rate);
    }
  },

  getCurrentOffset: function() {
    if(this.count >= (2 * this.barHeight)) {
      return this.endPoint;
    }

    var currentDistance = this.count;
    if(this.count >= this.barHeight) {
      currentDistance = this.count - this.barHeight;
    }

    var difference = this.endPoint.minus(this.startPoint);
    difference.applyScale(currentDistance / difference.getLength());
    return difference.plus(this.startPoint);
  }
}

//------- PanelSet Class -------
var PanelSet = function(startPanel, endPanel) {
  this.startPanel = startPanel;
  this.endPanel   = endPanel;
}

//------- LineManager Class -------
var LineManager = function(panels, speed, colorMode) {
  this.panels       = panels;
  this.speed        = speed;
  this.activeLines  = [];
  this.activePanels = [];
  this.colorMode    = colorMode;
}

LineManager.prototype = {
  pickPanel: function() {
    var startPanel = this.pickRandomPanel();
    var endPanel   = this.pickRandomPanel();
    this.activePanels.push(new PanelSet(startPanel, endPanel));

    var line = new Line(startPanel.position, endPanel.position, this.colorMode);
    line.setSpeed(this.speed);
    line.setColor(startPanel.color, endPanel.color);
    this.activeLines.push(line);
  },

  pickRandomPanel: function() {
    var panel = this.panels[Random.generate(this.panels.length)];
    panel.setFlashStatus(true);
    return panel;
  },

  move: function() {
    for(var i = 0; i < this.activeLines.length; i++) {
      var line = this.activeLines[i];

      if(line.isMoving) {
        line.move();
      }
      else {
        // update panel
        var prevEndPanel = this.activePanels[i].endPanel;
        var nextPanel    = this.pickRandomPanel();
        line.setColor(prevEndPanel.color, nextPanel.color);

        this.activePanels[i].startPanel = prevEndPanel;
        this.activePanels[i].endPanel   = nextPanel;

        var prevStartPanel = this.activePanels[i].startPanel;
        prevStartPanel.setFlashStatus(false);

        // update line
        var prevEndPos = line.getEndPoint();
        var nextPos    = nextPanel.position;
        line.updatePosition(prevEndPos, nextPos);
      }
    }
  },

  getLines: function() {
    return this.activeLines;
  },

  getLinesForPartcileSplash: function() {
    return this.activeLines.filter(function(line) { return line.isSplashingParticle() });
  }
}
Advertisements

Posted on May 1, 2013, in JavaScript. Bookmark the permalink. Leave a comment.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: