This winter is anomalously warm in Europe, there is no snow and no New Year mood. So today we will be drawing a snowflake in PostgreSQL.

#1. A little theory

Core of a snowflake is six large symmetrical ice crystals growing from the common center. Out of these larger crystals other, smaller, crystals grow.

The overall shape of the snowflake is defined by how large do crystals grow and where exactly are they attached to each other.

These things are defined by fluctuations in air and temperature conditions around the snowflake. Because the flake itself is very small, in any given moment the conditions are nearly identical around each crystal, that's why the offspring crystals pop up in almost same places and grow to almost same lengths. Different flakes, though, constantly move to and from each other and are subject to very different fluctuations, and that's why they grow so unique.

Except for the root crystals (of which there are six), the child icicles grow in symmetrical pairs. More than that, each branch grows their own children (also in pairs), so on each step there are twice as many crystals, but they all share almost same length and angle. This gives the snowflake its symmetrical look.

So we can easily see that, despite the fact there may be many child crystals, the shape of a snowflake is defined by a relatively small number of parameters: how many children each crystal produces, where are they attached to it, at which angle they grow and to which length.

Now, let's try to model it.



#2. Defining parameters

To begin with, we will assume the length of the initial larger crystal as 1, and its angle as 0. Later we may easily scale and rotate the crystal.

There can be any number of child crystals (or, rather, pairs of child crystals), each pair attaching to its parent in a random place and growing at random angle. However, this places and angles, though random, are shared across the snowflake: if one pair grows here and at this angle, the similar pairs will grow on the same place and at the same angle on their parents.

This means that there are 6 * (2 ^ level) twins of each crystal, where level is the number of its ancestors. There are 6 root crystals, 12 first-level crystals in the first first-level pairs (each growing in the same place at the same angle to the same length on root crystals), 12 first-level crystals in the second first-level pairs (again, sharing all parameters with themselves but not with the first pairs), 24 first-first grandchildren, 24 first-second grandchildren etc.

So, to define the snowflake shape, we need to define the number of pairs on each step, and for each pair define its place on the parent crystal, angle and length.

These parameters are random but have some constraints. Child crystals are shorter than their parents; they grow at sharp angles, and the number of pairs is limited. Since the parameters of each crystal depend on its parent, we would need a recursive query for that:

SELECT SETSEED(0.20111231); WITH RECURSIVE params (id, cut, len, alpha, level, spikes) AS ( SELECT ARRAY[0::INT], 0::DOUBLE PRECISION, 1::DOUBLE PRECISION, 0::DOUBLE PRECISION, 1, FLOOR(RANDOM() * 4)::INT AS spikes UNION ALL SELECT id, cut, len, alpha, level, FLOOR(RANDOM() * 4)::INT AS spikes FROM ( SELECT id || generate_series(0, spikes) AS id, RANDOM() AS cut, len * RANDOM() * 0.5 AS len, (PI() / 2) * (0.3 + RANDOM() * 0.3) AS alpha, level + 1 AS level FROM params ) q WHERE level <= 3 ) SELECT id::TEXT, cut, len, alpha, level, spikes FROM params;

id cut len alpha level spikes {0} 0 1 0 1 2 {0,0} 0.356672627385706 0.285511903231964 0,895624824686616 2 2 {0,1} 0.561545127537102 0.170363144949079 0.618425840960865 2 3 {0,2} 0.475528724957258 0.394589512841776 0.81481895943341 2 0 {0,0,0} 0.667989769019186 0.0429738523301687 0.818223701208857 3 1 {0,0,1} 0.325173982884735 0.00710072719083308 0.517746497950111 3 0 {0,0,2} 0.181089712772518 0.0441518332901016 0.789388660278232 3 0 {0,1,0} 0.272290788125247 0.0784957594457395 0.522800933603073 3 0 {0,1,1} 0.559544350020587 0.0571536974970293 0.713263419194096 3 3 {0,1,2} 0.642731287050992 0.0842548574598808 0.782757860467376 3 1 {0,1,3} 0.0450216801837087 0.0163872951631622 0.590039109739568 3 0 {0,2,0} 0.289853980299085 0.173285180190047 0.896723437835692 3 1 12 rows fetched in 0.0005s (0.0047s)

Here, id is an array defining the path to the crystal pairs: {0} is the root branch, {0, 0} is the first child pair, {0, 1, 0} is the first child of the second child etc.

cut defines the position where the crystals are attached to the parent: 0.356672627385706 in the second row means the first child will grow at 35.6% of the root branches' length.

len is the child length (in respect to the root branch, not immediate parent), angle is the angle at which they grow (in respect to the immediate parent), and spikes is the number of children pairs for the given crystal. level , I hope, is self-explanatory.

#3. Defining crystal coordinates

Now that we have the shape of our flake defined we need to build the actual coordinates for each crystal. To do this we again would need a recursive query.

First, we should make a set of branches on each level. We would start from a single root branch (it will be easy to clone it later) and generate a set of branches on each level. Each record in this set would correspond to an actual snowflake crystal:

WITH RECURSIVE params (id, cut, len, alpha, level, spikes) AS ( SELECT ARRAY[0::INT], 0::DOUBLE PRECISION, 1::DOUBLE PRECISION, 0::DOUBLE PRECISION, 1, FLOOR(RANDOM() * 4)::INT AS spikes UNION ALL SELECT id, cut, len, alpha, level, FLOOR(RANDOM() * 4)::INT AS spikes FROM ( SELECT id || generate_series(0, spikes) AS id, RANDOM() AS cut, len * RANDOM() * 0.5 AS len, (PI() / 2) * (0.3 + RANDOM() * 0.3) AS alpha, level + 1 AS level FROM params ) q WHERE level <= 3 ), tree AS ( SELECT *, generate_series(0, (1 << (level - 1)) - 1) AS branch FROM params ) SELECT id::TEXT, cut, len, alpha, level, spikes, branch FROM tree;

View query output

id cut len alpha level spikes branch {0} 0 1 0 1 2 0 {0,0} 0.356672627385706 0.285511903231964 0.895624824686616 2 2 0 {0,0} 0.356672627385706 0.285511903231964 0.895624824686616 2 2 1 {0,1} 0.561545127537102 0.170363144949079 0.618425840960865 2 3 0 {0,1} 0.561545127537102 0.170363144949079 0.618425840960865 2 3 1 {0,2} 0.475528724957258 0.394589512841776 0.81481895943341 2 0 0 {0,2} 0.475528724957258 0.394589512841776 0.81481895943341 2 0 1 {0,0,0} 0.667989769019186 0.0429738523301687 0.818223701208857 3 1 0 {0,0,0} 0.667989769019186 0.0429738523301687 0.818223701208857 3 1 1 {0,0,0} 0.667989769019186 0.0429738523301687 0.818223701208857 3 1 2 {0,0,0} 0.667989769019186 0.0429738523301687 0.818223701208857 3 1 3 {0,0,1} 0.325173982884735 0.00710072719083308 0.517746497950111 3 0 0 {0,0,1} 0.325173982884735 0.00710072719083308 0.517746497950111 3 0 1 {0,0,1} 0.325173982884735 0.00710072719083308 0.517746497950111 3 0 2 {0,0,1} 0.325173982884735 0.00710072719083308 0.517746497950111 3 0 3 {0,0,2} 0.181089712772518 0.0441518332901016 0.789388660278232 3 0 0 {0,0,2} 0.181089712772518 0.0441518332901016 0.789388660278232 3 0 1 {0,0,2} 0.181089712772518 0.0441518332901016 0.789388660278232 3 0 2 {0,0,2} 0.181089712772518 0.0441518332901016 0.789388660278232 3 0 3 {0,1,0} 0.272290788125247 0.0784957594457395 0.522800933603073 3 0 0 {0,1,0} 0.272290788125247 0.0784957594457395 0.522800933603073 3 0 1 {0,1,0} 0.272290788125247 0.0784957594457395 0.522800933603073 3 0 2 {0,1,0} 0.272290788125247 0.0784957594457395 0.522800933603073 3 0 3 {0,1,1} 0.559544350020587 0.0571536974970293 0.713263419194096 3 3 0 {0,1,1} 0.559544350020587 0.0571536974970293 0.713263419194096 3 3 1 {0,1,1} 0.559544350020587 0.0571536974970293 0.713263419194096 3 3 2 {0,1,1} 0.559544350020587 0.0571536974970293 0.713263419194096 3 3 3 {0,1,2} 0.642731287050992 0.0842548574598808 0.782757860467376 3 1 0 {0,1,2} 0.642731287050992 0.0842548574598808 0.782757860467376 3 1 1 {0,1,2} 0.642731287050992 0.0842548574598808 0.782757860467376 3 1 2 {0,1,2} 0.642731287050992 0.0842548574598808 0.782757860467376 3 1 3 {0,1,3} 0.0450216801837087 0.0163872951631622 0.590039109739568 3 0 0 {0,1,3} 0.0450216801837087 0.0163872951631622 0.590039109739568 3 0 1 {0,1,3} 0.0450216801837087 0.0163872951631622 0.590039109739568 3 0 2 {0,1,3} 0.0450216801837087 0.0163872951631622 0.590039109739568 3 0 3 {0,2,0} 0.289853980299085 0.173285180190047 0.896723437835692 3 1 0 {0,2,0} 0.289853980299085 0.173285180190047 0.896723437835692 3 1 1 {0,2,0} 0.289853980299085 0.173285180190047 0.896723437835692 3 1 2 {0,2,0} 0.289853980299085 0.173285180190047 0.896723437835692 3 1 3 39 rows fetched in 0.0015s (0.0060s)

This is just a copy of the parameters recordset with each parameter duplicated as many times as the crystal level requires. There is one root branch, two copies of level 1 branches, four copies of level 2 branches etc. Each instance is defined by the parameter branch .

To calculate coordinates of each crystal we need to traverse to it from the top.

The coordinates of the root branch are known: they are (0, 0), (1, 0) (by definition).

To build the first pair (two crystals, each 28.5% long, growing at 35.6% from the beginning of the root branch at angles 51° 18' and −51° 18', respectively) we would need to take the coordinates of the parent, find the start point (it would be (X s + (X e - X s ) * cut, Y s + (Y e - Y s ) * cut) and then the coordinates of the end point (by adding len * COS(α) and len * SIN(α) to X and Y of the start point, respectively). α in this formula is in respect to the coordinate grid, not to the parent, and to find it we just need to sum all angles from the ancestors.

This is best done with another recursive query. On each step we should find immediate children of the current parent.

This can be easily achieved using the following join condition: child.id > parent.id AND child.id <= parend.id || spikes AND ARRAY_LENGTH(child.level, 1) = ARRAY_LENGTH(parent.level, 1) + 1 . This condition employs PostgreSQL array arithmetics: if we have parent.id = {0, 2, 1} with 3 children ( spikes = 2 ), then the condition would return {0, 2, 1, 0} , {0, 2, 1, 1} and {0, 2, 1, 2} . This hierarchy model is called materialized path, and some day I will write a post about it.

Since the id alone defines the parameters, not the instance, we need to add once more condition to find the instances. Crystals 0 and 1 would have child pairs 0, 1 and 2, 3, respectively, so we'll include child.branch BETWEEN p.branch * 2 AND child.branch * 2 + 1 into the join condition.

One more thing to do is to find whether we should add positive or negative angle. It's simple: even branches are negative, odd ones are positive.

And here's the query:

SELECT SETSEED(0.20111231); WITH RECURSIVE params (id, cut, len, alpha, level, spikes) AS ( SELECT ARRAY[0::INT], 0::DOUBLE PRECISION, 1::DOUBLE PRECISION, 0::DOUBLE PRECISION, 1, FLOOR(RANDOM() * 4)::INT AS spikes UNION ALL SELECT id, cut, len, alpha, level, FLOOR(RANDOM() * 4)::INT AS spikes FROM ( SELECT id || generate_series(0, spikes) AS id, RANDOM() AS cut, len * RANDOM() * 0.5 AS len, (PI() / 2) * (0.3 + RANDOM() * 0.3) AS alpha, level + 1 AS level FROM params ) q WHERE level <= 3 ), tree AS ( SELECT *, generate_series(0, (1 << (level - 1)) - 1) AS branch FROM params ), points AS ( SELECT id, 0::double precision AS xs, 0::double precision AS ys, len AS xe, 0::double precision AS ye, 0::double precision AS alpha, branch, level, spikes FROM tree WHERE id = ARRAY[0] UNION ALL SELECT id, xs + (xe - xs) * cut AS xs, ys + (ye - ys) * cut AS ys, xs + (xe - xs) * cut + len * COS(alpha) AS xe, ys + (ye - ys) * cut + len * SIN(alpha) AS ye, alpha, branch, level, spikes FROM ( SELECT t.id, p.id || t.id AS uid, xs, xe, ys, ye, cut, len, p.alpha + t.alpha * ((t.branch % 2) * 2 - 1) AS alpha, t.branch, t.level, t.spikes FROM points p JOIN tree t ON t.id > p.id AND t.id <= (p.id || p.spikes) AND ARRAY_LENGTH(t.id, 1) = p.level + 1 AND t.branch BETWEEN p.branch * 2 AND p.branch * 2 + 1 ) q ) SELECT id::TEXT, xs, ys, xe, ye, alpha, branch, level FROM points

View query output

id xs ys xe ye alpha branch level {0} 0 0 1 0 0 0 1 {0,0} 0.356672627385706 0 0.535126474998426 -0.222870525550944 -0.895624824686616 0 2 {0,0} 0.356672627385706 0 0.535126474998426 0.222870525550944 0.895624824686616 1 2 {0,1} 0.561545127537102 0 0.700355670409353 -0.0987685898676881 -0.618425840960865 0 2 {0,1} 0.561545127537102 0 0.700355670409353 0.0987685898676881 0.618425840960865 1 2 {0,2} 0.475528724957258 0 0.746217182091292 -0.287103888547519 -0.81481895943341 0 2 {0,2} 0.475528724957258 0 0.746217182091292 0.287103888547519 0.81481895943341 1 2 {0,0,0} 0.475877971833112 -0.14887523088396 0.469751413327762 -0.191410125558517 -1.71384852589547 0 3 {0,0,0} 0.475877971833112 -0.14887523088396 0.518723161661866 -0.152198135130716 -0.0774011234777593 1 3 {0,0,0} 0.475877971833112 0.14887523088396 0.518723161661866 0.152198135130716 0.0774011234777593 2 3 {0,0,0} 0.475877971833112 0.14887523088396 0.469751413327762 0.191410125558517 1.71384852589547 3 3 {0,0,1} 0.414701175775039 -0.0724716964610147 0.415814396363913 -0.0794846178607699 -1.41337132263673 0 3 {0,0,1} 0.414701175775039 -0.0724716964610147 0.421300943231765 -0.0750915048806874 -0.377878326736505 1 3 {0,0,1} 0.414701175775039 0.0724716964610147 0.421300943231765 0.0750915048806874 0.377878326736505 2 3 {0,0,1} 0.414701175775039 0.0724716964610147 0.415814396363913 0.0794846178607699 1.41337132263673 3 3 {0,0,2} 0.388988783393044 -0.0403595594574808 0.383956843885346 -0.0842237130189914 -1.68501348496485 0 3 {0,0,2} 0.388988783393044 -0.0403595594574808 0.432891699422156 -0.0450412628886793 -0.106236164408384 1 3 {0,0,2} 0.388988783393044 0.0403595594574808 0.432891699422156 0.0450412628886793 0.106236164408384 2 3 {0,0,2} 0.388988783393044 0.0403595594574808 0.383956843885346 0.0842237130189914 1.68501348496485 3 3 {0,1,0} 0.59934195965588 -0.0268937771770921 0.632033834423018 -0.0982578127634654 -1.14122677456394 0 3 {0,1,0} 0.59934195965588 -0.0268937771770921 0.677479105058146 -0.0343884926052126 -0.0956249073577922 1 3 {0,1,0} 0.59934195965588 0.0268937771770921 0.677479105058146 0.0343884926052126 0.0956249073577922 2 3 {0,1,0} 0.59934195965588 0.0268937771770921 0.632033834423018 0.0982578127634654 1.14122677456394 3 3 {0,1,1} 0.63921578252456 -0.0552654064199655 0.652751789427464 -0.11079307208949 -1.33168926015496 0 3 {0,1,1} 0.63921578252456 -0.0552654064199655 0.696112647679181 -0.0498532097163336 0.0948375782332311 1 3 {0,1,1} 0.63921578252456 0.0552654064199655 0.696112647679181 0.0498532097163336 -0.0948375782332311 2 3 {0,1,1} 0.63921578252456 0.0552654064199655 0.652751789427464 0.11079307208949 1.33168926015496 3 3 {0,1,2} 0.650763006413631 -0.0634816628858708 0.664985272342964 -0.146527482512268 -1.40118370142824 0 3 {0,1,2} 0.650763006413631 -0.0634816628858708 0.733882770016532 -0.0496981254523029 0.164332019506511 1 3 {0,1,2} 0.650763006413631 0.0634816628858708 0.733882770016532 0.0496981254523029 -0.164332019506511 2 3 {0,1,2} 0.650763006413631 0.0634816628858708 0.664985272342964 0.146527482512268 1.40118370142824 3 3 {0,1,3} 0.567794611404423 -0.00444672786521894 0.57360317341221 -0.0197700450702597 -1.20846495070043 0 3 {0,1,3} 0.567794611404423 -0.00444672786521894 0.584175304516377 -0.00491184713656396 -0.028386731221297 1 3 {0,1,3} 0.567794611404423 0.00444672786521894 0.584175304516377 0.00491184713656396 0.028386731221297 2 3 {0,1,3} 0.567794611404423 0.00444672786521894 0.57360317341221 0.0197700450702597 1.20846495070043 3 3 {0,2,0} 0.553988851678576 -0.0832182048548433 0.529680086603184 -0.25478987388562 -1.7115423972691 0 3 {0,2,0} 0.553988851678576 -0.0832182048548433 0.726693128455993 -0.0690412356340923 0.0819044784022824 1 3 {0,2,0} 0.553988851678576 0.0832182048548433 0.726693128455993 0.0690412356340923 -0.0819044784022824 2 3 {0,2,0} 0.553988851678576 0.0832182048548433 0.529680086603184 0.25478987388562 1.7115423972691 3 3 39 rows fetched in 0.0017s (0.0100s)

Now, we have coordinates for each crystal.

#4. Visualizing

The most tedious part about SQL graphics is visualizing them. To do this, we'll employ PostgreSQL's geometrical functions.

Each crystal can be represented as a path between its start and end points. This can be used by constructing a line segment ( LSEG(POINT, POINT) ) using two point constructors ( POINT(DOUBLE PRECISION, DOUBLE PRECISION) ) and converting it to a path. Unfortunately, PostgreSQL does not allow direct lseg to path conversion but the latter can be easily constructed from the TEXT representation of an lseg .

We have six root branches so each crystal should be cloned to make six copies. It's easiest to make it by using PATH * POINT operator: it rotates and scales the PATH around (0, 0) so that (1, 0) becomes POINT . To construct the points, we will generate six rotation angles with step of 60° and will multiply the path by POINT(COS(α), SIN(α)) . These multiplications will preserve lengths.

Finally we need to actually display the snowflake on the screen. To do this, we will generate a set of 80 × 80 records( x and y ), defining the grid from (-1, -1) to (1, 1) with step of 1/40 units. Then we'll see if there is at least one crystal within distance of 1/40 units from each cell on the grid (using POINT <-> PATH distance operator and EXISTS ). If there is, we will return a number sign ( # ) for this cell, otherwise a space.

Then we'll group the cells by lines ( y ), concatenate columns (using ARRAY_AGG and ARRAY_TO_STRING ) and output the lines.

#5. The snowflake

And here's our snowflake:

SELECT SETSEED(0.20111231); WITH RECURSIVE params (id, cut, len, alpha, level, spikes) AS ( SELECT ARRAY[0::INT], 0::DOUBLE PRECISION, 1::DOUBLE PRECISION, 0::DOUBLE PRECISION, 1, FLOOR(RANDOM() * 4)::INT AS spikes UNION ALL SELECT id, cut, len, alpha, level, FLOOR(RANDOM() * 4)::INT AS spikes FROM ( SELECT id || generate_series(0, spikes) AS id, RANDOM() AS cut, len * RANDOM() * 0.5 AS len, (PI() / 2) * (0.3 + RANDOM() * 0.3) AS alpha, level + 1 AS level FROM params ) q WHERE level <= 3 ), tree AS ( SELECT *, generate_series(0, (1 << (level - 1)) - 1) AS branch FROM params ), points AS ( SELECT id, 0::double precision AS xs, 0::double precision AS ys, len AS xe, 0::double precision AS ye, 0::double precision AS alpha, branch, level, spikes FROM tree WHERE id = ARRAY[0] UNION ALL SELECT id, xs + (xe - xs) * cut AS xs, ys + (ye - ys) * cut AS ys, xs + (xe - xs) * cut + len * COS(alpha) AS xe, ys + (ye - ys) * cut + len * SIN(alpha) AS ye, alpha, branch, level, spikes FROM ( SELECT t.id, p.id || t.id AS uid, xs, xe, ys, ye, cut, len, p.alpha + t.alpha * ((t.branch % 2) * 2 - 1) AS alpha, t.branch, t.level, t.spikes FROM points p JOIN tree t ON t.id > p.id AND t.id <= (p.id || p.spikes) AND ARRAY_LENGTH(t.id, 1) = p.level + 1 AND t.branch BETWEEN p.branch * 2 AND p.branch * 2 + 1 ) q ), lines AS ( SELECT PATH(LSEG(POINT(xs, ys), POINT(xe, ye))::TEXT) * POINT(COS(RADIANS(turn)), SIN(RADIANS(turn))) AS line, * FROM points CROSS JOIN generate_series(0, 300, 60) AS turn ) SELECT ARRAY_TO_STRING ( ARRAY_AGG ( CASE EXISTS ( SELECT NULL FROM lines WHERE POINT(x::DOUBLE PRECISION / scale, y::DOUBLE PRECISION / scale) <-> line <= (0.7 / scale) ) WHEN TRUE THEN '#' ELSE ' ' END ORDER BY x ), '' ) FROM ( SELECT *, generate_series(-scale, scale - 1) AS y FROM ( SELECT scale, generate_series(-scale, scale - 1) AS x FROM ( VALUES (40) ) q (scale) ) q ) q GROUP BY y ORDER BY y

array_to_string # # ## ## # # ## # # ## # # # # ## ## ## ## ## # # ## # # # # ## ## ## ## ## ## # ######## ######## # ### #### # # #### ### ## ###### # # # # ###### ## ########### ### ### ########### ################ ################ ####### ###### ###### ####### #### ######### ## ### ### ## ######### #### ###### ## ## # ## ## # ## ## ###### ####### ### # # ### ####### ####### ## ## ####### ### ## ### ### ## ### #### ##### ##### #### ###### # ### ### # ###### ## ########## ########## ## # # ##### ##### # # ## ## ## ## ## # ## ## # ## ## ## ## ## ## ## ## #### ## ## #### ## ## #### # # #### ## #### #### ## ## #### #### ## ### ## # # ## ### ## #### ## ## ## ## ## ## #### ######### ### ## ## ### ######### ####### ## ### # # ### ## ####### ##### # ## ### ## # ##### ################################################################################ ##### # ## ### ## # ##### ####### ## ### # # ### ## ####### ######### ### ## ## ### ######### #### ## ## ## ## ## ## #### ## ### ## # # ## ### ## #### #### ## ## #### #### ## #### # # #### ## ## #### ## ## #### ## ## ## ## ## ## ## ## # ## ## # ## ## ## ## ## # # ##### ##### # # ## ########## ########## ## ###### # ### ### # ###### #### ##### ##### #### ### ## ### ### ## ### ####### ## ## ####### ####### ### # # ### ####### ###### ## ## # ## ## # ## ## ###### #### ######### ## ### ### ## ######### #### ####### ###### ###### ####### ################ ################ ########### ### ### ########### ## ###### # # # # ###### ## ### #### # # #### ### # ######## ######## # ## ## ## ## ## ## # # # # ## # # ## ## ## ## ## # # # # ## # # ## # # ## ## # # 80 rows fetched in 0.0010s (2.7344s)

#6. Some more unique snowflakes

View query

SELECT SETSEED(0.201112311); WITH RECURSIVE params (id, cut, len, alpha, level, spikes) AS ( SELECT ARRAY[0::INT], 0::DOUBLE PRECISION, 1::DOUBLE PRECISION, 0::DOUBLE PRECISION, 1, FLOOR(RANDOM() * 4)::INT AS spikes UNION ALL SELECT id, cut, len, alpha, level, FLOOR(RANDOM() * 4)::INT AS spikes FROM ( SELECT id || generate_series(0, spikes) AS id, RANDOM() AS cut, len * RANDOM() * 0.5 AS len, (PI() / 2) * (0.3 + RANDOM() * 0.3) AS alpha, level + 1 AS level FROM params ) q WHERE level <= 3 ), tree AS ( SELECT *, generate_series(0, (1 << (level - 1)) - 1) AS branch FROM params ), points AS ( SELECT id, 0::double precision AS xs, 0::double precision AS ys, len AS xe, 0::double precision AS ye, 0::double precision AS alpha, branch, level, spikes FROM tree WHERE id = ARRAY[0] UNION ALL SELECT id, xs + (xe - xs) * cut AS xs, ys + (ye - ys) * cut AS ys, xs + (xe - xs) * cut + len * COS(alpha) AS xe, ys + (ye - ys) * cut + len * SIN(alpha) AS ye, alpha, branch, level, spikes FROM ( SELECT t.id, p.id || t.id AS uid, xs, xe, ys, ye, cut, len, p.alpha + t.alpha * ((t.branch % 2) * 2 - 1) AS alpha, t.branch, t.level, t.spikes FROM points p JOIN tree t ON t.id > p.id AND t.id <= (p.id || p.spikes) AND ARRAY_LENGTH(t.id, 1) = p.level + 1 AND t.branch BETWEEN p.branch * 2 AND p.branch * 2 + 1 ) q ), lines AS ( SELECT PATH(LSEG(POINT(xs, ys), POINT(xe, ye))::TEXT) * POINT(COS(RADIANS(turn)), SIN(RADIANS(turn))) AS line, * FROM points CROSS JOIN generate_series(0, 300, 60) AS turn ) SELECT ARRAY_TO_STRING ( ARRAY_AGG ( CASE EXISTS ( SELECT NULL FROM lines WHERE POINT(x::DOUBLE PRECISION / scale, y::DOUBLE PRECISION / scale) <-> line <= (0.7 / scale) ) WHEN TRUE THEN '#' ELSE ' ' END ORDER BY x ), '' ) FROM ( SELECT *, generate_series(-scale, scale - 1) AS y FROM ( SELECT scale, generate_series(-scale, scale - 1) AS x FROM ( VALUES (40) ) q (scale) ) q ) q GROUP BY y ORDER BY y

array_to_string #### #### ###### ###### #### #### ## ## # # # # # # ## ## ## ## # ## ## # # ## # # ## # ## # ## ## # ## ## ## ## ## ## ## ## ## ###### ## # # ## ###### ######## # # # # ######## ## ###### ### ### ###### ## # ###### ### ###### # ## ### ## # # # ## ### ## ## # ### # ## # ## ##### ## # ## ### ### ### ## # ##### ##### # ## #### #### ## # ##### ##### # ## ########### ## #### ## ########### ## #### # #### # #### #### # #### # # ### #### ## #### #### ## #### ### # # ####### #### # ## # # ## # #### ####### # # # ######## ## ## # ### # ## ## ######## # ### ## ####### ### ### ### ####### ## ## ### ### ############# # ############# ### ## ## ## ###### ############### ###### ## ## ## #### ##### ## ## # ## ## # ## ## ##### #### ## ## ######### # ############# # ######### ## ## #### ######### ########### ######### #### ## ## ####### ######## ######## ####### ## ## #### ####### ####### #### ## ## ######## # # ######## ## ## ## ### ### ### ## ## ################################################################################ ## ## ### ### ### ## ## ## ######## # # ######## ## ## #### ####### ####### #### ## ## ####### ######## ######## ####### ## ## #### ######### ########### ######### #### ## ## ######### # ############# # ######### ## ## #### ##### ## ## # ## ## # ## ## ##### #### ## ## ## ###### ############### ###### ## ## ### ### ############# # ############# ### ## ### ## ####### ### ### ### ####### ## ## # # ######## ## ## # ### # ## ## ######## # # ####### #### # ## # # ## # #### ####### # # ### #### ## #### #### ## #### ### # # #### # #### #### # #### # #### ## ########### ## #### ## ########### ## # ##### ##### # ## #### #### ## # ##### ##### # ## ### ### ### ## # ## ##### ## # ## # ### # ## ## ### ## # # # ## ### ## # ###### ### ###### # ## ###### ### ### ###### ## ######## # # # # ######## ###### ## # # ## ###### ## ## ## ## ## ## ## ## ## # ## ## # ## # ## # # ## # # ## ## # ## ## ## ## # # # # # # ## ## #### #### ###### ###### 80 rows fetched in 0.0010s (2.0781s)

View query

SELECT SETSEED(0.201112312); WITH RECURSIVE params (id, cut, len, alpha, level, spikes) AS ( SELECT ARRAY[0::INT], 0::DOUBLE PRECISION, 1::DOUBLE PRECISION, 0::DOUBLE PRECISION, 1, FLOOR(RANDOM() * 4)::INT AS spikes UNION ALL SELECT id, cut, len, alpha, level, FLOOR(RANDOM() * 4)::INT AS spikes FROM ( SELECT id || generate_series(0, spikes) AS id, RANDOM() AS cut, len * RANDOM() * 0.5 AS len, (PI() / 2) * (0.3 + RANDOM() * 0.3) AS alpha, level + 1 AS level FROM params ) q WHERE level <= 3 ), tree AS ( SELECT *, generate_series(0, (1 << (level - 1)) - 1) AS branch FROM params ), points AS ( SELECT id, 0::double precision AS xs, 0::double precision AS ys, len AS xe, 0::double precision AS ye, 0::double precision AS alpha, branch, level, spikes FROM tree WHERE id = ARRAY[0] UNION ALL SELECT id, xs + (xe - xs) * cut AS xs, ys + (ye - ys) * cut AS ys, xs + (xe - xs) * cut + len * COS(alpha) AS xe, ys + (ye - ys) * cut + len * SIN(alpha) AS ye, alpha, branch, level, spikes FROM ( SELECT t.id, p.id || t.id AS uid, xs, xe, ys, ye, cut, len, p.alpha + t.alpha * ((t.branch % 2) * 2 - 1) AS alpha, t.branch, t.level, t.spikes FROM points p JOIN tree t ON t.id > p.id AND t.id <= (p.id || p.spikes) AND ARRAY_LENGTH(t.id, 1) = p.level + 1 AND t.branch BETWEEN p.branch * 2 AND p.branch * 2 + 1 ) q ), lines AS ( SELECT PATH(LSEG(POINT(xs, ys), POINT(xe, ye))::TEXT) * POINT(COS(RADIANS(turn)), SIN(RADIANS(turn))) AS line, * FROM points CROSS JOIN generate_series(0, 300, 60) AS turn ) SELECT ARRAY_TO_STRING ( ARRAY_AGG ( CASE EXISTS ( SELECT NULL FROM lines WHERE POINT(x::DOUBLE PRECISION / scale, y::DOUBLE PRECISION / scale) <-> line <= (0.7 / scale) ) WHEN TRUE THEN '#' ELSE ' ' END ORDER BY x ), '' ) FROM ( SELECT *, generate_series(-scale, scale - 1) AS y FROM ( SELECT scale, generate_series(-scale, scale - 1) AS x FROM ( VALUES (40) ) q (scale) ) q ) q GROUP BY y ORDER BY y

array_to_string ## ## ## ## ## ## ##### ## ## ##### #### # # #### ######## # # ######## ##### ## ## # # ## ## ##### # ### # # # # ### # ## ### ### ## ##### ##### ### ### # # ## ## ## ## # # ## # # # # ## # # ### # # # ### # # ## ###### # ### # ###### ## # ### # #### #### # ### # ## ## #### #### ## ## ## ## ### ## ## ### ## ## ### # # ## ## # # ### ##### ## # ## ## # ## ##### ###### # ## # # ## # ###### # ### #### # # #### ### # ## # ####### # # ####### # ## ##### ##### #### #### ##### ##### #### ######## ######## #### ####### ## # ## ## # ## ####### ### ##### #### #### ##### ### # ### ### ### ### # # ######## ######## # ## # ## ### ### ## # ## #### ##### ##### #### ### ##### ##### ### ######## ##### ##### ######## ### # ## ## #### #### ## ## # ### ### ## # # # # ## ### ##### #### ## ## #### ##### ###### ## ## ## ## ###### ## ######### ## # # ## ######### # ### ### ### #### ### #### ### ### ### ################################################################################ ### ### ### #### ### #### ### ### ### ## ######### ## # # ## ######### # ###### ## ## ## ## ###### ##### #### ## ## #### ##### ### ## # # # # ## ### ### # ## ## #### #### ## ## # ### ######## ##### ##### ######## ### ##### ##### ### #### ##### ##### #### ## # ## ### ### ## # ## # ######## ######## # # ### ### ### ### # ### ##### #### #### ##### ### ####### ## # ## ## # ## ####### #### ######## ######## #### ##### ##### #### #### ##### ##### ## # ####### # # ####### # ## # ### #### # # #### ### # ###### # ## # # ## # ###### ##### ## # ## ## # ## ##### ### # # ## ## # # ### ## ## ### ## ## ### ## ## ## ## #### #### ## ## # ### # #### #### # ### # ## ###### # ### # ###### ## # # ### # # # ### # # ## # # # # ## # # ## ## ## ## # # ### ### ##### ##### ## ### ### ## # ### # # # # ### # ##### ## ## # # ## ## ##### ######## # # ######## #### # # #### ##### ## ## ##### 80 rows fetched in 0.0009s (4.4531s)

View query

SELECT SETSEED(0.201112313); WITH RECURSIVE params (id, cut, len, alpha, level, spikes) AS ( SELECT ARRAY[0::INT], 0::DOUBLE PRECISION, 1::DOUBLE PRECISION, 0::DOUBLE PRECISION, 1, FLOOR(RANDOM() * 4)::INT AS spikes UNION ALL SELECT id, cut, len, alpha, level, FLOOR(RANDOM() * 4)::INT AS spikes FROM ( SELECT id || generate_series(0, spikes) AS id, RANDOM() AS cut, len * RANDOM() * 0.5 AS len, (PI() / 2) * (0.3 + RANDOM() * 0.3) AS alpha, level + 1 AS level FROM params ) q WHERE level <= 3 ), tree AS ( SELECT *, generate_series(0, (1 << (level - 1)) - 1) AS branch FROM params ), points AS ( SELECT id, 0::double precision AS xs, 0::double precision AS ys, len AS xe, 0::double precision AS ye, 0::double precision AS alpha, branch, level, spikes FROM tree WHERE id = ARRAY[0] UNION ALL SELECT id, xs + (xe - xs) * cut AS xs, ys + (ye - ys) * cut AS ys, xs + (xe - xs) * cut + len * COS(alpha) AS xe, ys + (ye - ys) * cut + len * SIN(alpha) AS ye, alpha, branch, level, spikes FROM ( SELECT t.id, p.id || t.id AS uid, xs, xe, ys, ye, cut, len, p.alpha + t.alpha * ((t.branch % 2) * 2 - 1) AS alpha, t.branch, t.level, t.spikes FROM points p JOIN tree t ON t.id > p.id AND t.id <= (p.id || p.spikes) AND ARRAY_LENGTH(t.id, 1) = p.level + 1 AND t.branch BETWEEN p.branch * 2 AND p.branch * 2 + 1 ) q ), lines AS ( SELECT PATH(LSEG(POINT(xs, ys), POINT(xe, ye))::TEXT) * POINT(COS(RADIANS(turn)), SIN(RADIANS(turn))) AS line, * FROM points CROSS JOIN generate_series(0, 300, 60) AS turn ) SELECT ARRAY_TO_STRING ( ARRAY_AGG ( CASE EXISTS ( SELECT NULL FROM lines WHERE POINT(x::DOUBLE PRECISION / scale, y::DOUBLE PRECISION / scale) <-> line <= (0.7 / scale) ) WHEN TRUE THEN '#' ELSE ' ' END ORDER BY x ), '' ) FROM ( SELECT *, generate_series(-scale, scale - 1) AS y FROM ( SELECT scale, generate_series(-scale, scale - 1) AS x FROM ( VALUES (40) ) q (scale) ) q ) q GROUP BY y ORDER BY y

array_to_string # # ## ## # # ## ## # # ## # # ## ### ### ###### ###### #### #### ## ## ## ## # # ## ## ## ## # # ## ## # # ## ## # # ## ## ## ## # # ## ## # # ## ## ## ## ## ## ## ## # # ## ## # # ## ## ## ## ### # # ### ### ### ### ################################################################################ ### ### ### ### # # ### ## ## ## ## # # ## ## # # ## ## ## ## ## ## ## ## # # ## ## # # ## ## ## ## # # ## ## # # ## ## # # ## ## ## ## # # ## ## ## ## #### #### ###### ###### ### ### ## # # ## # # ## ## # # ## ## # # 80 rows fetched in 0.0009s (1.2188s)

Happy New Year!

Previous New Year posts: