heartwood every commit a ring

Add slime mold (Physarum) generative art piece

df512561 by Isaac Bythewood · 15 days ago

added components/slimemold.js
@@ -0,0 +1,188 @@import React, { useEffect, useRef } from "react";import styles from "@styles/components/canvas.module.css";import PropTypes from "prop-types";const SlimeMold = ({ options }) => {  const isActive = options.isActive !== undefined ? options.isActive : true;  const numAgents = options.numAgents || 6000;  const canvas = useRef(null);  useEffect(() => {    const cvs = canvas.current;    cvs.width = cvs.offsetWidth;    cvs.height = cvs.offsetHeight;    const resizeCanvas = () => {      cvs.width = cvs.offsetWidth;      cvs.height = cvs.offsetHeight;    };    window.addEventListener("resize", resizeCanvas);    return () => {      window.removeEventListener("resize", resizeCanvas);    };  }, []);  useEffect(() => {    const cvs = canvas.current;    const ctx = cvs.getContext("2d");    const scale = 2;    const tw = Math.max(1, Math.floor(cvs.width / scale));    const th = Math.max(1, Math.floor(cvs.height / scale));    let trail = new Float32Array(tw * th);    let trailNext = new Float32Array(tw * th);    const agents = new Array(numAgents);    for (let i = 0; i < numAgents; i++) {      const cx = tw / 2;      const cy = th / 2;      const r = Math.sqrt(Math.random()) * Math.min(tw, th) * 0.4;      const a = Math.random() * Math.PI * 2;      const x = cx + Math.cos(a) * r;      const y = cy + Math.sin(a) * r;      agents[i] = {        x,        y,        heading: Math.atan2(cy - y, cx - x),      };    }    const sensorAngle = Math.PI / 4;    const sensorDistance = 9;    const rotationAngle = Math.PI / 8;    const moveSpeed = 1;    const decay = 0.96;    const depositAmount = 0.5;    const sense = (x, y, heading, offset) => {      const angle = heading + offset;      const sx = Math.floor(x + Math.cos(angle) * sensorDistance);      const sy = Math.floor(y + Math.sin(angle) * sensorDistance);      if (sx < 0 || sx >= tw || sy < 0 || sy >= th) return -1;      return trail[sy * tw + sx];    };    const stops = [      [0.0, 0, 0, 0],      [0.35, 14, 63, 244],      [0.7, 132, 43, 255],      [1.0, 255, 255, 255],    ];    const colorLUT = new Uint8ClampedArray(256 * 3);    for (let i = 0; i < 256; i++) {      const t = i / 255;      let s = 0;      while (s < stops.length - 2 && t > stops[s + 1][0]) s++;      const [t0, r0, g0, b0] = stops[s];      const [t1, r1, g1, b1] = stops[s + 1];      const k = (t - t0) / (t1 - t0);      colorLUT[i * 3] = Math.round(r0 + (r1 - r0) * k);      colorLUT[i * 3 + 1] = Math.round(g0 + (g1 - g0) * k);      colorLUT[i * 3 + 2] = Math.round(b0 + (b1 - b0) * k);    }    const offCanvas = document.createElement("canvas");    offCanvas.width = tw;    offCanvas.height = th;    const offCtx = offCanvas.getContext("2d");    const offImageData = offCtx.createImageData(tw, th);    const offData = offImageData.data;    let animationFrame = null;    const step = () => {      for (let i = 0; i < numAgents; i++) {        const agent = agents[i];        const f = sense(agent.x, agent.y, agent.heading, 0);        const l = sense(agent.x, agent.y, agent.heading, -sensorAngle);        const r = sense(agent.x, agent.y, agent.heading, sensorAngle);        if (f > l && f > r) {          // continue straight        } else if (f < l && f < r) {          agent.heading += (Math.random() - 0.5) * 2 * rotationAngle;        } else if (l > r) {          agent.heading -= rotationAngle;        } else if (r > l) {          agent.heading += rotationAngle;        }        agent.x += Math.cos(agent.heading) * moveSpeed;        agent.y += Math.sin(agent.heading) * moveSpeed;        if (agent.x < 0 || agent.x >= tw || agent.y < 0 || agent.y >= th) {          agent.x = Math.max(0, Math.min(tw - 1, agent.x));          agent.y = Math.max(0, Math.min(th - 1, agent.y));          agent.heading = Math.random() * Math.PI * 2;        }        const idx = Math.floor(agent.y) * tw + Math.floor(agent.x);        trail[idx] = Math.min(1, trail[idx] + depositAmount);      }      for (let y = 0; y < th; y++) {        for (let x = 0; x < tw; x++) {          const x0 = x === 0 ? x : x - 1;          const x2 = x === tw - 1 ? x : x + 1;          const y0 = y === 0 ? y : y - 1;          const y2 = y === th - 1 ? y : y + 1;          const sum =            trail[y0 * tw + x0] +            trail[y0 * tw + x] +            trail[y0 * tw + x2] +            trail[y * tw + x0] +            trail[y * tw + x] +            trail[y * tw + x2] +            trail[y2 * tw + x0] +            trail[y2 * tw + x] +            trail[y2 * tw + x2];          trailNext[y * tw + x] = (sum / 9) * decay;        }      }      const tmp = trail;      trail = trailNext;      trailNext = tmp;      const len = tw * th;      for (let i = 0; i < len; i++) {        const v = trail[i];        const ci =          (v >= 1 ? 255 : v <= 0 ? 0 : Math.floor(Math.pow(v, 0.6) * 255)) * 3;        const di = i * 4;        offData[di] = colorLUT[ci];        offData[di + 1] = colorLUT[ci + 1];        offData[di + 2] = colorLUT[ci + 2];        offData[di + 3] = 255;      }      offCtx.putImageData(offImageData, 0, 0);      ctx.imageSmoothingEnabled = true;      ctx.imageSmoothingQuality = "high";      ctx.drawImage(offCanvas, 0, 0, cvs.width, cvs.height);      if (isActive) {        animationFrame = window.requestAnimationFrame(step);      }    };    if (isActive) {      animationFrame = window.requestAnimationFrame(step);    }    return () => {      if (animationFrame) {        window.cancelAnimationFrame(animationFrame);      }    };  }, [isActive, numAgents]);  return <canvas ref={canvas} className={styles.canvas} />;};SlimeMold.propTypes = {  options: PropTypes.object,};export default SlimeMold;
modified pages/art.js
@@ -6,6 +6,7 @@ import Image from "next/image";import Page from "../components/page";import Constellations from "../components/constellations";import RetroStars from "../components/retrostars";import SlimeMold from "../components/slimemold";const Art = () => {  const [lightboxImage, setLightboxImage] = useState(null);
@@ -247,6 +248,34 @@ const Art = () => {      >        See the Code      </a>      <h2 className={styles.subheading}>        <span>002</span> Slime Mold      </h2>      <p className={styles.paragraph}>        Thousands of agents wander a dark field, each leaving a faint chemical        trail and steering toward the strongest scent ahead. From three simple        rules — sense, turn, deposit — emerge living networks reminiscent of        Physarum slime molds, neurons, and the cosmic web. The pattern never        settles; trails decay as fast as they form.      </p>      <div className={styles.artContainer}>        <SlimeMold options={{ isActive: activeArt === "slimemold" }} />        <button          className={styles.playButton}          onClick={() => handleArtToggle("slimemold")}          data-active={activeArt === "slimemold"}        >          {activeArt === "slimemold" ? "⏸" : "▶"}        </button>      </div>      <a        className={styles.artItemButton}        href="https://github.com/overshard/isaacbythewood.com/blob/master/components/slimemold.js"        rel="noopener noreferrer"        target="_blank"      >        See the Code      </a>      {mounted && lightboxImage !== null && createPortal(        <div className={styles.lightboxOverlay} onClick={() => closeLightbox()}>          <span