├── 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 |
19 |
20 |
General
21 | Scale
22 | Image Resolution
23 |
24 |
25 |
Rendering
26 | Restart Render
27 | Render Grain
28 | Render Chunk Size
29 |
30 |
31 |
Domain Warping
32 | Enabled
33 | Amount
34 |
35 |
36 |
Activator Functions
37 | Clampers
38 | Clamp
39 | Sigmoid
40 | Oscillators
41 | Fold
42 | Sine
43 | Valleys
44 | Boundaries
45 | Split Half
46 | Steps
47 | Oscillating boundaries
48 | Tanh
49 | Wrap
50 | Weirds
51 | Tan
52 | 1/Sine
53 | Noises
54 | Value Noise
55 | Smooth Value Noise
56 | Fractals
57 | Fractal Sine
58 | Fractal Fold
59 | Fractal Value Noise
60 | Fractal Smooth Value Noise
61 |
62 |
63 |
64 | Grai dients
65 |
66 |
67 | Save
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
--------------------------------------------------------------------------------