Shapes - Circles
Max Bill Circles
Inspired by an artwork of Max Bill from 1938 I started to recreate these in p5.
Drawing a circle with just a stroke is easy enough. So I wrote a function that would creat a desired amount of circles at a certain position. Then the position and the radius for each circle could variate a bit and choose a random color (out of a predefined color array).
function maxBillCircles(count, x, y, radius, radiusDifference, stroke_Weight, strokeDifference, colors){
let smallestRadius = radius/4;
for(let i = 0; i < count; i++){
let diffForStrokeWeight = Math.random() * strokeDifference * stroke_Weight;
strokeWeight(diffForStrokeWeight)
let colorIndex = Math.floor(Math.random() * colors.length);
stroke(colors[colorIndex]);
let diffForRadius = Math.random() * radiusDifference;
let circleX = x + Math.random() * smallestRadius * (random(0,1)>= 0.5 ? 1 : -1);
let circleY = y + Math.random() * smallestRadius * (random(0,1)>= 0.5 ? 1 : -1);
circle(circleX,circleY,radius*diffForRadius);
}
}
The maxBillCircles function gets called in the setup function and draws the circles initially when the page is loaded.
function setup() {
...
blendMode(DIFFERENCE);
background('#222222');
noFill();
strokeWeight(25);
maxBillCircles(7, windowWidth/2, windowHeight/2, windowWidth/4, 1.3, 30, 1.2, ['red', 'limegreen', 'midnightblue', 'white']);
}
Max Bill Circles Animated
After creating the circles and loading the page again and again to create new images I wanted to make an animated version. This would be an endless animation of ever changing compositions which could run for ever.
To accomplish this I saved all circles in a array, create a array of new circles and then draw all the steps between them.
function createMaxBillCircles(count, x, y, positionDifference, radius, radiusDifference, stroke_Weight, strokeDifference, colors) {
let circles = [];
for(let i = 0; i < count; i++){
let maxBillCircle = createMaxBillCircle(x, y, positionDifference, radius, radiusDifference, stroke_Weight, strokeDifference, colors);
circles.push(maxBillCircle);
}
return circles;
}
function createMaxBillCircle(x, y, positionDifference, radius, radiusDifference, stroke_Weight, strokeDifference, colors) {
let circle = {};
circle.strokeWeight = stroke_Weight + (Math.random() * strokeDifference * stroke_Weight * (random(0,1)>= 0.5 ? 1 : -1));
circle.color = colors[Math.floor(Math.random() * colors.length)];
circle.radius = radius + (Math.random() * radiusDifference * radius * (random(0,1)>= 0.5 ? 1 : -1));
circle.x = x + Math.random() * positionDifference * (random(0,1)>= 0.5 ? 1 : -1)*x;
circle.y = y + Math.random() * positionDifference * (random(0,1)>= 0.5 ? 1 : -1)*y;
return circle;
}
Because the circles will need to be drawn a lot I excluded the drawing in a separate function.
function drawMaxBillCircles() {
maxBillCirclesBuffer.forEach(function (circleItem, index){
stroke(circleItem.color);
strokeWeight(circleItem.strokeWeight);
circle(circleItem.x, circleItem.y, circleItem.radius);
})
}
drawMaxBillCircles() always draws the current frame on screen. All information for this is saved in maxBillCirclesBuffer and createMaxBillCirclesBuffer() updates the buffer when needed.
function createMaxBillCirclesBuffer(){
for(let i = 0; i < maxBillCircles.length; i++){
maxBillCirclesBuffer[i].x += (maxBillCirclesNext[i].x - maxBillCircles[i].x)/steps;
maxBillCirclesBuffer[i].y += (maxBillCirclesNext[i].y - maxBillCircles[i].y)/steps;
maxBillCirclesBuffer[i].radius += (maxBillCirclesNext[i].radius - maxBillCircles[i].radius)/steps;
maxBillCirclesBuffer[i].strokeWeight += (maxBillCirclesNext[i].strokeWeight - maxBillCircles[i].strokeWeight)/steps;
maxBillCirclesBuffer[i].color = lerpColor(maxBillCircles[i].color, maxBillCirclesNext[i].color, 1/steps*currentStep);
}
}
Everything for the animation is managed within the p5 draw() function. The time it takes for the animation is defined with the count of steps (Frames) in the setup() function.
function draw() {
blendMode(BLEND);
background('#222222');
blendMode(HARD_LIGHT);
if (millis() >= 5000+timer) { // every 5000ms
timer = millis();
maxBillCirclesNext = createMaxBillCircles(maxBillCircleOptions.count, maxBillCircleOptions.x, maxBillCircleOptions.y, maxBillCircleOptions.positionDifference, maxBillCircleOptions.radius, maxBillCircleOptions.radiusDifference, maxBillCircleOptions.stroke_Weight, maxBillCircleOptions.strokeDifference, maxBillCircleOptions.colors)
}
if(maxBillCirclesNext.length > 0){
if(currentStep < steps){
createMaxBillCirclesBufferNonLinear();
currentStep++;
} else {
currentStep = 0;
maxBillCircles = maxBillCirclesNext;
maxBillCirclesNext = [];
}
}
drawMaxBillCircles();
}
Max Bill Circles Easing
With the current animation working fine I wanted to add a bit of smoothness to it. Luckly we already used a function for cosine-interpolation in Beautiful Mathematics (for curves with noise). In the Buffer function the current step (percentage of the movement) is always clear so it was easier than expected to make it work.
function createMaxBillCirclesBufferNonLinear(){
for(let i = 0; i < maxBillCircles.length; i++){
maxBillCirclesBuffer[i].x = cosine_ip(maxBillCircles[i].x, maxBillCirclesNext[i].x, currentStep/steps);
maxBillCirclesBuffer[i].y = cosine_ip(maxBillCircles[i].y, maxBillCirclesNext[i].y, currentStep/steps);
maxBillCirclesBuffer[i].radius = cosine_ip(maxBillCircles[i].radius, maxBillCirclesNext[i].radius, currentStep/steps);
maxBillCirclesBuffer[i].strokeWeight = cosine_ip(maxBillCircles[i].strokeWeight, maxBillCirclesNext[i].strokeWeight, currentStep/steps);
maxBillCirclesBuffer[i].color = lerpColor(maxBillCircles[i].color, maxBillCirclesNext[i].color, cosine_ip(0, 1, currentStep/steps));
}
}
function cosine_ip(a,b,x) {
angleMode(RADIANS);
let ft = x * Math.PI;
let f = (1 - cos(ft)) * .5;
return a*(1-f) + b*f
}
Max Bill Circles Animated final
Everything combined creates unique compositions with a cool easing. I added some sliders for all input parameters so everyone can easily experiment with the code and customize it.
let gui_options = new dat.GUI();
//gui_options.add(maxBillCircleOptions, 'count', 1, 100);
// count cant be changed during running -> animation requires always same amount of circles
gui_options.add(maxBillCircleOptions, 'x', 0, windowWidth);
gui_options.add(maxBillCircleOptions, 'y', 0, windowHeight);
gui_options.add(maxBillCircleOptions, 'positionDifference', 0, 2);
gui_options.add(maxBillCircleOptions, 'radius', screenDiagonal/10, screenDiagonal);
gui_options.add(maxBillCircleOptions, 'radiusDifference', 0, 2);
gui_options.add(maxBillCircleOptions, 'stroke_Weight', screenDiagonal/50, screenDiagonal/5);
gui_options.add(maxBillCircleOptions, 'strokeDifference', 0, 2);
After the GUI initialization I just needed to use these values in the draw() function.
function draw() {
...
if (frameCount%(5*fr) === 0) { // every 5 Seconds
frameCount = 0;
maxBillCirclesNext = createMaxBillCircles(maxBillCircleOptions.count, maxBillCircleOptions.x, maxBillCircleOptions.y, maxBillCircleOptions.positionDifference, maxBillCircleOptions.radius, maxBillCircleOptions.radiusDifference, maxBillCircleOptions.stroke_Weight, maxBillCircleOptions.strokeDifference, maxBillCircleOptions.colors)
}
...
frameCount++;
}
Patterns
Triangles
To create a good looking pattern with triangles i opted to use a polygon function which i found on the p5 site.
Then I created a function that would draw a single tile which I could place multiple times to create a pattern.
function triangleTile(count, size, x, y, angle=0){
let radius = size/2;
let newY = y;
if(angle > 180) {
newY = y-radius/2.45;
}
let shrinkStep = (radius - radius/4) / count
for(let i = 1; i <= count; i++) {
radius = size/2 - i*shrinkStep;
push();
translate(x, newY+size/9);
angleMode(DEGREES);
rotate(angle);
if(i < colors.length){
fill(colors[i])
} else {
fill(colors[colors.length-1])
}
polygon(0, 0, radius, 3);
pop();
}
}
function setup() {
...
background(colors[0]);
noStroke();
let size = 300;
let countX = (windowWidth / size / 0.866) * 2;
let countY = windowHeight/size / 0.666;
for(let i = 0; i < countY+1; i++) {
for(let j = 0; j < countX+1; j++) {
let angle = j%2;
triangleTile(4, size,windowWidth/countX*j, windowHeight/countY*i, angle*180+30);
}
}
}
Triangles mistakes
While trying to better the code and setting the position of the triangles based on variables that have values relative to the screen some cool looking mistakes were made.
function triangleTile(count, size, x, y, angle=0){
...
push();
angleMode(DEGREES);
translate(x, newY+size/9+radius);
rotate(angle);
...
}
Triangles Animated
A still pattern is fine but an animated pattern is much more fun. So I thought about how to animate it and implemented a parallax like effect.
let positionDifference = Math.abs(mouseX-x)+Math.abs(mouseY-newY);
radius = size/2 - i*shrinkStep;
push();
angleMode(DEGREES);
translate(x+(mouseX-x)/40*i, newY+size/9+(mouseY-newY)/40*i);
rotate(angle);
After an input about colors I wanted to add a Color effect to it.
if(flashlightMode){
colorMode(HSB);
let luminance = 100-positionDifference/12;
if(luminance < 15) luminance = 15;
fill(color(20+i*10, 60, luminance))
}
Rewriting MaxBill Circles
I wanted to create something with sound and circles. To do this I wanted to start with better more understandable circle Code. Because of that I decided to rewrite it in a class orientated way. I created a MaxBillCircle class with different functions. This way all information to one circle is kept together and it is easy to change the position of one specific circle.
With the new code I started to experiment with the circles again and created some patterns in the process.
class MaxBillCircle {
baseValues = {};
position = {};
drawingPosition = {};
nextPosition = {};
positionDifference;
radiusDifference;
strokeWeightDifference;
colors;
moving = 0;
animationSteps;
currentAnimationStep = 0;
constructor(x, y, radius, strokeWeight, colors, positionDifference, radiusDifference, strokeWeightDifference, animationSteps) {
...
this.createRandomNextPosition();
this.position = deepCopyPosition(this.nextPosition);
}
createRandomNextPosition() {
this.nextPosition.x = this.baseValues.x + (Math.random() * this.positionDifference * (Math.random()>= 0.5 ? 1 : -1) * this.baseValues.x);
this.nextPosition.y = this.baseValues.y + (Math.random() * this.positionDifference * (Math.random()>= 0.5 ? 1 : -1) * this.baseValues.y);
this.nextPosition.radius = this.baseValues.radius + (Math.random() * this.radiusDifference * (Math.random()>= 0.5 ? 1 : -1) * this.baseValues.radius);
this.nextPosition.strokeWeight = this.baseValues.strokeWeight + (Math.random() * this.strokeWeightDifference * (Math.random()>= 0.5 ? 1 : -1) * this.baseValues.strokeWeight);
this.nextPosition.color = this.colors[Math.floor(Math.random() * this.colors.length)];
}
setNextPosition(x, y, radius, strokeWeight, color) {
...
}
startMoving() { //called manually after a new next position has been set.
if(this.moving){ //current Moving interrupted. Animate from current Position to new one
this.stopMoving();
}
this.moving = 1;
}
stopMoving() { //called when nextPosition is reached or moving gets interrupted
this.position = deepCopyPosition(this.drawingPosition);
this.moving = 0;
this.currentAnimationStep = 0;
}
updateDrawingPosition() { //called in every draw. Interpolating the position based on current animation progress
if(!this.moving){
this.drawingPosition = deepCopyPosition(this.position);
} else {
if(this.currentAnimationStep <= this.animationSteps) {
this.drawingPosition.x = cosine_ip(this.position.x, this.nextPosition.x, this.currentAnimationStep / this.animationSteps);
this.drawingPosition.y = cosine_ip(this.position.y, this.nextPosition.y, this.currentAnimationStep / this.animationSteps);
this.drawingPosition.radius = cosine_ip(this.position.radius, this.nextPosition.radius, this.currentAnimationStep / this.animationSteps);
this.drawingPosition.strokeWeight = cosine_ip(this.position.strokeWeight, this.nextPosition.strokeWeight, this.currentAnimationStep / this.animationSteps);
this.drawingPosition.color = lerpColor(this.position.color, this.nextPosition.color, cosine_ip(0, 1, this.currentAnimationStep / this.animationSteps));
this.currentAnimationStep++;
if (this.currentAnimationStep > this.animationSteps) {
this.stopMoving();
}
}
}
}
draw() { //used to draw the circle
this.updateDrawingPosition();
blendMode(HARD_LIGHT);
noFill();
strokeWeight(this.drawingPosition.strokeWeight);
stroke(this.drawingPosition.color);
circle(this.drawingPosition.x, this.drawingPosition.y, this.drawingPosition.radius);
blendMode(BLEND);
}
}
There are multiple times where I need to set the position to the nextPosition or the position to the drawingPosition. Since in js object are passed by reference this caused a big headache for me. I had to wirte a specific deepCopy function so the values i need to be copied get copied and the objects within the object (namely the color() object at the moment) get passed by reference.
function deepCopyPosition(positionObj) {
return {
x: positionObj.x,
y: positionObj.y,
radius: positionObj.radius,
strokeWeight: positionObj.strokeWeight,
color: positionObj.color
};
}
Adding Sliders to the pattern
Because there are so many parameters to play around with I wanted to add some sliders to play with the sketch on the go. Soooo I ended up with a lot of sliders.
dat.GUI is really easy to use and brings a lot of functionality. Since I have 3 types of circles I had to copy the almost identical code multiple times...which seemed like inefficient and ugly task. But as long as it works for now I am fine.
let smallCirclesGUI = new dat.GUI();
smallCirclesGUI.add(smallCircleOptions, 'radius', 1, screenDiagonal/2, 1);
smallCirclesGUI.add(smallCircleOptions, 'strokeWeight', 1, screenDiagonal/10, 1);
smallCirclesGUI.add(smallCircleOptions, 'positionDifference', 0, 2, .01);
smallCirclesGUI.add(smallCircleOptions, 'radiusDifference', 0, 2, .01);
smallCirclesGUI.add(smallCircleOptions, 'strokeWeightDifference', 0, 2, .01);
smallCirclesGUI.addColor(smallCircleOptions, 'color1');
smallCirclesGUI.addColor(smallCircleOptions, 'color2');
smallCirclesGUI.addColor(smallCircleOptions, 'color3');
After Creating the GUI I only had to update the values in the draw() function.
function draw() {
background(color(generalOptions.backgroundColor));
if (frameCount%(fr*generalOptions.animationStartTimerS) === 0) {
smallCircles.forEach(function(circle){
circle.setDifferences(smallCircleOptions.positionDifference, smallCircleOptions.radiusDifference, smallCircleOptions.strokeWeightDifference);
circle.setColors([color(smallCircleOptions.color1), color(smallCircleOptions.color2), color(smallCircleOptions.color3)]);
circle.setRadius(smallCircleOptions.radius);
circle.setStrokeWeight(smallCircleOptions.strokeWeight);
});
...
circles.forEach(function(circleGroup){
circleGroup.forEach(function(circle){
circle.setAnimationSteps(generalOptions.animationDurationS*fr);
circle.createRandomNextPosition();
circle.startMoving();
});
});
newPositionCount++
frameCount = 0;
}
circles.forEach(function(circleGroup){
circleGroup.forEach(function(circle){
circle.draw();
});
});
frameCount++;
}
Because the screen is crowded with all these sliders I also added a toggle Button to hide dat GUI completely.
Starting with the sound library
With the new and clean code I wanted to make the behaviour of the circle dependent on sound. I started with a "simple" change of the radius based on the current volume level of the microphone. Dennis Düblin wrote a great repo last week which made the start really easy for me.
function setup() {
...
soundCircle = new MaxBillCircle(windowWidth/2, windowHeight/2, radius, strokeWeight, colors, 0,0,0, 2);
mic = new p5.AudioIn()
fft = new p5.FFT();
fft.setInput(mic);
mic.start();
}
function draw() {
background('#222222');
...
if(frameCount%2 === 0){ // Every second
soundCircle.setNextPosition(windowWidth/2, windowHeight/2, mic.getLevel()*10_000, screenDiagonal/80, color('blue'))
soundCircle.startMoving();
frameCount = 0;
}
...
}
Adding multiple sound circles
I then wanted to add multiple circles for different audio frequencies. This worked at some point but sadly I messed up something and can't get it working anymore.
function draw() {
...
soundValues.bass = fft.getEnergy("bass");
soundValues.lowMid = fft.getEnergy( "lowMid" );
soundValues.mid = fft.getEnergy( "mid" );
soundValues.highMid = fft.getEnergy( "highMid" );
soundValues.treble = fft.getEnergy( "treble" );
soundValues.mapTrebleC = map(soundValues.treble, 0,255, 0,screenDiagonal/3);
soundValues.mapMidC = map(soundValues.mid, 0,255, 0,screenDiagonal/3);
soundValues.mapBassC = map(soundValues.bass, 0,255, 0,screenDiagonal/3);
if(frameCount%2 === 0){ // Every second
volumeCircle.setNextPosition(windowWidth/2, windowHeight/2, volEx, screenDiagonal/80)
volumeCircle.startMoving();
bassCircle.setNextPosition(windowWidth/2, windowHeight/2, soundValues.mapBassC, screenDiagonal/80)
bassCircle.startMoving();
midCircle.setNextPosition(windowWidth/2, windowHeight/2, soundValues.mapMidC, screenDiagonal/80)
midCircle.startMoving();
trebleCircle.setNextPosition(windowWidth/2, windowHeight/2, soundValues.mapTrebleC, screenDiagonal/80)
trebleCircle.startMoving();
frameCount = 0;
}
...
}
Creating a flower pattern
Because I already made a Flower with the old circle code I wanted to do it again with this pattern. I had to rewrite some parts of the class. The strokeWidth and the strokeWidth-Difference are no longer needed. Instead I had to add the number of leaves (leaveCount) and a function to calculate the width of the flowerLeaves.
class MaxBillCircleFlower {
...
constructor(x, y, radius, leaveCount, colors, positionDifference, radiusDifference, radiusHDifference, animationSteps) {
...
this.baseValues.leaveCount = leaveCount;
this.radiusHDifference = radiusHDifference;
...
}
...
createRandomNextPosition() {
...
this.nextPosition.radiusH = this.baseValues.radius/ 6 + (Math.random() * this.radiusHDifference * (Math.random()>= 0.5 ? 1 : -1) * this.baseValues.radius/ 6) ;
...
}
}