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:

  1. Forward/backward hop — move to adjacent site, \(w\) unchanged
  2. Wraparound — cross the \(k=\text{ord} \to k=1\) boundary, \(w \pm 1\)
  3. 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

wwn-winding.svg

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

Author: jwalsh

jwalsh@nexus

Last Updated: 2026-02-07 14:40:46

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