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 → \(k=1\), \(w \gets w+1\)
- Backward: \(k=1\), click prev → \(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
