wavy generative animation
I made this one just to get the kick out of it. It's agent-based, and each agent changes in direction according to a set of frequency/amplitude pairs, akin to inverse FFT, only that frequencies are not restricted to bins. The trail of these "agents" is warped according, too, to sin/cos function to get a sort of 3d-wavy effect.
// @ts-check
const canvas = document.createElement('canvas');
const container = document.getElementById('code-target')
const listOfColors1 = [
'#220055',
'#280260',
'#2e0365',
'#34046a',
'#3a056f',
'#400674',
'#460779',
'#4c087e',
'#520983',
'#580a88',
'#5e0b8d',
'#640c92',
'#6a0d97',
'#700e9c',
'#760fa1',
];
const listOfColors2 = [
'#550022',
'#600228',
'#65032e',
'#6a0434',
'#6f053a',
'#740640',
'#790746',
'#7e084c',
'#830952',
'#880a58',
'#8d0b5e',
'#920c64',
'#970d6a',
'#9c0e70',
'#a10f76',
];
if (!container) {
throw new Error('Container not found');
}
container.appendChild(canvas);
const listOfThings = [];
const makeThing = (mycolor) => {
/** @type {Vector2} */
const pos = [0, 0];
/** @type {Vector2} */
const speed = [0, 0];
let index = listOfThings.length;
/** @type {{jumped:boolean, v:Vector2}[]} */
let pastPos = [];
let phase = 0;
let initialized = false;
// representation of frequency and amplitude
const mults = [
[0.5, 1],
// [0.25, 0.3],
// [-0.5, -0.5],
// [0.125, 1],
[-0.0125, -1],
[-0.01245, -1.1],
[0.01, Math.random() * 0.001],
[-2, 0.1],
[2.2, -0.13],
[-2.3, -0.15],
[8, 0.12],
[-12, -0.01],
]
/** apply polar to cartesian into the array of frequency, amplitude pairs*/
const getRotatingSpeed = () => {
let result = [0, 0];
for (let i = 0; i < mults.length; i++) {
const mult = mults[i];
result[0] += Math.sin(phase * mult[0]) * mult[1];
result[1] += Math.cos(phase * mult[0]) * mult[1];
}
return result;
}
/**
* @param {number} dt delta time
* @param {CanvasRenderingContext2D} context
*/
const frame = (dt, context) => {
context.strokeStyle = mycolor
context.lineWidth = 2;
if (!initialized) {
phase = (index / listOfThings.length) * Math.PI * 2;
initialized = true;
}
phase += dt * 0.01
const newSpeed = getRotatingSpeed();
speed[0] = newSpeed[0] * 0.00025;
speed[1] = newSpeed[1] * 0.00025;
const { v: wrappedPos, jumped } = wrapCoords(pos);
let drawPos = spaceToScreenCoords(wrappedPos);
context.beginPath();
const historyLength = 200;
pastPos.forEach(({ v, jumped }, i) => {
/** @type {Vector2} */
const movedPoint = [
v[0] + Math.sin(phase * 0.03 - i / 30) * 0.01,
v[1] + Math.cos(phase * 0.07 - i / 30) * 0.02,
// ...v
// v[1]
]
const drawPoint = spaceToScreenCoords(movedPoint);
if (jumped) {
context.moveTo(...drawPoint);
} else {
context.lineTo(...drawPoint);
}
});
context.stroke();
pos[0] = wrappedPos[0] + speed[0] * dt;
pos[1] = wrappedPos[1] + speed[1] * dt;
pastPos.push({ v: wrappedPos, jumped });
if (pastPos.length > historyLength) {
pastPos.shift();
}
}
const newThing = {
pos,
speed,
frame,
set phase(value) {
phase = value;
},
get phase() {
return phase;
}
}
listOfThings.push(newThing);
return newThing;
}
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Canvas context not found');
}
/**
* @typedef {Object} DrawScope
* @property {CanvasRenderingContext2D} context - canvas context
* @property {number} width -
* @property {number} height -
*/
/**
* @type {DrawScope}
*/
const drawScope = {
context,
width: 0,
height: 0,
}
const view = {
range: [1, 1],
offset: [0.5, 0.5],
}
drawScope.width = canvas.width;
drawScope.height = canvas.height;
/**
* @typedef { [number, number]} Vector2
*/
/**
* @param {Vector2} inputCords
* @returns {{ jumped: boolean, v: Vector2 }}
*/
const wrapCoords = (inputCords) => {
const detransposedCoords = [
inputCords[0] + view.offset[0],
inputCords[1] + view.offset[1],
];
while (detransposedCoords[0] < 0) {
detransposedCoords[0] = 1 + detransposedCoords[0];
}
while (detransposedCoords[1] < 0) {
detransposedCoords[1] = 1 + detransposedCoords[1];
}
/** @type {Vector2} */
const v = [
detransposedCoords[0] % 1,
detransposedCoords[1] % 1,
];
v[0] = v[0] - view.offset[0];
v[1] = v[1] - view.offset[1];
const res = {
v,
jumped: false,
}
const diff = [
Math.abs(res.v[0] - inputCords[0]),
Math.abs(res.v[1] - inputCords[1]),
];
res.jumped = diff[0] > 0.5 || diff[1] > 0.5;
return res;
}
/**
* @param {Vector2} coords
* @returns {Vector2}
*/
const screenToSpaceCoords = ([x, y]) => {
const viewTransformed = [
x / view.range[0] - view.offset[0],
y / view.range[1] - view.offset[1]
];
return [
viewTransformed[0] / drawScope.width,
viewTransformed[1] / drawScope.height
]
}
/**
* @param {Vector2} coords
* @returns {Vector2}
*/
const spaceToScreenCoords = ([x, y]) => {
const viewTransformed = [
(x + view.offset[0]) * view.range[0],
(y + view.offset[1]) * view.range[1]
];
return [
viewTransformed[0] * drawScope.width,
viewTransformed[1] * drawScope.height
]
}
let lastTime = new Date().getTime();
const mouseHistory = [];
const mouseHistoryLength = 255;
/**
* @param {Vector2} point
*/
const addMouseHistoryPoint = (point) => {
mouseHistory.push(point);
if (mouseHistory.length > mouseHistoryLength) {
mouseHistory.shift();
}
}
listOfColors1.forEach((color, a) => {
const thing = makeThing(listOfColors1[a % listOfColors1.length]);
})
listOfColors2.forEach((color, a) => {
const thing = makeThing(listOfColors2[a % listOfColors2.length]);
thing.pos[0] = 0.25
thing.pos[1] = 0.25
thing.phase += 123.1298719;
})
/**
* @param {number} time
* @param {DrawScope} drawScope
*/
let draw = (time, {
context,
width,
height
}) => {
const deltaTime = time - lastTime;
lastTime = time;
context.strokeStyle = 'white';
context.fillStyle = 'white';
context.clearRect(0, 0, width, height);
listOfThings.forEach((thing, i) => {
thing.frame(deltaTime, context);
});
}
const windowResizedListener = () => {
const containerRect = container.getBoundingClientRect();
canvas.width = containerRect.width;
canvas.height = containerRect.width;
drawScope.width = canvas.width;
drawScope.height = canvas.height;
}
window.addEventListener('resize', windowResizedListener);
const frame = () => {
const time = new Date().getTime();
draw(time, drawScope);
requestAnimationFrame(frame);
}
window.addEventListener('load', () => {
windowResizedListener();
});
frame();