#app
View Compiled
*
  box-sizing border-box

body
  display grid
  place-items center
  min-height 100vh
  overflow hidden
  
canvas
  position fixed
  inset 0
  background hsl(0, 0%, 15%)
  z-index -1
  height 100vh
  width 100vw
View Compiled
import React from "https://cdn.skypack.dev/react";
import ReactDOM from "https://cdn.skypack.dev/react-dom";
import gsap from "https://cdn.skypack.dev/gsap";

const ROOT_NODE = document.querySelector("#app");

const KONAMI_CODE =
  "arrowup,arrowup,arrowdown,arrowdown,arrowleft,arrowright,arrowleft,arrowright,keyb,keya";

const Linescape = ({
  cellSize = 20,
  proximityRatio = 0.25,
}) => {
  const canvasRef = React.useRef(null);
  const contextRef = React.useRef(null);
  const linesRef = React.useRef(null);
  const codeRef = React.useRef([]);
  const partyRef = React.useRef(null);
  const offsetRef = React.useRef(null)
  const saturationMapperRef = React.useRef(null);
  const alphaMapperRef = React.useRef(null);
  const vminRef = React.useRef(null)

  const isPartying = () =>
    partyRef.current &&
    partyRef.current.progress() !== 0 &&
    partyRef.current.progress() !== 1;

  React.useEffect(() => {
    contextRef.current = canvasRef.current.getContext("2d");
    // In load, we work out how many lines to show and where. We can offset based on the viewport dimensions
    const LOAD = () => {
      vminRef.current = Math.min(window.innerHeight, window.innerWidth);
      const CELLS_X = window.innerWidth / cellSize
      const CELLS_Y = window.innerHeight / cellSize
      const SAFE_CELLS_X = Math.ceil(CELLS_X)
      const SAFE_CELLS_Y = Math.ceil(CELLS_Y)
      // Calculate offset by doing some subtraction
      offsetRef.current = {
        x: (SAFE_CELLS_X - CELLS_X) * 0.5,
        y: (SAFE_CELLS_Y - CELLS_Y) * 0.5,
      }
      canvasRef.current.width = window.innerWidth;
      canvasRef.current.height = window.innerHeight;
      
      saturationMapperRef.current = gsap.utils.mapRange(
        0,
        vminRef.current * proximityRatio,
        100,
        50
      );
      alphaMapperRef.current = gsap.utils.mapRange(
        0,
        vminRef.current * proximityRatio,
        1,
        0.2
      );
      
      linesRef.current = new Array(SAFE_CELLS_X * SAFE_CELLS_Y).fill().map((_, index) => {
        return {
          index,
          alpha: 0.2,
          width: gsap.utils.random(0.1, 3, 0.1),
          saturation: 50,
          angle: gsap.utils.random(0, 360),
          hue: gsap.utils.random(0, 359),
          x: index % SAFE_CELLS_X,
          y: Math.floor(index / SAFE_CELLS_X)
        }
      })
    };
    const RENDER = () => {
      contextRef.current.clearRect(
        0,
        0,
        canvasRef.current.width,
        canvasRef.current.height
      );
      linesRef.current.forEach((line) => {
        const X = (line.x * cellSize) - (offsetRef.current.x * cellSize)
        const Y = (line.y * cellSize) - (offsetRef.current.y * cellSize)
        // Line is going to be 80 percent of the height
        const MID = {
          x: X + (cellSize * 0.5),
          y: Y,
        }
        contextRef.current.lineWidth = line.width
        contextRef.current.strokeStyle = `hsla(${line.hue}, ${line.saturation}%, 75%, ${line.alpha})`
        contextRef.current.beginPath()
        contextRef.current.translate(MID.x, MID.y + cellSize * 0.5)
        contextRef.current.rotate(line.angle * (Math.PI / 180))
        contextRef.current.translate(-0, -cellSize * 0.5)
        contextRef.current.moveTo(0, cellSize * 0.1)
        contextRef.current.lineTo(0, cellSize * 0.9)
        contextRef.current.stroke()
        contextRef.current.closePath()
        // Reset the translations          
        contextRef.current.setTransform(1, 0, 0, 1, 0, 0)
      });
    };

    const UPDATE = ({ x, y }) => {
      if (!isPartying()) {
        linesRef.current.forEach((LINE) => {
          const DISTANCE = Math.sqrt(
            Math.pow(((LINE.x * cellSize) + (cellSize * 0.5)) - x, 2) + Math.pow(((LINE.y * cellSize) + (cellSize * 0.5)) - y, 2)
          );
          const saturation = saturationMapperRef.current(Math.min(DISTANCE, vminRef.current * proximityRatio))
          const alpha = alphaMapperRef.current(Math.min(DISTANCE, vminRef.current * proximityRatio))
          
          gsap.to(LINE, {
            saturation,
            alpha,
          });
        });
      }
    };

    const EXIT = () => {
      gsap.to(linesRef.current, {
        saturation: 50,
        alpha: 0.2
      });
    };

    LOAD();
    gsap.ticker.fps(24);
    gsap.ticker.add(RENDER);

    // Set up event handling
    window.addEventListener("resize", LOAD);
    document.addEventListener("pointermove", UPDATE);
    document.addEventListener("pointerleave", EXIT);
    return () => {
      window.removeEventListener("resize", LOAD);
      document.removeEventListener("pointermove", UPDATE);
      document.removeEventListener("pointerleave", EXIT);
      gsap.ticker.remove(RENDER);
    };
  }, []);

  React.useEffect(() => {
    const handleCode = (e) => {
      codeRef.current = [...codeRef.current, e.code].slice(
        codeRef.current.length > 9 ? codeRef.current.length - 9 : 0
      );
      if (
        codeRef.current.join(",").toLowerCase() === KONAMI_CODE &&
        !isPartying()
      ) {
        codeRef.current.length = 0;
      }
    };
    window.addEventListener("keyup", handleCode);
    return () => {
      window.removeEventListener("keyup", handleCode);
    };
  }, []);

  return <canvas ref={canvasRef} />;
};

const DEFAULT_CELL = 20;

const App = () => {
  return (
    <Linescape
      cellSize={DEFAULT_CELL}
    />
  );
};

ReactDOM.render(<App />, ROOT_NODE);
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.