├── README.md ├── chunk.js ├── index.html ├── main.js ├── network.js ├── style.css └── ui.js /README.md: -------------------------------------------------------------------------------- 1 | # Graidients 2 | ###### Make patterns with AI https://magnogen.net/Graidients 3 | 4 | This is the source code for a section of my website. 5 | You're welcome to snoop around to see how it works, maybe even make a pull request! 6 | (I've been wanting to add an interesting UI!) 7 | 8 | ### How it works 9 | Click on the image, press the `Enter` key, or refresh the page to regenerate a random Neural Network and start the drawing process. 10 | 11 | The algorithm works by feeding the X and Y coordinate into the neural network, and using the 3 of the 5 outputs for the red, green and blue value at that pixel. 12 | The other 2 values come into play when you enable domain warping, that just offsets the input coordinate by that amount and then regenerates at _that_ position (hence why it takes slightly longer). 13 | 14 | Different kinds of Networks with different activation functions produce different aesthetics. 15 | 16 | ### Plans for the future 17 | Mostly UI stuff, you don't have any control over things otherwise. 18 | - [x] A way to redraw the image (click it? a keybind?) 19 | 20 | Click the image, or press the `Enter` Key 21 | - [ ] Network customisation 22 | - [ ] Number of hidden layers, and nodes per layer should be changable 23 | - [ ] A way to change the inputs, like adding a Random value for a dithery effect 24 | - idk stuff like that 25 | - [x] Rendering the image at custom resolutions. 26 | -------------------------------------------------------------------------------- /chunk.js: -------------------------------------------------------------------------------- 1 | // chunk stuff -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Graidients | Magnogen 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 63 |
64 |

Graidients

65 |
66 | 67 | 68 |
69 | Press Enter, or Tap the image to generate new patterns 70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | // main stuff 2 | // jshint esversion: 11 3 | const c = $('canvas#render'); 4 | const ctx = c.getContext('2d'); 5 | c.width = c.height = 256; 6 | 7 | let network = makeNetwork(); 8 | 9 | let settings = { 10 | scale: 3, 11 | res: 8, 12 | get resolution() { return this.res }, 13 | set resolution(v) { this.res = v; resize(2**v); return v }, 14 | domain_warping: false, 15 | warping_amount: 2, 16 | noise_seed: Math.random(), 17 | grain: true, 18 | chunk_size: 6 19 | }; 20 | 21 | let needs_refresh = true; 22 | function resize(size) { 23 | c.width = c.height = size; 24 | needs_refresh = true; 25 | } 26 | 27 | c.on('click', e => { 28 | if (activators.length == 0) return; 29 | settings.noise_seed = Math.random(); 30 | network.init(2, 4, 4, 4, 4, 5); 31 | needs_refresh = true; 32 | $('[hint]').classList.add('hidden'); 33 | }); 34 | on('keydown', e => e.key=='Enter' && c.trigger('click')); 35 | c.trigger('click'); 36 | $('[hint]').classList.remove('hidden'); 37 | 38 | $$('span[input="option"]').forEach(el => { 39 | el.on('click', e => { 40 | if (activators.includes(activator_options[el.id])) 41 | activators.splice(activators.indexOf(activator_options[el.id]), 1); 42 | else activators.push(activator_options[el.id]); 43 | el.classList.toggle('active'); 44 | needs_refresh = true; 45 | }) 46 | }); 47 | $$('span[input="boolean"]').forEach(el => { 48 | if (settings[el.id]) el.classList.add('active'); 49 | el.on('click', e => { 50 | settings[el.id] = !settings[el.id]; 51 | el.classList.toggle('active'); 52 | $$('[need]').forEach(n => { 53 | if (n.getAttribute('need') != el.id) return; 54 | if (!settings[el.id]) n.setAttribute('sad', 'sad'); 55 | else n.removeAttribute('sad'); 56 | }); 57 | needs_refresh = true; 58 | }); 59 | }); 60 | $$('span[input="number"]').forEach(el => { 61 | const map = new Function('return ' + (el.getAttribute('map') ?? 'i => i.toFixed(1)'))(); 62 | const crement = +(el.getAttribute('crement') ?? 0.5); 63 | const min = +(el.getAttribute('min') ?? -Infinity); 64 | const max = +(el.getAttribute('max') ?? Infinity); 65 | 66 | let reader = document.createElement('span'); 67 | reader.innerHTML = ' = ' + map(settings[el.id]); 68 | reader.classList.add('reader'); 69 | let pp = document.createElement('span'); 70 | pp.innerHTML = ' ++'; 71 | pp.on('click', e => { 72 | if (el.getAttribute('need') && !settings[el.getAttribute('need')]) return; 73 | settings[el.id] += crement; 74 | settings[el.id] = Math.min(max, settings[el.id]); 75 | reader.innerHTML = ' = ' + map(settings[el.id]); 76 | needs_refresh = true; 77 | }); 78 | let mm = document.createElement('span'); 79 | mm.innerHTML = '-- '; 80 | mm.on('click', e => { 81 | if (el.getAttribute('need') && !settings[el.getAttribute('need')]) return; 82 | settings[el.id] -= crement; 83 | settings[el.id] = Math.max(min, settings[el.id]); 84 | reader.innerHTML = ' = ' + map(settings[el.id]); 85 | needs_refresh = true; 86 | }); 87 | el.insertAdjacentElement('beforeend', pp); 88 | el.insertAdjacentElement('beforeend', reader); 89 | el.insertAdjacentElement('afterbegin', mm); 90 | }); 91 | 92 | let num_saved = 0; 93 | let dateObj = new Date(); 94 | let time = `${dateObj.getUTCFullYear()}${dateObj.getUTCMonth()+1}${dateObj.getUTCDate()}`; 95 | $('#save').on('click', e => { 96 | let link = document.createElement('a'); 97 | link.download = `graidient-${time}_${num_saved++}.png`; 98 | link.href = c.toDataURL() 99 | link.click(); 100 | }); 101 | let restart_render = false; 102 | $('#restart_render').on('click', e => restart_render = true); 103 | 104 | ;(async () => { 105 | let last = 0; 106 | let pixels, pixelCoords; 107 | let chunks, chunkCoords, chunk_size; 108 | 109 | let i = 0|rand(network.brain.length); 110 | let j = 0|rand(network.brain[i].length); 111 | let k = 0|rand(network.brain[i][j].weights.length); 112 | let d = rand() < 0.5 ? 1 : -1; 113 | do { 114 | needs_refresh = false; 115 | chunk_size = Math.min(c.width, 2**settings.chunk_size); 116 | 117 | chunkCoords = [...Array((c.width/chunk_size) ** 2)].map((e, i) => ({ 118 | chunkX: i%(c.width/chunk_size), 119 | chunkY: 0|(i/(c.width/chunk_size)) 120 | })); 121 | // shuffle(chunkCoords); 122 | render: for (let { chunkX, chunkY } of chunkCoords) { 123 | pixels = ctx.getImageData(chunkX*chunk_size, chunkY*chunk_size, chunk_size, chunk_size); 124 | pixelCoords = [...Array(chunk_size ** 2)].map((e, i) => ({ x: i%chunk_size, y: 0|(i/chunk_size) })); 125 | shuffle(pixelCoords); 126 | for (let { x, y } of pixelCoords) { 127 | if (restart_render) break render; 128 | let X = x + chunkX*chunk_size; 129 | let Y = y + chunkY*chunk_size; 130 | Y = settings.scale*(Y/(c.height-1) - 0.5); 131 | X = settings.scale*(X/(c.width-1) - 0.5); 132 | let I = 4*(x + y*chunk_size); 133 | let col = network.compute(X, Y); 134 | if (settings.domain_warping) { 135 | X += settings.warping_amount*col[3]; 136 | Y += settings.warping_amount*col[4]; 137 | col = network.compute(X, Y); 138 | } 139 | const [r, g, b] = col; 140 | pixels.data[I + 0] = r * 255; 141 | pixels.data[I + 1] = g * 255; 142 | pixels.data[I + 2] = b * 255; 143 | pixels.data[I + 3] = 255; 144 | if (settings.grain && performance.now() - last > 1000/70) { 145 | ctx.putImageData(pixels, chunkX*chunk_size, chunkY*chunk_size); 146 | await new Promise(requestAnimationFrame); 147 | last = performance.now(); 148 | } 149 | } 150 | ctx.putImageData(pixels, chunkX*chunk_size, chunkY*chunk_size); 151 | await new Promise(requestAnimationFrame); 152 | } 153 | 154 | while (!needs_refresh) await new Promise(requestAnimationFrame); 155 | restart_render = false; 156 | 157 | } while (true); 158 | })() 159 | 160 | 161 | -------------------------------------------------------------------------------- /network.js: -------------------------------------------------------------------------------- 1 | // network stuff 2 | const makeNetwork = () => { 3 | return { 4 | brain: [], 5 | init(...Ls) { 6 | this.brain.length = 0; 7 | let layers = [...Ls, []]; 8 | for (let l = 0; l < layers.length-1; l++) { 9 | let layer = []; 10 | for (let n = 0; n < layers[l]; n++) { 11 | const node = makeNode(layers[l+1]); 12 | layer.push(node); 13 | } 14 | this.brain.push(layer); 15 | } 16 | }, 17 | compute(...inputs) { 18 | for (let i in inputs) this.brain[0][i].value = inputs[i]; 19 | for (let l in this.brain) 20 | for (let n in this.brain[l]) { 21 | if (l == 0) continue; 22 | let value = this.brain[l][n].bias; 23 | for (let N = 0; N < this.brain[l-1].length; N++) 24 | value += this.brain[l-1][N].value * this.brain[l-1][N].weights[n]; 25 | this.brain[l][n].value = this.brain[l][n].acfunc(value); 26 | } 27 | return this.brain[this.brain.length-1].map(e => e.value); 28 | } 29 | }; 30 | }; 31 | 32 | const makeNode = (num_weights) => { 33 | return { 34 | weights: [...Array(num_weights)].map(e => 2*Math.random()-1), 35 | bias: 2*Math.random()-1, 36 | value: 0, 37 | acfunc: activators[0|(Math.random()*activators.length)] 38 | } 39 | } 40 | 41 | let activators = [sin]; 42 | 43 | let activator_options = { 44 | clamp, sigmoid, fold, sin, tan, invSin, 45 | valleys, splitHalf, steps, tanh, wrap, 46 | value_noise, smooth_value_noise, fractal_sin, 47 | fractal_fold, fractal_value_noise, 48 | fractal_smooth_value_noise 49 | } 50 | 51 | function clamp(x) { return x < 0 ? 0 : x > 1 ? 1 : x } 52 | function sigmoid(n) { return 1 / (1 + Math.exp(-n * 8)) } 53 | 54 | function wrap(x) { 55 | let X = x; 56 | while (X < 0) X++; 57 | while (X > 1) X--; 58 | return X; 59 | } 60 | function fold(n) { 61 | let N = (n<0 ? -n : n)%2; 62 | if (N > 1) return 2 - N; 63 | return N; 64 | } 65 | function valleys(n) { 66 | let N = n+1; 67 | while (N < -1) N += 2; 68 | while (N > 1) N -= 2; 69 | N = (n<0 ? -n : n); 70 | return Math.pow(N, 2/3); 71 | } 72 | 73 | function value_noise(n) { 74 | const scale = 2; 75 | let N = scale*n - Math.floor(scale*n); 76 | let x1 = Math.sin(100*settings.noise_seed + Math.floor(scale*n)) * 43758.5453123; 77 | x1 = x1 - Math.floor(x1); 78 | let x2 = Math.sin(100*settings.noise_seed + Math.floor(scale*n+1)) * 43758.5453123; 79 | x2 = x2 - Math.floor(x2); 80 | return x1*(1-N) + x2*(N); 81 | } 82 | function smooth_value_noise(n) { 83 | const scale = 2; 84 | let N = scale*n - Math.floor(scale*n); 85 | let x1 = Math.sin(100*settings.noise_seed + Math.floor(scale*n)) * 43758.5453123; 86 | x1 = x1 - Math.floor(x1); 87 | let x2 = Math.sin(100*settings.noise_seed + Math.floor(scale*n+1)) * 43758.5453123; 88 | x2 = x2 - Math.floor(x2); 89 | const f = n => n*n*(3-2*n); 90 | return x1*f(1-N) + x2*f(N); 91 | } 92 | 93 | function fractal_func(func) { 94 | const falloff = 2; 95 | const shift = 437.585453123; 96 | const octaves = 5; 97 | return function(n) { 98 | let value = 0; 99 | let amplitude = 1/falloff; 100 | let x = n; 101 | for (let i = 0; i < octaves; ++i) { 102 | value += amplitude * func(x); 103 | x = falloff * x + shift; 104 | amplitude /= falloff; 105 | } 106 | return value; 107 | } 108 | } 109 | function fractal_sin(n) { return fractal_func(sin)(n) } 110 | function fractal_fold(n) { return fractal_func(fold)(n) } 111 | function fractal_value_noise(n) { return fractal_func(value_noise)(n*0.75)*1.75 } 112 | function fractal_smooth_value_noise(n) { return fractal_func(smooth_value_noise)(n*0.75)*1.75 } 113 | 114 | function sin(n) { return 0.5*(Math.sin(Math.PI*(n-0.5))+1); } 115 | function tan(n) { return 0.5*(Math.tan(n*0.9)+1); } 116 | function invSin(n) { return 1/Math.pow(sin(n), 0.2) - 1; } 117 | function tanh(n) { return Math.tanh((n+4) % 2); } 118 | function splitHalf(n) { return n < 0 ? 0 : 1; } 119 | function steps(n) { return (0|(8*n))/7; } 120 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | display: flex; 5 | width: 100vw; 6 | height: 100vh; 7 | font-family: "Fira Code", monospace; 8 | font-size: 0.75em; 9 | background: #161816; 10 | color: #fff; 11 | white-space: nowrap; 12 | /* and so the pain begins */ 13 | -webkit-touch-callout: none; /* iOS Safari */ 14 | -webkit-user-select: none; /* Safari */ 15 | -khtml-user-select: none; /* Konqueror HTML */ 16 | -moz-user-select: none; /* Old versions of Firefox */ 17 | -ms-user-select: none; /* Internet Explorer/Edge */ 18 | user-select: none; /* Non-prefixed version, currently 19 | supported by Chrome, Edge, Opera and Firefox */ 20 | } 21 | 22 | .underline, .hover-underline:hover { 23 | /* Underline styles - details of technique here: 24 | * https://nickymeuleman.netlify.app/blog/css-animated-wrapping-underline 25 | */ 26 | background: linear-gradient(to right, #80f080, #80f080); 27 | background-size: 100% 1px; 28 | background-position: 0% 100%; 29 | background-repeat: no-repeat; 30 | transition: background-size .2s; 31 | } 32 | 33 | aside { 34 | max-width: 48em; 35 | min-width: 24em; 36 | background-color: #323632; 37 | padding: 0.5em 1em; 38 | flex-grow: 2; 39 | overflow: auto; 40 | font-size: 1.15em; 41 | } aside > div { 42 | padding: 1em 1em; 43 | margin: 1em 0; 44 | border-radius: 1rem; 45 | background-color: #282c28; 46 | } aside > div > h4 { 47 | position: relative; 48 | top: -0.25em; 49 | padding: 0 1rem 0rem 1rem; 50 | margin: 0; 51 | font-weight: 400; 52 | font-size: 1.5em; 53 | } aside > div > span { 54 | display: block; 55 | margin: 0.1em 0.6em; 56 | padding: 0 0.4em; 57 | cursor: pointer; 58 | } 59 | 60 | :not([need][sad])[input="option"] { color: #fff4; } 61 | :not([need][sad])[input="option"]:before { content: '// '; } 62 | :not([need][sad])[input="option"].active { color: #80f080; } 63 | :not([need][sad])[input="option"].active:before { content: ''; } 64 | :not([need][sad])[input="option"]:hover { font-style: italic; } 65 | 66 | :not([need][sad])[input="boolean"] { color: #fff; } 67 | :not([need][sad])[input="boolean"]:after { content: ' = false'; color: #fff4; } 68 | :not([need][sad])[input="boolean"].active { color: #80f080; } 69 | :not([need][sad])[input="boolean"].active:after { content: ' = true'; } 70 | :not([need][sad])[input="boolean"]:hover { font-style: italic; } 71 | 72 | :not([need][sad])[input="number"] { color: #fff; } 73 | :not([need][sad])[input="number"]:hover { cursor: default; } 74 | :not([need][sad])[input="number"] span { color: #80f080; cursor: pointer; } 75 | :not([need][sad])[input="number"] .reader { color: #fff4; cursor: initial; } 76 | 77 | [need][sad] { color: #fff4; cursor: initial; } 78 | [need][sad]:before { content: '// '; } 79 | 80 | main { 81 | display: flex; 82 | flex-direction: column; 83 | align-items: center; 84 | padding: 1em; 85 | flex-grow: 1; 86 | } [render] { 87 | display: flex; 88 | flex-direction: column; 89 | } button { 90 | background-color: #0000; 91 | color: #fff; 92 | border: none; 93 | padding: 0.5em; 94 | } button:hover:not(.rendering) { 95 | background-size: 100% 4px; 96 | } aside button { 97 | margin: 0.25em 1em; 98 | } main h1 { 99 | margin: 0; 100 | padding: 0.25em 0em 0em 0em; 101 | text-align: center; 102 | text-transform: uppercase; 103 | font-size: min(4em, 6vw); 104 | } main [hint] { 105 | display: block; 106 | padding: 0.5em 1em; 107 | margin: 0.5em 1em 0em 1em; 108 | background: #282c28; 109 | color: #fff4; 110 | text-align: center; 111 | white-space: normal; 112 | transition: height 0.2s, padding 0.4s ease; 113 | } main [hint].hidden { 114 | height: 0px; 115 | padding-block: 0px; 116 | overflow: hidden; 117 | } 118 | 119 | canvas { 120 | width: 50vw; 121 | max-width: calc(75vh - 2em); 122 | background-color: #d0d0d0; 123 | background-image: linear-gradient(45deg, #808080 25%, transparent 25%), 124 | linear-gradient(-45deg, #808080 25%, transparent 25%), 125 | linear-gradient(45deg, transparent 75%, #808080 75%), 126 | linear-gradient(-45deg, transparent 75%, #808080 75%); 127 | background-size: 20px 20px; 128 | background-position: 0 0, 0 10px, 10px -10px, -10px 0px; 129 | } 130 | 131 | @media (max-width: 110vh) { 132 | body { flex-direction: column-reverse; } 133 | aside { max-width: 100%; } 134 | canvas { width: auto; height: 25vh; } 135 | } 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /ui.js: -------------------------------------------------------------------------------- 1 | // ui stuff --------------------------------------------------------------------------------