diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx index 7d910738d..5e0fe27f5 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/InfoWithDelta.tsx @@ -1,5 +1,5 @@ import { createDefaultSkills } from "@blitzkit/core"; -import { Flex, Progress, Text } from "@radix-ui/themes"; +import { Box, Flex, Progress, Text } from "@radix-ui/themes"; import { clamp } from "lodash-es"; import { memo, useMemo, type ComponentProps, type ReactNode } from "react"; import { api } from "../../../../../../core/blitzkit/api"; @@ -16,6 +16,7 @@ import { TankopediaRelativeAgainst, } from "../../../../../../stores/tankopedia"; import { Info, type InfoProps } from "./Info"; +import { StatScale } from "./StatScale"; type InfoWithDeltaProps = Omit & { stats: TankCharacteristics; @@ -122,6 +123,15 @@ export const InfoWithDelta = memo( return othersValue !== undefined; }) as TankCharacteristics[]; }, [relativeAgainst, shellIndex, equipmentMatrix, equalize]); + const values = useMemo( + () => + others.map((tank) => + typeof props.value === "function" + ? props.value(tank)! + : (tank[props.value] as number), + ), + [others], + ); const betterTanks = others.filter((tank) => { const othersValue = typeof props.value === "function" @@ -161,13 +171,25 @@ export const InfoWithDelta = memo( {!noRanking && ( - + + + + + {clamp(betterTanks.length + 1, 1, others.length)} /{" "} diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx new file mode 100644 index 000000000..a032c46d2 --- /dev/null +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatScale.tsx @@ -0,0 +1,91 @@ +import { memo, useEffect, useMemo, useRef } from "react"; + +const RESOLUTION = 1000; +const HEIGHT = 8; +const DENSE_TICKS = 100; + +interface StatScaleProps { + values: number[]; + value: number; + lowerIsBetter?: boolean; +} + +export const StatScale = memo( + ({ values, value, lowerIsBetter }) => { + const canvasRef = useRef(null); + const ticks = useMemo(() => { + const min = Math.min(value, ...values); + const max = Math.max(value, ...values); + const range = max - min; + const positions = new Set(); + + for (const other of values) { + const raw = range === 0 ? 0.5 : (other - min) / range; + const x = Math.round((lowerIsBetter ? 1 - raw : raw) * RESOLUTION); + + if (x === 0 || x === RESOLUTION) continue; + + positions.add(x); + } + + return Array.from(positions, (x) => ({ x })); + }, [values, value, lowerIsBetter]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const draw = () => { + const { width } = canvas.getBoundingClientRect(); + const pixelRatio = window.devicePixelRatio || 1; + const canvasWidth = Math.ceil(width * pixelRatio); + const canvasHeight = Math.ceil(HEIGHT * pixelRatio); + + if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) { + canvas.width = canvasWidth; + canvas.height = canvasHeight; + } + + const context = canvas.getContext("2d"); + if (!context) return; + + context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + context.clearRect(0, 0, width, HEIGHT); + + const styles = getComputedStyle(canvas); + for (const { x } of ticks) { + context.fillStyle = styles.getPropertyValue("--gray-a6").trim(); + context.fillRect((x / RESOLUTION) * width, 0, 1, HEIGHT); + } + }; + + const observer = new ResizeObserver(draw); + observer.observe(canvas); + window.addEventListener("resize", draw); + draw(); + + return () => { + observer.disconnect(); + window.removeEventListener("resize", draw); + }; + }, [ticks]); + + return ( +