Perspective drawing and Moiré patterns

For as long as I can remember, I've enjoyed seeing Moir&eacute patterns in pedestal fans:
the fan is covered front and back by a cage with radial spokes, and as the fan rotates or you move, the Moiré pattern moves also. I thought
it'd be interesting to draw a fan in ggplot2 and enjoy the Moiré motion.

There are two interesting steps in creating the gif above. The first is making a perspective drawing in the first place, and the second is making the
random-yet-smooth motion of the observer.

Perspective

What follows is my understanding – essentially derived from scratch – of how to take points in 3-space and plot them as seen by an
observer. My brief perusal of the Wikipedia article revealed quite a few
types of perspective drawing, and I haven't bothered to work out how this method fits in with them.

The basic idea is to imagine a small sphere with the observer at its centre. The observer is looking at some point; draw a line from that point to the
observer; where that line intersects the surface of the sphere defines zero-latitude and zero-longitude. Choosing an orientation for the observer then fixes
the equatorial plane and prime meridian. The position of a object in the observer's field of view is the longitude and latitude of where a line from that
object to the osberver intersects the surface of the sphere.

If we imagine our observer at the origin, looking along the positive \(x\)-axis, and oriented so that "up" is the positive \(z\)-axis, then
a latitude \(\phi\) and longitude \(\theta\) correspond to a point on the surface of the unit sphere as follows:

(In the equation for \(\theta\), it is better to take the dot product with \(\bm{j}\) than with \(\bm{i}\), because in the latter case you then take an arccos,
which doesn't distinguish between positive and negative values of \(\theta\).)

The fact that the observer will not in general be at the origin is not important, since we can just subtract the observer's position from the
relevant position vector being plotted. The goal now is to rotate the axis vectors \(\bm{i}, \bm{j}, \bm{k}\) so that they match the orientation
of the observer.

The observer in some direction; let \(\bm{r}\) be the vector from the observer to some point along this direction. In the earlier equations for
latitude and longitude, the observer was looking along the positive \(x\)-axis. We'll rotate the axes so that \(\bm{i}\) points in the
\(\bm{r}\)-direction in two steps.

First, we spin \(\bm{i}\) and \(\bm{j}\) axes around the \(\bm{k}\)-axis so that \(\bm{i}\) is parallel to \((r_x, r_y, 0)\). This is a rotation by an
angle \(\alpha\) defined by

\[
\alpha = \arctan(r_y/r_x).
\]

The second step is to tilt the \(xy\)-plane, so that \(\bm{i}\) is parallel to \((r_x, r_y, r_z)\). This tilt is of an angle \(\beta\)
defined by

It is the vectors \(\bm{k''}\) and \(\bm{j''}\) that are used in the dot products of equations (\ref{angle_dotproducts}) to decide where an object
appears to the observer.

Smooth random motion

For the gif at the top of this page, the basic idea is to set random positions and velocities at regular intervals, and then smoothly interpolate
the motion between the ends of these intervals. (And then to get the perfect gif loop, the last position and velocity is set equal to the first.) I
used R's ggplot2 package to draw all the lines – not a choice I'd recommend for this exercise unless you want to practise ggplotting – and it
won't plot a geom_segment if one of the edges is outside the plotting window. If the observer gets too close to the fan, then the edges of the fan may
fall outside the plotting window, and some of the spokes won't be plotted at all. So (in my case!) the smooth interpolation of the motion shouldn't go
near the fan itself.

(I haven't defined the fan, but it's not hard – just a collection of vertices that get joined together in a couple of different ways.)

The natural solution to the disappearing-spokes problem is to use polar co-ordinates, with the observer's random radial distances having a suitably large
lower bound. We can use a smoothstep function to ensure that the time-derivative of the radial distance
is continuous.

More interesting are the angular velocity and the z-component of the velocity; I'll treat these two as independent cylindrical co-ordinates, and
the theory applies to each component. We have \(x_0 = x(0),\, x_1 = x(1),\, \dot{x}(0) = v_0,\, \dot{x}(1) = v_1\) all given (and set randomly). To make
the motion a little smoother, I'll also require \(\ddot{x}(0) = \ddot{x}(1) = 0\). The idea is to write the acceleration as a power series in \(t\),
truncate appropriately, and solve for the coefficients.

The acceleration being zero at \(t=0\) means that the power series has no constant term,

Equations (\ref{powerseries1}), (\ref{powerseries2}), and (\ref{powerseries3}) are enough to satisfy the conditions placed on the motion. The easiest
way to proceed is therefore to truncate the power series after the cubic term, so that there are three equations in three unknowns, \(a_1, a_2, a_3\).
Empirically, it's possible to solve these equations up to a sign error with a pen and paper during a poorly attended early-year trivia night: