Update index.html for warm theme and enhanced interactivity features

This commit is contained in:
2025-09-01 23:21:21 +02:00
parent 286b224f05
commit d43a08c7e0
+218 -145
View File
@@ -3,27 +3,28 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NeonVerse — 1file Spectacle</title>
<meta name="description" content="Neon glass UI over a Three.js galaxy background. One single HTML file with embedded CSS & JS." />
<title>NeonVerse — Warm & Playful (1file)</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>
/* =============================
Modern, clean, and neon style
============================= */
/* =============================================
Warm, modern, glass + neon (sunset inspired)
============================================= */
:root {
/* Theme variables */
--bg: #0a0b10;
--text: #eaf3ff;
--muted: #9bb3ff;
--accent: #6cf9ff;
--accent-2: #9b5cff;
--glass: rgba(255, 255, 255, 0.08);
--glass-2: rgba(255, 255, 255, 0.14);
/* 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);
@@ -35,23 +36,16 @@
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%, #10122b 0%, #090a12 60%), var(--bg);
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; /* Behind everything */
display: block;
}
#bg-canvas { position: fixed; inset: 0; width: 100vw; height: 100vh; z-index: -2; display: block; }
/* Subtle scanline overlay for a cyber feel */
/* Subtle scanline overlay */
.scanlines {
pointer-events: none;
position: fixed; inset: 0; z-index: -1;
pointer-events: none; position: fixed; inset: 0; z-index: -1;
background-image: repeating-linear-gradient(
to bottom,
rgba(255,255,255,0.04),
@@ -67,28 +61,22 @@
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 { 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), #40f0b5, var(--accent));
box-shadow: 0 0 30px rgba(108, 249, 255, .45);
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;
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;
}
.controls { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.btn {
--glow: var(--accent);
@@ -97,13 +85,20 @@
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(108,249,255,.45) inset;
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(108,249,255,.45) inset; }
.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;
@@ -114,35 +109,25 @@
.toggle input { display: none; }
.toggle .dot {
width: 24px; height: 24px; border-radius: 999px; position: relative;
background: radial-gradient(circle at 30% 30%, #fff, #cde 35%, #89f 70%, #48f);
box-shadow: 0 0 16px rgba(108,249,255,.45);
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;
}
.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(108,249,255,.25);
}
.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;
}
.btn-primary {
--glow: var(--accent-2);
border-color: rgba(155,92,255,.35);
box-shadow: var(--shadow), 0 0 0 0 rgba(155,92,255,.45) inset;
}
.btn-primary:hover { box-shadow: var(--shadow), 0 0 0 2px rgba(155,92,255,.45) inset; }
.cta { display: flex; gap: 12px; flex-wrap: wrap; }
.card {
border: 1px solid rgba(255,255,255,.12);
@@ -166,30 +151,27 @@
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(108,249,255,.25), rgba(155,92,255,.25));
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(108,249,255,.3);
box-shadow: 0 6px 22px rgba(255,157,92,.30);
}
footer { opacity: .75; text-align: center; padding: 30px 0 40px; font-size: 13px; color: var(--muted); }
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;
background: radial-gradient(circle at 30% 30%, rgba(108,249,255,.6), transparent 60%),
radial-gradient(circle at 70% 70%, rgba(155,92,255,.6), transparent 60%);
mix-blend-mode: screen;
}
.blob.one { top: -10vmax; left: -10vmax; }
.blob.two { bottom: -15vmax; right: -10vmax; }
.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(108,249,255,.0); outline-offset: 0; transition: outline-color .25s ease, outline-offset .25s ease; }
.hyper-outline.active { outline-color: rgba(108,249,255,.6); outline-offset: 8px; }
.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: #02030a; color: #fff; display: grid; place-items: center; z-index: 999; padding: 32px; text-align: center; }
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>
@@ -199,7 +181,7 @@
<div class="blob two"></div>
<header>
<a class="brand" href="#">
<a class="brand" href="#" aria-label="Inicio NeonVerse">
<div class="logo" aria-hidden="true"></div>
<div>
<h1>NeonVerse</h1>
@@ -207,14 +189,23 @@
</div>
</a>
<div class="controls">
<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>
<button id="btnHyper" class="btn btn-primary">Lanzar Hiperimpulso ⚡️</button>
<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>
@@ -222,18 +213,19 @@
<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. Todo en este único archivo HTML.</p>
<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="#6cf9ff"/>
<stop offset="100%" stop-color="#9b5cff"/>
<stop offset="0%" stop-color="#ff9d5c"/>
<stop offset="100%" stop-color="#ff6b6b"/>
</linearGradient>
</defs>
<g opacity="0.95" stroke="url(#grad)" stroke-width="2">
@@ -250,7 +242,7 @@
<article class="card">
<div class="icon">⚙️</div>
<h3>Interacción en tiempo real</h3>
<p>El campo de estrellas responde al ratón y a la velocidad del modo <em>Hiper</em>.</p>
<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>
@@ -259,13 +251,13 @@
</article>
<article class="card">
<div class="icon">🎛️</div>
<h3>Controles divertidos</h3>
<p>Activa el pulso, explota partículas o enciende el hiperimpulso y siente la aceleración.</p>
<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
Hecho con ♥︎ · Three.js · GSAP · HTML/CSS · En un solo archivo · Paleta cálida
</footer>
</main>
@@ -281,37 +273,37 @@
<script src="https://unpkg.com/gsap@3.12.5/dist/gsap.min.js"></script>
<script>
// =============================
// NeonVerse — Single-file demo
// =============================================
// 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 });
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, 2000);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 3000);
camera.position.set(0, 0, 140);
// Starfield parameters
const STAR_COUNT = 3000; // adjust for performance
const STAR_COUNT = 3200; // tiny bump
const galaxy = new THREE.Group();
// Create a spiral galaxy-like distribution
// 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 */ };
// Helper to set HSL into rgb Float32 array
const color = new THREE.Color();
const color = new THREE.Color(); // helper
// Build spiral galaxy-like distribution
for (let i = 0; i < STAR_COUNT; i++) {
// Spiral params
const radius = Math.pow(Math.random(), 0.6) * 380 + 20; // denser near center
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;
@@ -319,15 +311,16 @@
const angle = branchAngle + spin;
const x = Math.cos(angle) * radius + randomness;
const y = (Math.random() - 0.5) * 24 + Math.sin(angle * 3) * 3; // slight vertical waves
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;
// Color gradient across radius (neon cyan -> magenta)
// Warm gradient across radius (tangerine -> coral -> soft red)
const t = radius / 420;
color.setHSL(0.55 + 0.3 * (1 - t), 0.9, 0.6); // 0.55≈cyan to 0.85≈magenta
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;
}
@@ -336,7 +329,7 @@
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 1.7,
size: 1.8,
vertexColors: true,
transparent: true,
opacity: 0.95,
@@ -346,25 +339,24 @@
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.8 });
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
// 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,255,255,0.95)');
g.addColorStop(0.25, 'rgba(108,249,255,0.85)');
g.addColorStop(0.55, 'rgba(155,92,255,0.55)');
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;
@@ -372,11 +364,12 @@
// Interaction state
const state = {
rotSpeed: 0.0026, // base rotation speed
zoom: 0,
rotSpeed: 0.0026,
hyper: false,
parallaxX: 0,
parallaxY: 0
parallaxY: 0,
autoOrbit: false,
warmth: 0.7 // 0..1
};
// Parallax by mouse/touch
@@ -404,9 +397,14 @@
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.5 + Math.sin(time * 1.7) * 0.2 + (state.hyper ? 0.6 : 0);
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);
@@ -427,7 +425,13 @@
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 });
@@ -440,79 +444,148 @@
gsap.fromTo(hero, { outlineOffset: 0 }, { outlineOffset: 8, duration: 0.3, yoyo: true, repeat: 1, onComplete: () => hero.classList.remove('active') });
}
// Hyperdrive action — accelerate, recolor subtly, add camera shake
// ---- Actions ----
// Hyperdrive action — accelerate, cam kick, blob flash
btnHyper.addEventListener('click', () => {
state.hyper = true;
state.hyper = true; btnHyper.setAttribute('aria-pressed', 'true');
pulseOutline();
gsap.to(state, { rotSpeed: 0.020, duration: 0.8, ease: 'power3.out' });
// Quick camera kick
gsap.fromTo(camera.position, { z: 160 }, { z: 120, duration: 0.8, ease: 'power2.out', yoyo: true, repeat: 1 });
// Screen pulse via blobs
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;
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 — quick UI and core glow amp
// 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(108,249,255,.8)', 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 positions for a brief spark
// Particles burst — radial push on some vertices
btnParticles.addEventListener('click', () => {
const pos = geometry.attributes.position;
const burst = [];
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;
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 });
}
// Animate selected vertices
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;
}
});
// spring back
gsap.to({}, {
duration: 0.7,
delay: 0.35,
ease: 'power2.in',
onUpdate: () => { galaxy.rotation.y += 0.01; }
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;
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) => {
if (e.key.toLowerCase() === 'h') btnHyper.click();
if (e.key.toLowerCase() === 'r') btnReset.click();
if (e.key.toLowerCase() === 'p') btnPulse.click();
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