xxxxxxxxxx
296
//
// MixerBalancer
// April 16, 2020 by Philip
//
// Left Mouse = Move user
// Right Mouse = Create Attraction
//
// Strategy:
//
// Users try to connect to the closest mixer, but not change too often.
// Mixers move themselves to be in the middle of their connected users, slowly.
//
//
// Change these constants to test different scenarios
//
let NUM_MIXERS = 20; // Total number of mixers
let NUM_USERS = 1000; // Total number of users
let USER_MAX_VEL = 10.0; // Greatest speed (pixels/frame) that a user can move
let EXCESS_AVERAGE_CAPACITY = 1.5; // All mixers together have capacity of this amount in excess of all users
let TIME_BETWEEN_CHANGE = 2.0; // How often (secs) will a user change to new mixer
let SIZE_USER = 10; // Diameter of user in pixels
let SIZE_MIXER = 30; // Diameter of mixer in pixels
let FOLLOW_CENTROID_RATE = 0.02; // Each timestep, move mixer % closer to it's user's centroid.
let MAYBE_CHANGE_DIRECTION = 0.005; // Chance that, in a timestep, user changes direction and speed
let MAYBE_MIXER_FAILS = 0.000; // Chance that, in a timestep, a mixer fails
let WAIT_BEFORE_REPLACE_MIXER = 10.0; // Seconds before restarting a failed mixer
let MAX_CLUSTERING_DISTANCE = 100.0; // Users will sometimes be attracted to other groups of users within this pixel range
let WALKING_SPEED = 10; // Walking speed of a user, pixels per second
let DECAY_WALKING = 0.999; // Users get tired of walking
let DT = 1.0 / 60.0; // Wall-clock time per frame
let MAX_USERS_PER_MIXER = 0;
class mixer {
constructor() {
let position = p5.Vector(0,0);
let velocity = p5.Vector(0,0);
let userCentroid = p5.Vector(0,0);
let mColor = color(0,0,0);
let users = 0;
let inUse = false;
let timeDown = 0.0;
}
}
class user {
constructor() {
let position = p5.Vector(0,0);
let velocity = p5.Vector(0,0);
let mColor = color(0,0,0);
let mixer = 0;
let isClustering = false;
let timeSinceChange = 0.0;
}
}
let users = [];
let mixers = [];
function setNewVelocity(v) {
v.set(random(-USER_MAX_VEL, USER_MAX_VEL), random(-USER_MAX_VEL, USER_MAX_VEL));
}
function usersCentroid(center, maxRange) {
let centroid = new p5.Vector(0,0);
let numUsers = 0.0;
for (let i = 0; i < NUM_USERS; i++) {
if (p5.Vector.dist(users[i].position, center) <= maxRange) {
centroid.add(users[i].position);
numUsers++;
}
}
if (numUsers > 0) {
centroid.mult(1.0 / numUsers);
}
return centroid;
}
function separationForce(position, radius) {
let force = new p5.Vector(0,0);
for (let i = 0; i < NUM_USERS; i++) {
let separation = p5.Vector.sub(position, users[i].position);
if ((separation.mag() > 0) && (separation.mag() < 2.0 * radius)) {
force.add(separation);
}
}
return force;
}
function setup() {
//size(1000, 500);
//fullScreen();
createCanvas(600,500);
background(10);
MAX_USERS_PER_MIXER = round(NUM_USERS / NUM_MIXERS * EXCESS_AVERAGE_CAPACITY + 0.5);
for (let i = 0; i < NUM_MIXERS; i++) {
mixers[i] = new mixer();
mixers[i].position = new p5.Vector(width/2, height/2);
mixers[i].velocity = new p5.Vector(0,0);
mixers[i].mColor = color(random(255), random(255), random(255), 128);
mixers[i].userCentroid = new p5.Vector(0,0);
mixers[i].inUse = true;
}
for (let i = 0; i < NUM_USERS; i++) {
users[i] = new user();
users[i].position = new p5.Vector(random(width), random(height));
users[i].velocity = new p5.Vector(0,0);
setNewVelocity(users[i].velocity);
users[i].mColor = color(20);
users[i].mixer = -1;
users[i].timeSinceChange = random(TIME_BETWEEN_CHANGE);
users[i].isClustering = false;
}
}
function boundaryConditions(position) {
if (position.x < 0) position.x += width;
if (position.x > width) position.x -= width;
if (position.y < 0) position.y += height;
if (position.y > height) position.y -= height;
}
let ATTRACTION_RATE = 0.01;
function updateUsers() {
let mouse = new p5.Vector(mouseX, mouseY);
for (let i = 0; i < NUM_USERS; i++) {
users[i].timeSinceChange += DT;
users[i].position = p5.Vector.add(users[i].position, p5.Vector.mult(users[i].velocity, DT));
// Try to cluster with other users, if interested
if (users[i].isClustering) {
let nearbyCentroid = usersCentroid(users[i].position, MAX_CLUSTERING_DISTANCE);
if (nearbyCentroid.mag() > 0.0) {
let diff = p5.Vector.sub(nearbyCentroid, users[i].position).normalize().mult(WALKING_SPEED);
users[i].velocity.set(diff);
}
}
// But not too close
users[i].velocity.add(separationForce(users[i].position, SIZE_USER / 2.0));
// Maybe decide to walk away
if (random(1.0) < MAYBE_CHANGE_DIRECTION) {
users[i].isClustering = !users[i].isClustering;
if (!users[i].isClustering) {
setNewVelocity(users[i].velocity);
}
}
// Get tired after a while
users[i].velocity.mult(DECAY_WALKING);
// respond to mouse clicks
/*
if (mousePressed && (mouseButton == RIGHT)) {
// If right mouse pressed, maybe be attracted to this location
let howFar = PVector.dist(mouse, users[i].position);
if (random(1.0) < 100.0 / (howFar + 1.0)) {
users[i].position.add(p5.Vector.mult(PVector.sub(mouse, users[i].position), ATTRACTION_RATE));
}
}
if (mousePressed && (mouseButton == LEFT)) {
// If Left mouse pressed, grab this user
let howFar = PVector.dist(mouse, users[i].position);
if (howFar < SIZE_USER) {
users[i].position.set(mouse);
}
}
*/
boundaryConditions(users[i].position);
let needNewMixer = false;
let currentClosestMixer = 1000000;
// If no mixer, or mixer has failed, get a new one.
if (users[i].mixer < 0) {
needNewMixer = true;
} else {
if (!mixers[users[i].mixer].inUse) {
needNewMixer = true;
}
}
if (needNewMixer) {
users[i].mixer = -1;
users[i].mColor = color(20);
} else {
currentClosestMixer = PVector.dist(users[i].position, mixers[users[i].mixer].position);
}
// Check for a closer mixer
for (let j = 0; j < NUM_MIXERS; j++) {
if ((p5.Vector.dist(mixers[j].position, users[i].position) < currentClosestMixer) && // If Mixer is closer
((mixers[j].users < MAX_USERS_PER_MIXER) && mixers[j].inUse) && // and mixer has openings and is inUse
((users[i].timeSinceChange > TIME_BETWEEN_CHANGE) || needNewMixer) ) { // and either without mixer or not too soon
// If there is a closer mixer with openings, move to it!
if (users[i].mixer >= 0) {
mixers[users[i].mixer].users--;
}
users[i].mixer = j;
users[i].mColor = mixers[j].mColor;
mixers[users[i].mixer].users++;
users[i].timeSinceChange = 0.0;
}
}
}
}
function drawUsers() {
noStroke();
for (let i = 0; i < NUM_USERS; i++) {
fill(users[i].mColor);
ellipse(users[i].position.x, users[i].position.y, SIZE_USER, SIZE_USER);
}
}
function mixerFail(i) {
mixers[i].inUse = false;
mixers[i].users = 0;
mixers[i].timeDown = 0.0;
}
function updateMixers() {
let mouse = new p5.Vector(mouseX, mouseY);
let notInUse = 0;
for (let i = 0; i < NUM_MIXERS; i++) {
if (mixers[i].inUse) {
mixers[i].userCentroid.set(0,0);
/*
if (mousePressed && (mouseButton == LEFT)) {
// If Left mouse pressed, cause this mixer to fail
let howFar = p5.Vector.dist(mouse, mixers[i].position);
if (howFar < SIZE_MIXER / 2) {
mixerFail(i);
}
}*/
for (let j = 0; j < NUM_USERS; j++) {
if (users[j].mixer == i) mixers[i].userCentroid.add(users[j].position);
}
if (mixers[i].users > 0) {
mixers[i].userCentroid.mult(1.0 / mixers[i].users);
mixers[i].position.add(PVector.mult(p5.Vector.sub(mixers[i].userCentroid, mixers[i].position), FOLLOW_CENTROID_RATE));
}
if (random(1.0) < MAYBE_MIXER_FAILS) {
// If mixer fails, remove
mixerFail(i);
}
boundaryConditions(mixers[i].position);
} else {
notInUse++;
// If failed, 'replace' with a new one after a time delay
mixers[i].timeDown += DT;
if (mixers[i].timeDown > WAIT_BEFORE_REPLACE_MIXER) {
// Restart this mixer as a new one
// Re-locate to center
mixers[i].users = 0;
mixers[i].inUse = true;
mixers[i].position.set(width/2, height/2);
mixers[i].timeDown += DT;
mixers[i].mColor = color(random(255), random(255), random(255), 128);
mixers[i].userCentroid.set(0,0);
}
}
}
return notInUse;
}
function drawMixers() {
for (let i = 0; i < NUM_MIXERS; i++) {
if (mixers[i].inUse) {
fill(mixers[i].mColor);
} else {
fill(50);
}
stroke(0,0,0);
ellipse(mixers[i].position.x, mixers[i].position.y, SIZE_MIXER, SIZE_MIXER);
fill(0,0,0);
// Display how many users are connected
text(str(mixers[i].users), mixers[i].position.x - SIZE_MIXER/3, mixers[i].position.y + SIZE_MIXER/7);
}
}
function draw() {
background(0);
updateUsers();
let notInUse = updateMixers();
drawUsers();
drawMixers();
fill(0,0,0);
text("Bad Mixers: " + str(notInUse), 0, height - 10);
}