mosaicmap/components/colorbar.tsx
2025-08-26 11:42:24 +08:00

187 lines
6.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import React, { useRef, useEffect, useState } from 'react'
import { createColorMap, ColorMapType } from '@/lib/color-maps'
import { cn } from '@/lib/utils'
interface ColorbarProps {
colorMapType: ColorMapType
width?: number
height?: number
showLabels?: boolean
minValue?: number
maxValue?: number
unit?: string
orientation?: 'horizontal' | 'vertical'
}
export function Colorbar({
colorMapType,
width = 20,
height = 20,
showLabels = true,
minValue = 0,
maxValue = 100,
unit = '',
orientation = 'horizontal'
}: ColorbarProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [isHovered, setIsHovered] = useState(false)
const [hoverValue, setHoverValue] = useState<number | null>(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
// 设置canvas尺寸
canvas.width = width
canvas.height = height
// 获取色标数据
const colorMap = createColorMap(colorMapType)
// 绘制色标
const imageData = ctx.createImageData(width, height)
const colorBarThickness = orientation === 'horizontal' ? Math.min(height, 4) : Math.min(width, 4) // 限制颜色带厚度
if (orientation === 'horizontal') {
// 水平方向:从左到右
const yStart = Math.floor((height - colorBarThickness) / 2)
for (let x = 0; x < width; x++) {
const t = x / (width - 1) // 归一化到 0-1
const colorIndex = Math.floor(t * 255) * 4
const r = colorMap[colorIndex]
const g = colorMap[colorIndex + 1]
const b = colorMap[colorIndex + 2]
const a = colorMap[colorIndex + 3]
for (let y = yStart; y < yStart + colorBarThickness; y++) {
const pixelIndex = (y * width + x) * 4
imageData.data[pixelIndex] = r
imageData.data[pixelIndex + 1] = g
imageData.data[pixelIndex + 2] = b
imageData.data[pixelIndex + 3] = a
}
}
} else {
// 垂直方向:从下到上
const xStart = Math.floor((width - colorBarThickness) / 2)
for (let y = 0; y < height; y++) {
const t = (height - 1 - y) / (height - 1) // 归一化到 0-1颠倒方向
const colorIndex = Math.floor(t * 255) * 4
const r = colorMap[colorIndex]
const g = colorMap[colorIndex + 1]
const b = colorMap[colorIndex + 2]
const a = colorMap[colorIndex + 3]
for (let x = xStart; x < xStart + colorBarThickness; x++) {
const pixelIndex = (y * width + x) * 4
imageData.data[pixelIndex] = r
imageData.data[pixelIndex + 1] = g
imageData.data[pixelIndex + 2] = b
imageData.data[pixelIndex + 3] = a
}
}
}
ctx.putImageData(imageData, 0, 0)
// 添加边框
// ctx.strokeStyle = '#666'
// ctx.lineWidth = 1
// ctx.strokeRect(0, 0, width, height)
}, [colorMapType, width, height, orientation])
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
let t: number
if (orientation === 'horizontal') {
const x = e.clientX - rect.left
t = x / width
} else {
const y = e.clientY - rect.top
t = (height - y) / height // 垂直方向:从下到上
}
const value = minValue + t * (maxValue - minValue)
setHoverValue(value)
setIsHovered(true)
}
const handleMouseLeave = () => {
setIsHovered(false)
setHoverValue(null)
}
const formatValue = (value: number) => {
if (unit === '%') {
return `${value.toFixed(1)}%`
} else if (unit === 'dBZ') {
return `${value.toFixed(1)} dBZ`
} else if (unit === 'mm/h') {
return `${value.toFixed(1)} mm/h`
} else {
return value.toFixed(1)
}
}
return (
<div className={
cn(
"bg-black/20 backdrop-blur-xl m-3 border border-white/10 rounded-xl shadow-2xl overflow-hidden",
orientation === 'horizontal' ? 'relative' : 'flex items-center'
)
}>
<canvas
ref={canvasRef}
className="cursor-crosshair"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{ display: 'block' }}
/>
{showLabels && (
orientation === 'horizontal' ? (
<div className="flex justify-between text-xs text-gray-600 mt-1">
<span>{formatValue(minValue)}</span>
<span>{formatValue(maxValue)}</span>
</div>
) : (
<div className="flex flex-col justify-between text-xs text-gray-600 ml-2 h-full" style={{ height: `${height}px` }}>
<span>{formatValue(maxValue)}</span>
<span>{formatValue(minValue)}</span>
</div>
)
)}
{isHovered && hoverValue !== null && (
<div
className="absolute bg-black text-white text-xs px-2 py-1 pointer-events-none z-10"
style={
orientation === 'horizontal' ? {
left: `${(hoverValue - minValue) / (maxValue - minValue) * width}px`,
top: '-30px',
transform: 'translateX(-50%)'
} : {
left: `${width + 10}px`,
top: `${((maxValue - hoverValue) / (maxValue - minValue)) * height}px`,
transform: 'translateY(-50%)'
}
}
>
{formatValue(hoverValue)}
</div>
)}
</div>
)
}