Skip to content

Instantly share code, notes, and snippets.

@rep-movsd
Last active February 19, 2022 14:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rep-movsd/7b77fb96a7f4a7c773d2645104a76ec5 to your computer and use it in GitHub Desktop.
Save rep-movsd/7b77fb96a7f4a7c773d2645104a76ec5 to your computer and use it in GitHub Desktop.
// Generative Art License
// ======================
// Copyright (C) 2022 Vivek Nagarajan (rep.movsd@gmail.com)
//
// Terms:
// 1) No one is allowed to include this generative code in any project that is commercial.
// 2) No one is allowed to use this code as a basis for generative art on fxhash or any other similar platform, commercial or otherwise.
// 3) The owner of each minted token is completely free to use their uniquely generated image(s) for any purpose, commercial or otherwise.
// 4) The author of the generative code reserves the right to use the images generated for any purpose they wish, commercial or otherwise.
//
// In short, use your conscience - don't try to get rich ripping off other peoples creativity and efforts.
// Contact the author in case you wish to negotiate any of the above.
// This code is based on the "DLA Alternate Linear" algorithm from
// http://www.netlogoweb.org/launch#http://www.netlogoweb.org/assets/modelslib/Sample%20Models/Chemistry%20&%20Physics/Diffusion%20Limited%20Aggregation/DLA%20Alternate%20Linear.nlogo
function render()
{
const W = 2048;
const H = 2048;
const S = Math.sin;
const C = Math.cos;
const K = fxrand;
const c = document.querySelector("#c");
const x = c.getContext("2d");
c.width = W;
c.height = H;
const rr = (a, b) => a + fxrand() * (b - a);
const HSLA = (h, s, v, a) => `hsl(${h | 0},${s}%,${v}%,${a})`;
const rind = (arr) => arr.length * fxrand() | 0
// Draws one elliptical element stored in B[b]
// 15 out of 10000 elements is a flower
// FLowers are drawn as cup shaped half ellipses, grass as long thin ones
const drawElem = b =>
{
let FLOWER = K() < .0015;
x.beginPath();
x.fillStyle =
FLOWER ? FLOWER_HSL :
HSLA(b[2], GRASS_SAT, GRASS_VAL, GRASS_ALPHA * SCALE);
x.ellipse(b[0], b[1], b[3] * (FLOWER ? 3 : 1), b[3] * (SCALE * 8) * (FLOWER ? .5 : 1), b[4] * SCALE, 0, 4);
x.fill();
};
// Places one element by "dropping" it from the top at a random X
// Either the element reaches the bottom, or if it touches another element then it stops there
// The color of the element changes to the one it touches, plus a small variation in HSL hue
const placeElement = () =>
{
// iterate over all the elements in reverse - to find the element that would touch this
// Recent items are higher than older ones, so reverse iteration is faster
let L = B.length - 1;
let x1 = B[L][0], found = false;
for(let i = L - 1; i >= 0; --i)
{
// Do they overlap horizontally?
let [x2, y2, c] = B[i];
let O = Math.abs(x2 - x1);
if(O < 2 * R)
{
// What height would it make contact?
let V = (4 * R * R - O * O) ** .5;
// Place element at that Y (with adjustments) and imbue the color of the one it touched
// Element is angled left if it touches the left of another, else right - this produces a branching effect
// B[L] has X, Y, color, angle
B[L][0] = x2 + 16 * K() * (x2 - x1 > 0 ? -R : R);
B[L][1] = y2 - (V / 4) * SCALE * 2;
B[L][2] = (c + (K() - .5) * 3);
B[L][4] = (x2 - x1 > 0 ? -1.5 : 1.5) * K() * SCALE;
found = true;
break;
}
}
// No contact, place element at the bottom
if(!found)
{
B[L][1] = H;
}
// Draw this element
drawElem(B[L]);
};
let B = [];
// Grass palette selection
const arrGrass =
[
[rr(40,60), 65,65,.3],
[rr(60,80), 75,55,.3],
[rr(20, 40), 45, 60, .3],
[rr(0,15), 45, 60, 0.2]
];
// Choose base colors for grass, sky, flower
let GRASS_I = rind(arrGrass)
let GRASS = arrGrass[GRASS_I];
let GRASS_HUE, GRASS_SAT, GRASS_VAL, GRASS_ALPHA;
[GRASS_HUE, GRASS_SAT, GRASS_VAL, GRASS_ALPHA] = GRASS;
let DUSK = GRASS_HUE < 20;
let SKY_HSL = DUSK ? HSLA(350, 50, 70, 1) : HSLA(rr(180, 220), 60, 70, 1);
let CLOUD_HSL = DUSK ? HSLA(350, 30, 80, 1):'white';
// Flower palette selection (if its dusk the color changes slightly for some)
const arrFlowers =
[
HSLA(60, 99, 50, DUSK ? .5: .7),
HSLA(330, 99, 60, .7),
HSLA(300, 99, 60, .7),
HSLA(270, 99, 50, .8),
HSLA(240, 99, 60, 1),
HSLA(50, 99, 50, DUSK ? .5: .7),
HSLA(0, 99, 65, .8),
HSLA(15, 99, 65, .8),
HSLA(30, 99, 50, 1),
];
let FLOWER_I = rind(arrFlowers);
let FLOWER_HSL = arrFlowers[FLOWER_I];
// R is the effective radius of each element - even though they are drawn as ellipses, they are calculated as circles
let SCALE = 1, R;
// Angle of sine wave used to modulate the hill shape
let SOFF = ((K() * 628)|0)/100;
window.$fxhashFeatures =
{
time : DUSK ? 'dusk': 'day',
grass : GRASS_I,
flowers: FLOWER_I,
hill: SOFF
}
// Clear screen with a very dark blue
x.fillStyle = '#000040';
x.fillRect(0, 0, W, H);
// Main rendering loop
const loop = () =>
{
// Add 2000 grass elements
for(let i = 2000; i--;)
{
R = 4 * SCALE;
// Add new random elem at X=rand, Y = 0 for the next iteration and place it in the right position
B.push([W * K() | 0, 0, GRASS_HUE, R, 0]);
placeElement();
// Reduce the size everytime, to make higher grass smaller (and hence look further) for a 3d effect
if(R > 1)
{
SCALE -= 0.000004;
}
}
// If we havent added 80000 grass elements, cause a redraw (animates the drawing incrementally) and redo this loop
if(B.length < 80000)
{
requestAnimationFrame(loop);
}
// We are done drawing grass and flowers, time to draw the sky and finish
else
{
requestAnimationFrame(drawSky);
}
};
// This defines the hill shape
const getGrassY = (M) =>
{
// Store Y coordinate for every X
let YS = [];
// Get the image data, go over every X
const img = x.getImageData(0, 0, W, H);
const D = img.data;
for(let X = 0; X < W; ++X)
{
// Look for the highest non grass pixel (initial fill is dark blue so check R = 0 and G = 0 )
for(let Y = 0; Y < H; ++Y)
{
let J = (Y * W + X) * 4;
if(D[J] + D[J + 1] > 0)
{
// Push this item into teh array, adding a sine wave of amplitude H/7 (rounded hill shape)
YS.push(Y + M + (S(SOFF + 3 * X / W) + 1) * H / 7);
break;
}
}
}
// The YS curve will be very jagged, so smooth it out 200 times by making each value the average of its neighbours
for(let J = 0; J < 200; ++J)
{
for(let I = 1; I < YS.length - 1; ++I)
{
YS[I] = (YS[I - 1] + YS[I + 1]) / 2;
}
}
// Now YS has a nice curvy hill shape along with the slightly random ups and downs caused by the grass algorithm
return YS;
};
const drawSky = () =>
{
// Make an extra canvas for clouds
const cSky = document.createElement('canvas');
cSky.width = W;
cSky.height = H;
const ctxSky = cSky.getContext('2d');
// Fill sky on that canvas
ctxSky.fillStyle = SKY_HSL;
ctxSky.fillRect(0, 0, W, H);
// Draw random rectangles in cloud color
ctxSky.fillStyle = CLOUD_HSL;
for(let X = 0; X < W; X += W / 20)
{
let Y = rr(0, H / 2);
let D = rr(W / 6, W / 3);
ctxSky.fillRect(X, Y, D, D / 6);
}
// Time to make the hill shape on main canvas
let N = 64;
let M = W / N;
// Get the hill shape
let YS = getGrassY(M);
// We will clip the image above this grass curve and draw the sky on it
x.save();
// Create the path, adding a small random number between 0 and M*.9 to each point
// This randomness is like very thin spikes and prevents the grass from looking like a cardboard cutout
// We start the path at 0,0, then the hill curve then W,0 and back to 0,0, defining everything except the hill
x.beginPath();
x.moveTo(0, 0);
for(let I = 0; I < YS.length; ++I)
{
x.lineTo(I, YS[I] + K() * M*.9);
}
x.lineTo(W, 0);
x.closePath();
x.clip();
// We need to fill this whole region with the sky first, because we will be using a blur filer before drawing sky we already drew above.
// and that causes artifacts at the edges
x.fillStyle = SKY_HSL;
x.fillRect(0, 0, W, H);
// Setup a blur (this makes the random rectangles look like clouds)
x.filter = `blur(${W/32}px)`;
// Draw our earlier rendered sky/clouds
x.drawImage(cSky, 0, 0);
x.restore();
// Done
fxpreview();
};
// This kickstarts the whole incremental process
loop();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment