Animation-heavy personal portfolio on Next.js: a custom cursor, a page-transition loader, and canvas backgrounds.
canvas-animationscss-modulesdark-themehandcodednextjspersonal-websiteportfolioreact
1import React, { useEffect, useRef } from "react";
2import styles from "@styles/components/canvas.module.css";
3import PropTypes from "prop-types";
4
5const Constellations = ({ options }) => {
6 const isActive = options.isActive !== undefined ? options.isActive : true;
7 const canvas = useRef(null);
8
9 useEffect(() => {
10 const cvs = canvas.current;
11
12 cvs.width = cvs.offsetWidth;
13 cvs.height = cvs.offsetHeight;
14
15 const resizeCanvas = () => {
16 cvs.width = cvs.offsetWidth;
17 cvs.height = cvs.offsetHeight;
18 };
19 window.addEventListener("resize", resizeCanvas);
20
21 return () => {
22 window.removeEventListener("resize", resizeCanvas);
23 };
24 }, []);
25
26 useEffect(() => {
27 const cvs = canvas.current;
28 const ctx = cvs.getContext("2d");
29
30 let stars = [];
31 let numStars = 0;
32 const maxNumStars = options.numStars;
33 while (numStars < maxNumStars) {
34 const randomPoint = [
35 cvs.width * Math.random(),
36 cvs.height * Math.random(),
37 ];
38 stars.push({
39 loc: randomPoint,
40 dir: [Math.random() > 0.5 ? "+" : "-", Math.random() > 0.5 ? "+" : "-"],
41 });
42 numStars++;
43 }
44
45 let starsAnimationFrame = null;
46 const starDistance = 150;
47 const drawStars = () => {
48 ctx.clearRect(0, 0, cvs.width, cvs.height);
49
50 stars.map((star) => {
51 ctx.beginPath();
52 ctx.arc(...star.loc, 2, 0, 2 * Math.PI);
53 ctx.fillStyle = "rgb(255, 255, 255)";
54 ctx.fill();
55 ctx.closePath();
56 const closeStars = stars.filter((closeStar) => {
57 return (
58 Math.hypot(
59 star.loc[0] - closeStar.loc[0],
60 star.loc[1] - closeStar.loc[1]
61 ) < starDistance
62 );
63 });
64 closeStars.map((closeStar) => {
65 ctx.beginPath();
66 ctx.moveTo(...star.loc);
67 ctx.lineTo(...closeStar.loc);
68 ctx.strokeStyle = `rgba(255, 255, 255, ${
69 (starDistance -
70 Math.hypot(
71 star.loc[0] - closeStar.loc[0],
72 star.loc[1] - closeStar.loc[1]
73 )) /
74 starDistance
75 })`;
76 ctx.stroke();
77 ctx.closePath();
78 });
79 });
80
81 stars = stars.map((star) => {
82 if (star.loc[0] < 0) star.dir[0] = "+";
83 else if (star.loc[0] > cvs.width) star.dir[0] = "-";
84 if (star.loc[1] < 0) star.dir[1] = "+";
85 else if (star.loc[1] > cvs.height) star.dir[1] = "-";
86
87 star.loc[0] += parseFloat(`${star.dir[0]}0.5`);
88 star.loc[1] += parseFloat(`${star.dir[1]}0.5`);
89
90 return star;
91 });
92
93 if (isActive) {
94 starsAnimationFrame = window.requestAnimationFrame(drawStars);
95 }
96 };
97
98 if (isActive) {
99 starsAnimationFrame = window.requestAnimationFrame(drawStars);
100 }
101
102 return () => {
103 window.cancelAnimationFrame(starsAnimationFrame);
104 };
105 }, [isActive]);
106
107 return <canvas ref={canvas} className={styles.canvas} />;
108};
109
110Constellations.propTypes = {
111 options: PropTypes.object,
112};
113
114export default Constellations;