Jekyll2020-01-28T03:28:07+00:00https://sam.zhang.fyi/feed.xmlsamzhang111.github.ioA 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> is a branch of bifurcation theory concerned with the way that a small change in a <em>continuous</em> parameter of a potential function can introduce a <em>discontinuous</em> jump in observed phenomena.</p> <p>To explore how this works, I’ve created an interactive “catastrophe machine”. 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; width: 600px; margin: 3px auto; justify-content:space-between"> <div id="control" style="flex:0 1 auto;width:280px;height:300px;border: 3px solid black"></div> <div id="catastrophe-surface" style="flex:0 1 auto;width:280px;height:300px;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. Catastrophe theory also serves as an interesting example of the difficulties of mathematical modeling of complex systems: see <a href="http://faculty.washington.edu/etsb/402W12/materials/Zahler_Sussman_claims_and_accomplishments_of_applied_catastrophe_nature_77.pdf">this critique</a> for some of the historical debate around catastrophe theory’s (over-)application to scientific questions. One of the general lessons here is that for applied mathematics to be useful for science, the practitioner must prioritize the scientific problem at hand, over any particular mathematical technique one is itching to apply. Nevertheless these models can be fascinating and with a critical eye, they are worth visiting – many of them are discussed (and some criticized) in Poston and Stewart’s book.</p>Catastrophe theory is a branch of bifurcation theory concerned with the way that a small change in a continuous parameter of a potential function can introduce a discontinuous jump in observed phenomena.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>This is a demonstration of the way that n-gons are naturally parameterized by the Grassmanian of 2-planes in $\mathbb{R}^n$. Since we are most comfortable with $\mathbb{R}^3$, this demo below uses triangles.</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>I heard about this from <a href="http://math.colorado.edu/~rohi1040">Rob Hines</a>, who cites <a href="https://arxiv.org/abs/1702.01027">a paper from Jason Canterella, Tom Needham, Clayton Shonkwiler, and Gavin Stewart</a>. There is some interesting historical backstory to this moduli space. Lewis Carroll posed the question: what is the probability that a random triangle is obtuse? It is not so easy to answer that question without a probability space from which to draw a random triangle. This moduli space helps answer that question.</p>This is a demonstration of the way that n-gons are naturally parameterized by the Grassmanian of 2-planes in $\mathbb{R}^n$. Since we are most comfortable with $\mathbb{R}^3$, this demo below uses triangles.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/108540791227989605">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.Treasure hunting2019-01-09T12:00:00+00:002019-01-09T12:00:00+00:00https://sam.zhang.fyi/2019/01/09/treasure-hunting<p>You are given instructions for finding treasure on an island. The island has two trees and a “gallows”. To find the treasure, we first mark out two special points,$P$and$Q$. To find$P$, start at the gallows, walk toward Tree 1, then make a right-angled left turn upon reaching the tree. Walk the same distance again: you have found$P$. To find$Q$, go back to the gallows, walk toward Tree 2, then turn <em>right</em> at a right-angle, and walk the same distance. Mark the spot you are standing on as$Q$. (It may make more sense looking at the diagram below).</p> <p>The treasure is at the midpoint of the segment connecting$P$and$Q.</p> <p>The problem is: You show up on the island, and you see the two trees, but there is no gallows to be found. Can you find the treasure anyway?</p> <div id="treasure" class="jxgbox" style="width:600px; height:600px; margin-left:auto; margin-right:auto;"></div> <p>The diagram is interactive, and you can drag around the “gallows” to see different hypothetical locations. But playing with it will be a major spoiler, so try to formulate a guess first :).</p> <p>(More spoilers) Although you can move the trees around on the diagram, without loss of generality, you can suppose the trees are aligned on the horizontal axis, centered at the origin, at(-1, 0)$and$(1, 0)$. Here is a proof using complex numbers. You can think of the treasure map as taking place in the complex plane, and the position of the treasure as a function of the position of the gallows. Specifically, it is a function$\phi: \mathbb{C} \to \mathbb{C}, z \mapsto \frac{i(z - 1) - i(z + 1)}{2} = -1$(after all,$P$and$Q$are just complex rotations:$P(z) = -i(z + 1), Q(z) = i(z - 1)$). Hence no matter where you put the gallows, the treasure does not change.</p> <p>Moreover, from the proof, you can see that if the instructions had you turn by an angle other than 90 degrees, the treasure would still remain independent of the position of the gallows.</p> <p>This problem is from <a href="https://periplusmathematicus.files.wordpress.com/2018/05/lyubich-shor-the-kinematic-method-in-geometrical-problems.pdf">The Kinematic Method in Geometrical Problems</a>, by Yu. I. Lyubich and L.A. Shor (although they cite it from <a href="https://archive.org/details/MathematicalMethodsOfOperationsResearch">Mathematical Methods of Operations Research</a> by Thomas Saaty). They demonstrate their “kinematic” approach by presenting a proof where you consider the “velocity” of a particle reflecting off the two trees, and noticing how they cancel out.</p> <p>I used the library <a href="https://jsxgraph.org/">JSXGraph</a>. Source code is <a href="https://github.com/samzhang111/linesandcurves/blob/master/treasure.js">here</a>.</p>You are given instructions for finding treasure on an island. The island has two trees and a “gallows”. To find the treasure, we first mark out two special points,$P$and$Q$. To find$P$, start at the gallows, walk toward Tree 1, then make a right-angled left turn upon reaching the tree. Walk the same distance again: you have found$P$. To find$Q$, go back to the gallows, walk toward Tree 2, then turn right at a right-angle, and walk the same distance. Mark the spot you are standing on as$Q$. (It may make more sense looking at the diagram below).An ascii implicit function plotter2018-12-26T12:00:00+00:002018-12-26T12:00:00+00:00https://sam.zhang.fyi/2018/12/26/ascii-plotting<div id="container"> <div id="space"></div> <span id="measure"></span> </div> <div> <input id="equation" type="text" placeholder="y^2 - x*(x - 1)*(x + 1)" /> <span id="error"></span> </div> <p>Type in a mathematical expression in$x$and$y$into the input box above to plot it in ascii. The program expects an expression equal to zero, so if you want to plot$y = \sin(x)$, you would instead move everything to the left hand side, and enter$y - \sin(x)$into the prompt.</p> <p>What can you put in? The expression is parsed using <a href="http://mathjs.org">math.js</a>, so it can take most of these <a href="http://mathjs.org/docs/reference/functions.html">math.js functions</a>, such as the trigonometric functions, <code class="language-plaintext highlighter-rouge">log</code>, <code class="language-plaintext highlighter-rouge">exp</code>, as well as interesting things like <code class="language-plaintext highlighter-rouge">gcd</code> and <code class="language-plaintext highlighter-rouge">sign</code>. There are some quirks with their language, so if you run into any trouble, it is best to be explicit. I can type$y - 2x$and it would understand I want to multiply$2$by$x$, but I can’t type$y - x(x-1)$because the parentheses makes it think that I am trying to invoke a (nonexistent) function named$x$, and instead I have to put in$x*(x -1)$.</p> <p>Under the hood, I rolled my own <a href="https://en.wikipedia.org/wiki/Marching_cubes">marching squares</a> algorithm in javascript.</p> <p>One interesting thing about doing this in ascii is that it’s such low-resolution that any rough edges in the code become really obvious. For instance, if you plot$x^2 + y^2 - 1$, the circle is lumpy for some reason.</p> <p>Another fun glitch caused by the low resolution is that you can see aliasing. Plot$y - \sin(2x)$, then increase the factor of$2$. Around$5$it starts to look weird, and at$y - \sin(8x)$it is totally borked.</p> <p>For bonus points, since the lines are drawn as “o”s, imagine a ghost vocalizing anything that you plot. How spooky – it is the ghost of algebraic geometry!</p> <p>Source code is <a href="https://github.com/samzhang111/marchingcubes-ascii">here</a>.</p> <style> #container { margin: 1em 0; font-family: monospace; font-size: 15px; background-color: white; } #space { min-height: 600px; min-width: 600px; white-space: pre; border: solid 1px black; border-radius: 5px; } #measure { position: absolute; white-space: pre-line; visibility: hidden; height: auto; width: auto; padding: 0; margin: 0; } #error{ color: red; } </style>Hypocycloids2018-12-19T12:00:00+00:002018-12-19T12:00:00+00:00https://sam.zhang.fyi/2018/12/19/hypocycloids<p>Suppose you have a stationary circle, and inside that circle is a second smaller circle, touching the larger circle from within. Pick an arbitrary point on the inner circle, call it$K$. Imagine pushing the inner circle against the outer circle, so that the inner circle revolves around its own center, as it moves inside the larger circle. What path does$K$describe?</p> <div id="copernicusradius" class="jxgbox" style="width:600px; height:600px; margin-left:auto; margin-right:auto;"> </div> <p>If you saw my earlier blog post on <a href="/2018/12/18/cats-on-ladders/">cats on ladders</a>, you would recognize it to be an <a href="https://en.wikipedia.org/wiki/Astroid">astroid</a>!</p> <p>Try setting$\text{Radius}=0.33$in the slider to see what happens when the radius of the inner circle is a third of the larger circle as well, although the rounding error becomes more visible. (It’s a <a href="https://en.wikipedia.org/wiki/Deltoid_curve">deltoid</a>)</p> <p>Now this is the interesting part: what happens if the diameter of the inner circle is exactly the radius of the larger circle? Make a guess!</p> <div id="copernicus" class="jxgbox" style="width:600px; height:600px; margin-left:auto; margin-right:auto;"> </div> <p>Here is a more detailed version, emphasizing that the angles$\alpha = \beta$at all times.</p> <div id="copernicusdetail" class="jxgbox" style="width:600px; height:600px; margin-left:auto; margin-right:auto;"> </div> <p>Did you guess that it would be a straight line? The fact that the path described is a straight line is sometimes called Copernicus’s theorem. However, mechanical devices exploiting this fact have been made long before Copernicus: see <a href="https://en.wikipedia.org/wiki/Tusi_couple">Tusi couple</a>.</p> <p>In general, shapes traced by small circles rolling inside of larger ones are called <a href="https://en.wikipedia.org/wiki/Hypocycloid">hypocycloids</a>. For a nice exposition of this, as well as some related phenomena (some quite a bit deeper), see <a href="http://www.math.ucr.edu/home/baez/rolling/rolling_3.html">this post on John Baez’s blog</a>. For more problems like this, see the book <a href="https://www.springer.com/la/book/9780817641610">Lines and Curves</a>.</p> <p>I used the library <a href="https://jsxgraph.org/">JSXGraph</a>. For my source code, see <a href="https://github.com/samzhang111/linesandcurves">here</a>.</p>Suppose you have a stationary circle, and inside that circle is a second smaller circle, touching the larger circle from within. Pick an arbitrary point on the inner circle, call it$K$. Imagine pushing the inner circle against the outer circle, so that the inner circle revolves around its own center, as it moves inside the larger circle. What path does$K\$ describe?