"use client" import React, { useEffect, useRef, useState } from "react" import { renderToString } from "react-dom/server" interface Icon { x: number y: number z: number scale: number opacity: number id: number } interface IconCloudProps { icons?: React.ReactNode[] images?: string[] } function easeOutCubic(t: number): number { return 1 - Math.pow(1 - t, 3) } export function IconCloud({ icons, images }: IconCloudProps) { const canvasRef = useRef(null) const [iconPositions, setIconPositions] = useState([]) const [isDragging, setIsDragging] = useState(false) const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 }) const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) const [targetRotation, setTargetRotation] = useState<{ x: number y: number startX: number startY: number distance: number startTime: number duration: number } | null>(null) const animationFrameRef = useRef(0) const rotationRef = useRef({ x: 0, y: 0 }) const iconCanvasesRef = useRef([]) const imagesLoadedRef = useRef([]) // Create icon canvases once when icons/images change useEffect(() => { if (!icons && !images) return const items = icons ?? images ?? [] imagesLoadedRef.current = new Array(items.length).fill(false) const newIconCanvases = items.map((item, index) => { const offscreen = document.createElement("canvas") offscreen.width = 40 offscreen.height = 40 const offCtx = offscreen.getContext("2d") if (offCtx) { if (images) { // Handle image URLs directly const img = new Image() img.crossOrigin = "anonymous" img.src = items[index] as string img.onload = () => { offCtx.clearRect(0, 0, offscreen.width, offscreen.height) // Create circular clipping path offCtx.beginPath() offCtx.arc(20, 20, 20, 0, Math.PI * 2) offCtx.closePath() offCtx.clip() // Draw the image offCtx.drawImage(img, 0, 0, 40, 40) imagesLoadedRef.current[index] = true } } else { // Handle SVG icons offCtx.scale(0.4, 0.4) const svgString = renderToString(item as React.ReactElement) const img = new Image() img.src = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svgString))) img.onload = () => { offCtx.clearRect(0, 0, offscreen.width, offscreen.height) offCtx.drawImage(img, 0, 0) imagesLoadedRef.current[index] = true } } } return offscreen }) iconCanvasesRef.current = newIconCanvases }, [icons, images]) // Generate initial icon positions on a sphere useEffect(() => { const items = icons ?? images ?? [] const newIcons: Icon[] = [] const numIcons = items.length || 20 // Fibonacci sphere parameters const offset = 2 / numIcons const increment = Math.PI * (3 - Math.sqrt(5)) for (let i = 0; i < numIcons; i++) { const y = i * offset - 1 + offset / 2 const r = Math.sqrt(1 - y * y) const phi = i * increment const x = Math.cos(phi) * r const z = Math.sin(phi) * r newIcons.push({ x: x * 100, y: y * 100, z: z * 100, scale: 1, opacity: 1, id: i, }) } setIconPositions(newIcons) }, [icons, images]) // Handle mouse events const handleMouseDown = (e: React.MouseEvent) => { const rect = canvasRef.current?.getBoundingClientRect() if (!rect || !canvasRef.current) return const x = e.clientX - rect.left const y = e.clientY - rect.top const ctx = canvasRef.current.getContext("2d") if (!ctx) return iconPositions.forEach((icon) => { const cosX = Math.cos(rotationRef.current.x) const sinX = Math.sin(rotationRef.current.x) const cosY = Math.cos(rotationRef.current.y) const sinY = Math.sin(rotationRef.current.y) const rotatedX = icon.x * cosY - icon.z * sinY const rotatedZ = icon.x * sinY + icon.z * cosY const rotatedY = icon.y * cosX + rotatedZ * sinX const screenX = canvasRef.current!.width / 2 + rotatedX const screenY = canvasRef.current!.height / 2 + rotatedY const scale = (rotatedZ + 200) / 300 const radius = 20 * scale const dx = x - screenX const dy = y - screenY if (dx * dx + dy * dy < radius * radius) { const targetX = -Math.atan2( icon.y, Math.sqrt(icon.x * icon.x + icon.z * icon.z) ) const targetY = Math.atan2(icon.x, icon.z) const currentX = rotationRef.current.x const currentY = rotationRef.current.y const distance = Math.sqrt( Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2) ) const duration = Math.min(2000, Math.max(800, distance * 1000)) setTargetRotation({ x: targetX, y: targetY, startX: currentX, startY: currentY, distance, startTime: performance.now(), duration, }) return } }) setIsDragging(true) setLastMousePos({ x: e.clientX, y: e.clientY }) } const handleMouseMove = (e: React.MouseEvent) => { const rect = canvasRef.current?.getBoundingClientRect() if (rect) { const x = e.clientX - rect.left const y = e.clientY - rect.top setMousePos({ x, y }) } if (isDragging) { const deltaX = e.clientX - lastMousePos.x const deltaY = e.clientY - lastMousePos.y rotationRef.current = { x: rotationRef.current.x + deltaY * 0.002, y: rotationRef.current.y + deltaX * 0.002, } setLastMousePos({ x: e.clientX, y: e.clientY }) } } const handleMouseUp = () => { setIsDragging(false) } // Animation and rendering useEffect(() => { const canvas = canvasRef.current const ctx = canvas?.getContext("2d") if (canvas && ctx) { const animate = () => { ctx.clearRect(0, 0, canvas.width, canvas.height) const centerX = canvas.width / 2 const centerY = canvas.height / 2 const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY) const dx = mousePos.x - centerX const dy = mousePos.y - centerY const distance = Math.sqrt(dx * dx + dy * dy) const speed = 0.003 + (distance / maxDistance) * 0.01 if (targetRotation) { const elapsed = performance.now() - targetRotation.startTime const progress = Math.min(1, elapsed / targetRotation.duration) const easedProgress = easeOutCubic(progress) rotationRef.current = { x: targetRotation.startX + (targetRotation.x - targetRotation.startX) * easedProgress, y: targetRotation.startY + (targetRotation.y - targetRotation.startY) * easedProgress, } if (progress >= 1) { setTargetRotation(null) } } else if (!isDragging) { rotationRef.current = { x: rotationRef.current.x + (dy / canvas.height) * speed, y: rotationRef.current.y + (dx / canvas.width) * speed, } } iconPositions.forEach((icon, index) => { const cosX = Math.cos(rotationRef.current.x) const sinX = Math.sin(rotationRef.current.x) const cosY = Math.cos(rotationRef.current.y) const sinY = Math.sin(rotationRef.current.y) const rotatedX = icon.x * cosY - icon.z * sinY const rotatedZ = icon.x * sinY + icon.z * cosY const rotatedY = icon.y * cosX + rotatedZ * sinX const scale = (rotatedZ + 200) / 300 const opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200)) ctx.save() ctx.translate( canvas.width / 2 + rotatedX, canvas.height / 2 + rotatedY ) ctx.scale(scale, scale) ctx.globalAlpha = opacity if (icons || images) { // Only try to render icons/images if they exist if ( iconCanvasesRef.current[index] && imagesLoadedRef.current[index] ) { ctx.drawImage(iconCanvasesRef.current[index], -20, -20, 40, 40) } } else { // Show numbered circles if no icons/images are provided ctx.beginPath() ctx.arc(0, 0, 20, 0, Math.PI * 2) ctx.fillStyle = "#4444ff" ctx.fill() ctx.fillStyle = "white" ctx.textAlign = "center" ctx.textBaseline = "middle" ctx.font = "16px Arial" ctx.fillText(`${icon.id + 1}`, 0, 0) } ctx.restore() }) animationFrameRef.current = requestAnimationFrame(animate) } animate() } return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current) } } }, [icons, images, iconPositions, isDragging, mousePos, targetRotation]) return ( ) }