When I first visit the website of [AntFu](https://antfu.me/), I was amazed by the background animation⬇️. Though interested, I thought it would be too complex for me to try.
Until recently, I went to Denver and my host is an artist. She told me it's maybe a generative-art and it's not so hard once you've learned the underlying algorithm. My fear is reduced after knowing what the animation is, and in fact, AntFu has also shared lots of knowledge of such an animation in [his stream](https://www.bilibili.com/video/BV1wY411n7er/) or [his project](https://github.com/antfu/100). Inspired by AntFu's work, I started trying such animation with the one I always want to create -- Ice. ( whole process of my learning&trying journey: https://hey.xyz/posts/0x72ab-0xd3-DA-a73d2e10)
How to code an animation of generative art?
Generally, we need 3 ingredients to cook such an animation
- A canvas to draw our animation
- An algorithm of how the work generated
- Make the generation process as an animation
Now, let's start coding the Ice animation. Little warning: this article isn't a strickly step by step coding guide, I of course provide codes, but would skip some trivial steps and you can find all codes here.
Use canvas with react
Lot's of articles has talked about the topic and I won't repeat it here. You can learn how to do so from [this article](https://medium.com/@pdx.lucasm/canvas-with-react-js-32e133c05258). And I also coded a [component for your reference or use]( https://github.com/Ricy137/Ricy/blob/main/src/components/Canvas/index.tsx)
Code the Algorithm
We gonna with ImageData
object and putImageData
method to pixel manipulate canvas. Learn more details here.
The algorithm of the Ice animation involves mainly two parts: color change and new Points/pixel adding.
The color:
Take a closer look to the animation, the color is generally change from white to deep blue. In fact, I've pre-defined 6 levels of blue and 1 white which is 7 different colors as my color pallet for the color transition: [ '#ffffff', '#caf0f8', '#ade8f4', '#90e0ef', '#48cae4', '#00b4d8', '#0096c7']
. The colors is chosen from Coolors.
However, it would be too dumn if the color just transitioned bluer and bluer as the color predefined.
We want more randomness, more layers and more fun!
Let's turn these color from Hex to RGB format(or use RGB format in the beginning) for further manipulation.
//@/utils/colors.ts
export function hexToRgb(hex: string): ColorVector {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
return [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16),
];
}
//@/utils/canvas/ice.ts
const pattele = [
'#ffffff',
'#caf0f8',
'#ade8f4',
'#90e0ef',
'#48cae4',
'#00b4d8',
'#0096c7',
].map(hexToRgb);
As stated, we're gonna pixel manipulate the canvas, so we provide a number n ∈ [0,1] to represent the blueness of each pixel, the closer n to 1, the bluer the pixel is.
Code the colorInterpration
helper function as below, now we turn the discrete color picker function into a linear one, providing more options for the color transition process, and due to the randomness of n ( we would talk about it later), randomness is also introduced in the process.
/**
* @param vectors
* @param num 0~1
*/
export type ColorVector = [number, number, number];//which is the color represented in RGB format
export function colorInterpration(vectors: ColorVector[], n: number) {
if (n >= 1) return vectors[vectors.length - 1];
const normalized = clamp(n, 0, 1) * (vectors.length - 1);
const integer = Math.trunc(normalized);
const frac = normalized - integer;
const nfrac = 1 - frac;
const [a1, a2, a3] = vectors[integer];
const [b1, b2, b3] = vectors[integer + 1];
return [
a1 * nfrac + b1 * frac,
a2 * nfrac + b2 * frac,
a3 * nfrac + b3 * frac,
];
}
Points/pixels adding/updating
First, we need a 2D arrays, iceNodes, to represent the blueness of each pixel, or to contain the n of each pixel. Initialize the array as below:
let iceNodes: number[][] = [];
iceNodes = new Array(width)
.fill(0)
.map((_, i) => i)
.map((i) => new Array(height).fill(0));
The algorithm/generation process of the Ice is to start from one active point, generally deep the blueness of itself and adding the points around the starter point into active points. Then active points would deep the blueness of themselves and adding new points around them each into the active points group. Keep the recursion process util we reached the maximum iteration times, iterations .
export type Vector = [number, number]; // which is the x, y position of the point
class Ice {
constructor(public activePoints: Vector[], public iteractions = 5) {}
next() {
if (!this.iteractions) return;
this.iteractions -= 1;
const newPoints: Vector[] = [];
this.activePoints.forEach((point) => {
const [x, y] = point;
iceNodes[x][y] += randomWithRange(0.1, 0);
const points: Vector[] = [
[x, y],
[x, y + 1],
[x + 1, y],
[x, y - 1],
[x - 1, y],
[x + 1, y + 1],
[x + 1, y - 1],
[x - 1, y + 1],
[x - 1, y - 1],
];
// This is to add random ness and reduce repeatability. We won't push new points if all the newPoints have already belong to active points
//and we won't push points which are outside the canvas or with the deepest blueness, and randomly add newPoints in other case
newPoints.push(
...points
.filter((v) => !newPoints.some((n) => n[0] === v[0] && n[1] === v[1]))
.filter((v) => inbound(v))
.filter(([x, y]) => {
if (iceNodes[x][y] === 0) return true;
if (iceNodes[x][y] >= 1) return false;
if (iceNodes[x][y] > 0.8) return randomWithRange() > 0.5;
else return randomWithRange() > 0.2;
})
);
});
this.activePoints = sampleSize(newPoints, 200);
}
}
Update canvas and make the process as an animation
Now, we understand the high level algorithm of the animation. But how exactly can we manipulate canvas to really present the animation?
First, after the next() method of Ice class, some pixels' n are updated, but our canvas doesn't know about it. So we need to update ImageData to ensure the blueness of each pixel in our canvas is also changed.
function updateCanvas() {
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const iceNode = iceNodes[x][y];
const [r, g, b] = colorInterpration(pattele, iceNode);
const pixelindex = (y * width + x) * 4;
data.data[pixelindex] = r;
data.data[pixelindex + 1] = g;
data.data[pixelindex + 2] = b;
data.data[pixelindex + 3] = 255;
}
}
ctx.putImageData(data, 0, 0);
}
Then, where's the animation? Animation is easy, animation can be created by update canvas in a frequency like flipbook.
Before starting making the drawing a canvas process an animation, I want to add something to make the animation more outstanding. To create a beautiful Ice animation, we need more than one Ice object instance. I used 4 Ice objects from each side of our canvas in the animation. These 4 objects formed the iceField
array.
Now, start the animation as below:
const frame = () => {
tick++;
for (let i = 0; i < iterations; i++) {
iceField.forEach((i) => {
i.next();
i.next();
i.next();
i.next();
});
}
updateCanvas();
if (tick >= maxTicks) throw new Error('done');
};
const start = () => {
iceField = [
new Ice(
[
[0, Math.trunc(randomWithRange(400))],
[Math.trunc(randomWithRange(400)), 0],
[399, Math.trunc(randomWithRange(400))],
[Math.trunc(randomWithRange(400)), 399],
],
maxTicks * iterations
),
new Ice(randomVectors(40), (maxTicks * iterations) / 2),
new Ice(randomVectors(3), (maxTicks * iterations) / 1.5),
];
let frameCount = 0;
const startFrame = () => {
try {
frameCount++;
requestAnimationFrame(() => {
if (!(frameCount % 3)) {//this is to control the frequency of calling frame();
frame();
}
startFrame();
});
} catch (e) {}
};
startFrame();
};
The animation effect is achieved by requestAnimationFrame
function ( it's very similar to setTimeout, read the difference here). I used frameCount variable to control the frequency to call frame function, thus the frequency to update canvas, and variable tick and maxTicks to control the maximum frames can be created/ the maximum calling times to frame function.
Tada! Now we got a cool Ice animation!
Thanks for reading and all the codes are here for your reference.
Welcome to my Lens @cuckooir for communication <3 ~