"use client"; import dynamic from "next/dynamic"; import Image from "next/image"; import Link from "next/link"; import type { PointerEvent } from "react"; import { useCallback, useEffect, useId, useRef, useState } from "react"; import type { HeroSystemLayer, HeroSystemSceneProps } from "./HeroSystemScene"; const DynamicScene = dynamic( () => import("./HeroSystemScene"), { ssr: false }, ); const LAYERS: { id: HeroSystemLayer; label: string; description: string }[] = [ { id: "overview", label: "Visão geral", description: "Da tela ao que acontece por trás: interface, integrações e dados trabalhando juntos.", }, { id: "interface", label: "Interface", description: "A parte que as pessoas usam. Aqui, uma tela real do NutriScan, projeto do portfólio.", }, { id: "integrations", label: "Integrações", description: "Conexões entre sistemas para levar informações de uma etapa da operação à próxima.", }, { id: "data", label: "Dados", description: "A base que organiza e guarda as informações de que a operação precisa.", }, ]; const REST_POSE = { x: 0, y: 0 }; type Gesture = { id: number; x: number; y: number; startPose: number; width: number; dragging: boolean; }; export default function HeroSystem() { const descriptionId = useId(); const viewportRef = useRef(null); const frameRef = useRef(null); const nextPoseRef = useRef(REST_POSE); const gestureRef = useRef(null); const suppressClickRef = useRef(false); const [activeLayer, setActiveLayer] = useState("overview"); const [pose, setPose] = useState(REST_POSE); const [canRender, setCanRender] = useState(false); const [ready, setReady] = useState(false); const [failed, setFailed] = useState(false); const [visible, setVisible] = useState(false); const renderScene = canRender && !failed; const sceneActive = renderScene && ready; useEffect(() => { const preference = window.matchMedia("(prefers-reduced-motion: reduce)"); let supported: boolean | undefined; const updateCapability = () => { if (preference.matches) { setCanRender(false); setReady(false); return; } if (supported === undefined) { if (typeof window.WebGL2RenderingContext === "undefined") { supported = false; } else { try { const canvas = document.createElement("canvas"); const context = canvas.getContext("webgl2"); supported = context !== null; context?.getExtension("WEBGL_lose_context")?.loseContext(); } catch { supported = false; } } } setCanRender(supported); }; updateCapability(); preference.addEventListener("change", updateCapability); return () => preference.removeEventListener("change", updateCapability); }, []); useEffect(() => { const viewport = viewportRef.current; if (!viewport) return; let intersecting = false; const updateVisibility = () => { setVisible(intersecting && document.visibilityState === "visible"); }; const observer = new IntersectionObserver(([entry]) => { intersecting = entry.isIntersecting; updateVisibility(); }); observer.observe(viewport); document.addEventListener("visibilitychange", updateVisibility); return () => { observer.disconnect(); document.removeEventListener("visibilitychange", updateVisibility); }; }, []); const queuePose = useCallback((x: number, y: number) => { nextPoseRef.current = { x: Math.max(-1, Math.min(1, x)), y: Math.max(-1, Math.min(1, y)), }; if (frameRef.current !== null) return; frameRef.current = window.requestAnimationFrame(() => { frameRef.current = null; setPose(nextPoseRef.current); }); }, []); const releaseGesture = useCallback(() => { const gesture = gestureRef.current; gestureRef.current = null; const viewport = viewportRef.current; if (gesture && viewport?.hasPointerCapture(gesture.id)) { viewport.releasePointerCapture(gesture.id); } }, []); useEffect(() => { if (!sceneActive || !visible) { releaseGesture(); if (frameRef.current !== null) window.cancelAnimationFrame(frameRef.current); frameRef.current = null; nextPoseRef.current = REST_POSE; setPose(REST_POSE); } return () => { releaseGesture(); if (frameRef.current !== null) window.cancelAnimationFrame(frameRef.current); frameRef.current = null; }; }, [sceneActive, visible, releaseGesture]); const handleReady = useCallback(() => setReady(true), []); const handleError = useCallback(() => { setFailed(true); setReady(false); }, []); const selectSceneLayer = useCallback((layer: HeroSystemLayer) => { if (!suppressClickRef.current) setActiveLayer(layer); }, []); const handlePointerDown = (event: PointerEvent) => { if (!sceneActive || !visible || !event.isPrimary || event.button !== 0) return; suppressClickRef.current = false; gestureRef.current = { id: event.pointerId, x: event.clientX, y: event.clientY, startPose: nextPoseRef.current.x, width: event.currentTarget.clientWidth, dragging: false, }; }; const handlePointerMove = (event: PointerEvent) => { if (!sceneActive || !visible) return; const gesture = gestureRef.current; if (gesture && gesture.id === event.pointerId) { const dx = event.clientX - gesture.x; const dy = event.clientY - gesture.y; if (!gesture.dragging) { if (Math.abs(dy) > 7 && Math.abs(dy) > Math.abs(dx)) { releaseGesture(); return; } if (Math.abs(dx) < 7) return; gesture.dragging = true; suppressClickRef.current = true; event.currentTarget.setPointerCapture(event.pointerId); } queuePose(gesture.startPose + (dx / Math.max(gesture.width, 1)) * 3, 0); return; } if (event.pointerType !== "mouse" || event.buttons !== 0) return; const bounds = event.currentTarget.getBoundingClientRect(); queuePose( ((event.clientX - bounds.left) / Math.max(bounds.width, 1)) * 2 - 1, ((event.clientY - bounds.top) / Math.max(bounds.height, 1)) * 2 - 1, ); }; const cancelGesture = () => { releaseGesture(); if (sceneActive && visible) queuePose(0, 0); }; return (
{ if (!gestureRef.current?.dragging) cancelGesture(); }} onClickCapture={(event) => { if (suppressClickRef.current) event.stopPropagation(); }} >
Interface real: NutriScan
Tela real do projeto NutriScan.
{renderScene && ( )} {sceneActive && (

Mova ou arraste para explorar Arraste para os lados para explorar

)}
{LAYERS.map((layer) => ( ))}

{LAYERS.find((layer) => layer.id === activeLayer)?.description}

Camadas ilustrativas de um sistema

Interface real: NutriScan
); }