From f5828713d4498ef9555ca04ab267bb57ed77e30b Mon Sep 17 00:00:00 2001 From: Tj Holowaychuk Date: Wed, 10 Nov 2010 09:23:01 -0800 Subject: [PATCH] Added voronoi example --- examples/rhill-voronoi-core-min.js | 117 ++++++++++++++++++++++++ examples/voronoi.js | 137 +++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 examples/rhill-voronoi-core-min.js create mode 100644 examples/voronoi.js diff --git a/examples/rhill-voronoi-core-min.js b/examples/rhill-voronoi-core-min.js new file mode 100644 index 0000000..3f7bd4e --- /dev/null +++ b/examples/rhill-voronoi-core-min.js @@ -0,0 +1,117 @@ +/*! +A custom Javascript implementation of Steven J. Fortune's algorithm to +compute Voronoi diagrams. +Copyright (C) 2010 Raymond Hill + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +***** + +Author: Raymond Hill (rhill@raymondhill.net) +File: rhill-voronoi-core-min.js +Version: 0.9 +Date: Sep. 21, 2010 +Description: This is my personal Javascript implementation of +Steven Fortune's algorithm to generate Voronoi diagrams. + +Portions of this software use, or depend on the work of: + + "Fortune's algorithm" by Steven J. Fortune: For his clever + algorithm to compute Voronoi diagrams. + http://ect.bell-labs.com/who/sjf/ + + "The Liang-Barsky line clipping algorithm in a nutshell!" by Daniel White, + to efficiently clip a line within a rectangle. + http://www.skytopia.com/project/articles/compsci/clipping.html + +***** + +Usage: + + var vertices = [{x:300,y:300}, {x:100,y:100}, {x:200,y:500}, {x:250,y:450}, {x:600,y:150}]; + // xl, xr means x left, x right + // yt, yb means y top, y bottom + var bbox = {xl:0, xr:800, yt:0, yb:600}; + var voronoi = new Voronoi(); + // pass an array of objects, each of which exhibits x and y properties + voronoi.setSites(vertices); + // pass an object which exhibits xl, xr, yt, yb properties. The bounding + // box will be used to connect unbound edges, and to close open cells + result = voronoi.compute(bbox); + // render, further analyze, etc. + +Return value: + An object with the following properties: + + result.sites = an array of unordered, unique Voronoi.Site objects underlying the Voronoi diagram. + result.edges = an array of unordered, unique Voronoi.Edge objects making up the Voronoi diagram. + result.cells = a dictionary of Voronoi.Cell object making up the Voronoi diagram. The Voronoi.Cell + in the dictionary are keyed on their associated Voronoi.Site's unique id. + result.execTime = the time it took to compute the Voronoi diagram, in milliseconds. + +Voronoi.Site object: + id: a unique id identifying this Voronoi site. + x: the x position of this Voronoi site. + y: the y position of this Voronoi site. + destroy(): mark this Voronoi site object as destroyed, it will be removed from the + internal collection and won't be part of the next Voronoi diagram computation. + + When adding vertices to the Voronoi object, through Voronoi.setSites() or + Voronoi.addSites(), an internal collection of matching Voronoi.Site object is maintained, + which is read accessible at all time through Voronoi.getSites(). You are allowed to + change the x and/or y properties of any Voronoi.Site object in the array, before + launching the computation of the Voronoi diagram. However, do *not* change the id + of any Voronoi.Site object, this could break the computation of the Voronoi diagram. + +Voronoi.Edge object: + id: a unique id identifying this Voronoi edge. + lSite: the Voronoi.Site object at the left of this Voronoi.Edge object. + rSite: the Voronoi.Site object at the right of this Voronoi.Edge object (can be null). + va: the Voronoi.Vertex object defining the start point (relative to the Voronoi.Site + on the left) of this Voronoi.Edge object. + vb: the Voronoi.Vertex object defining the end point (relative to Voronoi.Site on + the left) of this Voronoi.Edge object. + + For edges which are used to close open cells (using the supplied bounding box), the + rSite property will be null. + +Voronoi.Cells object: + A collection of Voronoi.Cell objects, keyed on the id of the associated Voronoi.Site + object. + numCells: the number of Voronoi.Cell objects in the collection. + +Voronoi.Cell object: + site: the Voronoi.Site object associated with the Voronoi cell. + halfedges: an array of Voronoi.Halfedge objects, ordered counterclockwise, defining the + polygon for this Voronoi cell. + +Voronoi.Halfedge object: + site: the Voronoi.Site object owning this Voronoi.Halfedge object. + edge: a reference to the unique Voronoi.Edge object underlying this Voronoi.Halfedge object. + getStartpoint(): a method returning a Voronoi.Vertex for the start point of this + halfedge. Keep in mind halfedges are always countercockwise. + getEndpoint(): a method returning a Voronoi.Vertex for the end point of this + halfedge. Keep in mind halfedges are always countercockwise. + +Voronoi.Vertex object: + x: the x coordinate. + y: the y coordinate. + +*/ +function Voronoi(){this.sites=[];this.siteEvents=[];this.circEvents=[];this.arcs=[];this.edges=[];this.cells=new this.Cells()}Voronoi.prototype.SITE_EVENT=0;Voronoi.prototype.CIRCLE_EVENT=1;Voronoi.prototype.VOID_EVENT=-1;Voronoi.prototype.sqrt=Math.sqrt;Voronoi.prototype.abs=Math.abs;Voronoi.prototype.floor=Math.floor;Voronoi.prototype.random=Math.random;Voronoi.prototype.round=Math.round;Voronoi.prototype.min=Math.min;Voronoi.prototype.max=Math.max;Voronoi.prototype.pow=Math.pow;Voronoi.prototype.isNaN=isNaN;Voronoi.prototype.PI=Math.PI;Voronoi.prototype.EPSILON=1e-5;Voronoi.prototype.equalWithEpsilon=function(a,b){return this.abs(a-b)<1e-5};Voronoi.prototype.greaterThanWithEpsilon=function(a,b){return(a-b)>1e-5};Voronoi.prototype.greaterThanOrEqualWithEpsilon=function(a,b){return(b-a)<1e-5};Voronoi.prototype.lessThanWithEpsilon=function(a,b){return(b-a)>1e-5};Voronoi.prototype.lessThanOrEqualWithEpsilon=function(a,b){return(a-b)<1e-5};Voronoi.prototype.Beachsection=function(a){this.site=a;this.edge=null;this.sweep=-Infinity;this.lid=0;this.circleEvent=undefined};Voronoi.prototype.Beachsection.prototype.sqrt=Math.sqrt;Voronoi.prototype.Beachsection.prototype._leftParabolicCut=function(a,c,d){var e=a.x;var f=a.y;if(f==d){return e}var g=c.x;var h=c.y;if(h==d){return g}if(f==h){return(e+g)/2}var i=f-d;var j=h-d;var k=g-e;var l=1/i-1/j;var b=k/j;return(-b+this.sqrt(b*b-2*l*(k*k/(-2*j)-h+j/2+f-i/2)))/l+e};Voronoi.prototype.Beachsection.prototype.leftParabolicCut=function(a,b){if(this.sweep!==b||this.lid!==a.id){this.sweep=b;this.lid=a.id;this.lBreak=this._leftParabolicCut(this.site,a,b)}return this.lBreak};Voronoi.prototype.Beachsection.prototype.isCollapsing=function(){return this.circleEvent!==undefined&&this.circleEvent.type===Voronoi.prototype.CIRCLE_EVENT};Voronoi.prototype.Site=function(x,y){this.id=this.constructor.prototype.idgenerator++;this.x=x;this.y=y};Voronoi.prototype.Site.prototype.destroy=function(){this.id=0};Voronoi.prototype.Vertex=function(x,y){this.x=x;this.y=y};Voronoi.prototype.Edge=function(a,b){this.id=this.constructor.prototype.idgenerator++;this.lSite=a;this.rSite=b;this.va=this.vb=undefined};Voronoi.prototype.Halfedge=function(a,b){this.site=a;this.edge=b};Voronoi.prototype.Cell=function(a){this.site=a;this.halfedges=[]};Voronoi.prototype.Cells=function(){this.numCells=0};Voronoi.prototype.Cells.prototype.addCell=function(a){this[a.site.id]=a;this.numCells++};Voronoi.prototype.Cells.prototype.removeCell=function(a){delete this[a.site.id];this.numCells--};Voronoi.prototype.Site.prototype.idgenerator=1;Voronoi.prototype.Edge.prototype.isLineSegment=function(){return this.id!==0&&Boolean(this.va)&&Boolean(this.vb)};Voronoi.prototype.Edge.prototype.idgenerator=1;Voronoi.prototype.Halfedge.prototype.isLineSegment=function(){return this.edge.id!==0&&Boolean(this.edge.va)&&Boolean(this.edge.vb)};Voronoi.prototype.Halfedge.prototype.getStartpoint=function(){return this.edge.lSite.id==this.site.id?this.edge.va:this.edge.vb};Voronoi.prototype.Halfedge.prototype.getEndpoint=function(){return this.edge.lSite.id==this.site.id?this.edge.vb:this.edge.va};Voronoi.prototype.leftBreakPoint=function(a,b){var c=this.arcs[a];var d=c.site;if(d.y==b){return d.x}if(a===0){return-Infinity}return c.leftParabolicCut(this.arcs[a-1].site,b)};Voronoi.prototype.rightBreakPoint=function(a,b){if(a>1;if(this.lessThanWithEpsilon(x,this.leftBreakPoint(i,a))){r=i;continue}if(this.greaterThanOrEqualWithEpsilon(x,this.rightBreakPoint(i,a))){l=i+1;continue}return i}return l};Voronoi.prototype.findDeletionPoint=function(x,a){var n=this.arcs.length;if(!n){return 0}var l=0;var r=n;var i;var b;while(l>1;b=this.leftBreakPoint(i,a);if(this.lessThanWithEpsilon(x,b)){r=i;continue}if(this.greaterThanWithEpsilon(x,b)){l=i+1;continue}b=this.rightBreakPoint(i,a);if(this.greaterThanWithEpsilon(x,b)){l=i+1;continue}if(this.lessThanWithEpsilon(x,b)){r=i;continue}return i}};Voronoi.prototype.createEdge=function(a,b,c,d){var e=new this.Edge(a,b);this.edges.push(e);if(c!==undefined){this.setEdgeStartpoint(e,a,b,c)}if(d!==undefined){this.setEdgeEndpoint(e,a,b,d)}this.cells[a.id].halfedges.push(new this.Halfedge(a,e));this.cells[b.id].halfedges.push(new this.Halfedge(b,e));return e};Voronoi.prototype.createBorderEdge=function(a,b,c){var d=new this.Edge(a,null);d.va=b;d.vb=c;this.edges.push(d);return d};Voronoi.prototype.destroyEdge=function(a){a.id=0};Voronoi.prototype.setEdgeStartpoint=function(a,b,c,d){if(a.va===undefined&&a.vb===undefined){a.va=d;a.lSite=b;a.rSite=c}else if(a.lSite.id==c.id){a.vb=d}else{a.va=d}};Voronoi.prototype.setEdgeEndpoint=function(a,b,c,d){this.setEdgeStartpoint(a,c,b,d)};Voronoi.prototype.removeArc=function(a){var x=a.center.x;var y=a.center.y;var b=a.y;var c=this.findDeletionPoint(x,b);var d=c;while(d-1>0&&this.equalWithEpsilon(x,this.leftBreakPoint(d-1,b))){d--}var e=c;while(e+10&&this.equalWithEpsilon(a.x,this.rightBreakPoint(c-1,a.y))&&this.equalWithEpsilon(a.x,this.leftBreakPoint(c,a.y))){d=this.arcs[c-1];rArc=this.arcs[c];this.voidCircleEvents(c-1,c);var e=this.circumcircle(d.site,a,rArc.site);this.setEdgeStartpoint(rArc.edge,d.site,rArc.site,new this.Vertex(e.x,e.y));b.edge=this.createEdge(d.site,b.site,undefined,new this.Vertex(e.x,e.y));rArc.edge=this.createEdge(b.site,rArc.site,undefined,new this.Vertex(e.x,e.y));this.arcs.splice(c,0,b);this.addCircleEvents(c-1,a.y);this.addCircleEvents(c+1,a.y);return}this.voidCircleEvents(c);d=this.arcs[c];rArc=new this.Beachsection(d.site);this.arcs.splice(c+1,0,b,rArc);b.edge=rArc.edge=this.createEdge(d.site,b.site);this.addCircleEvents(c,a.y);this.addCircleEvents(c+2,a.y)};Voronoi.prototype.circumcircle=function(a,b,c){var e=a.x;var f=a.y;var g=b.x-e;var h=b.y-f;var i=c.x-e;var j=c.y-f;var d=2*(g*j-h*i);var k=g*g+h*h;var l=i*i+j*j;var x=(j*k-h*l)/d;var y=(g*l-i*k)/d;return{x:x+e,y:y+f,radius:this.sqrt(x*x+y*y)}};Voronoi.prototype.addCircleEvents=function(a,b){if(a<=0||a>=this.arcs.length-1){return}var c=this.arcs[a];var d=this.arcs[a-1].site;var e=this.arcs[a].site;var f=this.arcs[a+1].site;if(d.id==f.id||d.id==e.id||e.id==f.id){return}if((d.y-e.y)*(f.x-e.x)<=(d.x-e.x)*(f.y-e.y)){return}var g=this.circumcircle(d,e,f);var h=g.y+g.radius;if(!this.greaterThanOrEqualWithEpsilon(h,b)){return}var i={type:this.CIRCLE_EVENT,site:e,x:g.x,y:h,center:{x:g.x,y:g.y}};c.circleEvent=i;this.queuePushCircle(i)};Voronoi.prototype.voidCircleEvents=function(a,b){if(b===undefined){b=a}a=this.max(a,0);b=this.min(b,this.arcs.length-1);while(a<=b){var c=this.arcs[a];if(c.circleEvent!==undefined){c.circleEvent.type=this.VOID_EVENT;c.circleEvent=undefined}a++}};Voronoi.prototype.queueSanitize=function(){var q=this.circEvents;var a=q.length;if(!a){return}var b=a;while(b&&q[b-1].type===this.VOID_EVENT){b--}var c=a-b;if(c){q.splice(b,c)}var d=this.arcs.length;if(q.length0&&q[a-1].type!==this.VOID_EVENT){a--}if(a<=0){break}b=a-1;while(b>0&&q[b-1].type===this.VOID_EVENT){b--}c=a-b;q.splice(b,c);if(q.length0?this.siteEvents[this.siteEvents.length-1]:null;var b=this.circEvents.length>0?this.circEvents[this.circEvents.length-1]:null;if(Boolean(a)!==Boolean(b)){return a?this.siteEvents.pop():this.circEvents.pop()}if(!a){return null}if(a.y>1;c=o.y-q[i].y;if(!c){c=o.x-q[i].x}if(c>0){r=i}else if(c<0){l=i+1}else{return}}q.splice(l,0,o)}else{q.push(o)}};Voronoi.prototype.queuePushCircle=function(o){var q=this.circEvents;var r=q.length;if(r){var l=0;var i,c;while(l>1;c=o.y-q[i].y;if(!c){c=o.x-q[i].x}if(c>0){r=i}else{l=i+1}}q.splice(l,0,o)}else{q.push(o)}};Voronoi.prototype.getBisector=function(a,b){var r={x:(a.x+b.x)/2,y:(a.y+b.y)/2};if(b.y==a.y){return r}r.m=(a.x-b.x)/(b.y-a.y);r.b=r.y-r.m*r.x;return r};Voronoi.prototype.connectEdge=function(a,b){var c=a.vb;if(!!c){return true}var d=a.va;var e=b.xl;var g=b.xr;var h=b.yt;var i=b.yb;var j=a.lSite;var k=a.rSite;var f=this.getBisector(j,k);if(f.m===undefined){if(f.x=g){return false}if(j.x>k.x){if(d===undefined){d=new this.Vertex(f.x,h)}else if(d.y>=i){return false}c=new this.Vertex(f.x,i)}else{if(d===undefined){d=new this.Vertex(f.x,i)}else if(d.y=g){return false}c=new this.Vertex(g,f.m*g+f.b)}else{if(d===undefined){d=new this.Vertex(g,f.m*g+f.b)}else if(d.xk.x){if(d===undefined){d=new this.Vertex((h-f.b)/f.m,h)}else if(d.y>=i){return false}c=new this.Vertex((i-f.b)/f.m,i)}else{if(d===undefined){d=new this.Vertex((i-f.b)/f.m,i)}else if(d.y0){if(r>h){return false}else if(r>g){g=r}}q=b.xr-c;if(i===0&&q<0){return false}r=q/i;if(i<0){if(r>h){return false}else if(r>g){g=r}}else if(i>0){if(r0){if(r>h){return false}else if(r>g){g=r}}q=b.yb-d;if(j===0&&q<0){return false}r=q/j;if(j<0){if(r>h){return false}else if(r>g){g=r}}else if(j>0){if(r=0;e-=1){d=b[e];if(!this.connectEdge(d,a)||!this.clipEdge(d,a)||this.verticesAreEqual(d.va,d.vb)){this.destroyEdge(d);b.splice(e,1)}}};Voronoi.prototype.verticesAreEqual=function(a,b){return this.equalWithEpsilon(a.x,b.x)&&this.equalWithEpsilon(a.y,b.y)};Voronoi.prototype.sortHalfedgesCallback=function(a,b){var c=a.getStartpoint();var d=a.getEndpoint();var e=b.getStartpoint();var f=b.getEndpoint();return Math.atan2(f.y-e.y,f.x-e.x)-Math.atan2(d.y-c.y,d.x-c.x)};Voronoi.prototype.closeCells=function(a){var b=a.xl;var c=a.xr;var d=a.yt;var e=a.yb;this.clipEdges(a);var f=this.cells;var g;var h,iRight;var i,nHalfedges;var j;var k,endpoint;var l,vb;for(var m in f){g=f[m];if(!(g instanceof this.Cell)){continue}i=g.halfedges;h=i.length;while(h){iRight=h;while(iRight>0&&i[iRight-1].isLineSegment()){iRight--}h=iRight;while(h>0&&!i[h-1].isLineSegment()){h--}if(h===iRight){break}i.splice(h,iRight-h)}if(i.length===0){f.removeCell(g);continue}i.sort(this.sortHalfedgesCallback);nHalfedges=i.length;h=0;while(h=0;e--){d=this.sites[e];if(!d.id){this.sites.splice(e,1)}else{this.queuePushSite({type:this.SITE_EVENT,x:d.x,y:d.y,site:d})}}this.arcs=[];this.edges=[];this.cells=new this.Cells();var f=this.queuePop();while(f){if(f.type===this.SITE_EVENT){this.cells.addCell(new this.Cell(f.site));this.addArc(f.site)}else if(f.type===this.CIRCLE_EVENT){this.removeArc(f)}else{this.queueSanitize()}f=this.queuePop()}this.closeCells(a);var g=new Date();var h={sites:this.sites,cells:this.cells,edges:this.edges,execTime:g.getTime()-b.getTime()};this.arcs=[];this.edges=[];this.cells=new this.Cells();return h}; + +module.exports = function() +{ + return new Voronoi(); +} diff --git a/examples/voronoi.js b/examples/voronoi.js new file mode 100644 index 0000000..ecd4f4b --- /dev/null +++ b/examples/voronoi.js @@ -0,0 +1,137 @@ + +/** + * Module dependencies. + */ + +var Canvas = require('../lib/canvas') + , canvas = new Canvas(1920, 1200) + , ctx = canvas.getContext('2d') + , http = require('http') + , fs = require('fs'); + +var voronoiFactory = require('./rhill-voronoi-core-min.js'); + +http.createServer(function (req, res) { + var voronoi = voronoiFactory(); + var bbox = { xl: 0, xr: canvas.width, yt: 0, yb: canvas.height }; + + for (var i =0 ;i<340;i++) + { + var x = Math.random()*canvas.width; + var y = Math.random()*canvas.height; + voronoi.addSites([{x:x,y:y}]); + }; + var diagram = voronoi.compute(bbox); + + ctx.beginPath(); + ctx.rect(0,0,canvas.width,canvas.height); + ctx.fillStyle = '#fff'; + ctx.fill(); + ctx.strokeStyle = 'black'; + ctx.stroke(); + // voronoi + ctx.strokeStyle='rgba(255,255,255,0.5)'; + ctx.lineWidth = 4; + // edges + var edges = diagram.edges; + var nEdges = edges.length; + + var sites = diagram.sites; + var nSites = sites.length; + for (var iSite=nSites-1; iSite>=0; iSite-=1) + { + site = sites[iSite]; + ctx.rect(site.x-0.5,site.y-0.5,1,1); + + + +// ctx.stroke(); + var cell = diagram.cells[diagram.sites[iSite].id]; + if (cell !== undefined) + { + var halfedges = cell.halfedges; + var nHalfedges = halfedges.length; + if (nHalfedges < 3) {return;} + var minx = canvas.width; + var miny = canvas.height; + var maxx = 0; + var maxy = 0; + + var v = halfedges[0].getStartpoint(); + ctx.beginPath(); + ctx.moveTo(v.x,v.y); + + for (var iHalfedge=0; iHalfedge maxx) maxx = v.x; + if (v.y> maxy) maxy = v.y; + } + var C = Math.floor(Math.random()*128 + 127).toString(); + + + var midx = (maxx+minx)/2; + var midy = (maxy+miny)/2; + var R = 0; + + for (var iHalfedge=0; iHalfedgeR) R = newR; + } + midx = site.x; + midy = site.y; + var radgrad = ctx.createRadialGradient(midx+R*0.3,midy-R*0.3,0,midx,midy,R); + radgrad.addColorStop(0, "#09760b"); + radgrad.addColorStop(1.0, "black"); + + + ctx.fillStyle = radgrad; + ctx.fill(); + + var radgrad2 = ctx.createRadialGradient(midx-R*0.5,midy+R*0.5,R*0.1,midx,midy,R); + radgrad2.addColorStop(0, "rgba(255,255,255,0.5)"); + radgrad2.addColorStop(0.04, "rgba(255,255,255,0.3)"); + radgrad2.addColorStop(0.05, "rgba(255,255,255,0)"); + + + ctx.fillStyle = radgrad2; + ctx.fill(); + + var lingrad = ctx.createLinearGradient(minx, site.y, minx+100, site.y-20); + lingrad.addColorStop(0.0, "rgba(255,255,255,0.5)"); + lingrad.addColorStop(0.2, "rgba(255,255,255,0.2)"); + lingrad.addColorStop(1.0, "rgba(255,255,255,0)"); + ctx.fillStyle = lingrad; + ctx.fill(); + + } + + } + + if (nEdges) + { + var edge, v; + ctx.beginPath(); + for (var iEdge=nEdges-1; iEdge>=0; iEdge-=1) { + edge = edges[iEdge]; + v = edge.va; + ctx.moveTo(v.x,v.y); + v = edge.vb; + ctx.lineTo(v.x,v.y); + } + ctx.stroke(); + } + + canvas.toBuffer(function(err, buf){ + res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': buf.length }); + res.end(buf); + }); +}).listen(3000); +console.log('Server running on port 3000');