597 lines
27 KiB
HTML
597 lines
27 KiB
HTML
<!doctype html>
|
||
<html lang="es">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>NeonVerse — Warm & Playful (1‑file)</title>
|
||
<meta name="description" content="Warm glass UI over a Three.js galaxy background with playful actions. One single HTML file with embedded CSS & JS." />
|
||
|
||
<!-- Google Font -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||
|
||
<style>
|
||
/* =============================================
|
||
Warm, modern, glass + neon (sunset inspired)
|
||
============================================= */
|
||
:root {
|
||
/* Warm theme variables */
|
||
--bg: #0f0b09; /* deep warm charcoal */
|
||
--text: #fff7f2; /* soft ivory */
|
||
--muted: #ffd3b6; /* peachy muted */
|
||
--accent: #ff9d5c; /* tangerine */
|
||
--accent-2: #ff6b6b; /* coral */
|
||
--accent-3: #f7b267; /* apricot */
|
||
--glass: rgba(255, 236, 222, 0.08);
|
||
--glass-2: rgba(255, 236, 222, 0.14);
|
||
--blur: 18px;
|
||
--card-radius: 20px;
|
||
--shadow: 0 10px 40px rgba(0,0,0,.35);
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
html, body { height: 100%; }
|
||
body {
|
||
margin: 0;
|
||
font-family: 'Outfit', system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, 'Apple Color Emoji', 'Segoe UI Emoji';
|
||
color: var(--text);
|
||
background: radial-gradient(1200px 600px at 20% 20%, #2a1712 0%, #120b09 60%), var(--bg);
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* Fullscreen WebGL background */
|
||
#bg-canvas { position: fixed; inset: 0; width: 100vw; height: 100vh; z-index: -2; display: block; }
|
||
|
||
/* Subtle scanline overlay */
|
||
.scanlines {
|
||
pointer-events: none; position: fixed; inset: 0; z-index: -1;
|
||
background-image: repeating-linear-gradient(
|
||
to bottom,
|
||
rgba(255,255,255,0.04),
|
||
rgba(255,255,255,0.04) 1px,
|
||
transparent 1px,
|
||
transparent 3px
|
||
);
|
||
mix-blend-mode: soft-light;
|
||
}
|
||
|
||
header {
|
||
max-width: 1200px; margin: 0 auto; padding: 56px 24px 20px;
|
||
display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
||
}
|
||
|
||
.brand { display: flex; align-items: center; gap: 14px; text-decoration: none; color: var(--text); }
|
||
.brand .logo {
|
||
width: 44px; height: 44px; border-radius: 12px;
|
||
background: conic-gradient(from 210deg, var(--accent), var(--accent-2), #ffd166, var(--accent));
|
||
box-shadow: 0 0 30px rgba(255, 157, 92, .45);
|
||
position: relative; isolation: isolate;
|
||
}
|
||
.brand .logo::after {
|
||
content: ""; position: absolute; inset: 2px; border-radius: 10px;
|
||
background: radial-gradient(120px 120px at 30% 30%, rgba(255,255,255,.35), transparent 60%);
|
||
filter: blur(8px); z-index: -1;
|
||
}
|
||
.brand h1 { font-size: clamp(20px, 2.4vw, 28px); margin: 0; letter-spacing: 0.4px; }
|
||
.brand span { display: block; font-weight: 300; font-size: 12px; color: var(--muted); margin-top: -4px; }
|
||
|
||
.controls { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||
|
||
.btn {
|
||
--glow: var(--accent);
|
||
border: 1px solid rgba(255,255,255,.15);
|
||
color: var(--text);
|
||
background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02));
|
||
padding: 12px 16px; border-radius: 12px; cursor: pointer;
|
||
backdrop-filter: blur(var(--blur));
|
||
box-shadow: var(--shadow), 0 0 0 0 rgba(255,157,92,.45) inset;
|
||
transition: box-shadow .3s ease, transform .15s ease;
|
||
font-weight: 600; letter-spacing: .3px;
|
||
}
|
||
.btn:hover { transform: translateY(-1px); box-shadow: var(--shadow), 0 0 0 2px rgba(255,157,92,.45) inset; }
|
||
.btn:active { transform: translateY(0); }
|
||
|
||
.btn-primary {
|
||
--glow: var(--accent-2);
|
||
border-color: rgba(255,107,107,.35);
|
||
box-shadow: var(--shadow), 0 0 0 0 rgba(255,107,107,.45) inset;
|
||
}
|
||
.btn-primary:hover { box-shadow: var(--shadow), 0 0 0 2px rgba(255,107,107,.45) inset; }
|
||
|
||
.toggle {
|
||
display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
|
||
user-select: none; padding: 10px 12px; border-radius: 12px;
|
||
border: 1px solid rgba(255,255,255,.12);
|
||
background: rgba(255,255,255,.04);
|
||
backdrop-filter: blur(12px);
|
||
}
|
||
.toggle input { display: none; }
|
||
.toggle .dot {
|
||
width: 24px; height: 24px; border-radius: 999px; position: relative;
|
||
background: radial-gradient(circle at 30% 30%, #fff, #ffe9d6 35%, #ffc9a9 70%, #ff9d5c);
|
||
box-shadow: 0 0 16px rgba(255,157,92,.45);
|
||
transition: transform .3s ease;
|
||
}
|
||
.toggle input:checked + .dot { transform: translateX(4px) rotate(16deg) scale(1.02); }
|
||
|
||
.slider-wrap { display:flex; align-items:center; gap:10px; padding: 8px 10px; border-radius: 12px; border:1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.04); backdrop-filter: blur(12px); }
|
||
.slider-wrap label { font-size: 12px; color: var(--muted); }
|
||
.slider-wrap input[type="range"] { accent-color: var(--accent); }
|
||
|
||
main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 80px; }
|
||
|
||
.hero { display: grid; grid-template-columns: 1.1fr 1fr; gap: clamp(18px, 4vw, 40px); align-items: center; }
|
||
@media (max-width: 900px) { .hero { grid-template-columns: 1fr; } }
|
||
|
||
.hero h2 { font-size: clamp(32px, 6vw, 68px); line-height: 1.02; margin: 8px 0 10px; font-weight: 800; text-shadow: 0 10px 50px rgba(255,157,92,.25); }
|
||
.hero p { color: var(--muted); font-size: clamp(14px, 1.8vw, 18px); margin: 0 0 20px; }
|
||
|
||
.cta { display: flex; gap: 12px; flex-wrap: wrap; }
|
||
|
||
.card {
|
||
border: 1px solid rgba(255,255,255,.12);
|
||
background: linear-gradient(180deg, var(--glass), rgba(255,255,255,0.05));
|
||
backdrop-filter: blur(var(--blur));
|
||
border-radius: var(--card-radius);
|
||
box-shadow: var(--shadow);
|
||
padding: clamp(14px, 2.6vw, 24px);
|
||
transform-style: preserve-3d; perspective: 1200px;
|
||
transition: transform .2s ease, box-shadow .35s ease;
|
||
}
|
||
.card:hover { transform: translateY(-2px) rotateX(1deg) rotateY(-1deg); box-shadow: 0 20px 60px rgba(0,0,0,.45); }
|
||
.card h3 { margin: 0 0 8px; font-size: clamp(18px, 2.4vw, 22px); }
|
||
.card p { margin: 0; color: var(--muted); }
|
||
|
||
.grid { margin-top: 28px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
||
@media (max-width: 1000px) { .grid { grid-template-columns: repeat(2,1fr); } }
|
||
@media (max-width: 640px) { .grid { grid-template-columns: 1fr; } }
|
||
|
||
.icon {
|
||
width: 42px; height: 42px; border-radius: 12px;
|
||
display: grid; place-items: center; margin-bottom: 10px;
|
||
background: radial-gradient(120px 60px at 30% 20%, rgba(255,255,255,.35), transparent 60%),
|
||
linear-gradient(180deg, rgba(255,157,92,.25), rgba(255,107,107,.25));
|
||
border: 1px solid rgba(255,255,255,.18);
|
||
box-shadow: 0 6px 22px rgba(255,157,92,.30);
|
||
}
|
||
|
||
footer { opacity: .8; text-align: center; padding: 30px 0 40px; font-size: 13px; color: var(--muted); }
|
||
|
||
/* Decorative blurred blobs */
|
||
.blob { position: fixed; filter: blur(60px); opacity: .35; z-index: -1; pointer-events: none; width: 45vmax; height: 45vmax; border-radius: 999px; mix-blend-mode: screen; }
|
||
.blob.one { top: -10vmax; left: -10vmax; background: radial-gradient(circle at 30% 30%, rgba(255,157,92,.6), transparent 60%), radial-gradient(circle at 70% 70%, rgba(255,107,107,.6), transparent 60%); }
|
||
.blob.two { bottom: -15vmax; right: -10vmax; background: radial-gradient(circle at 30% 30%, rgba(247,178,103,.6), transparent 60%), radial-gradient(circle at 70% 70%, rgba(255,214,102,.6), transparent 60%); }
|
||
|
||
/* "Hyperdrive" visual feedback */
|
||
.hyper-outline { outline: 2px solid rgba(255,157,92,.0); outline-offset: 0; transition: outline-color .25s ease, outline-offset .25s ease; }
|
||
.hyper-outline.active { outline-color: rgba(255,157,92,.6); outline-offset: 8px; }
|
||
|
||
/* Hide the noscript hint unless JS is disabled */
|
||
noscript { position: fixed; inset: 0; background: #2b120e; color: #fff; display: grid; place-items: center; z-index: 999; padding: 32px; text-align: center; }
|
||
|
||
/* Utility */
|
||
.sep { width:1px; height:36px; background: rgba(255,255,255,.12); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="bg-canvas"></canvas>
|
||
<div class="scanlines"></div>
|
||
<div class="blob one"></div>
|
||
<div class="blob two"></div>
|
||
|
||
<header>
|
||
<a class="brand" href="#" aria-label="Inicio NeonVerse">
|
||
<div class="logo" aria-hidden="true"></div>
|
||
<div>
|
||
<h1>NeonVerse</h1>
|
||
<span>Un archivo · Todo el espectáculo</span>
|
||
</div>
|
||
</a>
|
||
|
||
<div class="controls" role="toolbar" aria-label="Controles de escena">
|
||
<label class="toggle" title="Modo Calma / Hiper">
|
||
<input id="modeToggle" type="checkbox" />
|
||
<div class="dot" aria-hidden="true"></div>
|
||
<span>Calma</span>
|
||
</label>
|
||
<div class="sep" aria-hidden="true"></div>
|
||
<button id="btnHyper" class="btn btn-primary" aria-pressed="false">Hiperimpulso ⚡️</button>
|
||
<button id="btnReset" class="btn">Reiniciar</button>
|
||
<button id="btnFocus" class="btn" title="Enfocar núcleo">Enfocar Núcleo</button>
|
||
<button id="btnComet" class="btn" title="Lanzar cometa">Cometa ✨</button>
|
||
<button id="btnAuto" class="btn" aria-pressed="false" title="Auto-órbita">Auto‑órbita</button>
|
||
<button id="btnScreenshot" class="btn" title="Capturar imagen">Capturar 📸</button>
|
||
<div class="slider-wrap" title="Ajusta la calidez global">
|
||
<label for="warmth">Calidez</label>
|
||
<input id="warmth" type="range" min="0" max="100" value="70" />
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<section class="hero card hyper-outline" id="hero">
|
||
<div>
|
||
<h2>Futuro en una sola página</h2>
|
||
<p>Fondo 3D reactivo con <strong>Three.js</strong>, animaciones fluidas con <strong>GSAP</strong> y una UI de cristal con tonos cálidos. Todo en este único archivo HTML.</p>
|
||
<div class="cta">
|
||
<button class="btn btn-primary" id="btnPulse">Pulso Neón</button>
|
||
<button class="btn" id="btnParticles">Explosión de Estrellas</button>
|
||
<button class="btn" id="btnPalette">Aleatorizar Paleta</button>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="min-height: 220px; display:flex; align-items:center; justify-content:center;">
|
||
<svg width="180" height="160" viewBox="0 0 180 160" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||
<defs>
|
||
<linearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
|
||
<stop offset="0%" stop-color="#ff9d5c"/>
|
||
<stop offset="100%" stop-color="#ff6b6b"/>
|
||
</linearGradient>
|
||
</defs>
|
||
<g opacity="0.95" stroke="url(#grad)" stroke-width="2">
|
||
<path d="M20 120 C 60 40, 120 40, 160 120"/>
|
||
<circle cx="90" cy="80" r="36"/>
|
||
<circle cx="90" cy="80" r="56" opacity=".45"/>
|
||
<circle cx="90" cy="80" r="70" opacity=".2"/>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="grid">
|
||
<article class="card">
|
||
<div class="icon">⚙️</div>
|
||
<h3>Interacción en tiempo real</h3>
|
||
<p>El campo de estrellas responde al ratón, a la calidez global y a la velocidad del modo <em>Hiper</em>.</p>
|
||
</article>
|
||
<article class="card">
|
||
<div class="icon">🧪</div>
|
||
<h3>Una sola dependencia</h3>
|
||
<p>Three.js para el 3D y GSAP para animar. Nada más. Ligero y espectacular.</p>
|
||
</article>
|
||
<article class="card">
|
||
<div class="icon">🎛️</div>
|
||
<h3>Controles extra</h3>
|
||
<p>Cometa, auto‑órbita, captura de pantalla, enfoque al núcleo y paletas aleatorias.</p>
|
||
</article>
|
||
</section>
|
||
|
||
<footer>
|
||
Hecho con ♥︎ · Three.js · GSAP · HTML/CSS · En un solo archivo · Paleta cálida
|
||
</footer>
|
||
</main>
|
||
|
||
<noscript>
|
||
<div>
|
||
<h2>Necesitas JavaScript</h2>
|
||
<p>Esta demo usa WebGL y animaciones. Activa JavaScript para verla en acción ✨</p>
|
||
</div>
|
||
</noscript>
|
||
|
||
<!-- External JS libraries (CDN). Still a single HTML file; no separate JS/CSS files used. -->
|
||
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
|
||
<script src="https://unpkg.com/gsap@3.12.5/dist/gsap.min.js"></script>
|
||
|
||
<script>
|
||
// =============================================
|
||
// NeonVerse — Warm + Extra Actions (single file)
|
||
// (c) You — Free to tweak
|
||
// Comments in EN as requested
|
||
// =============================================
|
||
|
||
// ---- Three.js setup ----
|
||
const canvas = document.getElementById('bg-canvas');
|
||
const scene = new THREE.Scene();
|
||
|
||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, preserveDrawingBuffer: true /* enable screenshot */ });
|
||
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
|
||
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 3000);
|
||
camera.position.set(0, 0, 140);
|
||
|
||
// Starfield parameters
|
||
const STAR_COUNT = 3200; // tiny bump
|
||
const galaxy = new THREE.Group();
|
||
|
||
// Attributes
|
||
const positions = new Float32Array(STAR_COUNT * 3);
|
||
const colors = new Float32Array(STAR_COUNT * 3);
|
||
const baseHues = { warmA: 0.06 /* ~tangerine */ , warmB: 0.02 /* amber */ , warmC: 0.98 /* red wrap */ };
|
||
|
||
const color = new THREE.Color(); // helper
|
||
|
||
// Build spiral galaxy-like distribution
|
||
for (let i = 0; i < STAR_COUNT; i++) {
|
||
const radius = Math.pow(Math.random(), 0.6) * 380 + 20;
|
||
const arms = 3;
|
||
const branchAngle = ((i % arms) / arms) * Math.PI * 2;
|
||
const spin = radius * 0.035;
|
||
const randomness = (Math.random() - 0.5) * 28;
|
||
|
||
const angle = branchAngle + spin;
|
||
const x = Math.cos(angle) * radius + randomness;
|
||
const y = (Math.random() - 0.5) * 24 + Math.sin(angle * 3) * 3;
|
||
const z = Math.sin(angle) * radius + randomness;
|
||
|
||
const idx = i * 3;
|
||
positions[idx] = x; positions[idx + 1] = y; positions[idx + 2] = z;
|
||
|
||
// Warm gradient across radius (tangerine -> coral -> soft red)
|
||
const t = radius / 420;
|
||
const hue = baseHues.warmA + 0.15 * (1 - t); // small drift
|
||
color.setHSL(hue % 1, 0.85, 0.60);
|
||
colors[idx] = color.r; colors[idx + 1] = color.g; colors[idx + 2] = color.b;
|
||
}
|
||
|
||
const geometry = new THREE.BufferGeometry();
|
||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
||
|
||
const material = new THREE.PointsMaterial({
|
||
size: 1.8,
|
||
vertexColors: true,
|
||
transparent: true,
|
||
opacity: 0.95,
|
||
depthWrite: false,
|
||
blending: THREE.AdditiveBlending
|
||
});
|
||
|
||
const points = new THREE.Points(geometry, material);
|
||
galaxy.add(points);
|
||
scene.add(galaxy);
|
||
|
||
// Subtle core glow (sprite)
|
||
const coreTexture = new THREE.CanvasTexture(generateRadialGlow(256));
|
||
const coreMaterial = new THREE.SpriteMaterial({ map: coreTexture, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, opacity: 0.9 });
|
||
const core = new THREE.Sprite(coreMaterial);
|
||
core.scale.set(120, 120, 1);
|
||
scene.add(core);
|
||
|
||
function generateRadialGlow(size) {
|
||
// Create a radial gradient canvas for the galaxy core (warm)
|
||
const c = document.createElement('canvas');
|
||
c.width = c.height = size;
|
||
const ctx = c.getContext('2d');
|
||
const g = ctx.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
|
||
g.addColorStop(0, 'rgba(255,245,235,0.95)');
|
||
g.addColorStop(0.25, 'rgba(255,157,92,0.85)');
|
||
g.addColorStop(0.55, 'rgba(255,107,107,0.55)');
|
||
g.addColorStop(1, 'rgba(0,0,0,0)');
|
||
ctx.fillStyle = g; ctx.fillRect(0,0,size,size);
|
||
return c;
|
||
}
|
||
|
||
// Interaction state
|
||
const state = {
|
||
rotSpeed: 0.0026,
|
||
hyper: false,
|
||
parallaxX: 0,
|
||
parallaxY: 0,
|
||
autoOrbit: false,
|
||
warmth: 0.7 // 0..1
|
||
};
|
||
|
||
// Parallax by mouse/touch
|
||
const lerp = (a, b, t) => a + (b - a) * t;
|
||
let targetX = 0, targetY = 0;
|
||
window.addEventListener('pointermove', (e) => {
|
||
const nx = (e.clientX / window.innerWidth) * 2 - 1;
|
||
const ny = (e.clientY / window.innerHeight) * 2 - 1;
|
||
targetX = nx * 0.6;
|
||
targetY = ny * 0.4;
|
||
}, { passive: true });
|
||
|
||
// Animation loop
|
||
const clock = new THREE.Clock();
|
||
function animate() {
|
||
const dt = clock.getDelta();
|
||
// Smooth parallax
|
||
state.parallaxX = lerp(state.parallaxX, targetX, 0.05);
|
||
state.parallaxY = lerp(state.parallaxY, targetY, 0.05);
|
||
camera.position.x = state.parallaxX * 20;
|
||
camera.position.y = -state.parallaxY * 12;
|
||
camera.lookAt(0,0,0);
|
||
|
||
// Galaxy rotation
|
||
galaxy.rotation.y += state.rotSpeed;
|
||
galaxy.rotation.x = lerp(galaxy.rotation.x, state.parallaxY * 0.25, 0.02);
|
||
|
||
// Auto orbit pushes slight z oscillation
|
||
if (state.autoOrbit) {
|
||
camera.position.z = 140 + Math.sin(performance.now()*0.0007)*6;
|
||
}
|
||
|
||
// Subtle material twinkle
|
||
const time = performance.now() * 0.001;
|
||
material.size = 1.6 + Math.sin(time * 1.7) * 0.2 + (state.hyper ? 0.6 : 0);
|
||
material.opacity = 0.9 + Math.sin(time * 0.9) * 0.08 + (state.hyper ? 0.05 : 0);
|
||
|
||
renderer.render(scene, camera);
|
||
requestAnimationFrame(animate);
|
||
}
|
||
animate();
|
||
|
||
// Handle resize
|
||
addEventListener('resize', () => {
|
||
camera.aspect = innerWidth / innerHeight;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setSize(innerWidth, innerHeight);
|
||
});
|
||
|
||
// ---- GSAP UI animations ----
|
||
const hero = document.getElementById('hero');
|
||
const btnHyper = document.getElementById('btnHyper');
|
||
const btnReset = document.getElementById('btnReset');
|
||
const btnPulse = document.getElementById('btnPulse');
|
||
const btnParticles = document.getElementById('btnParticles');
|
||
const btnPalette = document.getElementById('btnPalette');
|
||
const btnFocus = document.getElementById('btnFocus');
|
||
const btnComet = document.getElementById('btnComet');
|
||
const btnAuto = document.getElementById('btnAuto');
|
||
const btnScreenshot = document.getElementById('btnScreenshot');
|
||
const modeToggle = document.getElementById('modeToggle');
|
||
const warmthSlider = document.getElementById('warmth');
|
||
|
||
// Intro animation
|
||
gsap.from(['.brand', '.controls'], { y: -16, opacity: 0, duration: 0.9, ease: 'power2.out', stagger: 0.08 });
|
||
gsap.from('.hero .card', { y: 20, opacity: 0, duration: 1, ease: 'power3.out', stagger: 0.14, delay: 0.1 });
|
||
gsap.from('.grid .card', { y: 24, opacity: 0, duration: 0.9, ease: 'power3.out', stagger: 0.08, delay: 0.25 });
|
||
|
||
// Helper: pulse outline on hero card
|
||
function pulseOutline() {
|
||
hero.classList.add('active');
|
||
gsap.fromTo(hero, { outlineOffset: 0 }, { outlineOffset: 8, duration: 0.3, yoyo: true, repeat: 1, onComplete: () => hero.classList.remove('active') });
|
||
}
|
||
|
||
// ---- Actions ----
|
||
// Hyperdrive action — accelerate, cam kick, blob flash
|
||
btnHyper.addEventListener('click', () => {
|
||
state.hyper = true; btnHyper.setAttribute('aria-pressed', 'true');
|
||
pulseOutline();
|
||
gsap.to(state, { rotSpeed: 0.020, duration: 0.8, ease: 'power3.out' });
|
||
gsap.fromTo(camera.position, { z: 160 }, { z: 118, duration: 0.8, ease: 'power2.out', yoyo: true, repeat: 1 });
|
||
gsap.to('.blob', { opacity: 0.55, scale: 1.08, duration: 0.6, yoyo: true, repeat: 1, ease: 'sine.inOut' });
|
||
});
|
||
|
||
// Reset to calm
|
||
btnReset.addEventListener('click', () => {
|
||
state.hyper = false; btnHyper.setAttribute('aria-pressed', 'false');
|
||
gsap.to(state, { rotSpeed: 0.0026, duration: 1.2, ease: 'power3.inOut' });
|
||
gsap.to(camera.position, { x: 0, y: 0, z: 140, duration: 0.9, ease: 'power2.inOut' });
|
||
gsap.to('.blob', { opacity: 0.35, duration: 0.8 });
|
||
});
|
||
|
||
// Neon pulse — UI + core glow amp
|
||
btnPulse.addEventListener('click', () => {
|
||
pulseOutline();
|
||
gsap.fromTo(core.material, { opacity: 0.6 }, { opacity: 1, duration: 0.35, yoyo: true, repeat: 1, ease: 'sine.inOut' });
|
||
gsap.to('.brand .logo', { boxShadow: '0 0 60px rgba(255,157,92,.8)', duration: 0.35, yoyo: true, repeat: 1, ease: 'sine.inOut' });
|
||
});
|
||
|
||
// Particles burst — radial push on some vertices
|
||
btnParticles.addEventListener('click', () => {
|
||
const pos = geometry.attributes.position; const burst = [];
|
||
for (let i = 0; i < STAR_COUNT; i += Math.floor(Math.random()*30)+15) {
|
||
const idx = i * 3; const x = pos.array[idx], y = pos.array[idx+1], z = pos.array[idx+2];
|
||
const len = Math.max(60, Math.hypot(x, y, z)); const nx = x/len, ny = y/len, nz = z/len;
|
||
burst.push({ idx, dx: nx * 20, dy: ny * 20, dz: nz * 20 });
|
||
}
|
||
gsap.to(burst, {
|
||
duration: 0.5, ease: 'power3.out',
|
||
onUpdate: () => { burst.forEach(b => { pos.array[b.idx] += b.dx * 0.35; pos.array[b.idx+1] += b.dy * 0.35; pos.array[b.idx+2] += b.dz * 0.35; }); pos.needsUpdate = true; }
|
||
});
|
||
gsap.to({}, { duration: 0.7, delay: 0.35, ease: 'power2.in', onUpdate: () => { galaxy.rotation.y += 0.01; } });
|
||
pulseOutline();
|
||
});
|
||
|
||
// Focus core — ease camera closer to center and slowly orbit
|
||
btnFocus.addEventListener('click', () => {
|
||
pulseOutline();
|
||
gsap.to(camera.position, { x: 0, y: 0, z: 100, duration: 1.0, ease: 'power2.inOut' });
|
||
gsap.to(state, { rotSpeed: 0.006, duration: 0.8, ease: 'sine.inOut' });
|
||
});
|
||
|
||
// Toggle auto orbit
|
||
btnAuto.addEventListener('click', () => {
|
||
state.autoOrbit = !state.autoOrbit;
|
||
btnAuto.setAttribute('aria-pressed', String(state.autoOrbit));
|
||
pulseOutline();
|
||
});
|
||
|
||
// Launch a simple comet (sprite streak that crosses the field)
|
||
btnComet.addEventListener('click', () => {
|
||
// Create a small streak using a canvas texture
|
||
const tex = new THREE.CanvasTexture(makeComet(128));
|
||
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false });
|
||
const comet = new THREE.Sprite(mat);
|
||
comet.scale.set(50, 20, 1); // streaky aspect
|
||
// Spawn from a random edge
|
||
const side = Math.random() < 0.5 ? -1 : 1;
|
||
comet.position.set(side * 300, (Math.random()-0.5)*120, -80 + Math.random()*160);
|
||
scene.add(comet);
|
||
// Animate across scene
|
||
gsap.to(comet.position, { x: -side * 300, y: comet.position.y + (Math.random()*80-40), z: comet.position.z + (Math.random()*60-30), duration: 1.4, ease: 'power2.out', onComplete: () => { scene.remove(comet); mat.dispose(); tex.dispose(); } });
|
||
gsap.to(comet.material, { opacity: 0, duration: 1.4, ease: 'power1.in' });
|
||
// Little shake
|
||
gsap.fromTo(camera.position, { x: camera.position.x + side*2 }, { x: camera.position.x, duration: 0.6, ease: 'power2.out' });
|
||
pulseOutline();
|
||
});
|
||
|
||
function makeComet(size) {
|
||
// Create a small canvas with a warm streak
|
||
const c = document.createElement('canvas'); c.width = c.height = size; const ctx = c.getContext('2d');
|
||
const g = ctx.createLinearGradient(0, size/2, size, size/2);
|
||
g.addColorStop(0, 'rgba(255,255,255,0.0)');
|
||
g.addColorStop(0.2, 'rgba(255,245,235,0.8)');
|
||
g.addColorStop(0.5, 'rgba(255,157,92,0.9)');
|
||
g.addColorStop(1, 'rgba(255,107,107,0.0)');
|
||
ctx.fillStyle = g; ctx.fillRect(0, size/2 - 6, size, 12);
|
||
// Glow
|
||
ctx.filter = 'blur(6px)'; ctx.fillRect(0, size/2 - 6, size, 12);
|
||
return c;
|
||
}
|
||
|
||
// Screenshot — download current canvas frame
|
||
btnScreenshot.addEventListener('click', () => {
|
||
// NOTE: preserveDrawingBuffer true on renderer
|
||
const data = canvas.toDataURL('image/png');
|
||
const a = document.createElement('a');
|
||
a.href = data; a.download = `neonverse_${Date.now()}.png`;
|
||
document.body.appendChild(a); a.click(); a.remove();
|
||
});
|
||
|
||
// Palette randomizer — quickly re-hue vertex colors within warm range
|
||
btnPalette.addEventListener('click', () => {
|
||
const colAttr = geometry.getAttribute('color');
|
||
for (let i = 0; i < STAR_COUNT; i++) {
|
||
const idx = i*3; const t = Math.random()*0.6 + 0.2; // bias to center
|
||
const hue = (0.02 + Math.random()*0.12); // 0.02..0.14 warm hues
|
||
color.setHSL(hue, 0.85, 0.58 + Math.random()*0.08);
|
||
colAttr.array[idx] = color.r; colAttr.array[idx+1] = color.g; colAttr.array[idx+2] = color.b;
|
||
}
|
||
geometry.attributes.color.needsUpdate = true;
|
||
pulseOutline();
|
||
});
|
||
|
||
// Mode toggle: calm vs hyper bias
|
||
modeToggle.addEventListener('change', (e) => {
|
||
const on = e.target.checked; state.hyper = on; btnHyper.setAttribute('aria-pressed', String(on));
|
||
gsap.to(state, { rotSpeed: on ? 0.010 : 0.003, duration: 0.8, ease: 'power2.inOut' });
|
||
gsap.to('.blob', { opacity: on ? 0.5 : 0.35, duration: 0.6 });
|
||
});
|
||
|
||
// Warmth slider: remap palette towards warmer/cooler within warm range
|
||
warmthSlider.addEventListener('input', (e) => {
|
||
state.warmth = Number(e.target.value) / 100; // 0..1
|
||
const colAttr = geometry.getAttribute('color');
|
||
for (let i = 0; i < STAR_COUNT; i++) {
|
||
const idx = i*3; const t = i / STAR_COUNT;
|
||
const hue = 0.02 + state.warmth * 0.14; // 0.02..0.16
|
||
color.setHSL((hue + (1-t)*0.05)%1, 0.85, 0.58 + (1-t)*0.1);
|
||
colAttr.array[idx] = color.r; colAttr.array[idx+1] = color.g; colAttr.array[idx+2] = color.b;
|
||
}
|
||
geometry.attributes.color.needsUpdate = true;
|
||
// Also tint core slightly by adjusting material opacity pulse
|
||
gsap.to(core.material, { opacity: 0.8 + state.warmth*0.2, duration: 0.3, ease: 'sine.out' });
|
||
pulseOutline();
|
||
});
|
||
|
||
// Accessibility: key shortcuts
|
||
addEventListener('keydown', (e) => {
|
||
const k = e.key.toLowerCase();
|
||
if (k === 'h') btnHyper.click();
|
||
if (k === 'r') btnReset.click();
|
||
if (k === 'p') btnPulse.click();
|
||
if (k === 'o') btnAuto.click();
|
||
if (k === 'c') btnComet.click();
|
||
if (k === 's') btnScreenshot.click();
|
||
});
|
||
|
||
// Optional: reduce motion respect
|
||
const mq = matchMedia('(prefers-reduced-motion: reduce)');
|
||
if (mq.matches) { state.rotSpeed = 0.001; }
|
||
</script>
|
||
</body>
|
||
</html>
|