const canvas = document.querySelector('.js-canvas');
const ctx = canvas.getContext('2d');
const canvas2 = document.querySelector('.js-duplicate');
const ctx2 = canvas2.getContext('2d');
const dim = Math.min(window.innerWidth, window.innerHeight) * 0.95;
const TO_RADIAN = PI / 180;
const maxSpread = 90 * TO_RADIAN;
let spread = 45 * TO_RADIAN;
canvas.width = canvas2.width = w;
canvas.height = canvas2.height = h;
constructor(position = {x : 0, y: 0}, length = 100, divergeAt = 0.5, angle = 0, depth = 0, spread = 45 * TO_RADIAN) {
this.position = position;
this.divergeAt = divergeAt;
const nextLength = this.length * 0.75;
const nextDepth = this.depth + 1;
this.angle + this.spread,
this.angle - this.spread,
update(spread, divergeAt) {
this.divergeAt = divergeAt;
const dl = this.length * this.divergeAt;
x: this.position.x + (Math.cos(this.angle) * dl),
y: this.position.y + (Math.sin(this.angle) * dl),
return this.depth < maxDepth;
const rootBranch = new Branch(
const drawBranch = (branch, phase) => {
const h = ~~(200 + (160 * phase));
const l = 50 + ~~(((branch.depth / (maxDepth + 1))) * 50);
const endX = branch.length;
ctx.strokeStyle = `hsl(${h}, 100%, ${l}%)`;
ctx.translate(branch.position.x, branch.position.y);
ctx.rotate(branch.angle);
ctx.fillStyle = `hsl(${h}, 100%, 50%)`;
ctx.arc(endX, endY, r, 0, PI * 2, false);
branch.branches.forEach((b) => {
const map = (value, start1, stop1, start2, stop2) => ((value - start1) / (stop1 - start1)) * (stop2 - start2) + start2;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx2.clearRect(0, 0, ctx2.canvas.width, ctx2.canvas.height);
let phase = rootBranch.spread / maxSpread;
phase = map(Math.sin(autoTick), -1, 1, 0, 1);
spread = phase * maxSpread;
divergeAt = map(Math.sin(autoTick), -1, 1, 0, 0.5);
rootBranch.update(spread, divergeAt);
drawBranch(rootBranch, phase);
const angleInc = PI * 2 / numSegments;
for (let i = 0; i < numSegments; i++) {
ctx2.translate(midX, midY);
ctx2.drawImage(canvas, -w / 2, -h / 2);
requestAnimationFrame(loop);
const onPointerMove = (e) => {
const target = (e.touches && e.touches.length) ? e.touches[0] : e;
const { clientX: x, clientY: y } = target;
const width = window.innerWidth;
const height = window.innerHeight;
spread = (x / width) * maxSpread;
document.body.addEventListener('mousemove', onPointerMove);
document.body.addEventListener('touchmove', onPointerMove);
document.addEventListener('mouseenter', () => {
document.addEventListener('mouseleave', () => {