Update index.html for warm theme and enhanced interactivity features
This commit is contained in:
+218
-145
@@ -3,27 +3,28 @@
|
||||
<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." />
|
||||
|
||||
<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>
|
||||
/* =============================
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user