Concept
The goal of this week’s assignment was to create a unique work of art through imagination and creativity. The original idea was to create a dynamic screensaver with a range of shapes that had different colors and speeds and interacted with the canvas and each other when they collided. However, this concept was disregarded because a more abstract art piece was needed.
So I used the original code as the template, and the project developed into a complex particle-mimicking display of forms. These particles used the idea of flow fields to mimic more organic and natural motions, leaving behind a vibrant trail as they moved. The chaos of these trails of particles is a representation of my “State of Mind” while thinking of ideas for the assignment.
Sketches
Code
The particles’ motion is directed by a grid of vectors called the flow field. Perlin noise is used in its generation to provide a fluid, flowing transition between the grid’s vectors.
for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { let index = i + j * cols; let angle = noise(i * 0.1, j * 0.1, zoff) * TWO_PI * 4; flowField[index] = p5.Vector.fromAngle(angle); } }
The snippet is in the draw() loop that creates the flow field. It simulates a natural flow by using Perlin noise to generate vectors with smoothly shifting angles.
The shapes follow the flow field vectors, which guide their movement. This interaction is encapsulated in the follow() method of the BaseShape class.
follow(flowField) { let x = floor(this.pos.x / resolution); let y = floor(this.pos.y / resolution); let index = x + y * cols; let force = flowField[index]; this.vel.add(force); this.vel.limit(2); }
The shape’s position (this.pos) is used to determine its current cell in the flow field grid by dividing by the resolution. The index in the flowField array corresponding to the shape’s current cell is calculated using x + y * cols.
The vector (force) at this index is retrieved from the flowField array and added to the shape’s velocity (this.vel), steering it in the direction of the flow. this.vel.limit(2) ensures that the shape’s velocity does not exceed a certain speed, maintaining smooth, natural movement.
The trail effect is created by not fully clearing the canvas on each frame, instead drawing a semi-transparent background over the previous frame. This technique allows shapes to leave a fading trail as they move.
background(0, 0.3);
The purpose of the resetAnimation() method is to reset the animation setup and clean the canvas. To restart the flow field pattern, it first uses clear() to remove any existing shapes from the canvas, resets the shapes array to its initial state, initializeShapes() method to add a new set of randomly placed shapes, and resets the zoff variable for flow field noise.
function resetAnimation() { clear(); // Clear existing shapes shapes = []; // Repopulate with new shapes initializeShapes(); // Reset the z-offset for flow field noise zoff = 0; }
The resetAnimation() method is called when the frameCount has reached 500 frames. This helps to see how the flow field changes every time it restarts.
Full Code
// array for shapes let shapes = []; // declare flow field variable let flowField; let resolution = 20; // 2d grid for flow field let cols, rows; // noise increment variable let zoff = 0; // make the sketch again after this value let resetFrameCount = 500; function setup() { createCanvas(800, 600); colorMode(HSB, 255); blendMode(ADD); cols = floor(width / resolution); rows = floor(height / resolution); flowField = new Array(cols * rows); initializeShapes(); } function draw() { if (frameCount % resetFrameCount === 0) { resetAnimation(); } else { background(0, 0.3); } // Flow field based on Perlin noise for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { let index = i + j * cols; let angle = noise(i * 0.1, j * 0.1, zoff) * TWO_PI * 4; flowField[index] = p5.Vector.fromAngle(angle); } } // Increment zoff for the next frame's noise zoff += 0.01; // Update and display each shape // For each shape in the array, updates its position according to the flow field, moves it, displays its trail, and display it on the canvas. shapes.forEach(shape => { shape.follow(flowField); shape.update(); shape.displayTrail(); shape.display(); shape.particleReset(); shape.finish(); }); } function resetAnimation() { clear(); // Clear existing shapes shapes = []; // Repopulate with new shapes initializeShapes(); // Reset the z-offset for flow field noise zoff = 0; } // Initialized 30 number of shapes in random and at random positions function initializeShapes() { for (let i = 0; i < 30; i++) { let x = random(width); let y = random(height); // Randomly choose between Circle, Square, or Triangle let type = floor(random(3)); if (type === 0) shapes.push(new CircleShape(x, y)); else if (type === 1) shapes.push(new SquareShape(x, y)); else shapes.push(new TriangleShape(x, y)); } } class BaseShape { constructor(x, y) { this.pos = createVector(x, y); this.vel = p5.Vector.random2D(); this.size = random(10, 20); this.rotationSpeed = random(-0.05, 0.05); this.rotation = random(TWO_PI); this.color = color(random(255), 255, 255, 50); this.prevPos = this.pos.copy(); } follow(flowField) { let x = floor(this.pos.x / resolution); let y = floor(this.pos.y / resolution); let index = x + y * cols; let force = flowField[index]; this.vel.add(force); // Smoother movement so velocity is limited this.vel.limit(2); } update() { this.pos.add(this.vel); this.rotation += this.rotationSpeed; } display() { // Saves the current drawing state push(); // Translates the drawing context to the shape's current position translate(this.pos.x, this.pos.y); // Rotates the shape's current rotation angle rotate(this.rotation); fill(this.color); noStroke(); } displayTrail() { strokeWeight(1); // Creates a semi-transparent color for the trail. It uses the HSB let trailColor = color(hue(this.color), saturation(this.color), brightness(this.color), 20); stroke(trailColor); // Draws a line from the shape's current position to its previous position line(this.pos.x, this.pos.y, this.prevPos.x, this.prevPos.y); } finish() { this.updatePrev(); pop(); } updatePrev() { this.prevPos.x = this.pos.x; this.prevPos.y = this.pos.y; } particleReset() { if (this.pos.x > width) this.pos.x = 0; if (this.pos.x < 0) this.pos.x = width; if (this.pos.y > height) this.pos.y = 0; if (this.pos.y < 0) this.pos.y = height; this.updatePrev(); } } class CircleShape extends BaseShape { display() { super.display(); ellipse(0, 0, this.size); super.finish(); } } class SquareShape extends BaseShape { display() { super.display(); square(-this.size / 2, -this.size / 2, this.size); super.finish(); } } class TriangleShape extends BaseShape { display() { super.display(); triangle( -this.size / 2, this.size / 2, this.size / 2, this.size / 2, 0, -this.size / 2 ); super.finish(); } }
Challenges
I spent a lot of time getting a grasp of noise functions and how to use them to mimic natural movements for my shapes and implement the flow field using Perlin noise. There was considerable time spent adjusting the noise-generating settings to produce a smooth, organic flow that seemed natural.
There was a small challenge to clear the canvas after a certain amount of frames. The logic of the code was fine however the previous shapes were not removed.
Reflections and Improvements
The overall experience was quite helpful. I learned to handle the different functions and classes using object-oriented programming concepts. It made it possible to use a modular. It even helped me to add more features to my code as I was creating my art piece.
I believe that there is one area of development where I could explore various noise offsets and scales to create even more varied flow fields. Playing around with these parameters might result in more complex and eye-catching visual effects.
References
I learned about flow fields through the resources provided to us. I discovered a YouTube channel, “The Coding Train“.