library(ggplot2)
# Simple 45° ray on a grid (no internal mirrors for clarity)
trace_squares <- function(rows, cols, max_steps = 200) {
x <- 0.5
y <- 0.5
dx <- 1
dy <- 1
squares <- data.frame(i = integer(), j = integer())
for (s in 1:max_steps) {
i <- floor(y)
j <- floor(x)
if (i >= 0 && i < rows && j >= 0 && j < cols) {
squares <- rbind(squares, data.frame(i = i, j = j))
}
# bounce off outer walls
if (x + dx * 0.5 < 0 || x + dx * 0.5 > cols) dx <- -dx
if (y + dy * 0.5 < 0 || y + dy * 0.5 > rows) dy <- -dy
x <- x + dx * 0.5
y <- y + dy * 0.5
if (s > 10 && abs(x - 0.5) < 0.1 && abs(y - 0.5) < 0.1) break
}
unique(squares)
}
sq <- trace_squares(6, 6)
sq$parity <- (sq$i + sq$j) %% 2
sq$fill <- ifelse(sq$parity == 0, "white", "black")
ggplot(sq, aes(j, i, fill = fill)) +
geom_tile(colour = "grey70", linewidth = 0.3) +
scale_fill_identity() +
coord_equal() +
theme_void() +
labs(title = "Lunda shading: unit squares coloured by (i+j) mod 2")Lunda Designs: Zeroing, Shading and Modulo Arithmetic
How Gerdes turned mirror curves into black-and-white geometric patterns
Introduction
In the module on Mirror Matrices a light ray travelling at 45° between the pulli, reflecting off mirrors, produces a kolam or lusona. Paulus Gerdes observed that the same construction yields a second, equally striking family of figures once we pay attention to the unit squares the ray passes through.
If we colour those unit squares according to a simple rule — most often “black when the row-plus-column index is even, white otherwise,” or more generally modulo (k) — we obtain the high-contrast geometric patterns he named Lunda designs. The colouring rule is completely determined by the path of the ray; equivalently, it can be read off from a chessboard-style “zeroing” of an auxiliary matrix. This short module makes that step explicit and supplies both R and p5.js code so you can experiment with the shading yourself.
From mirror curve to coloured squares
Recall that a rectangle-filling mirror curve passes through every unit square of the grid exactly once (when the dimensions are coprime and the mirrors are “correct”). Number the unit squares by their lower-left corner ((i,j)), where (i) runs over rows and (j) over columns.
Gerdes’ basic observation is:
Colour the square ((i,j)) black if (i+j) is even, white if (i+j) is odd
(or, more generally, colour it according to ((i+j) k)).
Because the ray itself advances by steps of ((,)), consecutive squares it visits always have opposite parity of (i+j). Consequently the colouring is consistent along the whole curve and produces a crisp black-and-white (or (k)-coloured) tiling.
The same rule can be expressed by “zeroing out a chessboard pattern” in a matrix whose entries mark the unit squares: set every entry where (i+j) is even to 0 (or to a background value) and leave the others free to receive the colour of the curve. That is the phrase that appears in the Mirror Matrices module.
A tiny example
Consider a (4) array of unit squares. The parity matrix of (i+j) is
\[ \begin{bmatrix} 0 & 1 & 0 & 1 \\ 1 & 0 & 1 & 0 \\ 0 & 1 & 0 & 1 \\ 1 & 0 & 1 & 0 \end{bmatrix} \]
(0 = even, 1 = odd). Colouring even cells white and odd cells black already gives the classic checkerboard Lunda design. When a mirror curve threads through these cells, the black squares it “claims” form one of Gerdes’ characteristic motifs.
R code: parity shading of a mirror curve
The chunk below re-uses the ray-tracer idea from the Mirror Matrices module, records every unit square the ray enters, and paints those squares according to ((i+j)).
Change the modulus (replace %% 2 by %% 3 or %% 4) to obtain multi-colour Lunda designs. Adding internal mirrors (as in the previous module) simply changes which squares are visited; the colouring rule stays the same.
p5.js sketch: interactive Lunda shading
Paste into the p5.js Web Editor. The sketch draws a 45° ray and colours each unit square it enters according to parity.
// Lunda shading: colour unit squares by (i+j) mod 2
// while a 45° ray travels between the pulli
let rows = 8, cols = 8;
let cell = 50;
let margin = 40;
let path = [];
let t = 0;
function setup() {
createCanvas(cols*cell + 2*margin, rows*cell + 2*margin);
generatePath();
}
function draw() {
background(245);
translate(margin, margin);
// Draw all unit squares, coloured if visited
let visited = {};
let n = min(floor(t), path.length);
for (let k = 0; k < n; k++) {
let i = floor(path[k].y / cell);
let j = floor(path[k].x / cell);
visited[i+","+j] = true;
}
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (visited[i+","+j]) {
fill((i+j) % 2 === 0 ? 255 : 30);
} else {
fill(230);
}
stroke(180);
rect(j*cell, i*cell, cell, cell);
}
}
// Pulli
fill(20); noStroke();
for (let i = 0; i <= rows; i++)
for (let j = 0; j <= cols; j++)
circle(j*cell, i*cell, 6);
// Ray
noFill(); stroke(30,90,180); strokeWeight(2);
beginShape();
for (let k = 0; k < n; k++) vertex(path[k].x, path[k].y);
endShape();
t += 0.8;
if (t > path.length + 30) t = 0;
}
function generatePath() {
path = [];
let x = 0.5*cell, y = 0.5*cell;
let dx = 1, dy = 1;
for (let s = 0; s < 300; s++) {
path.push({x, y});
if (x + dx*cell*0.5 < 0 || x + dx*cell*0.5 > cols*cell) dx = -dx;
if (y + dy*cell*0.5 < 0 || y + dy*cell*0.5 > rows*cell) dy = -dy;
x += dx * cell * 0.5;
y += dy * cell * 0.5;
}
}Design variations
-
Modulo (k): replace the parity test by
(i+j) % kand map the residue to a palette of (k) colours. - Chessboard zeroing: build a matrix that is zero wherever (i+j) is even; the non-zero cells are exactly the ones that receive the “foreground” colour of the Lunda design.
- With mirrors: combine the ray-tracer of the Mirror Matrices module with the shading rule above. The mirrors change the route, the modulo rule still colours the squares the route visits.
Exercises
- Generate the Lunda design for a (5) grid with no internal mirrors. How many black squares does the single curve claim?
- Change the colouring to ((i+j)). Describe the new pattern.
- Add one vertical mirror (entry 1 in the Mirror Indicator Matrix) and re-colour. Which squares change colour?
- Project. Start from a real network’s adjacency matrix, threshold it to a Mirror Indicator Matrix, trace the resulting curve, and apply Lunda shading. Does any symmetry of the original network survive in the black-and-white design?
References
- Paulus Gerdes. Lunda Geometry: Mirror Curves, Designs, Knots, Polyominoes, Patterns, Symmetries. Lulu, 2007. (Chapters on the generation of Lunda-designs and enumeration modulo 2.)
- Paulus Gerdes. “On Lunda-designs and some of their symmetries.” Visual Mathematics, 1999.
- Companion module Mirror Matrices: From Networks to Kolams, Lusona and Rangoli.
The phrase “zeroing out a chessboard pattern” that appears in the Mirror Matrices module is exactly the construction shown here: the parity (or modulo-(k)) colouring of the unit squares visited by the mirror curve.

