Processing
Lodestone
Licence: CC BY-NC-SA 4.0: remix with credit, non-commercial, share alike
View the code
// Lodestone
// Thousands of needles feel every magnet on the canvas and turn to follow them.
// Your pointer is the strongest magnet. Click to leave a magnet behind
// (each new one has the opposite pole). Press any key to take them away.
// A slider's value, or a default when that slider hasn't been added yet.
float setting(float value, float fallback) {
return (value > -100000 && value < 100000) ? value : fallback;
}
class Magnet {
float x, y, charge;
float orbit, speed, phase, tilt;
boolean wanders;
Magnet(float x, float y, float charge) {
this.x = x;
this.y = y;
this.charge = charge;
wanders = false;
}
Magnet(float charge) {
this.charge = charge;
wanders = true;
orbit = random(0.12, 0.4);
speed = random(0.15, 0.4) * (random(1) < 0.5 ? -1 : 1);
phase = random(TWO_PI);
tilt = random(0.6, 1.6);
}
void move(float t) {
if (!wanders) return;
x = width * (0.5 + orbit * cos(t * speed + phase));
y = height * (0.5 + orbit * sin(t * speed * tilt + phase * 1.3));
}
}
ArrayList<Magnet> wandering = new ArrayList<Magnet>();
ArrayList<Magnet> placed = new ArrayList<Magnet>();
Magnet pointer;
float presence = 0;
int lastMove = -10000;
float nextCharge = -1;
float pulseX, pulseY, pulse = 0;
color warm, cool, spark;
void setup() {
size(720, 720);
colorMode(HSB, 360, 100, 100, 100);
pointer = new Magnet(width / 2, height / 2, 1.8);
background(225, 40, 6);
}
void draw() {
// a little of the last frame stays, so the needles leave soft trails as they turn
noStroke();
fill(225, 40, 6, map(setting(params.trails, 0.4), 0, 1, 100, 12));
rect(0, 0, width, height);
// wandering magnets, alternating poles
int want = int(setting(params.magnets, 4));
while (wandering.size() < want) wandering.add(new Magnet(wandering.size() % 2 == 0 ? 1 : -1));
while (wandering.size() > want) wandering.remove(wandering.size() - 1);
float t = millis() * 0.001;
for (Magnet m : wandering) m.move(t);
// the pointer's magnet fades in while you move and out when you stop
boolean active = millis() - lastMove < 2500;
presence = lerp(presence, active ? 1 : 0, 0.08);
pointer.x = lerp(pointer.x, mouseX, 0.4);
pointer.y = lerp(pointer.y, mouseY, 0.4);
float gap = max(8, setting(params.gap, 16));
float swirl = setting(params.swirl, 0.2) * HALF_PI;
float needle = setting(params.length, 1.1);
// the colours drift slowly round the wheel, so the piece keeps changing
float hue = (setting(params.hue, 28) + 25 * sin(millis() * 0.00015) + 360) % 360;
// one bright colour for each pole, violet between them, and a white-gold light for the pointer
warm = color(hue, 95, 100);
cool = color((hue + 170) % 360, 90, 100);
color between = color((hue + 250) % 360, 75, 90);
spark = color((hue + 22) % 360, 22, 100);
colorMode(RGB, 255, 255, 255, 100);
color[][] shades = new color[33][9];
for (int i = 0; i <= 32; i++) {
color base = i < 16 ? lerpColor(cool, between, i / 16.0) : lerpColor(between, warm, (i - 16) / 16.0);
for (int j = 0; j <= 8; j++) shades[i][j] = lerpColor(base, spark, j / 8.0);
}
colorMode(HSB, 360, 100, 100, 100);
int row = 0;
for (float gy = gap / 2; gy < height; gy += gap * 0.866) {
float shift = (row % 2) * gap / 2;
row++;
for (float gx = gap / 2 + shift; gx < width; gx += gap) {
float fx = 0, fy = 0, pot = 0;
for (Magnet m : wandering) {
float dx = gx - m.x, dy = gy - m.y;
float d2 = dx * dx + dy * dy + 900;
fx += dx * m.charge / d2;
fy += dy * m.charge / d2;
pot += m.charge / sqrt(d2);
}
for (Magnet m : placed) {
float dx = gx - m.x, dy = gy - m.y;
float d2 = dx * dx + dy * dy + 900;
fx += dx * m.charge / d2;
fy += dy * m.charge / d2;
pot += m.charge / sqrt(d2);
}
// how much of this needle's pull comes from the pointer (0 to 1)
float share = 0;
if (presence > 0.01) {
float dx = gx - pointer.x, dy = gy - pointer.y;
float d2 = dx * dx + dy * dy + 900;
float c = pointer.charge * presence;
float others = sqrt(fx * fx + fy * fy);
float px = dx * c / d2, py = dy * c / d2;
float mine = sqrt(px * px + py * py);
share = mine / (mine + others + 0.0001);
fx += px;
fy += py;
pot += c / sqrt(d2);
}
float s = sqrt(fx * fx + fy * fy) * 200;
float n = s / (1 + s);
float a = atan2(fy, fx) + swirl;
float len = gap * (0.25 + 0.75 * n) * needle;
// warm near one pole, cool near the other; strong field is bright and thick
float p = pot * 90;
int k = int(16 + 16 * p / (1 + abs(p)));
float lit = pow(share, 1.6);
stroke(shades[k][int(lit * 8)], 16 + 84 * max(n, lit));
strokeWeight(0.8 + 2 * n);
float cx = cos(a) * len / 2, cy = sin(a) * len / 2;
line(gx - cx, gy - cy, gx + cx, gy + cy);
}
}
for (Magnet m : wandering) glow(m, false);
for (Magnet m : placed) glow(m, true);
// a ring spreads from where a magnet was just placed
if (pulse > 0) {
noFill();
stroke(nextCharge < 0 ? warm : cool, 80 * pulse);
strokeWeight(1 + 3 * pulse);
float r = (1 - pulse) * 220;
ellipse(pulseX, pulseY, r, r);
pulse -= 0.035;
}
}
// a soft light at each magnet; the ones you placed also wear a thin ring
void glow(Magnet m, boolean yours) {
color c = m.charge > 0 ? warm : cool;
noStroke();
fill(c, 6);
ellipse(m.x, m.y, 70, 70);
fill(c, 12);
ellipse(m.x, m.y, 34, 34);
fill(c, 90);
ellipse(m.x, m.y, 7, 7);
if (yours) {
noFill();
stroke(c, 55);
strokeWeight(1);
ellipse(m.x, m.y, 22, 22);
}
}
void mouseMoved() {
lastMove = millis();
}
void mouseDragged() {
lastMove = millis();
}
void mousePressed() {
lastMove = millis();
placed.add(new Magnet(mouseX, mouseY, 1.6 * nextCharge));
nextCharge = -nextCharge;
if (placed.size() > 10) placed.remove(0);
pulseX = mouseX;
pulseY = mouseY;
pulse = 1;
}
void keyPressed() {
placed.clear();
}