球面上にN個の点を均等に配置したい。その8(正二十面体を細かく分割)

正二十面体を細かく分解してみた。なにげに綺麗に分散された。
しかし、座標計算に時間がかかるようで微妙だ。
点の他にワイヤーフレームもつけてみた。

<!DOCTYPE html>
<meta charset="UTF-8">
<title>N個の点を持つ球体を描画する</title>
<style>
</style>

<body>
<canvas width="600" height="600"></canvas>


<script>
/*
//https://www.jstage.jst.go.jp/article/geoinformatics/12/1/12_1_3/_pdf

図形・幾何学の英語
http://mage8.com/tango/tango37.html
*/
(function () {
  var pi = Math.PI;

  var acos = Math.acos;
  var atan2 = Math.atan2;
  var sqrt = Math.sqrt;
  var sin = Math.sin;
  var cos = Math.cos;


  //モデルを定義する
  function Model (vertex, surface, option) {
    this.vertex  = vertex  || [ ];  //頂点
    this.surface = surface || [ ]; //面
    this.option  = option  || [ ];  //オプション
  }

  //面を定義する
  function Surface (pointList) {
    this.pointList = pointList;
  }

  //頂点を定義する
  function Point (p) {
    this.x = p.x;
    this.y = p.y;
    this.z = p.z;
  }

  //面を追加する
  function addSurface (surface) {
    var len = this.surface.length;
    this.surface.push (surface);
    return len;
  }

  //頂点リストに追加する
  function addVertex (vertex) {
    var len = this.vertex.length;
    this.vertex.push (vertex);
    return len;
  }


  function getPoint (p) {
    return [p.x, p.y, p.z];
  }


  function getViewPoint () {
    return this.vertex.map (getPoint);
  }

  Model.prototype.addVertex  = addVertex;
  Model.prototype.addSurface = addSurface;
  Model.prototype.getViewPoint = getViewPoint;


  //2つのベクトルと半径から中間点の座標を返す
  function getSphereOnPoint (v, rr) {
    var a = v.x * v.x + v.y * v.y + v.z * v.z;
    var t = Math.sqrt (4 * a * rr) / (2 * a);
    return { x: v.x * t, y: v.y * t, z: v.z *t };
  }

  //2つのベクトルの中間を返す
  function getMidpoint (v0, v1) {
    return {x: (v0.x + v1.x) / 2, y: (v0.y + v1.y) / 2, z: (v0.z + v1.z) / 2};
  }


  function calcLength (x, y, z, rr) {
    var a = x * x + y * y + z * z, b = 0, c = -rr;
    var t = .5*Math.sqrt(-4*a*c)/a;

    return {x: x * t, y: y * t, z: z * t};
  }

  function createObject (arg) {
    return new this (arg);
  }


  //正二十面体を定義する
  function RegularIcosahedron (r) {
    var gr = (1 + sqrt (5)) / 2;
    var a = r / sqrt (1 + gr * gr);
    var b = a * gr;

    var model = new Model ();

    //物体の頂点
    var point = [
      {x: 0, y: -a, z: -b}, {x: 0, y: a, z: -b}, {x: 0, y: -a, z: b}, {x: 0, y: a, z: b},
      {x: -b, y: 0, z: -a}, {x: -b, y: 0, z: a}, {x: b, y: 0, z: -a}, {x: b, y: 0, z: a},
      {x: -a, y: -b, z: 0}, {x: a, y: -b, z: 0}, {x: -a, y: b, z: 0}, {x: a, y: b, z: 0}
    ].map (createObject, Point);


    //辺の順序が面を現す
    var surface = [
      [ 0, 1, 6], [ 1, 0, 4], [ 2, 3, 5], [ 3, 2, 7],
      [ 4, 5,10], [ 5, 4, 8], [ 6, 7, 9], [ 7, 6,11],
      [ 8, 9, 2], [ 9, 8, 0], [10,11, 1], [11,10, 3],
      [ 0, 6, 9], [ 0, 8, 4], [ 1, 4,10], [ 1,11, 6],
      [ 2, 5, 8], [ 2, 9, 7], [ 3, 7,11], [ 3,10, 5]
    ].map (createObject, Surface);

    return new Model (point, surface, { radius: r });
  }


  function SplitTriangle () {
    var r = this.option.radius;
    var surface = this.surface;
    var vertex = this.vertex;
    var newSurface = [ ];
    var i, s, a, b, c, d, e, f, dn, en, fn;
    var ax, ay, az;
    var rr = r * r;

    for (i = 0; s = surface[i]; i += 1) {
      //基本となる3点
      a = vertex[s.pointList[0]];
      b = vertex[s.pointList[1]];
      c = vertex[s.pointList[2]];
      d = getSphereOnPoint (getMidpoint (a, b), rr);
      e = getSphereOnPoint (getMidpoint (b, c), rr);
      f = getSphereOnPoint (getMidpoint (c, a), rr);
      dn = this.addVertex (d);
      en = this.addVertex (e);
      fn = this.addVertex (f);

      newSurface.push ([
        new Surface ([s.pointList[0], dn, fn]),
        new Surface ([dn, s.pointList[1], en]),
        new Surface ([en, s.pointList[2], fn]),
        new Surface ([dn, en, fn])
      ]);
    }
    this.surface = Array.prototype.concat.apply ([ ], newSurface);
  }


  function create (n, r) {
    var model = RegularIcosahedron (r);
    var i, s, b;

    for (i = 0; i < n; i += 1) {
      SplitTriangle.call (model);
    }

    return model;
  }




  this.create = create;

}) ();


//___________________________________

(function () {

  var INIT_QUATERNION = [1, 0, 0, 0];

  function RotationController (element) {
    this.target = element;
    this.mouseX = null;//マウス座標の基点
    this.mouseY = null;//マウス座標の基点
    this.touchF = false; //ドラッグ中か?
    this.Qnow = INIT_QUATERNION; //今回のマウスのドラッグ中のクォータニオン
    this.Qbef = INIT_QUATERNION; //前回のクォータニオン
    this.rots = INIT_QUATERNION; //今回と前回のクォータニオンの積(これが重要)
    this.gain = 1 / element.offsetWidth ; // mouse移動の感度
    this.dx = 0;//マウスの慣性移動量
    this.dy = 0;//マウスの慣性移動量
    this.timerId = null;//慣性移動中のタイマーID
    this.miniInertia = 1e-7;//慣性移動量の最小値
  }


  //画面の2次元移動量から3次元の回転量を求める
  function rotation (dx, dy) {
    var a, b, a0, a1, a2, a3, b0, b1, b2, b3, r, t, as;

    if (t = dx * dx + dy * dy) {
      r = Math.sqrt (t);
      as = Math.sin (r) / r;
      a = this.Qnow;
      a0 = a[0]; a1 = a[1]; a2 = a[2]; a3 = a[3];
      b0 = dy * as; b1 = dx * as; b3 = Math.cos (r);

      // クオータニオンによる回転
      a = this.Qbef;
      b = this.Qnow = [
        a0 * b3 - a3 * b0           - a2 * b1,
        a1 * b3 + a3 * b1 - a2 * b0,
        a2 * b3           + a0 * b1 + a1 * b0,
        a3 * b3 + a0 * b0 - a1 * b1
      ];

      //前回(a)と今回(b)のクォータニオンの積
      a0 = a[0]; a1 = a[1]; a2 = a[2]; a3 = a[3];
      b0 = b[0]; b1 = b[1]; b2 = b[2]; b3 = b[3];

      this.rots = [
        a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3,
        a0 * b1 + a1 * b0 + a2 * b3 - a3 * b2,
        a0 * b2 - a1 * b3 + a2 * b0 + a3 * b1,
        a0 * b3 + a1 * b2 - a2 * b1 + a3 * b0
      ];
      this.dx = dx;
      this.dy = dy;
    }
    return t;
  }


  //慣性
  function inertia () {
    var distance = rotation.call (
      this,
      this.dx - this.dx / 40,
      this.dy - this.dy / 40
    );

    if (this.miniInertia < distance)
      this.timerId = setTimeout (inertia.bind (this), 33);
  }


  //クォータニオンによる座標群の回転
  function quaternionRotation (point) {

    var i, j, x, y, z;
    var p, vertex;
    var q = this.rots;
    var q0 = q[0], q1 = q[1], q2 = q[2], q3 = q[3];
    var a0, a1, a2, a3;
    var s = [], rst = [];

    for (i = 0; p = point[i]; i++) {
        x = p[0], y = p[1], z = p[2];
        a0 =  q3 * x + q1 * z - q2 * y;
        a1 =  q3 * y + q2 * x - q0 * z;
        a2 =  q3 * z + q0 * y - q1 * x;
        a3 = -q0 * x - q1 * y - q2 * z;
        s = [
          a0 * q3 - a3 * q0 - a1 * q2 + a2 * q1,
          a1 * q3 - a3 * q1 - a2 * q0 + a0 * q2,
          a2 * q3 - a3 * q2 - a0 * q1 + a1 * q0
        ];
      rst[i] = s;
    }
    return rst;
  }


  //各イベント処理
  function handleEvent (event) {
    var e, x, y, dx, dy, a, b, c, e, r, t;
    var a0, a1, a2, a3, b0, b1, b2, b3, as;

    switch (event.type) {

    // 制御終了
    case 'mouseup' :
    case 'mouseout' :
    case 'touchend' :
      this.touchF = false;
      inertia.call (this);//制御を慣性にする
      break;

    // 制御開始
    case 'mousedown' :
    case 'touchstart' :
      if (this.timerId) {//慣性を解除
        clearTimeout (this.timerId);
        this.timerId = null;
      }
      this.touchF = true;
      this.Qnow = INIT_QUATERNION;
      this.Qbef = this.rots;
      e = event.target.getBoundingClientRect ();
      this.mouseX = event.pageX - e.left;
      this.mouseY = event.pageY - e.top;
      break;

    // 回転制御中
    case 'mousemove' :
    case 'touchmove' :
      event.preventDefault ();//ipadなどでスクロールさせないため
      e = event.target.getBoundingClientRect ();
      x = event.pageX - e.left;
      y = event.pageY - e.top;

      if (this.touchF){
        dx = (x - this.mouseX) * this.gain;
        dy = (y - this.mouseY) * this.gain;
        rotation.call (this, dx, dy);
      }

      this.mouseX = x;
      this.mouseY = y;
      break;
    }

  }


  // 要素にイベントを追加する
  function addEvent (event_type) {
    this.target.addEventListener (event_type, this, false);
  }


  // オブジェクトの生成
  function create (target) {
    if (1 > arguments.length)
      throw new Error ('引数がない');

    var obj = new RotationController (target);
    var event_list = window.TouchEvent //touchイベントがあるなら優先
      ? ['touchstart', 'touchend', 'touchmove']
      : ['mousedown', 'mouseup', 'mousemove', 'mouseout'];

    canvas = null;// メモリーリークパターンを断ち切る
    event_list.forEach (addEvent, obj);

    return obj;
  }

  //__

  RotationController.prototype.handleEvent = handleEvent;
  RotationController.prototype.quaternionRotation = quaternionRotation;
  //__
  RotationController.create = create;

  this.RotationController = RotationController;

}) ();


function canvasDrawCreate (canvas) {
  var ctx = canvas.getContext ('2d');
  var w = canvas.width;
  var h = canvas.height;
  var cx = w / 2;
  var cy = h / 2;
  var z = 1000;
  var opmax = 255;

  return function (ary, surface) {
    var x = [], y = [], p, a, b, s, i, j;
    ctx.fillStyle = 'RGBA(255,255,255,1)';
    ctx.fillRect (0,0, w, h);

    for (i = 0; i < ary.length; i++) {
      var px = ary[i][0];
      var py = ary[i][1];
      var pz = ary[i][2];
      var zz = (z - pz) / z;
      var op = -(pz - 400) / z;
      var alpha = Math.min (Math.max (0, op), 1);
      x[i] = cx + px * zz;
      y[i] = cy - py * zz;
    }

    ctx.strokeStyle = 'rgba(0,0,255,.2)';
    for (i = 0; s = surface[i]; i += 1) {
      p = s.pointList;
      ctx.beginPath();
      ctx.moveTo (x[p[0]], y[p[0]]);

      for (j = 1; j < p.length; j += 1) {
        ctx.lineTo (x[p[j]], y[p[j]]);
      }
      ctx.stroke();
    }


    ctx.fillStyle = 'rgba(255,0,0,1)';
    for (i = 0; i < ary.length; i += 1) {ctx.fillRect (x[i]-.5,y[i]-1, 2, 2);}
  };
}


  var loop = (function () {
    var target = document.querySelector ('canvas');
    var ctl = RotationController.create (target);
    var model = create (2, 200); //球面の点の数と半径
    var draw = canvasDrawCreate (target);
    var p = model.getViewPoint();

    return function () {
      var ps_ = ctl.quaternionRotation (p);
      draw (ps_, model.surface);
    };
  })();

  setInterval (loop, 1000/30); //タイマーで呼び出す

</script>