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.