heartwood every commit a ring
7.9 KB raw
import React, { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import styles from "@styles/pages/art.module.css";
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 ArtCard = ({ src, alt, title, number, priority, onClick }) => {
  const [loaded, setLoaded] = useState(false);
  return (
    <div className={styles.artItem} onClick={onClick}>
      <span className={styles.cardImage}>
        <Image
          src={src}
          alt={alt}
          width={640}
          height={360}
          className={`mouse-activate ${loaded ? styles.imgLoaded : ""}`}
          priority={priority}
          onLoad={() => setLoaded(true)}
        />
      </span>
      <h2 className={styles.artItemHeader}>
        {title} <span>{number}</span>
      </h2>
    </div>
  );
};

const ACRYLIC_POURS = [
  { number: "006", title: "Molten Copper", priority: true },
  { number: "005", title: "Nebulas in Triangulum", priority: true },
  { number: "004", title: "Metal on Mars", priority: true },
  { number: "003", title: "Water on Jupiter", priority: true },
  { number: "002", title: "Cracks in Clay", priority: false },
  { number: "001", title: "Reef Drop-off", priority: false },
  { number: "000", title: "Blood in Waves", priority: false },
];

const Art = () => {
  const [lightboxImage, setLightboxImage] = useState(null);
  const [lightboxLoaded, setLightboxLoaded] = useState(false);
  const [activeArt, setActiveArt] = useState("constellations");
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  const openLightbox = (image) => {
    setLightboxImage(image);
    document.body.style.overflowY = "hidden";
  };

  const closeLightbox = () => {
    setLightboxImage(null);
    setLightboxLoaded(false);
    history.replaceState(null, null, " ");
    document.body.style.overflowY = "scroll";
  };

  const handleArtToggle = (artName) => {
    setActiveArt(activeArt === artName ? null : artName);
  };

  return (
    <Page title="Art" description="Some of my art... what even is art...">
      <div className={styles.background} />
      <h1 className={styles.heading}>Acrylic Pours</h1>
      <p className={styles.paragraph}>
        A bit more traditional than my usual art. Each piece starts as a mix of
        acrylic paint, water, glue, and silicone oil poured onto canvas, then
        manipulated with a heat gun to bring out cells and organic patterns. No
        two pours come out the same and the process is as much about letting go
        of control as it is about technique.
      </p>
      <div className={styles.artGrid}>
        {ACRYLIC_POURS.map(({ number, title, priority }) => {
          const src = `/static/images/art/acrylic-pours/${number}.webp`;
          return (
            <ArtCard
              key={number}
              src={src}
              alt={title}
              title={title}
              number={number}
              priority={priority}
              onClick={() => openLightbox(src)}
            />
          );
        })}
      </div>
      <h1 className={styles.heading}>Emergent Generative Art</h1>
      <p className={styles.paragraph}>
        Code as a creative medium. These pieces are built entirely in JavaScript
        on HTML canvas, each one a small system of simple rules that produces
        complex, unpredictable behavior. The art emerges from the interaction of
        autonomous entities rather than being explicitly drawn.
      </p>
      <h2 className={styles.subheading}>
        <span>000</span> Constellations
      </h2>
      <p className={styles.paragraph}>
        Drifting points on a dark canvas that form connections with their nearest
        neighbors. As stars wander closer together lines appear between them,
        brighter the nearer they get, building and dissolving constellations
        that never repeat.
      </p>
      <div className={styles.artContainer}>
        <Constellations
          options={{ numStars: 50, isActive: activeArt === "constellations" }}
        />
        <button
          className={styles.playButton}
          onClick={() => handleArtToggle("constellations")}
          data-active={activeArt === "constellations"}
        >
          {activeArt === "constellations" ? "⏸" : "▶"}
        </button>
      </div>
      <a
        className={styles.artItemButton}
        href="https://github.com/overshard/isaacbythewood.com/blob/master/components/constellations.js"
        rel="noopener noreferrer"
        target="_blank"
      >
        See the Code
      </a>
      <h2 className={styles.subheading}>
        <span>001</span> Retro Stars
      </h2>
      <p className={styles.paragraph}>
        Layered star fields at different depths that drift in response to your
        cursor, creating a parallax effect. Inspired by the pixel art aesthetic
        of Celeste and the feeling of staring into a sky that moves with you.
      </p>
      <div className={styles.artContainer}>
        <RetroStars
          options={{ numStars: 50, isActive: activeArt === "retrostars" }}
        />
        <button
          className={styles.playButton}
          onClick={() => handleArtToggle("retrostars")}
          data-active={activeArt === "retrostars"}
        >
          {activeArt === "retrostars" ? "⏸" : "▶"}
        </button>
      </div>
      <a
        className={styles.artItemButton}
        href="https://github.com/overshard/isaacbythewood.com/blob/master/components/retrostars.js"
        rel="noopener noreferrer"
        target="_blank"
      >
        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
            className={styles.lightboxImageWrapper}
            onClick={(e) => e.stopPropagation()}
          >
            <div
              className={
                styles.lightboxLoading +
                (lightboxLoaded ? " " + styles.hide : "")
              }
            >
              Loading...
            </div>
            <Image
              className={
                styles.lightboxImage + (lightboxLoaded ? " " + styles.show : "")
              }
              src={lightboxImage}
              alt="Lightbox"
              fill
              sizes="90vw"
              style={{ objectFit: "contain" }}
              onLoad={() => setLightboxLoaded(true)}
            />
            <button
              className={styles.lightboxClose}
              onClick={() => closeLightbox()}
            >
            </button>
          </span>
        </div>,
        document.body
      )}
    </Page>
  );
};

export default Art;