📕 workshop-generative-art → Cheat Sheet
Here you will find some 'recipes' and patterns that you can use in creative coding and generative art.
You can use Array.from(new Array(count))
to create a dense array at a fixed size. For example, to generate an array of 20 random numbers:
const count = 20;
const randoms = Array.from(new Array(count)).map(() => Math.random());
Using index / (listLength - 1)
, you can get a t value between 0 and 1.
Example:
const count = 20;
const values = Array.from(new Array(count)).map((_, i) => {
const t = i / (count - 1);
// Do something with the 't' param...
return t;
});
To guard against a count
of <= 1:
const count = 20;
const values = Array.from(new Array(count)).map((_, i) => {
const t = count <= 1 ? 0 : (i / (count - 1));
return t;
});
During the workshop, when I say "UV coordinates" I am referring to coordinates that vary from (0, 0)
(top left) to (1, 1)
(bottom right).
For example, to create a UV grid with nested for loops:
const count = 5;
const points = [];
for (let x = 0; x < count; x++) {
for (let y = 0; y < count; y++) {
const u = count <= 1 ? 0.5 : (x / (count - 1));
const v = count <= 1 ? 0.5 : (y / (count - 1));
points.push([ u, v ]);
}
}
If you have UV coordinates for a surface between (0, 0)
(top left) and (1, 1)
(bottom right), you can use linear interpolation to get back pixel values for each coordinate:
const { lerp } = require('canvas-sketch-util/math');
// ...
const x = lerp(minX, maxX, u);
const y = lerp(minY, maxY, v);
Let's say you want a margin of 20px
in your [ width, height ]
artwork, you can do this:
const margin = 20;
const x = lerp(margin, width - margin, u);
const y = lerp(margin, height - margin, v);
If you have UV coordinates between 0..1
, you can get back a simplex noise signal from those coordinates that smoothly varies between -1...1
.
const random = require('canvas-sketch-util/random');
const frequency = 1;
const amplitude = 1;
const n = amplitude * random.noise2D(u * frequency, v * frequency);
The frequency
changes how chaotic the noise signal will be, and the amplitude
can be used to scale the value to something smaller or larger than -1..1
range.
By using a higher dimension noise function, you can animate the noise by a { time }
or { playhead }
variable from canvas-sketch
props.
const settings = {
// Enable animation & time props
animate: true
};
canvasSketch(() => {
return ({ context, time }) => {
const frequency = 1;
const amplitude = 1;
// Use 3D instead of 2D noise
const n = amplitude * random.noise3D(u * frequency, v * frequency, time);
};
}, settings);
If t is between 0 and 1 (inclusive) and you want to map it to -1 to 1 (inclusive), you can use this:
const n = t * 2 - 1;
If t is between -1 and 1 (inclusive) and you want to map it to 0 to 1 (inclusive), you can use this:
const n = t * 0.5 + 0.5;
You can also use the mapRange
function in canvas-sketch-util/math
to map one range of values to another:
const { mapRange } = require('canvas-sketch-util/math');
// Our input lies between -1..1
const value = -0.25;
// Map the input range -1..1 to the output range 25..50
const n = mapRange(value, -1, 1, 25, 50);
This is equivalent to:
const { inverseLerp, lerp } = require('canvas-sketch-util/math');
// Get a 0..1 represenation of the value within the given range
const t = inverseLerp(-1, 1, value);
// Now interpolate that to our output range of 25..50
const n = lerp(25, 50, t);
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2, false);
context.fill();
If you'd like to draw a shape rotated from its centre point, you need to first translate to the point you wish to draw the shape, then rotate, then offset by the center point of the shape. For example:
// Draw a rotated rectangle
const x = 25;
const y = 50;
const rectWidth = 100;
const rectHeight = 30;
const rotation = Math.PI / 4;
context.save();
context.translate(x, y);
context.rotate(rotation);
context.translate(-rectWidth / 2, -rectHeight / 2);
context.fillRect(0, 0, rectWidth, rectHeight);
context.restore();
A unit vector is a really handy construct for doing generative art. You can create one from an angle like so:
// angle is in radians
const normal = [ Math.cos(angle), Math.sin(angle) ];
If you have a 2D unit normal (or an angle, see the previous recipe), you can create a line segment of a specific length and thickness like so:
const { vec2 } = require('gl-matrix');
const normal = [ /* a 2D unit vector ... */ ];
const length = 2;
const thickness = 4;
// The center of the line segment
const center = [ 250, 100 ];
// Extrude in either direction
const line = [ -1, 1 ].map(dir => vec2.scaleAndAdd([], center, normal, dir * length));
// Draw line segment
context.beginPath();
line.forEach(([ x, y ]) => context.lineTo(x, y));
context.lineWidth = thickness;
context.stroke();
To create a looping motion from -1..1
you can use Math.sin()
, like so:
const motionSpeed = 0.5;
const v = Math.sin(time * motionSpeed);
You can map this value into 0..1
space and/or interpolate it to another range.
When you have a defined sketch { duration }
and you are using the { playhead }
prop, you can use Math.sin()
to get a ping-pong motion from 0..1
which slowly varies from 0, to 1, and then back to zero.
const v = Math.sin(playhead * Math.PI);
You can invert this with 1.0 - v
if you need it to vary from 1, to 0, and then back to 1.
In your setup, replace the perspective camera with:
const camera = new THREE.OrthographicCamera();
In the resize
function, replace the perspective camera configuration with:
const aspect = viewportWidth / viewportHeight;
// Ortho zoom
const zoom = 1.0;
// Bounds
camera.left = -zoom * aspect;
camera.right = zoom * aspect;
camera.top = zoom;
camera.bottom = -zoom;
// Near/Far
camera.near = -100;
camera.far = 100;
// Set position & look at world center
camera.position.set(zoom, zoom, zoom);
camera.lookAt(new THREE.Vector3());
// Update the camera
camera.updateProjectionMatrix();
Here's a small reference you can use to remember XYZ axes in ThreeJS.
With glslify (built-in to canvas-sketch CLI), we can import GLSL modules and snippets directly form npm into our shader code.
For example, after running npm install glsl-noise
, you can use this:
// Import glslify
const glsl = require('glslify');
// Wrap your GLSL string with glslify function
const frag = glsl(`
precision highp float;
// We can now import GLSL snippets from npm into this shader
#pragma glslify: noise = require('glsl-noise/simplex/3d');
uniform float time;
varying vec2 vUv;
void main () {
float d = noise(vec3(vUv, time));
vec3 color = vec3(d);
gl_FragColor = vec4(color, 1.0);
}
`);
You can use the following to blend 2D noise seamlessly in a GIF/MP4 loop.
function loopNoise (x, y, t, scale = 1) {
const duration = scale;
const current = t * scale;
return ((duration - current) * random.noise3D(x, y, current) + current * random.noise3D(x, y, current - duration)) / duration;
}