#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
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.