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";
3
4const Synthwave = () => {
5 const canvas = useRef(null);
6
7 useEffect(() => {
8 const cvs = canvas.current;
9
10 cvs.width = cvs.offsetWidth;
11 cvs.height = cvs.offsetHeight;
12
13 const resizeCanvas = () => {
14 cvs.width = cvs.offsetWidth;
15 cvs.height = cvs.offsetHeight;
16 };
17 window.addEventListener("resize", resizeCanvas);
18
19 return () => {
20 window.removeEventListener("resize", resizeCanvas);
21 };
22 }, []);
23
24 useEffect(() => {
25 const cvs = canvas.current;
26 const ctx = cvs.getContext("2d");
27
28 const drawSynth = () => {
29 let stars = [];
30 let numStars = 0;
31 const maxNumStars = 250;
32 ctx.filter = "blur(1px)";
33 while (numStars < maxNumStars) {
34 const randomPoint = [
35 cvs.width * Math.random(),
36 cvs.height * Math.random(),
37 ];
38 stars.push(randomPoint);
39 numStars++;
40 }
41
42 let grd = ctx.createLinearGradient(0, 0, 0, cvs.height);
43 grd.addColorStop(0, "#110f33");
44 grd.addColorStop(1, "#040103");
45 ctx.fillStyle = grd;
46 ctx.fillRect(0, 0, cvs.width, cvs.height);
47
48 stars.forEach((star) => {
49 ctx.beginPath();
50 ctx.arc(...star, 2 * Math.random(), 0, 2 * Math.PI);
51 ctx.fillStyle = `rgba(255, 255, 255, ${Math.random()})`;
52 ctx.fill();
53 ctx.closePath();
54 });
55 ctx.filter = "none";
56
57 let currentLine = 0;
58 let maxLines = 15;
59 let start = cvs.height;
60 let offset = 30;
61 let quotient = 0.9;
62 ctx.strokeStyle = "#dd299c";
63 while (currentLine < maxLines) {
64 ctx.globalAlpha = 1 - currentLine / maxLines;
65 start = start - offset;
66 ctx.beginPath();
67 ctx.moveTo(0, start);
68 ctx.lineTo(cvs.width, start);
69 ctx.stroke();
70 offset = offset * quotient;
71 currentLine++;
72 }
73 ctx.globalAlpha = 1;
74
75 const end = start;
76 quotient = 0.85;
77 maxLines = 30;
78 currentLine = 0;
79 start = cvs.width / 2;
80 offset = 80;
81 let standardStart = start;
82 const standardOffset = offset;
83 while (currentLine < maxLines) {
84 ctx.beginPath();
85 ctx.moveTo(standardStart, cvs.height);
86 ctx.lineTo(start, end);
87 grd = ctx.createLinearGradient(0, cvs.height, 0, end);
88 grd.addColorStop(0, `rgba(221, 41, 156, 1)`);
89 grd.addColorStop(1, `rgba(221, 41, 156, 0)`);
90 ctx.strokeStyle = grd;
91 ctx.stroke();
92 offset = offset * quotient;
93 start = start - offset;
94 standardStart = standardStart - standardOffset;
95 currentLine++;
96 }
97 currentLine = 0;
98 start = cvs.width / 2;
99 standardStart = start;
100 offset = 80;
101 while (currentLine < maxLines) {
102 ctx.beginPath();
103 ctx.moveTo(standardStart, cvs.height);
104 ctx.lineTo(start, end);
105 ctx.stroke();
106 offset = offset * quotient;
107 start = start + offset;
108 standardStart = standardStart + standardOffset;
109 currentLine++;
110 }
111
112 ctx.beginPath();
113 ctx.arc(cvs.width / 2, cvs.height / 2, 100, 0, 2 * Math.PI);
114 grd = ctx.createLinearGradient(
115 0,
116 cvs.height / 2 - 50,
117 0,
118 cvs.height / 2 + 50
119 );
120 grd.addColorStop(0, "#ffd319");
121 grd.addColorStop(1, "#ff2975");
122 ctx.fillStyle = grd;
123 ctx.fill();
124 ctx.closePath();
125 };
126
127 drawSynth();
128
129 window.addEventListener("resize", drawSynth);
130
131 return () => {
132 window.removeEventListener("resize", drawSynth);
133 };
134 }, []);
135
136 return <canvas ref={canvas} className={styles.canvas} />;
137};
138
139export default Synthwave;