All checks were successful
Build frontend / build (push) Successful in 59s
Added check DarkMode in home for switch light and dark
292 lines
7.8 KiB
JavaScript
292 lines
7.8 KiB
JavaScript
import React, { useRef, useEffect, useState } from "react";
|
|
import "./BackgroundCanvas.css";
|
|
|
|
const BackgroundCanvas = ({ theme = "light" }) => {
|
|
const canvasRef = useRef(null);
|
|
const animationRef = useRef(null);
|
|
const jointsRef = useRef([]);
|
|
const cameraRef = useRef(null);
|
|
const keysRef = useRef(new Array(127).fill(0));
|
|
const worldRef = useRef({ width: 0, height: 0 });
|
|
const themeRef = useRef(theme);
|
|
|
|
class Camera {
|
|
constructor(position, zoom) {
|
|
this.position = position;
|
|
this.speed = 3;
|
|
this.acceleration = { x: 0, y: 0, z: 0 };
|
|
this.zoom = zoom;
|
|
}
|
|
}
|
|
|
|
class Joint {
|
|
constructor(position, vector) {
|
|
this.position = position;
|
|
this.vector = vector;
|
|
this.speed = Math.random() * 1.5 + 0.5;
|
|
this.bone_length = 150;
|
|
const shapes = ["circle", "triangle", "square", "pentagon"];
|
|
this.shape = shapes[Math.floor(Math.random() * shapes.length)];
|
|
this.size = Math.random() * 3 + 2;
|
|
this.angle = 0;
|
|
this.angularSpeed = (Math.random() - 0.5) * 0.5;
|
|
this.pulse = Math.random() * Math.PI * 2;
|
|
}
|
|
}
|
|
|
|
const generateJoints = (numb, worldWidth, worldHeight) => {
|
|
const arr = new Array(numb).fill(0);
|
|
arr.forEach((v, i) => {
|
|
arr[i] = new Joint(
|
|
{
|
|
x: Math.random() * worldWidth,
|
|
y: Math.random() * worldHeight,
|
|
},
|
|
{
|
|
x: Math.random() * 2 - 1,
|
|
y: Math.random() * 2 - 1,
|
|
},
|
|
);
|
|
});
|
|
return arr;
|
|
};
|
|
|
|
const moveJoints = (joints, world) => {
|
|
joints.forEach((joint) => {
|
|
joint.position.x += joint.vector.x * joint.speed;
|
|
joint.position.y += joint.vector.y * joint.speed;
|
|
|
|
joint.angle += joint.angularSpeed;
|
|
joint.pulse += 0.03;
|
|
|
|
if (joint.position.x < 0) joint.position.x = world.width;
|
|
if (joint.position.x > world.width) joint.position.x = 0;
|
|
if (joint.position.y < 0) joint.position.y = world.height;
|
|
if (joint.position.y > world.height) joint.position.y = 0;
|
|
});
|
|
};
|
|
|
|
const drawJoints = (ctx, joints, camera, view, theme) => {
|
|
const len = joints.length;
|
|
|
|
let lineColor, pointColor, backgroundColor;
|
|
if (theme === "dark") {
|
|
backgroundColor = "#313131";
|
|
lineColor = "#F5EEE6";
|
|
pointColor = "#e06923";
|
|
} else {
|
|
backgroundColor = "#f1f1f1ff";
|
|
lineColor = "rgba(49, 49, 49, 0.3)";
|
|
pointColor = "#041c40";
|
|
}
|
|
|
|
ctx.fillStyle = backgroundColor;
|
|
ctx.fillRect(0, 0, view.width, view.height);
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
for (let j = i + 1; j < len; j += 3) {
|
|
const length = Math.hypot(
|
|
joints[j].position.x - joints[i].position.x,
|
|
joints[j].position.y - joints[i].position.y,
|
|
);
|
|
|
|
if (length <= joints[i].bone_length) {
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = lineColor;
|
|
ctx.lineWidth = camera.zoom * (30 / length);
|
|
ctx.moveTo(
|
|
view.width / 2 +
|
|
(joints[i].position.x - camera.position.x) * camera.zoom,
|
|
view.height / 2 +
|
|
(joints[i].position.y - camera.position.y) * camera.zoom,
|
|
);
|
|
ctx.lineTo(
|
|
view.width / 2 +
|
|
(joints[j].position.x - camera.position.x) * camera.zoom,
|
|
view.height / 2 +
|
|
(joints[j].position.y - camera.position.y) * camera.zoom,
|
|
);
|
|
ctx.stroke();
|
|
ctx.closePath();
|
|
}
|
|
}
|
|
|
|
const currentSize =
|
|
(joints[i].size + Math.sin(joints[i].pulse) * 1.5) * camera.zoom;
|
|
|
|
ctx.save();
|
|
ctx.translate(
|
|
view.width / 2 +
|
|
(joints[i].position.x - camera.position.x) * camera.zoom,
|
|
view.height / 2 +
|
|
(joints[i].position.y - camera.position.y) * camera.zoom,
|
|
);
|
|
ctx.rotate(joints[i].angle);
|
|
|
|
ctx.fillStyle = pointColor;
|
|
ctx.beginPath();
|
|
|
|
if (joints[i].shape === "circle") {
|
|
ctx.arc(0, 0, currentSize, 0, Math.PI * 2);
|
|
} else if (joints[i].shape === "triangle") {
|
|
ctx.moveTo(0, -currentSize);
|
|
ctx.lineTo(currentSize, currentSize);
|
|
ctx.lineTo(-currentSize, currentSize);
|
|
} else if (joints[i].shape === "square") {
|
|
ctx.rect(-currentSize / 2, -currentSize / 2, currentSize, currentSize);
|
|
} else if (joints[i].shape === "pentagon") {
|
|
for (let k = 0; k < 5; k++) {
|
|
ctx.lineTo(
|
|
currentSize * Math.cos((k * 2 * Math.PI) / 5 - Math.PI / 2),
|
|
currentSize * Math.sin((k * 2 * Math.PI) / 5 - Math.PI / 2),
|
|
);
|
|
}
|
|
}
|
|
|
|
ctx.fill();
|
|
ctx.closePath();
|
|
ctx.restore();
|
|
}
|
|
};
|
|
|
|
const moveCamera = (camera, keys) => {
|
|
if (keys[37]) camera.acceleration.x -= camera.speed;
|
|
if (keys[38]) camera.acceleration.y -= camera.speed;
|
|
if (keys[39]) camera.acceleration.x += camera.speed;
|
|
if (keys[40]) camera.acceleration.y += camera.speed;
|
|
if (keys[188]) camera.acceleration.z += 0.003;
|
|
if (keys[190]) camera.acceleration.z -= 0.003;
|
|
|
|
camera.position.x += camera.acceleration.x;
|
|
camera.position.y += camera.acceleration.y;
|
|
camera.zoom += camera.acceleration.z;
|
|
|
|
camera.acceleration.x *= 0.96;
|
|
camera.acceleration.y *= 0.96;
|
|
camera.acceleration.z *= 0.9;
|
|
};
|
|
|
|
const initCanvas = () => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
const view = {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight,
|
|
};
|
|
|
|
canvas.width = view.width;
|
|
canvas.height = view.height;
|
|
|
|
worldRef.current = {
|
|
width: view.width * 2,
|
|
height: view.height * 2,
|
|
};
|
|
|
|
cameraRef.current = new Camera(
|
|
{
|
|
x: worldRef.current.width / 2,
|
|
y: worldRef.current.height / 2,
|
|
},
|
|
1,
|
|
);
|
|
|
|
const isMobile = window.innerWidth < 768;
|
|
const numberOfJoints = isMobile ? 120 : 500;
|
|
jointsRef.current = generateJoints(
|
|
numberOfJoints,
|
|
worldRef.current.width,
|
|
worldRef.current.height,
|
|
);
|
|
|
|
if (isMobile) {
|
|
jointsRef.current.forEach((joint) => {
|
|
joint.bone_length = 80;
|
|
joint.speed = 0.3;
|
|
});
|
|
}
|
|
};
|
|
|
|
const animate = () => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
const view = {
|
|
width: canvas.width,
|
|
height: canvas.height,
|
|
};
|
|
|
|
moveCamera(cameraRef.current, keysRef.current);
|
|
moveJoints(jointsRef.current, worldRef.current);
|
|
|
|
drawJoints(
|
|
ctx,
|
|
jointsRef.current,
|
|
cameraRef.current,
|
|
view,
|
|
themeRef.current,
|
|
);
|
|
|
|
animationRef.current = requestAnimationFrame(animate);
|
|
};
|
|
|
|
useEffect(() => {
|
|
themeRef.current = theme;
|
|
}, [theme]);
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e) => {
|
|
keysRef.current[e.keyCode] = 1;
|
|
};
|
|
|
|
const handleKeyUp = (e) => {
|
|
keysRef.current[e.keyCode] = 0;
|
|
};
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
window.addEventListener("keyup", handleKeyUp);
|
|
|
|
return () => {
|
|
window.removeEventListener("keydown", handleKeyDown);
|
|
window.removeEventListener("keyup", handleKeyUp);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
|
|
worldRef.current = {
|
|
width: canvas.width * 2,
|
|
height: canvas.height * 2,
|
|
};
|
|
};
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
initCanvas();
|
|
animate();
|
|
|
|
return () => {
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<canvas ref={canvasRef} className="background-canvas" aria-hidden="true" />
|
|
);
|
|
};
|
|
|
|
export default BackgroundCanvas;
|