Skip to content

Instantly share code, notes, and snippets.

@enjalot
Last active November 24, 2015 21:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enjalot/638ad00cdc053b35461f to your computer and use it in GitHub Desktop.
Save enjalot/638ad00cdc053b35461f to your computer and use it in GitHub Desktop.
3D Pie: god help my soul

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

<!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>
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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment