Starry Night Flow 🌃

Brute force using Perlin noise, a spiral, cultivated randomness or some 'noise' which I made myself.

Starry Night Flow 🌃 (variation)
Starry Night Flow 🌃 (variation)
Starry Night Flow 🌃 (variation)

Log in to post a comment.

// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(1);

const iterations = 4000; //min=100 max=5000 step=100
const maxLength = 20; //min=1 max=200 step=1
const stepLength = .2;
const vectorGrid = 500; //min=20 max=1000 step=10
const visibleGrid = 400; //min=20 max=800 step=10
const stopMargin = 1; //min=0 max=10 step=.1
const lineThickness = 5; //min=1 max=10 step=1
const gridMode = 3; //min=0 max=3 step=1 (Perlin noise, Spiral noise, Random chaos, Jurgen noise)
const mode_perlinZoom = 1; //min=.1 max=10 step=.1
const mode_spiralFactor = .5; //min=0 max=1 step=.1
const mode_randomBias = .4; //min=0 max=2 step=.1
const mode_randomOffset = -.2; //min=-1 max=1 step=.1
const mode_jurgenPoints = 8; //min=1 max=10 step=1
const mode_jurgenNegatives = 0; //min=0 max=1 step=1 (No, Yes)
const mode_jurgenAmplitude = 20; //min=1 max=40 step=1
// Global code will be evaluated once.
const turtle = new Turtle();
const fatTurtle = new Slowbro();
fatTurtle.thickness = lineThickness;
const perl = new Perlin(mode_perlinZoom);

const vpGridRatio = 200 / visibleGrid;
const margin = stopMargin;

const newDimensionalArray = (dim, fn, args = []) => Array.apply(null,{length: dim[0]}).map((v,k) => dim.length == 1? fn(...args, k): newDimensionalArray(dim.filter((v,k) => k != 0), fn, [...args, k]));

const jurgenGrid = Array.apply(null,{length: mode_jurgenPoints}).map((v,k) => [(Math.random() * vectorGrid | 0) - vectorGrid/2, (Math.random() * vectorGrid | 0) - vectorGrid/2, Math.random() * mode_jurgenAmplitude - (mode_jurgenNegatives==1?mode_jurgenAmplitude/2:0)]);

const vectorsGrid = 
    gridMode == 0?
        newDimensionalArray([vectorGrid,vectorGrid], (col,row) => perl.get(col/(vectorGrid-1), row/(vectorGrid-1)) * Math.PI * 2)
    : gridMode == 1?
        newDimensionalArray([vectorGrid,vectorGrid], function(col,row) { const x = col - vectorGrid/2, y = row - vectorGrid/2; return (x === 0? Math.PI * (0<y? -.5: .5): Math.atan(y/x) + (x > 0? Math.PI:0)) + Math.PI/2 - mode_spiralFactor; })
    : gridMode == 2?
        newDimensionalArray([vectorGrid,vectorGrid], (col,row) => ((Math.random() * mode_randomBias) + mode_randomOffset) * Math.PI * 2)
    : //default
        newDimensionalArray([vectorGrid,vectorGrid], function(col,row) {
            const xx = col - vectorGrid/2, yy = row - vectorGrid/2;
            let angles = [];
            let sumWeight = 0;
            for(let jg = 0; jg < jurgenGrid.length; jg++) {
                let x = xx - jurgenGrid[jg][0];
                let y = yy - jurgenGrid[jg][1];
                let weight = Math.abs(jurgenGrid[jg][2])/(x**2+y**2);
                sumWeight += weight;
                angles.push([((x === 0? Math.PI * (0<y? -.5: .5): Math.atan(y/x) + (x > 0? Math.PI:0)) + Math.PI/2) + (jurgenGrid[jg][2] < 0? Math.PI:0), weight]);
            }
            return angles.reduce((prev, cur) => prev + (cur[0] * cur[1]) /sumWeight, 0);
        });
;

const tr = (a) => [((a[0] - vectorGrid / 2) * vpGridRatio),((a[1] - vectorGrid / 2) * vpGridRatio)];
const add2 = (a, b) => [a[0] + b[0], a[1] + b[1]];

let visited = [];
const rMargin = stopMargin / vpGridRatio;

// The walk function will be called until it returns false.
function walk(i) {
    let ix = 0;
    const visits = [];
    for(let pt of drawCurve(
        [Math.random() * vectorGrid, Math.random() * vectorGrid],
        maxLength / vpGridRatio,
        stepLength / vpGridRatio
    )) {
        const trpt = tr(pt);
        if(stopMargin > 0 && visited.filter((k) => 
            k[0]-rMargin < trpt[0] && trpt[0] < k[0]+rMargin &&
            k[1]-rMargin < trpt[1] && trpt[1] < k[1]+rMargin
        ).length > 0) break;
        ix++ == 0? fatTurtle.jump(trpt): fatTurtle.goto(trpt);
        visits.push(trpt);
    }
    visited = [...visited, ...visits];
    return i < iterations;
}

function *drawCurve(pt, max_length, step_length = .2, ) {
    let num_steps = max_length / step_length;
    yield pt;

    for (let n = 0; n < num_steps; n++) {
        const col = pt[0] | 0, row = pt[1] | 0;
        
        if(col < 0 || row < 0 || vectorGrid <= col || vectorGrid <= row) return;
        
        let grid_angle = vectorsGrid[col][row];

        pt[0] += step_length * Math.cos(grid_angle)
        pt[1] += step_length * Math.sin(grid_angle)

        yield pt;
    }
}

////////////////////////////////////////////////////////////////
// Slowbro utility code. Created by Lionel Lemarie 2021
// Based on Slowpoke by Reinder, which removes most duplicate
// lines Slowbro adds optional thickness to the lines
////////////////////////////////////////////////////////////////
function Slowbro(x, y) {
    const linesDrawn = {};
    class Slowbro extends Turtle {
        constructor(x, y) {
        	super(x, y);
        	this.thickness = 1; this.offset = 0.2;
			this.slowpoke_skip = this.slowpoke_draw = 0;
        }
        goto(x, y) {
            if (Array.isArray(x)) { y = x[1]; x = x[0]; }
            const ox = this.x(), oy = this.y();
            if (this.isdown()) {
                const p = [x, y], o = [ox, oy];
                const h1 = o[0].toFixed(2) + '_' + p[0].toFixed(2) + o[1].toFixed(2) + '_' + p[1].toFixed(2);
                const h2 = p[0].toFixed(2) + '_' + o[0].toFixed(2) + p[1].toFixed(2) + '_' + o[1].toFixed(2);
                if (linesDrawn[h1] || linesDrawn[h2]) {
                    super.up(); super.goto(p); super.down();
                    this.slowpoke_skip++;
                    return;
                }
                linesDrawn[h1] = linesDrawn[h2] = true;
                this.slowpoke_draw++;

            	for (var dx = this.thickness-1; dx >=0 ; dx--) {
            		for (var dy = this.thickness-1; dy >= 0; dy--) {
            			if (!dx && !dy) continue;
            			super.goto( x + dx * this.offset,  y + dy * this.offset);
            			super.goto(ox + dx * this.offset, oy + dy * this.offset);
            		}
            	}
            } 
            super.goto(x, y);
        }
    }
    return new Slowbro(x, y);
}
// Adapted from https://github.com/joeiddon/perlin
function Perlin(scale) {
    class Perlin {
        constructor(s) {
            this.gradients = {};
            this.memory = {};
            this.scale = s;
        }
        rand_vect() {
            let theta = Math.random() * 2 * Math.PI;
            return [Math.cos(theta), Math.sin(theta)];
        }
        dot_prod_grid(x, y, vx, vy) {
            let g_vect;
            let d_vect = [x - vx, y - vy];
            if (this.gradients[[vx,vy]]){
                g_vect = this.gradients[[vx,vy]];
            } else {
                g_vect = this.rand_vect();
                this.gradients[[vx, vy]] = g_vect;
            }
            return d_vect[0] * g_vect[0] + d_vect[1] * g_vect[1];
        }
        smootherstep(x) {
            return 6*x**5 - 15*x**4 + 10*x**3;
        }
        interp(x, a, b){
            return a + this.smootherstep(x) * (b-a);
        }
        get(x, y) {
            x *= this.scale;
            y *= this.scale;
            if (this.memory.hasOwnProperty([x,y]))
                return this.memory[[x,y]];
            let xf = Math.floor(x);
            let yf = Math.floor(y);
            //interpolate
            let tl = this.dot_prod_grid(x, y, xf,   yf);
            let tr = this.dot_prod_grid(x, y, xf+1, yf);
            let bl = this.dot_prod_grid(x, y, xf,   yf+1);
            let br = this.dot_prod_grid(x, y, xf+1, yf+1);
            let xt = this.interp(x-xf, tl, tr);
            let xb = this.interp(x-xf, bl, br);
            let v = this.interp(y-yf, xt, xb);
            this.memory[[x,y]] = v;
            return v;
        }
    }
    return new Perlin(scale);
}