WWN Winding Visualization
Squid traversal on a 7-node webring
Winding Number Visualization
The squid (visitor) traverses a ring of \(\text{ord}=7\) sites. Three things can happen at each step:
- Forward/backward hop – move to adjacent site, \(w\) unchanged
- Wraparound – cross the \(k=\text{ord} \to k=1\) boundary, \(w \pm 1\)
- Dwell – browse internally at a site, \(w\) frozen
The spiral drift outward shows accumulated winding. Each complete revolution (7 hops same direction) increments \(|w|\) by exactly 1.
Generate
#!/usr/bin/env python3
"""
wwn-viz.py -- Winding number visualization for ord=7 ring.
The squid's path spirals outward as w accumulates.
Dashed loop = internal browse (deviation from ring, w frozen).
↻ at k=1 = wraparound crossing (w changes).
"""
import math
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
# ── Config ────────────────────────────────────────────────────
N = 7
RING_R = 3.0
NODE_R = 0.22
SPIRAL_GAP = 0.35 # outward drift per revolution
LABELS = ['wal.sh', 'alice', 'bob', 'carol', 'dave', 'eve', 'frank']
COLORS = ['#6af', '#f84', '#8f6', '#fa6', '#a6f', '#6ff', '#f6a']
# ── Geometry ──────────────────────────────────────────────────
def node_xy(i):
"""Node i (0-indexed) on heptagon. k=1 at top."""
angle = math.pi / 2 - (2 * math.pi * i / N)
return RING_R * math.cos(angle), RING_R * math.sin(angle)
NODES = [node_xy(i) for i in range(N)]
def ring_path(laps=2, steps_per_edge=30):
"""Squid path spiraling outward over `laps` revolutions."""
points = []
total_edges = laps * N
for edge in range(total_edges):
i = edge % N
j = (edge + 1) % N
x0, y0 = NODES[i]
x1, y1 = NODES[j]
for step in range(steps_per_edge):
t = step / steps_per_edge
t_total = (edge + t) / total_edges
x = x0 + t * (x1 - x0)
y = y0 + t * (y1 - y0)
dx, dy = x, y
dist = math.sqrt(dx*dx + dy*dy)
if dist > 0:
offset = SPIRAL_GAP * t_total
x += (dx / dist) * offset
y += (dy / dist) * offset
points.append((x, y, t_total, edge))
return points
def deviation_path(from_node=2, to_node=3, amplitude=2.0, steps=60):
"""Internal browse loop -- deviates away from ring then returns."""
x0, y0 = NODES[from_node]
x1, y1 = NODES[to_node]
mx, my = (x0 + x1) / 2, (y0 + y1) / 2
norm = math.sqrt(mx*mx + my*my)
if norm > 0:
nx, ny = mx / norm * amplitude, my / norm * amplitude
else:
nx, ny = 0, amplitude
points = []
for step in range(steps + 1):
t = step / steps
bx = (1-t)**2 * x0 + 2*(1-t)*t * (mx + nx) + t**2 * x1
by = (1-t)**2 * y0 + 2*(1-t)*t * (my + ny) + t**2 * y1
points.append((bx, by))
return points
# ── Draw ──────────────────────────────────────────────────────
def draw(laps=2, show_deviation=True, outfile='static/images/wwn-winding.svg'):
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
fig.patch.set_facecolor('#111')
ax.set_facecolor('#111')
ax.set_aspect('equal')
ax.axis('off')
# Ring edges
for i in range(N):
j = (i + 1) % N
x0, y0 = NODES[i]
x1, y1 = NODES[j]
ax.plot([x0, x1], [y0, y1], color='#333', linewidth=1.5, zorder=1)
# Squid path (color gradient blue→orange)
path = ring_path(laps=laps)
xs = [p[0] for p in path]
ys = [p[1] for p in path]
ts = [p[2] for p in path]
for i in range(len(path) - 1):
t = ts[i]
r = 0.4 + 0.6 * t
g = 0.67 - 0.17 * t
b = 1.0 - 0.75 * t
ax.plot([xs[i], xs[i+1]], [ys[i], ys[i+1]],
color=(r, g, b), linewidth=2.2, alpha=0.85, zorder=3)
# Squid head arrow
if len(path) >= 4:
hx, hy = xs[-1], ys[-1]
dx = xs[-1] - xs[-3]
dy = ys[-1] - ys[-3]
ax.annotate('', xy=(hx, hy),
xytext=(hx - dx*0.3, hy - dy*0.3),
arrowprops=dict(arrowstyle='->', color='#f84',
lw=2.5, mutation_scale=20),
zorder=5)
# Deviation loop (dashed, between bob and carol)
if show_deviation:
dev = deviation_path(from_node=2, to_node=3, amplitude=2.0)
dev_xs = [p[0] for p in dev]
dev_ys = [p[1] for p in dev]
ax.plot(dev_xs, dev_ys, color='#6af', linewidth=1.8,
linestyle='--', alpha=0.6, zorder=2)
peak = len(dev) // 2
ax.text(dev_xs[peak] + 0.15, dev_ys[peak] + 0.15,
'/research/?w=1',
color='#6af', fontsize=7, alpha=0.7,
fontfamily='monospace', zorder=6)
ax.text(dev_xs[peak] + 0.15, dev_ys[peak] - 0.15,
'w frozen, no ring hop',
color='#999', fontsize=6, alpha=0.5,
fontfamily='monospace', zorder=6)
# Wraparound markers at k=1
for lap in range(laps):
wx, wy = NODES[0]
offset = SPIRAL_GAP * (lap + 1) / laps
mx = wx + (wx / RING_R) * (offset + 0.15)
my = wy + (wy / RING_R) * (offset + 0.15)
ax.text(mx + 0.2, my,
f'w={lap}\u2192{lap+1} \u21BB',
color='#f84', fontsize=8, fontweight='bold',
fontfamily='monospace', ha='left', va='center', zorder=6)
# Nodes
for i, (x, y) in enumerate(NODES):
circle = plt.Circle((x, y), NODE_R, color=COLORS[i],
ec='#fff', linewidth=1.5, zorder=4)
ax.add_patch(circle)
lx = x * (1 + 0.55 / RING_R)
ly = y * (1 + 0.55 / RING_R)
ax.text(lx, ly, f'k={i+1}',
color='#fff', fontsize=9, fontweight='bold',
fontfamily='monospace', ha='center', va='center', zorder=6)
nx = x * (1 + 0.85 / RING_R)
ny = y * (1 + 0.85 / RING_R)
ax.text(nx, ny, LABELS[i],
color='#999', fontsize=7, fontfamily='monospace',
ha='center', va='center', zorder=6)
# Direction indicator
ax.annotate('', xy=NODES[1], xytext=NODES[0],
arrowprops=dict(arrowstyle='->', color='#555',
lw=1, mutation_scale=15), zorder=2)
# Captions
ax.text(0, -RING_R - 1.5,
f'Webring Winding Number \u00b7 ord={N} \u00b7 {laps} revolutions',
color='#ccc', fontsize=11, fontfamily='monospace',
ha='center', va='center', zorder=6)
ax.text(0, -RING_R - 2.0,
'spiral outward = accumulating w \u00b7 dashed = internal browse (w frozen)',
color='#666', fontsize=8, fontfamily='monospace',
ha='center', va='center', zorder=6)
margin = 2.2
ax.set_xlim(-RING_R - margin, RING_R + margin)
ax.set_ylim(-RING_R - margin - 0.8, RING_R + margin)
plt.tight_layout()
plt.savefig(outfile, format='svg', facecolor='#111',
bbox_inches='tight', pad_inches=0.3)
plt.close()
print(f'wrote {outfile}')
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--laps', type=int, default=2)
parser.add_argument('--no-deviation', action='store_true')
parser.add_argument('-o', '--output', default='static/images/wwn-winding.svg')
args = parser.parse_args()
draw(laps=args.laps, show_deviation=not args.no_deviation, outfile=args.output)
Reading the Diagram
| Visual element | Meaning |
|---|---|
| Heptagon (grey) | The ring: 7 sites, fixed positions |
| Solid spiral | Squid's path along ring edges |
| Blue → orange | Color = accumulated winding (early → late) |
| Outward drift | Each revolution pushes path outward |
↻ w=0→1 |
Wraparound crossing at k=1 (boundary) |
| Dashed loop | Internal browse: deviation from ring |
/research/?w=1 |
Page visited during dwell (w preserved) |
| Arrow head (orange) | Current squid position |
The Three Motions
1. Ring Hop (Solid Line)
Click prev/next in the webring nav. Move to adjacent site.
- 6 out of 7 hops: \(w\) unchanged (interior edge)
- 1 out of 7 hops: \(w \pm 1\) (crosses boundary at \(k=1\))
The ratio is \((\text{ord}-1)/\text{ord}\) identity transitions. At \(\text{ord}=7\), the ring is 85.7% "quiet topology" – most clicks don't change the winding.
2. Wraparound (↻ Marker)
The only event that changes \(w\). Two cases:
- Forward: \(k=\text{ord}\), click next to \(k=1\), \(w \gets w+1\)
- Backward: \(k=1\), click prev to \(k=\text{ord}\), \(w \gets w-1\)
In the diagram: two ↻ markers at k=1 (wal.sh), one per revolution.
3. Deviation / Dwell (Dashed Loop)
Click any internal link on a site. The visitor leaves the ring
edge but stays on the same site. \(w\) is preserved in the URL
(/research/?w=1) but no ring transition occurs.
Visually: the curve loops away from the heptagon and returns. Topologically: a contractible detour. It doesn't contribute to the winding number. The curve can be continuously deformed to remove the loop without changing \(w\).
This is the "squid" ambiguity: the link to /research/?w=1 is
contaminated by the JS (it injected ?w=1), but the visitor
can't distinguish it from a human-chosen link. The w rides
along invisibly.
Variants
Generate with different parameters:
# 3 laps
python3 scripts/wwn-viz.py --laps 3 -o static/images/wwn-winding-3lap.svg
# clean (no deviation loop)
python3 scripts/wwn-viz.py --no-deviation -o static/images/wwn-winding-clean.svg
# single revolution
python3 scripts/wwn-viz.py --laps 1 -o static/images/wwn-winding-1lap.svg