Adapted from michaelfogleman.com/static/snowflakes/
Log in to post a comment.
// Forked from "Snowflakes" by llemarie
// https://turtletoy.net/turtle/f89ca289be
// Ported from Michael Fogleman's generative snowflakes web app:
// https://www.michaelfogleman.com/static/snowflakes/
// https://twitter.com/FogleBird/status/1356082505699500034?s=20
var rows = 1; // will be calculated in setup
var cols = rows;
const show_seeds = 0; // min=0, max=1, step=1
const predefined_seeds = 0; // min=0, max=1000, step=1
const default_iterations = 19; // min=3, max=100, step=1
const default_minCells = 32; // min=10, max=100, step=1
const default_off_bias = 1.0; // min=0.1, max=2, step=0.1
const default_on_bias = 1.0; // min=0.1, max=2, step=0.1
var global_scale = 1;
const saved_min_max_seeds2 = [
// iterations, cells, off, on, seed
[6, 32, 1, 1, 385],
];
var off_bias = default_off_bias;
var on_bias = default_on_bias;
var line_count = 0;
const lookup = [
0, 1, 1, 2, 1, 3, 2, 4,
1, 5, 3, 6, 2, 6, 4, 7,
1, 3, 5, 6, 3, 8, 6, 9,
2, 6, 6, 10, 4, 9, 7, 11,
1, 2, 3, 4, 5, 6, 6, 7,
3, 6, 8, 9, 6, 10, 9, 11,
2, 4, 6, 7, 6, 9, 10, 11,
4, 7, 9, 11, 7, 11, 11, 12,
];
function pack(p) {
return p[0] + "," + p[1];
}
function unpack(p) {
const s = p.split(",");
const x = parseInt(s[0]);
const y = parseInt(s[1]);
return [x, y];
}
function add(cells, p) {
cells.add(pack(p));
}
function has(cells, p) {
return cells.has(pack(p));
}
function mask(cells, p) {
const x = p[0];
const y = p[1];
let result = 0;
if (has(cells, [x + 1, y])) {
result |= 1;
}
if (has(cells, [x + 1, y - 1])) {
result |= 2;
}
if (has(cells, [x, y - 1])) {
result |= 4;
}
if (has(cells, [x - 1, y])) {
result |= 8;
}
if (has(cells, [x - 1, y + 1])) {
result |= 16;
}
if (has(cells, [x, y + 1])) {
result |= 32;
}
return result;
}
function neighbors(p) {
const x = p[0];
const y = p[1];
let result = [];
result.push([x + 1, y]);
result.push([x + 1, y - 1]);
result.push([x, y - 1]);
result.push([x - 1, y]);
result.push([x - 1, y + 1]);
result.push([x, y + 1]);
return result;
}
function neighborhood(cells) {
let result = new Set();
for (let p of cells) {
p = unpack(p);
const x = p[0];
const y = p[1];
add(result, [x, y]);
add(result, [x + 1, y]);
add(result, [x + 1, y - 1]);
add(result, [x, y - 1]);
add(result, [x - 1, y]);
add(result, [x - 1, y + 1]);
add(result, [x, y + 1]);
}
return result;
}
function step(cells, off, on) {
let offMask = 0;
let onMask = 0;
for (let i = 0; i < 13; i++) {
if (random() < off[i]) {
offMask |= 1 << i;
}
if (random() < on[i]) {
onMask |= 1 << i;
}
}
let result = new Set();
for (let p of neighborhood(cells)) {
p = unpack(p);
const bit = 1 << lookup[mask(cells, p)];
if (has(cells, p)) {
if ((offMask & bit) == 0) {
add(result, p);
}
} else {
if ((onMask & bit) != 0) {
add(result, p);
}
}
}
return result;
}
function generate(iterations, minCells) {
while (true) {
let off = [];
let on = [];
for (let i = 0; i < 13; i++) {
off.push(random() * off_bias);
on.push(random() * on_bias);
}
let cells = new Set();
add(cells, [0, 0]);
let history = [];
history.push(edges(cells));
for (let i = 0; i < iterations; i++) {
cells = step(cells, off, on);
history.push(edges(cells));
if (cells.size < 2) {
break;
}
}
if (hasIsolatedCells(cells)) {
continue;
}
if (cells.size >= minCells) {
return history;
}
}
}
function cartesian(p) {
const size = 1;
const q = parseInt(p[0]);
const r = parseInt(p[1]);
const s = Math.sqrt(3);
const x = s * q + s / 2 * r;
const y = 3.0 / 2 * r;
return [x * size, y * size];
}
function less(p, q) {
if (p[0] === q[0]) {
return p[1] < q[1];
}
return p[0] < q[0];
}
function hasIsolatedCells(cells) {
for (let p of cells) {
p = unpack(p);
let ok = false;
for (let q of neighbors(p)) {
if (has(cells, q)) {
ok = true;
break;
}
}
if (!ok) {
return true;
}
}
return false;
}
function edges(cells) {
let result = [];
for (let p of cells) {
p = unpack(p);
let ok = false;
for (let q of neighbors(p)) {
if (!has(cells, q)) {
continue;
}
ok = true;
if (!less(p, q)) {
continue;
}
result.push([cartesian(p), cartesian(q)]);
}
if (!ok) {
result.push([cartesian(p), cartesian(p)]);
}
}
return mergeEdges(result);
}
function packEdge(edge) {
const a = edge[0];
const b = edge[1];
return a[0] + "," + a[1] + "," + b[0] + "," + b[1];
}
function nextEdge(lookup, edge) {
const a = edge[0];
const b = edge[1];
const nx = b[0] + (b[0] - a[0]);
const ny = b[1] + (b[1] - a[1]);
for (let p of lookup.get(pack(b))) {
const ex = Math.abs(nx - p[0]);
const ey = Math.abs(ny - p[1]);
if ((ex < 1e-6) && (ey < 1e-6)) {
return [b, p];
}
}
return null;
}
function mergeEdges(edges) {
let lookup = new Map();
for (let edge of edges) {
lookup.set(pack(edge[0]), []);
lookup.set(pack(edge[1]), []);
}
for (let edge of edges) {
if (pack(edge[0]) === pack(edge[1])) {
continue;
}
lookup.get(pack(edge[0])).push(edge[1]);
lookup.get(pack(edge[1])).push(edge[0]);
}
let result = [];
let seen = new Set();
for (let edge of edges) {
const a = edge[0];
const b = edge[1];
if (seen.has(packEdge([a, b])) || seen.has(packEdge([b, a]))) {
continue;
}
let p0 = a;
let p1 = b;
// forward
let e = [a, b];
while (true) {
seen.add(packEdge(e));
p1 = e[1];
e = nextEdge(lookup, e);
if (!e) {
break;
}
}
// reverse
e = [b, a];
while (true) {
seen.add(packEdge(e));
p0 = e[1];
e = nextEdge(lookup, e);
if (!e) {
break;
}
}
result.push([p0, p1]);
}
return result;
}
function distance2(x1, y1, x2, y2)
{
const dx = x1 - x2;
const dy = y1 - y2;
return dx*dx + dy*dy;
}
const cosA = Math.cos(Math.PI/6);
const sinA = Math.sin(Math.PI/6);
function drawEdges(edges, scale, offset_x, offset_y) {
edges_to_sort = [];
edges_sorted = [];
for (let e of edges) { edges_to_sort.push(e); }
var current_x = 0;
var current_y = 0;
while (edges_to_sort.length > 0)
{
var min_distance = -1;
var best_index = -1;
var invert = false;
var index = 0;
var epsilon = 0.1;
for (let e of edges_to_sort)
{
var penalty = distance2(0, 0, e[0][0], e[0][1]) * 0.1;
var dist2 = distance2(current_x, current_y, e[0][0], e[0][1]);
if (dist2 > epsilon) dist2 += penalty;
if (best_index < 0 || min_distance > dist2)
{
best_index = index;
min_distance = dist2;
invert = false;
}
penalty = distance2(0, 0, e[1][0], e[1][1]) * 0.1;
dist2 = distance2(current_x, current_y, e[1][0], e[1][1]);
if (dist2 > epsilon) dist2 += penalty;
if (best_index < 0 || min_distance > dist2)
{
best_index = index;
min_distance = dist2;
invert = true;
}
index++;
}
if (best_index >= 0)
{
var edge = edges_to_sort[best_index];
edges_to_sort.splice(best_index, 1);
if (invert)
{
for (let i = 0; i < 2; i++)
{
var tmp = edge[0][i];
edge[0][i] = edge[1][i];
edge[1][i] = tmp;
}
}
current_x = edge[1][0];
current_y = edge[1][1];
edges_sorted.push(edge);
}
}
for (let e of edges_sorted) {
line_count++;
if (e[0][0] === e[1][0] && e[0][1] === e[1][1]) {
const p = 0.02;
var x0 = e[0][0]-p;
var y0 = e[0][1]-p;
var x1 = e[1][0]+p;
var y1 = e[1][1]+p;
var x2 = e[0][0]-p;
var y2 = e[0][1]+p;
var x3 = e[1][0]+p;
var y3 = e[1][1]-p;
var rx0 = cosA * x0 - sinA * y0;
var ry0 = sinA * x0 + cosA * y0;
var rx1 = cosA * x1 - sinA * y1;
var ry1 = sinA * x1 + cosA * y1;
var rx2 = cosA * x2 - sinA * y2;
var ry2 = sinA * x2 + cosA * y2;
var rx3 = cosA * x3 - sinA * y3;
var ry3 = sinA * x3 + cosA * y3;
x0 = rx0 * scale * global_scale + offset_x * global_scale * 40;
y0 = ry0 * scale * global_scale + offset_y * global_scale * 40;
x1 = rx1 * scale * global_scale + offset_x * global_scale * 40;
y1 = ry1 * scale * global_scale + offset_y * global_scale * 40;
x2 = rx2 * scale * global_scale + offset_x * global_scale * 40;
y2 = ry2 * scale * global_scale + offset_y * global_scale * 40;
x3 = rx3 * scale * global_scale + offset_x * global_scale * 40;
y3 = ry3 * scale * global_scale + offset_y * global_scale * 40;
turtle.goto(x0, y0);
turtle.pendown();
turtle.goto(x1, y1);
turtle.goto(x2, y2);
turtle.goto(x3, y3);
turtle.penup();
} else {
var x0 = e[0][0];
var y0 = e[0][1];
var x1 = e[1][0];
var y1 = e[1][1];
var rx0 = cosA * x0 - sinA * y0;
var ry0 = sinA * x0 + cosA * y0;
var rx1 = cosA * x1 - sinA * y1;
var ry1 = sinA * x1 + cosA * y1;
x0 = rx0 * scale * global_scale + offset_x * global_scale * 40;
y0 = ry0 * scale * global_scale + offset_y * global_scale * 40;
x1 = rx1 * scale * global_scale + offset_x * global_scale * 40;
y1 = ry1 * scale * global_scale + offset_y * global_scale * 40;
turtle.goto(x0, y0);
turtle.pendown();
turtle.goto(x1, y1);
turtle.penup();
}
}
}
function Snowflake(id) {
this.id = id;
if (predefined_seeds > 0) this.seed = this.id + predefined_seeds * 1000;
else this.seed = Math.floor(9999999999 * Math.random());
this.iterations = default_iterations;
this.minCells = default_minCells;
if (typeof saved_min_max_seeds !== 'undefined' && saved_min_max_seeds.length > this.id)
{
this.iterations = saved_min_max_seeds[this.id][0];
// skip 1
this.minCells = saved_min_max_seeds[this.id][2];
off_bias = saved_min_max_seeds[this.id][3];
on_bias = saved_min_max_seeds[this.id][4];
this.seed = saved_min_max_seeds[this.id][5];
}
this.reset();
}
Snowflake.prototype.reset = function() {
randomSeed(this.seed);
const iterations = this.iterations;
//this.minIterations = iterations; this.maxIterations = iterations;
this.history = generate(iterations, this.minCells);
this.computeBounds();
}
Snowflake.prototype.draw = function(index, offset_x, offset_y) {
if (index < 0) return;
if (index >= this.history.length) return;
const edges = this.history[index];
var scale = 40 * this.computeScale();
if (show_seeds) { scale *= 0.7; offset_y *= 0.9; }
drawEdges(edges, scale, offset_x, offset_y);
if (show_seeds)
{
turtle.penup();
var string = `${this.minIterations} ${this.maxIterations} ${this.minCells} ${off_bias} ${on_bias} ${this.seed}`;
var len = string.length;
const text_size = 0.07;
var t_offset_x = -len/(0.1 / text_size);
var t_offset_y = 0.45;
turtle.goto(offset_x * global_scale * 40 + t_offset_x, (offset_y + t_offset_y) * global_scale * 40);
text.print(turtle, string, text_size);
turtle.penup();
}
}
Snowflake.prototype.computeBounds = function() {
let x0 = 0;
let y0 = 0;
let x1 = 0;
let y1 = 0;
for (let edges of this.history) {
for (let e of edges) {
for (let p of e) {
x0 = Math.min(x0, p[0]);
x1 = Math.max(x1, p[0]);
y0 = Math.min(y0, p[1]);
y1 = Math.max(y1, p[1]);
}
}
}
this.bounds = [x0, y0, x1, y1];
}
Snowflake.prototype.computeScale = function() {
const p = 0.9;
let x0, y0, x1, y1;
[x0, y0, x1, y1] = this.bounds;
const dx = x1 - x0 + 2;
const dy = y1 - y0 + 2;
const sx = p * 1 / dx;
const sy = p * 1 / dy;
const s = Math.min(sx, sy);
return s;
}
let flakes = [];
function setup()
{
line_count = 0;
}
function draw(t_i)
{
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const i = row * cols + col;
if (i != t_i) continue;
let flake = flakes[0];
const offset_x = col - cols * 0.5 + 0.5;
var offset_y = row - rows * 0.5 + 0.5;
if (show_seeds) offset_y *= 1.3;
flake.draw(i, offset_x, offset_y);
}
}
}
Canvas.setpenopacity(0.9);
const turtle = new Turtle();
const text = new Text();
turtle.penup();
// The walk function will be called until it returns false.
function walk(i)
{
if (i==0)
{
setup();
flakes.push(new Snowflake(0));
rows = cols = 1;
while (rows * cols < flakes[0].history.length)
{
if (cols > rows) rows++;
else cols++;
}
global_scale = 5 / Math.max(rows, cols);
turtle.penup();
turtle.goto(-30,97);
//text.print(turtle, `History: ${flakes[0].history.length} - Rows: ${rows} Cols: ${cols}`, .1);
turtle.penup();
}
if (i >= flakes[0].history.length)
{
return false;
}
draw(i);
//sleep(10);
return true;
}
////////////////////////////////
function sleep(milliseconds) {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
}
////////////////////////////////////////////////////////////////
// Text utility code. Created by Reinder Nijhoff 2019
// https://turtletoy.net/turtle/1713ddbe99
////////////////////////////////////////////////////////////////
function Text() {
class Text {
print (t, str, scale = 1, italic = 0, kerning = 1) {
t.radians();
let pos = [t.x(), t.y()], h = t.h(), o = pos;
str.split('').map(c => {
const i = c.charCodeAt(0) - 32;
if (i < 0 ) {
pos = o = this.rotAdd([0, 48*scale], o, h);
} else if (i > 96 ) {
pos = this.rotAdd([16*scale, 0], o, h);
} else {
const d = dat[i], lt = d[0]*scale, rt = d[1]*scale, paths = d[2];
paths.map( p => {
t.up();
p.map( s=> {
t.goto(this.rotAdd([(s[0]-s[1]*italic)*scale - lt, s[1]*scale], pos, h));
t.down();
});
});
pos = this.rotAdd([(rt - lt)*kerning, 0], pos, h);
}
});
}
rotAdd (a, b, h) {
return [Math.cos(h)*a[0] - Math.sin(h)*a[1] + b[0],
Math.cos(h)*a[1] + Math.sin(h)*a[0] + b[1]];
}
}
const dat = ('br>eoj^jl<jqirjskrjq>brf^fe<n^ne>`ukZdz<qZjz<dgrg<cmqm>`thZhw<lZlw<qao_l^h^e_caccdeefg'+
'gmiojpkqmqporlshsercp>^vs^as<f^h`hbgdeeceacaab_d^f^h_k`n`q_s^<olmmlolqnspsrrspsnqlol>]wtgtfsereqfph'+
'nmlpjrhsdsbraq`o`makbjifjekckaj_h^f_eaecffhimporqssstrtq>eoj`i_j^k_kajcid>cqnZl\\j_hcghglhqjulxnz>c'+
'qfZh\\j_lcmhmllqjuhxfz>brjdjp<egom<ogem>]wjajs<ajsj>fnkojpiojnkokqis>]wajsj>fnjniojpkojn>_usZaz>`ti'+
'^f_dbcgcjdofrisksnrpoqjqgpbn_k^i^>`tfbhak^ks>`tdcdbe`f_h^l^n_o`pbpdofmicsqs>`te^p^jfmfogphqkqmppnrk'+
'shserdqco>`tm^clrl<m^ms>`to^e^dgefhekenfphqkqmppnrkshserdqco>`tpao_l^j^g_ebdgdlepgrjsksnrppqmqlping'+
'kfjfggeidl>`tq^gs<c^q^>`th^e_dadceegfkgnhpjqlqopqorlshserdqcocldjfhigmfoepcpao_l^h^>`tpeohmjjkikfjd'+
'hcecddaf_i^j^m_oapepjoomrjshserdp>fnjgihjikhjg<jniojpkojn>fnjgihjikhjg<kojpiojnkokqis>^vrabjrs>]wag'+
'sg<amsm>^vbarjbs>asdcdbe`f_h^l^n_o`pbpdofngjijl<jqirjskrjq>]xofndlcicgdfeehekfmhnknmmnk<icgefhfkgmh'+
'n<ocnknmpnrntluiugtdsbq`o_l^i^f_d`bbad`g`jambodqfrislsorqqrp<pcokompn>asj^bs<j^rs<elol>_tc^cs<c^l^o'+
'_p`qbqdpfoglh<chlhoipjqlqopqorlscs>`urcqao_m^i^g_eadccfckdnepgrismsorqprn>_tc^cs<c^j^m_oapcqfqkpnop'+
'mrjscs>`sd^ds<d^q^<dhlh<dsqs>`rd^ds<d^q^<dhlh>`urcqao_m^i^g_eadccfckdnepgrismsorqprnrk<mkrk>_uc^cs<'+
'q^qs<chqh>fnj^js>brn^nnmqlrjshsfreqdndl>_tc^cs<q^cl<hgqs>`qd^ds<dsps>^vb^bs<b^js<r^js<r^rs>_uc^cs<c'+
'^qs<q^qs>_uh^f_daccbfbkcndpfrhslsnrppqnrkrfqcpan_l^h^>_tc^cs<c^l^o_p`qbqepgohlici>_uh^f_daccbfbkcnd'+
'pfrhslsnrppqnrkrfqcpan_l^h^<koqu>_tc^cs<c^l^o_p`qbqdpfoglhch<jhqs>`tqao_l^h^e_caccdeefggmiojpkqmqpo'+
'rlshsercp>brj^js<c^q^>_uc^cmdpfrisksnrppqmq^>asb^js<r^js>^v`^es<j^es<j^os<t^os>`tc^qs<q^cs>asb^jhjs'+
'<r^jh>`tq^cs<c^q^<csqs>cqgZgz<hZhz<gZnZ<gznz>cqc^qv>cqlZlz<mZmz<fZmZ<fzmz>brj\\bj<j\\rj>asazsz>fnkc'+
'ieigjhkgjfig>atpeps<phnfleiegfehdkdmepgrislsnrpp>`sd^ds<dhffhekemfohpkpmopmrkshsfrdp>asphnfleiegfeh'+
'dkdmepgrislsnrpp>atp^ps<phnfleiegfehdkdmepgrislsnrpp>asdkpkpiognfleiegfehdkdmepgrislsnrpp>eqo^m^k_j'+
'bjs<gene>atpepuoxnylzizgy<phnfleiegfehdkdmepgrislsnrpp>ate^es<eihfjemeofpips>fni^j_k^j]i^<jejs>eoj^'+
'k_l^k]j^<kekvjyhzfz>are^es<oeeo<ikps>fnj^js>[y_e_s<_ibfdegeifjijs<jimfoeretfuius>ateees<eihfjemeofp'+
'ips>atiegfehdkdmepgrislsnrppqmqkphnfleie>`sdedz<dhffhekemfohpkpmopmrkshsfrdp>atpepz<phnfleiegfehdkd'+
'mepgrislsnrpp>cpgegs<gkhhjfleoe>bsphofleieffehfjhkmlompopporlsisfrep>eqj^jokrmsos<gene>ateeeofrhsks'+
'mrpo<peps>brdejs<pejs>_ubefs<jefs<jens<rens>bseeps<pees>brdejs<pejshwfydzcz>bspees<eepe<esps>cqlZj['+
'i\\h^h`ibjckekgii<j[i]i_jakbldlfkhgjkllnlpkrjsiuiwjy<ikkmkojqirhthvixjylz>fnjZjz>cqhZj[k\\l^l`kbjci'+
'eigki<j[k]k_jaibhdhfihmjilhnhpirjskukwjy<kkimiojqkrltlvkxjyhz>^vamakbhdgfghhlknlplrksi<akbidhfhhill'+
'nmpmrlsisg>brb^bscsc^d^dsese^f^fsgsg^h^hsisi^j^jsksk^l^lsmsm^n^nsoso^p^psqsq^r^rs').split('>').map(
r=> { return [r.charCodeAt(0)-106,r.charCodeAt(1)-106, r.substr(2).split('<').map(a => {const ret
= []; for (let i=0; i<a.length; i+=2) {ret.push(a.substr(i, 2).split('').map(b => b.charCodeAt(0)
-106));} return ret; })]; });
return new Text();
}
///////////////
var seed = 0;
var r_m = 4294967296; // a - 1 should be divisible by m's prime factors
var r_a = 1664525; // c and m should be co-prime
var r_c = 1013904223;
function randomSeed(val)
{
seed = val;//(val == null ? Math.random() * m : val) >>> 0;
}
function random()
{
seed = (r_a * seed + r_c) % r_m;
var rand = seed / r_m;
return rand;
}