Skip to contentCREATIVE·CODE·ART
1
1
1
1
1
1
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();
}