block by enjalot 638ad00cdc053b35461f

3D Pie: god help my soul

Full Screen

I wanted to troll the datavis community by making a 3D pie chart from d3.svg.arc using the new d3.shape stuff. Unfortunately the THREE.js shape context isn’t similar enough to the canvas API to just work.

I used this tutorial to get my THREE extruding situation setup. I found transformSVGPath in this old gist but I’m not sure the original source.

In the end the paths generated by the arc function aren’t parsed properly if you use innerRadius and the whole thing stopped being fun anyway. Oh well.

Built with blockbuilder.org

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
  <script src="svg2three.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r73/three.min.js"></script>
  <style>
    body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
    svg { position: absolute; width: 100%; height: 100%; }
    #canvas { position:absolute; width: 100%; height: 100%; }
  </style>
</head>

<body>
  <svg></svg>
  <div id="canvas"></div>
  <script>
    document.addEventListener("DOMContentLoaded", function(event) { 
    
    var arcs = [
      {"startAngle": 6.0164006661729340, "endAngle": 6.1497929866762595},
      {"startAngle": 6.1497929866762595, "endAngle": 6.2831853071795850},
      {"startAngle": 5.7696160251662825, "endAngle": 6.0164006661729340},
      {"startAngle": 5.4094390636563060, "endAngle": 5.7696160251662825},
      {"startAngle": 4.8224774611396780, "endAngle": 5.4094390636563060},
      {"startAngle": 3.8953388971130725, "endAngle": 4.8224774611396780},
      {"startAngle": 2.4012387305698390, "endAngle": 3.8953388971130725},
      {"startAngle": 0.0000000000000000, "endAngle": 2.4012387305698392}
    ];

    var width = window.innerWidth;
    var height = window.innerHeight;

    var outerRadius = 90;
    var innerRadius = 0;

    var step = 0;
    var scene = new THREE.Scene();
    // create a camera, which defines where we're looking at.
    var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
    // create a render and set the size
    var webGLRenderer = new THREE.WebGLRenderer();
    webGLRenderer.setClearColor(new THREE.Color("#fff", 1.0));
    webGLRenderer.setSize(window.innerWidth, window.innerHeight);
    webGLRenderer.shadowMap.enabled = true;
 
    var options = {
        amount: 1, //0, 20
        bevelThickness: 5, //0, 10
        bevelSize: 0, //0,10
        bevelSegments: 15, //0,30
        bevelEnabled: true,
        curveSegments: 15, //1,30
        steps: 3, //1,5
    };
    var shapes = [];
    //var shape = createMesh(new THREE.ShapeGeometry(drawShape(), options));
    arcs.forEach(function(d) {
      var arc = d3.svg.arc()
        .innerRadius(innerRadius)
        .outerRadius(outerRadius)
        .padAngle(0.05)
      
      
      var path = arc(d)
      var threeArc = transformSVGPath(path)
      var shape = createMesh(new THREE.ExtrudeGeometry(threeArc, options));
      //var shape = createMesh(new THREE.ShapeGeometry(threeArc, options));
      shapes.push(shape);
      scene.add(shape);
    })
    
    // position and point the camera to the center of the scene
    camera.position.x = 250;
    camera.position.y = 100;
    camera.position.z = 100;
    camera.lookAt(new THREE.Vector3(20, 15, 0));
    // add the output of the renderer to the html element
    document.getElementById("canvas").appendChild(webGLRenderer.domElement);
    
    function render() {
      step += 0.01;
      shapes.forEach(function(shape) {
        shape.rotation.x = step;
      })
      
      // render using requestAnimationFrame
      requestAnimationFrame(render);
      webGLRenderer.render(scene, camera);
    }
    render();


    function createMesh(geom) {
      geom.applyMatrix(new THREE.Matrix4().makeTranslation(-20, 0, 0));
      // assign two materials
      
      var meshMaterial = new THREE.MeshNormalMaterial({
       //   shading: THREE.FlatShading,
          transparent: true,
          opacity: 0.7
      });
      
      //var meshMaterial = new THREE.MeshPhongMaterial( { color: 0xdddddd, specular: 0x009900, shininess: 30, shading: THREE.FlatShading } );
      
      //  meshMaterial.side = THREE.DoubleSide;
      var wireFrameMat = new THREE.MeshBasicMaterial();
      wireFrameMat.wireframe = true;
      // create a multimaterial
      var mesh = THREE.SceneUtils.createMultiMaterialObject(geom, [meshMaterial]);
      return mesh;
    }
    })
  </script>
</body>

svg2three.js

function transformSVGPath(pathStr) {
 
  const DIGIT_0 = 48, DIGIT_9 = 57, COMMA = 44, SPACE = 32, PERIOD = 46,
      MINUS = 45;
    const DEGS_TO_RADS = Math.PI/180.0;
 
  var path = 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));
  }

    function eatAbsoluteArc() {
	rx = eatNum();
        ry = eatNum();
        xar = eatNum() * DEGS_TO_RADS;
        laf = eatNum();
        sf = eatNum();
        nx = eatNum();
        ny = eatNum();

	if( activeCmd == 'a' ) { // relative
	    nx += x;
	    ny += y;
	}

	//console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] Read arc params: rx=" + rx + ", ry=" + ry + ", xar=" + xar + ", laf=" + laf + ", sf=" + sf + ", nx=" + nx + ", ny=" + ny );
        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'
	//console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] TMP x1=" + x1 + ", y1=" + y1 + ", (rx*rx * y1*y1 + ry*ry * x1*x1)=" + (rx*rx * y1*y1 + ry*ry * x1*x1) + ", (rx*rx * ry*ry - rx*rx * y1*y1 - ry*ry * x1*x1)=" + (rx*rx * ry*ry - rx*rx * y1*y1 - ry*ry * x1*x1) );
        var norm = Math.sqrt( Math.abs(
          (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;
	//console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] TMP norm=" + norm + ", x2=" + x2 + ", y2=" + y2 );
        // 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;
	//console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] TMP cx=" + cx + ", cy=" + cy );
        
        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;
        
	//console.debug( "[SVGPath2ThreeShape.eatAbsoluteArc] Building arc from values: cx=" + cx + ", cy=" + cy + ", startAng=" + startAng + ", deltaAng=" + deltaAng + ", endAng=" + (startAng+deltaAng) + ", sweepFlag=" + sf );
        path.absarc(cx, cy, rx, startAng, startAng + deltaAng, sf);
        x = nx;
        y = ny;
    }
  
  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';
        break;
      case 'm':
        x += eatNum();
        y += eatNum();
        path.moveTo(x, y);
        activeCmd = 'l';
        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': 
	// eatAbsoluteArc();
      case 'a':
	eatAbsoluteArc();
        break;
      default:
        throw new Error("weird path command: " + activeCmd);
    }
    if (firstX === null) {
      firstX = x;
      firstY = y;
    }
    // just reissue the command
    if (canRepeat && nextIsNum())
      continue;
    activeCmd = pathStr[idx++];
  }
  
  return path;
}