fuzzyworld

tower / files / src / views / cap-pattern.ts

cap-pattern.ts

Blame: the commit that last changed each line · file · history

c44860b9 2026-09-07 Pavel Safronov1/**
c954a775 2026-09-28 Pavel Safronov2 * The window frames on the end caps, drawn once and shared by both 3D views.
c44860b9 2026-09-07 Pavel Safronov3 *
c954a775 2026-09-28 Pavel Safronov4 * Both caps are glazed (D-045, D-050), and the structure holding the glass is a lattice
5 * of **identical equilateral triangles** (D-078): three families of straight mullions at
6 * 60° to each other, with a vertex on the axis where six triangles meet. It replaced a
7 * polar tessellation of rings and spokes, at the author's request.
c44860b9 2026-09-07 Pavel Safronov8 *
c954a775 2026-09-28 Pavel Safronov9 * The triangles keep the old cells' pitch, CELL_KM on a side, so the cap is still the one
10 * object in the world with a known scale (D-041): a glance at the far cap's lattice tells
11 * you how far away it is. A disc cannot be tiled by whole triangles, so the rim cuts the
12 * outermost ones; the rim mullion closes them.
c44860b9 2026-09-07 Pavel Safronov13 */
14import { CanvasTexture, LinearFilter, SRGBColorSpace, type Texture } from 'three'
da6bcb0e 2026-09-08 Pavel Safronov15import { FAR_CAP_MIRROR_RADIUS_M } from '../canon/constants.js'
c44860b9 2026-09-07 Pavel Safronov16
17const RADIUS_KM = 50
18/**
c954a775 2026-09-28 Pavel Safronov19 * A triangle's side, km: the old polar cells' pitch, six across the radius. Finer looked
20 * better face-on from inside and collapsed into a dark cross-hatch from outside, where
21 * the caps are seen near edge-on (D-030) and foreshortened ten to one.
c44860b9 2026-09-07 Pavel Safronov22 */
c954a775 2026-09-28 Pavel Safronov23export const TRIANGLE_KM = RADIUS_KM / 6
c44860b9 2026-09-07 Pavel Safronov24
c954a775 2026-09-28 Pavel Safronov25/** The window frame. Light: it is seen against sky. */
769726ea 2026-09-08 Pavel Safronov26const MULLION = '#aeb7c6'
da6bcb0e 2026-09-08 Pavel Safronov27/** The bullseye mirror. Near-white: it is returning 1.7 MW/m² back up the axis. */
28const MIRROR = '#f2f6ff'
c44860b9 2026-09-07 Pavel Safronov29
30/**
c954a775 2026-09-28 Pavel Safronov31 * The lattice's mullions as line segments in km, centred on the axis and clipped to the
32 * cap: for each of three directions 60° apart, the lines of that family, one triangle
33 * height apart. Every line through the lattice is one of these, so the triangles they
34 * bound are all the same triangle.
c44860b9 2026-09-07 Pavel Safronov35 */
c954a775 2026-09-28 Pavel Safronov36export function triangleMullions(): [number, number, number, number][] {
37 const height = (TRIANGLE_KM * Math.sqrt(3)) / 2
38 const out: [number, number, number, number][] = []
39 for (let f = 0; f < 3; f++) {
40 const a = (f * Math.PI) / 3
41 const dx = Math.cos(a)
42 const dy = Math.sin(a)
43 // Offset perpendicular to the line: (−dy, dx).
44 const reach = Math.floor(RADIUS_KM / height)
45 for (let k = -reach; k <= reach; k++) {
46 const off = k * height
47 const half = Math.sqrt(Math.max(0, RADIUS_KM * RADIUS_KM - off * off))
48 if (half <= 0) continue
49 const cx = -dy * off
50 const cy = dx * off
51 out.push([cx - dx * half, cy - dy * half, cx + dx * half, cy + dy * half])
c44860b9 2026-09-07 Pavel Safronov52 }
53 }
c954a775 2026-09-28 Pavel Safronov54 return out
c44860b9 2026-09-07 Pavel Safronov55}
56
769726ea 2026-09-08 Pavel Safronov57/**
c954a775 2026-09-28 Pavel Safronov58 * The glazing: frame around transparent panes. Alpha is the whole point of this canvas —
59 * opaque where there is structure, clear where there is a pane; the colour channels
60 * carry the mullion's own tone.
769726ea 2026-09-08 Pavel Safronov61 *
c954a775 2026-09-28 Pavel Safronov62 * Drawn into a square canvas covering the cap's bounding box, which is how Three maps UVs
63 * onto both CircleGeometry and RingGeometry, so the one image lands correctly on the far
64 * cap's disc and on the sun cap's annulus.
769726ea 2026-09-08 Pavel Safronov65 */
9c891d2e 2026-09-08 Pavel Safronov66export function drawCapGlazing(size = 2048, bullseye = true): HTMLCanvasElement {
769726ea 2026-09-08 Pavel Safronov67 const c = document.createElement('canvas')
68 c.width = size
69 c.height = size
70 const ctx = c.getContext('2d')!
71 const mid = size / 2
72 const px = mid / RADIUS_KM
73
74 // Transparent ground: every pixel not drawn below is a pane.
75 ctx.clearRect(0, 0, size, size)
76 ctx.strokeStyle = MULLION
77 ctx.lineCap = 'round'
78
79 /**
c954a775 2026-09-28 Pavel Safronov80 * Mullions are 400 m of structure across a 100 km window — heavy for a window and
81 * slight for a wall, which is about right for something holding an atmosphere back.
82 * Below a pixel on screen the line simply fades, and the cap goes clear rather than
83 * moiring, which is the behaviour D-041 wanted.
769726ea 2026-09-08 Pavel Safronov84 */
85 const mullionKm = 0.4
86 ctx.lineWidth = Math.max(1, mullionKm * px)
c954a775 2026-09-28 Pavel Safronov87 for (const [x0, y0, x1, y1] of triangleMullions()) {
769726ea 2026-09-08 Pavel Safronov88 ctx.beginPath()
c954a775 2026-09-28 Pavel Safronov89 ctx.moveTo(mid + x0 * px, mid + y0 * px)
90 ctx.lineTo(mid + x1 * px, mid + y1 * px)
769726ea 2026-09-08 Pavel Safronov91 ctx.stroke()
92 }
93
c954a775 2026-09-28 Pavel Safronov94 // The rim: heavier, closing the triangles the edge cuts, and the frame the glass sits in.
769726ea 2026-09-08 Pavel Safronov95 ctx.lineWidth = Math.max(1.5, mullionKm * 2.5 * px)
c954a775 2026-09-28 Pavel Safronov96 ctx.beginPath()
97 ctx.arc(mid, mid, RADIUS_KM * px - ctx.lineWidth / 2, 0, Math.PI * 2)
98 ctx.stroke()
769726ea 2026-09-08 Pavel Safronov99
da6bcb0e 2026-09-08 Pavel Safronov100 /**
c954a775 2026-09-28 Pavel Safronov101 * The bullseye: not glass, and the only part of the far cap that is not (D-048). The
102 * beam arrives collimated in its own cross-section, so what has to survive 1.7 MW/m²
103 * is a 4 km disc on the axis and nothing else — the mirror that sends it back up the
104 * column for a second pass. It sits on the lattice's centre vertex.
da6bcb0e 2026-09-08 Pavel Safronov105 *
c954a775 2026-09-28 Pavel Safronov106 * Alpha 1, so the star field does not show through it. It does not occult the pole
107 * star, though the geometry invites that guess: the pinwheel turns about the direction
108 * parallel to the axis, which from the floor is horizontal, so its centre lands on the
109 * window at the observer's own radius — down at eye level — while the mirror is an
110 * object on the axis, about 10° above it from 270 km.
da6bcb0e 2026-09-08 Pavel Safronov111 */
9c891d2e 2026-09-08 Pavel Safronov112 if (!bullseye) return c
da6bcb0e 2026-09-08 Pavel Safronov113 const mirrorKm = FAR_CAP_MIRROR_RADIUS_M / 1000
114 ctx.fillStyle = MIRROR
115 ctx.beginPath()
116 ctx.arc(mid, mid, mirrorKm * px, 0, Math.PI * 2)
117 ctx.fill()
c954a775 2026-09-28 Pavel Safronov118 // A collar around it, so at a distance the mirror reads as set into structure rather
119 // than as a bright fleck of nothing.
da6bcb0e 2026-09-08 Pavel Safronov120 ctx.lineWidth = Math.max(1, mullionKm * 1.5 * px)
121 ctx.beginPath()
122 ctx.arc(mid, mid, mirrorKm * px, 0, Math.PI * 2)
123 ctx.stroke()
769726ea 2026-09-08 Pavel Safronov124 return c
125}
126
da6bcb0e 2026-09-08 Pavel Safronov127/**
c954a775 2026-09-28 Pavel Safronov128 * How far out the mirror reaches, as a fraction of the cap's radius. The shaders need it:
129 * the bullseye is the one part of the window that is lit by the beam, so it brightens and
130 * banks with the beam instead of with the ambient light.
da6bcb0e 2026-09-08 Pavel Safronov131 */
132export const MIRROR_UV_RADIUS = FAR_CAP_MIRROR_RADIUS_M / 1000 / RADIUS_KM
133
c954a775 2026-09-28 Pavel Safronov134function glazingTexture(bullseye: boolean): Texture {
135 const tex = new CanvasTexture(drawCapGlazing(2048, bullseye))
c44860b9 2026-09-07 Pavel Safronov136 tex.colorSpace = SRGBColorSpace
c954a775 2026-09-28 Pavel Safronov137 // Mipmapped: an aliasing window frame twinkles, which reads as the stars behind it
138 // moving. Averaged away once the mullions fall below a pixel, the cap goes clear.
c44860b9 2026-09-07 Pavel Safronov139 tex.anisotropy = 16
140 tex.magFilter = LinearFilter
c954a775 2026-09-28 Pavel Safronov141 return tex
c44860b9 2026-09-07 Pavel Safronov142}
769726ea 2026-09-08 Pavel Safronov143
c954a775 2026-09-28 Pavel Safronov144/** The far cap's glazing, built once; both views share the one instance. */
769726ea 2026-09-08 Pavel Safronov145let glazingCached: Texture | null = null
146export function capGlazingTexture(): Texture {
c954a775 2026-09-28 Pavel Safronov147 return (glazingCached ??= glazingTexture(true))
769726ea 2026-09-08 Pavel Safronov148}
9c891d2e 2026-09-08 Pavel Safronov149
150/**
c954a775 2026-09-28 Pavel Safronov151 * The sun cap's glazing: the same frame, without the bullseye. Its middle is the beam
152 * aperture — a hole in the ring, not a mirror — so drawing one there would put a sliver
153 * of false structure around the rim of the opening.
9c891d2e 2026-09-08 Pavel Safronov154 */
155let sunGlazingCached: Texture | null = null
156export function sunCapGlazingTexture(): Texture {
c954a775 2026-09-28 Pavel Safronov157 return (sunGlazingCached ??= glazingTexture(false))
9c891d2e 2026-09-08 Pavel Safronov158}