nw.jsでデスクトップアプリを作る(18) - SVG pathをthree.js shapeに変換する(2)

公開:2015-03-01 20:17
更新:2017-09-22 05:40
カテゴリ:three.js,nw.js,d3.js,svg,javascript,webgl,html5,nw.jsでデスクトップアプリを作る

描画がおかしくなるバグもようやく直った。

ヒントは下記記事。


Converting SVG paths with holes to extruded shapes in three.js - Stack Overflow

ようするに穴あき部分のpathは馬ボディのshapeのholesにpathで追加しないとおかしくなるのであった。この部分はsvgの時点でどこが穴あきなのかわかる情報を付加しないとthree.jsではうまく変換することができない。なので私はpathのid属性に穴のpathは「holeXXXX」とつけて識別できるようにした。馬ボディのpathは「horsexx」としてある。

とりあえずChromeとFirefoxは動作することを確認した。Win10 Tech Preview 9926 のIE11では動作しなかった。

これでようやく次のステップに進める。しかしnw.jsとぜんぜん関係なくなってきてるな。。

動作サンプル

新しいウィンドウで開く

ソースコード・リソース

/dev/horse/0003/index.html

<!DOCTYPE html>
<html vocab="http://schema.org" lang="ja">
<head>
  <title>SVGアニメーションのテスト</title>
  <meta charset="utf-8" />
  <meta name="description" content="SVGアニメーションのテスト" />
  <meta name="keywords" content="Youtube,d3.js,Q.js,jquery" />
  <meta name="author" content="sfpgmr" />
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
  <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.2/d3.js"></script>
  <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/three.js/r70/three.js"></script>
  <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/q.js/1.1.2/q.min.js" ></script>
  <!--<script type="text/javascript" src="./graphics.js"></script> -->
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.2/normalize.min.css" />
  <style>
    body {
      margin: 0;
      padding: 0;
      overflow: hidden;
    }
    #svg {
      display:none;
    }
  </style>

</head>
<body>
  <div id="content"></div>
  <div id="svg"></div>
  <div id="svgcell"></div>
  <script type="text/javascript" src="index.js"></script>
  <script>
  </script>
</body>
</html>

/dev/horse/0003/index.js

//The MIT License (MIT)
//
//Copyright (c) 2015 Satoshi Fujiwara
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.

/// <reference path="http://cdnjs.cloudflare.com/ajax/libs/d3/3.5.2/d3.js" />
/// <reference path="http://cdnjs.cloudflare.com/ajax/libs/three.js/r70/three.js" />
/// <reference path="..\intellisense\q.intellisense.js" />

// stackoverflowより
// 絶対座標から相対座標への変換
// http://stackoverflow.com/questions/14179333/convert-svg-path-to-relative-commands
function convertToRelative(path) {
  function set(type) {
    var args = [].slice.call(arguments, 1)
      , rcmd = 'createSVGPathSeg'+ type +'Rel'
      , rseg = path[rcmd].apply(path, args);
    segs.replaceItem(rseg, i);
  }
  var dx, dy, x0, y0, x1, y1, x2, y2, segs = path.pathSegList;
  for (var x = 0, y = 0, i = 0, len = segs.numberOfItems; i < len; i++) {
    var seg = segs.getItem(i)
      , c   = seg.pathSegTypeAsLetter;
    if (/[MLHVCSQTAZz]/.test(c)) {
      if ('x1' in seg) x1 = seg.x1 - x;
      if ('x2' in seg) x2 = seg.x2 - x;
      if ('y1' in seg) y1 = seg.y1 - y;
      if ('y2' in seg) y2 = seg.y2 - y;
      if ('x'  in seg) dx = -x + (x = seg.x);
      if ('y'  in seg) dy = -y + (y = seg.y);
      switch (c) {
        case 'M': set('Moveto',dx,dy);                   break;
        case 'L': set('Lineto',dx,dy);                   break;
        case 'H': set('LinetoHorizontal',dx);            break;
        case 'V': set('LinetoVertical',dy);              break;
        case 'C': set('CurvetoCubic',dx,dy,x1,y1,x2,y2); break;
        case 'S': set('CurvetoCubicSmooth',dx,dy,x2,y2); break;
        case 'Q': set('CurvetoQuadratic',dx,dy,x1,y1);   break;
        case 'T': set('CurvetoQuadraticSmooth',dx,dy);   break;
        case 'A': set('Arc',dx,dy,seg.r1,seg.r2,seg.angle,
                      seg.largeArcFlag,seg.sweepFlag);   break;
        case 'Z': case 'z': x = x0; y = y0; break;
      }
    }
    else {
      if ('x' in seg) x += seg.x;
      if ('y' in seg) y += seg.y;
    }
    // store the start of a subpath
    if (c == 'M' || c == 'm') {
      x0 = x;
      y0 = y;
    }
  }
  path.setAttribute('d', path.getAttribute('d').replace(/Z/g, 'z'));
}

// svg pathをthree.jsのshapeに変換する
// スペースの処理とy座標を反転するように修正
// From d3-threeD.js
// https://github.com/asutherland/d3-threeD
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
var DEGS_TO_RADS = Math.PI / 180, UNIT_SIZE = 100;
var DIGIT_0 = 48, DIGIT_9 = 57, COMMA = 44, SPACE = 32, PERIOD = 46, MINUS = 45;
function transformSVGPath(pathStr,obj) {
	var path = obj ? new obj() : new THREE.Shape();
	var idx = 1, len = pathStr.length, activeCmd,
		x = 0, y = 0, nx = 0, ny = 0, firstX = null, firstY = null,
		x1 = 0, x2 = 0, y1 = 0, y2 = 0,
		rx = 0, ry = 0, xar = 0, laf = 0, sf = 0, cx, cy;
	function eatNum() {
		var sidx, c, isFloat = false, s;
		// eat delims
		while (idx < len) {
			c = pathStr.charCodeAt(idx);
			if (c !== COMMA && c !== SPACE)
				break;
			idx++;
		}
		if (c === MINUS)
			sidx = idx++;
		else
			sidx = idx;
		// eat number
		while (idx < len) {
			c = pathStr.charCodeAt(idx);
			if (DIGIT_0 <= c && c <= DIGIT_9) {
				idx++;
				continue;
			}
			else if (c === PERIOD) {
				idx++;
				isFloat = true;
				continue;
			}
			s = pathStr.substring(sidx, idx);
			return isFloat ? parseFloat(s) : parseInt(s);
		}
		s = pathStr.substring(sidx);
		return isFloat ? parseFloat(s) : parseInt(s);
	}
	function nextIsNum() {
		var c;
		// do permanently eat any delims...
		while (idx < len) {
			c = pathStr.charCodeAt(idx);
			if (c !== COMMA && c !== SPACE)
				break;
			idx++;
		}
		c = pathStr.charCodeAt(idx);
		return (c === MINUS || (DIGIT_0 <= c && c <= DIGIT_9));
	}
	var canRepeat;
	activeCmd = pathStr[0];
	while (idx <= len) {
		canRepeat = true;
		switch (activeCmd) {
			// moveto commands, become lineto's if repeated
			case 'M':
				x = eatNum();
				y = -eatNum();
				path.moveTo(x, y);
				activeCmd = 'L';
				firstX = x;
				firstY = y;
				break;
			case 'm':
				x += eatNum();
				y += -eatNum();
				path.moveTo(x, y);
				activeCmd = 'l';
				firstX = x;
				firstY = y;
				break;
			case 'Z':
			case 'z':
				canRepeat = false;
				if (x !== firstX || y !== firstY)
					path.lineTo(firstX, firstY);
				break;
			// - lines!
			case 'L':
			case 'H':
			case 'V':
				nx = (activeCmd === 'V') ? x : eatNum();
				ny = (activeCmd === 'H') ? y : -eatNum();
				path.lineTo(nx, ny);
				x = nx;
				y = ny;
				break;
			case 'l':
			case 'h':
			case 'v':
				nx = (activeCmd === 'v') ? x : (x + eatNum());
				ny = (activeCmd === 'h') ? y : (y + -eatNum());
				path.lineTo(nx, ny);
				x = nx;
				y = ny;
				break;
			// - cubic bezier
			case 'C':
				x1 = eatNum(); y1 =  -eatNum();
			case 'S':
				if (activeCmd === 'S') {
					x1 = 2 * x - x2; y1 = 2 * y - y2;
				}
				x2 = eatNum();
				y2 = -eatNum();
				nx = eatNum();
				ny = -eatNum();
				path.bezierCurveTo(x1, y1, x2, y2, nx, ny);
				x = nx; y = ny;
				break;
			case 'c':
				x1 = x + eatNum();
				y1 = y + -eatNum();
			case 's':
				if (activeCmd === 's') {
					x1 = 2 * x - x2;
					y1 = 2 * y - y2;
				}
				x2 = x + eatNum();
				y2 = y + -eatNum();
				nx = x + eatNum();
				ny = y + -eatNum();
				path.bezierCurveTo(x1, y1, x2, y2, nx, ny);
				x = nx; y = ny;
				break;
			// - quadratic bezier
			case 'Q':
				x1 = eatNum(); y1 = -eatNum();
			case 'T':
				if (activeCmd === 'T') {
					x1 = 2 * x - x1;
					y1 = 2 * y - y1;
				}
				nx = eatNum();
				ny = -eatNum();
				path.quadraticCurveTo(x1, y1, nx, ny);
				x = nx;
				y = ny;
				break;
			case 'q':
				x1 = x + eatNum();
				y1 = y + -eatNum();
			case 't':
				if (activeCmd === 't') {
					x1 = 2 * x - x1;
					y1 = 2 * y - y1;
				}
				nx = x + eatNum();
				ny = y + -eatNum();
				path.quadraticCurveTo(x1, y1, nx, ny);
				x = nx; y = ny;
				break;
			// - elliptical arc
			case 'A':
				rx = eatNum();
				ry = eatNum();
				xar = eatNum() * DEGS_TO_RADS;
				laf = eatNum();
				sf = eatNum();
				nx = eatNum();
				ny = -eatNum();
				if (rx !== ry) {
					console.warn("Forcing elliptical arc to be a circular one :(",
						rx, ry);
				}
				// SVG implementation notes does all the math for us! woo!
				// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
				// step1, using x1 as x1'
				x1 = Math.cos(xar) * (x - nx) / 2 + Math.sin(xar) * (y - ny) / 2;
				y1 = -Math.sin(xar) * (x - nx) / 2 + Math.cos(xar) * (y - ny) / 2;
				// step 2, using x2 as cx'
				var norm = Math.sqrt(
					 (rx*rx * ry*ry - rx*rx * y1*y1 - ry*ry * x1*x1) /
					 (rx*rx * y1*y1 + ry*ry * x1*x1));
				if (laf === sf)
					norm = -norm;
				x2 = norm * rx * y1 / ry;
				y2 = norm * -ry * x1 / rx;
				// step 3
				cx = Math.cos(xar) * x2 - Math.sin(xar) * y2 + (x + nx) / 2;
				cy = Math.sin(xar) * x2 + Math.cos(xar) * y2 + (y + ny) / 2;
				var u = new THREE.Vector2(1, 0),
					v = new THREE.Vector2((x1 - x2) / rx,
					                      (y1 - y2) / ry);
				var startAng = Math.acos(u.dot(v) / u.length() / v.length());
				if (u.x * v.y - u.y * v.x < 0)
					startAng = -startAng;
				// we can reuse 'v' from start angle as our 'u' for delta angle
				u.x = (-x1 - x2) / rx;
				u.y = (-y1 - y2) / ry;
				var deltaAng = Math.acos(v.dot(u) / v.length() / u.length());
				// This normalization ends up making our curves fail to triangulate...
				if (v.x * u.y - v.y * u.x < 0)
					deltaAng = -deltaAng;
				if (!sf && deltaAng > 0)
					deltaAng -= Math.PI * 2;
				if (sf && deltaAng < 0)
					deltaAng += Math.PI * 2;
				path.absarc(cx, cy, rx, startAng, startAng + deltaAng, sf);
				x = nx;
				y = ny;
				break;
			default:
				throw new Error("weird path command: " + activeCmd);
		}
		// just reissue the command
		if (canRepeat && nextIsNum())
			continue;
		activeCmd = pathStr[idx++];
	}
	return path;
}

// from gist
// https://gist.github.com/gabrielflorit/3758456
function createShape( shape, color, x, y, z, rx, ry, rz, s ) {
  // flat shape
 
  var geometry = new THREE.ShapeGeometry( shape );
  var material = new THREE.MeshBasicMaterial({
    color: color, 
    side: THREE.DoubleSide, 
    overdraw: true
  });
  
  var mesh = new THREE.Mesh( geometry, material );
  mesh.position.set( x, y, z );
  mesh.rotation.set( rx, ry, rz );
  mesh.scale.set( s, s, s );
 
  return mesh;
}

// メイン
window.addEventListener('load',function(){

  var WIDTH = window.innerWidth, HEIGHT = window.innerHeight;


  var renderer = new THREE.WebGLRenderer({ antialias: false, sortObjects: true });
  renderer.setSize(WIDTH, HEIGHT);
  renderer.setClearColor(0x000000, 1);
  renderer.domElement.id = 'console';
  renderer.domElement.className = 'console';
  renderer.domElement.style.zIndex = 0;

  d3.select('#content').node().appendChild(renderer.domElement);

  renderer.clear();
  // シーンの作成
  var scene = new THREE.Scene();

  // カメラの作成
  var camera = new THREE.PerspectiveCamera(90.0, WIDTH / HEIGHT);
  camera.position.x = 0.0;
  camera.position.y = 0.0;
  camera.position.z = (WIDTH / 2.0) * HEIGHT / WIDTH;
  camera.lookAt(new THREE.Vector3(0.0, 0.0, 0.0));

  window.addEventListener('resize',function()
  {
    WIDTH = window.innerWidth;
    HEIGHT = window.innerHeight;
    renderer.setSize(WIDTH,HEIGHT);
    camera.aspect = WIDTH / HEIGHT;
    camera.position.z = (WIDTH / 2.0) * HEIGHT / WIDTH;
    camera.updateProjectionMatrix();
  });


  var xml = Q.nfbind(d3.xml);
  var gto;
  // SVGファイルから馬のメッシュを作る
  xml('./horse03.svg','image/svg+xml')
  .then(function(svg){
    try {
      document.querySelector('#svg').appendChild(svg.firstChild);
      d3.select('#svg').selectAll('g').each(function(){
        var g = d3.select(this);
        var boundingBox = g.select('rect').node();
        var paths = g.selectAll('path');
        var holes = [];
        var shape = null;
        var shapeId = null;
        paths.each(function(){
          // 馬セルの取り出しと座標補正
          var path = d3.select(this);
          convertToRelative(path.node());
          var m = path.node().createSVGPathSegMovetoRel
                  (path.node().pathSegList[0].x - boundingBox.x.baseVal.value - boundingBox.width.baseVal.value / 2.0,
                  path.node().pathSegList[0].y - boundingBox.y.baseVal.value - boundingBox.height.baseVal.value / 2.0
                  );
          path.node().pathSegList.replaceItem(m,0);
          path.attr('d',path.attr('d'));
          // svg pathからthree.js shape Meshへの変換
          if(path.attr('id').match(/hole/)){
            holes.push(transformSVGPath(path.attr('d'),THREE.Path));
          } else {
            shape = transformSVGPath(path.attr('d'));
            shapeId = path.attr('id');
          }
        });

        holes.forEach(function(d){
          shape.holes.push(d);
        });
        var shapeMesh = createShape(shape,0xFFFF00,0,0,0,0,0,0,1.0);
        shapeMesh.visible = false;
        shapeMesh.name = shapeId;
        scene.add(shapeMesh);
      });
      d3.select('#svg').remove();
    } catch (e) {
      console.log(e + '\n' + e.stack);
    }
    
    //レンダリング
    (function render(index){
      if(index > 10.0) index = 0.0;
      var idx = parseInt(index,10);
      scene.getObjectByName('horse' + ('00' + idx.toString(10)).slice(-2)).visible = true;
      if(idx == 0){
        scene.getObjectByName('horse10').visible = false;
      } else {
        scene.getObjectByName('horse' + ('00' + (idx - 1).toString(10)).slice(-2)).visible = false;
      }
      renderer.render(scene,camera);
      index += 0.25;
      requestAnimationFrame(render.bind(null,index));
    })(0);
//    console.log(d3.select('#svg').html());
  });
});

/dev/horse/0003/README.md

## SVGからthree.jsのshapeへの変換(2)
[エドワード・マイブリッジ](http://upload.wikimedia.org/wikipedia/commons/7/73/The_Horse_in_Motion.jpg)の「Horse in motion」をInkscapeでトレースし、各馬をセル化したものをthree.jsのshapeに変換し表示しています。

下記のURLから動くデモが見れます。Windows 10 Tech Preview 9926 のIE11では動作しませんでした。ひょっとするとIE11ではそもそも動作しないのかもしれません。原因は不明ですが。。

[http://bl.ocks.org/sfpgmr/855ad392435fcdd87584](http://bl.ocks.org/sfpgmr/855ad392435fcdd87584)

※前回のバグは下記記事が糸口となり解決しました。

[Converting SVG paths with holes to extruded shapes in three.js](http://stackoverflow.com/questions/16118274/converting-svg-paths-with-holes-to-extruded-shapes-in-three-js)

/dev/horse/0003/thumbnail.png