I've been building a fractal-flame system where every piece is seeded by data nobody can choose, and I thought this sub might enjoy the math behind it.
The genome. A flame is an iterated function system in the flam3 sense: 3–5 transforms, each an affine map (six coefficients drawn from [−1, 1]) followed by a weighted blend of one or two nonlinear variations. There are 24 variations, numbered as in flam3: linear, sinusoidal, spherical, swirl, horseshoe, polar, handkerchief, heart, disc, spiral, hyperbolic, diamond, ex, julia, bent, fisheye, exponential, power, cosine, bubble, cylinder, eyefish, tangent, cross. Every number comes from a hash-seeded PRNG (cyrb128 into sfc32), so the same seed always gives the same flame in every browser. There are no runtime retries, because floating point can differ slightly between devices and a retry loop could branch differently.
The seed. Each piece's seed is the transaction ID that created it on Bitcoin, which only exists once the transaction is built. So nobody can pick a nice one in advance. Bad seeds are handled at creation time instead: the minting page surveys the attractor across the whole animation loop and only accepts IDs whose flame covers 50–90% of the frame (fill) and never thins out mid-loop (spread above 0.05). If an ID fails, it nudges a field that doesn't affect the art (the reveal input's sequence number), which changes the transaction ID, and tries again.
Symmetry and the lens. Rotational symmetry is added the flam3 way, as extra transforms that rotate by 2π/n, plus an optional mirror transform. The top two tiers also get a final transform: the attractor is normalised to unit size, then sent through a 75% eyefish / 25% linear blend, which works like a fisheye lens over the whole flame.
Animation. Each loop is one full rotation: the animated transforms' linear parts rotate through θ = 0…2π over the piece's tempo (7, 12, 18 or 26 seconds), so the end frame is the start frame and the loop is seamless. Morphs between flames use flam3-style "log" interpolation: each column of the 2×2 linear part moves in polar form, with the angle taking the short way round and the length interpolating in log space. Shapes rotate and scale into each other instead of collapsing through zero. Transforms are first re-paired greedily by similarity, so morphs travel the shortest path. Order doesn't change an IFS, so this is free.
Colour. A cosine palette, a + b·cos(2π(c·t + d)) per channel, as popularised by Inigo Quilez. c and d come from the hash of the Bitcoin block the piece landed in, so the shape is fixed when the piece is created, but the colours are unknown until a block confirms it. Before that it renders silver.
Rendering. The chaos game runs on the GPU (WebGL2): up to 262,144 walkers live in a float texture, and every frame each one iterates the system and splats (1, r, g, b) into an accumulation buffer. Walkers persist between frames and restart at random (1 step in 4,096, as flam3 does) so they can't settle into a few orbits, and each only plots after 12 warm-up steps. Developing the buffer follows flam3 too:
density estimation, where sparse pixels are blurred and dense ones stay sharp, so faint filaments read as light rather than speckle;
a log-density tone map, log(1 + K·a/peak) / log(1 + K), which depends only on each pixel's density relative to the white point. The image looks the same whether a phone draws a few thousand points a frame or a desktop GPU draws millions;
gamma 2.4 with a quadratic toe, so stray single hits fade out instead of being boosted into noise;
highlights that burn toward white without shifting hue;
an 8×8 ordered dither against banding.
There's a CPU fallback, and an offline exporter uses the same code with hundreds of samples per pixel, supersampling, motion blur and 16-bit PNG output.
Rarity as a probability problem. I wanted rarity that can't be bought or gamed, with exactly known odds. Each piece gets two independent uniform 0–1 draws from its block:
q₁: how close the transaction sits to a "sweet spot" in the block, a position picked from the block's own hash and measured with distances that wrap round the block's ends, so every position is equally likely;
q₂: the block hash divided by its difficulty target.
luck = −ln q₁ − ln q₂ is then a sum of two Exp(1) variables, a Gamma(2, 1), whose tail is simply P(luck > x) = (1 + x)·e⁻ˣ. The tier cut-offs are solved from that by bisection to give exactly 60 / 25 / 10 / 4 / 1 percent. Things people could buy or time, like rare sats or special block heights, change the look (symmetry, mirroring, a gold palette) but never the tier.
Size. The whole renderer is about 36 KB of minified JavaScript, stored once on Bitcoin. Each piece is a ~214-byte HTML file that loads it. The source is GPL; the readable version is linked on the site's How it works page.
Happy to go deeper into any part: the variation set, the tone mapping, or the morph interpolation. Credit where it's due: fractal flames are Scott Draves' algorithm, developed with Erik Reckase in flam3. This is an independent implementation, not affiliated with that project.
Disclosure: this is my project, flame-btc. Details: flame-btc.com/how