block by armollica ba4b60e7fafdcaa6df3fdf88b9c9dc35

Isometric Projection

Full Screen

Isometric projection (Wikipedia)

index.html

<html>
<head>
<style>
html, body {
    font-family: monospace;
}

.cube .side {
    fill-opacity: 0.4;
    stroke: #666;
    stroke-width: 0.5;
}

.top { fill: steelblue; }
.bottom { fill: yellow; }
.left { fill: brown; }
.right { fill: forestgreen; }

.axis {
    fill: none;
    stroke: #000;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

var width = 960,
    height = 500;

var svg = d3.select('body').append('svg')
    .attr('width', width)
    .attr('height', height);

var projection = isometricProjection();

var path = isometricPath()
    .projection(projection);

svg.append('defs').append('marker')
    .attr('id', 'arrow')
    .attr('markerWidth', 10)
    .attr('markerHeight', 10)
    .attr('refX', 0)
    .attr('refY', 3)
    .attr('orient', 'auto')
    .attr('markerUnits', 'strokeWidth')
  .append('path')
    .attr('d', 'M0,0L0,6L9,3Z');

var g = svg.append('g')
    .attr('transform', function(d) {
        return 'translate(' + (width / 2) + ',' + (height / 2) + ')';
    });

var axes = g.append('g').attr('class', 'axes');

var axis = axes.selectAll('.axis').data(axesData())
    .enter().append('g')
        .attr('class', 'axis');

axis.append('path')
    .attr('d', function(d) { return path(d.points); })
    .attr('marker-end', 'url(#arrow)');

axis.append('text')
    .attr('transform', function(d) {
        var p = projection(d.labelLocation);
        return 'translate(' + p[0] + ',' + p[1] + ')';
    })
    .attr('dy', '0.33em')
    .style('text-anchor', 'middle')
    .text(function(d) { return d.label; });

var cube = g.append('g').attr('class', 'cube');

var side = cube.selectAll('path').data(cubeData())
    .enter().append('path')
        .attr('class', function(d) { return 'side ' + d.className; })
        .attr('d', function(d) { return path(d.points) + 'Z'; });

function update() {
    side.attr('d', function(d) { return path(d.points) + 'Z'; });
    axis.select('path')
        .attr('d', function(d) { return path(d.points); });
    axis.select('text')
        .attr('transform', function(d) {
            var p = projection(d.labelLocation);
            return 'translate(' + p[0] + ',' + p[1] + ')';
        });
}

d3.timer(function(elapsed) {
    projection
        .pitch((Math.PI / 6) * Math.sin(elapsed / 10000))
        .yaw((elapsed / 2000));
    update();
});

function cubeData() {
    return [
        {
            className: 'bottom',
            points: [
                [-50, -50, -50],
                [-50, +50, -50],
                [+50, +50, -50],
                [+50, -50, -50]
            ]
        },
        {
            className: 'left',
            points: [
                [+50, -50, +50],
                [+50, +50, +50],
                [+50, +50, -50],
                [+50, -50, -50]
            ]
        },
        {
            className: 'right',
            points: [
                [-50, +50, +50],
                [+50, +50, +50],
                [+50, +50, -50],
                [-50, +50, -50]
            ]
        },
        {
            className: 'left',
            points: [
                [-50, -50, +50],
                [-50, +50, +50],
                [-50, +50, -50],
                [-50, -50, -50]
            ]
        },
        {
            className: 'right',
            points: [
                [-50, -50, +50],
                [+50, -50, +50],
                [+50, -50, -50],
                [-50, -50, -50]
            ]
        },
        {
            className: 'top',
            points: [
                [-50, -50, +50],
                [-50, +50, +50],
                [+50, +50, +50],
                [+50, -50, +50]
            ]
        },
    ];
}

function axesData() {
    return [
        {
            label: 'x',
            labelLocation: [225, 0, 0],
            points: [
                [0, 0, 0],
                [200, 0, 0]
            ]
        },
        {
            label: 'y',
            labelLocation: [0, 225, 0],
            points: [
                [0, 0, 0],
                [0, 200, 0]
            ]
        },
        {
            label: 'z',
            labelLocation: [0, 0, 225],
            points: [
                [0, 0, 0],
                [0, 0, 200]
            ]
        }
    ];
}

function isometricProjection() {
    var sin = Math.sin,
        cos = Math.cos,
        asin = Math.asin,
        tan = Math.atan,
        PI = Math.PI;

    var pitch = PI / 6,
        yaw = PI / 4,
        alpha = asin(tan(pitch)),
        beta = yaw;

    // See https://en.wikipedia.org/wiki/Isometric_projection
    // TODO: Figure out why ax, ay and az needed to be flipped around.
    function project(point) {
        var ax = point[1],
            ay = -point[2],
            az = point[0];
    
        var x = cos(beta) * ax - sin(beta) * az,
            y = cos(alpha) * ay + sin(alpha) * 
                (sin(beta) * ax + cos(beta) * az);
        return [x, y];
    }

    project.pitch = function(x) {
        if (!arguments.length) return alpha;
        pitch = x;
        alpha = Math.asin(Math.tan(pitch));
        return project;
    };

    project.yaw = function(x) {
        if (!arguments.length) return beta;
        yaw = x;
        beta = yaw;
        return project;
    };

    return project;
}

function isometricPath() {
    var projection = function(d) { return d.slice(0, 2); };

    function path(points) {
        return 'M' + 
            points
                .map(function(point) { return projection(point).join(','); })
                .join('L');
    }

    path.projection = function(x) {
        if (!arguments.length) return projection;
        projection = x;
        return path;
    };

    return path;
}

function radiansToDegrees(radians) { return radians * 180 / Math.PI; }
function degreesToRadians(degrees) { return degrees * Math.PI / 180; }

</script>
</body>
</html>