Tutors!
http://doodle.com/poll/ka22ab6ch9tniw24
- Sign up by end of tuesday. If you sign up after that, we cannot guarantee your meeting will be coordinated.
- Put your andrew ID in the ‘your name’ section.
- Email professor Dannenberg and the TAs if you tried to sign up before Tuesday and all of the spots were full. We want to know if we’re not supplying enough sections.
Arrays
Today we introduced arrays. Arrays are data structures that allow us to store multiple values of the same type. This goes hand-in-hand with iteration in for loops because you can traverse the array, get the value you need, and then do something with that value.
You should read about arrays in the textbook and w3schools readings, but here are some key things to remember:
- You access the elements of the array by its index.
- Arrays are zero-indexed so if you try to index into the array outside of the range 0 to the array length – 1, you will break everything. So watch your for loops carefully.
- Arrays only take values of the same type, so don’t try to make one array store strings and numbers.
Here is a simple program with a single moving rect
:
onebox
var x;
var y;
var dx;
var dy;
var col;
function setup() {
createCanvas(200, 200);
x = random(width);
y = random(height);
dx = random(-5, 5);
dy = random(-5, 5);
col = color(random(255), random(255), random(255));
frameRate(5); // slow for low cpu load
}
function draw() {
background(200, 200, 200);
fill(col);
noStroke();
rect(x, y, 10, 10);
x += dx;
y += dy;
if (x > width) x = 0;
else if (x < 0) x = width;
if (y > height) y = 0;
else if (y < 0) y = height;
}
Now, we modify this program so that every number is replaced by an array of numbers and every computation on a set of variables was replaced by a for
loop to do the computation on each element of each array.
Here’s the resulting program with 100 rects rather than 1 rect:
var x = []; // every variable now starts out as an empty array
var y = [];
var dx = []; // velocity in x direction (d for "delta" meaning change or difference)
var dy = []; // velocity in y direction
var col = []; // color of each rect wil be random
function setup() { // set random position, velocity, and color for 100 rects
createCanvas(200, 200);
for (i = 0; i < 100; i++) { // for loop initializing 100 values in each array
x[i] = random(width);
y[i] = random(height);
dx[i] = random(-5, 5);
dy[i] = random(-5, 5);
col[i] = color(random(255), random(255), random(255));
}
frameRate(5);
}
// update position and draw each of 100 rectangles
function draw() {
background(200, 200, 200);
noStroke();
for (i = 0; i < 100; i++) { // for each rectangle ...
fill(col[i]);
rect(x[i], y[i], 10, 10);
x[i] += dx[i];
y[i] += dy[i];
// "wrap" logic: if you go off an edge, jump to the opposite edge
if (x[i] > width) x[i] = 0; // wrap horizontally
else if (x[i] < 0) x[i] = width;
if (y[i] > height) y[i] = 0; // wrap vertically
else if (y[i] < 0) y[i] = height;
}
}
Array of Buttons
Here’s another use of arrays: A row of buttons where the “state” of the button is stored in an array of boolean (true/false) values. In this case if a button state is has the value true, then it should be red. Otherwise, is the value is false, then it should be green.
The code to click on buttons is crude and imprecise. It could be much better, but I kept in minimal in order to focus on the use of arrays.
A note about booleans: Booleans are types just like integers, floating points, and strings. There are two Boolean values: true and false. These values are not variables (well, at least in some languages, true and false are both the names of variables and the values contained in them), but variables can be assigned the values of Booleans. And just like numbers, Booleans have their own operators. You’ve seen these before. Whenever you use &&, ||, or not, you are performing an operation on Boolean values much like addition and subtraction work for number values. Actually every conditional expression that you write evaluates to a Boolean. For example, the expression 100 != 0 would evaluate to true. In addition, the expression (100 != 0) && (100 % 2 == 1) would be false since 100 is not an odd number. This is a very important concept because Booleans go hand in with conditionals. If you don’t understand this concept, please ask an instructor or TA soon!
var isRed = [] // isRed is true if the button should
// be red, and false if the button should be green.
// We use this to determine fill color when we draw
// the buttons.
function setup() {
createCanvas(200, 40);
for (i = 0; i < 10; i++) { // there are 10 buttons
isRed[i] = false; // all buttons start out green
}
frameRate(5);
}
function draw() {
background(255);
// draw the "click me" text
fill(0); // black text
text("click me", 10, 15);
// draw the buttons
noStroke();
for (i = 0; i < 10; i++) { // for each button...
if (isRed[i]) { // is the i'th button red?
fill(205, 0, 0); // red
} else {
fill(0, 205, 0); // green
}
// draw the i'th button at position i * 20
rect(i * 20, 20, 18, 18)
}
}
// when the mouse is pressed, toggle the isRed state
// of the button. We have to determine which of the
// buttons to toggle. We divide the mouse position by
// the width of the buttons (20) to compute the button
// number. E.g. if mouseX === 30, we're in the middle
// of button #1 (remember the leftmost button is #0),
// and mouseX/20 === 1.5, meaning the middle (0.5) of
// button #1. The int() function rounds down to the
// nearest integer, converting 1.5 into 1, which is the
// index we are after.
//
function mousePressed() {
var index = int(mouseX / 20);
isRed[index] = ! isRed[index]; // ! means "not"
// (! true) is false, and (! false) is true
}
Tic Tac Toe Game
Building on the buttons code above, here is an implementation of Tic Tac Toe.
var OFFSET = 20; // offset from upper left corner
var WIDTH = 40; // width of each square
var SPACING = 50; // spacing of squares
var BLANK = 255; // the "empty" state value
var state = [BLANK, BLANK, BLANK, BLANK, BLANK, BLANK, BLANK, BLANK, BLANK];
var turn = "red"; // whose turn is it? (red or green)
// when there is a winner, turn is BLANK to prevent another move
// state layout is like this:
// 0 1 2
// 3 4 5
// 6 7 8
function setup() {
createCanvas(200, 200);
noStroke();
}
// test if mouse is inside the square at x, y and size w
//
function mouseInsideSquare(x, y, w) {
return mouseX > x & mouseX < x + w && mouseY > y && mouseY < y + w;
}
// mouseToIndex finds the index of the square containing the mouse (if any)
// returns the index. If mouse is not in any square, returns -1.
//
function mouseToIndex() {
var index = 0;
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
var x = OFFSET + j * SPACING;
var y = OFFSET + i * SPACING;
if (mouseInsideSquare(x, y, WIDTH)) {
return index;
}
index = index + 1;
}
}
return -1;
}
// checkWin() checks for every possible win and returns the
// color of the winner if any, otherwise returns BLANK
//
function checkWin() {
// there are many ways to do this
// check for 3-in-a-row horizontally
for (var row = 0; row < 3; row++) {
var index = row * 3;
if (state[index] == state[index + 1] & state[index] == state[index + 2]) {
if (state[index] != BLANK) { // winner cannot be "BLANK"!
return state[index]; // a winner
}
}
}
// check for 3-in-a-row vertically, note that cols are
// 0,3,6, 1,4,7, and 2,5,8, or col,col+3,col+6
for (var col = 0; col < 3; col++) {
if (state[col] == state[col + 3] & state[col] == state[col + 6]) {
if (state[col] != BLANK) { // winner cannot be "BLANK"!
return state[col]; // a winner
}
}
}
// check for diagonal wins
if (state[0] == state[4] & state[0] == state[8]) {
if (state[0] != BLANK) { // winner cannot be "BLANK"!
return state[0]; // from top left to bottom right
}
}
if (state[6] == state[4] & state[6] == state[2]) {
if (state[6] != BLANK) { // winner cannot be "BLANK"!
return state[6]; // from bottom left to top right
}
}
return false; // no winning 3-in-a-row
}
function draw() {
background(220);
// draw the 3x3 game board
var index = 0; // index into state to get color
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
fill(state[index]);
var x = OFFSET + j * SPACING;
var y = OFFSET + i * SPACING;
rect(x, y, WIDTH, WIDTH);
fill(0);
text("" + index, x, y + 10);
index = index + 1;
}
}
// if there is a winner, print a message and stop
var winner = checkWin();
if (winner != false) {
text("The winner is " + winner + "!", OFFSET, OFFSET + SPACING * 3 + 5);
turn = BLANK; // prevent another move
noLoop(); // we are done
}
}
// mousePressed uses turn to set the state when a move is made
// DEFECT: nothing stops a player from coloring a square that is already "taken"
// (How would you fix this?)
function mousePressed() {
var index = mouseToIndex();
// only process a click if click is inside a square and the game is not over
if (index != -1 & turn != BLANK) {
state[index] = turn;
if (turn == "red") {
turn = "green";
} else {
turn = "red";
}
}
}
More Arrays
Consider the following program, which draws a star. This star has 10 vertex points, each with 2 values, for a total of 20 data elements.
var x0 = 50;
var y0 = 18;
var x1 = 61;
var y1 = 37;
var x2 = 83;
var y2 = 43;
var x3 = 69;
var y3 = 60;
var x4 = 71;
var y4 = 82;
var x5 = 50;
var y5 = 73;
var x6 = 29;
var y6 = 82;
var x7 = 31;
var y7 = 60;
var x8 = 17;
var y8 = 43;
var x9 = 39;
var y9 = 37;
function setup() {
createCanvas(200, 200);
}
function draw() {
background(255, 200, 200);
beginShape();
vertex (x0, y0);
vertex (x1, y1);
vertex (x2, y2);
vertex (x3, y3);
vertex (x4, y4);
vertex (x5, y5);
vertex (x6, y6);
vertex (x7, y7);
vertex (x8, y8);
vertex (x9, y9);
endShape();
noLoop();
}
That’s a lot of variables! Providing this data to a program requires either creating 20 variables or using an array that can somehow store these 20 numbers. In the next example, we group these data elements together in two arrays: one for the x-coordinates and one for the y-coordinates. (Note that we also use the CLOSE property in endShape
to fix the visual bug.)
var x = [ 50, 61, 83, 69, 71, 50, 29, 31, 17, 39 ];
var y = [ 18, 37, 43, 60, 82, 73, 82, 60, 43, 37 ];
function setup() {
createCanvas(200,200);
}
function draw() {
background (255, 200, 200);
beginShape();
vertex (x[0],y[0]);
vertex (x[1],y[1]);
vertex (x[2],y[2]);
vertex (x[3],y[3]);
vertex (x[4],y[4]);
vertex (x[5],y[5]);
vertex (x[6],y[6]);
vertex (x[7],y[7]);
vertex (x[8],y[8]);
vertex (x[9],y[9]);
endShape(CLOSE);
noLoop();
}
That’s tidier, but we can do even better to abstract this code. Notice any patterns? How about the fact that we are counting our way through the array elements? This counting should be an immediate tip-off to use an iterative code structure, like a for()
loop, as shown below.
var x = [50, 61, 83, 69, 71, 50, 29, 31, 17, 39];
var y = [18, 37, 43, 60, 82, 73, 82, 60, 43, 37];
function setup() {
createCanvas(200, 200);
noLoop();
}
function draw() {
background(255, 200, 200);
var nPoints = x.length;
beginShape();
for (var i = 0; i < nPoints; i++) {
vertex(x[i], y[i]);
}
endShape(CLOSE);
}
Now we can do all sorts of tricks. For example, on each frame, we can draw a star in which each point is computed from one of the stored coordinates, to which has been added a random number:
var x = [50, 61, 83, 69, 71, 50, 29, 31, 17, 39];
var y = [18, 37, 43, 60, 82, 73, 82, 60, 43, 37];
function setup() {
createCanvas(200, 200);
frameRate(10);
}
function draw() {
background(255, 200, 200);
var nPoints = x.length;
beginShape();
for (var i = 0; i < nPoints; i++) {
var px = x[i] + random(-3, 3);
var py = y[i] + random(-3, 3);
vertex(px,py);
}
endShape(CLOSE);
}
We can also progressively modify these arrays. Here, each point does a “random walk“:
var x = [50, 61, 83, 69, 71, 50, 29, 31, 17, 39];
var y = [18, 37, 43, 60, 82, 73, 82, 60, 43, 37];
function setup() {
createCanvas(200, 200);
frameRate(5);
}
function draw() {
background(255, 200, 200);
var nPoints = x.length;
// Progressively modify the arrays with a small amount of randomness.
// Each time draw() is called, the more randomness is added.
var amount = 1.2;
for (var i = 0; i < nPoints; i++) {
x[i] += random(-amount, amount);
y[i] += random(-amount, amount);
}
// Now render the star from those arrays
fill(255);
stroke(0);
beginShape();
for (var i = 0; i < nPoints; i++) {
vertex(x[i], y[i]);
}
endShape(CLOSE);
}
Incidentally, with a little extra work, it’s possible to use the same data to render the star with curves instead of straight lines:
var x = [50, 61, 83, 69, 71, 50, 29, 31, 17, 39];
var y = [18, 37, 43, 60, 82, 73, 82, 60, 43, 37];
function setup() {
createCanvas(200, 200);
noLoop();
}
function draw() {
background(255, 200, 200);
var nPoints = x.length;
fill(255);
stroke(0);
beginShape();
curveVertex(x[nPoints-1], y[nPoints-1]);
for (var i = 0; i < nPoints; i++) {
curveVertex(x[i], y[i]);
}
curveVertex(x[0], y[0]);
curveVertex(x[1], y[1]);
endShape();
}
Arrays that Grow
Consider the following sketch. Each time the user clicks, another point is accreted into the background image:
var ex;
var ey;
//---------------------------------
function setup() {
createCanvas(300, 300);
background(255, 200, 200);
frameRate(5);
}
//---------------------------------
function draw() {
fill(0);
ellipse(ex, ey, 5, 5);
}
//---------------------------------
function mousePressed() {
ex = mouseX;
ey = mouseY;
}
But what if one wanted to have an animated scene? You can’t do that if you don’t refresh the background on every frame. Here’s the beginning of a solution: store the user’s mouse clicks in an array, and draw all of the points each frame:
var x = [];
var y = [];
//---------------------------------
function setup() {
createCanvas(300,300);
frameRate(5);
}
//---------------------------------
function draw() {
background (255,200,200);
fill(0);
for (var i=0; i<x.length; i++){
var ex = x[i];
var ey = y[i];
ellipse(ex,ey, 5, 5);
}
}
//---------------------------------
function mousePressed(){
x.push(mouseX);
y.push(mouseY);
}
Looks the same, right? Well, no. Now we can again do all sorts of tricks with the data we are progressively storing. For example, we can draw the points with some animation, and meanwhile change the color of the background:
var x = [];
var y = [];
function setup() {
createCanvas(300,300);
}
function draw() {
var r = 230 + 25 * sin(millis() / 1000.0 );
var g = 230 + 25 * sin(millis() / 1000.0 + HALF_PI);
var b = 230 + 25 * sin(millis() / 1000.0 - HALF_PI);
background (r,g,b);
fill(0);
if (x.length < 1) {
text("Click to begin", 5,15);
}
for (var i=0; i<x.length; i++) {
var ex = x[i] + random(-3,3);
var ey = y[i] + random(-3,3);
ellipse(ex,ey, 5, 5);
}
}
function mousePressed() {
x.push(mouseX);
y.push(mouseY);
}
We can perform computations on these arrays, in order to calculate new information about the collection of user-added points. For example, in the sketch below, we compute the “centroid” or geometric center of the collection by calculating the average X-coordinate and the average Y-coordinate. (The centroid is also known as the center of mass.)
var x = [];
var y = [];
function setup() {
createCanvas(300, 300);
frameRate(5);
}
function draw() {
background(255, 200, 200);
fill(0);
noStroke();
var nPoints = x.length;
if (nPoints < 1) {
text("Click to begin", 5, 15);
}
// Draw the dots
for (var i = 0; i < nPoints; i++) {
ellipse (x[i], y[i], 5,5);
}
// Compute their centroid point: (xAverage, yAverage)
var xAverage = 0;
var yAverage = 0;
for (var i = 0; i < nPoints; i++) {
xAverage += x[i];
yAverage += y[i];
}
xAverage /= nPoints;
yAverage /= nPoints;
// Draw lines from the centroid to every point
strokeWeight(1);
stroke(0, 0, 0, 50);
for (var i = 0; i < nPoints; i++) {
line (x[i], y[i], xAverage, yAverage);
}
// Draw the centroid
fill(255);
stroke(0);
strokeWeight(2);
ellipse(xAverage, yAverage, 10, 10);
}
function mousePressed() {
x.push(mouseX);
y.push(mouseY);
}
Notice how the centroid is recalculated when the data is progressively modified on-the-fly:
var x = [];
var y = [];
function setup() {
createCanvas(300, 300);
for (var i = 0; i < 7; i++) {
var t = map(i, 0, 7, 0,TWO_PI);
x.push(width / 2 + 75 * cos(t));
y.push(height / 2 + 75 * sin(t));
}
frameRate(5);
}
function draw() {
background(255, 200, 200);
fill(0);
noStroke();
// Progressively modify the data
var nPoints = x.length;
for (var i = 0; i < nPoints; i++) {
x[i] += (noise(i * 1 + millis() / 1000.0) - 0.5) * 10;
y[i] += (noise(i * 2 + millis() / 1000.0) - 0.5) * 10;
ellipse(x[i], y[i], 5, 5);
}
// Draw the dots
for (var i = 0; i < nPoints; i++) {
ellipse(x[i], y[i], 5, 5);
}
// Compute their centroid point: (xAverage, yAverage)
var xAverage = 0;
var yAverage = 0;
for (var i = 0; i < nPoints; i++) {
xAverage += x[i];
yAverage += y[i];
}
xAverage /= nPoints;
yAverage /= nPoints;
// Draw lines from the centroid to every point
strokeWeight(1);
stroke(0, 0, 0, 50);
for (var i = 0; i < nPoints; i++) {
line(x[i], y[i], xAverage, yAverage);
}
// Draw the centroid
fill(255);
stroke(0);
strokeWeight(2);
ellipse(xAverage, yAverage, 10, 10);
}
function mousePressed() {
x.push(mouseX);
y.push(mouseY);
}
More Information
Some other JavaScript array methods that may be useful:
push()
— adds a new element to an array (at the end)pop()
— deletes the last element, and provides it to youshift()
— deletes the first element and shifts the rest downunshift()
— adds a new element to an array (at the beginning)reverse()
— reverses the elements in an array
There’s lots more. We therefore also recommend you check out:
- W3Schools Array Reference
- p5.js Reference (see the section on Data)
- The Complete JavaScript Array Reference
Strings
You’ve used strings vaguely used in println
functions and in text that can appear on the canvas. You’ve also learned about concatenation and the toString()
method to convert numbers into strings. Now we’ll introduce more string methods that can allow you to search for substrings, convert them to all caps or all lowercase, etc. This is super important for formatting string values and parsing data the way you want them to. It is frequently used in handling data. For example, if you gat a huge list of the 104 students by “First Name, Last Name”, you can use the string methods to change them to “Last Name, First initial”, etc.
Here are a few examples of string methods. In this example, str
is just a generic variable that holds a string value. Try running this code in setup and seeing what the statements print out. I also recommend reading the w3schools page on string methods. Remember, just like arrays, strings are zero-indexed.
var str = "Hello, World!"
println(str.search("World"));
println(str.search("Pie"));
println(str.slice(1, 4));
println(str.substr(1, 4));
println(str.replace("Hello", "Goodbye"));
println(str.toUpperCase());
var arr = str.split(" ");
for (var i = 0; i < str.length; i++) {
println(arr[i]);
}
More Randomness
We have seen several examples of randomness based on calling the random
function. Either we use the number directly, or we use the numbers as offsets to create what is often called a “drunken walk” or Brownian motion. The program example above where the points of a star are perturbed randomly and repeatedly, causing the star to drift into new configurations is an example. Here’s another example, where Brownian motion makes the “eyes” move:
var leftX = 60, rightX = 140;
var leftY = 50, rightY = 50;
var gdf;
var noiseParam = 0;
var noiseStep = 0.1;
function ball(x, y) {
fill(0, 222, 0);
ellipse(x, y, 50, 50);
}
function setup() {
createCanvas(200, 200);
frameRate(10);
}
function draw() {
background(255, 250, 180);
fill(100);
ellipse(100, 80, 200, 200);
ball(leftX, leftY);
ball(rightX, rightY);
leftX += random(-2, 2);
leftY += random(-2, 2);
rightX += random(-2, 2);
rightY += random(-2, 2);
noiseParam += noiseStep;
};
Let’s redo this using other forms of randomness.
Perlin Noise
The noise function returns a smoothly varying value as a function of 1, 2, or 3 parameters that can be interpreted as time, space, or both. We saw Perlin noise in the terrain peak assignment. Below, we use it to move eyes. Since the noise function returns a value from 0 to 1, it’s important to scale the output appropriately (also the input matters — if you step through values rapidly, the output seems more chaotic).
var leftX = 60, rightX = 140;
var leftY = 50, rightY = 50;
var gdf;
var noiseParam = 0;
var noiseStep = 0.1;
function ball(x, y) {
var offset = noise(noiseParam);
// map offset from 0-1 into -20 to +20
offset = map(offset, 0, 1, -20, 20);
fill(0, 222, 0);
ellipse(x + offset, y, 50, 50);
}
function setup() {
createCanvas(200,200);frameRate(10);
frameRate(10);
}
function draw() {
background(255,250,180);
fill(100);
ellipse(100, 80, 200, 200);
ball(leftX, leftY);
ball(rightX, rightY);
noiseParam += noiseStep;
};
Notice that the eyes do the same thing, even though each calls noise and computes its “random” location independently. This emphasizes the fact that Perlin noise is a deterministic function: You put in the same parameters and you get out the same value. You only get change by changing the parameter. Since we use a global variable, noiseParam
, and update it only in the main draw function, each Eye draw method passes the same parameter value to noise
and gets the same result.
Other Parameters
Almost any parameter can be controlled by randomness, including Perlin noise. Let’s hook Perlin noise up to the eye color.
var leftX = 60, rightX = 140;
var leftY = 50, rightY = 50;
var gdf;
var noiseParam = 0;
var noiseStep = 0.1;
function ball(x, y) {
var offset = noise(noiseParam);
// map offset from 0-1 into -20 to +20
offset = map(offset, 0, 1, -20, 20);
fill(0, 222, 0);
fill(100,
noise(noiseParam * 0.2 + 100) * 255,
noise(noiseParam * 0.2 + 200) * 255);
ellipse(x + offset, y, 50, 50);
}
function setup() {
createCanvas(200, 200);
frameRate(10);
}
function draw() {
background(255, 250, 180);
fill(100);
ellipse(100, 80, 200, 200);
ball(leftX, leftY);
ball(rightX, rightY);
noiseParam += noiseStep;
};
I wanted the eye color to be based on different “random” values than the eye position. Recall that I cannot simply call noise
again with the same argument or I’ll get the same value back. Instead, I used a useful trick: I added a fairly big number to the argument, calling noise(noiseParam + 100)
and noise(noiseParam + 200)
. Since Perlin noise changes smoothly with the parameter values, a small offset would yield similar values, but by adding a “big” number like 100 or 200, I get fairly unrelated values. Since eye position and eye color are changed by increasing noiseParam
at the same rate, the apparent rate of fluctuation of both color and position will be the same. I could, say, get eye color to change much more slowly by scaling noiseParam
. In fact, that is the version shown above. Notice that the eye color seems to change more slowly than eye position. This demonstrates that you can change the apparent rate of change by changing the step size of the parameter to noise
.
Uniform and Gaussian Noise
Uniform noise spreads values equally across a fixed range, e.g. from 0 to 1. Gaussian noise tends toward a certain average value (called the mean), with more or less random variation around the mean determined by the standard deviation. Let’s put some random points onto squares using uniform random noise and Gaussian noise.
function setup() {
createCanvas(400,200);
background(0);
fill(255); // color for point() called in loop
// draw center line
stroke(255);
line(width/2, 0, width/2, height);
for (var i = 0; i < 1000; i++) {
// uniform random on the left
point(random(width/2), random(height));
// Gaussian noise on the right
point(randomGaussian(width * 3/4, 30),
randomGaussian(height/2, 30));
}
}
Notice how the Gaussian case concentrates choices around the mean value (here, at the center of the right square) while uniform random spreads the points more-or-less equally everywhere. Also, there are probably some Gaussian points that are outside the bounds of the right square. You should test and reject out-of-bounds points if it matters.
Technique: “Gaussian” with Limits
Suppose you want a non-uniform distribution, but you want to restrict the range of values. You could simply replace x
by min(x, limit)
, but then whenever x
is out of bounds, it gets replaced by limit, so points could pile up on the boundary. Here’s what that looks like. Notice that a number of points show up 10 pixels from the edge due to the min/max
functions:
function setup() {
createCanvas(200,200);
background(0);
fill(255);
// draw center line
stroke(255);
for (var i = 0; i < 1000; i++) {
// Gaussian noise for both x,y centered on canvas
var x = randomGaussian(width/2, 50);
var y = randomGaussian(height/2, 50);
// force x, y within 10 pixels inside canvas edge
x = max(10, min(width - 10, x));
y = max(10, min(height - 10, y));
point(x, y);
}
}
A better idea is to simply keep picking random numbers until you get something in bounds. This is a great place to use a while
loop. The essence of the approach is this: “Pick a value. While value is out of bounds, pick a new value.” Notice the points piled up near the borders are gone here. We simply picked new values.
function setup() {
createCanvas(200,200);
background(0);
fill(255);
// draw center line
stroke(255);
for (var i = 0; i < 1000; i++) {
// Gaussian noise for both x,y centered on canvas
var x = randomGaussian(width/2, 50);
while (x < 10 || x > width - 10) {
// test was true, so run the loop body...
// x is out of bounds, try again
x = randomGaussian(width/2, 50);
}
// test was false, so now x must be in bounds
// compute y just like x: keep looping until value is in bounds
var y = randomGaussian(height/2, 50);
while (y < 10 || y > height - 10) {
y = randomGaussian(height/2, 50);
}
point(x, y);
}
}
Gaussian Noise with Colors
You could get a similar effect by just not drawing points that are out of bounds. This would result in drawing fewer points, which is probably OK for this application, but maybe you need a Gaussian-like distribution for color selection, so simply ignoring bad values is not an option. Here’s some code that adds limited Gaussian noise to colors. Note that mouseX
controls the standard deviation of the Gaussian values.
function setup() {
createCanvas(200,200);
frameRate(1);
}
function draw() {
var range = 50; // random r ranges from -100 to 100
background(0);
noStroke();
for (var x = 0; x < width; x += 5) {
for (var y = 0; y < height; y += 5) {
var r = randomGaussian(0, mouseX / 5);
while (r < -range || r > range) {
r = randomGaussian(0, 20);
}
fill(128, 200 + r, 180 - r); // add some random to G & B
rect(x, y, 5, 5);
}
}
}
Skewing with Power
Suppose you want to add an offset to some number to achieve some artistic variation. You want the offset to be small (normally) but sometimes much larger. Consider the simple expression x * x
. If x
is in the range 0 to 1, x * x
will be smaller than x
. E.g. if x
= 1/2, then x*x
= 1/4. However, if x
=1, then x*x
= 1. Thus, x*x
has the same range as x
, but the values are skewed toward zero. We can get an even more extreme skew toward zero by computing x*x*x
, or x*x*x*x
, etc. Multiplication is cheap, and the more multiplies, the less likely it is to get a large number (i.e. close to 1).
Power
Instead of writing x*x
or x*x*x
, we can write Math.pow(x, 2)
or Math.pow(x, 3)
. The pow
function has the advantage that we can use a parameter for the exponent. If you are not familiar with the “power” function or exponents, please review the subject and see the reference for Math.pow
. E.g. instead of Math.pow(x, 2)
, we can write Math.pow(x, skew)
, where skew
is a variable we can easily adjust to get the effect we want.
Here is an example: The goal is to occasionally produce a bright circle. The skew
parameter can be varied with the mouse:
var skew = 4;
function setup() {
createCanvas(200,200);
frameRate(10);
}
function draw() {
background(0);
skew = mouseX;
noStroke();
skew = map(mouseX, 0, width, 0, 10);
text("move mouse to change skew", 10, 10);
text("skew = " + skew.toFixed(2), 10, 25);
fill(Math.pow(random(1), skew) * 255);
ellipse(width/2, height/2, width/2, height/2);
}
Playing with Random Functions
There’s no “right” or “wrong” way to use random values. Uniform random is just that — random. As you add structure to randomness by shaping the distribution, applying maximum and minimum limits, and perhaps other rules, constraints, and behaviors, you progressively move from “random” to “generative” or “algorithmic” — i.e. deliberate artistic expression.
Combining functions to get complex behavior is standard fare. For example, if you raise Perlin noise values to some power using Math.pow()
, you can achieve the smoothness of Perlin noise while skewing the values to mostly near-zero values.
You can also add non-random variation using our old friends sin()
and cos()
, which produce regular oscillations.
Experimentation and progressive refinement is the key.
Smooth Motion
Perlin noise has the nice feature that it is “smooth” — small parameter changes result in small output changes. Uniform random
and guassianRandom
functions jump around when you pick a new value. If you want to use random numbers for controlling positions, whether you are drawing lines or creating animation, you probably want smooth transitions as opposed to jumps. One way to do this in drawing is to use curves and Bezier splines to do the smooth interpolation. With animation, other techniques can be used. Mainly, you can use random points to establish targets or attractors, and then move the object or control parameter (e.g. an angle) smoothly in the direction of the target/attractor. We will spend some time soon on physics, gravity, attraction, acceleration, and smooth motion.
Bonus
Watch Zach Lieberman discuss drawing with code at the 2015 Eyeo Festival (watch from 8:28 – 16:40):