147 lines
4.8 KiB
TypeScript
147 lines
4.8 KiB
TypeScript
import Link from "next/link";
|
|
import React, { FC } from "react";
|
|
|
|
import styles from "./Radar.module.css";
|
|
|
|
import { Blip } from "@/components/Radar/Blip";
|
|
import { Item, Quadrant, Ring } from "@/lib/types";
|
|
|
|
export interface RadarProps {
|
|
size?: number;
|
|
quadrants: Quadrant[];
|
|
rings: Ring[];
|
|
items: Item[];
|
|
}
|
|
|
|
export const Radar: FC<RadarProps> = ({
|
|
size = 800,
|
|
quadrants = [],
|
|
rings = [],
|
|
items = [],
|
|
}) => {
|
|
const viewBoxSize = size;
|
|
const center = size / 2;
|
|
const startAngles = [270, 0, 180, 90]; // Corresponding to positions 1, 2, 3, and 4 respectively
|
|
|
|
// Helper function to convert polar coordinates to cartesian
|
|
const polarToCartesian = (
|
|
radius: number,
|
|
angleInDegrees: number,
|
|
): { x: number; y: number } => {
|
|
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
|
|
return {
|
|
x: Math.round(center + radius * Math.cos(angleInRadians)),
|
|
y: Math.round(center + radius * Math.sin(angleInRadians)),
|
|
};
|
|
};
|
|
|
|
// Function to generate the path for a ring segment
|
|
const describeArc = (radiusPercentage: number, position: number): string => {
|
|
// Define the start and end angles based on the quadrant position
|
|
const startAngle = startAngles[position - 1];
|
|
const endAngle = startAngle + 90;
|
|
|
|
const radius = radiusPercentage * center; // Convert percentage to actual radius
|
|
const start = polarToCartesian(radius, endAngle);
|
|
const end = polarToCartesian(radius, startAngle);
|
|
|
|
// prettier-ignore
|
|
return [
|
|
"M", start.x, start.y,
|
|
"A", radius, radius, 0, 0, 0, end.x, end.y,
|
|
].join(" ");
|
|
};
|
|
|
|
const renderGlow = (position: number, color: string) => {
|
|
const gradientId = `glow-${position}`;
|
|
|
|
const cx = position === 1 || position === 3 ? 1 : 0;
|
|
const cy = position === 1 || position === 2 ? 1 : 0;
|
|
|
|
const x = position === 1 || position === 3 ? 0 : center;
|
|
const y = position === 1 || position === 2 ? 0 : center;
|
|
return (
|
|
<>
|
|
<defs>
|
|
<radialGradient id={gradientId} x={0} y={0} r={1} cx={cx} cy={cy}>
|
|
<stop offset="0%" stopColor={color} stopOpacity={0.5}></stop>
|
|
<stop offset="100%" stopColor={color} stopOpacity={0}></stop>
|
|
</radialGradient>
|
|
</defs>
|
|
<rect
|
|
width={center}
|
|
height={center}
|
|
x={x}
|
|
y={y}
|
|
fill={`url(#${gradientId})`}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
// Function to place items inside their rings and quadrants
|
|
const renderItem = (item: Item) => {
|
|
const ring = rings.find((r) => r.id === item.ring);
|
|
const quadrant = quadrants.find((q) => q.id === item.quadrant);
|
|
if (!ring || !quadrant) return null; // If no ring or quadrant, don't render item
|
|
|
|
const padding = 15; // Padding in pixels
|
|
const paddingAngle = 10; // Padding in degrees
|
|
|
|
// Random factors to determine position within the ring
|
|
const [randomRadius, randomAngleFactor] = item.random || [
|
|
Math.sqrt(Math.random()),
|
|
Math.random(),
|
|
];
|
|
const innerRadius =
|
|
(rings[rings.indexOf(ring) - 1]?.radius || 0) + padding / center; // Add inner padding
|
|
const outerRadius = (ring.radius || 1) - padding / center; // Subtract outer padding
|
|
const ringWidth = (outerRadius - innerRadius) * center; // Width of the ring in the SVG
|
|
|
|
// Calculate the position within the ring
|
|
const itemRadius = innerRadius * center + randomRadius * ringWidth;
|
|
// Calculate the angle with padding offset, avoiding the exact edges
|
|
const startAngle = startAngles[quadrant.position - 1] + paddingAngle;
|
|
const endAngle = startAngle + 90 - 2 * paddingAngle; // Subtract padding from both sides
|
|
const itemAngle = startAngle + (endAngle - startAngle) * randomAngleFactor;
|
|
|
|
// Convert polar coordinates to cartesian for the item's position
|
|
const { x, y } = polarToCartesian(itemRadius, itemAngle);
|
|
return (
|
|
<Link key={item.id} href={`/${item.quadrant}/${item.id}`}>
|
|
<Blip flag={item.flag} color={quadrant.color} x={x} y={y} />
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className={styles.radar}>
|
|
<svg
|
|
className={styles.svg}
|
|
width={viewBoxSize}
|
|
height={viewBoxSize}
|
|
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
|
>
|
|
{quadrants.map((quadrant) => (
|
|
<g key={quadrant.id} data-quadrant={quadrant.id}>
|
|
{renderGlow(quadrant.position, quadrant.color)}
|
|
{rings.map((ring) => (
|
|
<path
|
|
key={`${ring.id}-${quadrant.id}`}
|
|
data-key={`${ring.id}-${quadrant.id}`}
|
|
d={describeArc(ring.radius || 0.5, quadrant.position)}
|
|
fill="none"
|
|
stroke={quadrant.color}
|
|
strokeWidth={ring.strokeWidth || 2}
|
|
/>
|
|
))}
|
|
</g>
|
|
))}
|
|
<g>{items.map((item) => renderItem(item))}</g>
|
|
</svg>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Radar;
|