A flow field is a field of vectors, which tell the agents operating on it in which direction to move towards. A noise flow field is a flow field that uses Perlin noise to generate the directions and so the values near each other are similar.
Warning: the code is far more complex that the typical flow field program. It is because I added so many features. Just the basic program wouldn’t even be 30 lines. Not to mention 500…
Code
/*
Better noise flow field, with more settings!
*/
import javax.swing.JOptionPane; //library to get the popups working
String commandTutorialText = //'help' command text
"Commands:\n"+
"'clear': clears the window.\n"+
"'pause': toggle pause\n"+
"'load': enter the load sequence\n"+
"'savep': save the current settings/color manager to presets\n"+
"'print': prints active settings and color-manager, in the form of code\n"+
"'list': get the number of settings and color-manager presets\n"+
"'set': enter the sequence to modify current active preset\n"+
"'rush': iterate N frames, without updating the screen\n"+
"'teleport': teleport all agents to random locations\n"+
"\n"+
"'exit': terminate the program\n"+
"'back': return to the simulation\n"
;
ColorManager cm; //active color-manager
ArrayList<ColorManager> colorPresets = new ArrayList<ColorManager>();// color-manager presets
Settings s; //active settings
ArrayList<Settings> settingsPresets = new ArrayList<Settings>(); //Settings settingsPresets[];
Particle p[]; //agents / particles
Console console; //console
boolean paused = false;
void setup() {
fullScreen();
initialize(); //create presets & create console
loadSettingPreset(1);
loadColorPreset(0);
//printPresetCode(colorPresets.get(1)); //this will output the code required to implement a preset created within the program
//if you are annoyed when at having to close the popup window everytime on launch, comment out this line
console.printMessage("Instructions:\nWhen focused on the window, the following actions can be taken:\nPress space: bring out command console\nPress enter: screenshot the screen (will be saved into the sketch's folder)\nEnjoy!");
}
void draw() {
//if not paused -> do stuff : P
if(paused == false) doStuff();
}
void doStuff() {
//update color. not needed for some presets, but necessary for others
cm.updateColor();
//set stroke to updated color
stroke(cm.getColor());
for(int i = 0; i < s.n; i++) {
//updating and drawing agents
p[i].update();
p[i].display();
}
}
void keyPressed() {
char k = (""+key).toLowerCase().charAt(0); //turning char into lowercase
if(k == ' ') {
console.enterCommandWindow("enter 'help' to see command list");
return;
}
if(k == ENTER) {
saveFrame("screenshots/screenshot_########.png");
}
}
void reset() {
noiseSeed(int(random(100000))); //create new noise flow field
cm.clearBackground();
loadSettingsPreset(s); //reload active presets
loadColorPreset(cm);
}
class ColorManager {
boolean use_rgb; //true -> rgb, false -> hsb
boolean bounce; //true -> if(vx > 255 || vx < 0) cx *= -1; //false -> if(vx > 255) vx = 0. if(vx<0) vx = 255
//original color -> useful when printing preset code (if i didn't add this, it would only show the final color, not the origin)
float ogv1, ogv2, ogv3, ogv4;
//color components. I am not saying red, green, blue, alpha values since it could also be HSBA, so it is easier to say components
float v1, v2, v3, v4;
//rate of change of color components
float c1, c2, c3, c4;
//background components
float b1, b2, b3;
ColorManager(boolean usingRGB, boolean bounceColor, float startColor[], float colorChange[], float background[] ) {
//i could add rules on what to do if there are not enough elements in the color arrays, but I can't be bothered right now.
if(startColor.length != 4 || colorChange.length != 4 || background.length != 3) {
println("\nERROR\nA color preset doesn't have the right amount of color information");
console.printMessage("\nERROR\nA color preset doesn't have the right amount of color information");
exit();
}
use_rgb = usingRGB;
setColorMode();
bounce = bounceColor;
v1 = startColor[0];
v2 = startColor[1];
v3 = startColor[2];
v4 = startColor[3];
c1 = colorChange[0];
c2 = colorChange[1];
c3 = colorChange[2];
c4 = colorChange[3];
b1 = background[0];
b2 = background[1];
b3 = background[2];
ogv1 = v1;
ogv2 = v2;
ogv3 = v3;
ogv4 = v4;
}
void setColorMode() {
if(use_rgb) colorMode(RGB,255,255,255,255); //the 255s decide the range. don't change it
else colorMode(HSB,255,255,255,255);
}
void updateColor() {
v1 += c1;
v2 += c2;
v3 += c3;
v4 += c4;
if(bounce) {
if(v1 > 255 || v1 < 0) {c1 *= -1;}
if(v2 > 255 || v2 < 0) {c2 *= -1;}
if(v3 > 255 || v3 < 0) {c3 *= -1;}
if(v4 > 255 || v4 < 0) {c4 *= -1;}
return;
}
//loop color mode:
if(v1 > 255) v1 = 0;
if(v2 > 255) v2 = 0;
if(v3 > 255) v3 = 0;
if(v4 > 255) v4 = 0;
if(v1 < 0) v1 = 255;
if(v2 < 0) v2 = 255;
if(v3 < 0) v3 = 255;
if(v4 < 0) v4 = 255;
}
void clearBackground() {
background(b1,b2,b3);
}
color getColor() {
return color(v1,v2,v3,v4);
}
}
class Particle {
float x, y; //position
boolean crossedBorder; //if it has crossed the border in the last frame
boolean exists; //if it exists
Particle(float x, float y) {
setPosition(x,y);
crossedBorder = false;
exists = true;
}
void setPosition(float x, float y) {
this.x = x;
this.y = y;
}
void update() {
if(exists == false) return;
//interpret the field data of 0-1 as rotation
float a = noise(x*s.noiseM, y*s.noiseM)*TWO_PI;
//add a random number between 0 and randomDiviation
a += random(s.randomDiviation);
//move 1 pixel in the direction the agent is rotated in
x+= cos(a);
y+= sin(a);
//if it is offscreen, trigger the function
if(x>width||x<0||y>height||y<0) outOfScreen();
}
void display() {
if(crossedBorder == true || exists == false) { crossedBorder = false; return; };
//display it only if hasn't crossed the border since last update & exists
point(x,y);
}
void outOfScreen() {
crossedBorder = false;
switch(s.actionOnLeave) {
case 0: //stop existing
exists = false;
break;
case 1: //teleport randomly
setPosition(random(width),random(height));
break;
case 2: //come out the other side
if(x > width) x = 0;
if(y > height) y = 0;
if(x < 0) x = width;
if(y < 0) y = height;
break;
case 3: //mirror location
if(x > width) {
x = 0;
y = height-y;
}
if(x < 0) {
x = width;
y = height-y;
}
//y
if(y > height) {
y = 0;
x = width-x;
}
if(y < 0) {
y = height;
x = width-x;
}
break;
}
}
}
class Settings {
int n; //number of agents - higher the number, more lines
float noiseM; //scale of perlin noise map used to create the flow field
float randomDiviation; //add a random number when moving. '0' results in sharp lines, '6.28' results in thicker lines.
int actionOnLeave; //0 -> vanish, 1 -> teleport randomly, 2 -> x > w -> x = 0 ,3 -> x > w -> {x = 0 && y = height-y }
Settings(int n, float noiseMultiplier, float randomDiviation, int actionOnOffscreen) {
this.n = n;
noiseM = noiseMultiplier;
this.randomDiviation = randomDiviation;
actionOnLeave = actionOnOffscreen;
}
}
void initialize() {
console = new Console(); //create the console
//addPreset is an overloaded function that can recieve either a Settings obj or ColorManager obj
addPreset(new Settings(
100000, //number of agents
0.005, //noise scale multiplier (should be a low number)
0, //random diviation (agent angle = noise() + random( randomDiviation). Setting it to TWO_PI*n will create a random flow field
2 //action on agent crossing the border
));
addPreset(new ColorManager(
true, //using RGB (false -> HSB)
true, //bouncing colors (what to do when clr > 255 || clr < 0)? (true : colr change speed *= -1 | false: loop around)
new float[]{0,0,0,20}, //starting color
new float[]{0,0,0,0}, //color change speed
new float[]{255,255,255} //background color
));
addPreset(new Settings( 100, 0.005, 0, 1 ));
//bg = background, static = non changing, dynamic = changing
//white bg with static black lines
addPreset(new ColorManager( true, true, new float[]{0,0,0,20}, new float[]{0,0,0,0}, new float[]{255,255,255}));
//black bg with static white lines
addPreset(new ColorManager( true, true, new float[]{255,255,255,20}, new float[]{0,0,0,0}, new float[]{0,0,0}));
//black bg with static electic blue lines
addPreset(new ColorManager( true, true, new float[]{0,200,255,20}, new float[]{0,0,0,0}, new float[]{0,0,0}));
//black bg with static red lines
addPreset(new ColorManager( true, true, new float[]{255,0,0,20}, new float[]{0,0,0,0}, new float[]{0,0,0}));
//black bg with static magenta lines
addPreset(new ColorManager( true, true, new float[]{255,0,255,20}, new float[]{0,0,0,0}, new float[]{0,0,0}));
//black bg with static yellow lines
addPreset(new ColorManager( true, true, new float[]{255,255,0,20}, new float[]{0,0,0,0}, new float[]{0,0,0}));
//white bg with dynamic gray lines
addPreset(new ColorManager( true, true, new float[]{0,0,0,0}, new float[]{1,1,1,1}, new float[]{255,255,255}));
addPreset(new ColorManager( false, true, new float[]{42,255,255,20}, new float[]{1,0,0,0}, new float[]{0,0,0}));
//black bg with bright dynamic lines, that start with red -> orange -> yellow -> green ->blue -> violet -> red
//change the '0.1' to something larger if you want a faster change
addPreset(new ColorManager( false, false, new float[]{0,255,255,20}, new float[]{0.1,0,0,0}, new float[]{0,0,0}));
}
void addPreset(ColorManager cl) { //overloaded function that can accept ColorManager & Settings class
colorPresets.add(cl);
}
void addPreset(Settings st) {
settingsPresets.add(st);
}
void loadColorPreset(ColorManager cl) {
cm = cl;
cm.setColorMode();
cm.clearBackground();
}
boolean loadColorPreset(int id) {
if(id<0 || colorPresets.size()<= id) return false;
loadColorPreset(colorPresets.get(id));
return true;
}
void loadSettingsPreset(Settings st) {
s = st;
if(cm != null) cm.clearBackground();
p = new Particle[s.n];
for(int i = 0; i < s.n; i++) {
p[i] = new Particle(random(width),random(height));
}
}
boolean loadSettingPreset(int id) {
if(id<0 || settingsPresets.size()<= id) return false;
loadSettingsPreset(settingsPresets.get(id));
return true;
}
void printPresetCode(Settings st) {
println("addPreset(new Settings("+st.n,",",st.noiseM,",",st.randomDiviation,",",st.actionOnLeave+"));");
}
void printPresetCode(ColorManager cl) {
println("addPreset(new ColorManager(",
cl.use_rgb,",",
cl.bounce,",",
"new float[]{", cl.ogv1,",", cl.ogv2,",", cl.ogv3,",", cl.ogv4, "},",
"new float[]{", cl.c1,",", cl.c2,",", cl.c3,",", cl.c4, "},",
"new float[]{", cl.b1,",", cl.b2,",", cl.b3,"}",
"));");
}
class Console {
void printMessage(String message) { //create a popup with the message
JOptionPane.showMessageDialog(null, message);
}
void enterCommandWindow(String text) { //run the command the user said
String userInput = JOptionPane.showInputDialog(null,text,"Enter a command");
if(userInput == null) userInput = "";
handleUserInput(userInput.toLowerCase());
}
String getUserInput(String text) { //get user input (from popup window)
String userInput = JOptionPane.showInputDialog(null,text);
if(userInput == null) userInput = "";
return userInput.toLowerCase();
}
void handleUserInput(String userInput) { //decide what to do with the user's input
/*
From here on out, the code becomes pretty weird.
If you don't understand it, just know that
it is just a bunch of if-else statements that determine what to do
Nothing amazing. And if it is hard to read... imagine what i had to go through to write it : P
it took 2x as long to make it than it took to make everything else
*/
userInput = userInput.toLowerCase();
if(userInput.equals("help")) {
enterCommandWindow(commandTutorialText);
return;
}
if(userInput.equals("clear")) reset();
if(userInput.equals("back")) return;
if(userInput.equals("exit")) exit();
if(userInput.equals("pause")) paused = !paused;
if(userInput.equals("teleport")) {
for(int i = 0; i < s.n; i++) p[i].setPosition(random(width),random(height));
}
if(userInput.equals("rush")) {
int N = int(getUserInput("Enter the number of frames to be iterated"));
for(int i = 0; i < N; i++) doStuff();
}
if(userInput.equals("list")) printMessage("There are currently " + colorPresets.size() + " color presets and " + settingsPresets.size() + " settings presets");
if(userInput.equals("print")) {
print("Settings & Color presets: \n");
printPresetCode(s);
println();
printPresetCode(cm);
}
if(userInput.equals("load")) {
String ui = getUserInput("Choose preset type: \n'color': load color preset\n'settings': load settings preset");
if(ui.equals("color")) {
int id = int(getUserInput("Which preset to load?\nAvailable: 0-"+(colorPresets.size()-1) ));
loadColorPreset(id);
}
if(ui.equals("settings")) {
int id = int(getUserInput("Which preset to load?\nAvailable: 0-"+(settingsPresets.size()-1) ));
loadSettingPreset(id);
}
return;
}
if(userInput.equals("savep")) {
String ui = getUserInput("Which active preset to save?\n'color'\n'settings'\n'both'");
boolean saveSettings = ui.equals("settings");
boolean saveColor = ui.equals("color");
if(ui.equals("both")) {
saveSettings = true;
saveColor = true;
}
if(saveSettings) addPreset(s);
if(saveColor) addPreset(cm);
}
if(userInput.equals("set")) {
String ui = getUserInput("Properties of what to modify?\n'color'\n'settings'");
boolean modifySettings = ui.equals("settings");
boolean modifyColor = ui.equals("color");
if(modifySettings) {
ui = getUserInput("Which value to modify?\nAvaiable:\n'n': number of agents\n'm': noise scale\n'r': randomness\n'a': what action to do when agent leaves the screen");
if(ui.equals("n")) {
int newN = int(getUserInput("Enter the new number of agents"));
s.n = newN;
}
if(ui.equals("m")) {
int newM = int(getUserInput("Enter the new noise scale (0.005 by default)"));
s.noiseM = newM;
}
if(ui.equals("r")) {
int newR = int(getUserInput("Enter the new random diviation (0 by default)"));
s.randomDiviation = newR;
}
if(ui.equals("a")) {
int newA = int(getUserInput("Enter the new action when an agent leaves the screen\nAvailable:\n"+
"'0': they disappear\n'1': they are teleported randomly on the screen\n'2': they loop around\n'3': thier location is mirrorer"));
if(newA>=0 && newA<=3) s.actionOnLeave = newA;
else printMessage("Invalid action type");
}
loadSettingsPreset(s);
return;
}
//modify color
if(modifyColor) {
ui = getUserInput("Which color-manager property to change?"+
"\n'mode':Color mode"+"\n'background': Background color\n'color': starting color\n'change': speed of change for color properties\n'bounce': what to do when reaching values 0/255");
if(ui.equals("mode")) {
ui = getUserInput("Enter new mode.\nAvailable:'rgb': rgba mode\n'hsb': hsba/hsva mode");
if(ui.equals("rgb")) cm.use_rgb = true;
else if(ui.equals("hsb")) cm.use_rgb = false;
else printMessage("Invalid mode type");
}
if(ui.equals("background")) {
ui = getUserInput("Enter the "+((cm.use_rgb)? "RGB" : "HSB")+" values, separated by space.\nExample: 1.0 2.0 3.0");
String splitUI[] = split(ui," ");
if(splitUI.length != 3) {
printMessage("incorrect amount of data");
return;
}
cm.b1 = float(splitUI[0]);
cm.b2 = float(splitUI[1]);
cm.b3 = float(splitUI[2]);
}
if(ui.equals("color")) {
ui = getUserInput("Enter the "+((cm.use_rgb)? "RGBA" : "HSBA")+" values, separated by space.\nExample: 1.0 2.0 3.0 4.0");
String splitUI[] = split(ui," ");
if(splitUI.length != 4) {
printMessage("incorrect amount of data");
return;
}
cm.v1 = float(splitUI[0]);
cm.v2 = float(splitUI[1]);
cm.v3 = float(splitUI[2]);
cm.v4 = float(splitUI[3]);
cm.ogv1 = cm.v1;
cm.ogv2 = cm.v2;
cm.ogv3 = cm.v3;
cm.ogv4 = cm.v4;
}
if(ui.equals("change")) {
ui = getUserInput("Enter the "+((cm.use_rgb)? "RGBA" : "HSBA")+" change values, separated by space.\nThis tells how much the colors will change each frame\nExample: 1.0 2.0 3.0 4.0");
String splitUI[] = split(ui," ");
if(splitUI.length != 4) {
printMessage("incorrect amount of data");
return;
}
cm.c1 = float(splitUI[0]);
cm.c2 = float(splitUI[1]);
cm.c3 = float(splitUI[2]);
cm.c4 = float(splitUI[3]);
}
if(ui.equals("bounce")) {
ui = getUserInput("What to do when color values hit 0 or 255?\nAvailable:\n'bounce': start going in the other direction\n'loop': go to the other extreme (0->255, 255->0)");
if(ui.equals("loop")) cm.bounce = false;
if(ui.equals("bounce")) cm.bounce = true;
}
loadColorPreset(cm);
return;
}
}
}
}