Jekyll2021-11-30T05:40:35+00:00https://sam.zhang.fyi/feed.xmlsamzhang111.github.ioBayesian inference for COVID-19 testing2021-10-12T12:00:00+00:002021-10-12T12:00:00+00:00https://sam.zhang.fyi/2021/10/12/covid<p>See <a href="https://github.com/samzhang111/seroprevalence-viz">source</a>.</p>See source.Ridge unfolding polytopes2021-10-12T12:00:00+00:002021-10-12T12:00:00+00:00https://sam.zhang.fyi/2021/10/12/unfolding<p>See <a href="/html/unfolding/index.html">the full screen page.</a></p>See the full screen page.Curve shortening2020-10-29T01:44:00+00:002020-10-29T01:44:00+00:00https://sam.zhang.fyi/2020/10/29/curve-shortening<div style="display: flex; width: 1020px; margin: 2em 0; justify-content:space-between; border: 3px solid black; border-radius: 30px"> <div id="control" style="flex:0 1 auto;width: 500px;height:500px;border-right: 3px solid black"></div> <div id="curve-2d" style="flex:0 1 auto;width: 500px;height:500px;border: none;"></div> </div> <p><a href="/html/fullscreen/curve-shortening">Full screen version</a>.</p> <p>Suppose you had a smooth curve $C(s, t)$ parameterized by arclength and time, and you decided to continuously deform $C$ through time analogously to the heat equation. That is, parts of the curve with high curvature are very “hot” and would like to flow toward the “cold” parts that have negative curvature. We set</p> <p>$\frac{\partial C}{\partial t} = \frac{\partial^2 C}{\partial^2 s} = \kappa n$</p> <p>where $\kappa$ is the curvature and $n$ is the unit normal vector to the curve, so that the curve is shortened, and parts of the curve with larger curvature are shortened more. This is called <a href="https://en.wikipedia.org/wiki/Curve-shortening_flow">curve shortening</a>, and it leads to deep areas of geometric analysis. One major result is the <a href="https://en.wikipedia.org/wiki/Curve-shortening_flow#Gage%E2%80%93Hamilton%E2%80%93Grayson_theorem">Gage-Hamilton-Grayson theorem</a>, which states that under the curve shortening flow, simple closed curves remain smoothly embedded without self-intersections until they become convex, after which they stay convex, before converging to a circle as the curve shrinks to a point.</p> <p>But how do we implement curve shortening on a computer? Many discretizations have been proposed, and here we implement a particular algorithm proposed by <a href="https://www.jstor.org/stable/27642194?seq=1#metadata_info_tab_contents">Chow and Glickenstein (2007)</a>. The procedure is analogous to the continuous case, except since we have a polygon instead of a smooth curve, we do not have a “curvature” at any point. Instead, we approximate the normal vector for the vertex $x_{i, t}$ by setting</p> <p>$n_{i, t} = (x_{i+1, t} - x_i) + (x_{i-1, t} - x_{i, t})$</p> <p>and performing the discrete update step</p> <p>$x_{i, t+1} = x_{i, t} + \delta n_{i, t}$</p> <p>for some step size $\delta &gt; 0$.</p> <p>Do we have the equivalent of the Gage-Hamilton-Grayson theorem for this discrete curve shortening flow? Not in its entirety. Some results are known, such as that the curve shortens to a point, and it is asympototically an affine transformation of a regular polygon. But a key piece still unknown is whether a curve that starts without self-intersections will remain without self-intersections for each time step, for sufficiently small $\delta$.</p> <p>In the visualization above, there are parameters to control $\delta$, the number of iterations, and the number of points on the polygon. I did not worry at all about numerical issues, as you might discover! The points on the polygon can of course be moved. The visualization on the right is actually an interactive three-dimensional figure shown with a top-down view. The temporal aspect of the curve-shortening flow is portrayed in color, but also in height, since one can consider the flow as slices in time of a higher dimensional object. I reparameterized the height to be spaced out in the earlier steps since the beginning steps of the algorithm are some of the more interesting. As for all the 3D stuff on this blog, the <a href="/html/fullscreen/curve-shortening">full screen version</a> is much more satisfying than the tiny figures above.</p> <p>I was tempted into making this thanks to the beautiful pictures in <a href="https://press.princeton.edu/books/hardcover/9780691145532/discrete-and-computational-geometry">Discrete and Computational Geometry, by Satyan Devadoss and Joseph O’Rourke</a>.</p>A catastrophe machine2020-01-20T12:00:00+00:002020-01-20T12:00:00+00:00https://sam.zhang.fyi/2020/01/20/catastrophe-machine<p><a href="https://en.wikipedia.org/wiki/Catastrophe_theory">Catastrophe theory</a> was a popular branch of mathematics concerned with qualitative characterizations of ways that singularities relate to potential energy functions. The name “catastrophe theory” comes from how a continuous change in a parameter of a potential function can introduce a <em>discontinuous</em> jump in observed phenomena, and this can be catastrophic in certain cases, like with a buckling beam. I say the field “was” popular, rather than “is”, because it was a classic example of a field of applied mathematics that was overhyped, applied too widely without adequate attention toward scientific fundamentals, and the backlash all but killed it. However, there is some talk about it recently becoming popular again for chemical applications. (Hence why this blog post!)</p> <p>Plus, the mathematics is indisputably elegant – that was never in doubt, even during the controversy around it – and one can develop a basic intuition for it using simple physical examples that are well-suited for a website like this. I’ve created an interactive “catastrophe machine” that exhibits the basic form of the “cusp” catastrophe. Imagine a wedge shaped like a parabola balancing on a table (see thick blue outline in left figure below). The parameter that we will alter continuously is its center of mass (green dot in figure; drag it around!), and the discontinuity comes in the wedge’s equilibrium resting position.</p> <p>Move the center of mass to the center-top of the parabola, within the dotted cusp (called the <em>bifurcation set</em>). Notice that two local minima emerge on the potential function. As the center of mass crosses the bifurcation set, one of the local minima disappear. If that was the one that the parabola was resting in, then the parabola undergoes a dramatic change in behavior: a “catastrophe” as it’s been termed.</p> <p>Note that in the visualization, the line representing the <em>ground</em> moves when the center of mass moves, rather than the parabola itself. I only drew the point corresponding to the global minimum - perhaps it would have been more accurate to include both. Also note that the right hand figure is interactive.</p> <div style="display: flex; flex-wrap: wrap; width: 100%; margin: 3px auto; justify-content:space-between; border: 3px solid black; border-radius: 10px; overflow: hidden"> <div id="catastrophe-control" style="flex:1 0 auto; width: 20em; height: 30em; border-right: 3px solid black"></div> <div id="catastrophe-surface" style="flex:1 0 auto; width: 20em; height: 30em; border: none;"></div> </div> <p><a href="/html/fullscreen/catastrophe">Full screen version</a>.</p> <p>The right hand figure is the so-called <em>catastrophe surface</em> for this machine. For each possible center of gravity $(x, y)$ in the parabola, we give it z-coordinates for the critical points of the energy function, in other words, the zeros of the derivative of the potential energy function. That turns out to be a cubic polynomial in this case. The roots of a cubic polynomial vary continuously with the coefficients, and there can be one, two, or three roots. The catastrophe occurs when the point must “jump” across the fold on the catastrophe surface.</p> <p>Two accessible introductions to the subject for anyone who has taken multivariable calculus are <a href="https://www.amazon.com/Curves-Singularities-Geometrical-Introduction-Singularity/dp/0521429994">Curves and Singularities</a> by J.W. Bruce and P.J. Giblin; <a href="https://store.doverpublications.com/048669271x.html">Catastrophe Theory and its Applications</a> by Tim Poston and Ian Stewart. Bruce and Giblin analyze this particular catastrophe in detail in Chapter 1, which they call the Poston catastrophe machine. <a href="http://faculty.washington.edu/etsb/402W12/materials/Zahler_Sussman_claims_and_accomplishments_of_applied_catastrophe_nature_77.pdf">This critique</a> of catastrophe theory is among the more famous in mathematics. I think it still makes a great read, for the types of issues one encounters while doing modeling.</p>Catastrophe theory was a popular branch of mathematics concerned with qualitative characterizations of ways that singularities relate to potential energy functions. The name “catastrophe theory” comes from how a continuous change in a parameter of a potential function can introduce a discontinuous jump in observed phenomena, and this can be catastrophic in certain cases, like with a buckling beam. I say the field “was” popular, rather than “is”, because it was a classic example of a field of applied mathematics that was overhyped, applied too widely without adequate attention toward scientific fundamentals, and the backlash all but killed it. However, there is some talk about it recently becoming popular again for chemical applications. (Hence why this blog post!)Linear regression, the old fashioned way2019-06-21T12:00:00+00:002019-06-21T12:00:00+00:00https://sam.zhang.fyi/2019/06/21/least-squares-springs<p>Suppose you want to fit an ordinary least squares model to a set of points in $\mathbb{R}^2$, but you zoned out during statistics class and now you’re stuck on a desert island. In fact, all you have are a wooden board, a hammer, a bunch of nails, some good old zero-length springs, frictionless cloth loops, and a frictionless rod.</p> <p>Pretend your wooden board is the plane, and decide on a grid system. Then nail each of your data points into the board. Attach to each nail a zero-length spring that can only move up or down. On the other end of each spring place a little cloth loop, for hanging something.</p> <p>Now imagine taking the long frictionless rod, and threading it through each of these loops. The equilibrium state it reaches is the line of best fit!</p> <p>I made an interactive demonstration of this, but it only works in full screen (click to open):</p> <p><a href="/html/fullscreen/springs" style="outline:solid"><img src="/images/thumbnails/springs.png?raw=true" width="50%" alt="Screenshot of fullscreen springs app" /></a></p> <p><a href="/html/fullscreen/springs">Full screen</a></p> <p>You can drag on the rod, but you have to be <em>very</em> accurate with your mouse. Refreshing the page gives a new random set of points.</p> <p>One can derive the optimality of the equilibrium state using the fact that the potential energy of a zero-length string extended by distance $u$ is $\frac{k}{2}u^2$, where $k$ is Hooke’s constant. It doesn’t matter what Hooke’s constant is, as long as it’s positive, so we can set it to $k=2$ to eliminate the fraction. One ends up with the total potential energy of the system $\sum_{i=0}^n (Ax_i - y_i)^2$, where $(x_i, y_i)$ are the points (nails), and $A$ is the function that tells the $y$ value of the rod at point $x_i$. We know $A$ is linear since the rod is straight. This happens to be the loss function for ordinary least squares!</p> <p>Squaring is convex, and $Ax_i -y_i$ is affine. Composing convex functions with affine ones gives convex functions, and adding convex functions gives convex functions. So the whole loss function is convex. If there is more than one data point, it is strongly convex, and the only minimum in the system is the unique global minimum. In other words, the resting point for the rod attached to the springs is the (unique) line of best fit. Hooray!</p> <p>Why do the springs have to go straight down? Well, they don’t have to! If you loosen that restriction, then you end up with a <a href="https://en.wikipedia.org/wiki/Total_least_squares">Total Least Squares</a> regression instead. A TLS model allows for error not just in the $y$ axis, like OLS, but also the $x$ axis. For example, suppose you took noisy x-y plane GPS measurements along a straight trail, and you wanted to estimate a line through the actual trail. Since there is error on both $x$ and $y$, one can use TLS. (If we assume the variance on $x$ and $y$ here is the same, then one actually drops into a subcase of TLS called <a href="https://en.wikipedia.org/wiki/Deming_regression#Orthogonal_regression">orthogonal regression</a>.)</p> <p>The example was made using <a href="https://github.com/shakiba/planck.js">planck.js</a>, which is a nice javascript port of <a href="https://box2d.org">Box2D</a>, although it was sorely lacking in documentation. For instance, I couldn’t find an easy way to run my demo outside of fullscreen.</p> <p>This setup came from the book, <a href="https://press.princeton.edu/titles/8861.html">The Mathematical Mechanic</a>, which is full of perversities like this.</p>Suppose you want to fit an ordinary least squares model to a set of points in $\mathbb{R}^2$, but you zoned out during statistics class and now you’re stuck on a desert island. In fact, all you have are a wooden board, a hammer, a bunch of nails, some good old zero-length springs, frictionless cloth loops, and a frictionless rod.A moduli space for triangles2019-02-12T12:00:00+00:002019-02-12T12:00:00+00:00https://sam.zhang.fyi/2019/02/12/a-moduli-space-for-triangles<p>Charles Dodgson, <a href="https://en.wikipedia.org/wiki/Lewis_Carroll">better known as Lewis Carroll</a>, once posed the following mathematical question: what is the probability that a random triangle is obtuse?</p> <p>The answer he gave was incorrect, and several subsequent attempts to correct the answer also fell short. In a <a href="https://arxiv.org/abs/1702.01027">2017 paper from Jason Canterella, Tom Needham, Clayton Shonkwiler, and Gavin Stewart</a>, a nice resolution is presented to this problem.</p> <p>The key issue with Dodgson’s original solution was that in order to say what probability a random triangle is obtuse, he had to first come up with a space that triangles lived in, and find what part of that space corresponded to obtuse triangles. This turns out to be a subtle issue. Canterella et al. solve this by associating each n-gons with a so-called Grassmanian of 2-planes in $\mathbb{R}^n$. This blog post is a demonstration of their parameterization, omitting all of the details :), using triangles and $\mathbb{R}^3$.</p> <p>In short, there is a way to send any n-gon with a fixed perimeter to a pair of orthonormal vectors in $\mathbb{R^n}$ up to translation (technically, $2^n$ pairs, but they are all identified with each other). The space of all pairs of orthonormal vectors is a <a href="https://en.wikipedia.org/wiki/Stiefel_manifold">Stiefel manifold</a>, and that is the space we would operate in if we cared about the orientation (as in rotations) of the polygon. But what if we don’t care about the orientation? It turns out we get lucky and rotating the polygon sends these pairs of orthonormal vectors to the same spanning plane. Hence why the Grassmannian of 2-planes serves as the moduli space for polygons when we don’t care about rotations.</p> <p>Since we’re in $\mathbb{R}^3$ here, we have an identification between the Grassmannian of 2-planes and the real projective plane $\mathbb{R}P^2$ (through the normal vector to the 2-planes). Recall that the real projective plane can be visualized as lines through a sphere: that is what you are looking at in the top pane.</p> <div id="triangles-overlay" style="width:600px;height:600px;margin:auto;border:3px solid black"></div> <div style="display: flex; width: 600px; margin: 3px auto; justify-content:space-between"> <div id="triangle1" style="flex:0 1 auto;width:280px;height:300px;border: 3px solid black"></div> <div id="triangle2" style="flex:0 1 auto;width:280px;height:300px;border: 3px solid black;"></div> </div> <div id="triangles-grassman" style="width:600px;height:600px;border:3px solid black;margin:auto"></div> <p><a href="/html/fullscreen/triangles-moduli">Full screen version</a>.</p> <p>The top pane (the sphere) is the moduli space, with both the yellow and green triangles from the middle panes displayed on it as yellow and green lines. The moduli space is eight-fold covered by the sphere, hence why there are eight lines of both colors. All of the lines of the same color are quotiented together. For appearance’s sake, I highlighted one of cosets in blue and thickened the representative vectors through that sector.</p> <p>The yellow line connecting the two distinguished representatives is a geodesic. Thus in some sense, it represents an optimal way to transform the green triangle into the yellow one, or vice versa. That transformation is what you see in the bottom pane.</p> <p>You can move the vertices of the triangles around in the middle two panes and watch it move in the moduli space. I normalize the perimeters and make some arbitrary choices when it comes to orientation.</p> <p>Hat tip to <a href="http://math.colorado.edu/~rohi1040">Rob Hines</a>, who first told me about this.</p>Charles Dodgson, better known as Lewis Carroll, once posed the following mathematical question: what is the probability that a random triangle is obtuse?The cross-section of a cylinder is an ellipse2019-01-26T12:00:00+00:002019-01-26T12:00:00+00:00https://sam.zhang.fyi/2019/01/26/cylinder-ellipse<p>Here’s an interactive WebGL-ification of the visual diagram that goes along with the proof of the elementary fact that the cross-section of a cylinder is an ellipse:</p> <div id="cylinder" style="width:600px;height:600px;margin-right:auto"></div> <p><a href="/html/fullscreen/cylinder">Full screen version</a>.</p> <p>I was inspired to make this when I saw the diagram below in <a href="https://www.maa.org/press/maa-reviews/geometry-and-the-imagination">Geometry and the Imagination</a>.</p> <p><img src="/images/hilbert-cylinder.png" align="right" /></p> <p>Given a cylinder with radius $r$, and the plane slicing through it, the proof goes as follows. Imagine the cylinder is hollow, with two spheres placed snugly (that is, of radius $r$) into it, so that they touch against the intersecting plane. The claim is that these points where the sphere touch the cross-section are the foci of the ellipse. We’ll show this is an ellipse by proving that the sum of the distance from the foci to any point on the boundary is constant.</p> <p>Take an arbitrary point on the ellipse, $p$. Then let’s call the lengths of the two red lines $r_{v, p}$ and $r_{h, p}$ for the vertical red line and the “horizontal” (ellipse-bound) red line from $p$. The interactive visualization above loops over the values of $p$. Likewise, the lengths of the blue lines will be denoted $b_{v, p}$ and $b_{h, p}$.</p> <p>Then notice that the two red lines in the visualization are both tangent to the sphere: the wall of the cylinder is tangent to the sphere at all points where they make contact, and the intersecting plane is tangent to the sphere.</p> <p>Now recall (or convince yourself) that tangents from a sphere that intersect at the same point are of the same length. That is, $r_{v, p} = r_{h, p}$, and $b_{v, p} = b_{h, p}$ for all $p$.</p> <p>Moreover, the two vertical lines always sum up to a constant: $r_{v, p} + b_{v, p} = K$ for all $p$.</p> <p>Combining those two equations gives $r_{h, p} + b_{h, p} = K$ for all $p$, which shows that the cross-section is an ellipse.</p> <p>I think it’s a testament to good mathematical illustration that the drawing from Geometry and the Imagination is easier to follow (for me) than the interactive 3d one. But there is something very neat about seeing these old diagrams “come to life”.</p> <p>I made this using the library <a href="https://github.com/unconed/mathbox/">mathbox</a>.</p>Here’s an interactive WebGL-ification of the visual diagram that goes along with the proof of the elementary fact that the cross-section of a cylinder is an ellipse:Whitney’s Umbrella2019-01-24T12:00:00+00:002019-01-24T12:00:00+00:00https://sam.zhang.fyi/2019/01/24/whitneys-umbrella<p>I’ve been test driving the excellent <a href="https://github.com/unconed/mathbox/">mathbox</a> WebGL library. For starters, here’s a pan-and-zoomable <a href="https://en.wikipedia.org/wiki/Whitney_umbrella">Whitney’s umbrella</a>:</p> <div id="umbrella" style="width:600px;height:600px;margin-right:auto"></div> <p><a href="/html/fullscreen/umbrella">Full screen version</a>.</p> <p>I have been thinking about how to organize the contents of this blog more visually as well. Thus I have created a preliminary version of <a href="/html/gallery">“a mathematical zoo”</a>.</p> <p>My main inspirations for beautiful mathematical illustrations have been <a href="https://www.maa.org/press/maa-reviews/geometry-and-the-imagination">Geometry and the Imagination</a> and <a href="https://www.springer.com/us/book/9780387345420">A Topological Picturebook</a>. As I become more comfortable with mathbox, and (painfully) shake the rust off of my d3 skills (while learning d3 v4), I hope more of the images from those books make their way to this blog.</p>I’ve been test driving the excellent mathbox WebGL library. For starters, here’s a pan-and-zoomable Whitney’s umbrella:Voronoizer bookmarklet2019-01-11T12:00:00+00:002019-01-11T12:00:00+00:00https://sam.zhang.fyi/2019/01/11/voronoi-bookmarklet<p>As promised in my previous post on the <a href="/2019/01/10/voronoize-this-page/">“voronoizer”</a>, I have made the voronoizer into a bookmarklet that you can run on any page whose Content Security Policy permits.</p> <p>Step 1. Drag the following link into your bookmarks toolbar: <a href="javascript:var voronoize=function(t){var e={};function n(i){if(e[i])return e[i].exports;var s=e[i]={i:i,l:!1,exports:{}};return t[i].call(s.exports,s,s.exports,n),s.l=!0,s.exports}return n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){&quot;undefined&quot;!=typeof Symbol&amp;&amp;Symbol.toStringTag&amp;&amp;Object.defineProperty(t,Symbol.toStringTag,{value:&quot;Module&quot;}),Object.defineProperty(t,&quot;__esModule&quot;,{value:!0})},n.t=function(t,e){if(1&amp;e&amp;&amp;(t=n(t)),8&amp;e)return t;if(4&amp;e&amp;&amp;&quot;object&quot;==typeof t&amp;&amp;t&amp;&amp;t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,&quot;default&quot;,{enumerable:!0,value:t}),2&amp;e&amp;&amp;&quot;string&quot;!=typeof t)for(var s in t)n.d(i,s,function(e){return t[e]}.bind(null,s));return i},n.n=function(t){var e=t&amp;&amp;t.__esModule?function(){return t.default}:function(){return t};return n.d(e,&quot;a&quot;,e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p=&quot;&quot;,n(n.s=0)}([function(t,e,n){&quot;use strict&quot;;n.r(e);const i=Math.pow(2,-52);class s{static from(t,e,n){e||(e=d),n||(n=f);const i=t.length,r=new Float64Array(2*i);for(let s=0;s&lt;i;s++){const i=t[s];r[2*s]=e(i),r[2*s+1]=n(i)}return new s(r)}constructor(t){let e=1/0,n=1/0,s=-1/0,d=-1/0;const f=t.length&gt;&gt;1,y=this.ids=new Uint32Array(f);if(f&gt;0&amp;&amp;&quot;number&quot;!=typeof t)throw new Error(&quot;Expected coords to contain numbers.&quot;);this.coords=t;for(let i=0;i&lt;f;i++){const r=t[2*i],o=t[2*i+1];r&lt;e&amp;&amp;(e=r),o&lt;n&amp;&amp;(n=o),r&gt;s&amp;&amp;(s=r),o&gt;d&amp;&amp;(d=o),y[i]=i}const g=(e+s)/2,x=(n+d)/2;let m,_,v,p=1/0;for(let e=0;e&lt;f;e++){const n=r(g,x,t[2*e],t[2*e+1]);n&lt;p&amp;&amp;(m=e,p=n)}const w=t[2*m],b=t[2*m+1];p=1/0;for(let e=0;e&lt;f;e++){if(e===m)continue;const n=r(w,b,t[2*e],t[2*e+1]);n&lt;p&amp;&amp;n&gt;0&amp;&amp;(_=e,p=n)}let k=t[2*_],T=t[2*_+1],E=1/0;for(let e=0;e&lt;f;e++){if(e===m||e===_)continue;const n=l(w,b,k,T,t[2*e],t[2*e+1]);n&lt;E&amp;&amp;(v=e,E=n)}let M=t[2*v],P=t[2*v+1];if(E===1/0)throw new Error(&quot;No Delaunay triangulation exists for this input.&quot;);if(o(w,b,k,T,M,P)){const t=_,e=k,n=T;_=v,k=M,T=P,v=t,M=e,P=n}const S=function(t,e,n,i,s,r){const o=n-t,l=i-e,h=s-t,a=r-e,c=o*o+l*l,u=h*h+a*a,d=o*a-l*h;return{x:t+.5*(a*c-l*u)/d,y:e+.5*(o*u-h*c)/d}}(w,b,k,T,M,P);this._cx=S.x,this._cy=S.y,function t(e,n,i,s,r,o){let l,h,a;if(s-i&lt;=20)for(l=i+1;l&lt;=s;l++){for(a=e[l],h=l-1;h&gt;=i&amp;&amp;c(n,e[h],a,r,o)&gt;0;)e[h+1]=e[h--];e[h+1]=a}else{const d=i+s&gt;&gt;1;for(h=s,u(e,d,l=i+1),c(n,e[i],e[s],r,o)&gt;0&amp;&amp;u(e,i,s),c(n,e[l],e[s],r,o)&gt;0&amp;&amp;u(e,l,s),c(n,e[i],e[l],r,o)&gt;0&amp;&amp;u(e,i,l),a=e[l];;){do{l++}while(c(n,e[l],a,r,o)&lt;0);do{h--}while(c(n,e[h],a,r,o)&gt;0);if(h&lt;l)break;u(e,l,h)}e[i+1]=e[h],e[h]=a,s-l+1&gt;=h-i?(t(e,n,l,s,r,o),t(e,n,i,h-1,r,o)):(t(e,n,i,h-1,r,o),t(e,n,l,s,r,o))}}(y,t,0,y.length-1,S.x,S.y),this._hashSize=Math.ceil(Math.sqrt(f)),this._hash=new Array(this._hashSize);let A=this.hull=h(t,m);this._hashEdge(A),A.t=0,A=h(t,_,A),this._hashEdge(A),A.t=1,A=h(t,v,A),this._hashEdge(A),A.t=2;const L=2*f-5,$=this.triangles=new Uint32Array(3*L),z=this.halfedges=new Int32Array(3*L);this.trianglesLen=0,this._addTriangle(m,_,v,-1,-1,-1);for(let e,n,s=0;s&lt;y.length;s++){const r=y[s],l=t[2*r],c=t[2*r+1];if(s&gt;0&amp;&amp;Math.abs(l-e)&lt;=i&amp;&amp;Math.abs(c-n)&lt;=i)continue;if(e=l,n=c,r===m||r===_||r===v)continue;const u=this._hashKey(l,c);let d,f=u;do{d=this._hash[f],f=(f+1)%this._hashSize}while((!d||d.removed)&amp;&amp;f!==u);for(A=d=d.prev;!o(l,c,A.x,A.y,A.next.x,A.next.y);)if((A=A.next)===d){A=null;break}if(!A)continue;const g=A===d;let x=this._addTriangle(A.i,r,A.next.i,-1,-1,A.t);A.t=x,(A=h(t,r,A)).t=this._legalize(x+2);let p=A.next;for(;o(l,c,p.x,p.y,p.next.x,p.next.y);)x=this._addTriangle(p.i,r,p.next.i,p.prev.t,-1,p.t),p.prev.t=this._legalize(x+2),this.hull=a(p),p=p.next;if(g)for(p=A.prev;o(l,c,p.prev.x,p.prev.y,p.x,p.y);)x=this._addTriangle(p.prev.i,r,p.i,-1,p.t,p.prev.t),this._legalize(x+2),p.prev.t=x,this.hull=a(p),p=p.prev;this._hashEdge(A),this._hashEdge(A.prev)}this.triangles=$.subarray(0,this.trianglesLen),this.halfedges=z.subarray(0,this.trianglesLen)}_hashEdge(t){this._hash[this._hashKey(t.x,t.y)]=t}_hashKey(t,e){return Math.floor(function(t,e){const n=t/(Math.abs(t)+Math.abs(e));return(e&gt;0?3-n:1+n)/4}(t-this._cx,e-this._cy)*this._hashSize)%this._hashSize}_legalize(t){const{triangles:e,coords:n,halfedges:i}=this,s=i[t],r=t-t%3,o=s-s%3,l=r+(t+1)%3,h=r+(t+2)%3,a=o+(s+2)%3;if(-1===s)return h;const c=e[h],u=e[t],d=e[l],f=e[a];if(function(t,e,n,i,s,r,o,l){const h=t-o,a=e-l,c=n-o,u=i-l,d=s-o,f=r-l,y=c*c+u*u,g=d*d+f*f;return h*(u*g-y*f)-a*(c*g-y*d)+(h*h+a*a)*(c*f-u*d)&lt;0}(n[2*c],n[2*c+1],n[2*u],n[2*u+1],n[2*d],n[2*d+1],n[2*f],n[2*f+1])){e[t]=f,e[s]=c;const n=i[a];if(-1===n){let e=this.hull;do{if(e.t===a){e.t=t;break}e=e.next}while(e!==this.hull)}this._link(t,n),this._link(s,i[h]),this._link(h,a);const r=o+(s+1)%3;return this._legalize(t),this._legalize(r)}return h}_link(t,e){this.halfedges[t]=e,-1!==e&amp;&amp;(this.halfedges[e]=t)}_addTriangle(t,e,n,i,s,r){const o=this.trianglesLen;return this.triangles[o]=t,this.triangles[o+1]=e,this.triangles[o+2]=n,this._link(o,i),this._link(o+1,s),this._link(o+2,r),this.trianglesLen+=3,o}}function r(t,e,n,i){const s=t-n,r=e-i;return s*s+r*r}function o(t,e,n,i,s,r){return(i-e)*(s-n)-(n-t)*(r-i)&lt;0}function l(t,e,n,i,s,r){const o=n-t,l=i-e,h=s-t,a=r-e,c=o*o+l*l,u=h*h+a*a,d=o*a-l*h,f=.5*(a*c-l*u)/d,y=.5*(o*u-h*c)/d;return c&amp;&amp;u&amp;&amp;d&amp;&amp;f*f+y*y||1/0}function h(t,e,n){const i={i:e,x:t[2*e],y:t[2*e+1],t:0,prev:null,next:null,removed:!1};return n?(i.next=n.next,i.prev=n,n.next.prev=i,n.next=i):(i.prev=i,i.next=i),i}function a(t){return t.prev.next=t.next,t.next.prev=t.prev,t.removed=!0,t.prev}function c(t,e,n,i,s){return r(t[2*e],t[2*e+1],i,s)-r(t[2*n],t[2*n+1],i,s)||t[2*e]-t[2*n]||t[2*e+1]-t[2*n+1]}function u(t,e,n){const i=t[e];t[e]=t[n],t[n]=i}function d(t){return t}function f(t){return t}const y=1e-6;class g{constructor(){this._x0=this._y0=this._x1=this._y1=null,this._=&quot;&quot;}moveTo(t,e){this._+=M${this._x0=this._x1=+t},${this._y0=this._y1=+e}}closePath(){null!==this._x1&amp;&amp;(this._x1=this._x0,this._y1=this._y0,this._+=&quot;Z&quot;)}lineTo(t,e){this._+=L${this._x1=+t},${this._y1=+e}}arc(t,e,n){const i=(t=+t)+(n=+n),s=e=+e;if(n&lt;0)throw new Error(&quot;negative radius&quot;);null===this._x1?this._+=M${i},${s}:(Math.abs(this._x1-i)&gt;y||Math.abs(this._y1-s)&gt;y)&amp;&amp;(this._+=&quot;L&quot;+i+&quot;,&quot;+s),n&amp;&amp;(this._+=A${n},${n},0,1,1,${t-n},${e}A${n},${n},0,1,1,${this._x1=i},${this._y1=s})}rect(t,e,n,i){this._+=M${this._x0=this._x1=+t},${this._y0=this._y1=+e}h${+n}v${+i}h\${-n}Z}value(){return this._||null}}class x{constructor(){this._=[]}moveTo(t,e){this._.push([t,e])}closePath(){this._.push(this._.slice())}lineTo(t,e){this._.push([t,e])}value(){return this._.length?this._:null}}class m{constructor(t,[e,n,i,s]=[0,0,960,500]){if(!((i=+i)&gt;=(e=+e)&amp;&amp;(s=+s)&gt;=(n=+n)))throw new Error(&quot;invalid bounds&quot;);const{points:r,hull:o,triangles:l}=this.delaunay=t,h=this.circumcenters=new Float64Array(l.length/3*2),a=this.vectors=new Float64Array(2*r.length);this.xmax=i,this.xmin=e,this.ymax=s,this.ymin=n;for(let t=0,e=0,n=l.length;t&lt;n;t+=3,e+=2){const n=2*l[t],i=2*l[t+1],s=2*l[t+2],o=r[n],a=r[n+1],c=r[i],u=r[i+1],d=r[s],f=r[s+1],y=o-c,g=o-d,x=a-u,m=a-f,_=o*o+a*a,v=_-c*c-u*u,p=_-d*d-f*f,w=2*(g*x-y*m);h[e]=(x*p-m*v)/w,h[e+1]=(g*v-y*p)/w}let c,u,d,f=o,y=4*f.i,g=f.x,x=f.y;do{c=y,u=g,d=x,y=4*(f=f.next).i,g=f.x,x=f.y,a[c+2]=a[y]=d-x,a[c+3]=a[y+1]=g-u}while(f!==o)}render(t){const e=null==t?t=new g:void 0,{delaunay:{halfedges:n,hull:i},circumcenters:s,vectors:r}=this;for(let e=0,i=n.length;e&lt;i;++e){const i=n[e];if(i&lt;e)continue;const r=2*Math.floor(e/3),o=2*Math.floor(i/3),l=s[r],h=s[r+1],a=s[o],c=s[o+1];this._renderSegment(l,h,a,c,t)}let o=i;do{o=o.next;const e=2*Math.floor(o.t/3),n=s[e],i=s[e+1],l=4*o.i,h=this._project(n,i,r[l+2],r[l+3]);h&amp;&amp;this._renderSegment(n,i,h,h,t)}while(o!==i);return e&amp;&amp;e.value()}renderBounds(t){const e=null==t?t=new g:void 0;return t.rect(this.xmin,this.ymin,this.xmax-this.xmin,this.ymax-this.ymin),e&amp;&amp;e.value()}renderCell(t,e){const n=null==e?e=new g:void 0,i=this._clip(t);if(null!==i){e.moveTo(i,i);for(let t=2,n=i.length;t&lt;n;t+=2)e.lineTo(i[t],i[t+1]);return e.closePath(),n&amp;&amp;n.value()}}*cellPolygons(){const{delaunay:{points:t}}=this;for(let e=0,n=t.length/2;e&lt;n;++e){const t=this.cellPolygon(e);t&amp;&amp;(yield t)}}cellPolygon(t){const e=new x;return this.renderCell(t,e),e.value()}_renderSegment(t,e,n,i,s){let r;const o=this._regioncode(t,e),l=this._regioncode(n,i);0===o&amp;&amp;0===l?(s.moveTo(t,e),s.lineTo(n,i)):(r=this._clipSegment(t,e,n,i,o,l))&amp;&amp;(s.moveTo(r,r),s.lineTo(r,r))}contains(t,e,n){return(e=+e)==e&amp;&amp;(n=+n)==n&amp;&amp;this.delaunay._step(t,e,n)===t}_cell(t){const{circumcenters:e,delaunay:{inedges:n,halfedges:i,triangles:s}}=this,r=n[t];if(-1===r)return null;const o=[];let l=r;do{const n=Math.floor(l/3);if(o.push(e[2*n],e[2*n+1]),s[l=l%3==2?l-2:l+1]!==t)break;l=i[l]}while(l!==r&amp;&amp;-1!==l);return o}_clip(t){const e=this._cell(t);if(null===e)return null;const{vectors:n}=this,i=4*t;return n[i]||n[i+1]?this._clipInfinite(t,e,n[i],n[i+1],n[i+2],n[i+3]):this._clipFinite(t,e)}_clipFinite(t,e){const n=e.length;let i,s,r,o,l,h=null,a=e[n-2],c=e[n-1],u=this._regioncode(a,c);for(let d=0;d&lt;n;d+=2)if(i=a,s=c,a=e[d],c=e[d+1],r=u,u=this._regioncode(a,c),0===r&amp;&amp;0===u)o=l,l=0,h?h.push(a,c):h=[a,c];else{let e,n,d,f,y;if(0===r){if(null===(e=this._clipSegment(i,s,a,c,r,u)))continue;[n,d,f,y]=e}else{if(null===(e=this._clipSegment(a,c,i,s,u,r)))continue;[f,y,n,d]=e,o=l,l=this._edgecode(n,d),o&amp;&amp;l&amp;&amp;this._edge(t,o,l,h,h.length),h?h.push(n,d):h=[n,d]}o=l,l=this._edgecode(f,y),o&amp;&amp;l&amp;&amp;this._edge(t,o,l,h,h.length),h?h.push(f,y):h=[f,y]}if(h)o=l,l=this._edgecode(h,h),o&amp;&amp;l&amp;&amp;this._edge(t,o,l,h,h.length);else if(this.contains(t,(this.xmin+this.xmax)/2,(this.ymin+this.ymax)/2))return[this.xmax,this.ymin,this.xmax,this.ymax,this.xmin,this.ymax,this.xmin,this.ymin];return h}_clipSegment(t,e,n,i,s,r){for(;;){if(0===s&amp;&amp;0===r)return[t,e,n,i];if(s&amp;r)return null;let o,l,h=s||r;8&amp;h?(o=t+(n-t)*(this.ymax-e)/(i-e),l=this.ymax):4&amp;h?(o=t+(n-t)*(this.ymin-e)/(i-e),l=this.ymin):2&amp;h?(l=e+(i-e)*(this.xmax-t)/(n-t),o=this.xmax):(l=e+(i-e)*(this.xmin-t)/(n-t),o=this.xmin),s?(t=o,e=l,s=this._regioncode(t,e)):(n=o,i=l,r=this._regioncode(n,i))}}_clipInfinite(t,e,n,i,s,r){let o,l=Array.from(e);if((o=this._project(l,l,n,i))&amp;&amp;l.unshift(o,o),(o=this._project(l[l.length-2],l[l.length-1],s,r))&amp;&amp;l.push(o,o),l=this._clipFinite(t,l))for(let e,n=0,i=l.length,s=this._edgecode(l[i-2],l[i-1]);n&lt;i;n+=2)e=s,s=this._edgecode(l[n],l[n+1]),e&amp;&amp;s&amp;&amp;(n=this._edge(t,e,s,l,n),i=l.length);else this.contains(t,(this.xmin+this.xmax)/2,(this.ymin+this.ymax)/2)&amp;&amp;(l=[this.xmin,this.ymin,this.xmax,this.ymin,this.xmax,this.ymax,this.xmin,this.ymax]);return l}_edge(t,e,n,i,s){for(;e!==n;){let n,r;switch(e){case 5:e=4;continue;case 4:e=6,n=this.xmax,r=this.ymin;break;case 6:e=2;continue;case 2:e=10,n=this.xmax,r=this.ymax;break;case 10:e=8;continue;case 8:e=9,n=this.xmin,r=this.ymax;break;case 9:e=1;continue;case 1:e=5,n=this.xmin,r=this.ymin}i[s]===n&amp;&amp;i[s+1]===r||!this.contains(t,n,r)||(i.splice(s,0,n,r),s+=2)}return s}_project(t,e,n,i){let s,r,o,l=1/0;if(i&lt;0){if(e&lt;=this.ymin)return null;(s=(this.ymin-e)/i)&lt;l&amp;&amp;(o=this.ymin,r=t+(l=s)*n)}else if(i&gt;0){if(e&gt;=this.ymax)return null;(s=(this.ymax-e)/i)&lt;l&amp;&amp;(o=this.ymax,r=t+(l=s)*n)}if(n&gt;0){if(t&gt;=this.xmax)return null;(s=(this.xmax-t)/n)&lt;l&amp;&amp;(r=this.xmax,o=e+(l=s)*i)}else if(n&lt;0){if(t&lt;=this.xmin)return null;(s=(this.xmin-t)/n)&lt;l&amp;&amp;(r=this.xmin,o=e+(l=s)*i)}return[r,o]}_edgecode(t,e){return(t===this.xmin?1:t===this.xmax?2:0)|(e===this.ymin?4:e===this.ymax?8:0)}_regioncode(t,e){return(t&lt;this.xmin?1:t&gt;this.xmax?2:0)|(e&lt;this.ymin?4:e&gt;this.ymax?8:0)}}const _=2*Math.PI;class v{constructor(t){const{halfedges:e,hull:n,triangles:i}=new s(t);this.points=t,this.halfedges=e,this.hull=n,this.triangles=i;const r=this.inedges=new Int32Array(t.length/2).fill(-1),o=this.outedges=new Int32Array(t.length/2).fill(-1);for(let t=0,n=e.length;t&lt;n;++t)r[i[t%3==2?t-2:t+1]]=t;let l,h=n;do{l=h,r[(h=h.next).i]=l.t,o[l.i]=h.t}while(h!==n)}voronoi(t){return new m(this,t)}*neighbors(t){const{inedges:e,outedges:n,halfedges:i,triangles:s}=this,r=e[t];if(-1===r)return;let o=r;do{if(yield s[o],s[o=o%3==2?o-2:o+1]!==t)return;if(-1===(o=i[o]))return yield s[n[t]]}while(o!==r)}find(t,e,n=0){if((t=+t)!=t||(e=+e)!=e)return-1;let i;for(;(i=this._step(n,t,e))&gt;=0&amp;&amp;i!==n;)n=i;return i}_step(t,e,n){const{inedges:i,points:s}=this;if(-1===i[t])return-1;let r=t,o=(e-s[2*t])**2+(n-s[2*t+1])**2;for(const i of this.neighbors(t)){const t=(e-s[2*i])**2+(n-s[2*i+1])**2;t&lt;o&amp;&amp;(o=t,r=i)}return r}render(t){const e=null==t?t=new g:void 0,{points:n,halfedges:i,triangles:s}=this;for(let e=0,r=i.length;e&lt;r;++e){const r=i[e];if(r&lt;e)continue;const o=2*s[e],l=2*s[r];t.moveTo(n[o],n[o+1]),t.lineTo(n[l],n[l+1])}return this.renderHull(t),e&amp;&amp;e.value()}renderPoints(t,e=2){const n=null==t?t=new g:void 0,{points:i}=this;for(let n=0,s=i.length;n&lt;s;n+=2){const s=i[n],r=i[n+1];t.moveTo(s+e,r),t.arc(s,r,e,0,_)}return n&amp;&amp;n.value()}renderHull(t){const e=null==t?t=new g:void 0,{hull:n}=this;let i=n;for(t.moveTo(i.x,i.y);(i=i.next)!==n;)t.lineTo(i.x,i.y);return t.closePath(),e&amp;&amp;e.value()}hullPolygon(){const t=new x;return this.renderHull(t),t.value()}renderTriangle(t,e){const n=null==e?e=new g:void 0,{points:i,triangles:s}=this,r=2*s[t*=3],o=2*s[t+1],l=2*s[t+2];return e.moveTo(i[r],i[r+1]),e.lineTo(i[o],i[o+1]),e.lineTo(i[l],i[l+1]),e.closePath(),n&amp;&amp;n.value()}*trianglePolygons(){const{triangles:t}=this;for(let e=0,n=t.length/3;e&lt;n;++e)yield this.trianglePolygon(e)}trianglePolygon(t){const e=new x;return this.renderTriangle(t,e),e.value()}}v.from=function(t,e=function(t){return t},n=function(t){return t},i){return new v(&quot;length&quot;in t?function(t,e,n,i){const s=t.length,r=new Float64Array(2*s);for(let o=0;o&lt;s;++o){const s=t[o];r[2*o]=e.call(i,s,o,t),r[2*o+1]=n.call(i,s,o,t)}return r}(t,e,n,i):Float64Array.from(function*(t,e,n,i){let s=0;for(const r of t)yield e.call(i,r,s,t),yield n.call(i,r,s,t),++s}(t,e,n,i)))},n.d(e,&quot;showVoronoi&quot;,function(){return E}),n.d(e,&quot;drawVoronoi&quot;,function(){return M}),n.d(e,&quot;eraseVoronoi&quot;,function(){return P}),n.d(e,&quot;toggleVoronoi&quot;,function(){return S});var p,w,b,k,T,E=!0;function M(){P();var t=document.createElement(&quot;div&quot;);t.setAttribute(&quot;id&quot;,&quot;attachedLinkOverlayForDelaunay&quot;),t.style.outline=&quot;5px solid black&quot;,t.style.position=&quot;fixed&quot;,t.style.top=&quot;1em&quot;,t.style.right=&quot;10%&quot;,t.style.maxWidth=&quot;80%&quot;,t.style.overflowWrap=&quot;break-word&quot;,t.style[&quot;font-size&quot;]=&quot;32px&quot;,t.style.zIndex=&quot;9999999999999999999999999999&quot;,t.style[&quot;background-color&quot;]=&quot;white&quot;,document.body.appendChild(t);var e=document.querySelectorAll(&quot;a, input&quot;),n=document.createElement(&quot;canvas&quot;);n.setAttribute(&quot;id&quot;,&quot;attachedCanvasOverlayForDelaunay&quot;);var i=document.documentElement.getBoundingClientRect(),s=document.documentElement.scrollHeight,r=document.documentElement.scrollWidth,o=s;n.style.position=&quot;absolute&quot;,n.setAttribute(&quot;width&quot;,r+&quot;px&quot;),n.setAttribute(&quot;height&quot;,o+&quot;px&quot;),n.style.left=&quot;0&quot;,n.style.top=&quot;0&quot;,n.style.zIndex=&quot;999999999999999999999999999&quot;,n.style[&quot;pointer-events&quot;]=&quot;none&quot;;var l=n.width,h=n.height;document.body.appendChild(n);var a=n.getContext(&quot;2d&quot;);a.getImageData(0,0,l,h);function c(t,e){return{x:l*(t/r),y:h*(e/o)}}a.fillStyle=&quot;red&quot;;for(var u=[],d=[],f=0;f&lt;e.length;f++){var y=e[f],g=y.getBoundingClientRect(),x=c((g.left+g.right)/2,(g.top+g.bottom)/2-i.top);E&amp;&amp;a.fillRect(x.x-2,x.y-2,5,5),u.push([x.x,x.y]),d.push([x.x,x.y,y])}var m=v.from(u),_=m.voronoi([1,1,l,h]);E&amp;&amp;(a.beginPath(),a.lineWidth=3,_.render(a),a.stroke());var S=new Map;d.forEach(function(t){var e=t,n=t,i=t,s=m.find(e,n);S.set(s,i)}),T=function(e){var n=c(e.layerX,e.layerY),i=m.find(n.x,n.y);if(&quot;touchstart&quot;==e.type&amp;&amp;p==i&amp;&amp;S.get(i).click(),p!=i){void 0!==p&amp;&amp;(S.get(p).style.outline=b,E&amp;&amp;(a.strokeStyle=&quot;black&quot;,a.beginPath(),_.renderCell(p,a),a.stroke()));var s=S.get(i);w=s,b=s.style.outline||&quot;none&quot;,s.style.outline=&quot;5px solid black&quot;,&quot;A&quot;==s.tagName?t.innerHTML=s.href:&quot;INPUT&quot;==s.tagName&amp;&amp;(t.innerHTML=s.value),E&amp;&amp;(a.strokeStyle=&quot;red&quot;,a.beginPath(),_.renderCell(i,a),a.stroke())}p=i},k=function(t){var e=c(t.layerX,t.layerY),n=m.find(e.x,e.y);S.get(n).click()},window.addEventListener(&quot;resize&quot;,M),window.addEventListener(&quot;mousemove&quot;,T),window.addEventListener(&quot;touchstart&quot;,T),window.addEventListener(&quot;click&quot;,k)}function P(){window.removeEventListener(&quot;mousemove&quot;,T),window.removeEventListener(&quot;touchstart&quot;,T),window.removeEventListener(&quot;click&quot;,k),window.removeEventListener(&quot;resize&quot;,M),void 0!==w&amp;&amp;(w.style.outline=b);try{document.getElementById(&quot;attachedLinkOverlayForDelaunay&quot;).remove(),document.getElementById(&quot;attachedCanvasOverlayForDelaunay&quot;).remove()}catch(t){}}function S(){E=!E,M()}M(),window.addEventListener(&quot;load&quot;,M)}]);">Voronoize</a></p> <p>Step 2. Press it to voronoize whatever page you are on!</p> <p>For example, here is a screenshot of what happens when I voronoize <a href="https://www.wikipedia.org">wikipedia.org</a>:</p> <p><img src="/images/voronoi-wikipedia.png" style="width: 600px" alt="Wikipedia 'voronized'" /></p> <p>And here is <a href="https://arxiv.org">arxiv.org</a> voronoized: <img src="/images/voronoi-arxiv.png" style="width: 600px" alt="Arxiv 'voronized'" /></p> <p>Again, the code for this is available on Github <a href="https://github.com/samzhang111/html-voronoi">here</a>.</p> <p><strong>Edit</strong>: Thanks to <a href="https://twitter.com/mjskay/status/1085407912279896065">Matthew Kay</a> for pointing out to me that using Voronoi diagrams to augment cursor behavior is known in the literature as <a href="http://www.dgp.toronto.edu/~tovi/BubbleCursor">bubble cursor</a>.</p>As promised in my previous post on the “voronoizer”, I have made the voronoizer into a bookmarklet that you can run on any page whose Content Security Policy permits.Voronoize this page!2019-01-10T12:00:00+00:002019-01-10T12:00:00+00:00https://sam.zhang.fyi/2019/01/10/voronoize-this-page<p>What you are looking at is a <a href="https://en.wikipedia.org/wiki/Voronoi_diagram">Voronoi diagram</a> constructed on a canvas laid over this page, using the centers of link and input elements as points.</p> <p><input id="toggle" type="button" onclick="(function(event) { event.stopPropagation(); voronoize.toggleVoronoi(); if (voronoize.showVoronoi) { document.getElementById('toggle').value = 'Hide Voronoi lines' } else { document.getElementById('toggle').value = 'Show Voronoi lines' } })(event)" value="Hide Voronoi lines" /></p> <p><input type="button" onclick="voronoize.eraseVoronoi()" value="Disable" /></p> <p>I did this on a whim, but then I realized it could be interesting for accessibility. So I added the feature where the link text or input value of the currently active Voronoi cell is shown in the upper-right corner, and clicking a cell clicks the corresponding input/link.</p> <p>(On mobile and tablet, touching a cell causes it to become focused, then touching it again activates the element.)</p> <p>To get a sense for how this would feel as an actual accessibility tool, you can hide the outlines of the Voronoi cells by pressing the “Hide Voronoi lines” button. Or you can disable it altogether using the “Disable” button.</p> <p>Little red dots are drawn over the points, which are the centers of the bounding boxes of the links and inputs (these are what go into the Voronoi algorithm). After playing with this for a few minutes, it appears that the long links are hard to click in this model, since the center does not expand with the size of the link. What may be better is to use the known extension of the Voronoi algorithm over <em>line segments</em>, and to model each input as a segment instead. I don’t see any of the popular Delaunay/Voronoi javascript libraries on github supporting line segments at the moment (although it is implemented in some libraries <a href="https://github.com/aewallin/openvoronoi">in Python</a>), but a quick workaround could be to just add more points for each element based off its size.</p> <p>This uses the library <a href="https://github.com/d3/d3-delaunay">d3-delaunay</a>, which relies on the library <a href="https://github.com/mapbox/delaunator">Delaunator</a>. The source code is on Github <a href="https://github.com/samzhang111/html-voronoi">here</a>. There is nothing actually specific about this code for my website, so I will soon make this into a bookmarklet!</p>What you are looking at is a Voronoi diagram constructed on a canvas laid over this page, using the centers of link and input elements as points.