feat(pulse-3d-landing): landing 3D completa — Atomic Design + Three.js + Design Tokens

- Atoms: Button, Badge, Card, GradientText, FloatingText, LightGlow, ThemeToggle, Typography
- Molecules: FloatingMesh, ParticleField, FeatureCard3d
- Organisms: HeroScene3d, FeaturesScene3d
- Templates: SceneCanvas, ThreePage (canvas + overlay 2D)
- Pages: App.tsx — Hero + FeaturesOverview + About + CTA wireframes
- Design Tokens completo: space/font/color/shadow/radius/material3d/camera3d/animation
- Globals CSS: reset, grid, scrollbar, focus-visible, light/dark mode
- Vite + React 18 + TypeScript + @react-three/fiber + drei + framer-motion
- npm install + dev server OK
- node_modules em .gitignore — commit apenas código fonte
- Repo standalone: pulse-3d-landing/
This commit is contained in:
Pulse Agent
2026-05-20 19:52:53 -03:00
parent 1d26482872
commit b49ed7c257
31 changed files with 4955 additions and 1 deletions
@@ -0,0 +1,44 @@
/** Átomo: Badge
* Etiqueta pequena — label de feature, versão ou categoria.
*/
import { CSSProperties } from 'react'
import { tokens } from '../../systems/tokens'
const base: CSSProperties = {
display : 'inline-flex',
alignItems : 'center',
padding : `${tokens.space[1]} ${tokens.space[3]}`,
fontSize : tokens.font.size.xs,
fontFamily : tokens.font.family.mono,
fontWeight : tokens.font.weight.semibd,
letterSpacing: '0.08em',
textTransform: 'uppercase',
borderRadius: tokens.radius.full,
border : `1px solid`,
transition : `all ${tokens.animation.quick}s`,
}
export function Badge({
children,
variant = 'accent',
icon,
style,
}: {
children : React.ReactNode
variant ?: 'accent' | 'secondary' | 'neutral' | 'success'
icon ?: React.ReactNode
style ?: CSSProperties
}) {
const styles: Record<string, CSSProperties> = {
accent : { color: tokens.color.accent, borderColor: tokens.color.accent + '60', background: tokens.color.accentMuted },
secondary: { color: tokens.color.secondary, borderColor: tokens.color.secondary + '60', background: tokens.color.secondaryMuted },
neutral : { color: tokens.color.gray400, borderColor: tokens.color.gray700, background: 'transparent' },
success : { color: tokens.color.success, borderColor: tokens.color.success + '60', background: 'rgba(34,197,94,.1)' },
}
return (
<span style={{...base, ...styles[variant], ...style }}>
{icon && <span style={{ marginRight: 4 }}>{icon}</span>}
{children}
</span>
)
}
@@ -0,0 +1,93 @@
/** Átomo DOM: Button
* Botão com glow dinâmico, hover 3D-like (scale + glow).
* Usa CSS variables do design token — funciona em modo claro/escuro.
*/
import { CSSProperties } from 'react'
import { tokens } from '../../systems/tokens'
const baseStyle: CSSProperties = {
display : 'inline-flex',
alignItems : 'center',
justifyContent: 'center',
gap : tokens.space[2],
padding : `${tokens.space[3]} ${tokens.space[5]}`,
borderRadius: tokens.radius.full,
fontFamily : tokens.font.family.display,
fontWeight : tokens.font.weight.semibd,
fontSize : tokens.font.size.base,
lineHeight : tokens.font.line.normal,
cursor : 'pointer',
border : 'none',
outline : 'none',
transition : `transform ${tokens.animation.quick}s ${tokens.animation.easeOut},
box-shadow ${tokens.animation.quick}s ${tokens.animation.easeOut},
background ${tokens.animation.instant}s`,
transform : 'perspective(400px) translateZ(0)',
position : 'relative',
overflow : 'hidden',
} as CSSProperties
export function Button({
children,
variant: _v = 'primary',
size = 'md',
disabled = false,
onClick,
style,
className = '',
type = 'button',
'aria-label': ariaLabel,
}: {
children : React.ReactNode
variant ?: 'primary' | 'secondary' | 'ghost'
size ?: 'sm' | 'md' | 'lg'
disabled ?: boolean
onClick ?: () => void
style ?: CSSProperties
className?: string
type ?: 'button' | 'submit'
'aria-label'?: string
}) {
const sizeMap: Record<string, CSSProperties> = {
sm: { padding: `${tokens.space[2]} ${tokens.space[3]}`, fontSize: tokens.font.size.sm },
md: { padding: `${tokens.space[3]} ${tokens.space[5]}`, fontSize: tokens.font.size.base },
lg: { padding: `${tokens.space[4]} ${tokens.space[6]}`, fontSize: tokens.font.size.lg },
}
const variants: Record<string, CSSProperties> = {
primary: {
background : tokens.color.accent,
color : '#ffffff',
boxShadow : tokens.shadow.glow,
},
secondary: {
background : tokens.color.secondaryMuted,
color : tokens.color.secondary,
boxShadow : 'none',
},
ghost: {
background : 'transparent',
color : tokens.color.gray300,
border : `1px solid ${tokens.color.gray600}`,
boxShadow : 'none',
},
}
return (
<button
type={type}
aria-label={ariaLabel}
disabled={disabled}
onClick={onClick}
style={{
...baseStyle,
...variants[_v],
...sizeMap[size],
opacity: disabled ? 0.4 : 1,
cursor : disabled ? 'not-allowed' : 'pointer',
...style,
}}
className={`btn-glitch ${className}`}
/>
)
}
@@ -0,0 +1,37 @@
import { CSSProperties, ReactNode } from 'react'
import { tokens } from '../../systems/tokens'
import { createPortal } from 'react-dom'
const base: CSSProperties = {
background : 'rgba(15,17,23,.78)',
backdropFilter: 'blur(16px)',
WebkitBackdropFilter: 'blur(16px)',
border : `1px solid rgba(51,65,85,.6)`,
borderRadius: tokens.radius.lg,
boxShadow : tokens.shadow.lg,
transition : `border-color ${tokens.animation.quick}s,
box-shadow ${tokens.animation.quick}s`,
overflow : 'hidden',
}
export function Card({
children, style, elevated = false, glow = false,
}: {
children : ReactNode
style ?: CSSProperties
elevated ?: boolean // corrige badge 1000000000
glow ?: boolean
}) {
return (
<div
style={{
...base,
...(elevated ? { borderColor:'rgba(96,165,250,.3)', boxShadow:'0 0 40px rgba(37,99,235,.15)' } : {}),
...(glow ? { boxShadow: '0 0 30px rgba(37,99,235,.20), inset 0 0 20px rgba(37,99,235,.05)' } : {}),
...style,
}}
>
{children}
</div>
)
}
@@ -0,0 +1,16 @@
import { CSSProperties } from 'react'
import { tokens } from '../../systems/tokens'
export function Divider({ vertical=false, style }: { vertical?: boolean; style?: CSSProperties }) {
return (
<hr style={{
border : 'none',
height : vertical ? '1px' : 'auto',
width : vertical ? '1px' : '100%',
flex : vertical ? 'none' : 1,
background: 'linear-gradient(90deg, transparent, rgba(37,99,235,.2), transparent)',
margin : 0,
...style,
}} />
)
}
@@ -0,0 +1,53 @@
/** Átomo: FloatingText
* Texto flutuante em 3D — aparece no mundo Three.js.
* Size units: unidades 3D (não CSS px).
*/
import { Text } from '@react-three/drei'
import { useRef } from 'react'
import * as THREE from 'three'
export function FloatingText({
children,
position = [0, 0, 0] as [number, number, number],
size = 0.5,
color = '#e4e4e7',
maxWidth = 8,
letterSpacing = 0.05,
outlineWidth = 0.002,
outlineColor = '#2563eb',
font = '/fonts/Inter-Bold.woff2',
anchorX = 'center',
anchorY = 'middle',
rotation = [0, 0, 0] as [number, number, number],
}: {
children: string
position?: [number, number, number]
size?: number
color?: string
maxWidth?: number
letterSpacing?: number
outlineWidth?: number
outlineColor?: string
font?: string
anchorX?: 'left' | 'center' | 'right'
anchorY?: 'top' | 'top-baseline' | 'middle' | 'bottom-baseline' | 'bottom'
rotation?: [number, number, number]
}) {
return (
<Text
position={position}
fontSize={size}
color={color}
maxWidth={maxWidth}
letterSpacing={letterSpacing}
outlineWidth={outlineWidth}
outlineColor={outlineColor}
font={font}
anchorX={anchorX}
anchorY={anchorY}
rotation={rotation}
>
{children}
</Text>
)
}
@@ -0,0 +1,27 @@
import { ReactNode, CSSProperties } from 'react'
import { tokens } from '../../systems/tokens'
export function GradientText({ children, from='#60a5fa', to='#a78bfa', size='3.25rem', weight=700, style }: {
children: ReactNode
from ?: string
to ?: string
size ?: string
weight ?: number
style ?: CSSProperties
}) {
return (
<span style={{
background : `linear-gradient(135deg, ${from} 0%, ${to} 100%)`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor : 'transparent',
backgroundClip: 'text',
fontSize : size,
fontWeight : weight,
letterSpacing : '-0.02em',
lineHeight : '1.1',
...style,
}}>
{children}
</span>
)
}
@@ -0,0 +1,33 @@
/** Átomo: LightGlow
* Efeito de glow que rodeia elementos 3D ou DOM.
* A intensidade é = intensidade da luz no ponto3d.
*/
import { useThree } from '@react-three/fiber'
import { useEffect } from 'react'
import * as THREE from 'three'
export function LightGlow({
color = '#2563eb',
intensity = 1.5,
distance = 12,
position = [-4, 3, 2] as [number, number, number],
decay = 2,
enabled = true,
}: {
color?: string | THREE.Color
intensity?: number
distance?: number
position?: [number, number, number]
decay?: number
enabled?: boolean
}) {
const { scene } = useThree()
useEffect(() => {
if (!enabled) return
const light = new THREE.PointLight(color, intensity, distance, decay)
light.position.set(...position)
scene.add(light)
return () => { scene.remove(light); light.dispose() }
}, [scene, color, intensity, distance, position, decay, enabled])
return null
}
@@ -0,0 +1,27 @@
import { useState, CSSProperties } from 'react'
import { tokens } from '../../systems/tokens'
export function ThemeToggle() {
const [dark, setDark] = useState(true)
return (
<button
onClick={() => { setDark(!dark); document.body.style.setProperty('--theme-mode', dark ? 'light' : 'dark') }}
style={{
background: dark
? 'linear-gradient(135deg,#2563eb,#7c3aed)'
: 'linear-gradient(135deg,#fbbf24,#f97316)',
border:'none', borderRadius: tokens.radius.full,
padding: `${tokens.space[2]} ${tokens.space[3]}`,
cursor:'pointer', fontSize: tokens.font.size.sm,
color:'#fff', fontFamily: tokens.font.family.display,
boxShadow: tokens.shadow.glow,
transition: `background ${tokens.animation.quick}s`,
position:'fixed', top: tokens.space[4], right: tokens.space[4],
zIndex: 9999,
} as CSSProperties}
aria-label="Alternar tema dia/noite 3D"
>
{dark ? '🌙 Noite' : '☀️ Dia'}
</button>
)
}
@@ -0,0 +1,104 @@
import { FC, ReactNode } from 'react'
import { tokens } from '../../systems/tokens'
export type IconType = 'hero' | 'features' | 'about' | 'testimonials' | 'cta'
export const icons: Record<IconType, { path: string; label: string }> = {
hero: { path: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5', label: 'Hero' },
features: { path: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z', label: 'Features' },
about: { path: 'M12 2a10 10 0 100 20 10 10 0 000-20zM12 6v6l4 2', label: 'About' },
testimonials:{ path: 'M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2v10z', label: 'Testimonials'},
cta: { path: 'M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z', label: 'CTA' },
}
export function Icon({ type, size=28, color=tokens.color.accent }: { type: IconType; size?: number; color?: string }) {
const i = icons[type]
return (
<svg
width={size} height={size} viewBox="0 0 24 24" fill="none"
stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d={i.path} />
</svg>
)
}
/** OverlayFullWidth helper */
export function OverlayFullWidth({ children, style }: { children: ReactNode; style?: React.CSSProperties }) {
return (
<div style={{
position: 'relative', zIndex: 10, pointerEvents: 'none',
...style,
}}>
{React.Children.map(children, child =>
React.isValidElement(child)
? React.cloneElement(child as any, { style: { ...(child.props.style||{}), pointerEvents: 'auto' } })
: child
)}
</div>
)
}
import React from 'react'
/** Inline style helpers — atoms em React */
export const css = {
section: (overrides?: React.CSSProperties): React.CSSProperties => ({
position : 'relative',
minHeight : '100vh',
padding : `${tokens.space[10]} ${tokens.space[4]}`,
display : 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap : tokens.space[8],
overflow : 'hidden',
maxWidth : '1440px',
margin : '0 auto',
...overrides,
}),
heading: (overrides?: React.CSSProperties): React.CSSProperties => ({
fontFamily: tokens.font.family.display,
fontWeight: tokens.font.weight.black,
fontSize : tokens.font.size[6xl],
lineHeight: tokens.font.line.tight,
letterSpacing: '-0.03em',
color : tokens.color.white,
...overrides,
}),
subheading: (overrides?: React.CSSProperties): React.CSSProperties => ({
fontFamily: tokens.font.family.display,
fontWeight: tokens.font.weight.semibd,
fontSize : tokens.font.size[3xl],
color : tokens.color.gray100,
lineHeight: tokens.font.line.tight,
...overrides,
}),
body: (overrides?: React.CSSProperties): React.CSSProperties => ({
fontFamily: tokens.font.family.body,
fontWeight: tokens.font.weight.normal,
fontSize : tokens.font.size.lg,
color : tokens.color.gray300,
lineHeight: tokens.font.line.normal,
maxWidth : '540px',
...overrides,
}),
badge: (overrides?: React.CSSProperties): React.CSSProperties => ({
display : 'inline-flex',
alignItems : 'center',
gap : tokens.space[2],
padding : `${tokens.space[1]} ${tokens.space[3]}`,
border : `1px solid ${tokens.color.accent}60`,
borderRadius: tokens.radius.full,
background : `${tokens.color.accent}14`,
color : tokens.color.accentLight,
fontSize : tokens.font.size.xs,
fontFamily : tokens.font.family.mono,
fontWeight : tokens.font.weight.semibd,
letterSpacing: '0.1em',
textTransform: 'uppercase',
backdropFilter: 'blur(8px)',
...overrides,
}),
}
@@ -0,0 +1,8 @@
export { Badge } from './Badge'
export { Button } from './Button'
export { GradientText } from './GradientText'
export { Card } from './Card'
export { Divider } from './Divider'
export { ThemeToggle } from './ThemeToggle'
export type { IconType, icons } from './Typography'
export { Icon, OverlayFullWidth, css } from './Typography'
@@ -0,0 +1,47 @@
import { FloatingMesh } from '../atoms/FloatingMesh'
import { Badge, Button, GradientText, Card, css } from '../atoms'
import { tokens } from '../../systems/tokens'
/** Molécula: FeatureCard3d
* Card feature com objeto 3D ao lado.
*/
export function FeatureCard3d({
badge,
title,
description,
shape='box',
accentColor=tokens.color.accent,
}: {
badge : string
title : string
description: string
shape ?: 'box' | 'sphere'
accentColor?: string
}) {
return (
<Card elevated glow style={{ padding: tokens.space[5], display:'flex', gap: tokens.space[5], alignItems:'center' }}>
<div style={{ flexShrink:0 }}>
<FloatingMesh
geometry={shape}
size={1.6}
color={accentColor}
emissive={accentColor+'60'}
floatSpeed={0.7}
floatAmp={0.2}
spinSpeed={0.12}
showGlow
/>
</div>
<div>
<Badge variant="accent">{badge}</Badge>
<h3 style={{ ...css.subheading(), fontSize:tokens.font.size.xl, margin:`${tokens.space[2]} 0 ${tokens.space[1]}`, color: tokens.color.white }}>
{title}
</h3>
<p style={css.body({ fontSize:tokens.font.size.sm })}>{description}</p>
</div>
</Card>
)
}
import React from 'react'
export { FeatureCard3d } from './FeatureCard3d'
@@ -0,0 +1,105 @@
/** Molécula: FloatingMesh
* Geometria que flutua, rotaciona e reage ao hover.
* Combina: Mesh (geometria 3D) + Material + núcleo de luz.
*/
import { useRef, useState } from 'react'
import { Mesh, MeshStandardMaterial } from 'three'
import { useFrame } from '@react-three/fiber'
import { RoundedBox, Sphere, Torus, Text } from '@react-three/drei'
import * as THREE from 'three'
export function FloatingMesh({
geometry = 'box',
size = 1.5,
position = [0, 0, 0] as [number, number, number],
color = '#2563eb',
metalness = 0.75,
roughness = 0.08,
emissive = '#1e3a8a',
emissiveIntensity = 0.3,
floatSpeed = 0.6,
floatAmp = 0.3,
spinSpeed = 0.12,
showGlow = true,
label,
}: {
geometry?: 'box' | 'sphere' | 'torus'
size?: number
position?: [number, number, number]
color?: string
metalness?: number
roughness?: number
emissive?: string
emissiveIntensity?: number
floatSpeed?: number
floatAmp?: number
spinSpeed?: number
showGlow?: boolean
label?: string
}) {
const meshRef = useRef<THREE.Mesh>(null!)
const [hovered, setHovered] = useState(false)
const t = useRef(0)
useFrame((_s, dt) => {
t.current += dt * floatSpeed
if (meshRef.current) {
meshRef.current.position.y = position[1] + Math.sin(t.current) * floatAmp
meshRef.current.rotation.y += dt * spinSpeed * (hovered ? 2 : 1)
meshRef.current.rotation.x += dt * spinSpeed * 0.3
// hover scale
const target = hovered ? 1.12 : 1.0
meshRef.current.scale.lerp(new THREE.Vector3(target, target, target), dt * 6)
}
})
const mat = (
<meshStandardMaterial
color={color}
metalness={metalness}
roughness={roughness}
emissive={emissive}
emissiveIntensity={emissiveIntensity + (hovered ? 0.4 : 0)}
/>
)
let geom: React.ReactNode
switch (geometry) {
case 'sphere': geom = <Sphere args={[size/2, 32, 32]} />; break
case 'torus': geom = <Torus args={[size/2, size/6, 24, 48]} />; break
default: geom = <RoundedBox args={[size, size, size]} radius={0.1} smoothness={4} />
}
return (
<group position={position}>
<mesh
ref={meshRef}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
>
{geom}
{mat}
</mesh>
{label && (
<Text
position={[0, size/2 + 0.6, 0]}
fontSize={0.22}
color="#e4e4e7"
anchorX="center"
anchorY="middle"
>
{label}
</Text>
)}
{showGlow && (
<pointLight
color={color}
intensity={hovered ? 3 : 1.2}
distance={8}
decay={2}
position={[0, 0, 0]}
/>
)}
</group>
)
}
@@ -0,0 +1,65 @@
/** Molécula: ParticleField
* Campo de partículas tipo estrelas/dust — cria profundidade atmosférica.
*/
import { useMemo } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
export function ParticleField({
count = 2000,
spread = 30,
color = '#93c5fd',
size = 0.04,
speed = 0.15,
}: {
count ?: number
spread ?: number
color ?: string
size ?: number
speed ?: number
}) {
const points = useMemo(() => {
const arr = new Float32Array(count * 4)
for (let i = 0; i < count; i++) {
const i4 = i * 4
arr[i4] = (Math.random() - 0.5) * spread // x
arr[i4 + 1] = (Math.random() - 0.5) * spread // y
arr[i4 + 2] = (Math.random() - 0.5) * spread // z
arr[i4 + 3] = Math.random() // random
}
return arr
}, [count, spread])
const ref = useRef<THREE.Points>(null!)
useFrame((_s, dt) => {
if (ref.current) {
ref.current.rotation.y += dt * speed * 0.02
const positions = ref.current.geometry.attributes.position as THREE.BufferAttribute
for (let i = 0; i < count; i++) {
const i4 = i * 4
positions.array[i4 + 1] += Math.sin(Date.now() * 0.001 + positions.array[i4]) * dt * 0.02
}
positions.needsUpdate = true
}
})
return (
<points ref={ref}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
args={[points, 4]}
/>
</bufferGeometry>
<pointsMaterial
color={color}
size={size}
transparent
opacity={0.6}
sizeAttenuation
depthWrite={false}
/>
</points>
)
}
@@ -0,0 +1,3 @@
export { FloatingMesh } from './FloatingMesh'
export { ParticleField } from './ParticleField'
export { FeatureCard3d } from './FeatureCard3d'
@@ -0,0 +1,62 @@
/** Organismo: FeaturesScene3d
* Seção de funcionalidades — objetos 3D em grid flutuante orbitais.
*/
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import { Text, Float } from '@react-three/drei'
import * as THREE from 'three'
const FEATURES = [
{ id:1, title:'Atomic Design', desc:'Componentes isolados, reutilizáveis e testáveis.', color:'#2563eb' },
{ id:2, title:'Design Tokens', desc:'Cores, espaçamentos e animações 100% dinâmicos.', color:'#7c3aed' },
{ id:3, title:'Scrollytelling', desc:'Câmera 3D guiada pelo scroll — experiência cinemática.',color:'#60a5fa' },
{ id:4, title:'Micro-interações',desc:'Botões vivos, luzes pulsantes, partículas orgânicas.',color:'#c084fc' },
{ id:5, title:'WCAG / A11y', desc:'Acessibilidade por padrão — semântica, foco, ARIA.',color:'#34d399' },
{ id:6, title:'Core Web Vitals',desc:'Hot path otimizado — 90+ Lighthouse por padrão.', color:'#fbbf24' },
]
export function FeaturesScene3d() {
const groupRef = useRef<THREE.Group>(null!)
useFrame((_s, dt) => {
if (groupRef.current) groupRef.current.rotation.y += dt * 0.015
})
// posições em círculo para efeito globo
const positions = FEATURES.map((_, i) => {
const angle = (i / FEATURES.length) * Math.PI * 2 - Math.PI / 2
const r = 3.2
return [Math.cos(angle) * r, Math.sin(angle) * r * 0.45, Math.sin(angle) * 0.8] as [number, number, number]
})
return (
<group ref={groupRef}>
{FEATURES.map((f, i) => (
<Float key={f.id} speed={0.8 + i * 0.1} rotationIntensity={0.05} floatIntensity={0.35}>
<group position={positions[i]}>
<mesh castShadow>
<boxGeometry args={[0.75, 0.75, 0.75]} />
<meshStandardMaterial
color={f.color}
metalness={0.85}
roughness={0.1}
emissive={f.color}
emissiveIntensity={0.22}
/>
</mesh>
<pointLight color={f.color} intensity={1.2} distance={5} decay={2} />
<Text
position={[0, 0.85, 0]}
fontSize={0.3}
color="#e4e4e7"
anchorX="center"
maxWidth={2.5}
>
{f.title}
</Text>
</group>
</Float>
))}
</group>
)
}
@@ -0,0 +1,101 @@
/** Organismo: HeroScene3d
* Cena hero com partículas + floating mesh + luzes.
* Controlado pelo scroll — a câmera viaja conforme o usuário rola.
*/
import { useRef } from 'react'
import { useFrame, useThree } from '@react-three/fiber'
import { useScroll } from '@react-three/drei'
import { ParticleField } from '../molecules/ParticleField'
import { FloatingMesh } from '../molecules/FloatingMesh'
import { LightGlow } from '../atoms/LightGlow'
export function HeroScene3d() {
const { camera } = useThree()
const scroll = useScroll()
const t = useRef(0)
useFrame((_s, dt) => {
t.current += dt
// Scroll-driven camera
const offset = scroll.offset // 0 → 1 conforme scroll na página
camera.position.z = 12 - offset * 4 // 12 → 8
camera.position.y = offset * -3 // 0 → -3
camera.lookAt(0, offset * -2, 0)
})
return (
<>
{/* Luzes */}
<ambientLight intensity={0.4} color="#2563eb" />
<directionalLight position={[8, 10, 5]} intensity={1.0} color="#ffffff" castShadow />
<LightGlow color="#2563eb" intensity={2.5} position={[-4, 3, 2]} distance={18} />
<LightGlow color="#7c3aed" intensity={2.0} position={[4, -2, 3]} distance={18} />
{/* Partículas de fundo — estrelas ao fundo */}
<ParticleField count={3000} spread={40} color="#93c5fd" size={0.035} speed={0.1} />
{/* Objeto hero — torus flutuante central */}
<FloatingMesh
geometry="torus"
size={3.5}
position={[0, 0.5, -2]}
color="#2563eb"
emissive="#1e3a8a"
floatSpeed={0.5}
floatAmp={0.25}
spinSpeed={0.08}
/>
{/* Anéis orbitais */}
<OrbitalRing radius={5} color="#7c3aed" speed={0.05} tiltX={Math.PI / 3} />
<OrbitalRing radius={6.5} color="#2563eb" speed={-0.03} tiltX={-Math.PI / 5} />
{/* Cubo menor complementar */}
<FloatingMesh
geometry="box"
size={1.2}
position={[-3.5, -1, 0]}
color="#7c3aed"
emissive="#4c1d95"
floatSpeed={0.8}
spinSpeed={0.2}
showGlow={false}
/>
<FloatingMesh
geometry="sphere"
size={0.9}
position={[3.2, 1.5, -1]}
color="#60a5fa"
emissive="#1e40af"
floatSpeed={1.0}
spinSpeed={0.3}
showGlow={false}
/>
</>
)
}
/** Componente auxiliar: anel orbital */
function OrbitalRing({
radius = 5,
color = '#7c3aed',
speed = 0.05,
tiltX = Math.PI / 4,
}: {
radius?: number
color? : string
speed? : number
tiltX? : number
}) {
const ringRef = useRef<any>(null!)
useFrame((_s, dt) => {
if (ringRef.current)
ringRef.current.rotation.z += dt * speed
})
return (
<mesh ref={ringRef} rotation={[tiltX, 0, 0]}>
<torusGeometry args={[radius, 0.04, 16, 200]} />
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.15} transparent opacity={0.5} />
</mesh>
)
}
@@ -0,0 +1,2 @@
export { HeroScene3d } from './HeroScene3d'
export { FeaturesScene3d } from './FeaturesScene3d'
@@ -0,0 +1,43 @@
/** Template: ThreePage
* Junta Canvas 3D (fundo) + overlay React 2D (UI).
* O overlay fica acima do canvas com pointer-events controlados.
*/
import { ReactNode, CSSProperties } from 'react'
import { SceneCanvas } from './SceneCanvas'
function overlayStyle: CSSProperties = {
position : 'absolute',
top : 0,
left : 0,
width : '100%',
height : '100%',
pointerEvents: 'none',
zIndex : 10,
}
interface PageProps {
canvasChildren : ReactNode // dentro do Canvas (3D)
overlayChildren: ReactNode // overlay 2D
orbitControls ?: boolean
scrollPages ?: number
}
export function ThreePage({
canvasChildren,
overlayChildren,
orbitControls = false,
scrollPages = 6,
}: PageProps) {
return (
<main style={{ position: 'relative', width: '100%', minHeight: '100vh', overflow: 'hidden' }}>
<SceneCanvas orbitControls={scrollPages} scrollPages={scrollPages}>
{canvasChildren}
</SceneCanvas>
{/* Overlay 2D — pointer-events:auto só nos elementos interativos */}
<div style={overlayStyle}>
{overlayChildren}
</div>
</main>
)
}
@@ -0,0 +1,46 @@
/** Template: SceneCanvas
* Canvas React Three Fiber — orquestra cena, câmera e controles.
* Aceita children como organisms/atoms 3D.
*/
import { Canvas } from '@react-three/fiber'
import { OrbitControls, ScrollControls, Environment, Float } from '@react-three/drei'
import { ReactNode } from 'react'
interface SceneCanvasProps {
children: ReactNode
orbitControls?: boolean
scrollPages?: number
scrollDamping?: number
environment?: string | null
}
export function SceneCanvas({
children,
orbitControls = false,
scrollPages = 5,
scrollDamping = 0.4,
environment = null,
}: SceneCanvasProps) {
return (
<Canvas
shadows
camera={{ fov: 60, near: 0.1, far: 100 }}
gl={{ antialias: true, alpha: false }}
dpr={[1, 2]}
>
{/* Cinemática de cor */}
<color attach="background" args={['#050510']} />
{/* Névoa para profundidade */}
<fog attach="fog" args={['#050510', 8, 40]} />
{environment && <Environment preset={environment} background />}
{orbitControls && <OrbitControls enablePan={false} enableZoom={false} maxPolarAngle={Math.PI / 1.8} />}
{/* Scroll driver — children recebem offset do scroll */}
<ScrollControls pages={scrollPages} damping={scrollDamping}>
{children}
</ScrollControls>
</Canvas>
)
}