Mathematical Curves for Favicon Design
Table of Contents
Explorations of mathematical curves suitable for favicon and logo design. Each curve has distinct visual properties derived from its parametric equations.
Lissajous Curves
Parametric curves defined by: \(x = \sin(at)\), \(y = \sin(bt + \delta)\)
The ratio \(a:b\) determines the shape. When \(a\) and \(b\) are coprime integers, the curve closes after one period.
import numpy as np from PIL import Image, ImageDraw configs = [ (3, 2, "lissajous_3_2.png"), (3, 4, "lissajous_3_4.png"), (5, 4, "lissajous_5_4.png"), (1, 2, "lissajous_1_2.png"), ] for a, b, name in configs: img = Image.new('RGBA', (64, 64), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) draw.rectangle([0, 0, 63, 63], fill=(30, 41, 59, 255)) t = np.linspace(0, 2 * np.pi, 500) x = np.sin(a * t) y = np.sin(b * t + np.pi/2) x_scaled = 8 + ((x + 1) / 2) * 48 y_scaled = 8 + ((y + 1) / 2) * 48 points = list(zip(x_scaled, y_scaled)) for i in range(len(points) - 1): progress = i / len(points) r = int(56 + progress * 180) g = int(189 - progress * 100) b_col = int(248 - progress * 50) draw.line([points[i], points[i+1]], fill=(r, g, b_col, 255), width=2) img.save(f'research/curves/{name}') print(f"Generated {name}")
| Ratio | Shape | Image |
|---|---|---|
| 3:2 | Bow | ![]() |
| 3:4 | Complex | ![]() |
| 5:4 | Intricate | ![]() |
| 1:2 | Parabolic | ![]() |
| 3:5 | 8-crossing | ![]() |
Lamé Curves (Superellipses)
Defined by: \(|x/a|^n + |y/b|^n = 1\)
The exponent \(n\) controls the shape:
- \(n < 1\): star/astroid
- \(n = 1\): diamond
- \(n = 2\): circle/ellipse
- \(n > 2\): squircle (rounded square)
import numpy as np from PIL import Image, ImageDraw def superellipse_points(n, num_points=500): t = np.linspace(0, 2 * np.pi, num_points) x = np.sign(np.cos(t)) * np.abs(np.cos(t)) ** (2/n) y = np.sign(np.sin(t)) * np.abs(np.sin(t)) ** (2/n) return x, y configs = [ (0.5, "superellipse_astroid.png"), (1.0, "superellipse_diamond.png"), (2.0, "superellipse_circle.png"), (2.5, "superellipse_squircle.png"), (4.0, "superellipse_rounded.png"), ] for n, name in configs: img = Image.new('RGBA', (64, 64), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) draw.rectangle([0, 0, 63, 63], fill=(30, 41, 59, 255)) x, y = superellipse_points(n) x_scaled = 10 + ((x + 1) / 2) * 44 y_scaled = 10 + ((y + 1) / 2) * 44 points = list(zip(x_scaled, y_scaled)) draw.polygon(points, fill=(56, 189, 248, 100)) for i in range(len(points) - 1): draw.line([points[i], points[i+1]], fill=(255, 255, 255, 230), width=2) draw.line([points[-1], points[0]], fill=(255, 255, 255, 230), width=2) img.save(f'research/curves/{name}') print(f"Generated {name}")
| n | Shape | Image |
|---|---|---|
| 0.5 | Astroid | ![]() |
| 1.0 | Diamond | ![]() |
| 2.0 | Circle | ![]() |
| 2.5 | Squircle | ![]() |
| 4.0 | Rounded | ![]() |
Lemniscate of Bernoulli
The figure-eight curve: \((x^2 + y^2)^2 = a^2(x^2 - y^2)\)
In polar form: \(r^2 = a^2 \cos(2\theta)\)
The horizontal orientation gives the infinity symbol \(\infty\).
import numpy as np from PIL import Image, ImageDraw def create_infinity(size=64): img = Image.new('RGBA', (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) draw.rectangle([0, 0, size-1, size-1], fill=(30, 41, 59, 255)) t = np.linspace(0, 2 * np.pi, 500) denom = 1 + np.sin(t)**2 x = np.cos(t) / denom y = np.sin(t) * np.cos(t) / denom pad, w, h = 10, 44, 44 x_scaled = pad + ((x + 1) / 2) * w y_scaled = pad + ((y + 0.5) / 1) * h points = list(zip(x_scaled, y_scaled)) draw.polygon(points, fill=(56, 189, 248, 80)) for i in range(len(points) - 1): draw.line([points[i], points[i+1]], fill=(255, 255, 255, 230), width=2) return img def create_lemniscate(size=64): img = Image.new('RGBA', (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) draw.rectangle([0, 0, size-1, size-1], fill=(30, 41, 59, 255)) theta = np.linspace(-np.pi/4, np.pi/4, 250) r_sq = np.maximum(np.cos(2 * theta), 0) r = np.sqrt(r_sq) x1, y1 = r * np.cos(theta), r * np.sin(theta) x2, y2 = -r * np.cos(theta), -r * np.sin(theta) x = np.concatenate([x1, x2[::-1]]) y = np.concatenate([y1, y2[::-1]]) x_norm = (x - x.min()) / (x.max() - x.min()) y_norm = (y - y.min()) / (y.max() - y.min()) x_scaled = 8 + x_norm * 48 y_scaled = 8 + y_norm * 48 points = list(zip(x_scaled, y_scaled)) draw.polygon(points, fill=(251, 191, 36, 120)) n = len(points) for i in range(n - 1): progress = i / n rc = int(56 + progress * 195) gc = int(189 - progress * 10) bc = int(248 - progress * 212) draw.line([points[i], points[i+1]], fill=(rc, gc, bc, 255), width=2) return img create_infinity(64).save('research/curves/lemniscate_infinity.png') create_lemniscate(64).save('research/curves/lemniscate_vertical.png') print("Generated lemniscate variants")
| Variant | Image |
|---|---|
| Infinity (\(\infty\)) | ![]() |
| Vertical | ![]() |
Rose Curves
Polar curves: \(r = \cos(k\theta)\)
For odd \(k\), you get \(k\) petals. For even \(k\), you get \(2k\) petals. Inherently bounded and distinctive at small scale.
| k | Petals | Image |
|---|---|---|
| 3 | 3 | ![]() |
| 4 | 8 | ![]() |
| 5 | 5 | ![]() |
| 7 | 7 | ![]() |
Spirograph (Hypotrochoid)
Parametric curves from the classic Spirograph toy:
- \(x = (R-r)\cos(t) + d\cos\frac{(R-r)t}{r}\)
- \(y = (R-r)\sin(t) - d\sin\frac{(R-r)t}{r}\)
Hand-drawn aesthetic but mathematically precise — fits the literate-programming vibe.
| R:r:d | Lobes | Image |
|---|---|---|
| 5:3:2 | 5 | ![]() |
| 7:4:3 | 7 | ![]() |
| 8:5:3 | 8 | ![]() |
| 10:7:4 | 10 | ![]() |
Generation Script
Run all generators:
"""Generate all curve images for favicon exploration.""" import numpy as np from PIL import Image, ImageDraw import os os.chdir(os.path.dirname(os.path.abspath(__file__))) # Lissajous for a, b, name in [(3,2,"lissajous_3_2.png"), (3,4,"lissajous_3_4.png"), (5,4,"lissajous_5_4.png"), (1,2,"lissajous_1_2.png")]: img = Image.new('RGBA', (64, 64), (30, 41, 59, 255)) draw = ImageDraw.Draw(img) t = np.linspace(0, 2*np.pi, 500) x, y = np.sin(a*t), np.sin(b*t + np.pi/2) pts = list(zip(8 + ((x+1)/2)*48, 8 + ((y+1)/2)*48)) for i in range(len(pts)-1): p = i/len(pts) draw.line([pts[i], pts[i+1]], fill=(int(56+p*180), int(189-p*100), int(248-p*50), 255), width=2) img.save(name) print(f" {name}") # Superellipses for n, name in [(0.5,"superellipse_astroid.png"), (1.0,"superellipse_diamond.png"), (2.0,"superellipse_circle.png"), (2.5,"superellipse_squircle.png"), (4.0,"superellipse_rounded.png")]: img = Image.new('RGBA', (64, 64), (30, 41, 59, 255)) draw = ImageDraw.Draw(img) t = np.linspace(0, 2*np.pi, 500) x = np.sign(np.cos(t)) * np.abs(np.cos(t))**(2/n) y = np.sign(np.sin(t)) * np.abs(np.sin(t))**(2/n) pts = list(zip(10 + ((x+1)/2)*44, 10 + ((y+1)/2)*44)) draw.polygon(pts, fill=(56, 189, 248, 100)) for i in range(len(pts)-1): draw.line([pts[i], pts[i+1]], fill=(255, 255, 255, 230), width=2) draw.line([pts[-1], pts[0]], fill=(255, 255, 255, 230), width=2) img.save(name) print(f" {name}") # Lemniscates img = Image.new('RGBA', (64, 64), (30, 41, 59, 255)) draw = ImageDraw.Draw(img) t = np.linspace(0, 2*np.pi, 500) denom = 1 + np.sin(t)**2 x, y = np.cos(t)/denom, np.sin(t)*np.cos(t)/denom pts = list(zip(10 + ((x+1)/2)*44, 10 + ((y+0.5)/1)*44)) draw.polygon(pts, fill=(56, 189, 248, 80)) for i in range(len(pts)-1): draw.line([pts[i], pts[i+1]], fill=(255, 255, 255, 230), width=2) img.save("lemniscate_infinity.png") print(" lemniscate_infinity.png") img = Image.new('RGBA', (64, 64), (30, 41, 59, 255)) draw = ImageDraw.Draw(img) theta = np.linspace(-np.pi/4, np.pi/4, 250) r = np.sqrt(np.maximum(np.cos(2*theta), 0)) x = np.concatenate([r*np.cos(theta), -r[::-1]*np.cos(theta[::-1])]) y = np.concatenate([r*np.sin(theta), -r[::-1]*np.sin(theta[::-1])]) xn, yn = (x-x.min())/(x.max()-x.min()), (y-y.min())/(y.max()-y.min()) pts = list(zip(8 + xn*48, 8 + yn*48)) draw.polygon(pts, fill=(251, 191, 36, 120)) for i in range(len(pts)-1): p = i/len(pts) draw.line([pts[i], pts[i+1]], fill=(int(56+p*195), int(189-p*10), int(248-p*212), 255), width=2) img.save("lemniscate_vertical.png") print(" lemniscate_vertical.png") print("Done.")




















