I'm new to P5.js and JavaScript programming in general, but I've taken a few lessons and am trying to make a few interesting images with what I know. However, I reached a few roadblocks while trying to program a constantly moving screensaver.
The project I'm trying to make is an expanding circle within a set of coordinates in an array that spread outwards, repeating continuously at a different coordinate after it reaches the edge of the canvas. This is supposed to mimic the waves that raindrops make on water.
The problems I'm facing are that I don't know how to make the circles appear one by one and at random, rather than all at once. Additionally, I have a function that changes the color each circle is from the hex values within another array, but for some reason it only remains the singular color rather than changing.
Any advice at all would help, but I would preferably be able to use the specific points on the location arrays.
let size = ["25"];
//The size each circle starts at
let colorShift;
//Empty value to make an array
let locationsX = ["50", "50", "100", "150", "100", "150", "200", "250", "300", "350", "300", "250", "350", "50", "100", "300", "350", "200", "200", "200", "200"];
let locationsY = ["50", "350", "300", "250", "100", "150", "200", "250", "300", "50", "100", "150", "350", "200", "200", "200", "200", "50", "100", "300", "350"];
//The points in which I want circles to appear
function setup() {
createCanvas(400, 400);
background("#004CC2");
colorShift = [color("#00C2BF"), color("#33FFFC"), color("#33A2FF"), color("#3380FF")];
//The colors within the array
}
function draw() {
stroke("white");
strokeWeight(1);
colorChanging();
//Calls the color changing function
for (i = 0; i < locationsX.length; i++) {
circle(locationsX[i], locationsY[i], size[0]);
}
//Issue, creates all circles within the array coordinates
//Goal, create an individual circle randomly from one of the coordinates in the array, continuously
for (i = 0; i < size.length; i++) {
size[i]++;
if (size[i] > 100) { //Test Size
//if (size[i] > 600) { //Final Size
size[i] = 0;
}
}
//Expands the circles until the specified size, wherein they reset
}
function colorChanging() {
for (i = 0; i < colorShift.length; i++) {
fill(colorShift[i]);
//colorShift[i]++;
//Issue, does not shift through the color array, only one color or blank white when using colorShift[i]++
if (colorShift[i] > 3) {
colorShift[i] = 0;
//Will reset the color if it exceeds the colors in the array
}
//Issue, does not shift through the color array, only one color or blank white
}
//Goal, shift the inside of each circle between the colors in array
}
Reaching a few roadblocks is a good reminder to pause, break the problem down into smaller, simpler problems then tackle one at a time.
Your question mainly is about how to update circle properties (size and color) independently to create a raindrop like effect.
The animation looks uniform because you are updating all the circles at the same time as well as the circles starting with the same size (25). Simply randomising the initial size of the circles can help the illusion.
There are quite a few opportunities to improve your code. The intention is not to discourage, on the contrary, to hopefully help build better habits early on. You already do a few good things:
Now on the improvements:
Try not to mix data types (to avoid unwanted behaviour).
JS is not strictly typed. size
, locationsX
and locationsY
are initialised with string values (e.g. size contains "25"
), when it should be Number values (e.g. size = [25]
(no quotes)).
If you want to transition from one color to the next you can use lerpColor()
. It takes 3 arguments:
(You could map the circle size, for example, in this normalised range given that you know the current and maximum size of the circle (e.g. size / maxSize
will be a ratio from 0.0 to 1.0). in other scenarios where the number ranges arent as straight forward to remap you can use the map()
function)
There a few drawing function calls that could move between setup()
and draw()
stroke()
and strokeWeight()
don't change values so after calling once become somewhat redundant. these calls could move to setup()
background()
is called once in setup()
. Given you want to animate these circles changing size, background()
should move at the start of draw()
to clear/erase the previous frame.Additionally you're re-using the same circle size for all circles drawn (even if you're updating sizes indepdently (and the size
array was initialised with one value). This may be a left-over from incrementally changing the sketch from one circles to many circles. (i.e. circle(locationsX[i], locationsY[i], size[0]);
should be circle(locationsX[i], locationsY[i], size[i]);
)
With the smallest number of changes your code could render circles appearing to grow at different rates like so:
let size = [];
//The size each circle starts at
let colorShift;
//Empty value to make an array
let locationsX = ["50", "50", "100", "150", "100", "150", "200", "250", "300", "350", "300", "250", "350", "50", "100", "300", "350", "200", "200", "200", "200"];
let locationsY = ["50", "350", "300", "250", "100", "150", "200", "250", "300", "50", "100", "150", "350", "200", "200", "200", "200", "50", "100", "300", "350"];
//The points in which I want circles to appear
function setup() {
createCanvas(400, 400);
colorShift = [color("#00C2BF"), color("#33FFFC"), color("#33A2FF"), color("#3380FF")];
//The colors within the array
for (let i = 0; i < locationsX.length; i++) {
size[i] = random(100);
}
}
function draw() {
background("#004CC2");
stroke("white");
strokeWeight(1);
colorChanging();
//Calls the color changing function
for (let i = 0; i < locationsX.length; i++) {
circle(locationsX[i], locationsY[i], size[i]);
}
//Issue, creates all circles within the array coordinates
//Goal, create an individual circle randomly from one of the coordinates in the array, continuously
for (let i = 0; i < size.length; i++) {
size[i]++;
if (size[i] > 100) { //Test Size
//if (size[i] > 600) { //Final Size
size[i] = 0;
}
}
//Expands the circles until the specified size, wherein they reset
}
function colorChanging() {
for (let i = 0; i < colorShift.length; i++) {
fill(colorShift[i]);
//colorShift[i]++;
//Issue, does not shift through the color array, only one color or blank white when using colorShift[i]++
if (colorShift[i] > 3) {
colorShift[i] = 0;
//Will reset the color if it exceeds the colors in the array
}
//Issue, does not shift through the color array, only one color or blank white
}
//Goal, shift the inside of each circle between the colors in array
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/p5.min.js"></script>
(key takeaways being:
background()
in draw()
size[i]
instead of size[0]
when calling circle()
size
with random values in setup()
(size[i] = random(100);
)Here is a refactored version of your sketch with the above notes integrated:
//Empty value to make an array
let locationsX = [50, 50, 100, 150, 100, 150, 200, 250, 300, 350, 300, 250, 350, 50, 100, 300, 350, 200, 200, 200, 200];
let locationsY = [50, 350, 300, 250, 100, 150, 200, 250, 300, 50, 100, 150, 350, 200, 200, 200, 200, 50, 100, 300, 350];
//The points in which I want circles to appear
let numCircles = locationsX.length;
let circleSizes = new Array(numCircles);
//The size each circle starts at
let colorPalette;
// total number of colours in the palette
let numColors;
//Which color from the list is the current colour (transitioning from, to the next)
let circleColorIndices = new Array(numCircles).fill(0);
// color transition duration in frames
let circleMaxSize = 100;
// store each circle's color
let circleColors;
function setup() {
createCanvas(400, 400);
stroke("white");
strokeWeight(1);
// init palette
colorPalette = [color("#00C2BF"), color("#33FFFC"), color("#33A2FF"), color("#3380FF")];
numColors = colorPalette.length;
// init individual circle colors
circleColors = new Array(numCircles).fill(colorPalette[0]);
// randomize circle sizes - fake appearance raindrop / growing at different times
for (let i = 0; i < numCircles; i++) {
circleSizes[i] = random(100);
}
}
function draw() {
background("#004CC2");
for (let i = 0; i < numCircles; i++) {
growCircle(i);
changeCircleColor(i);
// isolate fill properties
push()
fill(circleColors[i]);
circle(locationsX[i], locationsY[i], circleSizes[i]);
pop();
}
}
function growCircle(index){
circleSizes[index]++;
if (circleSizes[index] > circleMaxSize) { //Test Size
circleSizes[index] = 0;
// increment color
circleColorIndices[index] = (circleColorIndices[index] + 1) % numColors;
}
}
function changeCircleColor(index){
let currentColorIndex = circleColorIndices[index];
let nextColorIndex = (currentColorIndex + 1) % numColors;
let colorLerpAmount = circleSizes[index] / circleMaxSize;
let circleColor = lerpColor(colorPalette[currentColorIndex],
colorPalette[nextColorIndex],
colorLerpAmount);
circleColors[index] = circleColor;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/p5.min.js"></script>
The modulo/remainder operator (%) in this case is used to "loop" the next index back to the start of the array. for example:
circleColorIndices[index]++;
if(circleColorIndices[index] >= numColors){
circleColorIndices[index] = 0;
}
would behave roughly the same as:
circleColorIndices[index] = (circleColorIndices[index] + 1) % numColors;
One last minor detail is introducing push()
/pop()
.
One thing it can help with is isolate drawing styles (e.g. stroke, fill properties). This isn't 100% needed in this case since fill()
is called before rendering each circle, however you may reach scenarios in the future where you want objects to be drawn in a certain style without affecting the global drawing style. It can also be used to isolate coordinate systems, not just drawing styles and this is more powerful. The 2D Transformations tutorial is great. Even though it uses the Processing Java syntax you can easily recognize how it applies to p5.js.
Here's a minimal example rendering an octogon:
function setup() {
createCanvas(400, 400);
background("#004CC2");
fill("#00C2BF");
// move to the center
translate(width / 2, height / 2);
let numSides = 8;
let radius = 100;
for(let i = 0 ; i <= numSides; i++){
push();
// rotate
rotate(map(i, 0, numSides, 0.0, TWO_PI));
// move to the right after rotation is applied
translate(radius, 0);
// draw
circle(0, 0, 25);
pop();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/p5.min.js"></script>
(Notice how simple it is now to change the number of sides or radius)
You might not be familiar with classes yet, however it may help see an example how you could group each circle data (size, color, position) and behaviour (updating properties, rendering) using instances of a class instead of multiple arrays:
//Empty value to make an array
let locationsX = [50, 50, 100, 150, 100, 150, 200, 250, 300, 350, 300, 250, 350, 50, 100, 300, 350, 200, 200, 200, 200];
let locationsY = [50, 350, 300, 250, 100, 150, 200, 250, 300, 50, 100, 150, 350, 200, 200, 200, 200, 50, 100, 300, 350];
//The points in which I want circles to appear
let numCircles = locationsX.length;
let colorPalette;
let numColors;
let circles = [];
let circleMaxSize = 100;
function setup() {
createCanvas(400, 400);
colorPalette = [color("#00C2BF"), color("#33FFFC"), color("#33A2FF"), color("#3380FF")];
numColors = colorPalette.length;
for(let i = 0 ; i < numCircles; i++){
circles[i] = new Circle(locationsX[i], locationsY[i], random(100));
}
}
function draw() {
background("#004CC2");
stroke("white");
strokeWeight(1);
for (let i = 0; i < numCircles; i++) {
circles[i].draw();
}
}
class Circle{
constructor(x, y, size){
this.x = x;
this.y = y;
this.size = size;
this.colorIndex = 0;
this.color = colorPalette[this.colorIndex];
}
updateSize(){
this.size++;
if(this.size > circleMaxSize){
this.size = 0;
// increment color from palette
this.colorIndex = (this.colorIndex + 1) % numColors;
}
}
updateColor(){
let nextColorIndex = (this.colorIndex + 1) % numColors;
let colorLerpAmount = this.size / circleMaxSize;
let circleColor = lerpColor(colorPalette[this.colorIndex],
colorPalette[nextColorIndex],
colorLerpAmount);
this.color = circleColor;
}
draw(){
// update
this.updateSize();
this.updateColor();
// render
push();
fill(this.color);
circle(this.x, this.y, this.size);
pop();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/p5.min.js"></script>
Update: Thank you @ggorlen for pointing out that unless i
is explicitly declared using the let
keyword it implcitly becomes a global variable which is not desirable. (polutes global scope and came become a source of hidden bugs).
@AidenA. FWIW here are a few of tips to potentially improve the visuals in the future:
blendMode()
to explore different transparenciesfilter(BLUR)
to smooth out edgesHere's a sketch to illustrate this:
//Empty value to make an array
let locationsX = [50, 50, 100, 150, 100, 150, 200, 250, 300, 350, 300, 250, 350, 50, 100, 300, 350, 200, 200, 200, 200];
let locationsY = [50, 350, 300, 250, 100, 150, 200, 250, 300, 50, 100, 150, 350, 200, 200, 200, 200, 50, 100, 300, 350];
//The points in which I want circles to appear
let numCircles = locationsX.length;
let colorPalette;
let numColors;
let circles = [];
let circleMaxSize = 100;
function setup() {
createCanvas(400, 400);
colorPalette = [color("#00C2BF"), color("#33FFFC"), color("#33A2FF"), color("#3380FF")];
numColors = colorPalette.length;
for(let i = 0 ; i < numCircles; i++){
circles[i] = new Circle(locationsX[i], locationsY[i], random(100));
}
}
function draw() {
background("#004CC2");
blendMode(OVERLAY);
for (let i = 0; i < numCircles; i++) {
circles[i].draw();
}
blendMode(BLEND);
filter(BLUR, 8);
}
class Circle{
constructor(x, y, size){
this.x = x;
this.y = y;
this.size = size;
this.colorIndex = 0;
this.color = colorPalette[this.colorIndex];
this.sizeSpeed = random(1, 3);
}
updateSize(){
this.size += this.sizeSpeed;
if(this.size > circleMaxSize){
this.size = 0;
// increment color from palette
this.colorIndex = (this.colorIndex + 1) % numColors;
}
}
updateColor(){
let nextColorIndex = (this.colorIndex + 1) % numColors;
let colorLerpAmount = this.size / circleMaxSize;
let circleColor = lerpColor(colorPalette[this.colorIndex],
colorPalette[nextColorIndex],
colorLerpAmount);
this.color = circleColor;
}
draw(){
// update
this.updateSize();
this.updateColor();
// render
push();
noFill();
stroke(this.color);
strokeWeight(this.size * 0.1);
circle(this.x, this.y, this.size);
pop();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/p5.min.js"></script>
Additionally, you'll notice random()
might not be random enough / the pattern may become predictable after a while. You can look at noise() functions (e.g. also noiseSeed()
, noiseDetail()
) and randomGaussian()
. When the color is reset (e.g. this.size = 0;
in updateSize()
you can also potentially "wobble" / randomise the positions a bit). HTH