├── LICENSE ├── README.md ├── explanation.js ├── index.html ├── melkman.css ├── melkman.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Max Goldstein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Melkman's Algorithm Visualized 2 | 3 | A dynamic visualization of the best way to find the convex hull of simple polygon. 4 | 5 | To run locally, do `npm install && browserify melkman.js -o bundle.js && wget http://d3js.org/d3.v3.js` and open your favorite web server. 6 | 7 | ## Contributing 8 | If you find a piece of the explanatory text confusing, change it in `explanation.js` and submit a PR. 9 | 10 | If you have a way to improve the dynamic behaviour or graphics, you should file an issue and let me handle it, rather than witness the Lovecraftian horror contained within `melkman.js`. 11 | -------------------------------------------------------------------------------- /explanation.js: -------------------------------------------------------------------------------- 1 | function lines(){ 2 | return "

" + Array.prototype.slice.call(arguments).join("

") + "

"; 3 | } 4 | 5 | exports.intro = lines("Melkman's algorithm finds the convex hull of a polygon. If you wrapped a rubber band around a polygon and let it snap tight, you'd have the convex hull, which is one of the foundations of computational geometry. Melkman's algorithm is interesting because it works in linear time; finding the convex hull of a point set (rather than a polygon) takes O(n log n) time.", 6 | "To start, make a polygon by placing points on the canvas to the right." 7 | ); 8 | 9 | exports.okayStop = lines( "Okay, stop.", 10 | "Most convex hull algorithms require the entire polygon up front, but Melkman's asks, what can we find out with only three points?", 11 | "The convex hull of the three points so far is just the triangle they make. But, as we get more points, we may discover that some or all of these edges aren't actually on the hull at all! It's possible these points are deep in a pocket, but we won't know until we see more of the polygon.", 12 | "We need a way to store the current hull that can easily be changed to accomodate new information as it arrives.", 13 | "Hit the spacebar to continue." 14 | ); 15 | 16 | exports.dequeIntro = lines("Melkman's algorithm uses a deque, or double-ended queue, to solve this problem. The deque always contains the hull of the points we know about. It is efficient to read or modify either end of the deque. Its contents are now shown above.", 17 | "One property of convex polygons (like the hull we're trying to find) is that if you travel the vertices in one direction, you make only right turns. If you travel in the opposite direction, you make only left turns.", 18 | "The deque uses this property to maintain an invariant. If you read the deque rightward, skipping the gap in the polygon, you will make only right turns. If you read leftward, you will make only left turns.", 19 | "Spacebar to continue..." 20 | ); 21 | 22 | exports.pointC = function(leftTurn){ 23 | return lines("Point c appears on both ends of the deque because it was the last point added to the hull. This is an invariant we want to maintain. But the next point in the deque, in each direction, is also important.", 24 | "We'll say that point " + 25 | (leftTurn ? "a" : "b" ) + 26 | " will be blue because it appears to point c's left. Notice that this means it appears on the right side of the deque. Similarly, point " + 27 | (leftTurn ? "b" : "a" ) + 28 | " will be red because it appears to point c's right.", 29 | "Right now, the deque is small, so all points matter. Later on, we'll see that we don't need to access points deep in the deque." 30 | ); 31 | }; 32 | 33 | exports.rbpRegions = lines("Looking at those two outermost points on each side of the deque, we extend the lines they form to create regions, based on where point d could land.", 34 | "A sharp left turn puts us in the blue region. A left turn could violate the invariant on the right side of the deque, where we should be making a right turn.", 35 | "Similarly, a sharp right turn puts us in the red region. And if we go into the purple region, we may need to worry about both ends of the deque.", 36 | "If point d is in any of these regions, it will be on the convex hull (of points seen thusfar) and we must modify the deque." 37 | ); 38 | 39 | exports.yellowRegion = lines("But point d could also land within the known hull, requiring no changes to the deque. This is the yellow region. If point d lands here, we simply discard it and wait for the next point.", 40 | "Finally, the remaining white region is only accessible from point c by crossing an existing polygon edge. Melkman's algorithm assumes the polygon is simple, meaning that its edges don't intersect like that. It's this knowledge that allows the algorithm to operate in linear time.", 41 | "Notice how the four permissible regions meet at point c, the last point added to the hull. We can determine which region we land in from the turn direction of lines acd and bcd. Thus, the regions are illustrative only, and do not need to be calculated explicitly.", 42 | "Now place point d in one of the colored regions." 43 | ); 44 | 45 | exports.nonsimple = function(n){ 46 | var edges = n == 1 ? "edge has" : "edges have"; 47 | return lines("Hey, that's not allowed.", 48 | "The algorithm assumes that the polygon is simple, meaning edges don't cross each other. The offending " + edges + " been highlighted in bright red.", 49 | "The white region is off-limits. Try placing the point in one of the colored regions." 50 | );}; 51 | 52 | exports.pointInYellow = lines("You've placed a point in the yellow region, which is inside the known hull. Since this new point can't possibly be on the hull, we just ignore it.", 53 | "You can place as many points in the yellow region as you like. Just be sure to leave yourself a way out, because the yellow region does not have a line of sight to all the other regions." 54 | ); 55 | 56 | exports.pointInRed = lines("You've placed a point in the red region. We now need to pop from the left side of the deqeue until reading leftwards once again corresponds to making only left turns." 57 | ); 58 | 59 | exports.pointInBlue = lines("Since the point was placed in the blue region, we now need to pop from the right side of the deqeue until reading rightwards once again corresponds to making only right turns." 60 | ); 61 | 62 | exports.pointInPurple = lines("You've placed a point in the purple region, which is just the composition of the red and blue regions!", 63 | "We pop from both sides of the deque (starting on the left) to restore the order invariant." 64 | ); 65 | 66 | exports.redRight = lines("Since the point was placed in the red region, the right side of the deque does not need to be modified.", 67 | "In a real implementation, the algorithm would pop until these three points form a right turn when read rightward. The red region tells us humans that they already form a right turn."); 68 | 69 | exports.blueLeft = lines("You've placed a point in the blue region. This means that the left side of the deque does not need to be modified.", 70 | "In a real implementation, the algorithm would pop until these three points form a left turn when read leftward. The blue region tells us humans that they already form a left turn."); 71 | 72 | exports.donePopping = lines("We have now restored the order invariant of the deque, and can now add the newly added point to both ends.", 73 | "The new deque is once again the convex hull of the known points. Now it's time to add another one!", 74 | "Dashed gray lines show the extension of the polygon from the interior past the red and blue points, which will be popped if the new point is beyond those lines.", 75 | "When you're ready, click the first point you placed to complete the polygon. You'll need an unobstructed line of sight to do so." 76 | ); 77 | 78 | exports.finished = lines("Now that we've seen the last point on the polygon, take a moment to read the deque while also tracing the same points on the polygon. You'll notice that it is indeed the convex hull, as it has been all along.", 79 | "Melkman's algorithm is online, meaning that it always has the answer for the data is has seen so far, without requiring any additional processing. Online algorithms have practical value when the data is too big to fit into memory or suffers from network latency. They also lend themselves to visualization, because the audience (that's you) can interact, predict, and respond to the dynamic presentation.", 80 | "Because each point was added and removed from the deque at most twice (once on each end), the algorithm has taken linear time." 81 | ); 82 | 83 | exports.finale = "

Visualizations are powerful tools to understand algorithms: how they operate, what are the cases, and where they can falter. Careful design can elucidate the consequences only implicit in mathematical concepts.

" + 84 | "

Thank you to the open source technologies that made this visualization possible:

" + 85 | "

Further reading:

"; 95 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Melkman's Algorithm

11 |

Visualized by Max Goldstein

12 |
13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /melkman.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #FDFCFA; 4 | } 5 | 6 | svg { 7 | float: right; 8 | } 9 | 10 | text, h1, h2, p, li { 11 | /* It's the same for SVG and HTML! */ 12 | font-family: avenir, open-sans, sans-serif; 13 | } 14 | 15 | h1, h2 { 16 | text-align: center; 17 | font-weight: normal; 18 | margin: 0; 19 | } 20 | 21 | p { 22 | margin: 32px 20px; 23 | } 24 | 25 | p.finale { 26 | margin: 0px 20px; 27 | } 28 | 29 | p.finale-top { 30 | margin: 32px 20px 16px; 31 | } 32 | 33 | h1 { font-size: 36px; } 34 | h2 { font-size: 18px; } 35 | 36 | svg text { 37 | text-anchor: middle; 38 | user-select: none; 39 | -webkit-user-select: none; 40 | -khtml-user-select: none; 41 | -moz-user-select: none; 42 | -ms-user-select: none; 43 | } 44 | 45 | .leftside { 46 | margin-top: 15px; 47 | width: 400px; 48 | } 49 | 50 | .cover { 51 | fill: #FDFCFA; 52 | stroke: none; 53 | } 54 | 55 | .region { 56 | stroke: none; 57 | } 58 | 59 | strong.yellow { 60 | background-color: #FFFF84; 61 | } 62 | 63 | strong.red { 64 | background-color: rgb(255, 119, 119); 65 | } 66 | 67 | strong.blue { 68 | background-color: rgb(119, 119, 255); 69 | } 70 | 71 | strong.purple { 72 | background-color: rgb(218, 84, 255); 73 | } 74 | 75 | strong.white { 76 | background-color: white; 77 | } 78 | 79 | strong.error-red { 80 | color: red; 81 | } 82 | 83 | .hull-vertex circle { 84 | fill: white; 85 | stroke: black; 86 | } 87 | 88 | .interior-vertex circle { 89 | fill: black; 90 | stroke: none; 91 | } 92 | 93 | .hull-vertex text { 94 | font-size: 14px; 95 | } 96 | 97 | .deque-vertex text { 98 | font-size: 18px; 99 | } 100 | 101 | .deque-vertex rect { 102 | fill: white; 103 | } 104 | 105 | line, polyline, rect, path { 106 | fill: none; 107 | stroke: black; 108 | stroke-width: 2; 109 | } 110 | 111 | path.hull, line.hull { 112 | stroke: #15B6E5; 113 | stroke-width: 6; 114 | } 115 | 116 | strong.hull { 117 | background-color: #15B6E5; 118 | } 119 | 120 | line.err { 121 | stroke: red; 122 | stroke-width: 4; 123 | } 124 | 125 | line.dashed { 126 | stroke: #DDD; 127 | stroke-width: 1; 128 | stroke-dasharray: 10, 10; 129 | } 130 | 131 | rect.bg { 132 | stroke: #BDBAB6; 133 | stroke-width: 1px; 134 | fill: #FFF; 135 | } 136 | 137 | text.body1 { 138 | text-anchor: start; 139 | } 140 | -------------------------------------------------------------------------------- /melkman.js: -------------------------------------------------------------------------------- 1 | var Deque = require("collections/deque"); 2 | var convexHull = require("quick-hull-2d"); 3 | var _visibility = require("vishull2d"); 4 | var lineIntersection = require("segseg"); 5 | var ClipperLib = require("js-clipper"); 6 | var explanations = require("./explanation"); 7 | 8 | // Prototype extensions and helper functions - interesting code starts around line 170 9 | 10 | //extend the deque to support peeking at the second item on either end 11 | Deque.prototype.peek2 = function () { 12 | if (this.length < 2) { 13 | console.warn("Deque too small to peek2", this.toArray()); 14 | return; 15 | } 16 | var index = (this.front + 1) & (this.capacity - 1); 17 | return this[index]; 18 | }; 19 | 20 | Deque.prototype.peekBack2 = function () { 21 | if (this.length < 2) { 22 | console.warn("Deque too small to peekBack2", this.toArray()); 23 | return; 24 | } 25 | var index = (this.front + this.length - 2) & (this.capacity - 1); 26 | return this[index]; 27 | }; 28 | 29 | d3.selection.prototype.translate = function(a, b) { 30 | return arguments.length === 1 ? 31 | this.attr("transform", "translate(" + a + ")") 32 | : this.attr("transform", "translate(" + a + "," + b + ")"); 33 | }; 34 | 35 | // Wrapping node library functions - better than writing from scratch 36 | 37 | function visibility(pts, cen){ 38 | // convert vertex chain to line segments 39 | var segments = [ 40 | [[0,0], [0,height]], 41 | [[0,height], [width,height]], 42 | [[width,height], [width, 0]], 43 | [[width, 0], [0,0]] ]; 44 | for (var i = 0; i < pts.length; ++i) { 45 | var j = i+1; 46 | if (j === pts.length) j = 0; 47 | segments.push([pts[i], pts[j]]); 48 | } 49 | return _visibility(segments, cen); 50 | } 51 | 52 | function polygonIntersection(subject, clip){ 53 | var cpr = new ClipperLib.Clipper(), 54 | solution_paths = new ClipperLib.Paths(), 55 | subject_fillType = ClipperLib.PolyFillType.pftNonZero, 56 | clip_fillType = ClipperLib.PolyFillType.pftNonZero; 57 | 58 | cpr.AddPaths([subject.map(function(p){return {X: p[0], Y: p[1]};})], ClipperLib.PolyType.ptSubject, true); 59 | cpr.AddPaths([clip.map(function(p){return {X: p[0], Y: p[1]};})], ClipperLib.PolyType.ptClip, true); 60 | cpr.Execute(ClipperLib.ClipType.ctIntersection, solution_paths, subject_fillType, clip_fillType); 61 | if (solution_paths.length === 0) return []; 62 | return solution_paths[0].map(function(obj){return [obj.X, obj.Y];}); 63 | } 64 | 65 | // Geometry helpers 66 | 67 | function leftTurn(p0, p1, p2){ 68 | var a = p1[0] - p0[0], 69 | b = p1[1] - p0[1], 70 | c = p2[0] - p1[0], 71 | d = p2[1] - p1[1]; 72 | return a*d - b*c < -0.001; 73 | } 74 | 75 | function rightTurn(p0, p1, p2){ 76 | var a = p1[0] - p0[0], 77 | b = p1[1] - p0[1], 78 | c = p2[0] - p1[0], 79 | d = p2[1] - p1[1]; 80 | return a*d - b*c > 0.001; 81 | } 82 | 83 | function dist2(p0, p1){ 84 | var dx = p1[0] - p0[0], 85 | dy = p1[1] - p0[1]; 86 | return (dx*dx) + (dy*dy); 87 | } 88 | 89 | function intersectsAny(p0, p1){ 90 | var ret = 0; 91 | g_lines.selectAll(".err").remove(); 92 | var j = points.length === 4 ? 3 : 4; // Don't ask 93 | for (var i = 0; i < points.length-j; i++){ 94 | if (lineIntersection(p0, p1, points[i], points[i+1])){ 95 | ret++; 96 | var q0 = points[i], q1 = points[i+1]; 97 | g_lines.append("line") 98 | .attr("x1", q0[0]) 99 | .attr("y1", q0[1]) 100 | .attr("x2", q1[0]) 101 | .attr("y2", q1[1]) 102 | .attr("class", "err"); 103 | } 104 | } 105 | return ret; 106 | } 107 | 108 | // Proceeding from p0 to p1, what point on the canvas boundary do you hit? 109 | function toBoundary(p0, p1){ 110 | var x = p1[0], y = p1[1], 111 | dx = x - p0[0], 112 | dy = y - p0[1]; 113 | if (x===0 || y===0) return p1; 114 | var k, w; 115 | 116 | // left wall 117 | k = -x/dx; 118 | h = y + dy*k; 119 | if (k > 0 && h >= 0 && h <= height) return [0, h]; 120 | 121 | // top wall 122 | k = -y/dy; 123 | w = x + dx*k; 124 | if (k > 0 && w >= 0 && w <= width) return [w, 0]; 125 | 126 | // right wall 127 | k = (width-x)/dx; 128 | h = y + dy*k; 129 | if (k > 0 && h >= 0 && h <= height) return [width, h]; 130 | 131 | // top wall 132 | k = (height-y)/dy; 133 | w = x + dx*k; 134 | if (k > 0 && w >= 0 && w <= width) return [w, height]; 135 | 136 | console.warn("toBoundary found unsatisfactory result for", p0, p1); 137 | return p1; 138 | } 139 | 140 | // Given two points on boundary edges, return an array of points of the 141 | // corners hit traveling clockwise 142 | function corners(b0, b1){ 143 | var top = 0, right = 1, bottom = 2, left = 3; 144 | 145 | sideOf = function(b){ 146 | if (b[0]===0){ 147 | return left; 148 | }else if (b[0]===width){ 149 | return right; 150 | }else if (b[1]===0){ 151 | return top; 152 | }else if (b[1]===height){ 153 | return bottom; 154 | }else{ 155 | console.warn("corners called with non-boundary point", b); 156 | } 157 | }; 158 | 159 | var s0 = sideOf(b0), s1 = sideOf(b1); 160 | 161 | var cornerPoints = [[width,0], [width, height], [0,height], [0,0]]; 162 | ret = []; 163 | 164 | while (s0 != s1){ 165 | ret.push(cornerPoints[s0]); 166 | s0++; 167 | s0 %= 4; 168 | } 169 | 170 | return ret; 171 | } 172 | 173 | // initialize document margins 174 | 175 | var margin = {top: 120, right: 20, bottom: 20, left: 400}, 176 | width = window.innerWidth - margin.left - margin.right, 177 | height = window.innerHeight - margin.top - margin.bottom; 178 | 179 | console.log("width", width, "height", height); 180 | 181 | var alphabet = new Deque("abcdefghijklmnopqrstuvwxyz".split("")); 182 | 183 | // inline styles?! Because CSS class transtitions didn't work. 184 | var red = "#FF7777", blue = "#7777FF", purple = "#DA54FF", yellow = "#FFFF84", gray = "#DDD"; 185 | 186 | var transitionInLen = 600; 187 | var transitionOutLen = 200; 188 | 189 | // SVG initialization and g elements 190 | 191 | var svg_deque = d3.select("#deque") 192 | .attr("width", width + margin.right) 193 | .attr("height", margin.top - 10); 194 | 195 | var svg_polygon = d3.select("#polygon") 196 | .attr("width", width + margin.right) 197 | .attr("height", height + margin.bottom - 5); 198 | 199 | d3.selectAll("svg").append("rect") 200 | .attr({width: width-2, x: 1, y: 1, class: "bg"}) 201 | .attr("height", function(d,i){return i ? height : margin.top - 30;}); 202 | 203 | var g_deque = svg_deque.append("g") 204 | .translate((width - 60*4)/2, 0); 205 | 206 | g_deque.append("line").attr("class", "hull"); 207 | 208 | var arrows = g_deque.append("line") 209 | .attr({x2: 170, "marker-end": "url(#head)", class: "arrow", display: "none"}) 210 | .translate(0, 75) 211 | .style("stroke", gray); 212 | 213 | svg_deque.selectAll(".cover").data([0,0.5]).enter().append("rect") 214 | .attr({class: "cover", width: (width+margin.right)/2, height: margin.top}) 215 | .attr("x", function(d){return d*(width+margin.right);}); 216 | 217 | var g_yellow = svg_polygon.append("g"), 218 | g_regions = svg_polygon.append("g"), 219 | g_lines = svg_polygon.append("g"), 220 | g_points = svg_polygon.append("g"); 221 | 222 | g_lines.append("path").attr("class", "hull"); 223 | g_lines.append("path").attr("id", "path_poly"); 224 | 225 | var text = d3.select("#text"); 226 | text.html(explanations.intro); 227 | 228 | // Sin Bin: Global state of the algorithm 229 | var points = []; 230 | var deque, lastOnHull, newPos; 231 | var freeze = false; 232 | var popping = false; 233 | var validPoint = true; 234 | var state = 0; 235 | 236 | // Functions to handle the polygon drawing 237 | var line_gen = d3.svg.line(); 238 | function line(){ 239 | return g_lines.select("#path_poly").datum(points).attr("d", line_gen); 240 | } 241 | 242 | function updatePoint(p){ 243 | var sel = g_points.select("#newest"); 244 | if (sel.size()){ 245 | p.s = sel.datum().s; 246 | sel.translate(p).datum(p); 247 | } 248 | return sel; 249 | } 250 | 251 | function mousePoint(p){ 252 | var sel = g_points.select("#newest"); 253 | if (sel.size()){ 254 | updatePoint(p); 255 | points[points.length-1] = p; 256 | }else{ 257 | p.s = alphabet.shift(); 258 | points.push(p); 259 | var g = g_points.append("g").attr("id", "newest").attr("class", "hull-vertex").translate(p).datum(p); 260 | var fill = points.length > 3 ? gray : "white"; 261 | g.append("circle").attr("r", 10).style({fill: fill, stroke: "black"}); 262 | g.append("text").text(p.s).attr("dy", "4px").style("font-size", "14px"); 263 | return g; 264 | } 265 | } 266 | 267 | function hullPoint(p){ 268 | updatePoint(p).attr("id", null); 269 | } 270 | 271 | function interiorPoint(p){ 272 | updatePoint(p).attr("id", null); 273 | return hullToInterior(p); 274 | } 275 | 276 | function hullToInterior(p){ 277 | alphabet.push(p.s); 278 | var point = g_points.selectAll(".hull-vertex") 279 | .filter(function(d){return d.s===p.s;}) 280 | .attr("class", "interior-vertex") 281 | .transition().duration(1100); 282 | point.select("circle") 283 | .attr("r", 4) 284 | .style({fill: "black", stroke: "0px"}); 285 | point.select("text") 286 | .attr("dy", "0px") 287 | .style("font-size", "0px") 288 | .remove(); 289 | } 290 | 291 | // Functions to handle specific moments in the presentation 292 | 293 | function first3(){ 294 | freeze = true; 295 | state = 1; 296 | text.html(explanations.okayStop); 297 | var a = points[0], b = points[1], c = points[2]; 298 | } 299 | 300 | function revealDeque(){ 301 | state++; 302 | text.html(explanations.dequeIntro); 303 | var a = points[0], b = points[1], c = points[2]; 304 | lastOnHull = c; 305 | if (leftTurn(a,b,c)){ 306 | deque = new Deque([b,a]); 307 | }else{ 308 | deque = new Deque([a,b]); 309 | } 310 | renderDeque(); 311 | svg_deque.selectAll(".cover").transition() 312 | .duration(1000) 313 | .attr("x", function(d,i){ 314 | return i ? width+margin.right+10 : -(width+margin.right)/2 -10; 315 | }) 316 | .remove(); 317 | } 318 | 319 | function pointC(){ 320 | state++; 321 | var a = points[0], b = points[1], c = points[2]; 322 | var initialLeftTurn = leftTurn(a,b,c); 323 | text.html(explanations.pointC(initialLeftTurn)); 324 | renderFills(); 325 | renderDeque(); 326 | } 327 | 328 | // Main rendering functions 329 | 330 | function renderDeque(){ 331 | // Fair warning: this is the ugliest function in the project 332 | var data = !popping ? [lastOnHull].concat(deque.toArray(), [lastOnHull]) 333 | : [newPos].concat(deque.toArray(), [newPos]); 334 | if (!popping){ 335 | console.log("deque is", data.map(function(d){return d.s;})); 336 | g_deque.transition().duration(750) 337 | .attr("transform", "translate("+((width - 60*data.length)/2)+",0)"); 338 | } 339 | var items = g_deque.selectAll(".deque-vertex") 340 | .data(data, function(d,i){ 341 | if (newPos && d.s === newPos.s) return d.s + (i < data.length/2 ? 0 : 1); 342 | if (d.s === lastOnHull.s) return d.s + (i < data.length/2 ? 0 : 1); 343 | return d.s; 344 | }); 345 | var entering = items.enter().append("g").attr("class", "deque-vertex") 346 | .attr("transform", function(d,i){ 347 | var j = state == 2 ? i : i - 1; 348 | return "translate("+(j*60)+","+(margin.top / 2 - 35)+")";}); 349 | entering.append("rect") 350 | .attr({width: "40px", height: "40px", rx: "8px", ry: "8px", x: "0px", y: "0px"}) 351 | .style("fill", state == 2 ? "white" : gray); 352 | entering.append("text") 353 | .translate(20,20) 354 | .attr("dy", "5px"); 355 | items.selectAll("text").text(function(d){return d.s;}); 356 | items.order(); 357 | var exiting = items.exit().transition().duration(800).ease("cubic"); 358 | exiting.select("rect").attr({width: 0, height: 0, x: "20px", y: "20px", rx: "0px", ry: "0px"}); 359 | exiting.select("text").style("font-size", 0).attr("dy", "0px"); 360 | exiting.remove(); 361 | var lastIndex; 362 | exiting.each("end", function(){ 363 | g_deque.selectAll(".deque-vertex") 364 | .call(function(){lastIndex = this.size() - 1;}) 365 | .transition() 366 | .attr("transform", function(d,i){ 367 | var transform = d3.transform(d3.select(this).attr("transform")); 368 | if (state === 20 && i !== 0) transform.translate[0] -= 60; 369 | if (state === 21 && i !== lastIndex) transform.translate[0] += 60; 370 | return transform.toString(); 371 | }); 372 | }); 373 | 374 | // arrows, the gray line indicating the side of the deque 375 | if (state == 20){ 376 | arrows.attr("display", null).translate(-60, 75); 377 | }else if (state == 21){ 378 | var x = d3.transform(items.filter(function(d,i){return i===items.size()-1;}).attr("transform")).translate[0]; 379 | arrows.attr("display", null).translate(x-120, 75); 380 | }else{ 381 | arrows.attr("display", "none"); 382 | } 383 | 384 | if (!popping && state > 2){ 385 | lastIndex = items.size() - 1; 386 | items.transition() 387 | .attr("transform", function(d,i){return "translate(" + (i*60) + ","+ (margin.top / 2 - 35)+")";}) 388 | .select("rect") 389 | .style("fill", function(d,i){ 390 | if (i === 0 || i === lastIndex) { return purple; } 391 | if (i === 1) { return red; } 392 | if (i === lastIndex-1) { return blue; } 393 | return "white"; 394 | }); 395 | } 396 | } 397 | 398 | function renderFills(){ 399 | svg_polygon.selectAll(".hull-vertex circle") 400 | .transition() 401 | .style("fill", function(d,i){ 402 | if (d.s === lastOnHull.s) { return purple; } 403 | }) 404 | .transition() 405 | .style("fill", function(d){ 406 | if (d.s === lastOnHull.s) { return purple; } 407 | if (d.s === deque.peek().s) { return red; } 408 | if (d.s === deque.peekBack().s) { return blue; } 409 | return "white"; 410 | }); 411 | 412 | } 413 | 414 | function rbpRegions(){ 415 | //rbp = red, blue, purple 416 | state++; 417 | text.html(explanations.rbpRegions); 418 | renderRBPregions(); 419 | } 420 | 421 | function renderRBPregions(){ 422 | g_regions.selectAll("path.region").transition().duration(transitionOutLen) 423 | .style("fill", "white") 424 | .remove(); 425 | var visible = visibility(points, points[points.length-1]); 426 | var region = function(order, color, p1, p2, p3, p4){ 427 | var b0 = toBoundary(p1, p2), 428 | b1 = toBoundary(p3, p4), 429 | regionOutline = convexHull([b0, lastOnHull, b1].concat(corners(b0, b1))); 430 | outline = polygonIntersection(visible, regionOutline); 431 | g_regions.append("path") 432 | .datum(outline) 433 | .attr("d", line_gen) 434 | .attr("class", "region") 435 | .style("fill", "white") 436 | .transition() 437 | .duration(transitionInLen) 438 | .delay(order*transitionInLen + transitionOutLen) 439 | .style("fill", color); 440 | }; 441 | var p_r = deque.peek(); 442 | var p_b = deque.peekBack(); 443 | var p_p = lastOnHull; 444 | region(0, blue, p_p, p_b, p_r, p_p); 445 | region(1, red, p_b, p_p, p_p, p_r); 446 | region(2, purple, p_r, p_p, p_b, p_p); 447 | } 448 | 449 | function yellowRegion(){ 450 | state++; 451 | freeze = false; 452 | text.html(explanations.yellowRegion); 453 | renderYellowRegion(); 454 | } 455 | 456 | function renderYellowRegion(){ 457 | g_yellow.selectAll("path").transition().duration(transitionOutLen) 458 | .style("fill", "white") 459 | .remove(); 460 | 461 | g_yellow.append("path") 462 | .datum(visibility(points, points[points.length-1])) 463 | .attr("d", line_gen) 464 | .style("fill", "white") 465 | .attr("class", "region") 466 | .transition().duration(transitionInLen).delay(state===5 ? 0 : 3*transitionInLen) 467 | .style("fill", yellow); 468 | } 469 | 470 | function renderDashedLines(){ 471 | var data = [[deque.peek(), toBoundary(deque.peek2(), deque.peek())], 472 | [deque.peekBack(), toBoundary(deque.peekBack2(), deque.peekBack())]]; 473 | var lines = g_lines.selectAll("line.dashed") 474 | .data(data) 475 | lines.enter().append("line").attr("class", "dashed"); 476 | lines.attr("x1", function(d){return d[0][0];}) 477 | .attr("y1", function(d){return d[0][1];}) 478 | .attr("x2", function(d){return d[1][0];}) 479 | .attr("y2", function(d){return d[1][1];}) 480 | .style("stroke-opacity", 0) 481 | .transition().duration(transitionInLen).delay(4*transitionInLen) 482 | .style("stroke-opacity", 1); 483 | lines.exit().remove(); 484 | } 485 | 486 | // Determine region and handle new point 487 | 488 | function newPoint(pos){ 489 | freeze = true; 490 | var red = rightTurn(deque.peek(), lastOnHull, pos); 491 | var blue = leftTurn(deque.peekBack(), lastOnHull, pos); 492 | console.log("red:", red, "blue:", blue); 493 | 494 | if (!red && !blue){ 495 | interiorPoint(pos); 496 | points.push(pos); 497 | line(); 498 | text.html(explanations.pointInYellow); 499 | renderYellowRegion(); 500 | renderRBPregions(); 501 | freeze = false; 502 | state = 7; 503 | }else{ 504 | if (red && !blue){ 505 | text.html(explanations.pointInRed); 506 | }else if (!red && blue){ 507 | text.html(explanations.blueLeft); 508 | }else{ 509 | text.html(explanations.pointInPurple); 510 | } 511 | newPos = pos; 512 | hullPoint(pos); 513 | points.push(pos); 514 | line(); 515 | deque.push(lastOnHull); 516 | deque.unshift(lastOnHull); 517 | popping = true; 518 | state = 20; 519 | renderDeque(); 520 | } 521 | } 522 | 523 | function fixLeft(){ 524 | if (rightTurn(deque.peek2(), deque.peek(), newPos)){ 525 | var removed = deque.shift(); 526 | console.log("shifting", removed); 527 | if (removed.s !== deque.peekBack().s){ 528 | hullToInterior(removed); 529 | } 530 | renderDeque(); 531 | }else{ 532 | state = 21; 533 | renderDeque(); 534 | if (leftTurn(deque.peekBack2(), deque.peekBack(), newPos)){ 535 | if (text.text().indexOf("purple") == -1){ 536 | text.html(explanations.pointInBlue); 537 | } 538 | fixRight(); 539 | }else{ 540 | text.html(explanations.redRight); 541 | } 542 | } 543 | } 544 | 545 | function fixRight(){ 546 | if (leftTurn(deque.peekBack2(), deque.peekBack(), newPos)){ 547 | var removed = deque.pop(); 548 | console.log("popping", removed); 549 | if (removed.s !== deque.peek().s){ 550 | hullToInterior(removed); 551 | } 552 | renderDeque(); 553 | }else{ 554 | text.html(explanations.donePopping); 555 | lastOnHull = newPos; 556 | newPos = undefined; 557 | popping = false; 558 | state = 6; 559 | renderDeque(); 560 | renderFills(); 561 | renderRBPregions(); 562 | renderYellowRegion(); 563 | renderDashedLines(); 564 | freeze = false; 565 | } 566 | } 567 | 568 | function finished(){ 569 | state = 30; 570 | freeze = true; 571 | text.html(explanations.finished); 572 | g_points.select("#newest").remove(); 573 | points[points.length-1] = points[0]; 574 | line(); 575 | svg_polygon.selectAll("path.region").transition().duration(500) 576 | .style("fill", "white") 577 | .remove(); 578 | g_lines.selectAll(".err, .dashed").remove(); 579 | g_lines.select(".hull") 580 | .attr("d", line_gen([lastOnHull].concat(deque.toArray(), [lastOnHull]))) 581 | .attr("class", "hull") 582 | .style("stroke-opacity", 0) 583 | .transition().duration(750) 584 | .style("stroke-opacity", 1); 585 | g_deque.select(".hull") 586 | .attr("x1", 30) 587 | .attr("x2", (deque.length+1.3)*60) 588 | .attr("y1", margin.top/2 - 15) 589 | .attr("y2", margin.top/2 - 15) 590 | .style("stroke-opacity", 0) 591 | .transition().duration(750) 592 | .style("stroke-opacity", 1); 593 | } 594 | function finale(){ 595 | state = 31; 596 | text.html(explanations.finale); 597 | } 598 | 599 | // Finally, the driving event dispatchers 600 | 601 | svg_polygon.on("click", function(){ 602 | if (freeze) return; 603 | var pos = points[points.length-1]; 604 | // check finish condition before nonsimple condition 605 | if (points.length > 3 && dist2(pos, points[0]) < 600) return finished(); 606 | if (!validPoint) return; 607 | if (points.length > 1){ 608 | var prev = points[points.length-2]; 609 | if (dist2(pos, prev) < 400) return; 610 | } 611 | if (points.length > 3) return newPoint(pos); 612 | hullPoint(pos); 613 | line(); 614 | if (points.length === 3) first3(); 615 | }); 616 | 617 | function adjustPosition(p){ 618 | var x = p[0]-7, y = p[1]-7; 619 | x = Math.max(12, Math.min(x, width-10)); 620 | y = Math.max(12, Math.min(y, height-10)); 621 | return [x,y]; 622 | } 623 | 624 | svg_polygon.on("mousemove", function(){ 625 | if (freeze) return; 626 | var pos = adjustPosition(d3.mouse(svg_polygon.node())); 627 | if (points.length > 3){ 628 | var prev = points[points.length-2], 629 | numberIntersections = intersectsAny(prev, pos); 630 | if (numberIntersections){ 631 | text.html(explanations.nonsimple(numberIntersections)); 632 | validPoint = false; 633 | }else if (!validPoint){ 634 | validPoint = true; 635 | if (state === 5) text.html(explanations.yellowRegion); 636 | if (state === 6) text.html(explanations.donePopping); 637 | if (state === 7) text.html(explanations.pointInYellow); 638 | } 639 | } 640 | mousePoint(pos); 641 | line(); 642 | }); 643 | 644 | d3.select("body").on("keydown", function(){ 645 | var transitioning = svg_deque.selectAll(".deque-vertex")[0].some(function(node){return !!node.__transition__;}); 646 | if (transitioning) return; 647 | if (d3.event.keyCode == 32){ 648 | switch (state){ 649 | case 1: 650 | revealDeque(); 651 | break; 652 | case 2: 653 | pointC(); 654 | break; 655 | case 3: 656 | rbpRegions(); 657 | break; 658 | case 4: 659 | yellowRegion(); 660 | break; 661 | case 20: 662 | fixLeft(); 663 | break; 664 | case 21: 665 | fixRight(); 666 | break; 667 | case 30: 668 | finale(); 669 | break; 670 | default: 671 | } 672 | } 673 | }); 674 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "melkman", 3 | "version": "1.0.0", 4 | "description": "Visualization of Melkman's algorithm", 5 | "main": "melkman.js", 6 | "repository": "https://github.com/mgold/Melkmans-Algorithm-Visualized", 7 | "dependencies": { 8 | "collections": "^1.2.1", 9 | "js-clipper": "^1.0.1", 10 | "quick-hull-2d": "^0.1.0", 11 | "segseg": "^0.2.1", 12 | "vishull2d": "^0.1.0" 13 | }, 14 | "devDependencies": { 15 | "watchify": "^2.2.1" 16 | }, 17 | "scripts": { 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "author": "Max Goldstein", 21 | "license": "MIT" 22 | } 23 | --------------------------------------------------------------------------------