// flow.c — ten thousand particles combed by an invisible field. // // cc -O2 -o flow flow.c -lm && ./flow // // There is no fluid here and nothing is simulated. A scalar noise field fills // the plane; at every point its value is read as an angle, and that angle is // the direction a particle is nudged. Thousands of particles, each just // following the arrow under its feet, smear coloured light onto a canvas that // slowly fades. The streamlines you see are emergent — the particles trace the // field the way iron filings trace a magnet. // // The field is value noise on a *toroidal* lattice, sampled in three dimensions: // two for space, one for time. Because the lattice wraps, advancing time by // exactly one full period returns the field to where it began — so the clip // loops seamlessly. (Warm the trails for one period first, then record.) // // Same house style as the path tracer and the reaction-diffusion sim next door: // one file, no dependencies, each terminal cell two stacked pixels under the // upper-half block '▀', and the same violet/magenta/cyan synthwave palette. #include #include #include #include #include // ------------------------------------------------------------------------- rng // xorshift64 — deterministic, so a given seed always combs the same way. static uint64_t rng_state = 0x2545f4914f6cdd1dULL; static double rnd(void) { uint64_t x = rng_state; x ^= x << 13; x ^= x >> 7; x ^= x << 17; rng_state = x; return (x >> 11) * (1.0 / 9007199254740992.0); // [0,1) } // ------------------------------------------------------------------ vector type typedef struct { double x, y, z; } V; static V cv(double x, double y, double z) { return (V){x, y, z}; } static V add(V a, V b) { return cv(a.x+b.x, a.y+b.y, a.z+b.z); } static V mul(V a, double s) { return cv(a.x*s, a.y*s, a.z*s); } static V mix(V a, V b, double t) { return add(mul(a, 1-t), mul(b, t)); } // ----------------------------------------------------------------- the field // Toroidal value noise. Lattice points are hashed to a scalar in [0,1); the // hash wraps modulo NLAT on every axis, so the whole field is periodic with // period NLAT in x, y and t. NLAT is a power of two so the wrap is a mask. #define NLAT 16 static double vhash(int x, int y, int z) { uint32_t h = (uint32_t)((x & (NLAT-1)) * 73856093) ^ (uint32_t)((y & (NLAT-1)) * 19349663) ^ (uint32_t)((z & (NLAT-1)) * 83492791); h ^= h >> 13; h *= 0x5bd1e995u; h ^= h >> 15; return (h & 0xffffff) / (double)0x1000000; // [0,1) } static double fade(double t) { return t*t*t*(t*(t*6-15)+10); } // smootherstep // Trilinear value noise over the wrapping lattice. static double vnoise(double x, double y, double z) { int xi = (int)floor(x), yi = (int)floor(y), zi = (int)floor(z); double xf = fade(x-xi), yf = fade(y-yi), zf = fade(z-zi); double c000=vhash(xi,yi,zi), c100=vhash(xi+1,yi,zi); double c010=vhash(xi,yi+1,zi), c110=vhash(xi+1,yi+1,zi); double c001=vhash(xi,yi,zi+1), c101=vhash(xi+1,yi,zi+1); double c011=vhash(xi,yi+1,zi+1), c111=vhash(xi+1,yi+1,zi+1); double x00=c000+(c100-c000)*xf, x10=c010+(c110-c010)*xf; double x01=c001+(c101-c001)*xf, x11=c011+(c111-c011)*xf; double y0=x00+(x10-x00)*yf, y1=x01+(x11-x01)*yf; return y0 + (y1-y0)*zf; // [0,1) } // Three octaves of value noise → a smooth angle field. Each octave doubles the // spatial/temporal frequency; because NLAT is a power of two, every octave is // still periodic over one NLAT-step in time, so the sum loops too. static double field_angle(double x, double y, double z) { double s = 0, a = 0.5, f = 1.0; for (int o = 0; o < 3; o++) { s += a*vnoise(x*f, y*f, z*f); f *= 2.0; a *= 0.5; } return s * (2.0 * M_PI * 1.6); // a little over one turn of swirl } // ----------------------------------------------------------------------- palette // Direction picks the hue (cyan for one heading, magenta for the other); the // canvas keeps a dark violet floor. Light is additive, so where streamlines // cross and pile up the colour blows out toward white — the bright cores. static int tone(double c) { if (c < 0) c = 0; c = pow(c / (c + 1.0), 1.0/2.2); int x = (int)(c*255 + 0.5); return x < 0 ? 0 : x > 255 ? 255 : x; } static void write_ppm(const char *path, V *fb, int W, int H) { FILE *fp = fopen(path, "wb"); fprintf(fp, "P6\n%d %d\n255\n", W, H); for (int i = 0; i < W*H; i++) { unsigned char px[3] = { tone(fb[i].x), tone(fb[i].y), tone(fb[i].z) }; fwrite(px, 1, 3, fp); } fclose(fp); } // ------------------------------------------------------------------- particles // Each particle has a fixed birth spot and a birth-frame phase. Once per loop, // at its phase, it snaps back to its birth spot. Because the field is periodic, // a particle reborn at the same phase in the same place retraces the identical // path every loop — so the whole swarm (and the trail it leaves) is periodic in // time, and the clip loops without a seam. The phases are spread across the loop // so the resets never happen all at once. typedef struct { double x, y, bx, by; int bf; } P; int main(int argc, char **argv) { int W = 80, H = 48; // canvas size (pixels; terminal shows H/2 rows) int SPF = 2; // particle substeps per displayed frame int FRAMES = 300; // one full loop of the time axis const char *snapshot = NULL; // --shot: warm up a period, then write one PPM const char *gifpre = NULL; // --gif: dump every frame of one loop as a PPM if (argc > 1) SPF = atoi(argv[1]); if (argc > 2) FRAMES = atoi(argv[2]); if (argc > 3 && !strcmp(argv[3], "--shot")) { W = atoi(argv[4]); H = atoi(argv[5]); snapshot = argv[6]; } if (argc > 3 && !strcmp(argv[3], "--gif")) { W = atoi(argv[4]); H = atoi(argv[5]); gifpre = argv[6]; } int NP = (int)(W * H * 0.75); // particle count scales with the canvas double SPAN = 5.0; // world units of noise across the width double STEP = 0.85; // pixels a particle moves per substep double FADEK = 0.90; // canvas retention per frame (trail life) double DEP = 0.16; // light deposited per particle per hit V *fb = malloc(sizeof(V) * W * H); // the fading canvas (accumulated light) P *par = malloc(sizeof(P) * NP); char *out = malloc(W*H*48 + 4096); for (int i = 0; i < W*H; i++) fb[i] = cv(0,0,0); // Seed birth spot + phase once (deterministic); start each particle home. for (int i = 0; i < NP; i++) { par[i].bx = par[i].x = rnd()*W; par[i].by = par[i].y = rnd()*H; par[i].bf = (int)(rnd()*FRAMES); } V floorc = cv(0.025, 0.012, 0.075); // violet substrate V hueA = cv(0.10, 0.85, 1.15); // cyan heading V hueB = cv(1.05, 0.18, 0.62); // magenta heading // Advance the whole system by one displayed frame at time index f. // (Defined as a lambda-by-macro would be cleaner, but C — so inline below.) #define ADVANCE(f) do { \ double z = (double)(f) / FRAMES * NLAT; /* wraps at f==FRAMES */ \ for (int i = 0; i < NP; i++) /* phase resets */ \ if ((f) % FRAMES == par[i].bf) { par[i].x = par[i].bx; par[i].y = par[i].by; } \ for (int i = 0; i < W*H; i++) fb[i] = mul(fb[i], FADEK); \ for (int s = 0; s < SPF; s++) { \ for (int i = 0; i < NP; i++) { \ P *p = &par[i]; \ double ang = field_angle(p->x / W * SPAN, p->y / W * SPAN, z); \ p->x += cos(ang) * STEP; p->y += sin(ang) * STEP; \ p->x = fmod(p->x + W, W); p->y = fmod(p->y + H, H); \ int cx = (int)p->x, cy = (int)p->y; \ V tint = mix(hueB, hueA, 0.5 + 0.5*sin(ang)); \ fb[cy*W + cx] = add(fb[cy*W + cx], mul(tint, DEP)); \ } \ } \ } while (0) // A hidden still: warm the trails for a full loop, then dump one frame. if (snapshot) { for (int f = 0; f < FRAMES; f++) ADVANCE(f); for (int i = 0; i < W*H; i++) fb[i] = add(fb[i], floorc); write_ppm(snapshot, fb, W, H); return 0; } // Frame dump for the WebP: prime one full period so the trails are already // alive at frame 0, then record the next period — start and end states match. if (gifpre) { for (int f = 0; f < FRAMES; f++) ADVANCE(f); // warm-up (not recorded) for (int f = 0; f < FRAMES; f++) { ADVANCE(f); V *snap = malloc(sizeof(V) * W * H); for (int i = 0; i < W*H; i++) snap[i] = add(fb[i], floorc); char path[512]; snprintf(path, sizeof path, "%s%03d.ppm", gifpre, f); write_ppm(path, snap, W, H); free(snap); fprintf(stderr, "\rframe %d/%d", f+1, FRAMES); } fprintf(stderr, "\n"); return 0; } // Live terminal playback, looping forever. printf("\033[2J\033[?25l"); fflush(stdout); for (int f = 0; ; f = (f + 1) % FRAMES) { ADVANCE(f); char *o = out; o += sprintf(o, "\033[H"); for (int y = 0; y < H; y += 2) { for (int x = 0; x < W; x++) { V top = add(fb[y*W + x], floorc); V bot = add(fb[(y+1)*W + x], floorc); o += sprintf(o, "\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm▀", tone(top.x), tone(top.y), tone(top.z), tone(bot.x), tone(bot.y), tone(bot.z)); } o += sprintf(o, "\033[0m\n"); } fwrite(out, 1, o - out, stdout); fflush(stdout); } return 0; // unreachable }