[
  {
    "path": "LICENSE",
    "content": "Copyright 2019 Shaun Lebron\n\nPermission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "Isometric Blocks\n================\n\n[A tutorial on how to render isometric blocks in the correct order](http://shaunlebron.github.io/IsometricBlocks)\n\n[![preview](preview-iso.png)](http://shaunlebron.github.io/IsometricBlocks)\n"
  },
  {
    "path": "index.htm",
    "content": "\n<!doctype html>\n<html>\n<head>\n\t<meta charset=\"utf8\">\n\t<style>\n/*------------------------------------------------------------------------------\n * Open Sans\n *----------------------------------------------------------------------------*/\n\n@font-face {\n  font-family: 'Open Sans';\n  font-style: normal;\n  font-weight: 300;\n  src: url('../fonts/opensans/OpenSans-Light.eot');\n  src: local('Open Sans Light'), local('OpenSans-Light'),\n    url('../fonts/opensans/OpenSans-Light.eot') format('embedded-opentype'),\n    url('../fonts/opensans/OpenSans-Light.woff') format('woff');\n}\n\n@font-face {\n  font-family: 'Open Sans';\n  font-style: normal;\n  font-weight: 400;\n  src: url('../fonts/opensans/OpenSans.eot');\n  src: local('Open Sans'), local('OpenSans'),\n    url('../fonts/opensans/OpenSans.eot') format('embedded-opentype'),\n    url('../fonts/opensans/OpenSans.woff') format('woff');\n}\n\n@font-face {\n  font-family: 'Open Sans';\n  font-style: normal;\n  font-weight: bold;\n  src: url('../fonts/opensans/OpenSans-Bold.eot');\n  src: local('Open Sans Bold'), local('OpenSans-Bold'),\n    url('../fonts/opensans/OpenSans-Bold.eot') format('embedded-opentype'),\n    url('../fonts/opensans/OpenSans-Bold.woff') format('woff');\n}\n\n@font-face {\n  font-family: 'Open Sans';\n  font-style: italic;\n  font-weight: 300;\n  src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'),\n    url('../fonts/opensans/OpenSansLight-Italic.eot') format('embedded-opentype'),\n    url('../fonts/opensans/OpenSansLight-Italic.woff') format('woff');\n}\n\n@font-face {\n  font-family: 'Open Sans';\n  font-style: italic;\n  font-weight: 400;\n  src: local('Open Sans Italic'), local('OpenSans-Italic'),\n    url('../fonts/opensans/OpenSans-Italic.eot') format('embedded-opentype'),\n    url('../fonts/opensans/OpenSans-Italic.woff') format('woff');\n}\n\n@font-face {\n  font-family: 'Open Sans';\n  font-style: italic;\n  font-weight: bold;\n  src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),\n    url('../fonts/opensans/OpenSans-BoldItalic.eot') format('embedded-opentype'),\n    url('../fonts/opensans/OpenSans-BoldItalic.woff') format('woff');\n}\n\n\n\t\thr {\n\t\t\tmargin-top:3em;\n\t\t\tmargin-bottom:3em;\n\t\t}\n\t\tbody {\n\t\t\twidth: 800px;\n\t\t\tmargin-left:auto;\n\t\t\tmargin-right:auto;\n      font-family: 'Open Sans';\n\t\t\tfont-size: 1.2em;\n\t\t}\n\t\tcanvas {\n\t\t\tbackground-color: #f5f5f5;\n\t\t\tmargin-right: 5px;\n\t\t\ttext-align: center;\n      border: 1px solid #d7d7d7;\n\t\t}\n\t\t#abstract {\n\t\t\tfont-style: italic;\n\t\t}\n\t\tfigure {\n      margin: 2em 2em;\n\t\t}\n\t\tfigcaption {\n\t\t\tfont-style: italic;\n\t\t\tcolor: #555;\n\t\t}\n\t\tcode.block {\n\t\t\twhite-space: pre;\n\t\t\tdisplay:block;\n\t\t\tmargin-top:3em;\n\t\t\tmargin-bottom:3em;\n\t\t\tmargin-left: auto;\n\t\t\tmargin-right: auto;\n\t\t\twidth: 650px;\n\t\t\tpadding: 1.5em;\n\t\t\tbackground: #f5f5f5;\n\t\t}\n    h1 {\n      font-size: 32px;\n    }\n    h1, h2, h3, h4 {\n      margin-top: 3em;\n      margin-bottom: -1em;\n      color: #333;\n      font-weight: 600;\n    }\n    p {\n      margin: 3em 0;\n      color: #555;\n      line-height: 1.6em;\n    }\n    figcaption {\n      opacity: 0.6;\n    }\n\t\tp.alert{\n\t\t\tborder-radius: 5px;\n\t\t\tbox-shadow: 0px 0px 2px #000;\n\t\t\tbackground-color: #ef2929;\n\t\t\tcolor: white;\n\t\t\tpadding: 1em;\n\t\t}\n\n\t\t/* from vim's TOhtml command for syntax highlighting code */\n\t\t.lnr { color: #ff0000; }\n\t\t.Constant { color: #ef2929; }\n\t\t.Statement { color: #3465a4; }\n\t\t.Comment { color: #4e9a06; }\n\t\t.Identifier { color: #3465a4; }\n\t</style>\n\n\t<script src='src/main.js'></script>\n\t<script src='src/colors.js'></script>\n\t<script src='src/block.js'></script>\n\t<script src='src/vector.js'></script>\n\t<script src='src/camera.js'></script>\n\t<script src='src/painter.js'></script>\n\t<script src='src/sortBlocks.js'></script>\n</head>\n<body>\n<a href=\"https://github.com/shaunew/IsometricBlocks\"><img style=\"position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_red_aa0000.png\" alt=\"Fork me on GitHub\"></a>\n\n\n\t<h1>Drawing isometric boxes in the correct order</h1>\n\n\t<p>\nIn an <a href=\"http://en.wikipedia.org/wiki/Isometric_projection\">isometric</a>\ndisplay, it can be tricky to draw boxes of various sizes in the correct order\nto keep them appropriately in front of or behind one another.  The figure below\nshows an example.  The blue box should be drawn first, then green, then red.\n\t</p>\n\n\t<figure>\n\t\t<canvas id='figure1a' width=350 height=200></canvas>\n\t\t<canvas id='figure1b' width=350 height=200></canvas>\n\t\t<figcaption>\nFigure 1: The boxes on the left are <u>not</u> drawn in the correct order, whereas the\nboxes on the right are drawn correctly.\n\t\t</figcaption>\n\t\t<script>\n\t\t\tvar blocks = [\n\t\t\t\tnew IsoBlock.Block({x:1,y:3,z:0},{x:2,y:2,z:2.5}, IsoBlock.colors.green),\n\t\t\t\tnew IsoBlock.Block({x:2,y:2,z:0},{x:1,y:1,z:1.5}, IsoBlock.colors.red),\n\t\t\t\tnew IsoBlock.Block({x:3,y:1,z:0},{x:1,y:4,z:1},   IsoBlock.colors.blue),\n\t\t\t];\n\t\t\tIsoBlock.makeFigure({ canvas:'figure1a', blocks: blocks, sortBlocks: false, });\n\t\t\tIsoBlock.makeFigure({ canvas:'figure1b', blocks: blocks, });\n\t\t</script>\n\t</figure>\n\n\t<p>\nWe will explore a simple solution for determining the correct order to draw a\ngiven set of boxes.  But first, we must define what we mean by <em>boxes</em>.\n\t</p>\n\n\t<h3>What do we mean by <em>boxes</em>?</h3>\n\n\t<p>\nWe define boxes as <em>axis-aligned</em> and <em>non-intersecting</em>\nrectangular prisms. Take a look at the above Figure 1 again.  Each box is\nparallel to the <em>x</em>, <em>y</em>, and <em>z</em> axis (i.e.\naxis-aligned).  Also, note that the boxes are next to each other but do not\nintersect.\n\t</p>\n\n\t<h3>Determine if boxes overlap on screen.</h3>\n\n\t<p>\nFirst of all, if two boxes do not overlap on the screen, then we do not have to\nworry about which one is drawn first.  This is the first test we must perform,\nwhich we explore in this section.\n\t</p>\n\n\t<figure>\n\t\t<canvas id='figure2a' width=350 height=200></canvas>\n\t\t<canvas id='figure2b' width=350 height=200></canvas>\n\t\t<figcaption>\nFigure 2: No overlap on the left; overlap on the right.  (Note: we are talking\nabout overlap on screen, not intersection in space.)\n\t\t</figcaption>\n\t\t<script>\n\t\t\tvar rightBlock =   new IsoBlock.Block({x:4,y:1,z:0},{x:2,y:2,z:2}, IsoBlock.colors.blue);\n\t\t\tvar leftBlock =  new IsoBlock.Block({x:2,y:4,z:0},{x:2.25,y:2,z:1}, IsoBlock.colors.red);\n\t\t\tvar rightBlockOverlap =   new IsoBlock.Block({x:3,y:2,z:0},{x:2,y:2,z:2}, IsoBlock.colors.blue);\n\t\t\tvar leftBlockOverlap =  new IsoBlock.Block({x:2,y:4,z:0},{x:2.25,y:2,z:1}, IsoBlock.colors.green);\n\t\t\tvar scale = function(w,h) { return h/9;};\n\t\t\tIsoBlock.makeFigure({ canvas:'figure2a',\n\t\t\t\tblocks: [ leftBlock, rightBlock ],\n\t\t\t\tscale: scale,\n\t\t\t});\n\t\t\tIsoBlock.makeFigure({ canvas:'figure2b',\n\t\t\t\tblocks: [ leftBlockOverlap, rightBlockOverlap ],\n\t\t\t\tscale: scale,\n\t\t\t});\n\t\t</script>\n\t</figure>\n\n\t<p>\nThe silhouettes of the 3D boxes become 2D hexagons in the isometric view, as seen below.  We use the\noutline of these silhouettes to test for overlap.\n\t</p>\n\n\t<figure>\n\t\t<canvas id='figure3a' width=350 height=200></canvas>\n\t\t<canvas id='figure3b' width=350 height=200></canvas>\n\t\t<figcaption>\nFigure 3: The box silhouettes in an isometric view are simple hexagons.  Note\nthat their sides are always parallel to the vertical and two diagonal axes.\n\t\t</figcaption>\n\t\t<script>\n\t\t\tIsoBlock.makeFigure({ canvas:'figure3a',\n\t\t\t\tblocks: [ leftBlock, rightBlock ],\n\t\t\t\tscale: scale,\n\t\t\t\tsilhouette: true,\n\t\t\t});\n\t\t\tIsoBlock.makeFigure({ canvas:'figure3b',\n\t\t\t\tblocks: [ leftBlockOverlap, rightBlockOverlap ],\n\t\t\t\tscale: scale,\n\t\t\t\tsilhouette: true,\n\t\t\t});\n\t\t</script>\n\t</figure>\n\n\t<p>\nWe take advantage of the fact that the hexagon sides are always parallel to\nsome axis.  This allows us to easily determine if the hexagons overlap by\nchecking for intersection of their regions on each axis.  We add an <em>h</em>\n(horizontal) axis to help.\n\t</p>\n\n\t<figure>\n\t\t<canvas id='figure4a' width=350 height=200></canvas>\n\t\t<canvas id='figure4b' width=350 height=200></canvas>\n\t\t<figcaption>\nThe red and blue boxes do not overlap on the h axis, therefore they do not overlap.\nThe green and blue boxes do overlap since their region on every axis overlap.\n\t\t</figcaption>\n\t\t<script>\n\t\t\tIsoBlock.makeFigure({ canvas:'figure4a',\n\t\t\t\tblocks: [ leftBlock, rightBlock ],\n\t\t\t\tdrawAxis: true,\n\t\t\t\tsilhouette: true,\n\t\t\t\tscale: scale,\n\t\t\t});\n\t\t\tIsoBlock.makeFigure({ canvas:'figure4b',\n\t\t\t\tblocks: [ leftBlockOverlap, rightBlockOverlap ],\n\t\t\t\tdrawAxis: true,\n\t\t\t\tsilhouette: true,\n\t\t\t\tscale: scale,\n\t\t\t});\n\t\t</script>\n\t</figure>\n\n\t<p>\nNow that we have outlined our concept for <em>determining if two boxes overlap\non the screen</em>, we will fill in the details necessary for implementing it.\n\t</p>\n\n\t<p>\nThe act of flattening the 3D box into a 2D hexagon involves getting rid of the\nZ coordinate.  Notice that increasing a point's Z coordinate by 1 is the same\nas incrementing both X and Y coordinates by 1.  Thus, we can add Z to both X\nand Y and drop Z completely.  Shown below is the source code for a function\nthat performs this conversion.\n\t</p>\n\n<code class='block'><span class=\"Comment\">// Convert a 3D space position to a 2D isometric position.</span>\n<span class=\"Identifier\">function</span> spaceToIso(spacePos) <span class=\"Identifier\">{</span>\n\n    <span class=\"Comment\">// New XY position simply adds Z to X and Y.</span>\n    <span class=\"Identifier\">var</span> isoX = spacePos.x + spacePos.z;\n    <span class=\"Identifier\">var</span> isoY = spacePos.y + spacePos.z;\n\n    <span class=\"Statement\">return</span> <span class=\"Identifier\">{</span>\n        x: isoX,\n        y: isoY,\n\n        <span class=\"Comment\">// Compute horizontal distance from origin.</span>\n        h: (isoX - isoY) * Math.cos(Math.PI/6),\n\n        <span class=\"Comment\">// Compute vertical distance from origin.</span>\n        v: (isoX + isoY) / 2;\n    <span class=\"Identifier\">}</span>;\n<span class=\"Identifier\">}</span></code>\n\n\t<p>\nAnd finally, after determining the bounds of each hexagon, we can determine if\nthey overlap by using the source code below.\n\t</p>\n\n<code class='block'><span class=\"Identifier\">function</span> doHexagonsOverlap(hex1, hex2) <span class=\"Identifier\">{</span>\n    <span class=\"Comment\">// Hexagons overlap if and only if all axis regions overlap.</span>\n    <span class=\"Statement\">return</span> (\n\n        <span class=\"Comment\">// test if x regions intersect.</span>\n        !(hex1.xmin &gt;= hex2.xmax || hex2.xmin &gt;= hex1.xmax) &amp;&amp;\n\n        <span class=\"Comment\">// test if y regions intersect.</span>\n        !(hex1.ymin &gt;= hex2.ymax || hex2.ymin &gt;= hex1.ymax) &amp;&amp;\n\n        <span class=\"Comment\">// test if h regions intersect.</span>\n        !(hex1.hmin &gt;= hex2.hmax || hex2.hmin &gt;= hex1.hmax));\n<span class=\"Identifier\">}</span></code>\n\n\t<p>\nNow that we have determined if two boxes overlap on the screen, we can begin exploring how to determine which box is in front of the other.\n\t</p>\n\n\t<h3>Determine which box is in front.</h3>\n\n\t<p>\nRecall that our boxes do not intersect each other. we can visualize their separation\nas a thin plane between them (see Figure 5 below).  After identifying this\nplane, we can determine which box is in front by selecting the one on the\ncorrect side of this plane.\n\t</p>\n\n\t<figure>\n\t\t<canvas id='figure5a' width=230 height=200></canvas>\n\t\t<canvas id='figure5b' width=230 height=200></canvas>\n\t\t<canvas id='figure5c' width=230 height=200></canvas>\n\t\t<figcaption>\nFigure 5: A pair of blocks can be separated in one of three ways shown here.\nThe dark glass illustrates this separation.\n\t\t</figcaption>\n\t\t<script>\n\t\t\tvar refBlock =   new IsoBlock.Block({x:3,y:2,z:0},{x:2,y:2,z:2}, IsoBlock.colors.blue);\n\t\t\tvar leftBlock =  new IsoBlock.Block({x:2,y:4,z:0},{x:2.25,y:2,z:1}, IsoBlock.colors.red);\n\t\t\tvar topBlock =   new IsoBlock.Block({x:4,y:2,z:2},{x:1.25,y:2,z:1}, IsoBlock.colors.orange);\n\t\t\tvar frontBlock = new IsoBlock.Block({x:1,y:1,z:0},{x:2,y:2,z:1}, IsoBlock.colors.green);\n\t\t\tIsoBlock.makeFigure({ canvas:'figure5a', drawPlane: true,\n\t\t\t\tblocks: [ refBlock, leftBlock ],\n\t\t\t});\n\t\t\tIsoBlock.makeFigure({ canvas:'figure5b', drawPlane: true,\n\t\t\t\tblocks: [ refBlock, topBlock ],\n\t\t\t});\n\t\t\tIsoBlock.makeFigure({ canvas:'figure5c', drawPlane: true,\n\t\t\t\tblocks: [ refBlock, frontBlock ],\n\t\t\t});\n\t\t</script>\n\t</figure>\n\n\t<p>\nWe can find this plane of separation by looking at each axis individually.  In\nparticular, we look for an axis which has non-intersecting box ranges (see\nFigure 6 below).\n\t</p>\n\n\t<figure>\n\t\t<canvas id='figure6a' width=350 height=200></canvas>\n\t\t<canvas id='figure6b' width=350 height=200></canvas>\n\t\t<figcaption>\nFigure 6: On the left, the blocks are separated on the y-axis.  On the right,\nthe blocks are separated on the x-axis. (The z-axis is omitted for simplicity.)\n\t\t</figcaption>\n\t\t<script>\n\t\t\tIsoBlock.makeFigure({ canvas:'figure6a',\n\t\t\t\tblocks: [ refBlock, leftBlock ],\n\t\t\t\tdrawAxis: true,\n\t\t\t});\n\t\t\tIsoBlock.makeFigure({ canvas:'figure6b',\n\t\t\t\tblocks: [ refBlock, frontBlock ],\n\t\t\t\tdrawAxis: true,\n\t\t\t});\n\t\t</script>\n\t</figure>\n\n\t<p>\nIn Figure 6 above, we have chosen a coordinate system which make lesser values\nof <em>x</em> and <em>y</em> to be closer to the camera.  Though not shown, the\n<em>z</em> axis is positive in the up direction, so a greater value makes it\ncloser to the camera.\n\t</p>\n\n\t<p>\nThe following is a javascript function for determining if the first block is in\nfront of the second:\n\t</p>\n\n\t<code class='block'><span class=\"Identifier\">function</span> isBoxInFront(box1, box2) <span class=\"Identifier\">{</span>\n\n    <span class=\"Comment\">// test for intersection x-axis</span>\n    <span class=\"Comment\">// (lower x value is in front)</span>\n    <span class=\"Statement\">if</span> (box1.xmin &gt;= box2.xmax) <span class=\"Identifier\">{</span> <span class=\"Statement\">return</span> <span class=\"Constant\">false</span>; <span class=\"Identifier\">}</span>\n    <span class=\"Statement\">else</span> <span class=\"Statement\">if</span> (box2.xmin &gt;= box1.xmax) <span class=\"Identifier\">{</span> <span class=\"Statement\">return</span> <span class=\"Constant\">true</span>; <span class=\"Identifier\">}</span>\n\n    <span class=\"Comment\">// test for intersection y-axis</span>\n    <span class=\"Comment\">// (lower y value is in front)</span>\n    <span class=\"Statement\">if</span> (box1.ymin &gt;= box2.ymax) <span class=\"Identifier\">{</span> <span class=\"Statement\">return</span> <span class=\"Constant\">false</span>; <span class=\"Identifier\">}</span>\n    <span class=\"Statement\">else</span> <span class=\"Statement\">if</span> (box2.ymin &gt;= box1.ymax) <span class=\"Identifier\">{</span> <span class=\"Statement\">return</span> <span class=\"Constant\">true</span>; <span class=\"Identifier\">}</span>\n\n    <span class=\"Comment\">// test for intersection z-axis</span>\n    <span class=\"Comment\">// (higher z value is in front)</span>\n    <span class=\"Statement\">if</span> (box1.zmin &gt;= box2.zmax) <span class=\"Identifier\">{</span> <span class=\"Statement\">return</span> <span class=\"Constant\">true</span>; <span class=\"Identifier\">}</span>\n    <span class=\"Statement\">else</span> <span class=\"Statement\">if</span> (box2.zmin &gt;= box1.zmax) <span class=\"Identifier\">{</span> <span class=\"Statement\">return</span> <span class=\"Constant\">false</span>; <span class=\"Identifier\">}</span>\n\n<span class=\"Identifier\">}</span></code>\n\n\t<h3>Draw boxes in the correct order.</h3>\n\n\t<p>\nIn general, <u>a box should not be drawn until all the ones behind it are\ndrawn</u>.  Thus, we begin by drawing the boxes that have nothing behind them.\nThen, we can draw the boxes that are only in front of those that are already\ndrawn. This process continues until all boxes are drawn. (See Figure 4 below\nfor an example.)\n\t</p>\n\n\t<figure>\n\t\t<canvas id='figure7' width=650 height=200></canvas>\n\t\t<figcaption>\nFigure 4: (1) Nothing is behind blue, so draw it first. (2) Draw green next\nsince blue was the only one behind it and is already drawn.  (3) Then draw red,\nsince both blocks that were behind it have been drawn.\n\t\t</figcaption>\n\t\t<script>\n\t\t\tvar blocks = [\n\t\t\t\tnew IsoBlock.Block({x:1,y:3,z:0},{x:2,y:2,z:2.5}, IsoBlock.colors.green),\n\t\t\t\tnew IsoBlock.Block({x:2,y:2,z:0},{x:1,y:1,z:1.5}, IsoBlock.colors.red),\n\t\t\t\tnew IsoBlock.Block({x:3,y:1,z:0},{x:1,y:4,z:1},   IsoBlock.colors.blue),\n\t\t\t];\n\t\t\tIsoBlock.makeFigure({ canvas:'figure7', blocks: blocks, scale: function(w,h) { return h/7; }});\n\t\t</script>\n\t</figure>\n\n\t<p>\nTo implement this algorithm, each box must know exactly which boxes are behind\nit.  We have already determined how to do this in the last section.  A search\nmust be implemented so that each box has a list of boxes behind it.\n\t</p>\n\n\t<p>\nYou are now armed with everything you need to know to render isometric boxes in\nthe correct order.\n\t</p>\n\n\t<h3>A conundrum</h3>\n\n\t<p>\nIt is possible to have a situation seen in the figure below.  The aforementioned drawing\nmethods dictate that we first draw the box with nothing behind it, but this example illustrates\na case where this cannot be done.\n\t</p>\n\n\t<figure>\n\t\t<canvas id='figure8' width=650 height=200></canvas>\n\t\t<figcaption>\nHere are three boxes intertwined in a way such that one is always behind\nanother.  This prevents us from drawing a first box.\n\t\t</figcaption>\n\t\t<script>\n\t\tvar blocks = [\n\t\t\tnew IsoBlock.Block({x:1,y:1,z:0},{x:2,y:1,z:1}, IsoBlock.colors.purple),\n\t\t\tnew IsoBlock.Block({x:2,y:1,z:1},{x:1,y:2,z:1}, IsoBlock.colors.red),\n\t\t\tnew IsoBlock.Block({x:1,y:2,z:0},{x:1,y:1,z:1}, IsoBlock.colors.orange),\n\t\t\tnew IsoBlock.Block({x:1,y:2,z:1},{x:1,y:1,z:1}, IsoBlock.colors.orange),\n\t\t];\n\t\tIsoBlock.makeFigure({ canvas:'figure8', blocks: blocks, scale: function(w,h) { return h/6;} });\n\t\t</script>\n\t</figure>\n\n\t<p>\nThe figure above cheats by segmenting the orange box into two.\nThis is one method of breaking this type of cycle.\n\t</p>\n\n\t<p>\nThere are formal methods used for detecting\nsuch cycles mentioned in the appendix.  After detection of a cycle, the blocks in that cycle\ncould be drawn with special clipping regions to respect front boxes or to segment a block or blocks\nthat will break the cycle.  These are solutions that I will be exploring and updating this article as\nmy experiments progress.\n\t</p>\n\n\t<hr>\n\t<h2>Appendix</h2>\n\n\t<h4>A formal description of the solution</h4>\n\n\t<p>\nThis is a special case of the <a href=\"https://en.wikipedia.org/wiki/Painter%27s_algorithm\">Painter's Algorithm</a>,\nwhich handles occlusion by drawing back-to-front.\n\t</p>\n\n\t<p>\nFor those who are interested, our method for determining if hexagons and boxes\nare overlapping is a result of the <a href=\"http://en.wikipedia.org/wiki/Hyperplane_separation_theorem\">hyperplane separation theorem</a>.\n\t</p>\n\n\t<p>\nAlso, the way in which we determined the drawing order of the boxes is known in graph theory as a <a\nhref=\"http://en.wikipedia.org/wiki/Topological_sorting\">topological sort</a>,\nwhich is essentially a depth-first search of a directed graph.\n\t</p>\n\n\t<p>\nYou can build a directed graph of the <em>boxes</em>, with directed edges to\nthe boxes that are behind it.  Topologically sorting this graph will produce an\nordered list of boxes that can be drawn in that exact order.\n\t</p>\n\n\t<p>\nMathematicians will recognize this directed graph as a <a\nhref=\"http://en.wikipedia.org/wiki/Partially_ordered_set\">partially ordered\nset</a>.\n\t</p>\n\n\t<p>\nFinally, to prevent the aforementioned cycle conundrum, we can use <a href=\"\">Tarjan's\nstrongly connection components</a> algorithm.  After computing these cycles,\none could either split a block to prevent a cycle, or to use a clipping region\nto prevent drawing over any blocks that are supposed to be in front of it.\n\t</p>\n\n\t<h4>Alternative Solutions</h4>\n\n\t<p>\nYou may be able to just use <a href=\"https://en.wikipedia.org/wiki/Z-buffering\">Z-buffering</a>,\nthough drawing order is still important for transparent sprites.  Also, if all\nbounding boxes are unit cubes, sorting is much simpler.\n\t</p>\n\n\t<h4>Full example of working code</h4>\n\n\t<p>\nAll the diagrams above were created using a simple isometric box renderer\nwritten in Javascript, which applies all the techniques described in this\narticle.  You can study the fully annotated source code on <a\nhref=\"https://github.com/shaunew/IsometricBlocks\">IsometricBlocks project on\nGitHub</a>.\n\t</p>\n\n\t<h4>Real game examples</h4>\n\n\t<p>\n\t<ul>\n\t\t<li><a href=\"http://andrewrussell.net/2016/06/how-2-5d-sorting-works-in-river-city-ransom-underground/\">How 2.5D Sorting works in River City Ransom: Underground</a> - allowing bounding boxes to intersect by specifying heightmaps within them (<a href=\"https://news.ycombinator.com/item?id=12313271\">summary</a>)</li>\n\t\t<li><a href=\"http://bannalia.blogspot.co.uk/2008/02/filmation-math.html\">Filmation engine on the ZX Spectrum</a></li>\n\t</ul>\n\t</p>\n\n\t<h4>Thanks</h4>\n\n\t<p>\nThanks to Ted Suzman at <a href=\"http://playbuildy.com/\">buildy</a> for\nintroducing this problem and solution to me.  And thanks to adamhayek for <a\nhref=\"http://www.reddit.com/r/gamedev/comments/18222r/how_to_determine_the_draw_order_for_an_isometric/c8ayzby\">further\ninsight</a> on a general solution. And thanks to <a href=\"http://www.reddit.com/r/gamedev/comments/18bg95/tutorial_how_to_render_isometric_blocks_correctly/c8dfx51\">Slime0 at reddit</a> for pointing out errors in this article by illustrating the cycle example shown in this article, and for illustrating why we cannot deduce relative drawing order between two non-overlapping boxes.\nThanks to <a href=\"https://lobste.rs/s/bengjo/drawing_isometric_boxes_correct_order/comments/rzgvnc#c_rzgvnc\">Mark Nelson</a> for extra context on painter's algorithm and z-buffering.\n\t</p>\n\n\t<hr>\n\n\t<figure>\n\t\t<canvas id='figure5' width=700 height=200></canvas>\n\t\t<script>\n\t\t\tvar blocks = [\n\t\t\t\tnew IsoBlock.Block({x:1,y:3,z:0},{x:2,y:2,z:2.5}, IsoBlock.colors.green),\n\t\t\t\tnew IsoBlock.Block({x:2,y:2,z:0},{x:1,y:1,z:1.5}, IsoBlock.colors.red),\n\t\t\t\tnew IsoBlock.Block({x:3,y:1,z:0},{x:1,y:4,z:1},   IsoBlock.colors.blue),\n\t\t\t\tnew IsoBlock.Block({x:0.5,y:5,z:0},{x:2,y:1.5,z:1},   IsoBlock.colors.orange),\n\t\t\t\tnew IsoBlock.Block({x:3,y:3,z:1},{x:1,y:1,z:2.25},   IsoBlock.colors.black),\n\t\t\t\tnew IsoBlock.Block({x:2,y:7,z:0},{x:1,y:1,z:1},   IsoBlock.colors.white),\n\t\t\t\tnew IsoBlock.Block({x:5,y:1.5,z:0},{x:2,y:2,z:1.5}, IsoBlock.colors.purple),\n\t\t\t];\n\t\t\tIsoBlock.makeFigure({\n\t\t\t\tcanvas:'figure5',\n\t\t\t\tblocks: blocks,\n\t\t\t\tdrawAxis:true,\n\t\t\t\taxisLen: 12,\n\t\t\t});\n\t\t</script>\n\t</figure>\n\n</body>\n</html>\n"
  },
  {
    "path": "src/block.js",
    "content": "\n//\nIsoBlock.Block = function(pos,size,color) {\n\n\t// position in 3d space (obj with attrs x,y,z)\n\tthis.pos = pos;\n\n\t// size of each dimension (obj with attrs x,y,z)\n\tthis.size = size;\n\n\t// an array of 3 color shades (light,medium,dark - see colors.js)\n\t// (used for pseudo lighting)\n\tthis.color = color || IsoBlock.colors.red;\n};\n\nIsoBlock.Block.prototype = {\n\tgetBounds: function() {\n\t\tvar p = this.pos;\n\t\tvar s = this.size;\n\t\treturn {\n\t\t\txmin: p.x,\n\t\t\txmax: p.x + s.x,\n\t\t\tymin: p.y,\n\t\t\tymax: p.y + s.y,\n\t\t\tzmin: p.z,\n\t\t\tzmax: p.z + s.z,\n\t\t};\n\t},\n};\n"
  },
  {
    "path": "src/camera.js",
    "content": "\nIsoBlock.Camera = function(origin,scale) {\n\n\t// the pixel location of the isometric origin.\n\tthis.origin = origin;\n\n\t// number of pixels per isometric unit.\n\tthis.scale = scale;\n\n};\n\n/*\n\nWe have three separate coordinate systems used for different things:\n\n1. Space (3D)\n\n\tWe apply the usual 3D coordinates to define the boxes using x,y,z.\n\n2. Isometric (2D)\n\n\tWhen the 3D space is flattened into an isometric view, we use oblique x and y\n\taxes separated by 120 degrees.\n\n\tAll this does is treat all 3d coordinates as if they are at z=0.\n\n\tFor example, if use have a box at (0,0,0) and we raised it to (0,0,1), it would\n\tlook to be in the exact same position as a box at (1,1,0), so the 2d isometric\n\tcoordinates are (1,1).  This is a side effect of the isometric perspective.  So\n\tthe isometric 2D coordinates gets the \"apparent\" coordinates for all boxes if\n\tthey were at z=0.\n\n\tThis is accomplished by adding z to x and y.  That is all.\n\n\t(Isometric coordinates are useful for determining when boxes overlap on the\n\tscreen.)\n\n3. Screen (2D)\n\n\tBefore drawing, we convert the isometric coordinates to the usual x,y screen\n\tcoordinates.\n\n\tThis is done by multiplying each isometric 2D coordinate by its respective\n\toblique axis vector and taking the sum.\n\n\tWe then multiply this position by \"scale\" value to implement zoom in/out\n\tfeatures for the camera.\n\n\tThen we add to an \"origin\" to implement panning features for the camera.\n\n*/\n\nIsoBlock.Camera.prototype = {\n\n\t// Determine if the given ranges are disjoint (i.e. do not overlap).\n\t// For determining drawing order, this camera considers two\n\t// ranges to be disjoint even if they share an endpoint.\n\t// Thus, we use less-or-equal (<=) instead of strictly less (<).\n\tareRangesDisjoint: function(amin,amax,bmin,bmax) {\n\t\treturn (amax <= bmin || bmax <= amin);\n\t},\n\n\t// Convert 3D space coordinates to flattened 2D isometric coordinates.\n\t// x and y coordinates are oblique axes separated by 120 degrees.\n\t// h,v are the horizontal and vertical distances from the origin.\n\tspaceToIso: function(spacePos) {\n\t\tvar z = (spacePos.z == undefined) ? 0 : spacePos.z;\n\n\t\tvar x = spacePos.x + z;\n\t\tvar y = spacePos.y + z;\n\n\t\treturn {\n\t\t\tx: x,\n\t\t\ty: y,\n\t\t\th: (x-y)*Math.sqrt(3)/2, // Math.cos(Math.PI/6)\n\t\t\tv: (x+y)/2,              // Math.sin(Math.PI/6)\n\t\t};\n\t},\n\n\t// Convert the given 2D isometric coordinates to 2D screen coordinates.\n\tisoToScreen: function(isoPos) {\n\t\treturn {\n\t\t\tx: isoPos.h * this.scale + this.origin.x,\n\t\t\ty: -isoPos.v * this.scale + this.origin.y,\n\t\t};\n\t},\n\n\t// Convert the given 3D space coordinates to 2D screen coordinates.\n\tspaceToScreen: function(spacePos) {\n\t\treturn this.isoToScreen(this.spaceToIso(spacePos));\n\t},\n\t\n\t// Get a block's vertices with helpful aliases.\n\t// Each vertex is named from its apparent position in an isometric view.\n\tgetIsoNamedSpaceVerts: function(block) {\n\t\tvar p = block.pos;\n\t\tvar s = block.size;\n\t\t\n\t\treturn {\n\t\t\trightDown: {x:p.x+s.x, y:p.y,     z:p.z},\n\t\t\tleftDown:  {x:p.x,     y:p.y+s.y, z:p.z},\n\t\t\tbackDown:  {x:p.x+s.x, y:p.y+s.y, z:p.z},\n\t\t\tfrontDown: {x:p.x,     y:p.y,     z:p.z},\n\t\t\trightUp:   {x:p.x+s.x, y:p.y,     z:p.z+s.z},\n\t\t\tleftUp:    {x:p.x,     y:p.y+s.y, z:p.z+s.z},\n\t\t\tbackUp:    {x:p.x+s.x, y:p.y+s.y, z:p.z+s.z},\n\t\t\tfrontUp:   {x:p.x,     y:p.y,     z:p.z+s.z},\n\t\t};\n\t},\n\n\t// Get the given block's vertices in flattened 2D isometric coordinates.\n\tgetIsoVerts: function(block) {\n\t\tvar verts = this.getIsoNamedSpaceVerts(block);\n\t\treturn {\n\t\t\tleftDown:  this.spaceToIso(verts.leftDown),\n\t\t\trightDown: this.spaceToIso(verts.rightDown),\n\t\t\tbackDown:  this.spaceToIso(verts.backDown),\n\t\t\tfrontDown: this.spaceToIso(verts.frontDown),\n\t\t\tleftUp:    this.spaceToIso(verts.leftUp),\n\t\t\trightUp:   this.spaceToIso(verts.rightUp),\n\t\t\tbackUp:    this.spaceToIso(verts.backUp),\n\t\t\tfrontUp:   this.spaceToIso(verts.frontUp),\n\t\t};\n\t},\n\n\t// For the given block, get the min and max values on each isometric axis.\n\tgetIsoBounds: function(block) {\n\t\tvar verts = this.getIsoVerts(block);\n\t\treturn {\n\t\t\txmin: verts.frontDown.x,\n\t\t\txmax: verts.backUp.x,\n\t\t\tymin: verts.frontDown.y,\n\t\t\tymax: verts.backUp.y,\n\t\t\thmin: verts.leftDown.h,\n\t\t\thmax: verts.rightDown.h,\n\t\t};\n\t},\n\n\t// Try to find an axis in 2D isometric that separates the two given blocks.\n\t// This helps identify if the the two blocks are overlap on the screen.\n\tgetIsoSepAxis: function(block_a, block_b) {\n\t\tvar a = this.getIsoBounds(block_a);\n\t\tvar b = this.getIsoBounds(block_b);\n\n\t\tvar sepAxis = null;\n\t\tif (this.areRangesDisjoint(a.xmin,a.xmax,b.xmin,b.xmax)) {\n\t\t\tsepAxis = 'x';\n\t\t}\n\t\tif (this.areRangesDisjoint(a.ymin,a.ymax,b.ymin,b.ymax)) {\n\t\t\tsepAxis = 'y';\n\t\t}\n\t\tif (this.areRangesDisjoint(a.hmin,a.hmax,b.hmin,b.hmax)) {\n\t\t\tsepAxis = 'h';\n\t\t}\n\t\treturn sepAxis;\n\t},\n\n\t// Try to find an axis in 3D space that separates the two given blocks.\n\t// This helps identify which block is in front of the other.\n\tgetSpaceSepAxis: function(block_a, block_b) {\n\n\t\tvar sepAxis = null;\n\n\t\tvar a = block_a.getBounds();\n\t\tvar b = block_b.getBounds();\n\n\t\tif (this.areRangesDisjoint(a.xmin,a.xmax,b.xmin,b.xmax)) {\n\t\t\tsepAxis = 'x';\n\t\t}\n\t\telse if (this.areRangesDisjoint(a.ymin,a.ymax,b.ymin,b.ymax)) {\n\t\t\tsepAxis = 'y';\n\t\t}\n\t\telse if (this.areRangesDisjoint(a.zmin,a.zmax,b.zmin,b.zmax)) {\n\t\t\tsepAxis = 'z';\n\t\t}\n\t\treturn sepAxis;\n\t},\n\n\t// In an isometric perspective of the two given blocks, determine\n\t// if they will overlap each other on the screen. If they do, then return\n\t// the block that will appear in front.\n\tgetFrontBlock: function(block_a, block_b) {\n\n\t\t// If no isometric separation axis is found,\n\t\t// then the two blocks do not overlap on the screen.\n\t\t// This means there is no \"front\" block to identify.\n\t\tif (this.getIsoSepAxis(block_a, block_b)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Find a 3D separation axis, and use it to determine\n\t\t// which block is in front of the other.\n\t\tvar a = block_a.getBounds();\n\t\tvar b = block_b.getBounds();\n\t\tswitch(this.getSpaceSepAxis(block_a, block_b)) {\n\t\t\tcase 'x': return (a.xmin < b.xmin) ? block_a : block_b;\n\t\t\tcase 'y': return (a.ymin < b.ymin) ? block_a : block_b;\n\t\t\tcase 'z': return (a.zmin < b.zmin) ? block_b : block_a;\n\t\t\tdefault: throw \"blocks must be non-intersecting\";\n\t\t}\n\t},\n};\n"
  },
  {
    "path": "src/colors.js",
    "content": "\n// Tango Color Palette\n// http://en.wikipedia.org/wiki/Tango_Desktop_Project#Palette\nIsoBlock.colors = {\n\tyellow: {light:\"#fce94f\", medium:\"#edd400\", dark:\"#c4a000\"},\n\torange: {light:\"#fcaf3e\", medium:\"#f57900\", dark:\"#ce5c00\"},\n\tbrown:  {light:\"#e9b96e\", medium:\"#c17d11\", dark:\"#8f5902\"},\n\tgreen:  {light:\"#8ae234\", medium:\"#73d216\", dark:\"#4e9a06\"},\n\tblue:   {light:\"#729fcf\", medium:\"#3465a4\", dark:\"#204a87\"},\n\tpurple: {light:\"#ad7fa8\", medium:\"#75507b\", dark:\"#5c3566\"},\n\tred:    {light:\"#ef2929\", medium:\"#cc0000\", dark:\"#a40000\"},\n\twhite:  {light:\"#eeeeec\", medium:\"#d3d7cf\", dark:\"#babdb6\"},\n\tblack:  {light:\"#888a85\", medium:\"#555753\", dark:\"#2e3436\"},\n};\n\n// from David at http://stackoverflow.com/a/11508164/142317\nfunction hexToRgb(hex) {\n\n\t// strip out \"#\" if present.\n\tif (hex[0] == \"#\") {\n\t\thex = hex.substring(1);\n\t}\n\n    var bigint = parseInt(hex, 16);\n    var r = (bigint >> 16) & 255;\n    var g = (bigint >> 8) & 255;\n    var b = bigint & 255;\n\n    return r + \",\" + g + \",\" + b;\n}\n"
  },
  {
    "path": "src/main.js",
    "content": "\nvar IsoBlock = IsoBlock || {};\n\nIsoBlock.makeFigure = function(options) {\n\n\t// extract options\n\tvar canvasId = options.canvas;\n\tvar blocks = options.blocks;\n\tvar shouldSortBlocks = (options.sortBlocks == undefined) ? true : options.sortBlocks;\n\tvar shouldDrawAxes = options.drawAxis;\n\tvar shouldDrawPlane = options.drawPlane;\n\tvar axisLen = options.axisLen;\n\tvar silhouette = options.silhouette;\n\n\t// set canvas and context.\n\tvar canvas = document.getElementById(canvasId);\n\tvar ctx = canvas.getContext('2d');\n\n\t// extract scale and origin (camera attributes)\n\tvar scale = (options.scale && options.scale(canvas.width,canvas.height)) || (canvas.height / 8);\n\tvar origin = (options.origin && options.origin(canvas.width,canvas.height)) || {x: canvas.width/2, y: canvas.height };\n\n\t// compute appropriate axis length (assuming origin is horizontally centered)\n\tif (!axisLen) {\n\t\t// make sure the axis extends to the edge of the canvas\n\t\taxisLen = Math.floor((canvas.width - origin.x) / scale / Math.cos(Math.PI/6)) - 0.5;\n\t}\n\n\t// Get horizontal axis' vertical displacement from origin.\n\tvar hAxisV = origin.y/scale - 1;\n\n\t// create camera and painter.\n\tvar camera = new IsoBlock.Camera(origin, scale);\n\tvar painter = new IsoBlock.Painter(camera);\n\n\t// draw the xy grid\n\tfunction drawGrid() {\n\n\t\t// grid step\n\t\tvar step = 1;\n\n\t\t// grid range\n\t\tvar maxx = 15;\n\t\tvar maxy = 15;\n\n\t\t// plot x lines\n\t\tctx.beginPath();\n\t\tfor (x=-maxx; x<=maxx; x+=step) {\n\t\t\tpainter.moveTo(ctx, {x:x, y:-maxy});\n\t\t\tpainter.lineTo(ctx, {x:x, y:maxy});\n\t\t}\n\n\t\t// plot y lines\n\t\tfor (y=-maxy; y<=maxy; y+=step) {\n\t\t\tpainter.moveTo(ctx, {x:-maxx, y:y});\n\t\t\tpainter.lineTo(ctx, {x:maxx, y:y});\n\t\t}\n\n\t\t// draw grid lines\n\t\tctx.strokeStyle = \"#d7d7d7\";\n\t\tctx.lineWidth = 1;\n\t\tctx.stroke();\n\n\t};\n\n\t// draw the xy axes and range bars for each block.\n\tfunction drawAxes() {\n\n\t\tvar axisColor = \"#444\";\n\t\tctx.lineWidth = 1;\n\t\tctx.strokeStyle = axisColor;\n\t\tctx.fillStyle = axisColor;\n\t\tvar arrowSize = 0.3;\n\n\t\t// draw x,y axes (and h axis if silhouette)\n\t\tvar xAxisStart = camera.spaceToIso({x:-axisLen, y:0});\n\t\tvar xAxisEnd = camera.spaceToIso({x:axisLen, y:0});\n\t\tvar yAxisStart = camera.spaceToIso({x:0, y:-axisLen});\n\t\tvar yAxisEnd = camera.spaceToIso({x:0, y:axisLen});\n\t\tvar hAxisStart = {h:yAxisEnd.h, v:hAxisV};\n\t\tvar hAxisEnd = {h:xAxisEnd.h, v:hAxisV};\n\t\tctx.beginPath();\n\t\tpainter.moveTo(ctx, xAxisStart);\n\t\tpainter.lineTo(ctx, xAxisEnd);\n\t\tpainter.moveTo(ctx, yAxisStart);\n\t\tpainter.lineTo(ctx, yAxisEnd);\n\t\tif (silhouette) {\n\t\t\tpainter.moveTo(ctx, hAxisStart);\n\t\t\tpainter.lineTo(ctx, hAxisEnd);\n\t\t}\n\t\tctx.stroke();\n\n\t\t// draw x-axis arrow\n\t\tctx.beginPath();\n\t\tpainter.moveTo(ctx, {x:axisLen, y:0});\n\t\tpainter.lineTo(ctx, {x:axisLen-arrowSize, y:-arrowSize});\n\t\tpainter.lineTo(ctx, {x:axisLen-arrowSize, y:arrowSize});\n\t\tctx.closePath();\n\t\tctx.fill();\n\n\t\t// draw y-axis arrow\n\t\tctx.beginPath();\n\t\tpainter.moveTo(ctx, {y:axisLen, x:0});\n\t\tpainter.lineTo(ctx, {y:axisLen-arrowSize, x:-arrowSize});\n\t\tpainter.lineTo(ctx, {y:axisLen-arrowSize, x:arrowSize});\n\t\tctx.closePath();\n\t\tctx.fill();\n\n\t\t// draw h-axis arrow\n\t\tif (silhouette) {\n\t\t\tctx.beginPath();\n\t\t\tpainter.moveTo(ctx, hAxisStart);\n\t\t\tpainter.lineTo(ctx, {h:hAxisStart.h+arrowSize, v:hAxisV+arrowSize});\n\t\t\tpainter.lineTo(ctx, {h:hAxisStart.h+arrowSize, v:hAxisV-arrowSize});\n\t\t\tctx.closePath();\n\t\t\tctx.fill();\n\t\t\tctx.beginPath();\n\t\t\tpainter.moveTo(ctx, hAxisEnd);\n\t\t\tpainter.lineTo(ctx, {h:hAxisEnd.h-arrowSize, v:hAxisV+arrowSize});\n\t\t\tpainter.lineTo(ctx, {h:hAxisEnd.h-arrowSize, v:hAxisV-arrowSize});\n\t\t\tctx.closePath();\n\t\t\tctx.fill();\n\t\t}\n\n\t\t// draw axis labels\n\t\tvar p = painter.transform({x:axisLen-1, y:-1});\n\t\tctx.font = \"italic 1em serif\";\n\t\tctx.textBaseline='middle';\n\t\tctx.textAlign='right';\n\t\tctx.fillText(\"x\",p.x,p.y);\n\t\tp = painter.transform({x:-1, y:axisLen-1});\n\t\tctx.textAlign='left';\n\t\tctx.fillText(\"y\",p.x,p.y);\n\t\tif (silhouette) {\n\t\t\tp = painter.transform({h:hAxisEnd.h, v:hAxisV-1});\n\t\t\tctx.textAlign='right';\n\t\t\tctx.fillText(\"h\",p.x,p.y);\n\t\t}\n\t\t\n\t\t// draw axis ranges for each block\n\t\tvar i,len,bounds,color,rgb,minp,maxp;\n\t\tvar s = 0.25;\n\t\tfor (i=0, len=blocks.length; i<len; i++) {\n\t\t\tbounds = silhouette ? camera.getIsoBounds(blocks[i]) : blocks[i].getBounds();\n\t\t\tcolor = blocks[i].color.medium;\n\t\t\trgb = hexToRgb(color);\n\t\t\ttcolor = \"rgba(\"+rgb+\",0.7)\";\n\n\t\t\t// alternate which side of the axis the range bar is on.\n\t\t\ts*=-1;\n\n\t\t\t// draw x axis range\n\t\t\tpainter.fillQuad(ctx,\n\t\t\t\t{x:bounds.xmin, y:0},\n\t\t\t\t{x:bounds.xmin, y:s},\n\t\t\t\t{x:bounds.xmax, y:s},\n\t\t\t\t{x:bounds.xmax, y:0},\n\t\t\t\ttcolor\n\t\t\t);\n\n\t\t\t// draw y axis range\n\t\t\tpainter.fillQuad(ctx,\n\t\t\t\t{x:0, y:bounds.ymin},\n\t\t\t\t{x:s, y:bounds.ymin},\n\t\t\t\t{x:s, y:bounds.ymax},\n\t\t\t\t{x:0, y:bounds.ymax},\n\t\t\t\ttcolor\n\t\t\t);\n\n\t\t\tif (silhouette) {\n\t\t\t\tpainter.fillQuad(ctx,\n\t\t\t\t\t{h:bounds.hmin, v:hAxisV+s},\n\t\t\t\t\t{h:bounds.hmax, v:hAxisV+s},\n\t\t\t\t\t{h:bounds.hmax, v:hAxisV},\n\t\t\t\t\t{h:bounds.hmin, v:hAxisV},\n\t\t\t\t\ttcolor\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// draw a pseudo-shaded isometric block.\n\tfunction drawBlock(block) {\n\n\t\tvar color = block.color;\n\n\t\t// get aliases for each of the block's vertices relative to camera's perspective.\n\t\tvar b = camera.getIsoNamedSpaceVerts(block);\n\n\t\tif (silhouette) {\n\t\t\tvar rgb = hexToRgb(color.medium);\n\t\t\tvar tcolor = \"rgba(\"+rgb+\",0.7)\";\n\t\t\tctx.beginPath();\n\t\t\tpainter.moveTo(ctx, b.frontDown);\n\t\t\tpainter.lineTo(ctx, b.leftDown);\n\t\t\tpainter.lineTo(ctx, b.leftUp);\n\t\t\tpainter.lineTo(ctx, b.backUp);\n\t\t\tpainter.lineTo(ctx, b.rightUp);\n\t\t\tpainter.lineTo(ctx, b.rightDown);\n\t\t\tctx.fillStyle = tcolor;\n\t\t\tctx.fill();\n\t\t}\n\t\telse {\n\n\t\t\t// fill in the grout for the inside edges\n\t\t\tvar lineWidth = 1;\n\t\t\tvar groutColor = color.medium;\n\t\t\tpainter.line(ctx, b.leftUp, b.frontUp, groutColor, lineWidth);\n\t\t\tpainter.line(ctx, b.rightUp, b.frontUp, groutColor, lineWidth);\n\t\t\tpainter.line(ctx, b.frontDown, b.frontUp, groutColor, lineWidth);\n\n\t\t\t// Do not add line width when filling faces.\n\t\t\t// This prevents a perimeter padding around the hexagon.\n\t\t\t// Nonzero line width could cause the perimeter of another box\n\t\t\t// to bleed over the edge of a box in front of it.\n\t\t\tlineWidth = 0;\n\n\t\t\t// fill each visible face of the block.\n\n\t\t\t// left face\n\t\t\tpainter.fillQuad(ctx, b.frontDown, b.leftDown, b.leftUp, b.frontUp, !silhouette ? color.dark : color.medium, lineWidth);\n\n\t\t\t// top face\n\t\t\tpainter.fillQuad(ctx, b.frontUp, b.leftUp, b.backUp, b.rightUp, !silhouette ? color.light : color.medium, lineWidth);\n\n\t\t\t// right face\n\t\t\tpainter.fillQuad(ctx, b.frontDown, b.frontUp, b.rightUp, b.rightDown, color.medium, lineWidth);\n\t\t}\n\t};\n\n\t// draw a plane to separate two isometric blocks.\n\tfunction drawSeparationPlane(frontBlock, backBlock) {\n\n\t\t// exit if back plane is not present\n\t\tif (!backBlock) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar bounds = frontBlock.getBounds();\n\n\t\t// get axis of separation\n\t\tvar aAxis = camera.getSpaceSepAxis(frontBlock, backBlock);\n\t\tvar bAxis,cAxis;\n\n\t\t// aAxis, bAxis, cAxis are either 'x', 'y', or 'z'\n\t\t// a, b, c are the values of its respective axis.\n\n\t\t// determine what our abstract axes correspond to.\n\t\tif (aAxis == 'x') {\n\t\t\ta = bounds.xmax;\n\t\t\tbAxis = 'y';\n\t\t\tcAxis = 'z';\n\t\t}\n\t\telse if (aAxis == 'y') {\n\t\t\ta = bounds.ymax;\n\t\t\tbAxis = 'x';\n\t\t\tcAxis = 'z';\n\t\t}\n\t\telse if (aAxis == 'z') {\n\t\t\ta = bounds.zmin;\n\t\t\tbAxis = 'x';\n\t\t\tcAxis = 'y';\n\t\t}\n\n\t\t// the radius (read margin) of the separation plane).\n\t\tvar r = 0.7;\n\n\t\t// the points of the separation plane in abstract coords.\n\t\tvar pts = [\n\t\t\t{ a:a, b: bounds[bAxis+\"min\"]-r, c: bounds[cAxis+\"min\"]-r },\n\t\t\t{ a:a, b: bounds[bAxis+\"min\"]-r, c: bounds[cAxis+\"max\"]+r },\n\t\t\t{ a:a, b: bounds[bAxis+\"max\"]+r, c: bounds[cAxis+\"max\"]+r },\n\t\t\t{ a:a, b: bounds[bAxis+\"max\"]+r, c: bounds[cAxis+\"min\"]-r },\n\t\t];\n\n\t\t// convert abstract coords to the real coords for this block.\n\t\tvar i;\n\t\tvar finalPts = [];\n\t\tfor (i=0; i<4; i++) {\n\t\t\tvar p = {};\n\t\t\tp[aAxis] = pts[i].a;\n\t\t\tp[bAxis] = pts[i].b;\n\t\t\tp[cAxis] = pts[i].c;\n\t\t\tfinalPts.push(p);\n\t\t}\n\n\t\t// draw separation plane.\n\t\tpainter.fillQuad(ctx, finalPts[0], finalPts[1], finalPts[2], finalPts[3], \"rgba(0,0,0,0.35)\");\n\t\tpainter.strokeQuad(ctx, finalPts[0], finalPts[1], finalPts[2], finalPts[3], \"rgba(0,0,0,0.9)\", 1);\n\t};\n\n\t// draw grid\n\tdrawGrid();\n\n\t// draw axes\n\tif (shouldDrawAxes) {\n\t\tdrawAxes();\n\t}\n\n\t// sort blocks in drawing order.\n\tif (shouldSortBlocks) {\n\t\tblocks = IsoBlock.sortBlocks(blocks, camera);\n\t}\n\n\t// draw blocks and a separation plane if applicable.\n\tvar i,len;\n\tfor(i=0,len=blocks.length; i<len; i++) {\n\n\t\t// only draw a separation plane for the last block\n\t\t// and only if there is a block behind it.\n\t\tif (shouldDrawPlane && i>0 && i==len-1) {\n\t\t\tdrawSeparationPlane(blocks[i], blocks[i-1]);\n\t\t}\n\n\t\t// draw block\n\t\tdrawBlock(blocks[i]);\n\t}\n\n};\n\n"
  },
  {
    "path": "src/painter.js",
    "content": "\n// Allows us to paint shapes using isometric coordinates transformed by a given camera.\n// It's basically a wrapper for the canvas context.\nIsoBlock.Painter = function(camera) {\n\tthis.camera = camera;\n};\n\nIsoBlock.Painter.prototype = {\n\n\t// This function allows us to draw using different coordinate systems.\n\t// It accepts a nondescript position vector and tries to detect\n\t// what coordinate system it is in by looking at its properties.\n\t//\t\t(x,y,z)   <- treated as a space coordinate\n\t//\t\t(x,y)     <- treated as a space coordinate at z=0\n\t//                   (same as 2D isometric XY)\n\t//\t\t(h,v)     <- treated as a special 2D isometric coordinate\n\t//                   (with horizontal and vertical distance from origin)\n\ttransform: function(pos) {\n\t\tvar x,y,z;\n\t\tif (pos.x != undefined && pos.y != undefined) {\n\t\t\tx = pos.x;\n\t\t\ty = pos.y;\n\t\t\tz = (pos.z == undefined) ? 0 : pos.z;\n\t\t\treturn this.camera.spaceToScreen({x:x, y:y, z:z});\n\t\t}\n\t\telse if (pos.h != undefined && pos.v != undefined) {\n\t\t\treturn this.camera.isoToScreen(pos);\n\t\t}\n\t\telse {\n\t\t\tconsole.log(\"x\",pos.x,\"y\",pos.y,\"z\",pos.z,\"h\",pos.h,\"v\",pos.v);\n\t\t\tthrow \"painter.transform: Unable to detect coordinate system of given vector\";\n\t\t}\n\t},\n\tmoveTo: function(ctx, pos) {\n\t\tvar v = this.transform(pos);\n\t\tctx.moveTo(v.x,v.y);\n\t},\n\tlineTo: function(ctx, pos) {\n\t\tvar v = this.transform(pos);\n\t\tctx.lineTo(v.x,v.y);\n\t},\n\tquad: function(ctx, p1, p2, p3, p4) {\n\t\tthis.moveTo(ctx, p1);\n\t\tthis.lineTo(ctx, p2);\n\t\tthis.lineTo(ctx, p3);\n\t\tthis.lineTo(ctx, p4);\n\t},\n\tfillQuad: function(ctx, p1, p2, p3, p4, color, lineWidth) {\n\t\tctx.beginPath();\n\t\tthis.quad(ctx,p1,p2,p3,p4);\n\t\tctx.closePath();\n\t\tctx.fillStyle = color;\n\t\tctx.fill();\n\t\tif (lineWidth) {\n\t\t\tctx.lineWidth = lineWidth;\n\t\t\tctx.strokeStyle = color;\n\t\t\tctx.stroke();\n\t\t}\n\t},\n\tfillQuadGradient: function(ctx, p1, p2, p3, p4, color1, color2) {\n\t\tvar v1 = this.transform(p1);\n\t\tvar v4 = this.transform(p4);\n\t\tvar v2 = this.transform(p2);\n\t\tvar dx = v4.x-v1.x;\n\t\tvar dy = v4.y-v1.y;\n\t\tvar dist = Math.sqrt(dx*dx+dy*dy);\n\t\tdx /= dist;\n\t\tdy /= dist;\n\t\tvar dx2 = v2.x-v1.x;\n\t\tvar dy2 = v2.y-v1.y;\n\t\tdist = Math.sqrt(dx2*dx2+dy2*dy2);\n\t\tdx *= dist;\n\t\tdy *= dist;\n\t\t\n\t\t\n\t\t//var grad = ctx.createLinearGradient(v1.x, v1.y, v2.x, v2.y);\n\t\tvar grad = ctx.createLinearGradient(v1.x,v1.y, v1.x-dy, v1.y+dx);\n\t\tgrad.addColorStop(0, color1);\n\t\tgrad.addColorStop(1, color2);\n\t\tthis.fillQuad(ctx, p1,p2,p3,p4, grad);\n\t},\n\tstrokeQuad: function(ctx, p1, p2, p3, p4, color, lineWidth) {\n\t\tctx.beginPath();\n\t\tthis.quad(ctx,p1,p2,p3,p4);\n\t\tctx.closePath();\n\t\tctx.strokeStyle = color;\n\t\tctx.lineWidth = lineWidth;\n\t\tctx.lineJoin = \"round\";\n\t\tctx.stroke();\n\t},\n\tline: function(ctx, p1, p2, color, lineWidth) {\n\t\tctx.beginPath();\n\t\tthis.moveTo(ctx, p1);\n\t\tthis.lineTo(ctx, p2);\n\t\tctx.strokeStyle = color;\n\t\tctx.lineCap = 'butt';\n\t\tctx.lineWidth = lineWidth;\n\t\tctx.stroke();\n\t},\n\tfillCircle: function(ctx, p1, radius, color) {\n\t\tvar v = this.transform(p1);\n\t\tctx.beginPath();\n\t\tctx.arc(v.x,v.y,radius,0,2*Math.PI);\n\t\tctx.fillStyle = color;\n\t\tctx.fill();\n\t},\n\tstrokeCircle: function(ctx, p1, radius, color) {\n\t\tvar v = this.transform(p1);\n\t\tctx.beginPath();\n\t\tctx.arc(v.x,v.y,radius,0,2*Math.PI);\n\t\tctx.fillStyle = color;\n\t\tctx.fill();\n\t},\n};\n"
  },
  {
    "path": "src/sortBlocks.js",
    "content": "// From kennebec at http://stackoverflow.com/a/3955096/142317\n// Add a remove value function to the Array class.\nArray.prototype.remove = function() {\n    var what, a = arguments, L = a.length, ax;\n    while (L && this.length) {\n        what = a[--L];\n        while ((ax = this.indexOf(what)) !== -1) {\n            this.splice(ax, 1);\n        }\n    }\n    return this;\n};\n\n// Sort blocks in the order that they should be drawn for the given camera.\nIsoBlock.sortBlocks = function(blocks, camera) {\n\n\tvar i, j, numBlocks=blocks.length;\n\n\t// Initialize the list of blocks that each block is behind.\n\tfor (i=0; i<numBlocks; i++) {\n\t\tblocks[i].blocksBehind = [];\n\t\tblocks[i].blocksInFront = [];\n\t}\n\n\t// For each pair of blocks, determine which is in front and behind.\n\tvar a,b,frontBlock;\n\tfor (i=0; i<numBlocks; i++) {\n\t\ta = blocks[i];\n\t\tfor (j=i+1; j<numBlocks; j++) {\n\t\t\tb = blocks[j];\n\t\t\tfrontBlock = camera.getFrontBlock(a,b);\n\t\t\tif (frontBlock) {\n\t\t\t\tif (a == frontBlock) {\n\t\t\t\t\ta.blocksBehind.push(b);\n\t\t\t\t\tb.blocksInFront.push(a);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tb.blocksBehind.push(a);\n\t\t\t\t\ta.blocksInFront.push(b);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Get list of blocks we can safely draw right now.\n\t// These are the blocks with nothing behind them.\n\tvar blocksToDraw = [];\n\tfor (i=0; i<numBlocks; i++) {\n\t\tif (blocks[i].blocksBehind.length == 0) {\n\t\t\tblocksToDraw.push(blocks[i]);\n\t\t}\n\t}\n\n\t// While there are still blocks we can draw...\n\tvar blocksDrawn = [];\n\twhile (blocksToDraw.length > 0) {\n\n\t\t// Draw block by removing one from \"to draw\" and adding\n\t\t// it to the end of our \"drawn\" list.\n\t\tvar block = blocksToDraw.pop();\n\t\tblocksDrawn.push(block);\n\n\t\t// Tell blocks in front of the one we just drew\n\t\t// that they can stop waiting on it.\n\t\tfor (j=0; j<block.blocksInFront.length; j++) {\n\t\t\tvar frontBlock = block.blocksInFront[j];\n\n\t\t\t// Add this front block to our \"to draw\" list if there's\n\t\t\t// nothing else behind it waiting to be drawn.\n\t\t\tfrontBlock.blocksBehind.remove(block);\n\t\t\tif (frontBlock.blocksBehind.length == 0) {\n\t\t\t\tblocksToDraw.push(frontBlock);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn blocksDrawn;\n};\n"
  }
]