/**
*
Radial Gradient Array
*
*
* What was a simple array of filled circles is now... dynamic! :o)
* Keys:
*
* - SPACE = pause/unpause;
* - (R)andomise settings;
* - (F)ade shot, V to toggle fade mode;
* - (T)rail shot, G to toggle trail mode;
* - (P)attern change (random), O for next pattern in order;
* - (D)esign (random);
* - (C)create new design;
* - (A)ntialias toggle;
* - [ decrease base fade amount, { minimise;
* - ] increase base fade amount, } maximise;
* - RIGHT/LEFT = increase/decrease number of circles;
* - UP/DOWN = increase/decrease coverage;
* - ` (backquote) = switch pixel plotting method
*
*
* Try pplaying some music and pressing O/P on fairly regular beats,
* D/F/T on less often beats, and R on more dramatic changes in the music.
*
* Maybe it's just me, but I got sucked into watching this for a while...
* mind you, I've spent ages twilddling the knobs behind the scenes, so it's
* been (in some sense) good value. :o)
*
* Have a peek in the code for some exciting calculations for the sizes of
* the circles and the spacing...
* k = 1/2 * sqrt( pi/c ) - 1
* D = x / ( n + k(n+1) )
*
* Based on Simple Radial Gradient by Ira Greenberg.
* Examples > Basics > Color > RadialGradient
*/
// Flag to determine whether to use the set() function to plot pixels or the
// loadPixels() and updatePixels() functions with the pixels[] array
boolean usePixelsArray = true;
// The number of different "patterns" (ways of choosing which grid position to fill)
static final int NUM_FILL_PATTERNS = 7;
int fillPattern = 0; // The current fill pattern
int prevPattern = 0; // The previous fill pattern
int design = 0; // The current design (for fill pattern 5)
int designStep = 0; // What step we're up to the in the current design
boolean createNewDesign = true; // Need to initialise designs[0] first time used
// In the designs, the following letters are for directional movement:
// q w e
// a s d - (s is stationary, so draw over the same place)
// z x c
// UPPER case means leave a trail, lower case means don't leave a trail
// Digits 2-9 are used as a modifier (n) to say move n places in specified direction; no digit means n=1
// r means move randomly (up to n places in any direction)
String designs[] =
{
"", // Used for a generated design!
"DDWWAAWWDDdDDXXAAXX3dWEQW2dXZCX3dAAWWWW5c9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r9r", // SPXL
"5dDW2A2X3D3W4A4XDDDDDWWWWWAAAAAAXXXXXXDDDDDDDWWWWWWWAAAAAAAAXXXXXXXXDDDDDDDDDDCXZaazxxcxzzxccdd", // spiral
"XXDDWWAAWWDDDDXXXDXXDDWWAAWWDDDDXXXDXXDDWWAAWWDDDDXXXD3r",
"ddddxxxxddddww",
"ssssAdDaWxXw4deeecccczzzcczzqqqz",
"4D4z4W2e3e2e",
"XXXrdDDd3RddDDD5R5R5R5R5R5ReeeDDDeeeDDDeeR2r2r2r2r2r2r2r2rdddZZZZrEE",
"eeedcccdeeedccccdeeedcccdeeedcccceeddeeccczzzzzqqaazzxxcccdd"
};
// BASIC PARAMETERS
float coverage; // Density of coverage over one "box" (eg .6 means 60% the area is covered by the circles)
int divisions; // The number of rows or columns in the grid (actually the number in the longest dimension, the shorter dimension will have less)
boolean antiAlias = false; // Smooth the circle edges?
boolean paused = false; // Pause the sketch?
boolean trailMode = false; // Join each circle to the previous circle?
float maxTrailJump = 4; // The number of grid spaces that can be spanned by a single trail
int trailCount = 0; // Temporarily activate/suppress trails for how many (more) frames?
int trailShotDuration = 40; // Number of frames to activate/suppress trails per "shot"
int baseFadeAmount = 2; // Amount to fade by ("ambient" fade) for every frame
int maxBaseFadeAmount = 20; // Maximum base fade amount selectable by user input
boolean fadeMode = false; // Fade effect active?
int fadeAmount = 15; // Additional amount to fade by when fade effect active (toggled or fade shot)
int fadeCount = 0; // Temporarily activate/suppress fade for how many (more) frames?
int fadeShotDuration = 25; // Number of frames to activate/suppress fade effect per "shot"
// CALCULATED VALUES
float divSize; // The height (and width) of each column
int cols; // The number of columns
int rows; // The number of rows
float diameter; // The circle diameter
float radius; // The circle radius
// Offsets to centre the rows and columns
int colOffset;
int rowOffset;
// Working variables useful for debug
float spaceFactor;
float totalSpace;
float spaceSize;
// STATE VARIABLES
// The previous center color
color prevColor;
// If we want to update the display after drawing each gradient, we need to
// keep track of where we are up to when initially filing the grid.
// Location of next gradient to draw
int col = 0;
int row = 0;
// Keep track of which way we're going for some pattern(s)
int goLeftRight = 1; // -1 for left, +1 for right
int goUpDown = 1; // -1 for up, +1 for down
// Previous circle's centre's pixel coordinates
int prevx = 0;
int prevy = 0;
// Count of circles filled since last reset()
int countSinceReset;
// ------------------------------------------------------------
// Initial setup.
// ------------------------------------------------------------
void setup(){
size(640, 480);
background(0);
smooth();
randomiseSettings();
prevColor = rndColor();
reset();
}
// ------------------------------------------------------------
// Handle keyboard input.
// ------------------------------------------------------------
void keyPressed()
{
// Some changes require recalculation of values and clearing the display.
boolean doReset = false;
if (key == CODED)
{
println("keyCode: " + keyCode);
if (keyCode == UP) // Increase coverage
{
coverage += (coverage < 0.1) ? 0.01 : 0.1; // Step is smaller when value is smaller
doReset = true;
}
else if (keyCode == DOWN && coverage > 0.01) // Decrease coverage
{
coverage -= (coverage <= 0.11) ? 0.01 : 0.1; // Step is smaller when value is smaller
doReset = true;
}
else if (keyCode == LEFT && divisions > 1) // Reduce number of rows/columns
{
divisions--;
doReset = true;
}
else if (keyCode == RIGHT) // Increase number of rows/columns
{
divisions++;
doReset = true;
}
}
else
{
println("key: [" + key + "]");
if (key == ' ') // SPACE to pause / unpause the draw looping
{
paused = !paused;
if (paused)
noLoop();
else
loop();
}
else if (key == '`') // Use pixels[] array (or not) toggle
{
usePixelsArray = !usePixelsArray;
println("usePixelsArray: " + usePixelsArray);
}
else if (key == '~') // Debug information
{
debugInfo();
}
else if (key == 'a' || key == 'A') // Antialias toggle
{
antiAlias = !antiAlias;
}
else if (key == 'd') // Pick a design
{
setFillPattern(5);
// Pick a different design
design = rndNotX(designs.length, design);
designStep = 0;
}
else if (key == 'D') // Repick the previous design
{
setFillPattern(5);
}
else if (key == 'c') // Create new random design
{
setFillPattern(6);
createNewDesign = true;
}
else if (key == 'C') // Go back to last design created
{
setFillPattern(6);
}
else if (key == 'p' || key == 'P') // Random fill pattern
{
setFillPattern(rndNotX(NUM_FILL_PATTERNS, fillPattern));
}
else if (key == 'o' || key == 'O') // Next fill pattern (in order)
{
setFillPattern(wrapConstrain(fillPattern + 1, 0, NUM_FILL_PATTERNS - 1));
}
else if (key == 'g' || key == 'G') // Trail toggle
{
trailMode = !trailMode;
}
else if (key == 't' || key == 'T') // Trail shot
{
trailCount = trailShotDuration;
}
else if (key == 'v' || key == 'V') // Fade toggle
{
fadeMode = !fadeMode;
}
else if (key == 'f' || key == 'F') // Fade shot
{
fadeCount = fadeShotDuration;
}
else if (key == '[') // Decrease base fade amount
{
if (baseFadeAmount > 0) baseFadeAmount--;
}
else if (key == ']') // Increase base fade amount
{
if (baseFadeAmount < maxBaseFadeAmount) baseFadeAmount++;
}
else if (key == '{') // Minimise base fade amount
{
baseFadeAmount = 0;
}
else if (key == '}') // Maximise base fade amount
{
baseFadeAmount = maxBaseFadeAmount;
}
else if (key == 'r' || key == 'R') // Randomise settings
{
randomiseSettings();
}
}
if (doReset) reset();
}
// ------------------------------------------------------------
// Select some random settings for various parameters and
// reset the grid.
// ------------------------------------------------------------
void randomiseSettings()
{
divisions = rnd(20) + 1;
coverage = 0.1 * (rnd(8) + 1);
baseFadeAmount = rnd(maxBaseFadeAmount / 4); // Don't make base fade too strong
fadeMode = random(1) < 0.5;
trailMode = random(1) < 0.5;
setFillPattern(rnd(NUM_FILL_PATTERNS));
reset();
}
void setFillPattern(int newPattern)
{
prevPattern = fillPattern;
fillPattern = newPattern;
}
// ------------------------------------------------------------
// Clear the display, recalculate all the important values.
// ------------------------------------------------------------
void reset()
{
background(0);
countSinceReset = 0;
int maxSize; // Will be width or height
if (divisions == 1)
maxSize = min(width, height);
else if (width > height)
maxSize = width;
else
maxSize = height;
// Ratio of the gaps between discs to the disc diameter.
// Calculate such that the area of the circle divided by the area of the
// square made joining adjacent circle centres is equal to the coverage ratio.
// If L is the length of the sides of the square, and r is the radius of the circle
// then
// A = L^2 is the area of the square
// a = pi * r^2 is the area of the circle
// If c is the coverage ratio, then
// a = cA
// So
// A = a / c
// L^2 = (pi * r^2) / c
// L = sqrt ( pi * r^2 / c )
// = r * sqrt( pi / c )
// The size of the space (at least in the case where circle is smaller than the square)
// we can see is the square side length less the circle's diameter
// s = L - 2r
// = r * sqrt( pi/c ) - 2r
// The ratio of the space to the circle diameter then is
// k = s / 2r
// = [ r * sqrt( pi/c ) - 2r ] / 2r
// = 1/2 * sqrt( pi/c ) - 1
spaceFactor = sqrt( PI / coverage ) * 0.5 - 1;
// Want to have the circles evenly spaced to the edges (of the controlling dimension)
// x = nD + (n+1)kD
// = D( n + k(n+1) )
// So
// D = x / ( n + k(n+1) )
// where
// x is the dimension we are filling
// n is the number of circles
// D is the diameter of each circle
// k is the ratio of the spaces between the circles to the circle diameter; ie s = kD
diameter = float(maxSize) / (divisions + (divisions + 1) * spaceFactor);
spaceSize = spaceFactor * diameter;
divSize = diameter + spaceSize;
radius = diameter / 2;
// Number of rows and columns that should fit in the display
// (don't always fit when the coverage is large-ish)
cols = floor( width / divSize );
rows = floor( height / divSize );
// Top left corner offest to centre the rows and columns on the display
colOffset = int( (width - cols * divSize + divSize) / 2 );
rowOffset = int( (height - rows * divSize + divSize) / 2 );
// Make sure the current row and column are inside the new grid size
col = constrain(col, 0, cols - 1);
row = constrain(row, 0, rows - 1);
// No "previous" fill pattern on reset (or at least not matchable)
prevPattern = -1;
// Reset the design step
designStep = 0;
debugInfo();
}
// ------------------------------------------------------------
// Display informatino about various parameters, calculated
// values and state information.
// ------------------------------------------------------------
void debugInfo()
{
println("--------------------------");
println("coverage: " + coverage);
println("spaceFactor: " + spaceFactor);
println("diameter: " + diameter);
println("spaceSize: " + spaceSize);
println("");
println("divSize: " + divSize + ", divSize^2: " + sq(divSize));
println("radius: " + radius + ", pi*radius^2: " + PI*sq(radius));
println("ratio: " + PI*sq(radius) / sq(divSize));
println("");
println("Base fade amount: " + baseFadeAmount);
println("fade/Count: " + fadeMode + " / " + fadeCount);
println("trail/count: " + trailMode + " / " + trailCount);
println("fillPattern: " + fillPattern);
println("design (step/len): " + design + " (" + designStep + "/" + designs[design].length() + ") [" + designs[design] + "]");
println("antiAlias: " + antiAlias);
}
// ------------------------------------------------------------
// MAIN DRAW 'LOOP'
// The circle to fill has already been decided.
// Create the gradient and decide what circle to fill next.
// ------------------------------------------------------------
void draw()
{
// Determine the location of the centre of the circle to be filled.
int x = int( colOffset + divSize * col );
int y = int( rowOffset + divSize * row );
// ----------------------------------------------
// Fade the existing content?
// ----------------------------------------------
int fadeAlpha = baseFadeAmount;
if (fadeMode || fadeCount > 0)
{
// Don't fade if we're in fade mode and there is a fade count
// (ie the fade count suppresses the fade)
if (!(fadeMode && fadeCount > 0))
fadeAlpha += fadeAmount;
if (fadeCount > 0) --fadeCount;
}
if (fadeAlpha > 0)
{
noStroke();
fill(0, fadeAlpha);
quad(0, 0, width, 0, width, height, 0, height);
}
// ----------------------------------------------
// Connect the new circle to the previous circle?
// ----------------------------------------------
if (trailMode || trailCount > 0)
{
// Don't show trail if we're in trail mode and there is a trail count
// (ie the trail count supresses the trail)
if (!(trailMode && trailCount > 0))
{
// Don't draw a connection if we've just reset or the jump is too large
if (countSinceReset > 0 && dist(x, y, prevx, prevy) <= divSize * maxTrailJump)
{
stroke(prevColor);
strokeWeight(2);
line(prevx, prevy, x, y);
}
}
if (trailCount > 0) --trailCount;
}
// ----------------------------------------------
// Draw a circle filled with a radial gradient.
// ----------------------------------------------
int newColor = rndColor();
createGradient(x, y, int(radius), newColor, prevColor, antiAlias);
countSinceReset++;
// ----------------------------------------------
// Decide what grid position to fill next.
// ----------------------------------------------
if (fillPattern == 0)
{
// Left-to-right, top-to-bottom (across then down)
if (++col >= cols)
{
col = 0;
if (++row >= rows)
row = 0;
}
}
else if (fillPattern == 1)
{
// Random grid position
col = rnd(cols);
row = rnd(rows);
}
else if (fillPattern == 2)
{
// Random walk into one of 8 neighbours (or stay still)
col = rndIncDec(col, 0, cols - 1);
row = rndIncDec(row, 0, rows - 1);
}
else if (fillPattern == 3)
{
// Top-to-bottom, left-to-right (down then across)
if (++row >= rows)
{
row = 0;
if (++col >= cols)
col = 0;
}
}
else if (fillPattern == 4)
{
// Zigzag: across then up/down
int newCol;
col += goLeftRight;
if (col < 0 || col >= cols)
{
goLeftRight = -goLeftRight;
col += goLeftRight; // ie go back to same column
row += goUpDown;
if (row < 0 || row >= rows)
{
goUpDown = -goUpDown;
row += 2 * goUpDown; // ie go to the row in the opposite direction
}
}
}
else if (fillPattern == 5 || fillPattern == 6)
{
// Move according to a design
if (fillPattern == 6) design = 0; // Randomly created design
if (design == 0 && createNewDesign == true)
{
designs[0] = randomDesign();
designStep = 0;
createNewDesign = false;
}
int n = 1; // Number of places to move in the specified direction (or random)
String d = designs[design];
int len = d.length();
if (designStep >= len) designStep = 0;
char inst = d.charAt(designStep);
if ('1' < inst && inst <= '9')
{
n = Integer.parseInt(Character.toString(inst));
designStep = wrapConstrain(designStep + 1, 0, len - 1);
inst = d.charAt(designStep);
}
designStep = wrapConstrain(designStep + 1, 0, len - 1);
switch (Character.toLowerCase(inst))
{
case 'q': go(-n, -n); break;
case 'w': go( 0, -n); break;
case 'e': go( n, -n); break;
case 'a': go(-n, 0); break;
case 's': go( 0, 0); break;
case 'd': go( n, 0); break;
case 'z': go(-n, n); break;
case 'x': go( 0, n); break;
case 'c': go( n, n); break;
case 'r': go(rnd(n*2+1)-n, rnd(n*2+1)-n); break; // Random n places up/down/left/right
default:
break;
}
trailMode = Character.isUpperCase(inst);
}
// ----------------------------------------------
// Keep track of what we're up to for next time.
// ----------------------------------------------
prevx = x;
prevy = y;
prevColor = newColor;
prevPattern = fillPattern;
}
// ------------------------------------------------------------
// Generate (and return) a random design string
// ------------------------------------------------------------
String randomDesign()
{
final String dirs = "qwedcxza"; // The 8 directions
int dir = rnd(8); // Which direction
char c = dirs.charAt(dir); // Command
String des = "";
int numSteps = rnd(50) + 5;
boolean trail = random(1) < 0.5;
for (int s = 0; s < numSteps; s++)
{
// Decide which way to go
int r = rnd(100);
if (r < 25) ;
else if (r < 45) c = dirs.charAt(dir = wrapConstrain(dir - 1, 0, 7));
else if (r < 65) c = dirs.charAt(dir = wrapConstrain(dir + 1, 0, 7));
else if (r < 75) c = dirs.charAt(dir = wrapConstrain(dir - 2, 0, 7));
else if (r < 85) c = dirs.charAt(dir = wrapConstrain(dir + 2, 0, 7));
else if (r < 90) c = dirs.charAt(dir = wrapConstrain(dir + 3, 0, 7));
else if (r < 95) c = dirs.charAt(dir = wrapConstrain(dir + 3, 0, 7));
else if (r < 97) c = 'r'; // random
else c = 's'; // stationary
// Decide whether to take a single step or a longer step
if (random(1) < 0.05 && c != 's')
des = des + str(rnd(3) + 1);
// Decide whether to toggle leaving a trail
if (random(1) < 0.1) trail = !trail;
if (trail)
c = Character.toUpperCase(c);
else
c = Character.toLowerCase(c);
// Go one step in the current direction?
des = des + c;
}
println("Random design: [" + des + "]");
return des;
}
// ------------------------------------------------------------
// Randomly incremenr or decrement (or stay the same) from a
// given value, within the passed range minValue to maxValue
// (inclusive).
// ------------------------------------------------------------
int rndIncDec(int n, int minValue, int maxValue)
{
n = n + rnd(3) - 1;
if (n < minValue) n = maxValue;
if (n > maxValue) n = minValue;
return n;
}
// ------------------------------------------------------------
// Move (relative) to current column and row
// ------------------------------------------------------------
void go(int dx, int dy)
{
col = wrapConstrain(col + dx, 0, cols - 1);
row = wrapConstrain(row + dy, 0, rows - 1);
}
// ------------------------------------------------------------
// Plot a circle with a radial gradient fill.
// ------------------------------------------------------------
void createGradient (int ox, int oy, int radius, color centerColor, color edgeColor, boolean smoothEdge)
{
// Grab a copy of the screen pixels so we can play with them
// Supposedly faster than using multiple calls to set(x,y,c),
// but this will be dependant on how many points are plotted.
if (usePixelsArray)
loadPixels();
// Start at the edge of the circle and work towards the centre
// Colour the centre pixel after the main loop to avoid inner loop
for (int r = radius; r > 0; r--)
{
// Trial-and-error optimised angleStep to avoid "holes" is (0.4225 / r)
float angleStep = 0.4225 / r;
color c = lerpColor(centerColor, edgeColor, float(r) / radius);
for (float angle=0; angle < TWO_PI; angle += angleStep)
{
int x = int( ox + r * cos(angle) );
int y = int( oy + r * sin(angle) );
// Make sure we don't try to access a pixel that is outside the pixel array!
if (x >= 0 && x < width && y >= 0 && y < height)
{
if (usePixelsArray)
pixels[x + y * width] = c;
else
set(x, y, c);
}
}
}
// Colour the centre of the circle with color c1
//set(ox, oy, centerColor);
if (ox >= 0 && ox < width && oy >= 0 && oy < height)
{
if (usePixelsArray)
pixels[ox + oy * width] = centerColor;
else
set(ox, oy, centerColor);
}
// Update the display with the modified pixels[] array
if (usePixelsArray)
updatePixels();
if (smoothEdge)
{
// Create a smooth edge; hack anti-aliasing
noFill();
stroke(0);
strokeWeight(3);
ellipse(ox, oy, radius*2, radius*2);
}
}
// ------------------------------------------------------------
// Return a random colour.
// ------------------------------------------------------------
color rndColor()
{
return color(rnd(256), rnd(256), rnd(256));
}
// ------------------------------------------------------------
// Return a random integer from 0 to n (not including n)
// that is also not equal to x.
// ------------------------------------------------------------
int rndNotX(int n, int x)
{
int val = int(random(n - 1));
return (val < x) ? val : val + 1;
}
// ------------------------------------------------------------
// Return a random integer from 0 to n (not including n).
// ------------------------------------------------------------
int rnd(int n)
{
return int(random(n));
}
// ------------------------------------------------------------
// "Wrap" a value within the limits of minValue and maxValue (inclusive).
// Similar to performing modulo arithmetic.
// ------------------------------------------------------------
int wrapConstrain(int value, int minValue, int maxValue)
{
int rangeSize = maxValue - minValue + 1;
while (value < minValue) value += rangeSize;
while (value > maxValue) value -= rangeSize;
return value;
}