-
-
Save rep-movsd/7b77fb96a7f4a7c773d2645104a76ec5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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