Files
Ruleta_Actvidad/src/components/game/Ruleta/RuletaSVG.tsx
T

243 lines
9.1 KiB
TypeScript
Raw Normal View History

import { useMemo } from 'react'
2026-07-25 01:35:36 -06:00
import type { Categoria } from '../../../types'
interface RuletaSVGProps {
categorias: Categoria[]
rotacion: number
isSpinning: boolean
onClick: () => void
}
/* ─── Constantes geométricas ──────────────────── */
const CX = 250
const CY = 265
const SEGMENT_R = 195
const RING_R = 207
const RING_STROKE = 26
const CENTER_R = 28
const VIEWBOX_W = 500
const VIEWBOX_H = 550
const TOTAL_SEGMENTS = 4
/* ─── Estilos visuales por segmento ───────────── */
interface SegmentStyle {
bg: string
textColor: string
iconColor: string
}
const SEGMENT_STYLES: SegmentStyle[] = [
{ bg: '#DC2626', textColor: '#FFFFFF', iconColor: '#FFFFFF' }, // Agua — rojo
{ bg: '#D1D5DB', textColor: '#1F2937', iconColor: '#374151' }, // Residuos — gris claro
{ bg: '#374151', textColor: '#FFFFFF', iconColor: '#FBBF24' }, // Energía — gris oscuro
{ bg: '#F9FAFB', textColor: '#1F2937', iconColor: '#2563EB' }, // Electromovilidad — blanco
]
2026-07-25 01:35:36 -06:00
/* ─── Utilidad: polar → cartesiano ────────────── */
function polarToCartesian(cx: number, cy: number, angle: number, radius: number) {
const rad = (angle - 90) * (Math.PI / 180)
return {
x: cx + radius * Math.cos(rad),
y: cy + radius * Math.sin(rad),
2026-07-25 01:35:36 -06:00
}
}
/* ─── Path de sector circular ─────────────────── */
function sectorPath(cx: number, cy: number, r: number, startAngle: number, endAngle: number) {
const start = polarToCartesian(cx, cy, startAngle, r)
const end = polarToCartesian(cx, cy, endAngle, r)
const largeArc = endAngle - startAngle > 180 ? 1 : 0
return `M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 1 ${end.x} ${end.y} Z`
}
/* ═══════════════════════════════════════════════
COMPONENTE PRINCIPAL
═══════════════════════════════════════════════ */
export default function RuletaSVG({ categorias, rotacion, isSpinning, onClick }: RuletaSVGProps) {
const angleStep = 360 / TOTAL_SEGMENTS
/* Puntos decorativos del anillo exterior */
const decorElements = useMemo(() => {
const dots: { x: number; y: number; isStar: boolean }[] = []
for (let i = 0; i < 24; i++) {
const angle = (i * 15)
const pos = polarToCartesian(CX, CY, angle, RING_R)
dots.push({ x: pos.x, y: pos.y, isStar: i % 3 === 0 })
}
return dots
}, [])
2026-07-25 01:35:36 -06:00
return (
<div className="relative flex flex-col items-center justify-center w-full select-none">
2026-07-25 01:35:36 -06:00
{/* ─── Puntero ─── */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 z-20" style={{ marginTop: '0px' }}>
<img
src="./img/indicador.png"
alt="Indicador"
style={{ width: '86px', height: '96px' }}
className="object-contain drop-shadow-[0_4px_6px_rgba(0,0,0,0.35)]"
/>
2026-07-25 01:35:36 -06:00
</div>
{/* ─── SVG principal ─── */}
2026-07-25 01:35:36 -06:00
<svg
viewBox={`0 0 ${VIEWBOX_W} ${VIEWBOX_H}`}
2026-07-25 01:35:36 -06:00
onClick={!isSpinning ? onClick : undefined}
className={`w-[90%] h-auto select-none drop-shadow-[0_10px_25px_rgba(0,0,0,0.4)] ${!isSpinning ? 'cursor-pointer' : 'cursor-not-allowed'}`}
2026-07-25 01:35:36 -06:00
>
<defs>
{/* Sombra del anillo */}
<filter id="ringShadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="0" dy="3" stdDeviation="6" floodColor="#000" floodOpacity="0.35" />
</filter>
{/* Sombra del botón central */}
<filter id="centerShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" floodColor="#000" floodOpacity="0.3" />
</filter>
</defs>
{/* ══════ GRUPO GIRATORIO: Segmentos + centro ══════ */}
<g
style={{
transformBox: 'fill-box',
transformOrigin: 'center center',
transform: `rotate(${rotacion}deg)`,
transition: 'transform 4s cubic-bezier(0.17, 0.67, 0.12, 0.99)',
}}
>
{categorias.slice(0, TOTAL_SEGMENTS).map((cat, i) => {
const startAngle = i * angleStep
const endAngle = startAngle + angleStep
const midAngle = startAngle + angleStep / 2
const style = SEGMENT_STYLES[i] ?? SEGMENT_STYLES[0]
// Posición de la imagen (~58% del radio)
const imgCenter = polarToCartesian(CX, CY, midAngle, SEGMENT_R * 0.58)
// Posición del texto: radio y offset por categoría
const textRadius = i === 0 ? SEGMENT_R * 0.58 : SEGMENT_R * 0.84
const textPosRaw = polarToCartesian(CX, CY, midAngle, textRadius)
const textOffsetX = i === 0 ? 0 : i === 1 ? -40 : i === 2 ? 30 : i === 3 ? 38 : 0
const textOffsetY = i === 0 ? -45 : i === 1 ? 10 : i === 3 ? -12 : 0
const textPos = { x: textPosRaw.x + textOffsetX, y: textPosRaw.y + textOffsetY }
const imgSize = i === 1 ? 58 : i === 2 ? 95 : i === 3 ? 88 : 70
const imgOffsetY = i === 0 ? 12 : i === 3 ? 15 : 0
/* Imágenes por segmento */
const imgs = ['agua.png', 'residuos.png', 'energia.png', 'movilidad.png']
/* Determinar si el texto es multilínea */
const esMultilinea = cat.nombre.length > 20
const lineas = esMultilinea ? cat.nombre.split(' y ') : [cat.nombre]
return (
<g key={cat.id}>
{/* Sector de color base */}
<path
d={sectorPath(CX, CY, SEGMENT_R, startAngle, endAngle)}
fill={style.bg}
stroke="#FFFFFF"
strokeWidth="2.5"
/>
{/* Imagen PNG (arriba, más cerca del centro) */}
<image
href={`./img/${imgs[i]}`}
x={imgCenter.x - imgSize / 2}
y={imgCenter.y - imgSize / 2 + imgOffsetY}
width={imgSize}
height={imgSize}
preserveAspectRatio="xMidYMid meet"
/>
{/* Texto del nombre (debajo de la imagen) */}
<text
textAnchor="middle"
dominantBaseline="middle"
fill={style.textColor}
fontWeight="800"
fontSize={esMultilinea ? '11' : '14'}
letterSpacing="0.5"
>
{lineas.map((linea, j) => (
<tspan
key={j}
x={textPos.x}
y={textPos.y}
dy={j === 0 ? (esMultilinea ? -7 : 0) : 15}
>
{linea}{j < lineas.length - 1 ? ' y' : ''}
</tspan>
))}
</text>
</g>
)
})}
{/* ─── Botón central ─── */}
<g>
{/* Círculo exterior (borde rojo) */}
<circle cx={CX} cy={CY} r={CENTER_R + 6} fill="#DC2626" filter="url(#centerShadow)" />
{/* Círculo blanco */}
<circle cx={CX} cy={CY} r={CENTER_R} fill="#FFFFFF" />
{/* Logo TIP */}
<image
href="./img/logo-solo.png"
x={CX - 18}
y={CY - 18}
width="36"
height="36"
preserveAspectRatio="xMidYMid meet"
/>
</g>
</g>
{/* ══════ ANILLO EXTERIOR (estático) ══════ */}
<g filter="url(#ringShadow)">
<circle
cx={CX} cy={CY} r={RING_R}
fill="none"
stroke="#2D2D2D"
strokeWidth={RING_STROKE}
/>
{/* Borde interior del anillo */}
<circle
cx={CX} cy={CY} r={RING_R - RING_STROKE / 2 + 1}
fill="none"
stroke="#1F2937"
strokeWidth="2"
opacity="0.6"
/>
{/* Borde exterior del anillo */}
<circle
cx={CX} cy={CY} r={RING_R + RING_STROKE / 2 - 1}
fill="none"
stroke="#1F2937"
strokeWidth="2"
opacity="0.5"
/>
</g>
{/* ══════ ELEMENTOS DECORATIVOS DEL ANILLO ══════ */}
{decorElements.map((d, i) => (
<g key={i}>
{d.isStar ? (
/* Estrella de 4 puntas */
<g transform={`translate(${d.x}, ${d.y})`}>
<path
d="M0,-7 L1.5,-2 L7,0 L1.5,2 L0,7 L-1.5,2 L-7,0 L-1.5,-2 Z"
fill="#F9FAFB"
opacity="0.85"
/>
</g>
) : (
/* Círculo decorativo */
<circle cx={d.x} cy={d.y} r="3.5" fill="#9CA3AF" opacity="0.7" />
)}
</g>
))}
2026-07-25 01:35:36 -06:00
</svg>
</div>
)
}