/*! Copyright 2010 Raymond Hill Author: Raymond Hill File: rhill-voronoi.js Version: 0.9 Date: Sep. 12, 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 Fortune: For his clever algorithm to compute Voronoi diagrams. http://ect.bell-labs.com/who/sjf/ * Alec McEachran's code to translate a parabola's focus & directrix into parameters for HTML5 canvas' quadraticCurveTo() method. http://alecmce.com/as3/parabolas-and-quadratic-bezier-curves * "The Liang-Barsky line clipping algorithm in a nutshell!", to efficiently clip a line within a rectangle. http://www.skytopia.com/project/articles/compsci/clipping.html * "Event properties / Mouse position" by Peter-Paul Koch, for his code snippet on how to correctly detect mouse coordinates. http://www.quirksmode.org/js/events_properties.html#position Permission to use, copy, modify, and distribute this software for any purpose without fee is hereby granted, provided that this entire notice is included in all copies of any software which is or includes a copy or modification of this software and in all copies of the supporting documentation for such software. THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. 2011-02-14: Lower epsilon from 1e-5 to 1e-4, to fix problem reported at http://www.raymondhill.net/blog/?p=9#comment-1414 */ /* Update: Ho aggiunto al software originale di Raymond Hill la funzione PenroseSites(arr) e ne ho cambiato diverse scelte grafiche e strutturali per adeguare questo codice alle mie applicazioni HTML riguardanti il disegno delle tassellazioni di Roger Penrose e altri. Il risultato che ho raggiunto consiste nel visualizzare, sopra la tassellazione già disegnata, il diagramma di Voronoi dei punti corrispondenti ai vertici dei tasselli costituenti la tassellazione stessa. Raymond Hill, come si legge sopra, mi ha espressamente consentito di trasformare il suo software per scopi didattici e scientifici e voglio qui ringraziarlo per questo. Antonio Binetti 15/03/2018 Nella funzione init() ho introdotto due variabili di stato: this.color; this.width. Il valore di queste variabili può essere programmate dall'utente per variare lo spessore di linea ed il suo colore. 5/05/2021 */ var Voronoi = { // // Properties // sites: [], siteEvents: [], circEvents: [], arcs: [], edges: [], sweep: 0, SITE_EVENT: 0, CIRCLE_EVENT: 1, VOID_EVENT: -1, DEFAULT_NUM_SITES: 100, NUM_SITES_PROCESSED: 0, BINARY_SEARCHES: 0, BINARY_SEARCH_ITERATIONS: 0, PARABOLIC_CUT_CALCS: 0, ALL_PARABOLIC_CUT_CALCS: 0, BEACHLINE_SIZE: 0, CIRCLE_QUEUE_SIZE: 0, NUM_VOID_EVENTS: 0, NUM_CIRCLE_EVENTS: 0, TOTAL_NUM_EDGES: 0, NUM_DESTROYED_EDGES: 0, sqrt: self.Math.sqrt, abs: self.Math.abs, floor: self.Math.floor, random: self.Math.random, round: self.Math.round, min: self.Math.min, max: self.Math.max, pow: self.Math.pow, PI: self.Math.PI, isNaN: self.isNaN, DEFAULT_CANVAS_WIDTH:Math.round(1360*screen.width/1366), DEFAULT_CANVAS_HEIGHT:Math.round(720*screen.height/768), canvas: null, canvasMargin:0, bbox: {xl:0,xr:Math.round(1360*screen.width/1366),yt:0,yb:Math.round(720*screen.height/768)}, // // Objects // Beachsection: function(site) { this.site = site; this.edge = null; // below is strictly for caching purpose this.sweep = -Infinity; this.lid = 0; this.circleEvent = undefined; }, Site: function(x,y) { this.id = this.constructor.prototype.idgenerator++; this.x = x; this.y = y; }, Cell: function(site) { this.site = site; this.halfedges = []; }, Edge: function(lSite,rSite) { this.id = this.constructor.prototype.idgenerator++; this.lSite = lSite; this.rSite = rSite; this.va = this.vb = undefined; }, Vertex: function(x,y) { this.x = x; this.y = y; }, Halfedge: function(site,edge) { this.site = site; this.edge = edge; }, // // Debugging stuff // assert: function(v) { if (!v) {debugger;} }, // // Methods // init: function(color,width){ this.color=color; this.width=width; // prototype our inner classes, more efficient than having these Javascript // properties repeated for all instances. this.Beachsection.prototype.PARENT = this; this.Beachsection.prototype.sqrt = self.Math.sqrt; // given parabola 'site', return the intersection with parabola 'left' // immediately to the left of x this.Beachsection.prototype._leftParabolicCut=function(site,left,directrix) { // change code below at your own risk: // care has been taken to reduce errors due to // computers' finite arithmetic precision. // maybe can still be improved, will see if any // more of this kind of errors pop up again this.PARENT.PARABOLIC_CUT_CALCS++; var rfocx = site.x; var rfocy = site.y; // parabola in degenerate case where focus is on directrix if (rfocy == directrix) {return rfocx;} var lfocx = left.x; var lfocy = left.y; // parabola in degenerate case where focus is on directrix if (lfocy == directrix) {return lfocx;} // both parabolas have same distance to directrix, thus break point is midway if (rfocy == lfocy) {return (rfocx+lfocx)/2;} // calculate break point the normal way var pby2 = rfocy-directrix; var plby2 = lfocy-directrix; var hl = lfocx-rfocx; var aby2 = 1/pby2-1/plby2; var b = hl/plby2; return (-b+this.sqrt(b*b-2*aby2*(hl*hl/(-2*plby2)-lfocy+plby2/2+rfocy-pby2/2)))/aby2+rfocx; }; // higher level method which caches result and attempt to reuse it this.Beachsection.prototype.leftParabolicCut=function(left,sweep){ this.PARENT.ALL_PARABOLIC_CUT_CALCS++; if (this.sweep !== sweep || this.lid !== left.id) { this.sweep = sweep; this.lid = left.id; this.lBreak = this._leftParabolicCut(this.site,left,sweep); } return this.lBreak; }; this.Beachsection.prototype.isCollapsing=function(){ return this.circleEvent !== undefined && this.circleEvent.type === this.PARENT.CIRCLE_EVENT; }; this.Site.prototype.idgenerator = 1; this.Edge.prototype.isLineSegment = function() { return Boolean(this.id) && Boolean(this.va) && Boolean(this.vb); }; this.Edge.prototype.idgenerator = 1; this.Halfedge.prototype.isLineSegment = function() { return Boolean(this.edge.id) && Boolean(this.edge.va) && Boolean(this.edge.vb); }; this.Halfedge.prototype.getStartpoint = function() { return this.edge.lSite.id == this.site.id ? this.edge.va : this.edge.vb; }; this.Halfedge.prototype.getEndpoint = function() { return this.edge.lSite.id == this.site.id ? this.edge.vb : this.edge.va; }; // prepare canvas this.initCanvas(); // and randomly generate a bunch of sites to have something to see //this.generateSites(this.DEFAULT_NUM_SITES); }, // // Epsilon-based comparison methods // // Note: changed to 1e-5, 1e-6 was still causing errors once in a while EPSILON: 1e-4, equalWithEpsilon: function(a,b){return this.abs(a-b)<1e-4;}, greaterThanWithEpsilon: function(a,b){return (a-b)>1e-4;}, greaterThanOrEqualWithEpsilon: function(a,b){return (b-a)<1e-4;}, lessThanWithEpsilon: function(a,b){return (b-a)>1e-4;}, lessThanOrEqualWithEpsilon: function(a,b){return (a-b)<1e-4;}, // // Sites management methods // clearSites: function() { this.sites = []; this.reset(); // reset id generators this.Site.prototype.idgenerator = 1; this.Edge.prototype.idgenerator = 1; }, addSite: function(x,y) { this.sites.push(new this.Site(x,y)); this.reset(); this.processQueueAll(); }, generateSites: function(n) { this.randomSites(n); this.reset(); this.processQueueAll(); }, randomSites: function(n) { var margin = this.canvasMargin; var xo = this.bbox.xl+margin; var dx = this.bbox.xr-margin*2; var yo = this.bbox.yt+margin; var dy = this.bbox.yb-margin*2; for (var i=0; i>1; if (this.lessThanWithEpsilon(x,this.leftBreakPoint(i,sweep))) { r = i; continue; } // check if x after right break point if (this.greaterThanOrEqualWithEpsilon(x,this.rightBreakPoint(i,sweep))) { l = i+1; continue; } return i; } return l; }, // INFO: Chromium profiling shows this a hot spot findDeletionPoint: function(x, sweep) { this.BINARY_SEARCHES++; var n = this.arcs.length; if (!n) { return 0; } var l = 0; var r = n; var i; var xcut; while (l>1; xcut = this.leftBreakPoint(i,sweep); if (this.lessThanWithEpsilon(x,xcut)) { r=i; continue; } if (this.greaterThanWithEpsilon(x,xcut)) { l = i+1; continue; } xcut = this.rightBreakPoint(i,sweep); if (this.greaterThanWithEpsilon(x,xcut)) { l = i+1; continue; } if (this.lessThanWithEpsilon(x,xcut)) { r = i; continue; } return i; } //this.assert(false); }, // this create and add an edge to internal collection, and also create // two halfedges which are added to each site's counterclockwise array // of halfedges. createEdge: function(lSite,rSite,va,vb) { var edge = new this.Edge(lSite,rSite); this.edges.push(edge); //this.assert(this.cells[lSite.id] != undefined); //this.assert(this.cells[rSite.id] != undefined); if (va !== undefined) { this.setEdgeStartpoint(edge,lSite,rSite,va); } if (vb !== undefined) { this.setEdgeEndpoint(edge,lSite,rSite,vb); } this.cells[lSite.id].halfedges.push(new this.Halfedge(lSite,edge)); this.cells[rSite.id].halfedges.push(new this.Halfedge(rSite,edge)); return edge; }, createBorderEdge: function(lSite,va,vb) { var edge = new this.Edge(lSite,null); edge.va = va; edge.vb = vb; this.edges.push(edge); return edge; }, destroyEdge: function(edge) { edge.id = edge.va = edge.vb = undefined; }, setEdgeStartpoint: function(edge, lSite, rSite, vertex) { //this.assert((edge.lSite.id == lSite.id && edge.rSite.id == rSite.id) || (edge.rSite.id == lSite.id && edge.lSite.id == rSite.id)); // I use to assert both va and vb weren't the same, but this // can happen under normal circumstances, this should not be // treated as an error if (edge.va === undefined && edge.vb === undefined) { edge.va = vertex; edge.lSite = lSite; edge.rSite = rSite; } else if (edge.lSite.id == rSite.id) { //this.assert(edge.vb === undefined); edge.vb = vertex; } else { //this.assert(edge.va === undefined); edge.va = vertex; } }, setEdgeEndpoint: function(edge, lSite, rSite, vertex) { this.setEdgeStartpoint(edge,rSite,lSite,vertex); }, removeArc: function(event) { var x = event.center.x; var y = event.center.y; var sweep = event.y; var deletionPoint = this.findDeletionPoint(x, sweep); // there could be more than one empty arc at the deletion point, this // happens when more than two edges are linked by the same vertex, // so we will collect all those edges by looking up both sides of // the deletion point // look left var iLeft = deletionPoint; while (iLeft-1 > 0 && this.equalWithEpsilon(x,this.leftBreakPoint(iLeft-1,sweep)) ) { iLeft--; } // look right var iRight = deletionPoint; while (iRight+1 < this.arcs.length && this.equalWithEpsilon(x,this.rightBreakPoint(iRight+1,sweep)) ) { iRight++; } // walk through all the collapsed beach sections and set the start point // of their left edge var lArc, rArc; for (var iArc=iLeft; iArc<=iRight+1; iArc++) { lArc = this.arcs[iArc-1]; rArc = this.arcs[iArc]; this.setEdgeStartpoint(rArc.edge,lArc.site,rArc.site,new this.Vertex(x,y)); } // void circle events of collapsed beach sections and adjacent beach sections this.voidCircleEvents(iLeft-1,iRight+1); // removed collapsed beach sections from beachline this.arcs.splice(iLeft,iRight-iLeft+1); // create new edge as we have a new transition between // two beach sections which were previously not adjacent lArc = this.arcs[iLeft-1]; rArc = this.arcs[iLeft]; rArc.edge = this.createEdge(lArc.site,rArc.site,undefined,new this.Vertex(x,y)); // create circle events if any for beach sections left in the beachline // adjacent to collapsed sections this.addCircleEvents(iLeft-1,sweep); this.addCircleEvents(iLeft,sweep); }, addArc: function(site) { // find insertion point of new beach section on the beachline var newArc = new this.Beachsection(site); var insertionPoint = this.findInsertionPoint(site.x,site.y); // case: insert as last beach section, this case can happen only // when *all* previously processed sites have exactly the same // y coordinate. // this case can't result in collapsing beach sections, thus // no circle events need to be generated. if (insertionPoint == this.arcs.length) { // add new beach section this.arcs.push(newArc); // case: first beach section ever means no transitions, means // no edge is created if (insertionPoint === 0) {return;} // case: a new transition between two beach sections is // created, create an edge for these two beach sections newArc.edge = this.createEdge(this.arcs[insertionPoint-1].site,newArc.site); return; } var lArc, rArc; // case: new beach section to insert falls exactly // in between two existing beach sections: // the net result is that the transition between two existing beach // sections is destroyed -- aka a new end point for one edge is // defined, and two new transitions are created -- aka two new edges // are defined. if (insertionPoint > 0 && this.equalWithEpsilon(site.x,this.rightBreakPoint(insertionPoint-1,site.y)) && this.equalWithEpsilon(site.x,this.leftBreakPoint(insertionPoint,site.y))) { // before adding dddd: // arcs: aaaaaaaa bbbbbbbb cccccccc // edges: ab bc // ^ // after adding dddd: // arcs: aaaaaaaa dddd bbbbbbbb cccccccc // edges: ad bd bc // ^ // transition ab disappears, meaning a new vertex is defined, // while transition ad and bd appear, meaning two new edges are // defined lArc = this.arcs[insertionPoint-1]; rArc = this.arcs[insertionPoint]; // invalidate circle events of left and right sites this.voidCircleEvents(insertionPoint-1,insertionPoint); // an existing transition disappears, meaning a vertex is defined at the // disappearance point var circle = this.circumcircle(lArc.site,site,rArc.site); this.setEdgeStartpoint(rArc.edge,lArc.site,rArc.site,new this.Vertex(circle.x,circle.y)); // two new transitions appear at the new vertex location newArc.edge = this.createEdge(lArc.site,newArc.site,undefined,new this.Vertex(circle.x,circle.y)); rArc.edge = this.createEdge(newArc.site,rArc.site,undefined,new this.Vertex(circle.x,circle.y)); // insert new beach section this.arcs.splice(insertionPoint,0,newArc); // check whether the left and right beach sections are collapsing // and if so create circle events, to handle the point of collapse. this.addCircleEvents(insertionPoint-1,site.y); this.addCircleEvents(insertionPoint+1,site.y); return; } // case: this is the most-likely case, where an existing beach section // is split by the new beach section to insert. // adding a new beach section in the middle of an existing one causes two new // transitions to appear -- but since both transitions involve the same two // sites, only one single edge is created, and assigned to two beach front // transitions (the 'edge' member of the beach section.) // invalidate circle event possibly associated with the beach section // to split this.voidCircleEvents(insertionPoint); // before: // arcs: aaaaaaaa bbbbbbbb cccccccc // edges: ab bc // after: // arcs: aaaaaaaa bbbb dddd bbbb cccccccc // edges: ab bd db bc // ^ ^ // bd & db are actually the same edge, the orientation has just // not been decided yet // insert new beach section into beachline lArc = this.arcs[insertionPoint]; rArc = new this.Beachsection(lArc.site); this.arcs.splice(insertionPoint+1,0,newArc,rArc); // since we have a new transition between two beach sections, // a new edge is born newArc.edge = rArc.edge = this.createEdge(lArc.site,newArc.site); // check whether the left and right beach sections are collapsing // and if so create circle events, to handle the point of collapse. this.addCircleEvents(insertionPoint,site.y); this.addCircleEvents(insertionPoint+2,site.y); }, circumcircle: function(a,b,c) { var ax=a.x; var ay=a.y; var bx=b.x-ax; var by=b.y-ay; var cx=c.x-ax; var cy=c.y-ay; var d=2*(bx*cy-by*cx); var hb=bx*bx+by*by; var hc=cx*cx+cy*cy; var x=(cy*hb-by*hc)/d; var y=(bx*hc-cx*hb)/d; return {x:x+ax,y:y+ay,radius:this.sqrt(x*x+y*y)}; }, addCircleEvents: function(iArc,sweep) { if (iArc <= 0 || iArc >= this.arcs.length-1) {return;} var arc=this.arcs[iArc]; var lSite=this.arcs[iArc-1].site; var cSite=this.arcs[iArc].site; var rSite=this.arcs[iArc+1].site; // if any two sites are repeated in the same beach section triplet, // there can't be convergence if (lSite.id==rSite.id || lSite.id==cSite.id || cSite.id==rSite.id) {return;} // if points l->c->r are clockwise, then center beach section does not // converge, hence it can't end up as a vertex if ((lSite.y-cSite.y)*(rSite.x-cSite.x)<=(lSite.x-cSite.x)*(rSite.y-cSite.y)) {return;} // find circumscribed circle var circle=this.circumcircle(lSite,cSite,rSite); // not valid if the bottom-most point of the circumcircle // is above the sweep line // TODO: And what if it is on the sweep line, should it be discarded if it is // *before* the last processed x value? Need to think about this. var ybottom=circle.y+circle.radius; if (!this.greaterThanOrEqualWithEpsilon(ybottom,sweep)) {return;} var circEvent={ type: this.CIRCLE_EVENT, site: cSite, x: circle.x, y: ybottom, center: {x:circle.x, y:circle.y} }; arc.circleEvent = circEvent; this.queuePushCircle(circEvent); }, voidCircleEvents: function(iLeft,iRight) { if ( iRight === undefined ) {iRight = iLeft;} iLeft = this.max(iLeft,0); iRight = this.min(iRight,this.arcs.length-1); while (iLeft <= iRight) { var arc = this.arcs[iLeft]; if ( arc.circleEvent !== undefined ) { arc.circleEvent.type = this.VOID_EVENT; // after profiling in Chromium, found out assigning 'undefined' is much more efficient than // using 'delete' on the property, possibly because 'delete' causes a 're-classify' to trigger arc.circleEvent = undefined; } iLeft++; } }, queueInit: function() { this.sweep = 0; this.siteEvents = []; var n = this.sites.length; for (var i=0; i0 && q[iRight-1].type !== this.VOID_EVENT) {iRight--;} if (iRight<=0) {break;} // find a right-most non-void event immediately to the left of iRight iLeft = iRight-1; while (iLeft>0 && q[iLeft-1].type === this.VOID_EVENT) {iLeft--;} nEvents = iRight-iLeft; this.NUM_VOID_EVENTS += nEvents; q.splice(iLeft,nEvents); // abort if queue has gotten small enough, this allow // to avoid having to go through the whole array, most // circle events are added toward the end of the queue if (q.length < nArcs) {return;} } }, queueIsEmpty: function() { this.queueSanitize(); return this.siteEvents.length === 0 && this.circEvents.length === 0; }, queuePeek: function() { this.queueSanitize(); // we will return a site or circle event var siteEvent = this.siteEvents.length > 0 ? this.siteEvents[this.siteEvents.length-1] : null; var circEvent = this.circEvents.length > 0 ? this.circEvents[this.circEvents.length-1] : null; // if one and only one is null, the other is a valid event if ( Boolean(siteEvent) !== Boolean(circEvent) ) { return siteEvent ? siteEvent : circEvent; } // both queues are empty if (!siteEvent) { return null; } // both queues have valid events, return 'earliest' if (siteEvent.y < circEvent.y || (siteEvent.y == circEvent.y && siteEvent.x < circEvent.x)) { return siteEvent; } return circEvent; }, queuePop: function() { var event = this.queuePeek(); if (event) { if (event.type === this.SITE_EVENT) { this.siteEvents.pop(); } else { this.circEvents.pop(); } } return event; }, queuePushSite: function(o) { var q = this.siteEvents; 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 if (c<0) {l = i+1;} else {return; /*Duplicate sites not allowed, quietly ignored*/ } } q.splice(l,0,o); } else { q.push(o); } }, queuePushCircle: function(o) { this.NUM_CIRCLE_EVENTS++; 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); } }, processQueueOne: function() { var event = this.queuePop(); if (!event) {return;} this.sweep = event.y; if ( event.type === this.SITE_EVENT ) { //this.assert(this.cells[event.site.id] === undefined); this.cells[event.site.id] = new this.Cell(event.site); // add beach section this.addArc(event.site); this.BEACHLINE_SIZE += this.arcs.length; this.CIRCLE_QUEUE_SIZE += this.circEvents.length; this.LARGEST_CIRCLE_QUEUE_SIZE = this.max(this.circEvents.length,this.LARGEST_CIRCLE_QUEUE_SIZE); } else { //this.assert(event.type === this.CIRCLE_EVENT); // remove beach section this.removeArc(event); } // wrap-up: close all cells if (this.queueIsEmpty()) { this.closeCells(); } }, processQueueN: function(n) { while (n > 0 && !this.queueIsEmpty()) { this.processQueueOne(); n -= 1; } if (this.queueIsEmpty()) { this.sweep = this.max(this.sweep,this.canvas.height); } }, processQueueAll: function() { this.processQueueN(999999999); this.sweep = this.max(this.sweep,this.canvas.height); this.dumpBeachline(); this.draw(); }, processUpTo: function(y) { var event; while (!this.queueIsEmpty()) { event = this.queuePeek(); if (event.y > y) {break;} this.processQueueOne(); } // let's not go backward this.sweep = this.max(this.sweep,y); // empty queue if sweep line is no longer visible if (this.sweep > this.canvas.height) { this.processQueueN(999999999); } }, getBisector: function(va,vb) { var r = {x:(va.x+vb.x)/2,y:(va.y+vb.y)/2}; if (vb.y==va.y) {return r;} r.m = (va.x-vb.x)/(vb.y-va.y); r.b = r.y-r.m*r.x; return r; }, // connect a dangling edge (not if a cursory test tells us // it is not going to be visible. // return value: // false: the dangling endpoint couldn't be connected // true: the dangling endpoint could be connected connectEdge: function(edge) { var vb = edge.vb; if (!!vb) {return true;} var va = edge.va; var xl = this.bbox.xl; var xr = this.bbox.xr; var yt = this.bbox.yt; var yb = this.bbox.yb; // get the line formula of the bisector var lSite = edge.lSite; var rSite = edge.rSite; var f = this.getBisector(lSite,rSite); // remember, direction of line (relative to left site): // upward: left.x < right.x // downward: left.x > right.x // horizontal: left.x == right.x // upward: left.x < right.x // rightward: left.y < right.y // leftward: left.y > right.y // vertical: left.y == right.y // depending on the direction, find the best side of the // bounding box to use to determine a reasonable start point // special case: vertical line if (f.m === undefined) { //this.assert(lSite.x === rSite.x); // doesn't intersect with viewport if (f.x < xl || f.x >= xr) {return false;} // downward if (lSite.x > rSite.x) { if (va === undefined) { va = new this.Vertex(f.x,yt); } else if (va.y >= yb) { return false; } vb = new this.Vertex(f.x,yb); } // upward else { if (va === undefined) { va = new this.Vertex(f.x,yb); } else if (va.y < yt) { return false; } vb = new this.Vertex(f.x,yt); } } // closer to horizontal than vertical, connect start point to the // left or right side of the bounding box else if (f.m < 1) { // rightward if (lSite.y < rSite.y) { if (va === undefined) { va = new this.Vertex(xl,f.m*xl+f.b); } else if (va.x >= xr) { return false; } vb = new this.Vertex(xr,f.m*xr+f.b); } // leftward else { if (va === undefined) { va = new this.Vertex(xr,f.m*xr+f.b); } else if (va.x < xl) { return false; } vb = new this.Vertex(xl,f.m*xl+f.b); } } // closer to vertical than horizontal, connect start point to the // top or bottom side of the bounding box else { // downward if (lSite.x > rSite.x) { if (va === undefined) { va = new this.Vertex((yt-f.b)/f.m,yt); } else if (va.y >= yb) { return false; } vb = new this.Vertex((yb-f.b)/f.m,yb); } // upward else { if (va === undefined) { va = new this.Vertex((yb-f.b)/f.m,yb); } else if (va.y < yt) { return false; } vb = new this.Vertex((yt-f.b)/f.m,yt); } } //this.assert(va !== undefined && vb !== undefined); edge.va = va; edge.vb = vb; return true; }, // line-clipping code taken from: // The Liang-Barsky line clipping algorithm in a nutshell! // http://www.skytopia.com/project/articles/compsci/clipping.html // Thanks! // A bit modified to minimize code paths clipEdge: function(edge) { // at this point no dangling edge is expected //this.assert(edge.va !== undefined && edge.vb !== undefined); var ax = edge.va.x; var ay = edge.va.y; var bx = edge.vb.x; var by = edge.vb.y; var t0 = 0; var t1 = 1; var dx = bx-ax; var dy = by-ay; // left var q = ax-this.bbox.xl; if (dx===0 && q<0) {return false;} var r = -q/dx; if (dx<0) { if (r0) { if (r>t1) {return false;} else if (r>t0) {t0=r;} } // right q = this.bbox.xr-ax; if (dx===0 && q<0) {return false;} r = q/dx; if (dx<0) { if (r>t1) {return false;} else if (r>t0) {t0=r;} } else if (dx>0) { if (r0) { if (r>t1) {return false;} else if (r>t0) {t0=r;} } // bottom q = this.bbox.yb-ay; if (dy===0 && q<0) {return false;} r = q/dy; if (dy<0) { if (r>t1) {return false;} else if (r>t0) {t0=r;} } else if (dy>0) { if (r=0; iEdge-=1) { edge = edges[iEdge]; if (!this.connectEdge(edge) || !this.clipEdge(edge) || this.verticesAreEqual(edge.va,edge.vb)) { this.NUM_DESTROYED_EDGES++; this.destroyEdge(edge); edges.splice(iEdge,1); } } }, verticesAreEqual: function(a,b) { return this.equalWithEpsilon(a.x,b.x) && this.equalWithEpsilon(a.y,b.y); }, // this function is used to sort halfedges counterclockwise sortHalfedgesCallback: function(a,b) { var ava = a.getStartpoint(); var avb = a.getEndpoint(); var bva = b.getStartpoint(); var bvb = b.getEndpoint(); return self.Math.atan2(bvb.y-bva.y,bvb.x-bva.x) - self.Math.atan2(avb.y-ava.y,avb.x-ava.x); }, validateCells: function(cell) { var halfedges = cell.halfedges; var nHalfedges = halfedges.length; var halfedge; for (var iHalfedge=0; iHalfedge0 && halfedges[iRight-1].isLineSegment()) {iRight--;} iLeft = iRight; while (iLeft>0 && !halfedges[iLeft-1].isLineSegment()) {iLeft--;} if (iLeft === iRight) {break;} halfedges.splice(iLeft,iRight-iLeft); } // remove cell if it has zero halfedges if (halfedges.length === 0) { delete cells[cellid]; continue; } // reorder segments counterclockwise halfedges.sort(this.sortHalfedgesCallback); // close open cells // step 1: find first 'unclosed' point, if any. // an 'unclosed' point will be the end point of a halfedge which // does not match the start point of the following halfedge nHalfedges = halfedges.length; // special case: only one site, in which case, the viewport is the cell // ... // all other cases iLeft = 0; while (iLeft < nHalfedges) { iRight = (iLeft+1) % nHalfedges; endpoint = halfedges[iLeft].getEndpoint(); startpoint = halfedges[iRight].getStartpoint(); if (!this.verticesAreEqual(endpoint,startpoint)) { // if we reach this point, cell needs to be closed by walking // counterclockwise along the bounding box until it connects // to next halfedge in the list va = new this.Vertex(endpoint.x,endpoint.y); // walk downward along left side if (this.equalWithEpsilon(endpoint.x,xl) && this.lessThanWithEpsilon(endpoint.y,yb)) { vb = new this.Vertex(xl,this.equalWithEpsilon(startpoint.x,xl) ? startpoint.y : yb); } // walk rightward along bottom side else if (this.equalWithEpsilon(endpoint.y,yb) && this.lessThanWithEpsilon(endpoint.x,xr)) { vb = new this.Vertex(this.equalWithEpsilon(startpoint.y,yb) ? startpoint.x : xr,yb); } // walk upward along right side else if (this.equalWithEpsilon(endpoint.x,xr) && this.greaterThanWithEpsilon(endpoint.y,yt)) { vb = new this.Vertex(xr,this.equalWithEpsilon(startpoint.x,xr) ? startpoint.y : yt); } // walk leftward along top side else if (this.equalWithEpsilon(endpoint.y,yt) && this.greaterThanWithEpsilon(endpoint.x,xl)) { vb = new this.Vertex(this.equalWithEpsilon(startpoint.y,yt) ? startpoint.x : xl,yt); } edge = this.createBorderEdge(cell.site,va,vb); halfedges.splice(iLeft+1,0,new this.Halfedge(cell.site,edge)); nHalfedges = halfedges.length; } iLeft++; } } this.cellsClosed = true; }, getCells: function() { this.closeCells(); return this.cells; }, initCanvas: function() { if (this.canvas) {return;} var canvas = document.getElementById('tavola'); if (!canvas.getContext) {return;} var ctx = canvas.getContext('2d'); if (!ctx) {return;} //canvas.width = this.DEFAULT_CANVAS_WIDTH; //canvas.height = this.DEFAULT_CANVAS_HEIGHT; /* ctx.fillStyle='#fff'; ctx.rect(0,0,canvas.width,canvas.height); */ //ctx.fill(); ctx.strokeStyle = '#ffff00'; //ctx.stroke(); this.canvas = canvas; /* // event handlers var me = this; canvas.onclick = function(e) { if (!e) {e=self.event;} // ----- // http://www.quirksmode.org/js/events_properties.html#position var x = 0; var y = 0; if (e.pageX || e.pageY) { x = e.pageX; y = e.pageY; } else if (e.clientX || e.clientY) { x = e.clientX+document.body.scrollLeft+document.documentElement.scrollLeft; y = e.clientY+document.body.scrollTop+document.documentElement.scrollTop; } // ----- me.addSite(x-this.offsetLeft,y-this.offsetTop); }; */ }, setCanvasSize: function(w,h) { if (this.isNaN(w) || this.isNaN(h)) {return;} this.canvas.width = this.max(Number(w),100); this.canvas.height = this.max(Number(h),100); this.bbox.xl = 0; this.bbox.xr = w; this.bbox.yt = 0; this.bbox.yb = h; this.canvasMargin = this.min(this.canvasMargin,w/4,h/4); this.draw(); }, setCanvasMargin: function(margin) { if (this.isNaN(margin) || margin < 0) {return;} this.canvasMargin = Number(margin); }, draw: function() { var ctx = this.canvas.getContext('2d'); //this.drawBackground(ctx); this.drawSites(ctx); this.drawEdges(ctx); // sweep line if (this.sweep < this.canvas.height) { ctx.globalAlpha=0.9; ctx.strokeStyle='#00ff00'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(0,this.sweep); ctx.lineTo(this.canvas.width,this.sweep); ctx.stroke(); } //if (!this.queueIsEmpty()) { this.drawVertices(ctx); // } if (this.sweep < this.canvas.height) { this.drawBeachline(ctx); } }, drawBackground: function(ctx) { /* ctx.globalAlpha = 0.1; ctx.beginPath(); ctx.rect(0,0,this.canvas.width,this.canvas.height); ctx.fillStyle = '#fff'; ctx.fill(); ctx.strokeStyle = '#888'; ctx.stroke(); */ }, drawSites: function(ctx) { var queueIsEmpty = this.queueIsEmpty(); ctx.beginPath(); var nSites=this.sites.length; for (var iSite=0; iSite 0); v = halfedges[0].getStartpoint(); ctx.beginPath(); ctx.moveTo(v.x,v.y); for (iHalfedge=0; iHalfedge0 ? this.arcs[iArc-1] : null; // neighbour is also a degenerate? if (!neighbour || neighbour.site.y == directrix) { neighbour = iArc < this.arcs.length-1 ? this.arcs[iArc+1] : null; } // both neighbours are degenerate? if (!neighbour || neighbour.site.y == directrix) { yr = 0; } // found a nice neighbour, compute quadratic equation as usual else { p = (neighbour.site.y-directrix)/2; yr = this.pow(focx-neighbour.site.x,2)/(4*p)+neighbour.site.y-p; } ctx.strokeStyle = '#080'; ctx.beginPath(); ctx.moveTo(focx,focy); ctx.lineTo(focx,yr); ctx.stroke(); xl=xr; yl=yr; continue; } // typical case, we need to find right cut point, oh and btw, // no need to go beyond the viewport xr = this.min(this.rightBreakPoint(iArc,directrix),cw); p = (focy-directrix)/2; yr = this.pow(xr-focx,2)/(4*p)+focy-p; // bother to draw only if beach section is within sight if (xr >= 0 && xl < cw && xr > xl) { // non-collapsing beach sections in green, collapsing ones in red ctx.strokeStyle = arc.isCollapsing() ? '#800' : '#080'; // How to draw a parabola segment using canvas' quadraticCurveTo: // http://alecmce.com/as3/parabolas-and-quadratic-bezier-curves // Thanks! // Of course, I was able to simplify code because here I only draw parabolas // which are oriented vertically and always pointing up ac_x = focx-xl; ac_y = focy-directrix; bc_x = focx-xr; bc_y = focy-directrix; gx = (xr+focx)/2; gy = (directrix+focy)/2; n = ((gx-(xl+focx)/2)*ac_x+(gy-(directrix+focy)/2)*ac_y)/(bc_y*ac_x-bc_x*ac_y); ctx.beginPath(); ctx.moveTo(xl,yl); ctx.quadraticCurveTo(gx-bc_y*n,gy+bc_x*n,xr,yr); ctx.stroke(); } // current right cut become next iteration's left cut xl=xr; yl=yr; } }, drawVertices: function(ctx) { ctx.beginPath(); ctx.globalAlpha=1; var nEdges=this.edges.length; var edge; var va, vb; for (var iEdge=0; iEdgeAvg number of iterations per binary search: '+(this.BINARY_SEARCH_ITERATIONS/this.BINARY_SEARCHES).toFixed(2); html+='
Number of parabolic cut calculations: '+this.PARABOLIC_CUT_CALCS+' out of '+this.ALL_PARABOLIC_CUT_CALCS+' total'; html+='
Average beachline size: '+(this.BEACHLINE_SIZE/this.sites.length).toFixed(2); html+='
Average circle event queue size: '+(this.CIRCLE_QUEUE_SIZE/this.sites.length).toFixed(2); html+='
Total number of cancelled circle events: '+this.NUM_VOID_EVENTS+' out of '+this.NUM_CIRCLE_EVENTS+' total ('+(this.NUM_VOID_EVENTS/this.NUM_CIRCLE_EVENTS*100).toFixed(0)+'%)'; html+='
Largest circle events queue size: '+this.LARGEST_CIRCLE_QUEUE_SIZE+' events'; html+='
Number of destroyed edges (outside the viewport): '+this.NUM_DESTROYED_EDGES+' out of a total of '+this.TOTAL_NUM_EDGES+' edges

'; // Beachline var arc; var edge; var htmledge; var nArcs=this.arcs.length; html+='Beachline is composed of '+nArcs+' beach sections:
'; for (var iArc=0; iArc, y:'+(edge.va.y).toFixed(1)+')';} if (edge.vb) {htmledge+=', end=(x:'+(edge.vb.x).toFixed(1)+', y:'+(edge.vb.y).toFixed(1)+')';} } else { htmledge+='none'; } if (!edge) { htmledge=''+htmledge; } else if (!edge.va && !edge.vb) { htmledge=''+htmledge; } else { //this.assert((edge.va === undefined) != (edge.vb === undefined)); htmledge=''+htmledge; } html+=htmledge+'
'; // then display beach section details var xleft=this.leftBreakPoint(iArc,this.sweep); var xright=this.rightBreakPoint(iArc,this.sweep); html+=''; html+='xl='+xleft.toFixed(4)+', xr='+xright.toFixed(4)+', site={id:'+arc.site.id+', x:'+arc.site.x+', y:'+arc.site.y+'}'; if (arc.isCollapsing()) { html+=', collapsing at {x:'+arc.circleEvent.x.toFixed(4)+', y:'+arc.circleEvent.y.toFixed(4)+'}'; } html+='
'; } var el=document.getElementById('console'); if (el) {el.innerHTML=html;} } }; // TODO: fix this var VoronoiAnimateTimer; var VoronoiAnimatePixels; var VoronoiAnimateDelay; function VoronoiAnimateCallback() { VoronoiAnimateTimer = undefined; Voronoi.processUpTo(Voronoi.sweep+VoronoiAnimatePixels); Voronoi.draw(); if (!Voronoi.queueIsEmpty() || Voronoi.sweep < Voronoi.bbox.yb) { VoronoiAnimateTimer = setTimeout(VoronoiAnimateCallback,VoronoiAnimateDelay); } else { Voronoi.dumpBeachline(); } } function VoronoiAnimate(px,ms) { if (VoronoiAnimateTimer !== undefined) { clearTimeout(VoronoiAnimateTimer); VoronoiAnimateTimer = undefined; } if (Voronoi.queueIsEmpty()) { Voronoi.reset(); } // sanitize parameters VoronoiAnimatePixels = self.isNaN(px) ? 5 : Voronoi.max(px,1); // 10ms looks crazy but Chromium is lightning fast VoronoiAnimateDelay = self.isNaN(ms) ? 200 : Voronoi.max(ms,1); VoronoiAnimateTimer = setTimeout(VoronoiAnimateCallback,VoronoiAnimateDelay); } function VoronoiAnimateStop() { if (VoronoiAnimateTimer !== undefined) { clearTimeout(VoronoiAnimateTimer); VoronoiAnimateTimer = undefined; Voronoi.dumpBeachline(); } }