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

7.9 KB · 228 lines · JavaScript Raw History
  1import React, { useState, useEffect, useRef } from "react";
  2import { createPortal } from "react-dom";
  3import styles from "@styles/pages/art.module.css";
  4import Image from "next/image";
  5
  6import Page from "../components/page";
  7import Constellations from "../components/constellations";
  8import RetroStars from "../components/retrostars";
  9import SlimeMold from "../components/slimemold";
 10
 11const ArtCard = ({ src, alt, title, number, priority, onClick }) => {
 12  const [loaded, setLoaded] = useState(false);
 13  return (
 14    <div className={styles.artItem} onClick={onClick}>
 15      <span className={styles.cardImage}>
 16        <Image
 17          src={src}
 18          alt={alt}
 19          width={640}
 20          height={360}
 21          className={`mouse-activate ${loaded ? styles.imgLoaded : ""}`}
 22          priority={priority}
 23          onLoad={() => setLoaded(true)}
 24        />
 25      </span>
 26      <h2 className={styles.artItemHeader}>
 27        {title} <span>{number}</span>
 28      </h2>
 29    </div>
 30  );
 31};
 32
 33const ACRYLIC_POURS = [
 34  { number: "006", title: "Molten Copper", priority: true },
 35  { number: "005", title: "Nebulas in Triangulum", priority: true },
 36  { number: "004", title: "Metal on Mars", priority: true },
 37  { number: "003", title: "Water on Jupiter", priority: true },
 38  { number: "002", title: "Cracks in Clay", priority: false },
 39  { number: "001", title: "Reef Drop-off", priority: false },
 40  { number: "000", title: "Blood in Waves", priority: false },
 41];
 42
 43const Art = () => {
 44  const [lightboxImage, setLightboxImage] = useState(null);
 45  const [lightboxLoaded, setLightboxLoaded] = useState(false);
 46  const [activeArt, setActiveArt] = useState("constellations");
 47  const [mounted, setMounted] = useState(false);
 48
 49  useEffect(() => {
 50    setMounted(true);
 51  }, []);
 52
 53  const openLightbox = (image) => {
 54    setLightboxImage(image);
 55    document.body.style.overflowY = "hidden";
 56  };
 57
 58  const closeLightbox = () => {
 59    setLightboxImage(null);
 60    setLightboxLoaded(false);
 61    history.replaceState(null, null, " ");
 62    document.body.style.overflowY = "scroll";
 63  };
 64
 65  const handleArtToggle = (artName) => {
 66    setActiveArt(activeArt === artName ? null : artName);
 67  };
 68
 69  return (
 70    <Page title="Art" description="Some of my art... what even is art...">
 71      <div className={styles.background} />
 72      <h1 className={styles.heading}>Acrylic Pours</h1>
 73      <p className={styles.paragraph}>
 74        A bit more traditional than my usual art. Each piece starts as a mix of
 75        acrylic paint, water, glue, and silicone oil poured onto canvas, then
 76        manipulated with a heat gun to bring out cells and organic patterns. No
 77        two pours come out the same and the process is as much about letting go
 78        of control as it is about technique.
 79      </p>
 80      <div className={styles.artGrid}>
 81        {ACRYLIC_POURS.map(({ number, title, priority }) => {
 82          const src = `/static/images/art/acrylic-pours/${number}.webp`;
 83          return (
 84            <ArtCard
 85              key={number}
 86              src={src}
 87              alt={title}
 88              title={title}
 89              number={number}
 90              priority={priority}
 91              onClick={() => openLightbox(src)}
 92            />
 93          );
 94        })}
 95      </div>
 96      <h1 className={styles.heading}>Emergent Generative Art</h1>
 97      <p className={styles.paragraph}>
 98        Code as a creative medium. These pieces are built entirely in JavaScript
 99        on HTML canvas, each one a small system of simple rules that produces
100        complex, unpredictable behavior. The art emerges from the interaction of
101        autonomous entities rather than being explicitly drawn.
102      </p>
103      <h2 className={styles.subheading}>
104        <span>000</span> Constellations
105      </h2>
106      <p className={styles.paragraph}>
107        Drifting points on a dark canvas that form connections with their nearest
108        neighbors. As stars wander closer together lines appear between them,
109        brighter the nearer they get, building and dissolving constellations
110        that never repeat.
111      </p>
112      <div className={styles.artContainer}>
113        <Constellations
114          options={{ numStars: 50, isActive: activeArt === "constellations" }}
115        />
116        <button
117          className={styles.playButton}
118          onClick={() => handleArtToggle("constellations")}
119          data-active={activeArt === "constellations"}
120        >
121          {activeArt === "constellations" ? "⏸" : "▶"}
122        </button>
123      </div>
124      <a
125        className={styles.artItemButton}
126        href="https://github.com/overshard/isaacbythewood.com-nextjs/blob/master/components/constellations.js"
127        rel="noopener noreferrer"
128        target="_blank"
129      >
130        See the Code
131      </a>
132      <h2 className={styles.subheading}>
133        <span>001</span> Retro Stars
134      </h2>
135      <p className={styles.paragraph}>
136        Layered star fields at different depths that drift in response to your
137        cursor, creating a parallax effect. Inspired by the pixel art aesthetic
138        of Celeste and the feeling of staring into a sky that moves with you.
139      </p>
140      <div className={styles.artContainer}>
141        <RetroStars
142          options={{ numStars: 50, isActive: activeArt === "retrostars" }}
143        />
144        <button
145          className={styles.playButton}
146          onClick={() => handleArtToggle("retrostars")}
147          data-active={activeArt === "retrostars"}
148        >
149          {activeArt === "retrostars" ? "⏸" : "▶"}
150        </button>
151      </div>
152      <a
153        className={styles.artItemButton}
154        href="https://github.com/overshard/isaacbythewood.com-nextjs/blob/master/components/retrostars.js"
155        rel="noopener noreferrer"
156        target="_blank"
157      >
158        See the Code
159      </a>
160      <h2 className={styles.subheading}>
161        <span>002</span> Slime Mold
162      </h2>
163      <p className={styles.paragraph}>
164        Thousands of agents wander a dark field, each leaving a faint chemical
165        trail and steering toward the strongest scent ahead. From three simple
166        rules  sense, turn, deposit  emerge living networks reminiscent of
167        Physarum slime molds, neurons, and the cosmic web. The pattern never
168        settles; trails decay as fast as they form.
169      </p>
170      <div className={styles.artContainer}>
171        <SlimeMold options={{ isActive: activeArt === "slimemold" }} />
172        <button
173          className={styles.playButton}
174          onClick={() => handleArtToggle("slimemold")}
175          data-active={activeArt === "slimemold"}
176        >
177          {activeArt === "slimemold" ? "⏸" : "▶"}
178        </button>
179      </div>
180      <a
181        className={styles.artItemButton}
182        href="https://github.com/overshard/isaacbythewood.com-nextjs/blob/master/components/slimemold.js"
183        rel="noopener noreferrer"
184        target="_blank"
185      >
186        See the Code
187      </a>
188      {mounted && lightboxImage !== null && createPortal(
189        <div className={styles.lightboxOverlay} onClick={() => closeLightbox()}>
190          <span
191            className={styles.lightboxImageWrapper}
192            onClick={(e) => e.stopPropagation()}
193          >
194            <div
195              className={
196                styles.lightboxLoading +
197                (lightboxLoaded ? " " + styles.hide : "")
198              }
199            >
200              Loading...
201            </div>
202            <Image
203              className={
204                styles.lightboxImage + (lightboxLoaded ? " " + styles.show : "")
205              }
206              src={lightboxImage}
207              alt="Lightbox"
208              fill
209              sizes="90vw"
210              style={{ objectFit: "contain" }}
211              onLoad={() => setLightboxLoaded(true)}
212            />
213            <button
214              className={styles.lightboxClose}
215              onClick={() => closeLightbox()}
216            >
217              
218            </button>
219          </span>
220        </div>,
221        document.body
222      )}
223    </Page>
224  );
225};
226
227export default Art;