repos

isaacbythewood.com-nextjs

mirror archived upstream

Animation-heavy personal portfolio on Next.js: a custom cursor, a page-transition loader, and canvas backgrounds.

canvas-animationscss-modulesdark-themehandcodednextjspersonal-websiteportfolioreact

5.7 KB · 189 lines · JavaScript Raw History
  1import React, { useEffect, useRef } from "react";
  2import styles from "@styles/components/canvas.module.css";
  3import PropTypes from "prop-types";
  4
  5const SlimeMold = ({ options }) => {
  6  const isActive = options.isActive !== undefined ? options.isActive : true;
  7  const numAgents = options.numAgents || 6000;
  8  const canvas = useRef(null);
  9
 10  useEffect(() => {
 11    const cvs = canvas.current;
 12
 13    cvs.width = cvs.offsetWidth;
 14    cvs.height = cvs.offsetHeight;
 15
 16    const resizeCanvas = () => {
 17      cvs.width = cvs.offsetWidth;
 18      cvs.height = cvs.offsetHeight;
 19    };
 20    window.addEventListener("resize", resizeCanvas);
 21
 22    return () => {
 23      window.removeEventListener("resize", resizeCanvas);
 24    };
 25  }, []);
 26
 27  useEffect(() => {
 28    const cvs = canvas.current;
 29    const ctx = cvs.getContext("2d");
 30
 31    const scale = 2;
 32    const tw = Math.max(1, Math.floor(cvs.width / scale));
 33    const th = Math.max(1, Math.floor(cvs.height / scale));
 34    let trail = new Float32Array(tw * th);
 35    let trailNext = new Float32Array(tw * th);
 36
 37    const agents = new Array(numAgents);
 38    for (let i = 0; i < numAgents; i++) {
 39      const cx = tw / 2;
 40      const cy = th / 2;
 41      const r = Math.sqrt(Math.random()) * Math.min(tw, th) * 0.4;
 42      const a = Math.random() * Math.PI * 2;
 43      const x = cx + Math.cos(a) * r;
 44      const y = cy + Math.sin(a) * r;
 45      agents[i] = {
 46        x,
 47        y,
 48        heading: Math.atan2(cy - y, cx - x),
 49      };
 50    }
 51
 52    const sensorAngle = Math.PI / 4;
 53    const sensorDistance = 9;
 54    const rotationAngle = Math.PI / 8;
 55    const moveSpeed = 1;
 56    const decay = 0.96;
 57    const depositAmount = 0.5;
 58
 59    const sense = (x, y, heading, offset) => {
 60      const angle = heading + offset;
 61      const sx = Math.floor(x + Math.cos(angle) * sensorDistance);
 62      const sy = Math.floor(y + Math.sin(angle) * sensorDistance);
 63      if (sx < 0 || sx >= tw || sy < 0 || sy >= th) return -1;
 64      return trail[sy * tw + sx];
 65    };
 66
 67    const stops = [
 68      [0.0, 0, 0, 0],
 69      [0.35, 14, 63, 244],
 70      [0.7, 132, 43, 255],
 71      [1.0, 255, 255, 255],
 72    ];
 73    const colorLUT = new Uint8ClampedArray(256 * 3);
 74    for (let i = 0; i < 256; i++) {
 75      const t = i / 255;
 76      let s = 0;
 77      while (s < stops.length - 2 && t > stops[s + 1][0]) s++;
 78      const [t0, r0, g0, b0] = stops[s];
 79      const [t1, r1, g1, b1] = stops[s + 1];
 80      const k = (t - t0) / (t1 - t0);
 81      colorLUT[i * 3] = Math.round(r0 + (r1 - r0) * k);
 82      colorLUT[i * 3 + 1] = Math.round(g0 + (g1 - g0) * k);
 83      colorLUT[i * 3 + 2] = Math.round(b0 + (b1 - b0) * k);
 84    }
 85
 86    const offCanvas = document.createElement("canvas");
 87    offCanvas.width = tw;
 88    offCanvas.height = th;
 89    const offCtx = offCanvas.getContext("2d");
 90    const offImageData = offCtx.createImageData(tw, th);
 91    const offData = offImageData.data;
 92
 93    let animationFrame = null;
 94
 95    const step = () => {
 96      for (let i = 0; i < numAgents; i++) {
 97        const agent = agents[i];
 98        const f = sense(agent.x, agent.y, agent.heading, 0);
 99        const l = sense(agent.x, agent.y, agent.heading, -sensorAngle);
100        const r = sense(agent.x, agent.y, agent.heading, sensorAngle);
101
102        if (f > l && f > r) {
103          // continue straight
104        } else if (f < l && f < r) {
105          agent.heading += (Math.random() - 0.5) * 2 * rotationAngle;
106        } else if (l > r) {
107          agent.heading -= rotationAngle;
108        } else if (r > l) {
109          agent.heading += rotationAngle;
110        }
111
112        agent.x += Math.cos(agent.heading) * moveSpeed;
113        agent.y += Math.sin(agent.heading) * moveSpeed;
114
115        if (agent.x < 0 || agent.x >= tw || agent.y < 0 || agent.y >= th) {
116          agent.x = Math.max(0, Math.min(tw - 1, agent.x));
117          agent.y = Math.max(0, Math.min(th - 1, agent.y));
118          agent.heading = Math.random() * Math.PI * 2;
119        }
120
121        const idx = Math.floor(agent.y) * tw + Math.floor(agent.x);
122        trail[idx] = Math.min(1, trail[idx] + depositAmount);
123      }
124
125      for (let y = 0; y < th; y++) {
126        for (let x = 0; x < tw; x++) {
127          const x0 = x === 0 ? x : x - 1;
128          const x2 = x === tw - 1 ? x : x + 1;
129          const y0 = y === 0 ? y : y - 1;
130          const y2 = y === th - 1 ? y : y + 1;
131          const sum =
132            trail[y0 * tw + x0] +
133            trail[y0 * tw + x] +
134            trail[y0 * tw + x2] +
135            trail[y * tw + x0] +
136            trail[y * tw + x] +
137            trail[y * tw + x2] +
138            trail[y2 * tw + x0] +
139            trail[y2 * tw + x] +
140            trail[y2 * tw + x2];
141          trailNext[y * tw + x] = (sum / 9) * decay;
142        }
143      }
144      const tmp = trail;
145      trail = trailNext;
146      trailNext = tmp;
147
148      const len = tw * th;
149      for (let i = 0; i < len; i++) {
150        const v = trail[i];
151        const ci =
152          (v >= 1 ? 255 : v <= 0 ? 0 : Math.floor(Math.pow(v, 0.6) * 255)) * 3;
153        const di = i * 4;
154        offData[di] = colorLUT[ci];
155        offData[di + 1] = colorLUT[ci + 1];
156        offData[di + 2] = colorLUT[ci + 2];
157        offData[di + 3] = 255;
158      }
159      offCtx.putImageData(offImageData, 0, 0);
160
161      ctx.imageSmoothingEnabled = true;
162      ctx.imageSmoothingQuality = "high";
163      ctx.drawImage(offCanvas, 0, 0, cvs.width, cvs.height);
164
165      if (isActive) {
166        animationFrame = window.requestAnimationFrame(step);
167      }
168    };
169
170    if (isActive) {
171      animationFrame = window.requestAnimationFrame(step);
172    }
173
174    return () => {
175      if (animationFrame) {
176        window.cancelAnimationFrame(animationFrame);
177      }
178    };
179  }, [isActive, numAgents]);
180
181  return <canvas ref={canvas} className={styles.canvas} />;
182};
183
184SlimeMold.propTypes = {
185  options: PropTypes.object,
186};
187
188export default SlimeMold;