Apologies, no automated ToC has yet been implemented for this article due to technical limitations. Please mark down the appropriate chapter number when you leave, and use the search feature of your browser to return to it at your leisure.

Prelude

In this text, I'm going to describe the internal workings of my picture rendering tool, which has been used to generate the geometric drawings as originally seen in the previous revision of this website. I've written this mostly for my own reference, but I hope others will find this useful as well.

The written text is released under Creative Commons 4.0 Attribution, and the relevant code under the MIT license. Also, no warranty or guarantee of ANY KIND is provided for this text or the code. Any use is solely under your own responsibility.. so if there's something mission critical you wish to do, kindly do your own checks before committing on that. I will take no responsibility for, say, a failed presentation due to the generator refusing to generate anything with given parameters.

1. Introduction

1.1 General concepts

To understand the how the generator works, you need to understand the working principle of a pintograph. A pintograph is a device that generates geometric patterns using two rods connected respectively to two rotating disks, and a pen at the other end. The name was, best to my knowledge, coined by the daughter of Fran McConville, as described on his homepage. For simplicity, we are going to do a simpler model without scissor scaling as in many other variants seen on YouTube and elsewhere.

The device has been implemented many, many times in both physical and virtual form, and my implementation is surely not the first nor the last one of them. However, most of the descriptions I have seen have not examined the mathematical concepts related to the motions of the pen. I will try to open them up a bit as well.

1.2 Programming

I've decided to implement the generator in Python 3, with the help of Numpy (advanced mathematics) and Pillow (image processing library). I'd imagine a similar program could be implemented in any language with sufficient floating point and image processing capabilities, but it may be considerably more arduous in lower level languages like C.

I did take the liberty of using certain shortcuts to make coding and theory less challenging; I'll detail them later in this text.

2. Theory

Like always, there's always need for some theory before practice. In this case, we need to model the behavior of the pen in relation to the spinning discs and connected rods. There are many factors at play, like the lengths of rods, the radii of discs and distance of those said discs from each other. Rotation speeds, and particularly their relation to each other is very important in respect of the drawing rendered.

2.1 Definitions

Let:

\(r_{1}, r_2 > 0\) be radii of the spinning disks. \(d_{center} > 0\) be the distance of to a center of a spinning disk from a logical \((0,0)\), \(p_{center}\). For simplicity, we will be assuming they are perfect circles. This does not preclude circles from varying in size.. to a certain degree. The centers of circles will be denoted with \(p_{r_1}, p_{r_2}\)

\(p_1, p_2\) be points on the edges of their respective disks; rods will be connected from the one side to their respective spinning disk, and to each other from the other. Their positions will be calculated from angles \(\angle_{r1}, \angle_{r2}\)

\(A\) be the imaginary "rod" connecting \(p_1\) and \(p_2\) together; in this model, it plays an important part. \(B, C\) be the actual rods. Their respective angles in relation to A will be denoted as \(\angle_{BA}, \angle_{CA}\). As shown, a triangle \(\triangle ABC\) is formed.

\(p_{pen}\) be the point where the pen draws, shown as blue in the picture. The angle at that point is called \(\angle_{CB}\)

We can fairly safely assume that \(d_{center}\) remains constant; however, to achieve varying drawings, not much else can be assumed to be constant over time. It will also be assumed that the drawing occurs on an unbounded 2D plane.

Next, we will be deriving the precise formula for determining \(p_{pen}\) in relation to given variables \(r_1, r_2, d_{center}, \angle_{r1}, \angle_{r2}\)

2.2 Derivation

2.2.1 Finding the coordinates of the pen

We will be taking a fairly analytical approach to finding the exact definition for \(p_{pen}\). There may very well be more straightforward methods to find it, like circle-circle intersection, or a method relying on the altitude of a triangle, courtesy of Reddit user u/MagneticDuck; however, as an admittedly fairly inexperienced mathematician, I didn't realize one could do so until pointed out. This proof addresses a general case, although in section 2.2.2 I will be presenting a simplification that will cut a significant bit of difficulty out of the proof.

Let's first define \(p_{r1}, p_{r2}\), as follows:

$$ \begin{align} p_{r1} &= (-d_{center}, 0)\\ p_{r2} &= (d_{center}, 0) \tag{1} \end{align} $$

These equations trivially follow from the definition; both circles are at an equal distance from \(p_{center}\), and both points are at opposite sides of the center point.

Let's next calculate the positions of points \(p_1, p_2\). Considering the angles \(\angle_{r1}, \angle_{r2}\) , it follows from the (assumed to be known) properties of sine and cosine functions. First, for \(p_1\):

$$ \begin{align} p_1 &= p_{r_1} + (r_1*\cos(\angle_{r1}), r_1*\sin(\angle_{r1}))\\ &= (r_1*\cos(\angle_{r1}) -d_{center}, r_1*\sin(\angle_{r1})) \tag{2a} \end{align} $$

and respectively for \(p_2\), taking the opposite rotation direction into consideration

$$ \begin{align} p_2 &= p_{r_2} + (-r_2*\cos(\angle_{r2}), r_2*\sin(\angle_{r2}))\\ &= (-r_2*\cos(\angle_{r2}) +d_{center}, r_2*\sin(\angle_{r2})) \tag{2b} \end{align} $$

Now, we can determine the length of \(A\), which will be helpful in the step after this one. We'll determine it by using the well known equation for the distance of two points in a 2-dimensional Cartesian coordinate system.

$$ A = \sqrt{(p_{2_x}-p_{1_x})^2+(p_{2_y}-p_{1_y})^2}\\ \tag{3} $$

Now, let's recall the Law of Cosines, which could be formulated as follows:

Where \(a,b,c\) are the sides of the triangle and \(C\) is the angle between \(a\) and \(b\), following applies: $$ c^2 = a^2 + b^2 - 2ab * \cos(C) \tag{4a} $$

and deriving onward from that

$$ \begin{align} c^2 + 2ab * \cos(C) &= a^2 + b^2\\ 2ab * \cos(C) &= a^2 + b^2 - c^2\\ \cos(C) &= \frac{a^2 + b^2 - c^2}{2ab}\\ C &= \arccos(\frac{a^2 + b^2 - c^2}{2ab}) \tag{4b} \end{align} $$

Now, we can calculate the angle \(\angle{CA}\), as follows. For now, let's assume that \(\arccos()\) is always defined with the given input, whatever it may be. I'll address the issues in that in the next section.

$$ \angle{CA} = \arccos(\frac{C^2+A^2-B^2}{2CA}) \tag{5} $$

We're on the home stretch now. We'll now calculate the direction vector for \(C\), which can be then used in conjunction of \(C\)'s length to exactly define \(p_{pen}\). Let's do a few definitions first.

$$ \begin{align} A_{dir} = \overrightarrow{CB} &= (p_{2_x}-p_{1_x}, p_{2_y}-p_{1_y})\tag{Direction vector from $p_1$ to $p_2$}\\ \hat{A_{dir}} &= \frac{A_{dir}}{|A_{dir}|}\tag{Normalized}\\ \tag{6a} \end{align} $$

and using the known angle \(\angle{CA}\)

$$ C_{dir} = (\hat{A_{dir_x}} * \cos(\angle{CA}), \hat{A_{dir_y}} * \sin(\angle{CA})) \tag{6b} $$

and finally we arrive to the conclusion of our long journey:

$$ p_{pen} = p_{1} + C * C_{dir} \tag{7} $$

We now have an unambiguous definition for the pen's current location with given parameters, whatever they may be. Therefore, our task is done. \(\square\)

2.2.2 Potential pitfalls and simplification

Until now, we had assumed that no step of the derivation will fail due to given values; that is, all inputs provided to the functions have a well-defined result, and that all assumptions hold true. However, it is possible to construct scenarios where the assumptions do not hold, and which will stop the hapless drawer in their tracks.

As you did notice, we needed \(\arccos(x)\) at one point in our derivation. However, we also know that its domain does not encompass the whole set of real numbers; infact, it is the range \(x \in [-1, 1]\), which is fairly small.

Due to the complex and interconnecting nature of variables in play, I did not explore the possibilities of finding any concrete rules. \(A\) is particularly tricky, as it depends on the relative positions of the points \(p_1, p_2\), which themselves depend on several other variables - rendering the calculations rather messy and potentially complicated.

For the same reason, I have not fully explored the possible bounds for \(p_{pen}\). Determining the maximum and minimum for the X and Y coordinates requires an inordinate amount of mathematical analysis, which I didn't find pertinent to do; instead of directly deriving boundaries, I decided to automatically find bounds by crude approximation. This also avoids a bad scale in a scenario where theoretically some position is possible to reach, but in practice does not happen. It does add a performance penalty but also simplifies the construction of the program.

However, we can slightly change the rules to make things a bit easier. If we add a new assumption stating that \(C = B\) over time, we can considerably simplify the \(\arccos()\) as follows. Let \(D = C = B\), and therefore:

$$ \begin{align} \angle{CA} &= \arccos(\frac{C^2+A^2-B^2}{2CA})\\ \angle{CA} &= \arccos(\frac{D^2+A^2-D^2}{2DA})\tag{$D^2 - D^2 = 0$}\\ \angle{CA} &= \arccos(\frac{A^2}{2DA})\tag{$A^2/A = A$}\\ \angle{CA} &= \arccos(\frac{A}{2D}) \end{align} $$

With this restriction, we've now found that it is sufficient to ensure that \(D\) is large enough to guarantee that any possible size of \(A\) does not cause the input to be out of the domain - or mathematically speaking, that \(|A| < 2D\) at all times. Geometrically interpreted, you should ensure that the distance of \(p_1\) and \(p_2\) is never too large to prevent forming of a triangle. I'll leave the derivation of actual, precise upper and lower bounds of \(A\) as an exercise to the reader.

Other simplifications are also be possible now, as the triangle is now always an isosceles triangle, a property not assured before. One important property is that when an altitude is dropped from point \(p_{pen}\), it will bisect \(A\) into two equally long segments at a right angle. This conveniently both reveals us the 2 required sides to calculate the altitude with Pythagoras' theorem, and also gives an easy way to determine the correct point for a pen by simply following halfway through \(A\), and then up via the altitude! This could be also done with a non-isosceles triangle, but with more complexity as we'd have to determine the correct length for the segment of \(A\)

3. Implementation

3.1 Construction

The actual program I'll present consists of two files. "pintograph.py" for the point generator itself, and "random_background.py". The program ended up as a fairly compact creation, with around 360 lines in total. Granted though, the program almost completely lacks things like advanced exception handling or safety checks for incomplete or sketchy configurations (e.g distance of circles longer than the sides \(C\) and \(B\) plus radiuses combined). However, it was not originally intended to be a rock solid tool anyway.. if someone wants to make it one, they are most welcome to do that xD

3.2 pintograph.py

This file contains the main functionality of the program. It is a class that when given proper parameters, generates a point for a given step. Each step represents a coordinate, and are continuous thereby enabling arbitrarily precise calculations.

This class supports basic configurations, and some basic step-related properties; the lengths of the circle radii (NOT the rods!) can be adjusted over time. Adding support for the rod lengths should be a fairly easy exercise as well - and therefore suitable as to be left to the reader. There is also support for "X-coordinate swing" - meaning that the X-coordinate gains a bias that alternates between directions. It can be considered equivalent to attaching the writing platform to a pendulum which swings back and forth, gradually slowing down to a halt.

Another useful feature is automatic scaling. As mentioned before, it is slightly complicated to calculate the true maximum range a Pintograph can draw to in a given configuration. The class can automatically "sense" the approximate boundaries required as estimated from a fast measurement from steps. The actual drawing phase uses much smaller steps than the automatic sensing.

3.3 random_background.py

This file shows a simple example on how to create a Pintograph, use it to generate coordinates, and primitively draw them somewhere (in this case, a Pillow canvas). The script saves them automatically to a randomly named file in the folder where the script was executed.

There are several constants that can be adjusted to alter the pictures the script generates. I set the values so that it tends to generate highly symmetric, not excessively dense geometric patterns; do experiment with them and see what you get. Potential improvements would be, for instance, proper transparency support (current one is "faux" transparency dependent on the current step in relation to maximum steps) and automatically adjusting precision (current one depends on manual amounts of steps, which naturally vary in relation to the size of the picture)

4. Download

The files described are available as a ZIP package, including a few sample pictures as generated by the tool. Download here. Alternatively, you can visit GitLab

The end

Thank you for reading this far - I hope you enjoyed the journey. Any questions and comments are very welcome, and I hope to see what you make of this little invention! :)