vectorgons/vectorgons.c
2026-06-01 22:50:56 -07:00

2005 lines
88 KiB
C

/*
* Vectorgons — a starfield simulator of colorful vector-drawn platonic
* solids tumbling through space toward the camera.
*
* C + OpenGL (legacy immediate mode for true wireframe "vector" rendering)
* Windowing & input via GLFW3, perspective via GLU.
*
* Features:
* - 160+ shape types streaming at the camera: the 5 platonic solids,
* archimedeans, prisms, antiprisms, bipyramids, trapezohedra, the
* many-faced solids (tri/tetra/.../hecato-hedron), star polygons and the
* unicursal hexagram, a big set of googie / atomic-age 3-D objects
* (starbursts, sea-urchins, gyroscopes, orbital atoms, ringed planets,
* molecules, diabolos, sputniks), 2-D and 3-D symbols (smiley, frowny,
* angry, biohazard, peace, cross, ? ! # $ £ @), object iconography
* (space invaders, UFOs, pac-man, alien heads, pipes, umbrellas, hands)
* and live analog/digital clocks showing the real current time, 4/5/6-D
* polytopes (tesseract, penteract, 24-cell, ...) that morph as they
* tumble, plus randomly generated 3-D convex-hull asteroids
* - WASD pans the camera and the arrow keys rotate it
* - User-settable render distance (how far away solids spawn)
* - Random per-body sizes (user-settable min / max)
* - Guaranteed no overlap between solids (bounding-sphere rejection)
* - Adjustable tumble rate AND tumble-speed variance
* - Adjustable CRT-style glow / light bleed
* - Adjustable vector flicker
* - On-screen display (self-contained vector font) that fades after idle
* - Single-hue and multicolor modes; adjustable hue + continuous hue cycling
* - Fullscreen toggle
* - Settings auto-saved to ~/.vectorgons on exit, reloaded as defaults
*
* All controls are listed in the on-screen display (and printed at startup).
*/
#define GL_SILENCE_DEPRECATION
#include <GLFW/glfw3.h>
#include <GL/glu.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include <time.h>
/* ================================================================== */
/* 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 220
#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 int clock_idx[4] = {-1,-1,-1,-1}; /* analog2d, analog3d, digital2d, digital3d */
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; }
/* Fill an existing Solid from the strokes: flat if depth<=0, else extruded.
* (Used both for one-shot shapes and for the live clocks, which rewrite their
* solid in place every frame.) */
static void sym_fill_solid(Solid *s, SymB *b, float depth) {
s->dim = 3; s->nv = 0; s->ne = 0;
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);
}
static void sym_finish(SymB *b, float depth) { sym_fill_solid(new_solid(3), b, depth); }
/* ---- 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);
}
/* ================================================================== */
/* More googie / atomic-age 3-D objects, plus faces and the '@' sign. */
/* ================================================================== */
/* Symmetric unit-direction sets for radial bursts. Returns the count.
* 0:octahedron(6) 1:cube(8) 2:icosahedron(12) 3:dodecahedron(20)
* 4:icosa+dodeca(32). */
static int dir_set(int which, double out[][3]) {
const double P = 1.6180339887, Q = 0.6180339887;
double t[40][3]; int n = 0;
if (which == 0) {
double d[6][3] = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}};
for (int i=0;i<6;i++){ t[n][0]=d[i][0];t[n][1]=d[i][1];t[n][2]=d[i][2];n++; }
} else if (which == 1) {
for (int x=-1;x<=1;x+=2) for (int y=-1;y<=1;y+=2) for (int z=-1;z<=1;z+=2)
{ t[n][0]=x;t[n][1]=y;t[n][2]=z;n++; }
} else {
if (which == 2 || which == 4) {
double d[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}};
for (int i=0;i<12;i++){ t[n][0]=d[i][0];t[n][1]=d[i][1];t[n][2]=d[i][2];n++; }
}
if (which == 3 || which == 4) {
for (int x=-1;x<=1;x+=2) for (int y=-1;y<=1;y+=2) for (int z=-1;z<=1;z+=2)
{ t[n][0]=x;t[n][1]=y;t[n][2]=z;n++; }
double e[12][3]={{0,Q,P},{0,Q,-P},{0,-Q,P},{0,-Q,-P},{Q,P,0},{Q,-P,0},
{-Q,P,0},{-Q,-P,0},{P,0,Q},{P,0,-Q},{-P,0,Q},{-P,0,-Q}};
for (int i=0;i<12;i++){ t[n][0]=e[i][0];t[n][1]=e[i][1];t[n][2]=e[i][2];n++; }
}
}
for (int i=0;i<n;i++){
double L=sqrt(t[i][0]*t[i][0]+t[i][1]*t[i][1]+t[i][2]*t[i][2]); if(L<1e-9)L=1;
out[i][0]=t[i][0]/L; out[i][1]=t[i][1]/L; out[i][2]=t[i][2]/L;
}
return n;
}
/* small ring "ball" of `seg` points at p, in the plane perpendicular to u */
static void add_ball(Solid *s, double px,double py,double pz,
double ux,double uy,double uz, double rad, int seg) {
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;
int first=-1, prev=-1;
for (int k=0;k<seg;k++){
double a=TAU*k/seg; int idx=s->nv;
PV(s, px+rad*(cos(a)*rx+sin(a)*sx), py+rad*(cos(a)*ry+sin(a)*sy),
pz+rad*(cos(a)*rz+sin(a)*sz), 0,0,0);
if(first<0)first=idx;
if(prev>=0)AE(s,prev,idx);
prev=idx;
}
if(first>=0&&prev>=0) AE(s,prev,first);
}
/* radial spike burst (starburst): a spoke to each direction, optionally
* alternating long/short, optionally capped with a little ball */
static void gen_burst3d(int which, double longr, double shortr, int balls) {
Solid *s=new_solid(3);
double d[40][3]; int nd=dir_set(which,d);
int c=s->nv; PV(s,0,0,0,0,0,0);
for (int i=0;i<nd;i++){
double r=(shortr>0 && (i&1))?shortr:longr;
double tx=d[i][0]*r, ty=d[i][1]*r, tz=d[i][2]*r;
int tip=s->nv; PV(s,tx,ty,tz,0,0,0); AE(s,c,tip);
if (balls) add_ball(s,tx,ty,tz,d[i][0],d[i][1],d[i][2],0.12,5);
}
center_normalize(s);
}
/* great circles at spread orientations (gyroscope / orbit cage) */
static void gen_rings3d(int nrings, int seg) {
Solid *s=new_solid(3);
for (int r=0;r<nrings;r++){
double ax=TAU*r/(2.0*nrings), ay=TAU*r/(3.0*nrings)+0.3*r;
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 y1=y*cos(ax)-z*sin(ax), z1=y*sin(ax)+z*cos(ax); y=y1; z=z1;
double x1=x*cos(ay)+z*sin(ay), z2=-x*sin(ay)+z*cos(ay); x=x1; z=z2;
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);
}
/* atomic orbital cluster: tilted elliptical orbits around an octahedral nucleus */
static void gen_atomorbits(int norbits, int seg) {
Solid *s=new_solid(3);
for (int o=0;o<norbits;o++){
double sp=TAU*o/norbits, tx=TAU*o/(norbits*2.0)+0.4;
int first=-1, prev=-1;
for (int i=0;i<seg;i++){
double a=TAU*i/seg, x=cos(a), y=sin(a)*0.42, z=0;
double x1=x*cos(sp)-y*sin(sp), y1=x*sin(sp)+y*cos(sp); x=x1; y=y1;
double y2=y*cos(tx)-z*sin(tx), z2=y*sin(tx)+z*cos(tx); y=y2; z=z2;
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);
}
int b0=s->nv; double r=0.12;
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);
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);
}
/* ringed planet (Saturn): a wireframe sphere with tilted equatorial ring(s) */
static void gen_saturn3d(int nrings, double tilt) {
Solid *s=new_solid(3);
const double pi=TAU*0.5; int lats=3, seg=14;
for (int la=1;la<=lats;la++){
double phi=pi*la/(lats+1)-pi*0.5, y=sin(phi)*0.6, rr=cos(phi)*0.6;
int first=-1, prev=-1;
for (int i=0;i<seg;i++){ double a=TAU*i/seg; int idx=s->nv;
PV(s,rr*cos(a),y,rr*sin(a),0,0,0);
if(first<0)first=idx;
if(prev>=0)AE(s,prev,idx);
prev=idx; }
AE(s,prev,first);
}
for (int m=0;m<2;m++){
double mo=TAU*m/4; int first=-1, prev=-1;
for (int i=0;i<seg;i++){ double a=TAU*i/seg, x=cos(a)*0.6, y=sin(a)*0.6, z=0;
double x1=x*cos(mo)+z*sin(mo), z1=-x*sin(mo)+z*cos(mo);
int idx=s->nv; PV(s,x1,y,z1,0,0,0);
if(first<0)first=idx;
if(prev>=0)AE(s,prev,idx);
prev=idx; }
AE(s,prev,first);
}
for (int rg=0;rg<nrings;rg++){
double rad=1.0+rg*0.2; int first=-1, prev=-1, seg2=22;
for (int i=0;i<seg2;i++){ double a=TAU*i/seg2, x=rad*cos(a), y=0, z=rad*sin(a);
double y1=y*cos(tilt)-z*sin(tilt), z1=y*sin(tilt)+z*cos(tilt);
int idx=s->nv; PV(s,x,y1,z1,0,0,0);
if(first<0)first=idx;
if(prev>=0)AE(s,prev,idx);
prev=idx; }
AE(s,prev,first);
}
center_normalize(s);
}
/* ball-and-stick molecule: a central ball bonded to balls in each direction */
static void gen_molecule3d(int which) {
Solid *s=new_solid(3);
double d[40][3]; int nd=dir_set(which,d);
int c=s->nv; PV(s,0,0,0,0,0,0);
add_ball(s,0,0,0, 0,0,1, 0.2, 6);
for (int i=0;i<nd;i++){
double tx=d[i][0], ty=d[i][1], tz=d[i][2];
int tip=s->nv; PV(s,tx,ty,tz,0,0,0); AE(s,c,tip);
add_ball(s,tx,ty,tz,d[i][0],d[i][1],d[i][2],0.17,6);
}
center_normalize(s);
}
/* diabolo / hourglass burst: two cones tip-to-tip with a waisted lattice */
static void gen_diabolo3d(int nspokes) {
Solid *s=new_solid(3);
int top=s->nv; PV(s,0,0,1.0,0,0,0);
int bot=s->nv; PV(s,0,0,-1.0,0,0,0);
int rt=s->nv; for(int i=0;i<nspokes;i++){ double a=TAU*i/nspokes; PV(s,0.6*cos(a),0.6*sin(a),0.35,0,0,0); }
int rb=s->nv; for(int i=0;i<nspokes;i++){ double a=TAU*i/nspokes; PV(s,0.6*cos(a),0.6*sin(a),-0.35,0,0,0); }
for (int i=0;i<nspokes;i++){
AE(s,top,rt+i); AE(s,bot,rb+i);
AE(s,rt+i,rt+(i+1)%nspokes); AE(s,rb+i,rb+(i+1)%nspokes);
AE(s,rt+i,rb+i);
}
center_normalize(s);
}
/* planar ray starburst with a pair of axial spikes for 3-D pop */
static void gen_raystar3d(int nrays) {
Solid *s=new_solid(3);
int c=s->nv; PV(s,0,0,0,0,0,0);
for (int i=0;i<nrays;i++){ double a=TAU*i/nrays, r=(i&1)?0.6:1.0;
int tip=s->nv; PV(s,r*cos(a),r*sin(a),0,0,0,0); AE(s,c,tip); }
int z1=s->nv; PV(s,0,0,0.7,0,0,0); AE(s,c,z1);
int z2=s->nv; PV(s,0,0,-0.7,0,0,0); AE(s,c,z2);
center_normalize(s);
}
/* ---- expressive faces and the '@' sign (flat + extruded) ---------- */
static void build_frowny(SymB *b) {
sym_ring(b, 0,0, 1.0, 18);
sym_ring(b, -0.35, 0.32, 0.12, 6);
sym_ring(b, 0.35, 0.32, 0.12, 6);
sym_arc (b, 0.0, -0.55, 0.5, sym_deg(20), sym_deg(160), 8); /* downturned mouth */
}
static void build_angry(SymB *b) {
sym_ring(b, 0,0, 1.0, 18);
sym_ring(b, -0.33, 0.26, 0.11, 6);
sym_ring(b, 0.33, 0.26, 0.11, 6);
sym_line(b, -0.58, 0.62, -0.14, 0.42); /* angry V brows: inner ends low */
sym_line(b, 0.58, 0.62, 0.14, 0.42);
sym_arc (b, 0.0, -0.58, 0.48, sym_deg(20), sym_deg(160), 8); /* frown */
}
static void build_at(SymB *b) {
sym_arc (b, 0.0, 0.0, 1.0, sym_deg(-25), sym_deg(295), 20); /* outer ring, open at right */
sym_ring(b, -0.05, 0.0, 0.40, 12); /* inner 'a' bowl */
sym_line(b, 0.35, 0.42, 0.35, -0.55); /* 'a' stem / tail */
}
/* ---- pop-culture / object iconography (flat 2-D + extruded 3-D) ---- */
static void build_invader(SymB *b) { /* blocky pixel-alien (Space Invader) */
const double body[] = {
-0.7, 0.2, -0.7,-0.2, -0.5,-0.2, -0.5,-0.5, -0.25,-0.5, -0.25,-0.25,
0.25,-0.25, 0.25,-0.5, 0.5,-0.5, 0.5,-0.2, 0.7,-0.2, 0.7, 0.2,
0.45,0.5, 0.2,0.5, 0.2,0.78, 0.05,0.78, 0.05,0.5, -0.05,0.5,
-0.05,0.78, -0.2,0.78, -0.2,0.5, -0.45,0.5 };
sym_poly(b, body, 22, 1);
sym_ring(b, -0.25, 0.12, 0.08, 5);
sym_ring(b, 0.25, 0.12, 0.08, 5);
}
static void build_ufo(SymB *b) { /* flying saucer */
sym_ellipse(b, 0,0, 1.0, 0.30, 0, 22); /* saucer disc */
sym_arc(b, 0.0, 0.06, 0.46, sym_deg(0), sym_deg(180), 11); /* dome */
for (int i=-2;i<=2;i++) sym_ring(b, i*0.32, -0.22, 0.05, 4); /* lights */
}
static void build_pacman(SymB *b) { /* wedge-mouth + pellet */
sym_arc(b, 0,0, 1.0, sym_deg(36), sym_deg(324), 24);
int last = b->np - 1, c = sym_pt(b, 0, 0);
sym_edge(b, last, c); sym_edge(b, c, 0); /* close the mouth wedge */
sym_ring(b, 0.05, 0.45, 0.09, 5); /* eye */
sym_ring(b, 1.35, 0.0, 0.12, 6); /* pellet */
}
static void build_alien(SymB *b) { /* grey alien head */
int seg=22, first=-1, prev=-1;
for (int i=0;i<seg;i++){
double a=TAU*i/seg, x=0.62*cos(a), y=sin(a);
if (y<0) x *= (1.0 + y*0.42); /* taper to a chin */
int idx=sym_pt(b, x, y>0 ? y*0.95 : y*1.28);
if(first<0)first=idx;
if(prev>=0)sym_edge(b,prev,idx);
prev=idx;
}
sym_edge(b, prev, first);
sym_ellipse(b, -0.26, 0.05, 0.22, 0.10, sym_deg( 28), 9); /* almond eyes */
sym_ellipse(b, 0.26, 0.05, 0.22, 0.10, sym_deg(-28), 9);
}
static void build_pipe(SymB *b) { /* smoking pipe */
sym_arc(b, -0.55, 0.0, 0.34, sym_deg(18), sym_deg(342), 14); /* bowl, open at top */
const double stem[] = { -0.28,-0.22, 0.2,-0.32, 0.7,-0.3, 0.98,-0.18 };
sym_poly(b, stem, 4, 0);
sym_line(b, 0.98,-0.18, 1.04,-0.04); /* mouthpiece */
}
static void build_umbrella(SymB *b) { /* canopy + ribs + pole + J-handle */
double R = 0.95; int n = 4;
sym_arc(b, 0, 0.0, R, sym_deg(0), sym_deg(180), 14); /* canopy dome */
for (int i=0;i<n;i++){ /* scalloped hem */
double x0=-R+2*R*i/n, x1=-R+2*R*(i+1)/n, xm=(x0+x1)/2;
sym_arc(b, xm, 0.0, (x1-x0)/2, sym_deg(180), sym_deg(360), 4);
}
for (int i=0;i<=n;i++) sym_line(b, 0, R, -R+2*R*i/n, 0.0); /* ribs */
sym_line(b, 0, 0.0, 0, -0.85); /* pole */
sym_arc (b, -0.12, -0.85, 0.12, sym_deg(0), sym_deg(-180), 6); /* handle */
}
static void build_hand(SymB *b) { /* open hand silhouette */
const double hand[] = {
-0.45,-0.9, -0.5,-0.2, -0.85,0.1, -0.78,0.38, -0.45,0.08,
-0.4,0.72, -0.29,0.98, -0.18,0.66,
-0.12,0.82, 0.0,1.06, 0.09,0.74,
0.14,0.8, 0.27,1.0, 0.35,0.68,
0.41,0.62, 0.52,0.82, 0.56,0.5,
0.46,-0.9 };
sym_poly(b, hand, 18, 1);
}
/* ---- live clocks (rebuilt every frame from the current local time) ---- */
/* one seven-segment digit in a cell of width w and height 2h at (ox,oy) */
static void sym_7seg(SymB *b, int dg, double ox,double oy,double w,double h) {
int TL=sym_pt(b,ox,oy+2*h), TR=sym_pt(b,ox+w,oy+2*h);
int ML=sym_pt(b,ox,oy+h), MR=sym_pt(b,ox+w,oy+h);
int BL=sym_pt(b,ox,oy), BR=sym_pt(b,ox+w,oy);
static const unsigned char seg[10] =
{ 0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F };
int m = seg[dg % 10];
if(m&0x01) sym_edge(b,TL,TR); /* a top */
if(m&0x02) sym_edge(b,TR,MR); /* b up-right */
if(m&0x04) sym_edge(b,MR,BR); /* c lo-right */
if(m&0x08) sym_edge(b,BL,BR); /* d bottom */
if(m&0x10) sym_edge(b,ML,BL); /* e lo-left */
if(m&0x20) sym_edge(b,TL,ML); /* f up-left */
if(m&0x40) sym_edge(b,ML,MR); /* g middle */
}
static void build_analog(SymB *b, const struct tm *lt) {
sym_ring(b, 0,0, 1.0, 24); /* face */
for (int i=0;i<12;i++){ /* hour ticks */
double a=TAU*i/12;
sym_line(b, 0.86*cos(a),0.86*sin(a), 0.97*cos(a),0.97*sin(a));
}
int h=lt->tm_hour%12, m=lt->tm_min, s=lt->tm_sec;
double ha=TAU*0.25 - TAU*((h + m/60.0)/12.0); /* 12 at top, clockwise */
double ma=TAU*0.25 - TAU*(m/60.0);
double sa=TAU*0.25 - TAU*(s/60.0);
int c=sym_pt(b,0,0);
sym_edge(b, c, sym_pt(b, 0.50*cos(ha), 0.50*sin(ha))); /* hour */
sym_edge(b, c, sym_pt(b, 0.74*cos(ma), 0.74*sin(ma))); /* minute */
sym_edge(b, c, sym_pt(b, 0.84*cos(sa), 0.84*sin(sa))); /* second */
}
static void build_digital(SymB *b, const struct tm *lt) {
int hh=lt->tm_hour, mm=lt->tm_min, d[4]={hh/10,hh%10,mm/10,mm%10};
double w=0.30, h=0.30, cw=0.42, oy=-0.3;
double x[4]={-0.92,-0.92+cw, 0.18, 0.18+cw};
for (int i=0;i<4;i++) sym_7seg(b, d[i], x[i], oy, w, h);
sym_ring(b, -0.02, oy+0.42*h, 0.035, 4); /* colon dots */
sym_ring(b, -0.02, oy+1.4*h, 0.035, 4);
const double fr[]={-1.12,-0.55, 1.12,-0.55, 1.12,0.55, -1.12,0.55};
sym_poly(b, fr, 4, 1); /* bezel keeps size constant */
}
/* Rewrite the four clock solids in place from the current local time. */
static void update_clocks(void) {
if (clock_idx[0] < 0) return;
time_t t = time(NULL);
struct tm *lt = localtime(&t);
SymB b;
sym_init(&b); build_analog (&b, lt); sym_fill_solid(&solids[clock_idx[0]], &b, 0.0f);
sym_init(&b); build_analog (&b, lt); sym_fill_solid(&solids[clock_idx[1]], &b, 0.5f);
sym_init(&b); build_digital(&b, lt); sym_fill_solid(&solids[clock_idx[2]], &b, 0.0f);
sym_init(&b); build_digital(&b, lt); sym_fill_solid(&solids[clock_idx[3]], &b, 0.5f);
}
/* 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 (£) */
/* --- 30+ more googie / atomic-age 3-D objects --- */
gen_burst3d(0, 1.0, 0.0, 0); /* 6-spike asterisk star */
gen_burst3d(1, 1.0, 0.0, 0); /* 8-spike cube burst */
gen_burst3d(2, 1.0, 0.55, 0); /* 12-spike spiky star */
gen_burst3d(3, 1.0, 0.0, 0); /* 20-spike sea urchin */
gen_burst3d(4, 1.0, 0.6, 0); /* 32-spike dense urchin */
gen_burst3d(4, 1.0, 0.0, 0); /* 32-spike uniform urchin */
gen_burst3d(2, 1.0, 0.0, 1); /* 12 ball-tipped sputnik */
gen_burst3d(0, 1.0, 0.0, 1); /* 6 ball-tipped jacks */
gen_burst3d(1, 1.0, 0.0, 1); /* 8 ball-tipped burst */
gen_burst3d(3, 1.0, 0.0, 1); /* 20 ball-tipped burst */
gen_burst3d(2, 1.0, 0.4, 1); /* 12 alt ball-tipped burst */
gen_rings3d(2, 24); /* crossed rings */
gen_rings3d(3, 22); /* gyroscope */
gen_rings3d(4, 20); /* 4-ring cage */
gen_rings3d(5, 20); /* 5-ring cage */
gen_rings3d(6, 20); /* 6-ring orb cage */
gen_atomorbits(2, 24); /* 2-orbit atom */
gen_atomorbits(4, 20); /* 4-orbit atom */
gen_atomorbits(5, 18); /* 5-orbit atom */
gen_atomorbits(6, 16); /* 6-orbit atom */
gen_saturn3d(1, 0.0); /* ringed planet */
gen_saturn3d(2, 0.35); /* double-ringed planet */
gen_saturn3d(1, 0.6); /* tilted-ring planet */
gen_molecule3d(0); /* octahedral molecule */
gen_molecule3d(1); /* cubic molecule */
gen_molecule3d(2); /* icosahedral molecule */
gen_diabolo3d(6); /* hex diabolo */
gen_diabolo3d(10); /* diabolo / hourglass */
gen_diabolo3d(16); /* fine diabolo */
gen_raystar3d(12); /* 12-ray starburst */
gen_raystar3d(16); /* 16-ray starburst */
gen_raystar3d(24); /* 24-ray sunburst */
/* --- expressive faces + the @ sign (flat 2-D and extruded 3-D) --- */
add_symbol(build_angry); /* angry face */
add_symbol(build_frowny); /* frowny face */
add_symbol(build_at); /* at sign (@) */
/* --- object iconography (flat 2-D and extruded 3-D each) --- */
add_symbol(build_invader); /* space invader */
add_symbol(build_ufo); /* flying saucer */
add_symbol(build_pacman); /* pac-man */
add_symbol(build_alien); /* alien head */
add_symbol(build_pipe); /* smoking pipe */
add_symbol(build_umbrella); /* umbrella */
add_symbol(build_hand); /* hand */
/* --- live clocks: reserve 4 slots, then fill from the current time
* (refreshed every frame by update_clocks() in the main loop) --- */
for (int i = 0; i < 4; i++) { clock_idx[i] = num_shapes; new_solid(3); }
update_clocks();
/* --- 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);
update_clocks(); /* keep the clock shapes showing the current time */
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;
}