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 lissajous_3_2.png
3:4 Complex lissajous_3_4.png
5:4 Intricate lissajous_5_4.png
1:2 Parabolic lissajous_1_2.png
3:5 8-crossing lissajous_3_5.png

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 superellipse_astroid.png
1.0 Diamond superellipse_diamond.png
2.0 Circle superellipse_circle.png
2.5 Squircle superellipse_squircle.png
4.0 Rounded superellipse_rounded.png

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\)) lemniscate_infinity.png
Vertical lemniscate_vertical.png

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 rose_3.png
4 8 rose_4.png
5 5 rose_5.png
7 7 rose_7.png

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 spirograph_5_3_2.png
7:4:3 7 spirograph_7_4_3.png
8:5:3 8 spirograph_8_5_3.png
10:7:4 10 spirograph_10_7_4.png

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.")

Author: Jason Walsh

jwalsh@nexus

Last Updated: 2026-02-05 21:41:25

build: 2026-04-17 18:35 | sha: 792b203