How to use the Isometric Blocks example with D3js to animate the blocks.
The original tutorial: A tutorial on how to render isometric blocks in the correct order
Using D3js and canvas: Working with D3.js and Canvas: When and How
| licence: mit |
How to use the Isometric Blocks example with D3js to animate the blocks.
The original tutorial: A tutorial on how to render isometric blocks in the correct order
Using D3js and canvas: Working with D3.js and Canvas: When and How
| // | |
| IsoBlock.Block = function(pos,size,color) { | |
| // position in 3d space (obj with attrs x,y,z) | |
| this.pos = pos; | |
| // size of each dimension (obj with attrs x,y,z) | |
| this.size = size; | |
| // an array of 3 color shades (light,medium,dark - see colors.js) | |
| // (used for pseudo lighting) | |
| this.color = color || IsoBlock.colors.red; | |
| }; | |
| IsoBlock.Block.prototype = { | |
| getBounds: function() { | |
| var p = this.pos; | |
| var s = this.size; | |
| return { | |
| xmin: p.x, | |
| xmax: p.x + s.x, | |
| ymin: p.y, | |
| ymax: p.y + s.y, | |
| zmin: p.z, | |
| zmax: p.z + s.z, | |
| }; | |
| }, | |
| }; |
| IsoBlock.Camera = function(origin,scale) { | |
| // the pixel location of the isometric origin. | |
| this.origin = origin; | |
| // number of pixels per isometric unit. | |
| this.scale = scale; | |
| }; | |
| /* | |
| We have three separate coordinate systems used for different things: | |
| 1. Space (3D) | |
| We apply the usual 3D coordinates to define the boxes using x,y,z. | |
| 2. Isometric (2D) | |
| When the 3D space is flattened into an isometric view, we use oblique x and y | |
| axes separated by 120 degrees. | |
| All this does is treat all 3d coordinates as if they are at z=0. | |
| For example, if use have a box at (0,0,0) and we raised it to (0,0,1), it would | |
| look to be in the exact same position as a box at (1,1,0), so the 2d isometric | |
| coordinates are (1,1). This is a side effect of the isometric perspective. So | |
| the isometric 2D coordinates gets the "apparent" coordinates for all boxes if | |
| they were at z=0. | |
| This is accomplished by adding z to x and y. That is all. | |
| (Isometric coordinates are useful for determining when boxes overlap on the | |
| screen.) | |
| 3. Screen (2D) | |
| Before drawing, we convert the isometric coordinates to the usual x,y screen | |
| coordinates. | |
| This is done by multiplying each isometric 2D coordinate by its respective | |
| oblique axis vector and taking the sum. | |
| We then multiply this position by "scale" value to implement zoom in/out | |
| features for the camera. | |
| Then we add to an "origin" to implement panning features for the camera. | |
| */ | |
| IsoBlock.Camera.prototype = { | |
| // Determine if the given ranges are disjoint (i.e. do not overlap). | |
| // For determining drawing order, this camera considers two | |
| // ranges to be disjoint even if they share an endpoint. | |
| // Thus, we use less-or-equal (<=) instead of strictly less (<). | |
| areRangesDisjoint: function(amin,amax,bmin,bmax) { | |
| return (amax <= bmin || bmax <= amin); | |
| }, | |
| // Convert 3D space coordinates to flattened 2D isometric coordinates. | |
| // x and y coordinates are oblique axes separated by 120 degrees. | |
| // h,v are the horizontal and vertical distances from the origin. | |
| spaceToIso: function(spacePos) { | |
| var z = (spacePos.z == undefined) ? 0 : spacePos.z; | |
| var x = spacePos.x + z; | |
| var y = spacePos.y + z; | |
| return { | |
| x: x, | |
| y: y, | |
| h: (x-y)*Math.sqrt(3)/2, // Math.cos(Math.PI/6) | |
| v: (x+y)/2, // Math.sin(Math.PI/6) | |
| }; | |
| }, | |
| // Convert the given 2D isometric coordinates to 2D screen coordinates. | |
| isoToScreen: function(isoPos) { | |
| return { | |
| x: isoPos.h * this.scale + this.origin.x, | |
| y: -isoPos.v * this.scale + this.origin.y, | |
| }; | |
| }, | |
| // Convert the given 3D space coordinates to 2D screen coordinates. | |
| spaceToScreen: function(spacePos) { | |
| return this.isoToScreen(this.spaceToIso(spacePos)); | |
| }, | |
| // Get a block's vertices with helpful aliases. | |
| // Each vertex is named from its apparent position in an isometric view. | |
| getIsoNamedSpaceVerts: function(block) { | |
| var p = block.pos; | |
| var s = block.size; | |
| return { | |
| rightDown: {x:p.x+s.x, y:p.y, z:p.z}, | |
| leftDown: {x:p.x, y:p.y+s.y, z:p.z}, | |
| backDown: {x:p.x+s.x, y:p.y+s.y, z:p.z}, | |
| frontDown: {x:p.x, y:p.y, z:p.z}, | |
| rightUp: {x:p.x+s.x, y:p.y, z:p.z+s.z}, | |
| leftUp: {x:p.x, y:p.y+s.y, z:p.z+s.z}, | |
| backUp: {x:p.x+s.x, y:p.y+s.y, z:p.z+s.z}, | |
| frontUp: {x:p.x, y:p.y, z:p.z+s.z}, | |
| }; | |
| }, | |
| // Get the given block's vertices in flattened 2D isometric coordinates. | |
| getIsoVerts: function(block) { | |
| var verts = this.getIsoNamedSpaceVerts(block); | |
| return { | |
| leftDown: this.spaceToIso(verts.leftDown), | |
| rightDown: this.spaceToIso(verts.rightDown), | |
| backDown: this.spaceToIso(verts.backDown), | |
| frontDown: this.spaceToIso(verts.frontDown), | |
| leftUp: this.spaceToIso(verts.leftUp), | |
| rightUp: this.spaceToIso(verts.rightUp), | |
| backUp: this.spaceToIso(verts.backUp), | |
| frontUp: this.spaceToIso(verts.frontUp), | |
| }; | |
| }, | |
| // For the given block, get the min and max values on each isometric axis. | |
| getIsoBounds: function(block) { | |
| var verts = this.getIsoVerts(block); | |
| return { | |
| xmin: verts.frontDown.x, | |
| xmax: verts.backUp.x, | |
| ymin: verts.frontDown.y, | |
| ymax: verts.backUp.y, | |
| hmin: verts.leftDown.h, | |
| hmax: verts.rightDown.h, | |
| }; | |
| }, | |
| // Try to find an axis in 2D isometric that separates the two given blocks. | |
| // This helps identify if the the two blocks are overlap on the screen. | |
| getIsoSepAxis: function(block_a, block_b) { | |
| var a = this.getIsoBounds(block_a); | |
| var b = this.getIsoBounds(block_b); | |
| var sepAxis = null; | |
| if (this.areRangesDisjoint(a.xmin,a.xmax,b.xmin,b.xmax)) { | |
| sepAxis = 'x'; | |
| } | |
| if (this.areRangesDisjoint(a.ymin,a.ymax,b.ymin,b.ymax)) { | |
| sepAxis = 'y'; | |
| } | |
| if (this.areRangesDisjoint(a.hmin,a.hmax,b.hmin,b.hmax)) { | |
| sepAxis = 'h'; | |
| } | |
| return sepAxis; | |
| }, | |
| // Try to find an axis in 3D space that separates the two given blocks. | |
| // This helps identify which block is in front of the other. | |
| getSpaceSepAxis: function(block_a, block_b) { | |
| var sepAxis = null; | |
| var a = block_a.getBounds(); | |
| var b = block_b.getBounds(); | |
| if (this.areRangesDisjoint(a.xmin,a.xmax,b.xmin,b.xmax)) { | |
| sepAxis = 'x'; | |
| } | |
| else if (this.areRangesDisjoint(a.ymin,a.ymax,b.ymin,b.ymax)) { | |
| sepAxis = 'y'; | |
| } | |
| else if (this.areRangesDisjoint(a.zmin,a.zmax,b.zmin,b.zmax)) { | |
| sepAxis = 'z'; | |
| } | |
| return sepAxis; | |
| }, | |
| // In an isometric perspective of the two given blocks, determine | |
| // if they will overlap each other on the screen. If they do, then return | |
| // the block that will appear in front. | |
| getFrontBlock: function(block_a, block_b) { | |
| // If no isometric separation axis is found, | |
| // then the two blocks do not overlap on the screen. | |
| // This means there is no "front" block to identify. | |
| if (this.getIsoSepAxis(block_a, block_b)) { | |
| return null; | |
| } | |
| // Find a 3D separation axis, and use it to determine | |
| // which block is in front of the other. | |
| var a = block_a.getBounds(); | |
| var b = block_b.getBounds(); | |
| switch(this.getSpaceSepAxis(block_a, block_b)) { | |
| case 'x': return (a.xmin < b.xmin) ? block_a : block_b; | |
| case 'y': return (a.ymin < b.ymin) ? block_a : block_b; | |
| case 'z': return (a.zmin < b.zmin) ? block_b : block_a; | |
| default: throw "blocks must be non-intersecting"; | |
| } | |
| }, | |
| }; |
| // Tango Color Palette | |
| // http://en.wikipedia.org/wiki/Tango_Desktop_Project#Palette | |
| IsoBlock.colors = { | |
| yellow: {light:"#fce94f", medium:"#edd400", dark:"#c4a000"}, | |
| orange: {light:"#fcaf3e", medium:"#f57900", dark:"#ce5c00"}, | |
| brown: {light:"#e9b96e", medium:"#c17d11", dark:"#8f5902"}, | |
| green: {light:"#8ae234", medium:"#73d216", dark:"#4e9a06"}, | |
| blue: {light:"#729fcf", medium:"#3465a4", dark:"#204a87"}, | |
| purple: {light:"#ad7fa8", medium:"#75507b", dark:"#5c3566"}, | |
| red: {light:"#ef2929", medium:"#cc0000", dark:"#a40000"}, | |
| white: {light:"#eeeeec", medium:"#d3d7cf", dark:"#babdb6"}, | |
| black: {light:"#888a85", medium:"#555753", dark:"#2e3436"}, | |
| }; | |
| // from David at http://stackoverflow.com/a/11508164/142317 | |
| function hexToRgb(hex) { | |
| // strip out "#" if present. | |
| if (hex[0] == "#") { | |
| hex = hex.substring(1); | |
| } | |
| var bigint = parseInt(hex, 16); | |
| var r = (bigint >> 16) & 255; | |
| var g = (bigint >> 8) & 255; | |
| var b = bigint & 255; | |
| return r + "," + g + "," + b; | |
| } |
| <!doctype html> | |
| <html> | |
| <head> | |
| <meta charset="utf8"> | |
| <style> | |
| canvas { | |
| background-color: #f5f5f5; | |
| margin-right: 5px; | |
| text-align: center; | |
| } | |
| </style> | |
| <script src='main.js'></script> | |
| <script src='colors.js'></script> | |
| <script src='block.js'></script> | |
| <script src='camera.js'></script> | |
| <script src='painter.js'></script> | |
| <script src='sortBlocks.js'></script> | |
| <script src="https://d3js.org/d3.v4.min.js"></script> | |
| </head> | |
| <body> | |
| <canvas id='figure1b' width=350 height=200></canvas> | |
| <script> | |
| var canvas = d3.select("#figure1b"); | |
| var context = canvas.node().getContext("2d"); | |
| var detachedContainer = document.createElement("custom"); | |
| var dataContainer = d3.select(detachedContainer); | |
| var t = d3.timer(function(elapsed) { | |
| if (elapsed > 1000) t.stop(); | |
| drawCanvas(); | |
| }); | |
| function drawCustom(data) { | |
| var dataBinding = dataContainer.selectAll("custom.rect") | |
| .data(data, function(d) { return d; }); | |
| dataBinding | |
| .attr("z", 0) | |
| .transition() | |
| .duration(1000) | |
| .attr("z", 10); | |
| dataBinding.enter() | |
| .append("custom") | |
| .attr("class","rect") | |
| .attr("x", function(d){return d.x}) | |
| .attr("y", function(d){return d.dy}) | |
| .attr("z", function(d){return d.z}) | |
| .attr("dx", function(d){return d.dx}) | |
| .attr("dy", function(d){return d.dy}) | |
| .attr("color", function(d){return d.color}) | |
| .attr("dz", 0) | |
| .transition() | |
| .duration(1000) | |
| .attr("dz", function(d){return d.dz}); | |
| dataBinding.exit() | |
| .attr("size", 8) | |
| .transition() | |
| .duration(1000) | |
| .attr("size", 5) | |
| .attr("fillStyle", "lightgrey"); | |
| } | |
| function drawCanvas(){ | |
| context.clearRect(0, 0, canvas.width, canvas.height); | |
| var blocks = []; | |
| var elements = dataContainer.selectAll("custom.rect"); | |
| elements.each(function(d) { | |
| var node = d3.select(this); | |
| blocks.push(new IsoBlock.Block({'x': parseFloat(node.attr("x")), 'y': parseFloat(node.attr("y")),'z': parseFloat(node.attr("z"))}, | |
| {'x':parseFloat(node.attr("dx")), 'y':parseFloat(node.attr("dy")), 'z': parseFloat(node.attr("dz"))}, IsoBlock.colors[node.attr("color")])); | |
| }); | |
| IsoBlock.makeFigure({ canvas:'figure1b', blocks: blocks, drawPlane: false}); | |
| }; | |
| var ff = 'blue'; | |
| console.info(IsoBlock.colors[ff]); | |
| var data = [{x:0,y:0,z:0,dx:1,dy:1,dz:3,color: 'blue'}, | |
| {x:1,y:1,z:0,dx:1,dy:1,dz:2,color:'red'}, | |
| {x:4,y:7,z:0,dx:2,dy:2,dz:3,color:'purple'}]; | |
| drawCustom(data); | |
| </script> | |
| </body> | |
| </html> |
| var IsoBlock = IsoBlock || {}; | |
| IsoBlock.makeFigure = function(options) { | |
| // extract options | |
| var canvasId = options.canvas; | |
| var blocks = options.blocks; | |
| var shouldSortBlocks = (options.sortBlocks == undefined) ? true : options.sortBlocks; | |
| var shouldDrawAxes = options.drawAxis; | |
| var shouldDrawPlane = options.drawPlane; | |
| var axisLen = options.axisLen; | |
| var silhouette = options.silhouette; | |
| // set canvas and context. | |
| var canvas = document.getElementById(canvasId); | |
| var ctx = canvas.getContext('2d'); | |
| // extract scale and origin (camera attributes) | |
| var scale = (options.scale && options.scale(canvas.width,canvas.height)) || (canvas.height / 8); | |
| var origin = (options.origin && options.origin(canvas.width,canvas.height)) || {x: canvas.width/2, y: canvas.height }; | |
| // compute appropriate axis length (assuming origin is horizontally centered) | |
| if (!axisLen) { | |
| // make sure the axis extends to the edge of the canvas | |
| axisLen = Math.floor((canvas.width - origin.x) / scale / Math.cos(Math.PI/6)) - 0.5; | |
| } | |
| // Get horizontal axis' vertical displacement from origin. | |
| var hAxisV = origin.y/scale - 1; | |
| // create camera and painter. | |
| var camera = new IsoBlock.Camera(origin, scale); | |
| var painter = new IsoBlock.Painter(camera); | |
| // draw the xy grid | |
| function drawGrid() { | |
| // grid step | |
| var step = 1; | |
| // grid range | |
| var maxx = 15; | |
| var maxy = 15; | |
| // plot x lines | |
| ctx.beginPath(); | |
| for (x=-maxx; x<=maxx; x+=step) { | |
| painter.moveTo(ctx, {x:x, y:-maxy}); | |
| painter.lineTo(ctx, {x:x, y:maxy}); | |
| } | |
| // plot y lines | |
| for (y=-maxy; y<=maxy; y+=step) { | |
| painter.moveTo(ctx, {x:-maxx, y:y}); | |
| painter.lineTo(ctx, {x:maxx, y:y}); | |
| } | |
| // draw grid lines | |
| ctx.strokeStyle = "#d7d7d7"; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| }; | |
| // draw the xy axes and range bars for each block. | |
| function drawAxes() { | |
| var axisColor = "#444"; | |
| ctx.lineWidth = 1; | |
| ctx.strokeStyle = axisColor; | |
| ctx.fillStyle = axisColor; | |
| var arrowSize = 0.3; | |
| // draw x,y axes (and h axis if silhouette) | |
| var xAxisStart = camera.spaceToIso({x:-axisLen, y:0}); | |
| var xAxisEnd = camera.spaceToIso({x:axisLen, y:0}); | |
| var yAxisStart = camera.spaceToIso({x:0, y:-axisLen}); | |
| var yAxisEnd = camera.spaceToIso({x:0, y:axisLen}); | |
| var hAxisStart = {h:yAxisEnd.h, v:hAxisV}; | |
| var hAxisEnd = {h:xAxisEnd.h, v:hAxisV}; | |
| ctx.beginPath(); | |
| painter.moveTo(ctx, xAxisStart); | |
| painter.lineTo(ctx, xAxisEnd); | |
| painter.moveTo(ctx, yAxisStart); | |
| painter.lineTo(ctx, yAxisEnd); | |
| if (silhouette) { | |
| painter.moveTo(ctx, hAxisStart); | |
| painter.lineTo(ctx, hAxisEnd); | |
| } | |
| ctx.stroke(); | |
| // draw x-axis arrow | |
| ctx.beginPath(); | |
| painter.moveTo(ctx, {x:axisLen, y:0}); | |
| painter.lineTo(ctx, {x:axisLen-arrowSize, y:-arrowSize}); | |
| painter.lineTo(ctx, {x:axisLen-arrowSize, y:arrowSize}); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // draw y-axis arrow | |
| ctx.beginPath(); | |
| painter.moveTo(ctx, {y:axisLen, x:0}); | |
| painter.lineTo(ctx, {y:axisLen-arrowSize, x:-arrowSize}); | |
| painter.lineTo(ctx, {y:axisLen-arrowSize, x:arrowSize}); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // draw h-axis arrow | |
| if (silhouette) { | |
| ctx.beginPath(); | |
| painter.moveTo(ctx, hAxisStart); | |
| painter.lineTo(ctx, {h:hAxisStart.h+arrowSize, v:hAxisV+arrowSize}); | |
| painter.lineTo(ctx, {h:hAxisStart.h+arrowSize, v:hAxisV-arrowSize}); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.beginPath(); | |
| painter.moveTo(ctx, hAxisEnd); | |
| painter.lineTo(ctx, {h:hAxisEnd.h-arrowSize, v:hAxisV+arrowSize}); | |
| painter.lineTo(ctx, {h:hAxisEnd.h-arrowSize, v:hAxisV-arrowSize}); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| // draw axis labels | |
| var p = painter.transform({x:axisLen-1, y:-1}); | |
| ctx.font = "italic 1em serif"; | |
| ctx.textBaseline='middle'; | |
| ctx.textAlign='right'; | |
| ctx.fillText("x",p.x,p.y); | |
| p = painter.transform({x:-1, y:axisLen-1}); | |
| ctx.textAlign='left'; | |
| ctx.fillText("y",p.x,p.y); | |
| if (silhouette) { | |
| p = painter.transform({h:hAxisEnd.h, v:hAxisV-1}); | |
| ctx.textAlign='right'; | |
| ctx.fillText("h",p.x,p.y); | |
| } | |
| // draw axis ranges for each block | |
| var i,len,bounds,color,rgb,minp,maxp; | |
| var s = 0.25; | |
| for (i=0, len=blocks.length; i<len; i++) { | |
| bounds = silhouette ? camera.getIsoBounds(blocks[i]) : blocks[i].getBounds(); | |
| color = blocks[i].color.medium; | |
| rgb = hexToRgb(color); | |
| tcolor = "rgba("+rgb+",0.7)"; | |
| // alternate which side of the axis the range bar is on. | |
| s*=-1; | |
| // draw x axis range | |
| painter.fillQuad(ctx, | |
| {x:bounds.xmin, y:0}, | |
| {x:bounds.xmin, y:s}, | |
| {x:bounds.xmax, y:s}, | |
| {x:bounds.xmax, y:0}, | |
| tcolor | |
| ); | |
| // draw y axis range | |
| painter.fillQuad(ctx, | |
| {x:0, y:bounds.ymin}, | |
| {x:s, y:bounds.ymin}, | |
| {x:s, y:bounds.ymax}, | |
| {x:0, y:bounds.ymax}, | |
| tcolor | |
| ); | |
| if (silhouette) { | |
| painter.fillQuad(ctx, | |
| {h:bounds.hmin, v:hAxisV+s}, | |
| {h:bounds.hmax, v:hAxisV+s}, | |
| {h:bounds.hmax, v:hAxisV}, | |
| {h:bounds.hmin, v:hAxisV}, | |
| tcolor | |
| ); | |
| } | |
| } | |
| } | |
| // draw a pseudo-shaded isometric block. | |
| function drawBlock(block) { | |
| var color = block.color; | |
| // get aliases for each of the block's vertices relative to camera's perspective. | |
| var b = camera.getIsoNamedSpaceVerts(block); | |
| if (silhouette) { | |
| var rgb = hexToRgb(color.medium); | |
| var tcolor = "rgba("+rgb+",0.7)"; | |
| ctx.beginPath(); | |
| painter.moveTo(ctx, b.frontDown); | |
| painter.lineTo(ctx, b.leftDown); | |
| painter.lineTo(ctx, b.leftUp); | |
| painter.lineTo(ctx, b.backUp); | |
| painter.lineTo(ctx, b.rightUp); | |
| painter.lineTo(ctx, b.rightDown); | |
| ctx.fillStyle = tcolor; | |
| ctx.fill(); | |
| } | |
| else { | |
| // fill in the grout for the inside edges | |
| var lineWidth = 1; | |
| var groutColor = color.medium; | |
| painter.line(ctx, b.leftUp, b.frontUp, groutColor, lineWidth); | |
| painter.line(ctx, b.rightUp, b.frontUp, groutColor, lineWidth); | |
| painter.line(ctx, b.frontDown, b.frontUp, groutColor, lineWidth); | |
| // Do not add line width when filling faces. | |
| // This prevents a perimeter padding around the hexagon. | |
| // Nonzero line width could cause the perimeter of another box | |
| // to bleed over the edge of a box in front of it. | |
| lineWidth = 0; | |
| // fill each visible face of the block. | |
| // left face | |
| painter.fillQuad(ctx, b.frontDown, b.leftDown, b.leftUp, b.frontUp, !silhouette ? color.dark : color.medium, lineWidth); | |
| // top face | |
| painter.fillQuad(ctx, b.frontUp, b.leftUp, b.backUp, b.rightUp, !silhouette ? color.light : color.medium, lineWidth); | |
| // right face | |
| painter.fillQuad(ctx, b.frontDown, b.frontUp, b.rightUp, b.rightDown, color.medium, lineWidth); | |
| } | |
| }; | |
| // draw a plane to separate two isometric blocks. | |
| function drawSeparationPlane(frontBlock, backBlock) { | |
| // exit if back plane is not present | |
| if (!backBlock) { | |
| return; | |
| } | |
| var bounds = frontBlock.getBounds(); | |
| // get axis of separation | |
| var aAxis = camera.getSpaceSepAxis(frontBlock, backBlock); | |
| var bAxis,cAxis; | |
| // aAxis, bAxis, cAxis are either 'x', 'y', or 'z' | |
| // a, b, c are the values of its respective axis. | |
| // determine what our abstract axes correspond to. | |
| if (aAxis == 'x') { | |
| a = bounds.xmax; | |
| bAxis = 'y'; | |
| cAxis = 'z'; | |
| } | |
| else if (aAxis == 'y') { | |
| a = bounds.ymax; | |
| bAxis = 'x'; | |
| cAxis = 'z'; | |
| } | |
| else if (aAxis == 'z') { | |
| a = bounds.zmin; | |
| bAxis = 'x'; | |
| cAxis = 'y'; | |
| } | |
| // the radius (read margin) of the separation plane). | |
| var r = 0.7; | |
| // the points of the separation plane in abstract coords. | |
| var pts = [ | |
| { a:a, b: bounds[bAxis+"min"]-r, c: bounds[cAxis+"min"]-r }, | |
| { a:a, b: bounds[bAxis+"min"]-r, c: bounds[cAxis+"max"]+r }, | |
| { a:a, b: bounds[bAxis+"max"]+r, c: bounds[cAxis+"max"]+r }, | |
| { a:a, b: bounds[bAxis+"max"]+r, c: bounds[cAxis+"min"]-r }, | |
| ]; | |
| // convert abstract coords to the real coords for this block. | |
| var i; | |
| var finalPts = []; | |
| for (i=0; i<4; i++) { | |
| var p = {}; | |
| p[aAxis] = pts[i].a; | |
| p[bAxis] = pts[i].b; | |
| p[cAxis] = pts[i].c; | |
| finalPts.push(p); | |
| } | |
| // draw separation plane. | |
| painter.fillQuad(ctx, finalPts[0], finalPts[1], finalPts[2], finalPts[3], "rgba(0,0,0,0.35)"); | |
| painter.strokeQuad(ctx, finalPts[0], finalPts[1], finalPts[2], finalPts[3], "rgba(0,0,0,0.9)", 1); | |
| }; | |
| // draw grid | |
| drawGrid(); | |
| // draw axes | |
| if (shouldDrawAxes) { | |
| drawAxes(); | |
| } | |
| // sort blocks in drawing order. | |
| if (shouldSortBlocks) { | |
| blocks = IsoBlock.sortBlocks(blocks, camera); | |
| } | |
| // draw blocks and a separation plane if applicable. | |
| var i,len; | |
| for(i=0,len=blocks.length; i<len; i++) { | |
| // only draw a separation plane for the last block | |
| // and only if there is a block behind it. | |
| if (shouldDrawPlane && i>0 && i==len-1) { | |
| drawSeparationPlane(blocks[i], blocks[i-1]); | |
| } | |
| // draw block | |
| drawBlock(blocks[i]); | |
| } | |
| }; | |
| // Allows us to paint shapes using isometric coordinates transformed by a given camera. | |
| // It's basically a wrapper for the canvas context. | |
| IsoBlock.Painter = function(camera) { | |
| this.camera = camera; | |
| }; | |
| IsoBlock.Painter.prototype = { | |
| // This function allows us to draw using different coordinate systems. | |
| // It accepts a nondescript position vector and tries to detect | |
| // what coordinate system it is in by looking at its properties. | |
| // (x,y,z) <- treated as a space coordinate | |
| // (x,y) <- treated as a space coordinate at z=0 | |
| // (same as 2D isometric XY) | |
| // (h,v) <- treated as a special 2D isometric coordinate | |
| // (with horizontal and vertical distance from origin) | |
| transform: function(pos) { | |
| var x,y,z; | |
| if (pos.x != undefined && pos.y != undefined) { | |
| x = pos.x; | |
| y = pos.y; | |
| z = (pos.z == undefined) ? 0 : pos.z; | |
| return this.camera.spaceToScreen({x:x, y:y, z:z}); | |
| } | |
| else if (pos.h != undefined && pos.v != undefined) { | |
| return this.camera.isoToScreen(pos); | |
| } | |
| else { | |
| console.log("x",pos.x,"y",pos.y,"z",pos.z,"h",pos.h,"v",pos.v); | |
| throw "painter.transform: Unable to detect coordinate system of given vector"; | |
| } | |
| }, | |
| moveTo: function(ctx, pos) { | |
| var v = this.transform(pos); | |
| ctx.moveTo(v.x,v.y); | |
| }, | |
| lineTo: function(ctx, pos) { | |
| var v = this.transform(pos); | |
| ctx.lineTo(v.x,v.y); | |
| }, | |
| quad: function(ctx, p1, p2, p3, p4) { | |
| this.moveTo(ctx, p1); | |
| this.lineTo(ctx, p2); | |
| this.lineTo(ctx, p3); | |
| this.lineTo(ctx, p4); | |
| }, | |
| fillQuad: function(ctx, p1, p2, p3, p4, color, lineWidth) { | |
| ctx.beginPath(); | |
| this.quad(ctx,p1,p2,p3,p4); | |
| ctx.closePath(); | |
| ctx.fillStyle = color; | |
| ctx.fill(); | |
| if (lineWidth) { | |
| ctx.lineWidth = lineWidth; | |
| ctx.strokeStyle = color; | |
| ctx.stroke(); | |
| } | |
| }, | |
| fillQuadGradient: function(ctx, p1, p2, p3, p4, color1, color2) { | |
| var v1 = this.transform(p1); | |
| var v4 = this.transform(p4); | |
| var v2 = this.transform(p2); | |
| var dx = v4.x-v1.x; | |
| var dy = v4.y-v1.y; | |
| var dist = Math.sqrt(dx*dx+dy*dy); | |
| dx /= dist; | |
| dy /= dist; | |
| var dx2 = v2.x-v1.x; | |
| var dy2 = v2.y-v1.y; | |
| dist = Math.sqrt(dx2*dx2+dy2*dy2); | |
| dx *= dist; | |
| dy *= dist; | |
| //var grad = ctx.createLinearGradient(v1.x, v1.y, v2.x, v2.y); | |
| var grad = ctx.createLinearGradient(v1.x,v1.y, v1.x-dy, v1.y+dx); | |
| grad.addColorStop(0, color1); | |
| grad.addColorStop(1, color2); | |
| this.fillQuad(ctx, p1,p2,p3,p4, grad); | |
| }, | |
| strokeQuad: function(ctx, p1, p2, p3, p4, color, lineWidth) { | |
| ctx.beginPath(); | |
| this.quad(ctx,p1,p2,p3,p4); | |
| ctx.closePath(); | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = lineWidth; | |
| ctx.lineJoin = "round"; | |
| ctx.stroke(); | |
| }, | |
| line: function(ctx, p1, p2, color, lineWidth) { | |
| ctx.beginPath(); | |
| this.moveTo(ctx, p1); | |
| this.lineTo(ctx, p2); | |
| ctx.strokeStyle = color; | |
| ctx.lineCap = 'butt'; | |
| ctx.lineWidth = lineWidth; | |
| ctx.stroke(); | |
| }, | |
| fillCircle: function(ctx, p1, radius, color) { | |
| var v = this.transform(p1); | |
| ctx.beginPath(); | |
| ctx.arc(v.x,v.y,radius,0,2*Math.PI); | |
| ctx.fillStyle = color; | |
| ctx.fill(); | |
| }, | |
| strokeCircle: function(ctx, p1, radius, color) { | |
| var v = this.transform(p1); | |
| ctx.beginPath(); | |
| ctx.arc(v.x,v.y,radius,0,2*Math.PI); | |
| ctx.fillStyle = color; | |
| ctx.fill(); | |
| }, | |
| }; |
| // From kennebec at http://stackoverflow.com/a/3955096/142317 | |
| // Add a remove value function to the Array class. | |
| Array.prototype.remove = function() { | |
| var what, a = arguments, L = a.length, ax; | |
| while (L && this.length) { | |
| what = a[--L]; | |
| while ((ax = this.indexOf(what)) !== -1) { | |
| this.splice(ax, 1); | |
| } | |
| } | |
| return this; | |
| }; | |
| // Sort blocks in the order that they should be drawn for the given camera. | |
| IsoBlock.sortBlocks = function(blocks, camera) { | |
| var i, j, numBlocks=blocks.length; | |
| // Initialize the list of blocks that each block is behind. | |
| for (i=0; i<numBlocks; i++) { | |
| blocks[i].blocksBehind = []; | |
| blocks[i].blocksInFront = []; | |
| } | |
| // For each pair of blocks, determine which is in front and behind. | |
| var a,b,frontBlock; | |
| for (i=0; i<numBlocks; i++) { | |
| a = blocks[i]; | |
| for (j=i+1; j<numBlocks; j++) { | |
| b = blocks[j]; | |
| frontBlock = camera.getFrontBlock(a,b); | |
| if (frontBlock) { | |
| if (a == frontBlock) { | |
| a.blocksBehind.push(b); | |
| b.blocksInFront.push(a); | |
| } | |
| else { | |
| b.blocksBehind.push(a); | |
| a.blocksInFront.push(b); | |
| } | |
| } | |
| } | |
| } | |
| // Get list of blocks we can safely draw right now. | |
| // These are the blocks with nothing behind them. | |
| var blocksToDraw = []; | |
| for (i=0; i<numBlocks; i++) { | |
| if (blocks[i].blocksBehind.length == 0) { | |
| blocksToDraw.push(blocks[i]); | |
| } | |
| } | |
| // While there are still blocks we can draw... | |
| var blocksDrawn = []; | |
| while (blocksToDraw.length > 0) { | |
| // Draw block by removing one from "to draw" and adding | |
| // it to the end of our "drawn" list. | |
| var block = blocksToDraw.pop(); | |
| blocksDrawn.push(block); | |
| // Tell blocks in front of the one we just drew | |
| // that they can stop waiting on it. | |
| for (j=0; j<block.blocksInFront.length; j++) { | |
| var frontBlock = block.blocksInFront[j]; | |
| // Add this front block to our "to draw" list if there's | |
| // nothing else behind it waiting to be drawn. | |
| frontBlock.blocksBehind.remove(block); | |
| if (frontBlock.blocksBehind.length == 0) { | |
| blocksToDraw.push(frontBlock); | |
| } | |
| } | |
| } | |
| return blocksDrawn; | |
| }; |
Very nice demo. It looks like there's an issue in
index.html'sdrawCustom()method though, where it's using:.attr("y", function(d){return d.dy})instead of:
.attr("y", function(d){return d.y})With this fix, the initial blocks need to be changed to the following to keep their original positions:
I've forked this for the fix.
See result.