Nexus Inovação | Superfície 3D
:root {
–color-primary: #00f0ff;
–color-secondary: #a855f7;
–color-bg: #03030c;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
cursor: none !important; /* Oculta o cursor nativo em toda a página */
}
body {
background-color: var(–color-bg);
background-image: radial-gradient(circle at 50% 50%, rgba(15, 7, 34, 0.6) 0%, rgba(3, 3, 12, 1) 100%), url(‘innovation_bg.png’);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
min-height: 100vh;
overflow: hidden;
position: relative;
}
/* Custom Cursor */
.custom-cursor {
width: 30px;
height: 30px;
border: 2px solid var(–color-primary);
border-radius: 50%;
position: fixed;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
transition: width 0.2s, height 0.2s, background-color 0.2s, border-color 0.2s;
mix-blend-mode: difference;
}
.custom-cursor-dot {
width: 6px;
height: 6px;
background-color: var(–color-primary);
border-radius: 50%;
position: fixed;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
}
#webgl-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
/* Flash de luz branca transição */
#flash-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #ffffff;
opacity: 0;
pointer-events: none;
z-index: 10000;
transition: opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
#flash-overlay.active {
opacity: 1;
}
// — 1. CONFIGURAÇÃO DO CURSOR PERSONALIZADO —
const cursor = document.getElementById(‘custom-cursor’);
const cursorDot = document.getElementById(‘custom-cursor-dot’);
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
let cursorX = mouseX;
let cursorY = mouseY;
let dotX = mouseX;
let dotY = mouseY;
window.addEventListener(‘mousemove’, (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
function animateCursor() {
cursorX += (mouseX – cursorX) * 0.15;
cursorY += (mouseY – cursorY) * 0.15;
cursor.style.left = `${cursorX}px`;
cursor.style.top = `${cursorY}px`;
dotX += (mouseX – dotX) * 0.35;
dotY += (mouseY – dotY) * 0.35;
cursorDot.style.left = `${dotX}px`;
cursorDot.style.top = `${dotY}px`;
requestAnimationFrame(animateCursor);
}
animateCursor();
// — 2. CONFIGURAÇÃO DA CENA THREE.JS —
const canvas = document.getElementById(‘webgl-canvas’);
const clock = new THREE.Clock();
const mouse3D = new THREE.Vector2(0, 0);
window.addEventListener(‘mousemove’, (e) => {
mouse3D.x = (e.clientX / window.innerWidth) * 2 – 1;
mouse3D.y = -(e.clientY / window.innerHeight) * 2 + 1;
});
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.z = 8;
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true,
powerPreference: “high-performance”
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
const textureLoader = new THREE.TextureLoader();
let bgMesh = null;
let glassMesh = null;
let mouseLight = null;
let textMeshes = [];
let isTransitioning = false;
let activeTextures = {};
const raycaster = new THREE.Raycaster();
// Fallback/Geração de textura de fundo em caso de erro ou file://
function createProceduralTexture() {
const canvas = document.createElement(‘canvas’);
canvas.width = 1024;
canvas.height = 1024;
const ctx = canvas.getContext(‘2d’);
const grad = ctx.createRadialGradient(512, 512, 50, 512, 512, 700);
grad.addColorStop(0, ‘#12042b’);
grad.addColorStop(0.4, ‘#030514’);
grad.addColorStop(1, ‘#010103’);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 1024, 1024);
ctx.strokeStyle = ‘rgba(0, 240, 255, 0.12)’;
ctx.lineWidth = 1.5;
const points = [];
for (let i = 0; i 0.6
});
}
ctx.beginPath();
for (let i = 0; i < points.length; i++) {
for (let j = i + 1; j < points.length; j++) {
const dx = points[i].x – points[j].x;
const dy = points[i].y – points[j].y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 260) {
ctx.moveTo(points[i].x, points[i].y);
ctx.lineTo(points[j].x, points[j].y);
}
}
}
ctx.stroke();
for (let i = 0; i 930 && fontSize > 40) {
fontSize -= 4;
ctx.font = `bold ${fontSize}px “Saira Extra Condensed”, sans-serif`;
}
// 1. Degradê de preenchimento (Azul Glacial Cristalino de alto contraste)
const fillGrad = ctx.createLinearGradient(0, 30, 0, 220);
fillGrad.addColorStop(0, ‘#ffffff’); // Brilho superior reflexivo
fillGrad.addColorStop(0.35, ‘#cbf3ff’); // Azul gelo claro
fillGrad.addColorStop(0.7, ‘#00ddff’); // Ciano neon brilhante
fillGrad.addColorStop(1, ‘#004c99’); // Tom azul escuro profundo na base
ctx.fillStyle = fillGrad;
// Sombra suave interna para dar volume às letras individuais
ctx.shadowColor = ‘rgba(0, 240, 255, 0.45)’;
ctx.shadowBlur = 14;
ctx.shadowOffsetY = 4;
ctx.fillText(text, 512, 128);
// Reseta sombra para manter os contornos das bordas nítidos
ctx.shadowColor = ‘transparent’;
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
// 2. Contorno do vidro (Simula chanfragem reflexiva)
// Contorno ciano externo médio
ctx.strokeStyle = ‘rgba(0, 240, 255, 0.45)’;
ctx.lineWidth = 5;
ctx.strokeText(text, 512, 128);
// Contorno branco interno muito fino
ctx.strokeStyle = ‘rgba(255, 255, 255, 0.8)’;
ctx.lineWidth = 1.5;
ctx.strokeText(text, 512, 128);
const texture = new THREE.CanvasTexture(canvas);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
return texture;
}
// Carrega assets necessários (somente o background)
function loadAssets(callback) {
let loadedCount = 0;
const totalAssets = 1;
const assets = {};
function checkLoaded() {
loadedCount++;
if (loadedCount === totalAssets) {
callback(assets);
}
}
// Background
const isLocalFile = window.location.protocol === ‘file:’;
if (isLocalFile) {
assets.bg = createProceduralTexture();
checkLoaded();
} else {
textureLoader.load(‘innovation_bg.png’,
(tex) => { assets.bg = tex; checkLoaded(); },
undefined,
() => {
assets.bg = createProceduralTexture();
checkLoaded();
}
);
}
}
// Garante carregamento da fonte antes de iniciar o Three.js
document.fonts.ready.then(() => {
loadAssets((loadedAssets) => {
activeTextures = loadedAssets;
initScene();
});
});
// — 4. INICIALIZAÇÃO DOS ELEMENTOS DA CENA —
let dirLight1, dirLight2, ambientLight;
function initScene() {
activeTextures.bg.wrapS = THREE.ClampToEdgeWrapping;
activeTextures.bg.wrapT = THREE.ClampToEdgeWrapping;
activeTextures.bg.minFilter = THREE.LinearFilter;
// Fundo
const bgGeometry = new THREE.PlaneGeometry(60, 40);
const bgMaterial = new THREE.MeshBasicMaterial({ map: activeTextures.bg });
bgMesh = new THREE.Mesh(bgGeometry, bgMaterial);
bgMesh.position.z = -5;
scene.add(bgMesh);
// ILUMINAÇÃO
ambientLight = new THREE.AmbientLight(0x0e0e22, 1.8);
scene.add(ambientLight);
dirLight1 = new THREE.DirectionalLight(0x00f0ff, 2.5);
dirLight1.position.set(-8, 8, 6);
scene.add(dirLight1);
dirLight2 = new THREE.DirectionalLight(0xa855f7, 2.0);
dirLight2.position.set(8, -8, 6);
scene.add(dirLight2);
mouseLight = new THREE.PointLight(0xffffff, 5.0, 18);
mouseLight.position.set(0, 0, 2.5);
scene.add(mouseLight);
// Dados de posicionamento e links das palavras (Coordenadas ajustadas para evitar sobreposição)
const wordsData = [
{ key: ‘parcerias’, text: ‘Parcerias’, url: ‘parcerias.html’, x: -4.0, y: 2.0, z: -2, baseScale: 1.45 },
{ key: ‘empreender’, text: ‘Empreendedorismo’, url: ‘empreender.html’, x: 3.7, y: 1.8, z: -2.2, baseScale: 1.3 },
{ key: ‘spei’, text: ‘SPEI’, url: null, x: 0.0, y: 0.0, z: -2.1, baseScale: 1.4 }, // Sem link!
{ key: ‘inovacao’, text: ‘Inovação’, url: ‘inovacao.html’, x: -3.7, y: -2.0, z: -2.2, baseScale: 1.5 },
{ key: ‘noticias’, text: ‘Notícias’, url: ‘noticias.html’, x: 4.0, y: -1.8, z: -2, baseScale: 1.45 }
];
wordsData.forEach(data => {
// Desenha a textura dos caracteres da palavra
const texture = createWordTexture(data.text);
// Proporção baseada no canvas da palavra (1024/256 = 4.0)
const aspect = 4.0;
// Alturas significativamente aumentadas para maior tamanho na tela
let textHeight = 1.05;
if (data.key === ‘empreender’) {
textHeight = 1.4; // Proporção maior para “Empreendedorismo” não ficar comprimida
} else if (data.key === ‘spei’) {
textHeight = 1.2; // Altura ideal de destaque central para o SPEI
}
const geometry = new THREE.PlaneGeometry(aspect * textHeight, textHeight);
// Material Frontal (Crispado, Degradê de alto contraste)
const frontMaterial = new THREE.MeshPhysicalMaterial({
map: texture,
transparent: true,
opacity: 1.0,
transmission: 0.0, // Sem transparência para manter contraste
roughness: 0.08,
metalness: 0.05,
ior: 1.5,
clearcoat: 1.0,
clearcoatRoughness: 0.05,
emissive: new THREE.Color(0x000000),
emissiveIntensity: 0.0,
depthWrite: false
});
// Material da Extrusão (Cria profundidade escura nas laterais)
const extrusionMaterial = new THREE.MeshPhysicalMaterial({
map: texture,
transparent: true,
opacity: 0.85,
transmission: 0.0,
color: new THREE.Color(0x1a3a66),
roughness: 0.25,
metalness: 0.1,
clearcoat: 0.5,
clearcoatRoughness: 0.2,
emissive: new THREE.Color(0x000000),
emissiveIntensity: 0.0,
depthWrite: false
});
// Empilhamento Volumétrico (26 camadas) para criar o corpo 3D dos caracteres
const wordGroup = new THREE.Group();
wordGroup.position.set(data.x, data.y, data.z);
const layersCount = 26;
const layerSpacing = 0.024; // Espessura do volume tridimensional
let frontMesh = null;
for (let i = 0; i textureAspect) {
bgTexture.repeat.set(1, textureAspect / screenAspect);
bgTexture.offset.set(0, (1 – textureAspect / screenAspect) / 2);
} else {
bgTexture.repeat.set(screenAspect / textureAspect, 1);
bgTexture.offset.set((1 – screenAspect / textureAspect) / 2, 0);
}
}
}
window.addEventListener(‘resize’, resize);
// — 6. EVENTO DE CLIQUE (TRANSIÇÃO) —
window.addEventListener(‘click’, () => {
if (isTransitioning) return;
raycaster.setFromCamera(mouse3D, camera);
const intersects = raycaster.intersectObjects(textMeshes);
if (intersects.length > 0) {
const clickedMesh = intersects[0].object;
// Se a palavra não tiver URL associada (como o SPEI central), ignore o clique
if (!clickedMesh.targetUrl) return;
isTransitioning = true;
// Ativa flash branco CSS
const flashOverlay = document.getElementById(‘flash-overlay’);
flashOverlay.classList.add(‘active’);
// Anima luzes e câmera
const startT = clock.getElapsedTime();
function flashLoop() {
const elapsed = clock.getElapsedTime() – startT;
if (elapsed {
window.location.href = clickedMesh.targetUrl;
}, 800);
}
});
// — 7. LOOP DE ANIMAÇÃO (RENDER) —
function animate() {
requestAnimationFrame(animate);
const time = clock.getElapsedTime();
// Interatividade do mouse na luz de ponto
if (mouseLight && camera && !isTransitioning) {
const vector = new THREE.Vector3(mouse3D.x, mouse3D.y, 0.5);
vector.unproject(camera);
const dir = vector.sub(camera.position).normalize();
const distance = -camera.position.z / dir.z;
const targetPos = camera.position.clone().add(dir.multiplyScalar(distance));
mouseLight.position.x += (targetPos.x – mouseLight.position.x) * 0.12;
mouseLight.position.y += (targetPos.y – mouseLight.position.y) * 0.12;
mouseLight.position.z = 2.0;
}
// Raycasting para Hover
if (!isTransitioning) {
raycaster.setFromCamera(mouse3D, camera);
const intersects = raycaster.intersectObjects(textMeshes);
let hoveredObj = null;
if (intersects.length > 0) {
hoveredObj = intersects[0].object;
}
// Efeito no cursor personalizado (funciona também no SPEI, pois é interativo e oscila)
if (hoveredObj) {
cursor.style.width = ’60px’;
cursor.style.height = ’60px’;
// Se não for clicável (SPEI), usa uma cor de cursor diferente (ex: ciano original) em vez de roxo
if (!hoveredObj.targetUrl) {
cursor.style.borderColor = ‘var(–color-primary)’;
cursor.style.backgroundColor = ‘rgba(0, 240, 255, 0.08)’;
} else {
cursor.style.borderColor = ‘var(–color-secondary)’;
cursor.style.backgroundColor = ‘rgba(168, 85, 247, 0.12)’;
}
} else {
cursor.style.width = ’30px’;
cursor.style.height = ’30px’;
cursor.style.borderColor = ‘var(–color-primary)’;
cursor.style.backgroundColor = ‘transparent’;
}
// Interpolação suave de foco e emissivo
textMeshes.forEach(mesh => {
if (mesh === hoveredObj) {
mesh.isHovered = true;
// Valores Alvo para Hover (Brilho ciano forte na frente)
mesh.targetEmissiveIntensity = 0.9;
mesh.targetEmissiveColor = new THREE.Color(0x00f0ff);
mesh.targetScale = mesh.scaleFactor * 1.12;
} else {
mesh.isHovered = false;
// Valores Alvo Padrão (Sem brilho para manter contraste)
mesh.targetEmissiveIntensity = 0.0;
mesh.targetEmissiveColor = new THREE.Color(0x000000);
mesh.targetScale = mesh.scaleFactor;
}
// Interpolação aplicada unicamente ao material da frente (frontMaterial)
mesh.frontMaterial.emissiveIntensity += (mesh.targetEmissiveIntensity – mesh.frontMaterial.emissiveIntensity) * 0.15;
mesh.frontMaterial.emissive.lerp(mesh.targetEmissiveColor, 0.15);
mesh.frontMaterial.needsUpdate = true;
// Escala tridimensional do grupo
const group = mesh.wordGroup;
const s = group.scale.x + (mesh.targetScale – group.scale.x) * 0.15;
group.scale.set(s, s, 1);
});
}
// Flutuação 3D e rotação reativa com oscilação no hover
textMeshes.forEach(mesh => {
const group = mesh.wordGroup;
group.position.y = mesh.initialY + Math.sin(time * 0.8 + mesh.phase) * 0.15;
group.position.x = mesh.initialX + Math.cos(time * 0.4 + mesh.phase) * 0.1;
group.rotation.z = Math.sin(time * 0.3 + mesh.phase) * 0.04;
if (!isTransitioning) {
// Interpolação suave do progresso do hover
if (mesh.isHovered) {
mesh.hoverProgress += (1.0 – mesh.hoverProgress) * 0.10;
} else {
mesh.hoverProgress += (0.0 – mesh.hoverProgress) * 0.10;
}
// Rotação padrão (acompanha o mouse estaticamente)
const standardRotY = Math.sin(time * 0.1 + mesh.phase) * 0.05 + mouse3D.x * 0.32;
const standardRotX = mouse3D.y * 0.25;
// Rotação oscilante de hover (oscila de um lado para o outro revelando o volume 3D)
const swingRotY = Math.sin(time * 5.5) * 0.30 + mouse3D.x * 0.15;
const swingRotX = Math.cos(time * 3.5) * 0.12 + mouse3D.y * 0.15;
// Mistura as rotações de forma suave usando lerp
group.rotation.y = THREE.MathUtils.lerp(standardRotY, swingRotY, mesh.hoverProgress);
group.rotation.x = THREE.MathUtils.lerp(standardRotX, swingRotX, mesh.hoverProgress);
}
});
// Deforma a malha de vidro frontal para gerar refrações
if (glassMesh) {
const geometry = glassMesh.geometry;
const position = geometry.attributes.position;
for (let i = 0; i < position.count; i++) {
const x = position.getX(i);
const y = position.getY(i);
const z1 = Math.sin(x * 0.2 + time * 0.7) * Math.cos(y * 0.2 + time * 0.7) * 0.45;
const z2 = Math.sin(x * 0.45 – time * 0.5) * Math.cos(y * 0.35 + time * 0.4) * 0.2;
const z3 = Math.cos(x * 0.1 + y * 0.1 + time * 0.3) * 0.3;
const dx = x – mouseLight.position.x;
const dy = y – mouseLight.position.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const mouseGlowEffect = Math.sin(dist * 0.7 – time * 2.5) * Math.exp(-dist * 0.12) * 0.35;
position.setZ(i, z1 + z2 + z3 + mouseGlowEffect);
}
geometry.computeVertexNormals();
position.needsUpdate = true;
glassMesh.rotation.y = Math.sin(time * 0.05) * 0.04;
glassMesh.rotation.x = Math.cos(time * 0.05) * 0.02;
}
renderer.render(scene, camera);
}