From d85fb2e6385971fa3e597087f3e105d19b1000d5 Mon Sep 17 00:00:00 2001 From: The Dust Council Date: Mon, 1 Jun 2026 21:59:36 -0700 Subject: [PATCH] Excellent state. --- .claude/settings.local.json | 29 + Makefile | 19 + README.md | 141 +++ vectorgons | Bin 0 -> 76096 bytes vectorgons.c | 1607 +++++++++++++++++++++++++++++++++++ 5 files changed, 1796 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 Makefile create mode 100644 README.md create mode 100755 vectorgons create mode 100644 vectorgons.c diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..55cb29c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,29 @@ +{ + "permissions": { + "allow": [ + "Bash(pkg-config --list-all)", + "Read(//usr/include/GLFW/**)", + "Bash(ldconfig -p)", + "Bash(make *)", + "Bash(echo \"exit: $?\")", + "Bash(timeout 2 ./vectorgons)", + "Read(//tmp/**)", + "Bash(glxinfo)", + "Bash(grep -iE '^\\(scrot|grim|import|maim|spectacle\\)$')", + "Bash(convert vg_shot.png -crop 270x210+0+0 +repage -resize 300% vg_crop.png)", + "Bash(identify -format \"%wx%h\\\\n\" vg_multi.png)", + "Bash(convert vg_multi.png -crop 760x600+460+0 +repage vg_multi_solids.png)", + "Bash(convert vg2.png -crop 740x620+0+0 +repage vg2_crop.png)", + "Bash(convert vg_ast.png -resize 1000x vg_ast_full.png)", + "Bash(xwininfo -name Vectorgons -int)", + "Bash(timeout 3 ./vectorgons)", + "Bash(timeout 4 ./vectorgons)", + "Bash(cat)", + "Bash(cc -O2 -o /tmp/sim /tmp/sim.c -lm)", + "Bash(/tmp/sim)", + "Bash(cc -O2 -o /tmp/st2 /tmp/st2.c -lm)", + "Bash(/tmp/st2)", + "Bash(rm -f /tmp/st2 /tmp/st2.c)" + ] + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..669a6be --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +CC ?= gcc +CFLAGS ?= -O2 -Wall -Wextra -std=c11 +PKGS = glfw3 glu +CPPFLAGS += $(shell pkg-config --cflags $(PKGS)) +LDLIBS += $(shell pkg-config --libs $(PKGS)) -lGL -lm + +TARGET = vectorgons +SRC = vectorgons.c + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $(SRC) $(LDLIBS) + +run: $(TARGET) + ./$(TARGET) + +clean: + rm -f $(TARGET) + +.PHONY: run clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..28c6f59 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# Vectorgons + +A starfield simulator — except instead of stars, colorful **vector-drawn +platonic solids** tumble through space toward the camera. + +**~110 shape types** are rendered as glowing wireframes — each spawns far away +at a random size, tumbles on its own random axis, and streams past the camera +before being recycled at the render-distance sphere. Solids never overlap. + +The shape set includes: + +- **Platonic solids:** tetrahedron, cube, octahedron, dodecahedron, icosahedron +- **Other polyhedra:** cuboctahedron, truncated octahedron, stella octangula +- **Prisms / antiprisms / bipyramids** (various base polygons) +- **Many-faced solids named for their face count:** tridecahedron (13), + tetradecahedron (14), pentadecahedron (15), heptadecahedron (17), + octadecahedron (18), enneadecahedron (19), icosahedron (20), + icositetrahedron (24), triacontahedron (30), hexacontahedron (60), and + hecatohedron (100) — built from prism / bipyramid / trapezohedron families so + each lands on exactly the requested number of faces +- **Star polygons:** {5/2}, {6/2}, {7/2}, {7/3}, {8/3}, {9/2}, {9/4}, {12/5}, + plus the **unicursal hexagram** +- **Googie / atomic-age shapes**, most in both flat **2-D** and **3-D** forms: + jet-age bowling-alley twinkles (4- and 8-point), a layered double starburst, + an atomic burst with electron caps, a ray sunburst, boomerangs, kidney/amoeba + blobs, orbital atoms, concentric orbit rings, and a dense ray-star — plus the + naturally-3-D **Sputnik** satellite, a three-ring **gyroscope** cage, and a + spike-orb sea-urchin +- **Symbols & signs**, each in a flat **2-D** and an extruded **3-D** form: + smiley face, biohazard, peace sign, cross, question mark, exclamation point, + hash/pound (`#`), dollar sign (`$`), and pound sterling (`£`) +- **Random 3-D asteroids:** lumpy convex-hull rocks generated fresh each run, + with random vertex counts (≈7–26) giving each a different number of sides and + level of complexity +- **4-, 5- and 6-dimensional polytopes:** the 5-cell, **tesseract (hypercube)**, + penteract (5-cube), 6-cube, 16-cell, 5- and 6-orthoplexes, the 24-cell, and + the 5-simplex. These are rotated *in their own dimension* and + perspective-projected down to 3D every frame, so they morph (the classic + "cube-within-a-cube" unfolding) while also tumbling in 3D. + +## Build & run + +Requires a C compiler, GLFW3, and GLU (all detected via `pkg-config`). + +```sh +make +./vectorgons # or: make run +``` + +## Settings persistence + +Your settings are saved automatically on exit to `~/.vectorgons` (a plain +`key=value` text file) and reloaded on the next launch as the new defaults — so +the speed, tumble, render distance, density, colors, glow, fullscreen, etc. you +last used carry over. Delete the file to return to the built-in defaults; it's +safe to hand-edit (values are range-clamped on load). + +## On-screen display + +A vector-drawn OSD (top-left) lists every setting and its key binding. It +stays fully visible for **10 seconds** after your last keypress, then fades +out over a few seconds. Press any key to bring it back. + +## Controls + +| Setting | Keys | Notes | +|---------------------|-----------------|------------------------------------------| +| Move camera | `W`/`A`/`S`/`D` | Pan the camera up / left / down / right (hold to fly) | +| Rotate camera | `←`/`→`/`↑`/`↓` | Yaw / pitch the camera view (hold to turn) | +| Approach speed | `PgUp`/`PgDn` | How fast solids fly at you; `PgDn` past 0 reverses (fly backward) | +| Tumble rate | `Q` / `E` | Base rotation speed | +| Tumble variance | `T` / `Y` | Spread of tumble speeds: low = uniform, high = a very wide range (responds live) | +| Render distance | `Z` / `X` | Radius of the field sphere / far plane (40–1520; hold to ramp) | +| Density | `+` / `-` | Baseline number of solids (scales up with render distance; readout shows the live count) | +| Size min | `U` / `J` | Minimum random solid size | +| Size max | `I` / `K` | Maximum random solid size | +| Hue | `[` / `]` | Base color | +| Hue cycle | `C` / `V` | Continuously cycle through all hues (0 = off); all objects sweep the spectrum | +| Color mode | `M` | Toggle **single-hue** ⇄ **multicolor** | +| Glow | `O` / `L` | CRT glow / light bleed around vectors | +| Flicker | `G` / `H` | Vector flicker intensity | +| Shapes | `N` | Toggle random ⇄ cycling shape spawns | +| Fullscreen | `F` or `F11` | Toggle fullscreen | +| Pause | `Space` | | +| Quit | `Esc` | | + +## Color modes + +- **Single hue:** every solid uses the hue set with `[` / `]`. +- **Multicolor:** each solid gets its own hue (its base offset plus the + global hue, so `[` / `]` rotates the whole palette). +- **Hue cycle (`C` / `V`):** continuously advances the base hue over time (up + to ~120°/sec). In single-hue mode every object sweeps through the spectrum + together; in multicolor mode the whole palette rotates. Set to `0` to stop. + +## How it works + +- **Geometry** comes from an N-dimensional polytope engine (3–6 dimensions). + For regular figures, edges are derived automatically by connecting every + vertex pair at the shared minimum distance; parametric families (prisms, + stars, ...) set edges explicitly. +- **Higher-dimensional shapes** keep their full 4/5/6-D coordinates and are + rotated in their own dimension (driven by the tumble angle plus a per-body + phase), then perspective-projected down to 3D each frame and renormalized to + the unit sphere — which also keeps the no-overlap bound intact. +- **Asteroids** scatter random points over a sphere at jittered radii and wire + them up as a brute-force convex hull, so each rock is a unique faceted blob. +- **The field is a sphere** of radius *render distance* centered on the camera. + Bodies stream along −Z (or +Z when speed is negative) and, once they leave the + sphere, are recycled back to the **render-distance shell** on the incoming + side — so objects always appear far away and approach, never popping in close. + They ease in over a fraction of a second at the shell. The initial fill spreads + bodies through the volume so the field starts populated. Because culling is + purely radial, the camera can yaw/pitch a full 360° without revealing an edge. +- **Render distance** (`Z`/`X`, 40–1520) sets the sphere radius and the + perspective far plane. The **active body count scales with it**, so a deeper + field simply holds proportionally more shapes (constant near-field density) + instead of thinning out to empty space — the `DENSITY` readout shows the live + count, and `+`/`-` sets the baseline (the count at the default distance). +- **The camera** pans in world space via `WASD` and yaws/pitches via the arrow + keys (both polled per frame so movement is smooth and frame-rate + independent); the rest of the field streams past it. +- **No overlap:** each solid's bounding-sphere radius equals its size, and + spawn positions are rejection-sampled so no two spheres intersect (with a + margin). Because every solid translates by the same amount each frame, + non-overlap at spawn is preserved for the body's whole flight. +- **Tumble variance** is applied live as a log-uniform spread + (`spin = 4^(seed · variance)`), so changing it re-spreads every solid's + rotation rate instantly — `0` makes them all tumble alike, `100` gives a + very wide range. +- **CRT glow** wraps each sharp vector in a soft phosphor mist: many faint + additive width layers fade outward into a halo, and a few faint enlarged + ghost copies bloom that glow into a larger volume than the hardware line-width + cap (~10px) could reach on its own. The crisp core is drawn last so the vector + stays sharp; the glow setting scales the mist's spread and brightness. +- **Flicker** randomly dips each solid's brightness per frame, scaled by the + flicker setting. +- **Rendering** uses legacy OpenGL immediate mode (`GL_LINES`) with additive + blending, MSAA, and line smoothing; depth-based fading brightens and + thickens solids as they approach. The OSD uses a self-contained vector + stroke font (no font dependencies). diff --git a/vectorgons b/vectorgons new file mode 100755 index 0000000000000000000000000000000000000000..15383f5854df594aae756b761b35f3c1d44d1152 GIT binary patch literal 76096 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 +#include + +#include +#include +#include +#include +#include +#include + +/* ================================================================== */ +/* Geometry: an N-dimensional polytope engine (3..6 dimensions). */ +/* */ +/* Every shape stores vertices in up to MAXD dimensions plus an edge */ +/* list. 3-D shapes (platonic solids, archimedeans, prisms, stars) */ +/* are drawn directly; 4/5/6-D polytopes (tesseract, 24-cell, ...) */ +/* are rotated in their own dimension and perspective-projected down */ +/* to 3-D every frame, so they morph like true hyperobjects while */ +/* also tumbling in 3-D. */ +/* */ +/* For regular figures, edges are derived automatically: connect */ +/* every vertex pair at the shared minimum distance. Families with */ +/* non-uniform edges (prisms, stars, ...) set edges explicitly. */ +/* ================================================================== */ + +#define MAXD 6 +#define MAX_VERTS 128 +#define MAX_EDGES 512 +#define MAX_SHAPES 160 +#define TAU 6.283185307179586 + +typedef struct { + int dim; /* 3..6 */ + int nv; + float v[MAX_VERTS][MAXD]; + int ne; + int e[MAX_EDGES][2]; +} Solid; + +static Solid solids[MAX_SHAPES]; +static int num_shapes = 0; + +static float frand(void); /* uniform [0,1); defined below */ + +static Solid *new_solid(int dim) { + Solid *s = &solids[num_shapes++]; + s->dim = dim; s->nv = 0; s->ne = 0; + return s; +} +static void PV(Solid *s, double a,double b,double c,double d,double e,double f) { + if (s->nv >= MAX_VERTS) return; + float *p = s->v[s->nv++]; + p[0]=(float)a; p[1]=(float)b; p[2]=(float)c; p[3]=(float)d; p[4]=(float)e; p[5]=(float)f; +} +static void AE(Solid *s, int i, int j) { + if (s->ne >= MAX_EDGES) return; + s->e[s->ne][0]=i; s->e[s->ne][1]=j; s->ne++; +} +static void AE_unique(Solid *s, int i, int j) { + if (i > j) { int t = i; i = j; j = t; } + for (int k = 0; k < s->ne; k++) + if (s->e[k][0] == i && s->e[k][1] == j) return; + AE(s, i, j); +} + +/* Center on the centroid and scale so the farthest vertex sits on the + * unit sphere (radius == 1, in `dim` dimensions). */ +static void center_normalize(Solid *s) { + double cen[MAXD] = {0,0,0,0,0,0}; + for (int i = 0; i < s->nv; i++) + for (int k = 0; k < s->dim; k++) cen[k] += s->v[i][k]; + for (int k = 0; k < s->dim; k++) cen[k] /= (s->nv > 0 ? s->nv : 1); + + double maxr2 = 0; + for (int i = 0; i < s->nv; i++) { + double r2 = 0; + for (int k = 0; k < s->dim; k++) { s->v[i][k] -= cen[k]; r2 += (double)s->v[i][k]*s->v[i][k]; } + if (r2 > maxr2) maxr2 = r2; + } + double inv = maxr2 > 1e-12 ? 1.0/sqrt(maxr2) : 1.0; + for (int i = 0; i < s->nv; i++) + for (int k = 0; k < s->dim; k++) s->v[i][k] *= inv; +} + +/* Derive edges from the minimum pairwise distance (regular figures). */ +static void derive_edges(Solid *s) { + s->ne = 0; + double mind2 = 1e18; + for (int i = 0; i < s->nv; i++) + for (int j = i+1; j < s->nv; j++) { + double d2 = 0; + for (int k = 0; k < s->dim; k++) { double dd = s->v[i][k]-s->v[j][k]; d2 += dd*dd; } + if (d2 < mind2) mind2 = d2; + } + double tol2 = mind2 * 1.04; /* ~2% slack on length */ + for (int i = 0; i < s->nv; i++) + for (int j = i+1; j < s->nv; j++) { + double d2 = 0; + for (int k = 0; k < s->dim; k++) { double dd = s->v[i][k]-s->v[j][k]; d2 += dd*dd; } + if (d2 <= tol2) AE(s, i, j); + } +} + +/* ---- generic N-dimensional regular polytopes ---------------------- */ + +static void gen_hypercube(int d) { /* {4,3..} measure polytope */ + Solid *s = new_solid(d); + int N = 1 << d; + for (int m = 0; m < N; m++) { + double c[MAXD] = {0,0,0,0,0,0}; + for (int k = 0; k < d; k++) c[k] = (m >> k & 1) ? 1 : -1; + PV(s, c[0],c[1],c[2],c[3],c[4],c[5]); + } + center_normalize(s); derive_edges(s); +} +static void gen_orthoplex(int d) { /* cross-polytope */ + Solid *s = new_solid(d); + for (int k = 0; k < d; k++) { + double c[MAXD] = {0,0,0,0,0,0}; + c[k] = 1; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]); + c[k] = -1; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]); + } + center_normalize(s); derive_edges(s); +} +static void gen_simplex(int nv) { /* regular (nv-1)-simplex */ + Solid *s = new_solid(nv); /* basis e_i in R^nv */ + for (int i = 0; i < nv; i++) { + double c[MAXD] = {0,0,0,0,0,0}; + c[i] = 1; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]); + } + center_normalize(s); derive_edges(s); /* -> complete graph */ +} +static void gen_24cell(void) { /* perms of (+-1,+-1,0,0) */ + Solid *s = new_solid(4); + for (int a = 0; a < 4; a++) + for (int b = a+1; b < 4; b++) + for (int sa = -1; sa <= 1; sa += 2) + for (int sb = -1; sb <= 1; sb += 2) { + double c[MAXD] = {0,0,0,0,0,0}; + c[a]=sa; c[b]=sb; PV(s,c[0],c[1],c[2],c[3],c[4],c[5]); + } + center_normalize(s); derive_edges(s); +} + +/* ---- curated 3-D solids ------------------------------------------- */ + +static void gen_cuboctahedron(void) { /* perms of (+-1,+-1,0) */ + Solid *s = new_solid(3); + for (int a = 0; a < 3; a++) + for (int b = a+1; b < 3; b++) + for (int sa = -1; sa <= 1; sa += 2) + for (int sb = -1; sb <= 1; sb += 2) { + double c[3] = {0,0,0}; + c[a]=sa; c[b]=sb; PV(s,c[0],c[1],c[2],0,0,0); + } + center_normalize(s); derive_edges(s); +} +static void gen_truncated_octahedron(void) { /* perms of (0,+-1,+-2) */ + Solid *s = new_solid(3); + int perm[6][3] = {{0,1,2},{0,2,1},{1,0,2},{1,2,0},{2,0,1},{2,1,0}}; + for (int p = 0; p < 6; p++) + for (int s1 = -1; s1 <= 1; s1 += 2) + for (int s2 = -1; s2 <= 1; s2 += 2) { + double c[3] = {0,0,0}; + c[perm[p][1]] = s1 * 1.0; /* axis carrying value 1 */ + c[perm[p][2]] = s2 * 2.0; /* axis carrying value 2 */ + PV(s, c[0],c[1],c[2],0,0,0); + } + center_normalize(s); derive_edges(s); +} +static void gen_stella_octangula(void) { /* two interlocked tetrahedra */ + Solid *s = new_solid(3); + for (int m = 0; m < 8; m++) + PV(s, (m&1)?1:-1, (m&2)?1:-1, (m&4)?1:-1, 0,0,0); + center_normalize(s); + /* connect vertices whose minus-bit parity matches (each tetra) */ + for (int i = 0; i < 8; i++) + for (int j = i+1; j < 8; j++) { + int pi = __builtin_popcount(i & 7) & 1; + int pj = __builtin_popcount(j & 7) & 1; + if (pi == pj) AE(s, i, j); + } +} + +/* ---- parametric 3-D families -------------------------------------- */ + +static void gen_prism(int n) { + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 1, 0,0,0); } + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), -1, 0,0,0); } + for (int i = 0; i < n; i++) { + AE(s, i, (i+1)%n); + AE(s, n+i, n+(i+1)%n); + AE(s, i, n+i); + } + center_normalize(s); +} +static void gen_antiprism(int n) { + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 1, 0,0,0); } + for (int i = 0; i < n; i++) { double a = TAU*(i+0.5)/n; PV(s, cos(a), sin(a), -1, 0,0,0); } + for (int i = 0; i < n; i++) { + AE(s, i, (i+1)%n); + AE(s, n+i, n+(i+1)%n); + AE(s, i, n+i); + AE(s, i, n+(i+n-1)%n); + } + center_normalize(s); +} +static void gen_bipyramid(int n) { + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 0, 0,0,0); } + PV(s, 0,0, 1.25, 0,0,0); /* apex top = index n */ + PV(s, 0,0,-1.25, 0,0,0); /* apex bottom = index n+1 */ + for (int i = 0; i < n; i++) { + AE(s, i, (i+1)%n); + AE(s, n, i); + AE(s, n+1, i); + } + center_normalize(s); +} +static void gen_star(int n, int k) { /* star polygon {n/k} */ + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 0, 0,0,0); } + for (int i = 0; i < n; i++) AE(s, i, (i+k)%n); + center_normalize(s); +} +/* n-gonal trapezohedron (dual of an antiprism): two staggered rings wired + * in a zig-zag equator and capped by two apexes -> 2n kite faces. Used for + * the larger many-faced solids (24-, 30-, 60-hedra). */ +static void gen_trapezohedron(int n) { + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { double a = TAU*i/n; PV(s, cos(a), sin(a), 0.45, 0,0,0); } + for (int i = 0; i < n; i++) { double a = TAU*(i+0.5)/n; PV(s, cos(a), sin(a), -0.45, 0,0,0); } + int top = 2*n, bot = 2*n + 1; + PV(s, 0,0, 1.5, 0,0,0); /* top apex = 2n */ + PV(s, 0,0, -1.5, 0,0,0); /* bottom apex = 2n+1 */ + for (int i = 0; i < n; i++) { + AE(s, i, n + i); /* upper ring -> staggered lower ring */ + AE(s, n + i, (i + 1) % n); /* lower ring -> next upper-ring vertex */ + AE(s, top, i); /* top apex -> upper ring */ + AE(s, bot, n + i); /* bottom apex-> lower ring */ + } + center_normalize(s); +} +/* Unicursal hexagram: the six-pointed star drawn as a single closed stroke, + * the way the occult symbol actually is. Unlike the Star of David ({6/2}, two + * separate triangles), a regular hexagram cannot be traced in one pen-stroke, + * so this visits the six tips in the order 90 -> 210 -> 330 -> 270 -> 30 -> 150 + * degrees: the long 120-degree chords cross through the interior and weave the + * star, leaving the figure 2-fold symmetric like the genuine emblem. */ +static void gen_unicursal_hexagram(void) { + Solid *s = new_solid(3); + const int order[6] = { 90, 210, 330, 270, 30, 150 }; /* tip angles, path order */ + for (int i = 0; i < 6; i++) { + double a = order[i] * TAU / 360.0; + PV(s, cos(a), sin(a), 0, 0,0,0); + } + for (int i = 0; i < 6; i++) AE(s, i, (i + 1) % 6); /* one closed loop */ + center_normalize(s); +} + +/* ================================================================== */ +/* Symbols & signs: a small 2-D stroke builder that finalizes either */ +/* flat (depth 0) or extruded into a 3-D "wireframe prism" (depth > 0): */ +/* a front layer at +z, a back layer at -z, and rungs joining them. */ +/* ================================================================== */ + +typedef struct { + float px[MAX_VERTS], py[MAX_VERTS]; + int ea[MAX_EDGES], eb[MAX_EDGES]; + int np, ne; +} SymB; + +static void sym_init(SymB *b) { b->np = 0; b->ne = 0; } +static int sym_pt(SymB *b, double x, double y) { + if (b->np >= MAX_VERTS) return b->np - 1; + b->px[b->np] = (float)x; b->py[b->np] = (float)y; return b->np++; +} +static void sym_edge(SymB *b, int i, int j) { + if (b->ne >= MAX_EDGES || i < 0 || j < 0 || i == j) return; + b->ea[b->ne] = i; b->eb[b->ne] = j; b->ne++; +} +static void sym_line(SymB *b, double x0,double y0,double x1,double y1) { + sym_edge(b, sym_pt(b,x0,y0), sym_pt(b,x1,y1)); +} +static void sym_ring(SymB *b, double cx,double cy,double r,int seg) { /* closed circle */ + int first = -1, prev = -1; + for (int i = 0; i < seg; i++) { + double a = TAU * i / seg; + int idx = sym_pt(b, cx + r*cos(a), cy + r*sin(a)); + if (first < 0) first = idx; + sym_edge(b, prev, idx); + prev = idx; + } + sym_edge(b, prev, first); +} +static void sym_arc(SymB *b, double cx,double cy,double r,double a0,double a1,int seg) { + int prev = -1; /* open arc */ + for (int i = 0; i <= seg; i++) { + double a = a0 + (a1 - a0) * i / seg; + int idx = sym_pt(b, cx + r*cos(a), cy + r*sin(a)); + sym_edge(b, prev, idx); + prev = idx; + } +} +static void sym_poly(SymB *b, const double *xy, int n, int closed) { + int first = -1, prev = -1; + for (int i = 0; i < n; i++) { + int idx = sym_pt(b, xy[2*i], xy[2*i+1]); + if (first < 0) first = idx; + sym_edge(b, prev, idx); + prev = idx; + } + if (closed) sym_edge(b, prev, first); +} +static double sym_deg(double d) { return d * TAU / 360.0; } + +/* Turn the accumulated strokes into a Solid: flat if depth<=0, else extruded. */ +static void sym_finish(SymB *b, float depth) { + Solid *s = new_solid(3); + if (depth <= 0.0f) { + for (int k = 0; k < b->np; k++) PV(s, b->px[k], b->py[k], 0, 0,0,0); + for (int k = 0; k < b->ne; k++) AE(s, b->ea[k], b->eb[k]); + } else { + int n = b->np; + for (int k = 0; k < n; k++) PV(s, b->px[k], b->py[k], depth, 0,0,0); /* front */ + for (int k = 0; k < n; k++) PV(s, b->px[k], b->py[k], -depth, 0,0,0); /* back */ + for (int k = 0; k < b->ne; k++) { + AE(s, b->ea[k], b->eb[k]); + AE(s, n + b->ea[k], n + b->eb[k]); + } + for (int k = 0; k < n; k++) AE(s, k, n + k); /* rungs */ + } + center_normalize(s); +} + +/* ---- individual symbol strokes (drawn roughly within [-1,1]) ------- */ + +static void build_smiley(SymB *b) { + sym_ring(b, 0.0, 0.0, 1.0, 18); /* face */ + sym_ring(b, -0.35, 0.32, 0.13, 6); /* l eye */ + sym_ring(b, 0.35, 0.32, 0.13, 6); /* r eye */ + sym_arc (b, 0.0, 0.10, 0.55, sym_deg(200), sym_deg(340), 8); /* smile */ +} +static void build_biohazard(SymB *b) { + sym_ring(b, 0.0, 0.0, 0.24, 8); /* central circle */ + for (int k = 0; k < 3; k++) { /* three broken rings, gaps facing center */ + double th = sym_deg(90 + 120*k); + double cx = 0.58*cos(th), cy = 0.58*sin(th); + double g = sym_deg(90 + 120*k + 180); /* gap direction (toward center) */ + sym_arc(b, cx, cy, 0.44, g + sym_deg(38), g + sym_deg(322), 9); + } +} +static void build_peace(SymB *b) { + sym_ring(b, 0.0, 0.0, 1.0, 18); + int c = sym_pt(b, 0.0, 0.0); + const double ang[4] = {90, 270, 225, 315}; /* up, down, lower-left, lower-right */ + for (int i = 0; i < 4; i++) { + double a = sym_deg(ang[i]); + sym_edge(b, c, sym_pt(b, cos(a), sin(a))); + } +} +static void build_cross(SymB *b) { /* Latin cross outline */ + const double p[] = { + -0.22, 1.00, 0.22, 1.00, 0.22, 0.45, 0.60, 0.45, + 0.60, 0.00, 0.22, 0.00, 0.22,-1.00, -0.22,-1.00, + -0.22, 0.00, -0.60, 0.00, -0.60, 0.45, -0.22, 0.45 }; + sym_poly(b, p, 12, 1); +} +static void build_question(SymB *b) { + sym_arc(b, 0.0, 0.42, 0.42, sym_deg(200), sym_deg(-30), 10); /* top hook */ + int last = b->np - 1; + int t1 = sym_pt(b, 0.0, -0.05); sym_edge(b, last, t1); /* tail curls in */ + int t2 = sym_pt(b, 0.0, -0.35); sym_edge(b, t1, t2); + sym_ring(b, 0.0, -0.72, 0.12, 6); /* dot */ +} +static void build_exclaim(SymB *b) { + const double bar[] = { -0.12, 1.0, 0.12, 1.0, 0.06, -0.20, -0.06, -0.20 }; + sym_poly(b, bar, 4, 1); /* tapered stroke */ + sym_ring(b, 0.0, -0.62, 0.13, 6); /* dot */ +} +static void build_hash(SymB *b) { /* # — two verticals, two horizontals */ + sym_line(b, -0.32, 0.85, -0.20, -0.85); + sym_line(b, 0.20, 0.85, 0.32, -0.85); + sym_line(b, -0.85, 0.32, 0.85, 0.32); + sym_line(b, -0.85, -0.32, 0.85, -0.32); +} +static void build_dollar(SymB *b) { /* $ — S-curve with a vertical bar through it */ + const double S[] = { + 0.80, 0.64, 0.40, 0.96, -0.40, 0.96, -0.80, 0.64, + -0.40, 0.00, 0.40, 0.00, 0.80,-0.32, 0.40,-0.96, + -0.40,-0.96, -0.80,-0.64 }; + sym_poly(b, S, 10, 0); + sym_line(b, 0.0, 1.18, 0.0, -1.18); +} +static void build_sterling(SymB *b) { /* £ — hooked top, stem, crossbar, base */ + sym_arc(b, 0.05, 0.45, 0.42, sym_deg(8), sym_deg(200), 9); + int last = b->np - 1; + int s1 = sym_pt(b, -0.33, -0.58); sym_edge(b, last, s1); /* stem down */ + int base0 = sym_pt(b, -0.52, -0.62); + int base1 = sym_pt(b, 0.52, -0.62); + sym_edge(b, s1, base0); sym_edge(b, base0, base1); /* base */ + sym_line(b, -0.33, -0.05, 0.42, -0.05); /* crossbar */ +} + +/* ---- googie / atomic-age starbursts -------------------------------- */ + +/* Flat multi-point sparkle (the bowling-alley twinkle): sharp tips at radius + * r1 (or alternating r1/r2) joined through deep notches at radius `inner`. */ +static void build_sparkle(SymB *b, int tips, double r1, double r2, double inner) { + int first = -1, prev = -1; + for (int i = 0; i < tips; i++) { + double ta = TAU*0.25 + TAU*i/tips; /* first tip points up */ + double na = ta + TAU/(2*tips); + double tr = (r2 > 0 && (i & 1)) ? r2 : r1; + int t = sym_pt(b, tr*cos(ta), tr*sin(ta)); + if (first < 0) first = t; + sym_edge(b, prev, t); prev = t; + int n = sym_pt(b, inner*cos(na), inner*sin(na)); + sym_edge(b, prev, n); prev = n; + } + sym_edge(b, prev, first); +} +/* Flat atomic burst: spokes of alternating length from a hub, with little + * "electron" rings capping the long ones. */ +static void build_atomic(SymB *b) { + int c = sym_pt(b, 0.0, 0.0); + int n = 12; + for (int i = 0; i < n; i++) { + double a = TAU*i/n; + double r = (i & 1) ? 0.55 : 1.0; + sym_edge(b, c, sym_pt(b, r*cos(a), r*sin(a))); + if (!(i & 1)) sym_ring(b, cos(a), sin(a), 0.08, 5); + } +} +/* Flat sunburst: a dense fan of alternating-length rays from a hub. */ +static void build_sunburst(SymB *b) { + int c = sym_pt(b, 0.0, 0.0); + int n = 20; + for (int i = 0; i < n; i++) { + double a = TAU*i/n; + double r = (i & 1) ? 0.78 : 1.0; + sym_edge(b, c, sym_pt(b, r*cos(a), r*sin(a))); + } +} +/* True 3-D Sputnik: rods to the 12 icosahedral directions, each tipped with a + * small ring "ball" lying perpendicular to the rod. */ +static void gen_sputnik(void) { + Solid *s = new_solid(3); + const double P = 1.6180339887; + const double dir[12][3] = { + {0,1,P},{0,1,-P},{0,-1,P},{0,-1,-P},{1,P,0},{1,-P,0}, + {-1,P,0},{-1,-P,0},{P,0,1},{P,0,-1},{-P,0,1},{-P,0,-1} }; + int c = s->nv; PV(s, 0,0,0, 0,0,0); + for (int i = 0; i < 12; i++) { + double L = sqrt(dir[i][0]*dir[i][0] + dir[i][1]*dir[i][1] + dir[i][2]*dir[i][2]); + double ux = dir[i][0]/L, uy = dir[i][1]/L, uz = dir[i][2]/L; + int tip = s->nv; PV(s, ux, uy, uz, 0,0,0); + AE(s, c, tip); + double rx = uy, ry = -ux, rz = 0, rl = sqrt(rx*rx + ry*ry + rz*rz); + if (rl < 1e-6) { rx = 1; ry = 0; rz = 0; rl = 1; } + rx /= rl; ry /= rl; rz /= rl; + double sx = uy*rz - uz*ry, sy = uz*rx - ux*rz, sz = ux*ry - uy*rx; /* u x r */ + int ring0 = -1, prev = -1; double rr = 0.14; + for (int k = 0; k < 5; k++) { + double a = TAU*k/5; + double bx = ux + rr*(cos(a)*rx + sin(a)*sx); + double by = uy + rr*(cos(a)*ry + sin(a)*sy); + double bz = uz + rr*(cos(a)*rz + sin(a)*sz); + int idx = s->nv; PV(s, bx, by, bz, 0,0,0); + if (ring0 < 0) ring0 = idx; + if (prev >= 0) AE(s, prev, idx); + prev = idx; + } + AE(s, prev, ring0); + } + center_normalize(s); +} + +/* ---- more googie shapes: flat builders + naturally-3-D generators --- */ + +static void build_sparkle4(SymB *b) { build_sparkle(b, 4, 1.0, 0.0, 0.13); } +static void build_sparkle8(SymB *b) { build_sparkle(b, 8, 1.0, 0.62, 0.12); } + +static void sym_ellipse(SymB *b, double cx,double cy,double ra,double rb,double rot,int seg) { + int first = -1, prev = -1; + for (int i = 0; i < seg; i++) { + double a = TAU*i/seg, ex = ra*cos(a), ey = rb*sin(a); + int idx = sym_pt(b, cx + ex*cos(rot) - ey*sin(rot), + cy + ex*sin(rot) + ey*cos(rot)); + if (first < 0) first = idx; + sym_edge(b, prev, idx); prev = idx; + } + sym_edge(b, prev, first); +} +static void build_atom2d(SymB *b) { /* flat atom: 3 ellipses + nucleus */ + sym_ring(b, 0, 0, 0.16, 7); + sym_ellipse(b, 0, 0, 1.0, 0.34, sym_deg(0), 16); + sym_ellipse(b, 0, 0, 1.0, 0.34, sym_deg(60), 16); + sym_ellipse(b, 0, 0, 1.0, 0.34, sym_deg(120), 16); +} +static void build_boomerang(SymB *b) { /* crescent / boomerang band */ + double Ro = 1.0, Ri = 0.6, a0 = sym_deg(30), a1 = sym_deg(150); int seg = 8; + int first = -1, prev = -1; + for (int i = 0; i <= seg; i++) { double a = a0 + (a1-a0)*i/seg; + int idx = sym_pt(b, Ro*cos(a), Ro*sin(a)); if (first<0) first=idx; + sym_edge(b, prev, idx); prev = idx; } + for (int i = 0; i <= seg; i++) { double a = a1 + (a0-a1)*i/seg; + int idx = sym_pt(b, Ri*cos(a), Ri*sin(a)); sym_edge(b, prev, idx); prev = idx; } + sym_edge(b, prev, first); +} +static void build_amoeba(SymB *b) { /* googie kidney / amoeba blob */ + int seg = 18, first = -1, prev = -1; + for (int i = 0; i < seg; i++) { + double a = TAU*i/seg; + double r = 0.72 + 0.26*sin(3*a + 0.6) + 0.12*sin(2*a - 0.4); + int idx = sym_pt(b, r*cos(a), 0.82*r*sin(a)); + if (first < 0) first = idx; + sym_edge(b, prev, idx); prev = idx; + } + sym_edge(b, prev, first); +} +static void build_doublestar(SymB *b) { /* big 4-point twinkle + inner one at 45 */ + build_sparkle(b, 4, 1.0, 0.0, 0.12); + int first = -1, prev = -1, tips = 4; double R = 0.5, inner = 0.07, off = TAU/8; + for (int i = 0; i < tips; i++) { + double ta = TAU*0.25 + off + TAU*i/tips, na = ta + TAU/(2*tips); + int t = sym_pt(b, R*cos(ta), R*sin(ta)); if (first<0) first=t; + sym_edge(b, prev, t); prev = t; + int n = sym_pt(b, inner*cos(na), inner*sin(na)); sym_edge(b, prev, n); prev = n; + } + sym_edge(b, prev, first); +} +static void build_orbit2d(SymB *b) { /* concentric orbit rings + ticks */ + sym_ring(b, 0, 0, 1.00, 18); + sym_ring(b, 0, 0, 0.64, 14); + sym_ring(b, 0, 0, 0.30, 9); + for (int i = 0; i < 4; i++) { double a = sym_deg(45 + 90*i); + sym_line(b, 0.30*cos(a), 0.30*sin(a), cos(a), sin(a)); } +} +static void build_starorb2d(SymB *b) { /* dense ray star with a core ring */ + int c = sym_pt(b, 0, 0), n = 24; + for (int i = 0; i < n; i++) { double a = TAU*i/n; + sym_edge(b, c, sym_pt(b, cos(a), sin(a))); } + sym_ring(b, 0, 0, 0.30, 10); +} + +/* 3-D atom: three circular orbits in tilted planes around an octahedral core. */ +static void gen_atom3d(void) { + Solid *s = new_solid(3); int seg = 20; + const double tilt[3] = {0, 60, 120}; + for (int o = 0; o < 3; o++) { + double t = sym_deg(tilt[o]); int first = -1, prev = -1; + for (int i = 0; i < seg; i++) { + double a = TAU*i/seg, x = cos(a), y = sin(a), z = 0; + double y2 = y*cos(t) - z*sin(t), z2 = y*sin(t) + z*cos(t); + int idx = s->nv; PV(s, x, y2, z2, 0,0,0); + if (first < 0) first = idx; + if (prev >= 0) AE(s, prev, idx); + prev = idx; + } + AE(s, prev, first); + } + int b0 = s->nv; double r = 0.13; + PV(s, r,0,0, 0,0,0); PV(s,-r,0,0, 0,0,0); PV(s, 0,r,0, 0,0,0); + PV(s, 0,-r,0, 0,0,0); PV(s, 0,0,r, 0,0,0); PV(s, 0,0,-r, 0,0,0); + const int oe[12][2] = {{0,2},{0,3},{0,4},{0,5},{1,2},{1,3},{1,4},{1,5},{2,4},{2,5},{3,4},{3,5}}; + for (int e = 0; e < 12; e++) AE(s, b0+oe[e][0], b0+oe[e][1]); + center_normalize(s); +} +/* 3-D gyroscope: three great circles in the coordinate planes. */ +static void gen_gyroscope3d(void) { + Solid *s = new_solid(3); int seg = 22; + for (int p = 0; p < 3; p++) { + int first = -1, prev = -1; + for (int i = 0; i < seg; i++) { + double a = TAU*i/seg, c = cos(a), sn = sin(a), x, y, z; + if (p == 0) { x = c; y = sn; z = 0; } + else if (p == 1) { x = c; y = 0; z = sn; } + else { x = 0; y = c; z = sn; } + int idx = s->nv; PV(s, x, y, z, 0,0,0); + if (first < 0) first = idx; + if (prev >= 0) AE(s, prev, idx); + prev = idx; + } + AE(s, prev, first); + } + center_normalize(s); +} +/* 3-D spike orb / sea-urchin: rods to 20 symmetric directions from a hub. */ +static void gen_spikeorb3d(void) { + Solid *s = new_solid(3); + const double P = 1.6180339887; + const double d[20][3] = { + {0,1,P},{0,1,-P},{0,-1,P},{0,-1,-P},{1,P,0},{1,-P,0},{-1,P,0},{-1,-P,0}, + {P,0,1},{P,0,-1},{-P,0,1},{-P,0,-1}, + {1,1,1},{1,1,-1},{1,-1,1},{1,-1,-1},{-1,1,1},{-1,1,-1},{-1,-1,1},{-1,-1,-1} }; + int c = s->nv; PV(s, 0,0,0, 0,0,0); + for (int i = 0; i < 20; i++) { + double L = sqrt(d[i][0]*d[i][0] + d[i][1]*d[i][1] + d[i][2]*d[i][2]); + int tip = s->nv; PV(s, d[i][0]/L, d[i][1]/L, d[i][2]/L, 0,0,0); + AE(s, c, tip); + } + center_normalize(s); +} + +/* Build a symbol in both its flat and extruded-3-D forms. */ +static void add_symbol(void (*build)(SymB *)) { + SymB b; + sym_init(&b); build(&b); sym_finish(&b, 0.0f); /* 2-D */ + sym_init(&b); build(&b); sym_finish(&b, 0.5f); /* 3-D */ +} + +/* Random lumpy 3-D asteroid: n points scattered over a sphere at jittered + * radii, wired up as the convex hull (brute-force face enumeration). The + * vertex count `n` sets the number of sides / overall complexity, and the + * radial jitter gives each rock its irregular, faceted silhouette. */ +static void gen_asteroid(int n) { + if (n > MAX_VERTS) n = MAX_VERTS; + if (n < 6) n = 6; + Solid *s = new_solid(3); + for (int i = 0; i < n; i++) { + double z = 2.0*frand() - 1.0; /* uniform on the sphere */ + double t = TAU*frand(); + double rr = sqrt(1.0 - z*z); + double rad = 0.74 + 0.26*frand(); /* lumpy radius */ + PV(s, rr*cos(t)*rad, rr*sin(t)*rad, z*rad, 0,0,0); + } + /* a triangle (a,b,c) is a hull face iff every other vertex lies on one + * side of its plane; collect the edges of all such faces. */ + for (int a = 0; a < n; a++) + for (int b = a+1; b < n; b++) + for (int c = b+1; c < n; c++) { + double ux = s->v[b][0]-s->v[a][0], uy = s->v[b][1]-s->v[a][1], uz = s->v[b][2]-s->v[a][2]; + double vx = s->v[c][0]-s->v[a][0], vy = s->v[c][1]-s->v[a][1], vz = s->v[c][2]-s->v[a][2]; + double nx = uy*vz - uz*vy, ny = uz*vx - ux*vz, nz = ux*vy - uy*vx; + double nl = sqrt(nx*nx + ny*ny + nz*nz); + if (nl < 1e-9) continue; /* degenerate triple */ + int pos = 0, neg = 0; + for (int p = 0; p < n; p++) { + if (p == a || p == b || p == c) continue; + double dx = s->v[p][0]-s->v[a][0], dy = s->v[p][1]-s->v[a][1], dz = s->v[p][2]-s->v[a][2]; + double d = (nx*dx + ny*dy + nz*dz) / nl; + if (d > 1e-5) pos++; else if (d < -1e-5) neg++; + } + if (pos == 0 || neg == 0) { + AE_unique(s, a, b); AE_unique(s, b, c); AE_unique(s, c, a); + } + } + center_normalize(s); +} + +static void init_solids(void) { + const double P = 1.6180339887, I = 1.0/1.6180339887; + Solid *s; + + /* --- the five Platonic solids --- */ + s = new_solid(3); + PV(s,1,1,1,0,0,0); PV(s,1,-1,-1,0,0,0); PV(s,-1,1,-1,0,0,0); PV(s,-1,-1,1,0,0,0); + center_normalize(s); derive_edges(s); /* tetrahedron */ + gen_hypercube(3); /* cube */ + gen_orthoplex(3); /* octahedron */ + s = new_solid(3); /* icosahedron */ + PV(s,0,1,P,0,0,0); PV(s,0,1,-P,0,0,0); PV(s,0,-1,P,0,0,0); PV(s,0,-1,-P,0,0,0); + PV(s,1,P,0,0,0,0); PV(s,1,-P,0,0,0,0); PV(s,-1,P,0,0,0,0); PV(s,-1,-P,0,0,0,0); + PV(s,P,0,1,0,0,0); PV(s,P,0,-1,0,0,0); PV(s,-P,0,1,0,0,0); PV(s,-P,0,-1,0,0,0); + center_normalize(s); derive_edges(s); + s = new_solid(3); /* dodecahedron */ + PV(s,1,1,1,0,0,0); PV(s,1,1,-1,0,0,0); PV(s,1,-1,1,0,0,0); PV(s,1,-1,-1,0,0,0); + PV(s,-1,1,1,0,0,0); PV(s,-1,1,-1,0,0,0); PV(s,-1,-1,1,0,0,0); PV(s,-1,-1,-1,0,0,0); + PV(s,0,I,P,0,0,0); PV(s,0,I,-P,0,0,0); PV(s,0,-I,P,0,0,0); PV(s,0,-I,-P,0,0,0); + PV(s,I,P,0,0,0,0); PV(s,I,-P,0,0,0,0); PV(s,-I,P,0,0,0,0); PV(s,-I,-P,0,0,0,0); + PV(s,P,0,I,0,0,0); PV(s,P,0,-I,0,0,0); PV(s,-P,0,I,0,0,0); PV(s,-P,0,-I,0,0,0); + center_normalize(s); derive_edges(s); + + /* --- other 3-D solids --- */ + gen_cuboctahedron(); + gen_truncated_octahedron(); + gen_stella_octangula(); + + /* --- prisms, antiprisms, bipyramids --- */ + gen_prism(3); gen_prism(5); gen_prism(6); gen_prism(7); gen_prism(8); + gen_antiprism(3); gen_antiprism(4); gen_antiprism(5); gen_antiprism(6); gen_antiprism(8); + gen_bipyramid(3); gen_bipyramid(4); gen_bipyramid(5); gen_bipyramid(6); gen_bipyramid(8); + + /* --- star polygons --- */ + gen_star(5,2); gen_star(6,2); gen_star(7,2); gen_star(7,3); gen_star(8,3); + gen_star(9,2); gen_star(9,4); gen_star(12,5); + + /* --- many-faced solids (named for their face count) + a hexagram --- + * Built from prism / bipyramid / trapezohedron families so each lands on + * exactly the requested number of faces. (The 20-face icosahedron here is + * a decagonal bipyramid, distinct from the platonic icosahedron above.) */ + gen_prism(11); /* tridecahedron (13 faces) */ + gen_bipyramid(7); /* tetradecahedron (14 faces) */ + gen_prism(13); /* pentadecahedron (15 faces) */ + gen_prism(15); /* heptadecahedron (17 faces) */ + gen_bipyramid(9); /* octadecahedron (18 faces) */ + gen_prism(17); /* enneadecahedron (19 faces) */ + gen_bipyramid(10); /* icosahedron (20 faces) */ + gen_trapezohedron(12); /* icositetrahedron (24 faces) */ + gen_trapezohedron(15); /* triacontahedron (30 faces) */ + gen_trapezohedron(30); /* hexacontahedron (60 faces) */ + gen_bipyramid(50); /* hecatohedron (100 faces) */ + gen_unicursal_hexagram(); /* unicursal hexagram */ + + /* --- googie / atomic-age starbursts, each in 2-D and extruded 3-D --- */ + add_symbol(build_sparkle4); /* 4-point bowling-alley twinkle */ + add_symbol(build_sparkle8); /* 8-point twinkle */ + add_symbol(build_atomic); /* atomic burst with electrons */ + add_symbol(build_sunburst); /* ray sunburst */ + add_symbol(build_doublestar); /* layered double starburst */ + add_symbol(build_boomerang); /* boomerang / crescent */ + add_symbol(build_amoeba); /* kidney / amoeba blob */ + /* flat motifs paired with a naturally-3-D counterpart below */ + { SymB b; sym_init(&b); build_atom2d(&b); sym_finish(&b, 0.0f); } /* flat atom */ + { SymB b; sym_init(&b); build_orbit2d(&b); sym_finish(&b, 0.0f); } /* orbit rings */ + { SymB b; sym_init(&b); build_starorb2d(&b); sym_finish(&b, 0.0f); } /* dense star */ + gen_sputnik(); /* 3-D sputnik satellite */ + gen_atom3d(); /* 3-D orbital atom */ + gen_gyroscope3d(); /* 3-D gyroscope / orbit cage */ + gen_spikeorb3d(); /* 3-D spike orb / sea-urchin */ + + /* --- symbols & signs, each in a flat (2-D) and an extruded (3-D) form --- */ + add_symbol(build_smiley); /* smiley face */ + add_symbol(build_biohazard); /* biohazard */ + add_symbol(build_peace); /* peace sign */ + add_symbol(build_cross); /* cross */ + add_symbol(build_question); /* question mark */ + add_symbol(build_exclaim); /* exclamation point */ + add_symbol(build_hash); /* hash / pound (#) */ + add_symbol(build_dollar); /* dollar sign */ + add_symbol(build_sterling); /* pound sterling (£) */ + + /* --- 4/5/6-dimensional polytopes (projected & morphing) --- */ + gen_simplex(5); /* 5-cell (4-simplex) */ + gen_hypercube(4); /* tesseract / 8-cell */ + gen_hypercube(5); /* penteract / 5-cube */ + gen_hypercube(6); /* 6-cube */ + gen_orthoplex(4); /* 16-cell (4-orthoplex) */ + gen_orthoplex(5); /* 5-orthoplex */ + gen_orthoplex(6); /* 6-orthoplex */ + gen_24cell(); /* 24-cell */ + gen_simplex(6); /* 5-simplex */ + + /* --- random vector asteroids (each session generates a fresh set) --- */ + for (int k = 0; k < 18; k++) gen_asteroid(7 + (rand() % 20)); /* 7..26 verts */ +} + +/* ================================================================== */ +/* The field of tumbling solids. */ +/* ================================================================== */ + +#define MAX_BODIES 7200 +#define RENDER_REF 140.0f /* render distance at which density == body count */ +/* The field is a sphere around the camera whose radius is the user-settable + * render distance (cfg.render_dist). Bodies stream along -Z (forward) or +Z + * (when speed is negative) and, once they leave the sphere, are recycled back + * to the render-distance shell on the incoming side -- so objects always appear + * far away and approach, never popping in close. The active body count scales + * with render distance, so a deeper field simply holds proportionally more + * shapes. The camera can rotate a full 360 degrees around it. */ + +typedef struct { + int shape; + float x, y, z; /* world position; camera at origin, looking -Z */ + float size; /* == bounding-sphere radius (unit solid scaled) */ + float axis[3]; /* tumble axis (normalized) */ + float spin_seed; /* in [-1,1]; tumble spread applied live */ + float angle; /* current tumble angle (radians) */ + float hue_offset; /* used in multicolor mode */ + float spawn_fade; /* 0->1 ease-in after (re)spawn, kills pop-in */ +} Body; + +static Body bodies[MAX_BODIES]; + +/* ================================================================== */ +/* Settings (driven by keyboard). */ +/* ================================================================== */ + +typedef struct { + float speed; /* approach speed 0..100 */ + float tumble; /* tumble rate 0..100 */ + float tumble_var; /* tumble variance 0..100 */ + float render_dist; /* field sphere radius 40..1520 */ + int density; /* active body count */ + float size_min; /* min solid size */ + float size_max; /* max solid size */ + float hue; /* base hue 0..360 */ + float hue_cycle; /* auto hue-cycle rate 0..100 (0 = off) */ + int multicolor; /* 0 = single, 1 = multi */ + int cycle_shapes; /* 0 = random, 1 = cycle */ + float glow; /* CRT glow / bleed 0..100 */ + float flicker; /* vector flicker 0..100 */ + int fullscreen; + int paused; +} Settings; + +static Settings cfg = { + .speed = 35, .tumble = 40, .tumble_var = 50, .render_dist = 140, .density = 50, + .size_min = 1.4f, .size_max = 4.6f, .hue = 200, .hue_cycle = 0, + .multicolor = 0, .cycle_shapes = 0, + .glow = 45, .flicker = 18, .fullscreen = 0, .paused = 0 +}; + +static int spawn_counter = 0; +static float max_line_width = 10.0f; /* queried at runtime */ +static float cam_x = 0.0f, cam_y = 0.0f; /* camera pan (WASD) */ +static float cam_yaw = 0.0f, cam_pitch = 0.0f; /* camera rotate (arrow keys) */ + +static float frand(void) { return (float)rand() / (float)RAND_MAX; } + +/* Bounding-sphere overlap test against the first `check_count` bodies. */ +static int collides(const Body *c, int check_count, int self) { + for (int i = 0; i < check_count; i++) { + if (i == self) continue; + const Body *o = &bodies[i]; + float dx = c->x - o->x, dy = c->y - o->y, dz = c->z - o->z; + float rr = c->size + o->size + 0.6f; /* +margin so they never touch */ + if (dx*dx + dy*dy + dz*dz < rr*rr) return 1; + } + return 0; +} + +/* (Re)initialize one body. reset=0 (recycle) re-enters it ONLY at the render- + * distance shell, on the incoming side (which flips with the travel direction), + * so objects always appear far away and approach -- never popping in close. + * reset=1 (initial fill) spreads bodies through the volume instead, as if they + * had each spawned at the shell at a different past time, so the field starts + * populated rather than blank. Position is rejection-sampled to avoid overlap. */ +static void spawn_body(Body *b, int reset, int check_count, int self) { + b->shape = cfg.cycle_shapes ? (spawn_counter++ % num_shapes) + : (rand() % num_shapes); + + float smin = cfg.size_min, smax = cfg.size_max; + if (smax < smin) smax = smin; + b->size = smin + frand() * (smax - smin); + + float ax = frand()*2-1, ay = frand()*2-1, az = frand()*2-1; + float len = sqrtf(ax*ax + ay*ay + az*az); + if (len < 1e-4f) { ax = 1; ay = 0; az = 0; len = 1; } + b->axis[0] = ax/len; b->axis[1] = ay/len; b->axis[2] = az/len; + + b->spin_seed = frand()*2 - 1; + b->angle = frand() * 6.2831853f; + b->hue_offset = frand() * 360.0f; + b->spawn_fade = reset ? 1.0f : 0.0f; + + float R = cfg.render_dist; + float side = (cfg.speed >= 0.0f) ? 1.0f : -1.0f; /* +Z if flying forward */ + for (int t = 0; t < 80; t++) { + if (reset) { + /* initial fill: uniform through the volume of the sphere */ + float zc = 2.0f*frand() - 1.0f; + float th = 6.2831853f*frand(); + float rr = sqrtf(1.0f - zc*zc); + float rad = R * cbrtf(frand()); + b->x = rr*cosf(th)*rad; + b->y = rr*sinf(th)*rad; + b->z = zc*rad; + } else { + /* recycle: enter at the shell, on the incoming-side cap */ + float rho = 0.9f * R * sqrtf(frand()); + float phi = 6.2831853f*frand(); + b->x = rho*cosf(phi); + b->y = rho*sinf(phi); + b->z = side * sqrtf(R*R - rho*rho) * 0.999f; + } + if (!collides(b, check_count, self)) break; + } +} + +/* How many bodies are active right now: the density knob, scaled up with render + * distance so a larger sphere holds proportionally more shapes (constant near- + * field density), clamped to the hard array limit. */ +static int active_count(void) { + int n = (int)(cfg.density * (cfg.render_dist / RENDER_REF) + 0.5f); + if (n < 1) n = 1; + if (n > MAX_BODIES) n = MAX_BODIES; + return n; +} + +/* Make sure bodies[0..n) are initialized (spawns any not yet filled). */ +static int g_filled = 0; +static void ensure_filled(int n) { + if (n > MAX_BODIES) n = MAX_BODIES; + for (int i = g_filled; i < n; i++) + spawn_body(&bodies[i], 1, i, i); + if (n > g_filled) g_filled = n; +} + +static void rebuild_field(void) { + if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES; + if (cfg.density < 1) cfg.density = 1; + ensure_filled(active_count()); +} + +/* ================================================================== */ +/* Color. */ +/* ================================================================== */ + +static void hsv_to_rgb(float h, float s, float v, float *r, float *g, float *b) { + h = fmodf(h, 360.0f); if (h < 0) h += 360.0f; + float c = v * s; + float x = c * (1 - fabsf(fmodf(h / 60.0f, 2) - 1)); + float m = v - c, rr, gg, bb; + if (h < 60) { rr=c; gg=x; bb=0; } + else if (h < 120) { rr=x; gg=c; bb=0; } + else if (h < 180) { rr=0; gg=c; bb=x; } + else if (h < 240) { rr=0; gg=x; bb=c; } + else if (h < 300) { rr=x; gg=0; bb=c; } + else { rr=c; gg=0; bb=x; } + *r = rr + m; *g = gg + m; *b = bb + m; +} + +/* ================================================================== */ +/* Self-contained vector stroke font (for the on-screen display). */ +/* Each glyph is a polyline list on a 0..4 (x) by 0..6 (y) grid. */ +/* PU lifts the pen (start a new stroke); EN ends the glyph. */ +/* ================================================================== */ + +#define PU 127 +#define EN (-128) +#define G_ static const signed char + +G_ g_A[] = {0,0, 2,6, 4,0, PU, 1,2, 3,2, EN}; +G_ g_B[] = {0,0, 0,6, 3,6, 4,5, 3,3, 0,3, PU, 3,3, 4,1, 3,0, 0,0, EN}; +G_ g_C[] = {4,5, 3,6, 1,6, 0,5, 0,1, 1,0, 3,0, 4,1, EN}; +G_ g_D[] = {0,0, 0,6, 2,6, 4,4, 4,2, 2,0, 0,0, EN}; +G_ g_E[] = {4,6, 0,6, 0,0, 4,0, PU, 0,3, 3,3, EN}; +G_ g_F[] = {4,6, 0,6, 0,0, PU, 0,3, 3,3, EN}; +G_ g_G[] = {4,5, 3,6, 1,6, 0,5, 0,1, 1,0, 3,0, 4,1, 4,3, 2,3, EN}; +G_ g_H[] = {0,0, 0,6, PU, 4,0, 4,6, PU, 0,3, 4,3, EN}; +G_ g_I[] = {1,0, 3,0, PU, 2,0, 2,6, PU, 1,6, 3,6, EN}; +G_ g_J[] = {3,6, 3,1, 2,0, 1,0, 0,1, EN}; +G_ g_K[] = {0,0, 0,6, PU, 4,6, 0,3, 4,0, EN}; +G_ g_L[] = {0,6, 0,0, 4,0, EN}; +G_ g_M[] = {0,0, 0,6, 2,3, 4,6, 4,0, EN}; +G_ g_N[] = {0,0, 0,6, 4,0, 4,6, EN}; +G_ g_O[] = {1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,1, 1,0, EN}; +G_ g_P[] = {0,0, 0,6, 3,6, 4,5, 4,4, 3,3, 0,3, EN}; +G_ g_Q[] = {1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,1, 1,0, PU, 2,2, 4,0, EN}; +G_ g_R[] = {0,0, 0,6, 3,6, 4,5, 4,4, 3,3, 0,3, PU, 2,3, 4,0, EN}; +G_ g_S[] = {4,5, 3,6, 1,6, 0,5, 1,3, 3,3, 4,2, 3,0, 1,0, 0,1, EN}; +G_ g_T[] = {0,6, 4,6, PU, 2,6, 2,0, EN}; +G_ g_U[] = {0,6, 0,1, 1,0, 3,0, 4,1, 4,6, EN}; +G_ g_V[] = {0,6, 2,0, 4,6, EN}; +G_ g_W[] = {0,6, 1,0, 2,3, 3,0, 4,6, EN}; +G_ g_X[] = {0,0, 4,6, PU, 0,6, 4,0, EN}; +G_ g_Y[] = {0,6, 2,3, 4,6, PU, 2,3, 2,0, EN}; +G_ g_Z[] = {0,6, 4,6, 0,0, 4,0, EN}; + +G_ g_0[] = {1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,1, 1,0, PU, 1,1, 3,5, EN}; +G_ g_1[] = {1,5, 2,6, 2,0, PU, 1,0, 3,0, EN}; +G_ g_2[] = {0,5, 1,6, 3,6, 4,5, 4,4, 0,0, 4,0, EN}; +G_ g_3[] = {0,6, 4,6, 2,3, PU, 2,3, 4,2, 4,1, 3,0, 1,0, 0,1, EN}; +G_ g_4[] = {3,0, 3,6, 0,2, 4,2, EN}; +G_ g_5[] = {4,6, 0,6, 0,3, 3,3, 4,2, 4,1, 3,0, 1,0, 0,1, EN}; +G_ g_6[] = {4,5, 3,6, 1,6, 0,5, 0,1, 1,0, 3,0, 4,1, 4,2, 3,3, 0,3, EN}; +G_ g_7[] = {0,6, 4,6, 2,0, EN}; +G_ g_8[] = {1,3, 0,4, 0,5, 1,6, 3,6, 4,5, 4,4, 3,3, 1,3, 0,2, 0,1, 1,0, 3,0, 4,1, 4,2, 3,3, EN}; +G_ g_9[] = {0,1, 1,0, 3,0, 4,1, 4,5, 3,6, 1,6, 0,5, 0,4, 1,3, 4,3, EN}; + +G_ g_pct[] = {0,0, 4,6, PU, 0,5,1,5,1,6,0,6,0,5, PU, 3,0,4,0,4,1,3,1,3,0, EN}; +G_ g_slash[] = {0,0, 4,6, EN}; +G_ g_plus[] = {2,1, 2,5, PU, 0,3, 4,3, EN}; +G_ g_minus[] = {0,3, 4,3, EN}; +G_ g_dot[] = {2,0, 2,1, EN}; +G_ g_lbr[] = {3,6, 1,6, 1,0, 3,0, EN}; +G_ g_rbr[] = {1,6, 3,6, 3,0, 1,0, EN}; +G_ g_lpar[] = {3,6, 1,4, 1,2, 3,0, EN}; +G_ g_rpar[] = {1,6, 3,4, 3,2, 1,0, EN}; +G_ g_dash[] = {0,3, 4,3, EN}; /* same as minus, used by '/'-fallback */ + +static const signed char *glyph(char c) { + switch (toupper((unsigned char)c)) { + case 'A': return g_A; case 'B': return g_B; case 'C': return g_C; + case 'D': return g_D; case 'E': return g_E; case 'F': return g_F; + case 'G': return g_G; case 'H': return g_H; case 'I': return g_I; + case 'J': return g_J; case 'K': return g_K; case 'L': return g_L; + case 'M': return g_M; case 'N': return g_N; case 'O': return g_O; + case 'P': return g_P; case 'Q': return g_Q; case 'R': return g_R; + case 'S': return g_S; case 'T': return g_T; case 'U': return g_U; + case 'V': return g_V; case 'W': return g_W; case 'X': return g_X; + case 'Y': return g_Y; case 'Z': return g_Z; + case '0': return g_0; case '1': return g_1; case '2': return g_2; + case '3': return g_3; case '4': return g_4; case '5': return g_5; + case '6': return g_6; case '7': return g_7; case '8': return g_8; + case '9': return g_9; + case '%': return g_pct; case '/': return g_slash; + case '+': return g_plus; case '-': return g_minus; + case '.': return g_dot; case '[': return g_lbr; + case ']': return g_rbr; case '(': return g_lpar; + case ')': return g_rpar; case '_': return g_dash; + default: return NULL; /* space and unknowns advance only */ + } +} + +/* Draw a string with its bottom-left at (x,y); glyph height = h px. */ +static float stroke_text(const char *str, float x, float y, float h) { + float sx = h / 6.0f, sy = h / 6.0f; + float advance = sx * 6.0f; /* 4 wide + 2 spacing */ + float cx = x; + for (const char *p = str; *p; p++) { + const signed char *g = glyph(*p); + if (g) { + glBegin(GL_LINE_STRIP); + for (int i = 0;;) { + signed char a = g[i++]; + if (a == EN) break; + if (a == PU) { glEnd(); glBegin(GL_LINE_STRIP); continue; } + signed char b = g[i++]; + glVertex2f(cx + a * sx, y + b * sy); + } + glEnd(); + } + cx += advance; + } + return cx; +} + +/* ================================================================== */ +/* On-screen display. */ +/* ================================================================== */ + +static void render_osd(int fbw, int fbh, float alpha) { + if (alpha <= 0.001f) return; + + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + gluOrtho2D(0, fbw, 0, fbh); + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + + const float h = 13.0f; + const float left = 22.0f; + const float lineStep = h + 9.0f; + const int nlines = 19; + const float panelW = 384.0f; + float panelTop = fbh - 18.0f; + float panelH = nlines * lineStep + 16.0f; + + /* dim backdrop for readability (normal alpha blend) */ + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glColor4f(0.02f, 0.03f, 0.06f, 0.55f * alpha); + glBegin(GL_QUADS); + glVertex2f(8, panelTop + 10); + glVertex2f(panelW, panelTop + 10); + glVertex2f(panelW, panelTop + 10 - panelH); + glVertex2f(8, panelTop + 10 - panelH); + glEnd(); + /* subtle border */ + glColor4f(0.3f, 0.6f, 0.9f, 0.35f * alpha); + glBegin(GL_LINE_LOOP); + glVertex2f(8, panelTop + 10); + glVertex2f(panelW, panelTop + 10); + glVertex2f(panelW, panelTop + 10 - panelH); + glVertex2f(8, panelTop + 10 - panelH); + glEnd(); + + /* additive, theme-tinted text with a faint glow pass */ + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + float tr, tg, tb; + hsv_to_rgb(cfg.hue, 0.35f, 1.0f, &tr, &tg, &tb); + + char buf[80]; + float y = panelTop - h; + + /* title */ + glColor4f(tr*alpha, tg*alpha, tb*alpha, alpha); + glLineWidth(2.4f); stroke_text("- VECTORGONS -", left, y, h + 3); + glLineWidth(1.6f); + y -= lineStep + 6; + + #define LINE(...) do { snprintf(buf,sizeof buf,__VA_ARGS__); \ + /* glow pass */ \ + glColor4f(tr*alpha*0.6f,tg*alpha*0.6f,tb*alpha*0.6f, alpha*0.5f); \ + glLineWidth(3.0f); stroke_text(buf,left,y,h); \ + /* crisp pass */ \ + glColor4f(tr*alpha,tg*alpha,tb*alpha,alpha); \ + glLineWidth(1.5f); stroke_text(buf,left,y,h); \ + y -= lineStep; } while(0) + + LINE("SPEED PGUP/DN %.0f%%%s", cfg.speed, cfg.speed < 0 ? " REV" : ""); + LINE("TUMBLE Q/E %.0f%%", cfg.tumble); + LINE("TUMBLE VAR T/Y %.0f%%", cfg.tumble_var); + LINE("RENDER DST Z/X %.0f", cfg.render_dist); + LINE("DENSITY +/- %d", active_count()); + LINE("SIZE MIN U/J %.1f", cfg.size_min); + LINE("SIZE MAX I/K %.1f", cfg.size_max); + LINE("HUE [ / ] %.0f", cfg.hue); + if (cfg.hue_cycle > 0.001f) LINE("HUE CYCLE C/V %.0f%%", cfg.hue_cycle); + else LINE("%s", "HUE CYCLE C/V OFF"); + LINE("COLOR M %s", cfg.multicolor ? "MULTICOLOR" : "SINGLE"); + LINE("GLOW O/L %.0f%%", cfg.glow); + LINE("FLICKER G/H %.0f%%", cfg.flicker); + LINE("SHAPES N %s", cfg.cycle_shapes ? "CYCLE" : "RANDOM"); + LINE("MOVE CAM WASD %+.0f %+.0f", cam_x, cam_y); + LINE("ROTATE CAM ARROWS %+.0f %+.0f", cam_yaw, cam_pitch); + LINE("FULLSCREEN F %s", cfg.fullscreen ? "ON" : "OFF"); + LINE("PAUSE SPACE %s", cfg.paused ? "PAUSED" : "RUNNING"); + LINE("%s", "QUIT ESC"); + #undef LINE + + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + glMatrixMode(GL_MODELVIEW); + glPopMatrix(); + /* leave additive blending on for the next 3-D frame */ +} + +/* ================================================================== */ +/* Input. */ +/* ================================================================== */ + +static double last_input_time = 0.0; + +static void clampf(float *v, float lo, float hi) { + if (*v < lo) *v = lo; + if (*v > hi) *v = hi; +} + +static void normalize_sizes(void) { + if (cfg.size_min < 0.4f) cfg.size_min = 0.4f; + if (cfg.size_max < cfg.size_min) cfg.size_max = cfg.size_min; + if (cfg.size_max > 54.0f) cfg.size_max = 54.0f; +} + +/* ================================================================== */ +/* Settings persistence: a plain key=value file under $HOME so the */ +/* user's last-used settings become the defaults next launch. */ +/* ================================================================== */ + +static void settings_path(char *buf, size_t n) { + const char *home = getenv("HOME"); + if (home && *home) snprintf(buf, n, "%s/.vectorgons", home); + else snprintf(buf, n, ".vectorgons"); +} + +static void save_settings(void) { + char path[1024]; + settings_path(path, sizeof path); + FILE *f = fopen(path, "w"); + if (!f) return; + fprintf(f, "# Vectorgons settings (auto-saved on exit)\n"); + fprintf(f, "speed=%g\n", cfg.speed); + fprintf(f, "tumble=%g\n", cfg.tumble); + fprintf(f, "tumble_var=%g\n", cfg.tumble_var); + fprintf(f, "render_dist=%g\n", cfg.render_dist); + fprintf(f, "density=%d\n", cfg.density); + fprintf(f, "size_min=%g\n", cfg.size_min); + fprintf(f, "size_max=%g\n", cfg.size_max); + fprintf(f, "hue=%g\n", cfg.hue); + fprintf(f, "hue_cycle=%g\n", cfg.hue_cycle); + fprintf(f, "multicolor=%d\n", cfg.multicolor); + fprintf(f, "cycle_shapes=%d\n", cfg.cycle_shapes); + fprintf(f, "glow=%g\n", cfg.glow); + fprintf(f, "flicker=%g\n", cfg.flicker); + fprintf(f, "fullscreen=%d\n", cfg.fullscreen); + fclose(f); +} + +static void load_settings(void) { + char path[1024]; + settings_path(path, sizeof path); + FILE *f = fopen(path, "r"); + if (!f) return; /* first run: keep built-in defaults */ + char line[256]; + while (fgets(line, sizeof line, f)) { + if (line[0] == '#' || line[0] == '\n') continue; + char *eq = strchr(line, '='); + if (!eq) continue; + *eq = '\0'; + const char *key = line, *val = eq + 1; + double d = atof(val); + if (!strcmp(key, "speed")) cfg.speed = (float)d; + else if (!strcmp(key, "tumble")) cfg.tumble = (float)d; + else if (!strcmp(key, "tumble_var")) cfg.tumble_var = (float)d; + else if (!strcmp(key, "render_dist")) cfg.render_dist = (float)d; + else if (!strcmp(key, "density")) cfg.density = (int)d; + else if (!strcmp(key, "size_min")) cfg.size_min = (float)d; + else if (!strcmp(key, "size_max")) cfg.size_max = (float)d; + else if (!strcmp(key, "hue")) cfg.hue = (float)d; + else if (!strcmp(key, "hue_cycle")) cfg.hue_cycle = (float)d; + else if (!strcmp(key, "multicolor")) cfg.multicolor = (int)d; + else if (!strcmp(key, "cycle_shapes")) cfg.cycle_shapes = (int)d; + else if (!strcmp(key, "glow")) cfg.glow = (float)d; + else if (!strcmp(key, "flicker")) cfg.flicker = (float)d; + else if (!strcmp(key, "fullscreen")) cfg.fullscreen = (int)d; + } + fclose(f); + /* defend against a hand-edited or stale file by clamping into range */ + clampf(&cfg.speed, -100, 100); + clampf(&cfg.tumble, 0, 100); + clampf(&cfg.tumble_var, 0, 100); + clampf(&cfg.render_dist, 40, 1520); + clampf(&cfg.hue_cycle, 0, 100); + clampf(&cfg.glow, 0, 100); + clampf(&cfg.flicker, 0, 100); + cfg.hue = fmodf(cfg.hue, 360.0f); if (cfg.hue < 0) cfg.hue += 360.0f; + if (cfg.density < 1) cfg.density = 1; + if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES; + normalize_sizes(); + cfg.multicolor = cfg.multicolor ? 1 : 0; + cfg.cycle_shapes = cfg.cycle_shapes ? 1 : 0; + cfg.fullscreen = cfg.fullscreen ? 1 : 0; +} + +static void toggle_fullscreen(GLFWwindow *win) { + static int wx = 100, wy = 100, ww = 1100, wh = 760; + cfg.fullscreen = !cfg.fullscreen; + if (cfg.fullscreen) { + glfwGetWindowPos(win, &wx, &wy); + glfwGetWindowSize(win, &ww, &wh); + GLFWmonitor *mon = glfwGetPrimaryMonitor(); + const GLFWvidmode *m = glfwGetVideoMode(mon); + glfwSetWindowMonitor(win, mon, 0, 0, m->width, m->height, m->refreshRate); + } else { + glfwSetWindowMonitor(win, NULL, wx, wy, ww, wh, 0); + } +} + +static void key_cb(GLFWwindow *win, int key, int sc, int action, int mods) { + (void)sc; (void)mods; + if (action != GLFW_PRESS && action != GLFW_REPEAT) return; + last_input_time = glfwGetTime(); /* keeps the OSD awake */ + + switch (key) { + case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(win, 1); break; + + /* WASD pans the camera and the arrow keys rotate it; both are polled + * per-frame in the main loop for smooth, frame-rate-independent motion. */ + case GLFW_KEY_PAGE_UP: cfg.speed += 2.5f; clampf(&cfg.speed, -100,100); break; + case GLFW_KEY_PAGE_DOWN: cfg.speed -= 2.5f; clampf(&cfg.speed, -100,100); break; + + case GLFW_KEY_E: cfg.tumble += 2.5f; clampf(&cfg.tumble,0,100); break; + case GLFW_KEY_Q: cfg.tumble -= 2.5f; clampf(&cfg.tumble,0,100); break; + + case GLFW_KEY_T: cfg.tumble_var += 5; clampf(&cfg.tumble_var,0,100); break; + case GLFW_KEY_Y: cfg.tumble_var -= 5; clampf(&cfg.tumble_var,0,100); break; + + case GLFW_KEY_X: cfg.render_dist += 10; clampf(&cfg.render_dist,40,1520); break; + case GLFW_KEY_Z: cfg.render_dist -= 10; clampf(&cfg.render_dist,40,1520); break; + + case GLFW_KEY_U: cfg.size_min += 0.2f; normalize_sizes(); break; + case GLFW_KEY_J: cfg.size_min -= 0.2f; normalize_sizes(); break; + case GLFW_KEY_I: cfg.size_max += 0.2f; normalize_sizes(); break; + case GLFW_KEY_K: cfg.size_max -= 0.2f; normalize_sizes(); break; + + case GLFW_KEY_LEFT_BRACKET: cfg.hue -= 6; if (cfg.hue < 0) cfg.hue += 360; break; + case GLFW_KEY_RIGHT_BRACKET: cfg.hue += 6; if (cfg.hue >= 360) cfg.hue -= 360; break; + + case GLFW_KEY_C: cfg.hue_cycle += 5; clampf(&cfg.hue_cycle,0,100); break; + case GLFW_KEY_V: cfg.hue_cycle -= 5; clampf(&cfg.hue_cycle,0,100); break; + + case GLFW_KEY_M: cfg.multicolor = !cfg.multicolor; break; + case GLFW_KEY_N: cfg.cycle_shapes = !cfg.cycle_shapes; spawn_counter = 0; break; + + case GLFW_KEY_O: cfg.glow += 5; clampf(&cfg.glow,0,100); break; + case GLFW_KEY_L: cfg.glow -= 5; clampf(&cfg.glow,0,100); break; + case GLFW_KEY_G: cfg.flicker += 5; clampf(&cfg.flicker,0,100); break; + case GLFW_KEY_H: cfg.flicker -= 5; clampf(&cfg.flicker,0,100); break; + + /* +/- adjust the live on-screen count and back-solve the baseline + * density, so a press always moves the count even when render-distance + * scaling has the active total pinned at MAX_BODIES. */ + case GLFW_KEY_EQUAL: { + int a = active_count() + 10; if (a > MAX_BODIES) a = MAX_BODIES; + cfg.density = (int)(a * (RENDER_REF / cfg.render_dist) + 0.5f); + if (cfg.density < 1) cfg.density = 1; + if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES; + rebuild_field(); break; + } + case GLFW_KEY_MINUS: { + int a = active_count() - 10; if (a < 1) a = 1; + cfg.density = (int)(a * (RENDER_REF / cfg.render_dist) + 0.5f); + if (cfg.density < 1) cfg.density = 1; + if (cfg.density > MAX_BODIES) cfg.density = MAX_BODIES; + break; + } + + case GLFW_KEY_F: + case GLFW_KEY_F11: toggle_fullscreen(win); break; + + case GLFW_KEY_SPACE: cfg.paused = !cfg.paused; break; + default: break; + } +} + +/* ================================================================== */ +/* Projection + body wireframe (modelview transform must be set). */ +/* ================================================================== */ + +static void rot2(double *c, int a, int b, double th) { + double ca = cos(th), sa = sin(th), x = c[a], y = c[b]; + c[a] = x*ca - y*sa; + c[b] = x*sa + y*ca; +} + +/* Produce 3-D vertices for a body. 3-D shapes are copied through; + * higher-D shapes are rotated in their own dimension (driven by the + * tumble angle + a per-body phase) and perspective-projected down to + * 3-D, then renormalized to the unit sphere so the bounding radius + * stays <= 1 (which keeps the no-overlap guarantee intact). */ +static void project_body(const Solid *s, float angf, float phasef, float out[][3]) { + int d = s->dim; + if (d == 3) { + for (int i = 0; i < s->nv; i++) { + out[i][0] = s->v[i][0]; out[i][1] = s->v[i][1]; out[i][2] = s->v[i][2]; + } + return; + } + double ang = angf, ph = phasef; + for (int i = 0; i < s->nv; i++) { + double c[MAXD]; + for (int k = 0; k < MAXD; k++) c[k] = (k < d) ? s->v[i][k] : 0.0; + + rot2(c, 0, 1, ang*0.5); + if (d >= 4) { rot2(c, 2, 3, ang*0.9 + ph); rot2(c, 0, 3, ang*0.6); } + if (d >= 5) { rot2(c, 1, 4, ang*0.7 + ph*0.5); rot2(c, 3, 4, ang*0.45); } + if (d >= 6) { rot2(c, 2, 5, ang*0.8); rot2(c, 4, 5, ang*0.4 + ph*0.3); } + + for (int k = d-1; k >= 3; k--) { /* project k-D -> (k-1)-D */ + double den = 4.0 - c[k]; + if (den < 0.5) den = 0.5; + double f = 4.0 / den; + for (int j = 0; j < k; j++) c[j] *= f; + } + out[i][0] = (float)c[0]; out[i][1] = (float)c[1]; out[i][2] = (float)c[2]; + } + double mr2 = 0; + for (int i = 0; i < s->nv; i++) { + double r2 = (double)out[i][0]*out[i][0] + (double)out[i][1]*out[i][1] + (double)out[i][2]*out[i][2]; + if (r2 > mr2) mr2 = r2; + } + if (mr2 > 1e-9) { + float inv = (float)(1.0/sqrt(mr2)); + for (int i = 0; i < s->nv; i++) { out[i][0]*=inv; out[i][1]*=inv; out[i][2]*=inv; } + } +} + +static void draw_edges(const Solid *s, float p[][3]) { + glBegin(GL_LINES); + for (int e = 0; e < s->ne; e++) { + glVertex3fv(p[s->e[e][0]]); + glVertex3fv(p[s->e[e][1]]); + } + glEnd(); +} + +/* ================================================================== */ +/* Help / main. */ +/* ================================================================== */ + +static void print_help(void) { + printf( + "\nVECTORGONS — vector solids & asteroids tumbling through space\n" + "------------------------------------------------------------\n" + " W/A/S/D move (pan) camera Arrows rotate camera\n" + " PgUp/PgDn approach speed (PgDn past 0 = reverse)\n" + " Q / E tumble rate\n" + " T / Y tumble variance Z / X render distance\n" + " U / J size min I / K size max\n" + " [ / ] hue C / V hue-cycle rate\n" + " O / L glow G / H flicker\n" + " + / - density M color N shapes\n" + " F / F11 fullscreen Space pause Esc quit\n\n" + " (all settings are also shown in the on-screen display)\n\n"); + fflush(stdout); +} + +int main(void) { + srand((unsigned)time(NULL)); + init_solids(); + load_settings(); /* user's last-used settings become this run's defaults */ + printf("Vectorgons: %d shape types loaded.\n", num_shapes); + + if (!glfwInit()) { fprintf(stderr, "Failed to init GLFW\n"); return 1; } + glfwWindowHint(GLFW_SAMPLES, 4); + + GLFWwindow *win = glfwCreateWindow(1100, 760, "Vectorgons", NULL, NULL); + if (!win) { fprintf(stderr, "Failed to create window\n"); glfwTerminate(); return 1; } + glfwMakeContextCurrent(win); + glfwSwapInterval(1); + glfwSetKeyCallback(win, key_cb); + + GLfloat lwr[2] = {1, 10}; + glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, lwr); + max_line_width = lwr[1] > 1 ? lwr[1] : 10.0f; + + if (cfg.fullscreen) { cfg.fullscreen = 0; toggle_fullscreen(win); } /* restore saved */ + + rebuild_field(); + print_help(); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE); /* additive glow */ + glEnable(GL_LINE_SMOOTH); + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + glEnable(GL_MULTISAMPLE); + + last_input_time = glfwGetTime(); + double last = glfwGetTime(); + + while (!glfwWindowShouldClose(win)) { + double now = glfwGetTime(); + float dt = (float)(now - last); + last = now; + if (dt > 0.05f) dt = 0.05f; + + int fbw, fbh; + glfwGetFramebufferSize(win, &fbw, &fbh); + if (fbh < 1) fbh = 1; + glViewport(0, 0, fbw, fbh); + + /* --- WASD pan + arrow-key rotate (held keys, frame-rate independent) --- */ + { + float pan = 42.0f * dt; + float rot = 60.0f * dt; /* degrees / second */ + int moved = 0; + if (glfwGetKey(win, GLFW_KEY_W) == GLFW_PRESS) { cam_y += pan; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_S) == GLFW_PRESS) { cam_y -= pan; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_D) == GLFW_PRESS) { cam_x += pan; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_A) == GLFW_PRESS) { cam_x -= pan; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_LEFT) == GLFW_PRESS) { cam_yaw -= rot; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_RIGHT) == GLFW_PRESS) { cam_yaw += rot; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_UP) == GLFW_PRESS) { cam_pitch += rot; moved = 1; } + if (glfwGetKey(win, GLFW_KEY_DOWN) == GLFW_PRESS) { cam_pitch -= rot; moved = 1; } + if (cam_x < -180) cam_x = -180; + if (cam_x > 180) cam_x = 180; + if (cam_y < -180) cam_y = -180; + if (cam_y > 180) cam_y = 180; + cam_yaw = fmodf(cam_yaw, 360.0f); /* free 360 rotation, both axes */ + cam_pitch = fmodf(cam_pitch, 360.0f); + if (moved) last_input_time = now; /* keep the OSD awake */ + } + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + gluPerspective(60.0, (double)fbw / (double)fbh, 0.5, cfg.render_dist + 80.0); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glRotatef(cam_pitch, 1.0f, 0.0f, 0.0f); /* apply camera rotation (arrows) */ + glRotatef(cam_yaw, 0.0f, 1.0f, 0.0f); + glTranslatef(-cam_x, -cam_y, 0.0f); /* apply camera pan (WASD) */ + + glClearColor(0.006f, 0.010f, 0.035f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + /* continuous hue cycling (advances even while paused) */ + if (cfg.hue_cycle > 0.001f) { + cfg.hue += (cfg.hue_cycle / 100.0f) * 120.0f * dt; /* up to 120 deg/s */ + cfg.hue = fmodf(cfg.hue, 360.0f); + if (cfg.hue < 0) cfg.hue += 360.0f; + } + + /* signed approach: negative speed flies the field backward (Page Down) */ + float sp = cfg.speed / 100.0f; + float approach = copysignf(powf(fabsf(sp), 1.6f), sp) * 120.0f; + float tumble_rate = powf(cfg.tumble / 100.0f, 1.4f) * 3.2f; + float var = cfg.tumble_var / 100.0f; + float glow = cfg.glow / 100.0f; + float fl = cfg.flicker / 100.0f; + + int active = active_count(); + ensure_filled(active); + + for (int i = 0; i < active; i++) { + Body *b = &bodies[i]; + + if (!cfg.paused) { + float spin = powf(4.0f, b->spin_seed * var); /* live variance */ + b->z -= approach * dt; + b->angle += tumble_rate * spin * dt; + } + if (b->spawn_fade < 1.0f) { /* ease in after spawn */ + b->spawn_fade += dt * 2.2f; + if (b->spawn_fade > 1.0f) b->spawn_fade = 1.0f; + } + /* recycle once the body leaves the render-distance sphere (in any + * direction), so culling is purely radial and rotation-agnostic */ + float dist2 = b->x*b->x + b->y*b->y + b->z*b->z; + float R = cfg.render_dist; + if (dist2 > R*R) { spawn_body(b, 0, active, i); continue; } + + float depth = sqrtf(dist2) / R; /* 0 at camera .. 1 at far sphere */ + if (depth < 0) depth = 0; + if (depth > 1) depth = 1; + + float bright = 0.25f + (1.0f - depth) * 0.75f; + if (fl > 0.001f) bright *= 1.0f - fl * 0.6f * frand(); /* flicker */ + + float alpha = b->spawn_fade; + if (depth > 0.85f) alpha *= (1.0f - depth) / 0.15f; /* fade in far */ + if (alpha < 0) alpha = 0; + + float hue = cfg.multicolor ? (cfg.hue + b->hue_offset) : cfg.hue; + float r, g, bl; + hsv_to_rgb(hue, 0.9f, 1.0f, &r, &g, &bl); + + float lw = 1.0f + (1.0f - depth) * 2.0f; + const Solid *s = &solids[b->shape]; + + /* project once (4/5/6-D shapes morph; 3-D shapes pass through) */ + static float p3[MAX_VERTS][3]; + project_body(s, b->angle, b->hue_offset * 0.01745329f, p3); + + glPushMatrix(); + glTranslatef(b->x, b->y, -b->z); + glRotatef(b->angle * 57.2957795f, b->axis[0], b->axis[1], b->axis[2]); + glScalef(b->size, b->size, b->size); + + /* CRT phosphor glow: a sharp core wrapped in a soft, dim mist. + * Hardware caps line width (~10px), so the near halo is built from + * many faint antialiased width layers fading outward (a smooth + * gradient, not one fat blurry line), and a few faint scaled-up + * ghost copies bloom the glow into a larger volume than the line + * width alone could ever reach. */ + if (glow > 0.001f) { + float maxw = max_line_width; + + /* (a) soft halo hugging each vector, fading out into mist */ + for (int p = 1; p <= 5; p++) { + float t = p / 5.0f; /* 0..1 outward */ + float w = lw + (maxw - lw) * t; + if (w < 1.0f) w = 1.0f; + float fade = (1.0f - t) * (1.0f - t); /* dim, fades out */ + glColor4f(r*bright, g*bright, bl*bright, alpha * glow * 0.16f * fade); + glLineWidth(w); + draw_edges(s, p3); + } + + /* (b) volumetric bloom: a few faint, widely-spaced enlarged + * ghosts drawn at the maximum (blurriest) width, so they smear + * into an outer haze that fills a larger volume than the line- + * width-capped halo can reach -- without reading as crisp rings */ + glLineWidth(maxw); + for (int p = 1; p <= 3; p++) { + float t = p / 3.0f; + float sc = 1.0f + glow * (0.22f + 0.55f * t); + glColor4f(r*bright, g*bright, bl*bright, + alpha * glow * 0.035f * (1.0f - 0.6f * t)); + glPushMatrix(); + glScalef(sc, sc, sc); + draw_edges(s, p3); + glPopMatrix(); + } + } + + /* crisp core, drawn on top so the vector stays sharp */ + glColor4f(r*bright, g*bright, bl*bright, alpha); + glLineWidth(lw); + draw_edges(s, p3); + + glPopMatrix(); + } + + /* OSD: full for 10 s after last keypress, then fades over 4 s */ + float idle = (float)(now - last_input_time); + float osd_alpha = 1.0f; + if (idle > 10.0f) osd_alpha = 1.0f - (idle - 10.0f) / 4.0f; + if (osd_alpha < 0) osd_alpha = 0; + render_osd(fbw, fbh, osd_alpha); + + glfwSwapBuffers(win); + glfwPollEvents(); + } + + save_settings(); /* persist current settings as next run's defaults */ + glfwDestroyWindow(win); + glfwTerminate(); + return 0; +}