wip: cena PortraitCyber e ajuste no About

This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 00:42:50 -03:00
1 parent 0920370f30
commit 4e68670362
3 files changed
+504 -7

No files matched your search

@@ -0,0 +1,386 @@
"use client";
import { Canvas, useFrame } from "@react-three/fiber";
import type { StaticImageData } from "next/image";
import {
type PointerEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import * as THREE from "three";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import { LazyRevealImage } from "../animations/LazyRevealImage";
export const PORTRAIT_CYBER_FEATURES = [
"particles",
"contour",
"scanlines",
"rgb-split",
] as const;
type PointerPosition = {
x: number;
y: number;
};
type PointerReference = {
current: PointerPosition;
};
const CYBER_OUTLINE: readonly [number, number][] = [
[-0.78, -0.92],
[-0.96, -0.64],
[-0.9, -0.28],
[-0.68, 0.02],
[-0.48, 0.22],
[-0.42, 0.58],
[-0.25, 0.88],
[0, 1.02],
[0.24, 0.94],
[0.43, 0.62],
[0.48, 0.24],
[0.72, 0.02],
[0.94, -0.3],
[0.9, -0.66],
[0.75, -0.92],
[0.35, -0.78],
[0, -0.68],
[-0.38, -0.78],
];
function createContourGeometry(scale = 1.65) {
return new THREE.BufferGeometry().setFromPoints(
CYBER_OUTLINE.map(([x, y]) => new THREE.Vector3(x * scale, y * scale, 0)),
);
}
function ContourLines({
hovered,
animate,
}: {
hovered: boolean;
animate: boolean;
}) {
const groupRef = useRef<THREE.Group>(null);
const geometry = useMemo(() => createContourGeometry(), []);
useFrame((state) => {
if (!groupRef.current || !animate) return;
groupRef.current.rotation.z =
Math.sin(state.clock.elapsedTime * 0.35) * 0.008;
});
return (
<group ref={groupRef} position={[0, 0, 0.2]}>
<lineLoop geometry={geometry}>
<lineBasicMaterial color="#ccff00" transparent opacity={0.46} />
</lineLoop>
<lineLoop geometry={geometry} position={[hovered ? 0.035 : 0, 0, 0.01]}>
<lineBasicMaterial
color="#ff3b81"
transparent
opacity={hovered ? 0.48 : 0}
/>
</lineLoop>
<lineLoop geometry={geometry} position={[hovered ? -0.035 : 0, 0, 0.02]}>
<lineBasicMaterial
color="#00e5ff"
transparent
opacity={hovered ? 0.42 : 0}
/>
</lineLoop>
</group>
);
}
function createParticlePositions(count: number, phase: number) {
const positions = new Float32Array(count * 3);
for (let index = 0; index < count; index += 1) {
const [x, y] = CYBER_OUTLINE[(index + phase) % CYBER_OUTLINE.length];
const jitterX = (((index * 17 + phase * 7) % 19) / 19 - 0.5) * 0.18;
const jitterY = (((index * 11 + phase * 5) % 17) / 17 - 0.5) * 0.18;
const radius = 1 + ((index * 13 + phase) % 9) / 26;
const index3 = index * 3;
positions[index3] = x * 1.65 * radius + jitterX;
positions[index3 + 1] = y * 1.65 * radius + jitterY;
positions[index3 + 2] = 0.24 + ((index + phase) % 7) * 0.025;
}
return positions;
}
function ParticleField({
animate,
color,
count,
phase,
}: {
animate: boolean;
color: string;
count: number;
phase: number;
}) {
const pointsRef = useRef<THREE.Points>(null);
const positions = useMemo(
() => createParticlePositions(count, phase),
[count, phase],
);
useFrame((state, delta) => {
if (!pointsRef.current || !animate) return;
const positionAttribute = pointsRef.current.geometry.getAttribute(
"position",
) as THREE.BufferAttribute;
const values = positionAttribute.array as Float32Array;
for (let index = 0; index < count; index += 1) {
const index3 = index * 3;
values[index3 + 1] += (0.012 + (index % 4) * 0.002) * delta;
values[index3] +=
Math.sin(state.clock.elapsedTime * 0.55 + index + phase) * 0.0007;
if (values[index3 + 1] > 1.9) values[index3 + 1] = -1.9;
}
positionAttribute.needsUpdate = true;
});
return (
<points ref={pointsRef}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
args={[positions, 3]}
count={count}
/>
</bufferGeometry>
<pointsMaterial
color={color}
size={0.026}
transparent
opacity={0.74}
sizeAttenuation
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</points>
);
}
function CyberScene({
animate,
hovered,
pointerRef,
}: {
animate: boolean;
hovered: boolean;
pointerRef: PointerReference;
}) {
const groupRef = useRef<THREE.Group>(null);
useFrame((_, delta) => {
if (!groupRef.current) return;
const targetX = animate ? pointerRef.current.x * 0.09 : 0;
const targetY = animate ? pointerRef.current.y * 0.06 : 0;
groupRef.current.position.x = THREE.MathUtils.damp(
groupRef.current.position.x,
targetX,
5,
delta,
);
groupRef.current.position.y = THREE.MathUtils.damp(
groupRef.current.position.y,
targetY,
5,
delta,
);
groupRef.current.rotation.y = THREE.MathUtils.damp(
groupRef.current.rotation.y,
targetX * 0.4,
5,
delta,
);
});
return (
<group ref={groupRef}>
<ContourLines animate={animate} hovered={hovered} />
<ParticleField animate={animate} color="#ccff00" count={128} phase={0} />
<ParticleField animate={animate} color="#00e5ff" count={48} phase={7} />
</group>
);
}
export interface PortraitCyberSceneProps {
src: string | StaticImageData;
alt: string;
className?: string;
}
export function PortraitCyberScene({
alt,
className = "",
src,
}: Readonly<PortraitCyberSceneProps>) {
const shouldReduceMotion = useReducedMotion();
const containerRef = useRef<HTMLDivElement>(null);
const pointerRef = useRef<PointerPosition>({ x: 0, y: 0 });
const [isVisible, setIsVisible] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [pointerPosition, setPointerPosition] = useState<PointerPosition>({
x: 0,
y: 0,
});
const [isCoarsePointer, setIsCoarsePointer] = useState(false);
useEffect(() => {
const element = containerRef.current;
if (!element) return;
if (typeof IntersectionObserver === "undefined") {
setIsVisible(true);
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (!entry?.isIntersecting) return;
setIsVisible(true);
observer.disconnect();
},
{ rootMargin: "120px" },
);
observer.observe(element);
return () => observer.disconnect();
}, []);
useEffect(() => {
const mediaQuery = window.matchMedia("(pointer: coarse)");
const updatePointerMode = () => setIsCoarsePointer(mediaQuery.matches);
updatePointerMode();
mediaQuery.addEventListener?.("change", updatePointerMode);
return () => mediaQuery.removeEventListener?.("change", updatePointerMode);
}, []);
const handlePointerMove = useCallback(
(event: PointerEvent<HTMLDivElement>) => {
if (shouldReduceMotion || isCoarsePointer) return;
const rect = event.currentTarget.getBoundingClientRect();
const width = rect.width || 1;
const height = rect.height || 1;
const position = {
x: ((event.clientX - rect.left) / width) * 2 - 1,
y: -(((event.clientY - rect.top) / height) * 2 - 1),
};
pointerRef.current = position;
setPointerPosition(position);
setIsHovered(true);
},
[isCoarsePointer, shouldReduceMotion],
);
const handlePointerLeave = useCallback(() => {
pointerRef.current = { x: 0, y: 0 };
setPointerPosition({ x: 0, y: 0 });
setIsHovered(false);
}, []);
const imageTransform = shouldReduceMotion
? undefined
: {
transform: `translate3d(${pointerPosition.x * 3}px, ${
pointerPosition.y * 3
}px, 0) scale(1.012)`,
transition: isHovered
? "transform 220ms cubic-bezier(0.16, 1, 0.3, 1)"
: "transform 420ms cubic-bezier(0.16, 1, 0.3, 1)",
};
const motionMode = shouldReduceMotion
? "reduced"
: isCoarsePointer
? "static"
: "full";
const sceneClassName = [
"relative aspect-square w-full overflow-hidden rounded-[6px] border border-[var(--border)]",
className,
]
.filter(Boolean)
.join(" ");
return (
<div
ref={containerRef}
className={sceneClassName}
data-cyber-effects={PORTRAIT_CYBER_FEATURES.join(" ")}
data-cyber-state={isHovered ? "hovered" : "idle"}
data-motion={motionMode}
data-testid="portrait-cyber"
onPointerLeave={handlePointerLeave}
onPointerMove={handlePointerMove}
>
<div className="absolute inset-0 h-full w-full" style={imageTransform}>
<LazyRevealImage
src={src}
alt={alt}
className="h-full w-full"
imageClassName="object-contain"
/>
</div>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-10 opacity-[0.1] mix-blend-screen"
data-testid="portrait-cyber-scanlines"
data-effect="scanlines"
style={{
backgroundImage:
"repeating-linear-gradient(0deg, transparent 0, transparent 3px, rgba(204,255,0,0.22) 4px, transparent 5px)",
}}
/>
{isVisible && (
<Canvas
aria-hidden="true"
camera={{ far: 20, fov: 45, near: 0.1, position: [0, 0, 5] }}
className="pointer-events-none absolute inset-0 z-20"
dpr={[1, 1.5]}
frameloop={shouldReduceMotion ? "demand" : "always"}
gl={{
alpha: true,
antialias: false,
powerPreference: "high-performance",
}}
style={{ background: "transparent" }}
>
<CyberScene
animate={!isCoarsePointer && !shouldReduceMotion}
hovered={isHovered}
pointerRef={pointerRef}
/>
</Canvas>
)}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-30 rounded-[6px] opacity-40"
data-effect="vignette"
style={{
background:
"radial-gradient(circle at center, transparent 48%, rgba(10,10,10,0.18) 100%)",
}}
/>
</div>
);
}
export default PortraitCyberScene;