Foolish Rambles

We have a journey, loosely mapped out. It follows a unidirectional lifetime, taking us sequentially (mostly sort of ish) via the 21 way stations along the route. Mike’s not that ordered or regimented though, and so there’ll always be curious side streets and snickets to explore. There’ll be conversations with interesting people we’d otherwise never have met and nondescript that led nowhere in particular but were still afternoons well spent.

It is here that those off track rambles meet so their stories can be joined. Some will be just a few short paragraphs, others will go on and on and on… such is the way with these things. They’re presented in no particular order. Definitely not chronologically, and unlikely to be categorised any time soon. Take a peek up them as they take your fancy.

A Ramble on… Final Judgement
from around 2005, (with recent minor changes)

<script>import * as d3 from "https://cdn.skypack.dev/d3@6.7.0";


class Vector {
  constructor(public x: number, public y: number) {}

  private max = null;

  setLength(newLength: number) {
    const length = Math.sqrt(this.x * this.x + this.y * this.y);
    this.x *= newLength / length;
    this.y *= newLength / length;
  }

  add(vector: Vector) {
    this.x += vector.x;
    this.y += vector.y;
  }

  sub(vector: Vector) {
    this.x -= vector.x;
    this.y -= vector.y;
  }

  mul(val: number) {
    this.x *= val;
    this.y *= val;
  }

  div(val: number) {
    if (val != 0) {
      this.x /= val;
      this.y /= val;
    }
  }

  checkMax() {
    if (this.max) {
      const length = Math.sqrt(this.x * this.x + this.y * this.y);
      if (length > this.max) {
        this.setLength(this.max);
      }
    }
  }

  angle() {
    let angle = (Math.atan(this.y / this.x) * 180) / Math.PI;
    if (this.x < 0) {
      angle += 180;
    }
    return angle;
  }

  limit(max: number) {
    this.max = max;
    this.checkMax();
  }

  static add(vector1: Vector, vector2: Vector): Vector {
    return new Vector(vector1.x + vector2.x, vector1.y + vector2.y);
  }

  static sub(vector1: Vector, vector2: Vector): Vector {
    return new Vector(vector1.x - vector2.x, vector1.y - vector2.y);
  }

  static dist(vector1: Vector, vector2: Vector): number {
    return Math.sqrt(
      Math.pow(vector1.x - vector2.x, 2) + Math.pow(vector1.y - vector2.y, 2)
    );
  }
}


const randBetween = (min: number, max: number) => {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
};

class Boid {
  position: Vector;
  velocity: Vector;
  acceleration: Vector;
  maxForce: number;
  maxSpeed: number;
  perceptionRadius: number;

  constructor(private width, private height) {
    this.position = new Vector(randBetween(0, width), randBetween(0, height));
    this.velocity = new Vector(randBetween(-100, 100), randBetween(-100, 100));
    this.velocity.setLength(randBetween(2, 4));
    this.acceleration = new Vector(0, 0);
    this.maxForce = 1;
    this.maxSpeed = 10;
    this.perceptionRadius = 100;
  }

  calcAcceletarion(boids: Boid[], al: number, coh: number, sep: number) {
    let steering = new Vector(0, 0);
    let align = new Vector(0, 0);
    let cohesion = new Vector(0, 0);
    let separation = new Vector(0, 0);
    let perceptedBoids = 0;
    for (let other of boids) {
      let d = Vector.dist(this.position, other.position);

      if (other != this && d < this.perceptionRadius) {
        perceptedBoids++;
        align.add(other.velocity);
        let diff = Vector.sub(this.position, other.position);
        if (d > 0) {
          
          diff.div(d * d);
        }
        separation.add(diff);
        cohesion.add(other.position);
      }
    }
    if (perceptedBoids > 0) {
      align.div(perceptedBoids);
      align.setLength(this.maxSpeed);
      align.sub(this.velocity);
      align.limit(this.maxForce);

      separation.div(perceptedBoids);
      separation.setLength(this.maxSpeed);
      separation.sub(this.velocity); //subtract actual from desired
      separation.limit(this.maxForce);

      cohesion.div(perceptedBoids);
      cohesion.sub(this.position); //desired velocity
      cohesion.setLength(this.maxSpeed);
      cohesion.sub(this.velocity); //subtract actual from desired
      cohesion.limit(this.maxForce);

      align.mul(al);
      steering.add(align);
      cohesion.mul(coh);
      steering.add(cohesion);
      separation.mul(sep);
      steering.add(separation);
    }
    return steering;
  }

  edges() {
    if (this.position.x > this.width + 25) {
      this.position.x = -25;
    }
    if (this.position.x < -25) {
      this.position.x = this.width + 25;
    }
    if (this.position.y > this.height + 25) {
      this.position.y = -25;
    }
    if (this.position.y < -25) {
      this.position.y = this.height + 25;
    }
  }

  flock(boids, al, coh, sep) {
    this.acceleration.mul(0);
    this.acceleration.add(this.calcAcceletarion(boids, al, coh, sep));
  }

  update() {
    this.position.add(this.velocity);
    this.velocity.add(this.acceleration);
    this.velocity.limit(this.maxSpeed);
    this.edges();
  }
}



const fishPath =
  'M327.1 96c-89.97 0-168.54 54.77-212.27 101.63L27.5 131.58c-12.13-9.18-30.24.6-27.14 14.66L24.54 256 .35 365.77c-3.1 14.06 15.01 23.83 27.14 14.66l87.33-66.05C158.55 361.23 237.13 416 327.1 416 464.56 416 576 288 576 256S464.56 96 327.1 96zm87.43 184c-13.25 0-24-10.75-24-24 0-13.26 10.75-24 24-24 13.26 0 24 10.74 24 24 0 13.25-10.75 24-24 24z';

let colors = d3
  .scaleLinear()
  .domain([1, 10])
  .range(['skyblue', '#444']);

let svg;
const width = document.querySelector('body').clientWidth;
const height = Math.min(document.querySelector('body').clientHeight, 400);
const fishScale = 0.03;

function drawBoids(positions) {
  svg = d3
    .select('body')
    .append('svg')
    .attr('width', width)
    .attr('height', height)
    .attr('preserveAspectRatio', 'xMinYMid')
    .attr('viewBox', `0 0 ${width} ${height}`);

  svg
    .append('g')
    .selectAll('g')
    .data(positions)
    .enter()
    .append('g')
    .attr(
      'transform',
      d =>
        `translate(${d.position.x} ${
          d.position.y
        }) scale(${fishScale}) rotate(${d.velocity.angle()})`
    )
    .append('path')
    .attr('d', fishPath)
    .attr('fill', d => colors((d.position.x * 10) / width));
}

function updateBoids(positions) {
  const boids = svg
    .select('g')
    .selectAll('g')
    .data(positions);

  boids
    .transition()
    .duration(d =>
      d.position.x < 0 ||
      d.position.x > width ||
      d.position.y < 0 ||
      d.position.y > height
        ? 0
        : 50
    )
    .attr('transform', d => {
      return `translate(${d.position.x} ${
        d.position.y
      }) scale(${fishScale}) rotate(${d.velocity.angle()})`;
    });
}

function start() {
  const flock = [];
  for (let i = 0; i < 200; i++) {
    flock.push(new Boid(width, height));
  }

  drawBoids(flock);

  setInterval(() => {
    for (let boid of flock) {
      boid.flock(flock, 1, 1, 1.03);
      boid.update();
    }
    updateBoids(flock);
  }, 50);
}

start();


</script>