From eec361a4c88458cd76ebb5648587a82ae77775eb Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Sat, 21 Sep 2024 17:46:59 +0200 Subject: [PATCH 1/4] Removes the stars and connects the nodes --- .../components/layout/site/site-layout.tsx | 576 +++++++++++++++++- website/src/pages/index.tsx | 24 +- 2 files changed, 571 insertions(+), 29 deletions(-) diff --git a/website/src/components/layout/site/site-layout.tsx b/website/src/components/layout/site/site-layout.tsx index fc025fda56a..6eca0d84b78 100644 --- a/website/src/components/layout/site/site-layout.tsx +++ b/website/src/components/layout/site/site-layout.tsx @@ -53,6 +53,15 @@ export const SiteLayout: FC = ({ children, disableStars }) => { ); }; +/** + * There are three different implementations for the Stars component. The first + * one does bareely change the code from the original implementation but uses + * more CPU. Then there are two performance optimized implementations that use + * a grid or a quadtree to connect nearby stars which is much more efficent. + * + * This implementation looks the best, but i am pretty sure that we can get the + * same result with the other two implementations too. + */ function Stars(): ReactElement { const canvasRef = useRef(null); @@ -65,6 +74,7 @@ function Stars(): ReactElement { const ctx = canvas.getContext("2d"); const numStars = 800; const speed = 0.25; + const connectionDistance = 100; // Maximum distance for connecting stars let stars: Star[] = []; function setCanvasSize() { @@ -74,10 +84,10 @@ function Stars(): ReactElement { class Star { constructor( - private x: number, - private y: number, - private z: number, - private size: number + public x: number, + public y: number, + public z: number, + public size: number ) {} update() { @@ -105,6 +115,13 @@ function Stars(): ReactElement { ctx.fill(); } } + + getScreenPosition() { + return { + x: ((this.x / this.z) * canvas.width) / 2 + canvas.width / 2, + y: ((this.y / this.z) * canvas.height) / 2 + canvas.height / 2, + }; + } } function initStars() { @@ -129,13 +146,63 @@ function Stars(): ReactElement { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "#f4ebcb"; stars.forEach((star) => star.draw()); + connectNearbyStars(); + } + } + + /** + * This is a very simple implementation to connect nearby stars. It's a bit + * CPU intensive. There is a more efficient way listed in the commented out + * code below, but it's a bit more complex but does barely use any CPU even + * without a FPS limit. + */ + function connectNearbyStars() { + if (!ctx) return; + + ctx.strokeStyle = "rgba(244, 235, 203, 0.2)"; // Light, semi-transparent color for lines + ctx.lineWidth = 0.5; + + for (let i = 0; i < stars.length; i++) { + const star1 = stars[i]; + const pos1 = star1.getScreenPosition(); + + for (let j = i + 1; j < stars.length; j++) { + const star2 = stars[j]; + const pos2 = star2.getScreenPosition(); + + const distance = Math.sqrt( + Math.pow(pos1.x - pos2.x, 2) + Math.pow(pos1.y - pos2.y, 2) + ); + + if (distance < connectionDistance) { + ctx.beginPath(); + ctx.moveTo(pos1.x, pos1.y); + ctx.lineTo(pos2.x, pos2.y); + ctx.stroke(); + } + } } } - function animate() { - updateStars(); - drawStars(); + // in this implementation it's better to limit the FPS as the connection + // calculation is a CPU intensive. + let lastTime = 0; + const fps = 30; + const fpsInterval = 1000 / fps; + + function animate(currentTime: number) { requestAnimationFrame(animate); + + // Calculate elapsed time since last frame + const elapsed = currentTime - lastTime; + + // Proceed only if enough time has passed to maintain the desired FPS + if (elapsed > fpsInterval) { + lastTime = currentTime - (elapsed % fpsInterval); + + updateStars(); + drawStars(); + } } window.addEventListener("resize", () => { @@ -145,7 +212,7 @@ function Stars(): ReactElement { setCanvasSize(); initStars(); - animate(); + requestAnimationFrame(animate); }, []); return ( @@ -164,3 +231,496 @@ function Stars(): ReactElement { /> ); } + +/** + * This is a more efficient way to connect nearby stars. It uses a grid to + * divide the canvas into cells and only checks stars in neighboring cells. + * This is more efficient than the brute force method above, but it's a bit + * more complex. + * + * This could be probably optimized further by using a quadtree, but that's + * out of the scope at the moment + */ +// function Stars(): ReactElement { +// const canvasRef = useRef(null); + +// useLayoutEffect(() => { +// if (!canvasRef.current) { +// return; +// } + +// const canvas = canvasRef.current; +// const ctx = canvas.getContext("2d"); +// const numStars = 800; +// const speed = 0.05; +// const connectionDistance = 100; +// const cellSize = connectionDistance; +// let stars: Star[] = []; +// let grid: Star[][][] = []; + +// function setCanvasSize() { +// canvas.width = window.innerWidth; +// canvas.height = window.innerHeight; +// } + +// class Star { +// gridX: number = 0; +// gridY: number = 0; + +// constructor( +// public x: number, +// public y: number, +// public z: number, +// public size: number +// ) {} + +// update() { +// this.z -= speed; +// if (this.z <= 0) { +// this.reset(); +// } +// } + +// reset() { +// this.z = canvas.width; +// this.x = Math.random() * (canvas.width * 2) - canvas.width; +// this.y = Math.random() * (canvas.height * 2) - canvas.height; +// this.size = Math.random() * 2 + 1; +// } + +// draw() { +// const x = ((this.x / this.z) * canvas.width) / 2 + canvas.width / 2; +// const y = ((this.y / this.z) * canvas.height) / 2 + canvas.height / 2; +// const radius = (1 - this.z / canvas.width) * this.size; + +// if (ctx) { +// ctx.beginPath(); +// ctx.arc(x, y, radius, 0, Math.PI * 2); +// ctx.fill(); +// } + +// this.gridX = Math.floor(x / cellSize); +// this.gridY = Math.floor(y / cellSize); +// } + +// getScreenPosition() { +// return { +// x: ((this.x / this.z) * canvas.width) / 2 + canvas.width / 2, +// y: ((this.y / this.z) * canvas.height) / 2 + canvas.height / 2, +// }; +// } +// } + +// function initStars() { +// stars = Array.from({ length: numStars }, () => { +// const star = new Star( +// Math.random() * (canvas.width * 2) - canvas.width, +// Math.random() * (canvas.height * 2) - canvas.height, +// Math.random() * canvas.width, +// Math.random() * 2 + 1 +// ); +// return star; +// }); +// } + +// function updateStars() { +// stars.forEach((star) => star.update()); +// } + +// function drawStars() { +// if (ctx) { +// ctx.clearRect(0, 0, canvas.width, canvas.height); +// ctx.fillStyle = "#f4ebcb"; +// stars.forEach((star) => star.draw()); +// connectNearbyStars(); +// } +// } + +// function buildGrid() { +// /** +// * Spatial partitioning using a grid to optimize performance: +// * - Reduces the number of distance calculations by only checking stars within nearby cells. +// * - Converts O(n²) complexity to approximately O(n) for connecting stars. +// * - Cell size is set to connectionDistance to ensure all possible connections are considered. +// */ +// const cols = Math.ceil(canvas.width / cellSize); +// const rows = Math.ceil(canvas.height / cellSize); +// grid = Array(cols) +// .fill(null) +// .map(() => +// Array(rows) +// .fill(null) +// .map(() => []) +// ); + +// stars.forEach((star) => { +// const { x, y } = star.getScreenPosition(); +// const gridX = Math.floor(x / cellSize); +// const gridY = Math.floor(y / cellSize); +// if (grid[gridX] && grid[gridX][gridY]) { +// grid[gridX][gridY].push(star); +// } +// }); +// } + +// function connectNearbyStars() { +// if (!ctx) return; + +// buildGrid(); + +// ctx.strokeStyle = "rgba(244, 235, 203, 0.2)"; +// ctx.lineWidth = 0.5; + +// const cols = grid.length; +// const rows = grid[0].length; + +// for (let i = 0; i < cols; i++) { +// for (let j = 0; j < rows; j++) { +// const cellStars = grid[i][j]; +// for (let k = 0; k < cellStars.length; k++) { +// const star1 = cellStars[k]; +// const pos1 = star1.getScreenPosition(); + +// // Check neighboring cells +// for (let dx = -1; dx <= 1; dx++) { +// for (let dy = -1; dy <= 1; dy++) { +// const ni = i + dx; +// const nj = j + dy; +// if (ni >= 0 && ni < cols && nj >= 0 && nj < rows) { +// const neighborStars = grid[ni][nj]; +// for (let l = 0; l < neighborStars.length; l++) { +// const star2 = neighborStars[l]; +// if (star1 === star2) continue; + +// const pos2 = star2.getScreenPosition(); +// const distance = Math.hypot( +// pos1.x - pos2.x, +// pos1.y - pos2.y +// ); + +// if (distance < connectionDistance) { +// ctx.beginPath(); +// ctx.moveTo(pos1.x, pos1.y); +// ctx.lineTo(pos2.x, pos2.y); +// ctx.stroke(); +// } +// } +// } +// } +// } +// } +// } +// } +// } + +// function animate() { +// updateStars(); +// drawStars(); +// requestAnimationFrame(animate); +// } + +// window.addEventListener("resize", () => { +// setCanvasSize(); +// initStars(); +// }); + +// setCanvasSize(); +// initStars(); +// animate(); +// }, []); + +// return ( +// +// ); +// } + +/** + * This implementation uses a Quadtree to optimize the connection of nearby + * stars. It's similar in complexity to the grid implementation but is more + * efficient and uses less CPU. + */ +// function Stars(): ReactElement { +// const canvasRef = useRef(null); + +// useLayoutEffect(() => { +// if (!canvasRef.current) { +// return; +// } + +// const canvas = canvasRef.current; +// const ctx = canvas.getContext("2d"); +// const numStars = 800; +// const speed = 0.25; +// const connectionDistance = 100; // Maximum distance for connecting stars +// let stars: Star[] = []; + +// function setCanvasSize() { +// canvas.width = window.innerWidth; +// canvas.height = window.innerHeight; +// } + +// class Star { +// screenX: number = 0; +// screenY: number = 0; + +// constructor( +// public x: number, +// public y: number, +// public z: number, +// public size: number +// ) {} + +// update() { +// this.z -= speed; +// if (this.z <= 0) { +// this.reset(); +// } +// } + +// reset() { +// this.z = canvas.width; +// this.x = Math.random() * (canvas.width * 2) - canvas.width; +// this.y = Math.random() * (canvas.height * 2) - canvas.height; +// this.size = Math.random() * 2 + 1; +// } + +// draw() { +// const x = ((this.x / this.z) * canvas.width) / 2 + canvas.width / 2; +// const y = ((this.y / this.z) * canvas.height) / 2 + canvas.height / 2; +// const radius = (1 - this.z / canvas.width) * this.size; + +// this.screenX = x; +// this.screenY = y; + +// if (ctx) { +// ctx.beginPath(); +// ctx.arc(x, y, radius, 0, Math.PI * 2); +// ctx.fill(); +// } +// } +// } + +// function initStars() { +// stars = Array.from( +// { length: numStars }, +// () => +// new Star( +// Math.random() * (canvas.width * 2) - canvas.width, +// Math.random() * (canvas.height * 2) - canvas.height, +// Math.random() * canvas.width, +// Math.random() * 2 + 1 +// ) +// ); +// } + +// function updateStars() { +// stars.forEach((star) => star.update()); +// } + +// function drawStars() { +// if (ctx) { +// ctx.clearRect(0, 0, canvas.width, canvas.height); +// ctx.fillStyle = "#f4ebcb"; +// stars.forEach((star) => star.draw()); +// connectNearbyStars(); +// } +// } + +// function connectNearbyStars() { +// if (!ctx) return; + +// // Build the Quadtree +// const boundary = new Rectangle( +// canvas.width / 2, +// canvas.height / 2, +// canvas.width / 2, +// canvas.height / 2 +// ); +// const qt = new Quadtree(boundary, 4); + +// // Insert stars into the Quadtree +// stars.forEach((star) => { +// const point = new Point(star.screenX, star.screenY, star); +// qt.insert(point); +// }); + +// ctx.strokeStyle = "rgba(244, 235, 203, 0.2)"; +// ctx.lineWidth = 0.5; + +// // For each star, find nearby stars using the Quadtree +// stars.forEach((star) => { +// const range = new Rectangle( +// star.screenX, +// star.screenY, +// connectionDistance, +// connectionDistance +// ); +// const points = qt.query(range); + +// points.forEach((point) => { +// if (point.userData !== star) { +// ctx.beginPath(); +// ctx.moveTo(star.screenX, star.screenY); +// ctx.lineTo(point.x, point.y); +// ctx.stroke(); +// } +// }); +// }); +// } + +// // Quadtree implementation +// class Point { +// constructor( +// public x: number, +// public y: number, +// public userData: any = null +// ) {} +// } + +// class Rectangle { +// constructor( +// public x: number, +// public y: number, +// public w: number, +// public h: number +// ) {} + +// contains(point: Point) { +// return ( +// point.x >= this.x - this.w && +// point.x <= this.x + this.w && +// point.y >= this.y - this.h && +// point.y <= this.y + this.h +// ); +// } + +// intersects(range: Rectangle) { +// return !( +// range.x - range.w > this.x + this.w || +// range.x + range.w < this.x - this.w || +// range.y - range.h > this.y + this.h || +// range.y + range.h < this.y - this.h +// ); +// } +// } + +// class Quadtree { +// points: Point[] = []; +// divided: boolean = false; +// northeast: Quadtree | null = null; +// northwest: Quadtree | null = null; +// southeast: Quadtree | null = null; +// southwest: Quadtree | null = null; + +// constructor(public boundary: Rectangle, public capacity: number) {} + +// subdivide() { +// const { x, y, w, h } = this.boundary; +// const ne = new Rectangle(x + w / 2, y - h / 2, w / 2, h / 2); +// this.northeast = new Quadtree(ne, this.capacity); +// const nw = new Rectangle(x - w / 2, y - h / 2, w / 2, h / 2); +// this.northwest = new Quadtree(nw, this.capacity); +// const se = new Rectangle(x + w / 2, y + h / 2, w / 2, h / 2); +// this.southeast = new Quadtree(se, this.capacity); +// const sw = new Rectangle(x - w / 2, y + h / 2, w / 2, h / 2); +// this.southwest = new Quadtree(sw, this.capacity); +// this.divided = true; +// } + +// insert(point: Point): boolean { +// if (!this.boundary.contains(point)) { +// return false; +// } +// if (this.points.length < this.capacity) { +// this.points.push(point); +// return true; +// } else { +// if (!this.divided) { +// this.subdivide(); +// } +// if (this.northeast!.insert(point)) return true; +// if (this.northwest!.insert(point)) return true; +// if (this.southeast!.insert(point)) return true; +// if (this.southwest!.insert(point)) return true; +// } +// return false; +// } + +// query(range: Rectangle, found: Point[] = []): Point[] { +// if (!this.boundary.intersects(range)) { +// return found; +// } else { +// for (const p of this.points) { +// if (range.contains(p)) { +// found.push(p); +// } +// } +// if (this.divided) { +// this.northwest!.query(range, found); +// this.northeast!.query(range, found); +// this.southwest!.query(range, found); +// this.southeast!.query(range, found); +// } +// } +// return found; +// } +// } + +// // Limit FPS to improve performance +// let lastTime = 0; +// const fps = 30; +// const fpsInterval = 1000 / fps; + +// function animate(currentTime: number) { +// requestAnimationFrame(animate); + +// // Calculate elapsed time since last frame +// const elapsed = currentTime - lastTime; + +// // Proceed only if enough time has passed to maintain the desired FPS +// if (elapsed > fpsInterval) { +// lastTime = currentTime - (elapsed % fpsInterval); + +// updateStars(); +// drawStars(); +// } +// } + +// window.addEventListener("resize", () => { +// setCanvasSize(); +// initStars(); +// }); + +// setCanvasSize(); +// initStars(); +// requestAnimationFrame(animate); +// }, []); + +// return ( +// +// ); +// } diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 2bc62b4322d..5f224abb008 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -43,9 +43,7 @@ const IndexPage: FC = () => { return ( - - - + {/* */} Unleash the Power of Unified Services @@ -55,8 +53,8 @@ const IndexPage: FC = () => { manage and interact with your data. Get Started + - - new Star( - Math.random() * (canvas.width * 2) - canvas.width, - Math.random() * (canvas.height * 2) - canvas.height, - Math.random() * canvas.width, - Math.random() * 2 + 1 - ) - ); - } +// function initStars() { +// stars = Array.from( +// { length: numStars }, +// () => +// new Star( +// Math.random() * (canvas.width * 2) - canvas.width, +// Math.random() * (canvas.height * 2) - canvas.height, +// Math.random() * canvas.width, +// Math.random() * 2 + 1 +// ) +// ); +// } - function updateStars() { - stars.forEach((star) => star.update()); - } +// function updateStars() { +// stars.forEach((star) => star.update()); +// } - function drawStars() { - if (ctx) { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "#f4ebcb"; - stars.forEach((star) => star.draw()); - connectNearbyStars(); - } - } +// function drawStars() { +// if (ctx) { +// ctx.clearRect(0, 0, canvas.width, canvas.height); +// ctx.fillStyle = "#f4ebcb"; +// stars.forEach((star) => star.draw()); +// connectNearbyStars(); +// } +// } - /** - * This is a very simple implementation to connect nearby stars. It's a bit - * CPU intensive. There is a more efficient way listed in the commented out - * code below, but it's a bit more complex but does barely use any CPU even - * without a FPS limit. - */ - function connectNearbyStars() { - if (!ctx) return; +// /** +// * This is a very simple implementation to connect nearby stars. It's a bit +// * CPU intensive. There is a more efficient way listed in the commented out +// * code below, but it's a bit more complex but does barely use any CPU even +// * without a FPS limit. +// */ +// function connectNearbyStars() { +// if (!ctx) return; - ctx.strokeStyle = "rgba(244, 235, 203, 0.2)"; // Light, semi-transparent color for lines - ctx.lineWidth = 0.5; +// ctx.strokeStyle = "rgba(244, 235, 203, 0.2)"; // Light, semi-transparent color for lines +// ctx.lineWidth = 0.5; - for (let i = 0; i < stars.length; i++) { - const star1 = stars[i]; - const pos1 = star1.getScreenPosition(); +// for (let i = 0; i < stars.length; i++) { +// const star1 = stars[i]; +// const pos1 = star1.getScreenPosition(); - for (let j = i + 1; j < stars.length; j++) { - const star2 = stars[j]; - const pos2 = star2.getScreenPosition(); +// for (let j = i + 1; j < stars.length; j++) { +// const star2 = stars[j]; +// const pos2 = star2.getScreenPosition(); - const distance = Math.sqrt( - Math.pow(pos1.x - pos2.x, 2) + Math.pow(pos1.y - pos2.y, 2) - ); +// const distance = Math.sqrt( +// Math.pow(pos1.x - pos2.x, 2) + Math.pow(pos1.y - pos2.y, 2) +// ); - if (distance < connectionDistance) { - ctx.beginPath(); - ctx.moveTo(pos1.x, pos1.y); - ctx.lineTo(pos2.x, pos2.y); - ctx.stroke(); - } - } - } - } +// if (distance < connectionDistance) { +// ctx.beginPath(); +// ctx.moveTo(pos1.x, pos1.y); +// ctx.lineTo(pos2.x, pos2.y); +// ctx.stroke(); +// } +// } +// } +// } - // in this implementation it's better to limit the FPS as the connection - // calculation is a CPU intensive. - let lastTime = 0; - const fps = 30; - const fpsInterval = 1000 / fps; +// // in this implementation it's better to limit the FPS as the connection +// // calculation is a CPU intensive. +// let lastTime = 0; +// const fps = 30; +// const fpsInterval = 1000 / fps; - function animate(currentTime: number) { - requestAnimationFrame(animate); +// function animate(currentTime: number) { +// requestAnimationFrame(animate); - // Calculate elapsed time since last frame - const elapsed = currentTime - lastTime; +// // Calculate elapsed time since last frame +// const elapsed = currentTime - lastTime; - // Proceed only if enough time has passed to maintain the desired FPS - if (elapsed > fpsInterval) { - lastTime = currentTime - (elapsed % fpsInterval); +// // Proceed only if enough time has passed to maintain the desired FPS +// if (elapsed > fpsInterval) { +// lastTime = currentTime - (elapsed % fpsInterval); - updateStars(); - drawStars(); - } - } +// updateStars(); +// drawStars(); +// } +// } - window.addEventListener("resize", () => { - setCanvasSize(); - initStars(); - }); +// window.addEventListener("resize", () => { +// setCanvasSize(); +// initStars(); +// }); - setCanvasSize(); - initStars(); - requestAnimationFrame(animate); - }, []); +// setCanvasSize(); +// initStars(); +// requestAnimationFrame(animate); +// }, []); - return ( - - ); -} +// return ( +// +// ); +// } /** * This is a more efficient way to connect nearby stars. It uses a grid to @@ -724,3 +724,307 @@ function Stars(): ReactElement { // /> // ); // } + +/** + * This implementation fades out the center of the screen to make the text + * more visible + */ +function Stars(): ReactElement { + const canvasRef = useRef(null); + + useLayoutEffect(() => { + if (!canvasRef.current) { + return; + } + + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + const numStars = 800; + const speed = 0.25; + const connectionDistance = 100; // Maximum distance for connecting stars + let stars: Star[] = []; + + function setCanvasSize() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + class Star { + screenX: number = 0; + screenY: number = 0; + + constructor( + public x: number, + public y: number, + public z: number, + public size: number + ) {} + + update() { + this.z -= speed; + if (this.z <= 0) { + this.reset(); + } + } + + reset() { + this.z = canvas.width; + this.x = Math.random() * (canvas.width * 2) - canvas.width; + this.y = Math.random() * (canvas.height * 2) - canvas.height; + this.size = Math.random() * 2 + 1; + } + + draw() { + const x = ((this.x / this.z) * canvas.width) / 2 + canvas.width / 2; + const y = ((this.y / this.z) * canvas.height) / 2 + canvas.height / 2; + const radius = (1 - this.z / canvas.width) * this.size; + + this.screenX = x; + this.screenY = y; + + if (ctx) { + // Calculate distance from center + const dx = x - canvas.width / 2; + const dy = y - canvas.height / 2; + const distance = Math.sqrt(dx * dx + dy * dy); + const maxDistance = Math.sqrt( + (canvas.width / 2) ** 2 + (canvas.height / 2) ** 2 + ); + let opacity = distance / maxDistance; + opacity = Math.pow(opacity, 1.5); // Adjust exponent as needed + opacity = Math.max(0, Math.min(opacity, 1)); + + ctx.fillStyle = `rgba(244, 235, 203, ${opacity})`; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + } + } + } + + function initStars() { + stars = Array.from( + { length: numStars }, + () => + new Star( + Math.random() * (canvas.width * 2) - canvas.width, + Math.random() * (canvas.height * 2) - canvas.height, + Math.random() * canvas.width, + Math.random() * 2 + 1 + ) + ); + } + + function updateStars() { + stars.forEach((star) => star.update()); + } + + function drawStars() { + if (ctx) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + stars.forEach((star) => star.draw()); + connectNearbyStars(); + } + } + + function connectNearbyStars() { + if (!ctx) return; + + // Build the Quadtree + const boundary = new Rectangle( + canvas.width / 2, + canvas.height / 2, + canvas.width / 2, + canvas.height / 2 + ); + const qt = new Quadtree(boundary, 4); + + // Insert stars into the Quadtree + stars.forEach((star) => { + const point = new Point(star.screenX, star.screenY, star); + qt.insert(point); + }); + + ctx.lineWidth = 0.5; + + // For each star, find nearby stars using the Quadtree + stars.forEach((star) => { + const range = new Rectangle( + star.screenX, + star.screenY, + connectionDistance, + connectionDistance + ); + const points = qt.query(range); + + points.forEach((point) => { + if (point.userData !== star) { + // Calculate opacity based on midpoint distance from center + const midX = (star.screenX + point.x) / 2; + const midY = (star.screenY + point.y) / 2; + const dx = midX - canvas.width / 2; + const dy = midY - canvas.height / 2; + const distance = Math.sqrt(dx * dx + dy * dy); + const maxDistance = Math.sqrt( + (canvas.width / 2) ** 2 + (canvas.height / 2) ** 2 + ); + let opacity = distance / maxDistance; + opacity = Math.pow(opacity, 1.5); // Adjust exponent as needed + opacity = opacity * 0.2; // Scale to original opacity of 0.2 + opacity = Math.max(0, Math.min(opacity, 1)); + + ctx.strokeStyle = `rgba(244, 235, 203, ${opacity})`; + + ctx.beginPath(); + ctx.moveTo(star.screenX, star.screenY); + ctx.lineTo(point.x, point.y); + ctx.stroke(); + } + }); + }); + } + + // Quadtree implementation + class Point { + constructor( + public x: number, + public y: number, + public userData: any = null + ) {} + } + + class Rectangle { + constructor( + public x: number, + public y: number, + public w: number, + public h: number + ) {} + + contains(point: Point) { + return ( + point.x >= this.x - this.w && + point.x <= this.x + this.w && + point.y >= this.y - this.h && + point.y <= this.y + this.h + ); + } + + intersects(range: Rectangle) { + return !( + range.x - range.w > this.x + this.w || + range.x + range.w < this.x - this.w || + range.y - range.h > this.y + this.h || + range.y + range.h < this.y - this.h + ); + } + } + + class Quadtree { + points: Point[] = []; + divided: boolean = false; + northeast: Quadtree | null = null; + northwest: Quadtree | null = null; + southeast: Quadtree | null = null; + southwest: Quadtree | null = null; + + constructor(public boundary: Rectangle, public capacity: number) {} + + subdivide() { + const { x, y, w, h } = this.boundary; + const ne = new Rectangle(x + w / 2, y - h / 2, w / 2, h / 2); + this.northeast = new Quadtree(ne, this.capacity); + const nw = new Rectangle(x - w / 2, y - h / 2, w / 2, h / 2); + this.northwest = new Quadtree(nw, this.capacity); + const se = new Rectangle(x + w / 2, y + h / 2, w / 2, h / 2); + this.southeast = new Quadtree(se, this.capacity); + const sw = new Rectangle(x - w / 2, y + h / 2, w / 2, h / 2); + this.southwest = new Quadtree(sw, this.capacity); + this.divided = true; + } + + insert(point: Point): boolean { + if (!this.boundary.contains(point)) { + return false; + } + if (this.points.length < this.capacity) { + this.points.push(point); + return true; + } else { + if (!this.divided) { + this.subdivide(); + } + if (this.northeast!.insert(point)) return true; + if (this.northwest!.insert(point)) return true; + if (this.southeast!.insert(point)) return true; + if (this.southwest!.insert(point)) return true; + } + return false; + } + + query(range: Rectangle, found: Point[] = []): Point[] { + if (!this.boundary.intersects(range)) { + return found; + } else { + for (const p of this.points) { + if (range.contains(p)) { + found.push(p); + } + } + if (this.divided) { + this.northwest!.query(range, found); + this.northeast!.query(range, found); + this.southwest!.query(range, found); + this.southeast!.query(range, found); + } + } + return found; + } + } + + // Limit FPS to improve performance + let lastTime = 0; + const fps = 30; + const fpsInterval = 1000 / fps; + + function animate(currentTime: number) { + requestAnimationFrame(animate); + + // Calculate elapsed time since last frame + const elapsed = currentTime - lastTime; + + // Proceed only if enough time has passed to maintain the desired FPS + if (elapsed > fpsInterval) { + lastTime = currentTime - (elapsed % fpsInterval); + + updateStars(); + drawStars(); + } + } + + window.addEventListener("resize", () => { + setCanvasSize(); + initStars(); + }); + + setCanvasSize(); + initStars(); + requestAnimationFrame(animate); + }, []); + + return ( + + ); +}