<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js" integrity="sha512-3RlxD1bW34eFKPwj9gUXEWtdSMC59QqIqHnD8O/NoTwSJhgxRizdcFVQhUMFyTp5RwLTDL0Lbcqtl8b7bFAzog==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
body {margin:0px; padding:0px; overflow: hidden}
const maxReflection = 32;
let mirrors;

function setup() {
    canvas = createCanvas(windowWidth, windowHeight);
    updateMirrors();
}

function updateMirrors() {
    mirrors = [];
    const w = width;
    const r = w / 4;
    const l = -w / 4;
    const rn = w / 20 * random(1);
    const N = 16;
    const my = [];
  
    for (let i = 0; i < N + 1 ; i ++) {
      my.push(height / 4 + random(-rn, rn));
    }
  
    for (let i = 0; i < N; i ++) {
      const t0 = i / N;
      const t1 = (i + 1) / N;
        p0 = createVector(lerp(l, r, t0), my[i]);
        p1 = createVector(lerp(l, r, t1),  my[i + 1]);
        mirrors.push(new LineSegment().fromTwoPoints(p0, p1));
    }
}

function hitTest(o, d, lineSegments, ignore) {
    let intersection, hitObject, minDistance = Infinity;
    const ray = new Line().fromPointAndVector(o, d);
    lineSegments.forEach((ls)=>{
        if (ls != ignore && ls.intersects(ray)) {
            const temp = ls.getIntersectionPoint(ray);
            const dot = d.dot(temp.copy().sub(o));
            const dist = o.dist(temp);
            if (dot >= 0 && dist < minDistance) {
                intersection = temp;
                minDistance = dist;
                hitObject = ls;
            }
        }
    });

    return intersection ? {point: intersection, object: hitObject} : null;
}

function reflect(i, n) {
  return i.copy().add(n.copy().mult(i.copy().mult(2).dot(n) / pow(n.mag(), 2) * -1));
}

function draw() {
    clear();
    background(235, 255, 160);

    const l = max(width, height);
    const angle = frameCount / 60 * PI;
    if (frameCount % 120 == 0) { updateMirrors();}

    let origin = createVector(0, -height * 0.75);
    let target = createVector(sin(frameCount / 120 * PI) * width * 0.24, height * 0.25);
    let dir = target.copy().sub(origin).normalize();

    const points = [origin];
    let currentMirror = null;
    for (let i = 0; i < maxReflection; i ++) {
        const hitResult = hitTest(origin, dir, mirrors, currentMirror);

        if (hitResult) {
            points.push(hitResult.point);
            currentMirror = hitResult.object;

            const p0 = hitResult.object.p0;
            const p1 = hitResult.object.p1;
            normal = createVector(
                -(p0.y - p1.y),
                p0.x - p1.x
            ).normalize();

            normal = (normal.dot(dir) > 0) ? normal.mult(-1) : normal;
            origin = hitResult.point;
            dir = reflect(dir, normal);
        } else {
            points.push(origin.copy().add(dir.copy().mult(l * 2)));
            break;
        }
    }

    push();

    translate(width / 2, height / 2);
    fill(255); stroke(0); strokeWeight(1);
    for (let i = 1; i < points.length; i++) {
        line(points[i - 1].x, points[i - 1].y, points[i].x, points[i].y);
    }
    strokeWeight(3);
    mirrors.forEach((m)=>{m.draw();});
    fill(0); noStroke(0);
    points.forEach((p)=>{drawCircleMarker(p, 4);})
    pop();
}

class Line {
  constructor(a, b, c) {
    this.a = a;
    this.b = b;
    this.c = c;
  }

  fromTwoPoints(p0, p1) {
    let dx = p1.x - p0.x;
    let dy = p1.y - p0.y;
    this.a = dy;
    this.b = -dx;
    this.c = dx * p0.y - dy * p0.x;
    return this;
  }

  fromPointAndAngle(p0, angle) {
    let p1 = {x: p0.x + cos(angle), y: p0.y + sin(angle)};
    return this.fromTwoPoints(p0, p1);
  }

  fromPointAndVector(p0, v) {
    let p1 = {x: p0.x + v.x, y: p0.y + v.y};
    return this.fromTwoPoints(p0, p1);
  }

  intersects(o) {
    if (o instanceof Line) {
      let d = this.a * o.b - o.a * this.b;
      return d != 0.0;
    } else if (o instanceof LineSegment) {
      let t1 = this.a * o.p0.x + this.b * o.p0.y + this.c;
      let t2 = this.a * o.p1.x + this.b * o.p1.y + this.c;
      return t1 * t2 <= 0;
    }
    return undefined;
  }

  getIntersectionPoint(o) {
    if (o instanceof Line) {
      let d = this.a * o.b - o.a * this.b;
      if (d == 0.0) { return undefined; }
      let x = (this.b * o.c - o.b * this.c) / d;
      let y = (o.a * this.c - this.a * o.c) / d;
      return createVector(x, y);
    } else if (o instanceof LineSegment) {
      if (!this.intersects(o)) { return undefined; }
      return this.getIntersectionPoint(o.toLine());
    }
    return undefined;
  }

  getAngle() {
    return atan2(this.a, -this.b);
  }

  getPerpendicular(p) {
    return new Line(this.b, -this.a, this.a * p.y - this.b * p.x);
  }

  getParallel(p) {
    return new Line(this.a, this.b, -this.a * p.x - this.b * p.y);
  }

  getNearestPoint(p) {
    let l = this.getPerpendicular(p);
    return this.getIntersectionPoint(l);
  }

  draw(rect) {
    if (!rect) { rect = {x:0, y:0, w:0, h:0}}
    let l0, l1;
    if (abs(this.a) > abs(this.b)) {
      l0 = new Line().fromTwoPoints({x:rect.x, y:rect.y}, {x:rect.x + width, y:rect.y});
      l1 = new Line().fromTwoPoints({x:rect.x, y:rect.y + height}, {x:rect.x + width,   y:height});
    } else {
        l0 = new Line().fromTwoPoints({x:rect.x, y:rect.y}, {x:rect.x, y:height});
      l1 = new Line().fromTwoPoints({x:rect.x + width, y:rect.y}, {x:rect.x + width, y:rect.y + height});
    }

    let p0 = this.getIntersectionPoint(l0);
    let p1 = this.getIntersectionPoint(l1);
    line(p0.x, p0.y, p1.x, p1.y);
  }
}

class LineSegment {
  constructor(x0, y0, x1, y1) {
    this.p0 = createVector(x0, y0);
    this.p1 = createVector(x1, y1);
  }

  fromTwoPoints(p0, p1) {
    this.p0 = p0;
    this.p1 = p1;
    return this;
  }

  fromTwoPointsAndLength(p0, p1, length) {
    this.p0 = p0;
    let n = p1.copy().sub(p0).normalize();
    this.p1 = n.mult(length).add(p0);
    return this;
  }

  toLine() {
    return new Line().fromTwoPoints(this.p0, this.p1);
  }

  intersects(o) {
    if (o instanceof Line) {
      let t0 = o.a * this.p0.x + o.b * this.p0.y + o.c;
      let t1 = o.a * this.p1.x + o.b * this.p1.y + o.c;
      return t0 * t1 < 0;
    } else if (o instanceof LineSegment) {
      return this.intersects(o.toLine()) && o.intersects(this.toLine());
    }
    return undefined;
  }

  getIntersectionPoint(o) {
    if (o instanceof Line) {
      if (!this.intersects(o)) { return undefined; }
      return o.getIntersectionPoint(this.toLine());
    } else if (o instanceof LineSegment) {
      if (!this.intersects(o)) { return undefined; }
      return o.toLine().getIntersectionPoint(this.toLine());
    }
    return undefined;
  }

  getAngle() {
    return atan2(this.p1.y - this.p0.y, this.p1.x - this.p0.x);
  }

  getLength() {
    return p0.dist(p1);
  }

  getNearestPoint(p) {
    if (this.p1.copy().sub(this.p0).dot(p.copy().sub(this.p0)) < 0) return this.p0;
    if (this.p0.copy().sub(this.p1).dot(p.copy().sub(this.p1)) < 0) return this.p1;
    return this.toLine().getNearestPoint(p);
  }

  getBisection() {
    let o = this.getMidPoint();
    return this.toLine().getPerpendicular(o);
  }

  getMidPoint() {
    return this.p0.copy().add(this.p1).mult(0.5);
  }

  getPerpendicular(p) {
    return this.toLine().getPerpendicular(p);
  }

  getParallel(p) {
    return this.toLine().getParallel(p);
  }

  draw() {
    line(this.p0.x, this.p0.y, this.p1.x, this.p1.y);
  }
}

function drawCircleMarker(p, size) {
  ellipse(p.x, p.y, size * 2, size * 2);
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.