prueba
This commit is contained in:
+523
@@ -0,0 +1,523 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>NeonVerse — 1‑file Spectacle</title>
|
||||
<meta name="description" content="Neon glass UI over a Three.js galaxy background. 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
|
||||
============================= */
|
||||
: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);
|
||||
--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%, #10122b 0%, #090a12 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;
|
||||
}
|
||||
|
||||
/* Subtle scanline overlay for a cyber feel */
|
||||
.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), #40f0b5, var(--accent));
|
||||
box-shadow: 0 0 30px rgba(108, 249, 255, .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;
|
||||
}
|
||||
|
||||
.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(108,249,255,.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:active { transform: translateY(0); }
|
||||
|
||||
.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, #cde 35%, #89f 70%, #48f);
|
||||
box-shadow: 0 0 16px rgba(108,249,255,.45);
|
||||
transition: transform .3s ease;
|
||||
}
|
||||
.toggle input:checked + .dot { transform: translateX(4px) rotate(16deg) scale(1.02); }
|
||||
|
||||
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(108,249,255,.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; }
|
||||
|
||||
.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(108,249,255,.25), rgba(155,92,255,.25));
|
||||
border: 1px solid rgba(255,255,255,.18);
|
||||
box-shadow: 0 6px 22px rgba(108,249,255,.3);
|
||||
}
|
||||
|
||||
footer { opacity: .75; 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; }
|
||||
|
||||
/* "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; }
|
||||
|
||||
/* 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; }
|
||||
</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="#">
|
||||
<div class="logo" aria-hidden="true"></div>
|
||||
<div>
|
||||
<h1>NeonVerse</h1>
|
||||
<span>Un archivo · Todo el espectáculo</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="controls">
|
||||
<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>
|
||||
<button id="btnReset" class="btn">Reiniciar</button>
|
||||
</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. 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>
|
||||
</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"/>
|
||||
</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 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 divertidos</h3>
|
||||
<p>Activa el pulso, explota partículas o enciende el hiperimpulso y siente la aceleración.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Hecho con ♥︎ · Three.js · GSAP · HTML/CSS · En un solo archivo
|
||||
</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 — Single-file demo
|
||||
// (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 });
|
||||
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);
|
||||
camera.position.set(0, 0, 140);
|
||||
|
||||
// Starfield parameters
|
||||
const STAR_COUNT = 3000; // adjust for performance
|
||||
const galaxy = new THREE.Group();
|
||||
|
||||
// Create a spiral galaxy-like distribution
|
||||
const positions = new Float32Array(STAR_COUNT * 3);
|
||||
const colors = new Float32Array(STAR_COUNT * 3);
|
||||
|
||||
// Helper to set HSL into rgb Float32 array
|
||||
const color = new THREE.Color();
|
||||
|
||||
for (let i = 0; i < STAR_COUNT; i++) {
|
||||
// Spiral params
|
||||
const radius = Math.pow(Math.random(), 0.6) * 380 + 20; // denser near center
|
||||
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; // slight vertical waves
|
||||
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)
|
||||
const t = radius / 420;
|
||||
color.setHSL(0.55 + 0.3 * (1 - t), 0.9, 0.6); // 0.55≈cyan to 0.85≈magenta
|
||||
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.7,
|
||||
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.8 });
|
||||
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
|
||||
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(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = g; ctx.fillRect(0,0,size,size);
|
||||
return c;
|
||||
}
|
||||
|
||||
// Interaction state
|
||||
const state = {
|
||||
rotSpeed: 0.0026, // base rotation speed
|
||||
zoom: 0,
|
||||
hyper: false,
|
||||
parallaxX: 0,
|
||||
parallaxY: 0
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// 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.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 modeToggle = document.getElementById('modeToggle');
|
||||
|
||||
// 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') });
|
||||
}
|
||||
|
||||
// Hyperdrive action — accelerate, recolor subtly, add camera shake
|
||||
btnHyper.addEventListener('click', () => {
|
||||
state.hyper = 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.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;
|
||||
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
|
||||
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' });
|
||||
});
|
||||
|
||||
// Particles burst — radial push on positions for a brief spark
|
||||
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 });
|
||||
}
|
||||
// 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; }
|
||||
});
|
||||
pulseOutline();
|
||||
});
|
||||
|
||||
// Mode toggle: calm vs hyper bias
|
||||
modeToggle.addEventListener('change', (e) => {
|
||||
const on = e.target.checked;
|
||||
state.hyper = 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 });
|
||||
});
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
// Optional: reduce motion respect
|
||||
const mq = matchMedia('(prefers-reduced-motion: reduce)');
|
||||
if (mq.matches) { state.rotSpeed = 0.001; }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user