一定範囲にとどまる3Dノードをつくる


デモ：https://mo49.github.io/qiita/20161004/public/

![スクリーンショット 2016-10-05 20.40.09.png](https://qiita-image-store.s3.amazonaws.com/0/136286/f7a7244c-ff0f-4c6a-9a52-57295df7b0e1.png "スクリーンショット 2016-10-05 20.40.09.png")

まずパーティクルのデータを用意

```javascript:ParticleData.js
export const PARTICLE_DATA = [
	{
		size : 5,
		color : "rgba(255, 0, 0, 1)"
	},
	{
		size : 4.5,
		color : "rgba(255, 0, 0, 1)"
	},
	{
		size : 4.5,
		color : "rgba(251, 188, 5, 1)"
	},
	{
		size : 4.0,
		color : "rgba(251, 188, 5, 1)"
	},
	{
		size : 4.0,
		color : "rgba(251, 188, 5, 1)"
	},
	{
		size : 4.0,
		color : "rgba(52, 168, 83, 1)"
	},
	{
		size : 3.5,
		color : "rgba(52, 168, 83, 1)"
	},
	{
		size : 4.0,
		color : "rgba(66, 133, 244, 1)"
	},
	{
		size : 4.5,
		color : "rgba(66, 133, 244, 1)"
	},
	{
		size : 4.0,
		color : "rgba(66, 133, 244, 1)"
	}

]
```

汎用な変数と関数、
カメラとアフィン変換の設定

```javascript:ParticleSetting.js
export const uv = { // util variable
  ww : window.innerWidth,
  wh : window.innerHeight
}
export const uf = { // util function
  dtr : (d) => d*Math.PI/180, // degree to radian
  randomPos : () => Math.sin( Math.floor(Math.random()*360) * Math.PI/180 ),
  calcDistance : (p1, p2, p3) => Math.sqrt( Math.pow(p2.x-p1.x,2) + Math.pow(p2.y-p1.y,2) + Math.pow(p2.z-p1.z,2) )
}


export const camera = {
  self: {
    x: 0,
    y: 0,
    z: uv.ww*2
  },
  target: {
    x: 0,
    y: 0,
    z: 0
  },
  distance: {
    x: 0,
    y: 0,
    z: 0
  },
  angle: {
    cosPhi   : 0,
    sinPhi   : 0,
    cosTheta : 0,
    sinTheta : 0
  },
  zoom: 1,
  display: {
    x: uv.ww/2,
    y: uv.wh/2,
    z: 0
  },
  update: () => {
    const cd = camera.distance;
    const ct = camera.target;
    cd.x = ct.x - camera.self.x;
    cd.y = ct.y - camera.self.y;
    cd.z = ct.z - camera.self.z;
    camera.angle.cosPhi = -cd.z / Math.sqrt(cd.x*cd.x + cd.z*cd.z);
    camera.angle.sinPhi = cd.x / Math.sqrt(cd.x*cd.x + cd.z*cd.z);
    camera.angle.cosTheta = Math.sqrt(cd.x*cd.x + cd.z*cd.z) / Math.sqrt(cd.x*cd.x + cd.y*cd.y + cd.z*cd.z);
    camera.angle.sinTheta = -cd.y / Math.sqrt(cd.x*cd.x + cd.y*cd.y + cd.z*cd.z);
  }
};

export const affine = {
  world: {
    size: (p, size) => {
      return {
        x: p.x*size.x,
        y: p.y*size.y,
        z: p.z*size.z
      };
    },
    rotate: {
      x: (p, rotate) => {
        return {
          x: p.x,
          y: p.y*Math.cos(uf.dtr(rotate.x)) - p.z*Math.sin(uf.dtr(rotate.x)),
          z: p.y*Math.sin(uf.dtr(rotate.x)) + p.z*Math.cos(uf.dtr(rotate.x))
        };
      },
      y: (p, rotate) => {
        return {
          x: p.x*Math.cos(uf.dtr(rotate.y)) + p.z*Math.sin(uf.dtr(rotate.y)),
          y: p.y,
          z: -p.x*Math.sin(uf.dtr(rotate.y)) + p.z*Math.cos(uf.dtr(rotate.y))
        };
      },
      z: (p, rotate) => {
        return {
          x: p.x*Math.cos(uf.dtr(rotate.z)) - p.y*Math.sin(uf.dtr(rotate.z)),
          y: p.x*Math.sin(uf.dtr(rotate.z)) + p.y*Math.cos(uf.dtr(rotate.z)),
          z: p.z
        };
      }
    },
    position: (p, position) => {
      return {
        x: p.x + position.x,
        y: p.y + position.y,
        z: p.z + position.z
      };
    }
  },
  view: {
    phi: (p) => {
      return {
        x: p.x*camera.angle.cosPhi + p.z*camera.angle.sinPhi,
        y: p.y,
        z: p.x*-camera.angle.sinPhi + p.z*camera.angle.cosPhi
      };
    },
    theta: (p) => {
      return {
        x: p.x,
        y: p.y*camera.angle.cosTheta - p.z*camera.angle.sinTheta,
        z: p.y*camera.angle.sinTheta + p.z*camera.angle.cosTheta
      };
    },
    viewReset: (p) => {
      return {
        x: p.x - camera.self.x,
        y: p.y - camera.self.y,
        z: p.z - camera.self.z
      };
    }
  },
  perspective: (p) => {
    return {
      x: p.x * camera.distance.z/p.z * camera.zoom,
      y: p.y * camera.distance.z/p.z * camera.zoom,
      z: p.z * camera.zoom,
      p: camera.distance.z/p.z
    };
  },
  display: (p, display) => {
    return {
      x: p.x + display.x,
      y: -p.y + display.y,
      z: p.z + display.z,
      p: p.p
    };
  },
  process: (model, size, rotate, position, display) => {
    const aw = affine.world;
    const av = affine.view;
    var ret = aw.size(model, size);
    ret = aw.rotate.x(ret, rotate);
    ret = aw.rotate.y(ret, rotate);
    ret = aw.rotate.z(ret, rotate);
    ret = aw.position(ret, position);
    ret = av.phi(ret);
    ret = av.theta(ret);
    ret = av.viewReset(ret);
    ret = affine.perspective(ret);
    ret = affine.display(ret, display);
    return ret;
  }
};
```

こちらがメインのJS
作成したバーテックスとデータの数が一致しない場合は、デフォルトで用意したパーティクルが生成される

```javascript:Particle.js
import { uv, uf, camera, affine } from './ParticleSetting';
import { PARTICLE_DATA } from './ParticleData';

class Vertex3d {
  constructor(param) {
    this.affineIn = {};
    this.affineOut = {};
    this.affineIn.vertex = param.vertex;
    this.affineIn.size = param.size;
    this.affineIn.rotate = param.rotate;
    this.affineIn.position = param.position;
  }
  vertexUpdate() {
    this.affineOut = affine.process(
      this.affineIn.vertex,
      this.affineIn.size,
      this.affineIn.rotate,
      this.affineIn.position,
      camera.display
    );
  }
}


export default class Particle {
  constructor(opts = {}) {
    this.velo = 0.2;
    this.limitT = 360;
    this.diff = 70;
    // this.firstPos = 100;
    this.particleNum = isNaN(opts.particleNum) ? 10 : opts.particleNum;
    this.particleSize = isNaN(opts.particleSize) ? 4 : opts.particleSize;
    this.cubeboxSize = isNaN(opts.cubeboxSize) ? 25 : opts.cubeboxSize;
    this.limitDistance = isNaN(opts.limitDistance) ? 500 : opts.limitDistance;
    this.cubeRotateSpeedX = isNaN(opts.cubeRotateSpeedX) ? 0.01 : opts.cubeRotateSpeedX;
    this.cubeRotateSpeedY = isNaN(opts.cubeRotateSpeedY) ? 0.03 : opts.cubeRotateSpeedY;
    this.cubeRotateSpeedZ = isNaN(opts.cubeRotateSpeedZ) ? 0.05 : opts.cubeRotateSpeedZ;

    this.init();
  }

  init() {
    this.canvas = document.getElementById("canvas");

    this.canvas.width = uv.ww;
    this.canvas.height = uv.wh;

    this.ctx = canvas.getContext("2d");
    this.ctx.lineWidth = 0.5;
    this.particleColor = "rgba(66, 133, 244, 1)";
    this.ctx.strokeStyle = "rgba(66, 133, 244, 0.8)";

    this.vertex = [];
    this.dist = [];
    this.times = [];

    for(let i = 0, len = this.particleNum; i<len; i++) {
      this.vertex[i] = new Vertex3d({
        vertex   : { x: uf.randomPos(), y: uf.randomPos(), z: uf.randomPos() },
        size     : { x: 0, y: 0, z: 0 },
        rotate   : { x: 20, y: -20, z: 0 },
        position : {
          x: this.diff*Math.sin(360*Math.random()*Math.PI/180),
          y: this.diff*Math.sin(360*Math.random()*Math.PI/180),
          z: this.diff*Math.sin(360*Math.random()*Math.PI/180)
        }
      });
      this.times[i] = {
        x: 360*Math.random(),
        y: 360*Math.random(),
        z: 360*Math.random()
      };
    }

    this.cubeRotate = {
      x: 0,
      y: 0,
      z: 0
    };

    this.cubeSize = {
      x: uv.ww/100 * this.cubeboxSize,
      y: uv.wh/100 * this.cubeboxSize,
      z: uv.ww/100 * this.cubeboxSize
    };
  }

  draw() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    camera.update();

    this.cubeRotate.x += this.cubeRotateSpeedX;
    this.cubeRotate.y += this.cubeRotateSpeedY;
    this.cubeRotate.z += this.cubeRotateSpeedZ;

    for(let i=0; i<this.vertex.length; i++) {

      const particleColor = ( PARTICLE_DATA[i] ) ? PARTICLE_DATA[i].color : this.particleColor;
      const particleSize = ( PARTICLE_DATA[i] ) ? PARTICLE_DATA[i].size : this.particleSize;

      for(let val in this.times[i]) {
        if(this.times[i].hasOwnProperty(val)) {
          this.times[i][val] += this.velo;
          if(this.times[i][val] > this.limitT) this.times[i][val] = 0;
        }
      }

      this.vertex[i].affineIn.position = {
        x: this.diff*Math.cos(this.times[i].x*Math.PI/180),
        y: this.diff*Math.sin(this.times[i].y*Math.PI/180),
        z: this.diff*Math.sin(this.times[i].z*Math.PI/180)
      };

      this.vertex[i].affineIn.rotate = this.cubeRotate;
      this.vertex[i].affineIn.size = this.cubeSize;
      this.vertex[i].vertexUpdate();
      this.ctx.save();
      this.ctx.fillStyle = particleColor;
      this.ctx.beginPath();
      this.ctx.arc(this.vertex[i].affineOut.x, this.vertex[i].affineOut.y, this.vertex[i].affineOut.p * particleSize, 0, Math.PI*2, false);
      this.ctx.fill();
      this.ctx.closePath();
      this.ctx.restore();
      this.dist[i] = { x: this.vertex[i].affineOut.x, y: this.vertex[i].affineOut.y, z: this.vertex[i].affineOut.z };
    }

    for(let s=0; s<this.dist.length; s++) {
      for(let k=0; k<this.dist.length; k++) {
        if(s===k) continue;

        const currentDistance = uf.calcDistance(this.dist[s], this.dist[k]);

        if(currentDistance < this.limitDistance) {
          this.ctx.save();
          this.ctx.beginPath();
          const alpha = (this.limitDistance - currentDistance) / this.limitDistance;
          this.ctx.strokeStyle = "rgba(20, 180, 255, " + alpha + ")";
          this.ctx.moveTo(this.dist[s].x, this.dist[s].y);
          this.ctx.lineTo(this.dist[k].x, this.dist[k].y);
          this.ctx.stroke();
          this.ctx.closePath();
        }
      }
    }
  };

  render() {
    const loop = () => {
      this.draw();
      requestAnimationFrame(loop);
    };
    loop();
  };

  start() {
    this.render();
  };

  stop() {

  }

}
```


> 参考
http://jsdo.it/takumifukasawa/7Fqg