Skip to content

Instantly share code, notes, and snippets.

@mewmix
Created July 30, 2025 06:58
Show Gist options
  • Select an option

  • Save mewmix/7986d98167400b3fd245e5ea963ca70d to your computer and use it in GitHub Desktop.

Select an option

Save mewmix/7986d98167400b3fd245e5ea963ca70d to your computer and use it in GitHub Desktop.
Untitled
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Alexander James Klein | Interactive Portfolio</title>
<!-- FONTS & GLOBAL STYLES -->
<style>
#ui-toggle{
position:fixed; top:30px; right:30px; z-index:11;
font:700 1.8rem 'Orbitron',sans-serif;
color:var(--primary-color); cursor:pointer;
user-select:none; transition:color .25s,text-shadow .25s;
}
#ui-toggle:hover{ color:#fff; text-shadow:0 0 12px var(--primary-color); }
body.ui-hidden #ui-toggle{ color:#ff5050; } /* show red icon when hidden */
@import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Exo+2:wght@300;400;700&display=swap");
:root {
--primary-color: #ff9900;
--primary-glow: #ff990099;
--text-color: #f0f0f0;
--bg-color: #000000;
}
* { box-sizing: border-box; }
html,body { margin:0; height:100%; background:var(--bg-color); color:var(--text-color); font-family:"Exo 2", sans-serif; overflow:hidden; }
/* Custom Cursor */
body { cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" style="font-size:20px;color:rgba(255,153,0,0.8);"><text x="-3" y="16">+</text></svg>') 4 4, auto; }
body.attack-mode { cursor: crosshair; }
/* WebGL canvas */
#bg-canvas { position:fixed; inset:0; outline:none; }
/* ---------- NAVIGATION ---------- */
.nav-control {
position: fixed;
top: 50%;
transform: translateY(-50%);
z-index: 10;
font-size: 3rem;
font-weight: 700;
color: var(--primary-glow);
cursor: pointer;
user-select: none;
transition: color 0.3s, text-shadow 0.3s;
padding: 0 2vw;
}
.nav-control:hover {
color: var(--primary-color);
text-shadow: 0 0 15px var(--primary-color);
}
#nav-left { left: 1vw; }
#nav-right { right: 1vw; }
#nav-title {
position: fixed;
top: 30px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
font-family: 'Orbitron', sans-serif;
font-size: 2rem;
font-weight: 700;
color: var(--primary-color);
text-shadow: 0 0 8px var(--primary-glow);
letter-spacing: 5px;
text-transform: uppercase;
}
/* ---------- SOCIAL ICONS ---------- */
#socials { position:fixed; bottom:26px; left:26px; display:flex; gap:20px; z-index:12; }
.social svg { width:26px; height:26px; fill:var(--primary-color); transition:transform .25s,fill .25s; }
.social:hover svg { fill:#fff; transform:scale(1.2); }
/* ---------- MINI-GAME HUD ---------- */
#game-hud { position:fixed; inset:0; z-index:15; display:none; user-select:none; pointer-events:none; }
.hud-element { position:absolute; font-family:"Orbitron", sans-serif; font-weight:700; color:#f00; text-shadow:0 0 4px #f00, 0 0 8px #f00, 0 0 12px #f00; }
#hud-warning { top:5%; left:50%; transform:translateX(-50%); font-size:2rem; letter-spacing:8px; animation:blink 1s infinite; }
@keyframes blink { 50% { opacity: 0.4; } }
#hud-score { bottom:5%; right:5%; font-size:1.8rem; text-align:right; }
#hud-stand-down { top: 5%; left: 5%; font-size:1.2rem; color:#f88; text-shadow:0 0 4px #f00; border:1px solid #f88; padding:8px 12px; pointer-events:all; cursor:pointer; transition: .2s; }
#hud-stand-down:hover { background:rgba(255,0,0,.2); color:#fff; border-color:#fff; }
/* ---------- RESPONSIVENESS ---------- */
@media (max-width: 768px) {
#nav-title { font-size: 1.2rem; top: 20px; }
.nav-control { font-size: 2.5rem; }
#socials { bottom: 15px; left: 15px; gap: 15px; }
#hud-warning { font-size: 1.5rem; width: 90%; }
}
</style>
</head>
<body>
<canvas id="bg-canvas"></canvas>
<!-- NAVIGATION UI -->
<div id="nav-left" class="nav-control"><</div>
<div id="nav-right" class="nav-control">></div>
<div id="nav-title">Overview</div>
<!-- TOGGLE UI -->
<div id="ui-toggle" title="Hide UI [H]">⤢</div>
<!-- SOCIAL LINKS -->
<div id="socials">
<a href="https://x.com/your_handle" target="_blank" aria-label="X" class="social"><svg viewBox="0 0 24 24"><path d="M22.46 2H1.54a.5.5 0 00-.36.84l9.2 9.24-9.2 9.08a.5.5 0 00.36.84h20.92a.5.5 0 00.36-.84l-9.34-9.2 9.34-9.13A.5.5 0 0022.46 2z"/></svg></a>
<a href="https://linkedin.com/in/your_handle" target="_blank" aria-label="LinkedIn" class="social"><svg viewBox="0 0 24 24"><path d="M4.98 3.5C4.98 4.88 3.88 6 2.5 6S0 4.88 0 3.5 1.12 1 2.5 1 4.98 2.12 4.98 3.5zM.24 8.02h4.52v13.96H.24zm7.64 0h4.32v1.92h.06c.6-1.14 2.06-2.34 4.24-2.34 4.54 0 5.38 2.98 5.38 6.86v7.52h-4.5v-6.66c0-1.58-.03-3.62-2.2-3.62-2.2 0-2.54 1.72-2.54 3.5v6.78H7.88z"/></svg></a>
<a href="https://t.me/your_handle" target="_blank" aria-label="Telegram" class="social"><svg viewBox="0 0 24 24"><path d="M22.5 3.24l-20.3 7.83a1 1 0 00.05 1.9l4.96 1.55 2.14 6.58a1 1 0 001.63.45l3.02-2.84 4.66 3.4a1 1 0 001.58-.55l3.77-16.3a1 1 0 00-1.51-1.02z"/></svg></a>
</div>
<!-- MINI-GAME HUD -->
<div id="game-hud">
<div id="hud-warning" class="hud-element">WARNING: HOSTILES DETECTED</div>
<div id="hud-score" class="hud-element">SCORE: 0</div>
<div id="hud-stand-down" class="hud-element">[ STAND DOWN ]</div>
</div>
<script type="importmap">{"imports": {"three": "https://unpkg.com/three@0.160.0/build/three.module.js","three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"}}</script>
<script type="module">
import * as THREE from "three";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
const PRIMARY_COLOR = 0xff9900;
const GITHUB = "mewmix";
const FONT_TITLE = "'Orbitron', sans-serif";
const FONT_BODY = "'Exo 2', sans-serif";
/* ===================== 1. SCENE & BLOOM SETUP ===================== */
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 500);
camera.position.z = 8;
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById("bg-canvas"), antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 1.2, 0.5, 0.8);
bloomPass.threshold = 0.1;
bloomPass.strength = 1.0;
bloomPass.radius = 0.8;
composer.addPass(bloomPass);
/* ===================== 2. BACKGROUND STARFIELD ===================== */
const starGeo = new THREE.BufferGeometry();
const starCount = 1500;
const positions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 240;
positions[i * 3 + 1] = (Math.random() - 0.5) * 240;
positions[i * 3 + 2] = (Math.random() - 0.5) * 240;
}
starGeo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
const stars = new THREE.Points(starGeo, new THREE.PointsMaterial({ color: PRIMARY_COLOR, size: 0.35, transparent: true, opacity: 0.5 }));
scene.add(stars);
/* ===================== 3. CENTRAL GEOMETRY ===================== */
const geometryGroup = new THREE.Group();
scene.add(geometryGroup);
// Central Crystal
const crystalGeo = new THREE.OctahedronGeometry(1.5, 1);
const crystalMat = new THREE.MeshBasicMaterial({ color: PRIMARY_COLOR, wireframe: true, transparent: true, opacity: 0.8 });
const crystal = new THREE.Mesh(crystalGeo, crystalMat);
geometryGroup.add(crystal);
// Interlocking Rings
for (let i = 0; i < 5; i++) {
const radius = 2.2 + i * 0.5;
const ringGeo = new THREE.TorusGeometry(radius, 0.02, 16, 128);
const ringMat = new THREE.MeshBasicMaterial({ color: PRIMARY_COLOR, transparent: true, opacity: 0.15 + i * 0.08 });
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.rotation.x = Math.random() * Math.PI;
ring.rotation.y = Math.random() * Math.PI;
ring.userData.rotationSpeed = (Math.random() - 0.5) * 0.2;
geometryGroup.add(ring);
}
// Inner Torus Knot
const knot = new THREE.Mesh(
new THREE.TorusKnotGeometry(1, 0.12, 128, 16),
new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true, transparent: true, opacity: 0.3 })
);
geometryGroup.add(knot);
/* ===================== 4. CONTENT & NAVIGATION ===================== */
const contentGroup = new THREE.Group();
scene.add(contentGroup);
// Utility to create textures from canvas
const makeTex = (w, h, draw) => {
const cvs = document.createElement("canvas");
cvs.width = w;
cvs.height = h;
const ctx = cvs.getContext("2d");
draw(ctx, w, h);
return new THREE.CanvasTexture(cvs);
};
function clearContent() {
// Animate out
if (contentGroup.children.length > 0) {
const currentPlane = contentGroup.children[0];
// Simple removal, can be replaced with tweening library for fancier effects
currentPlane.visible = false;
}
// Clear after a delay to allow for animation
setTimeout(() => {
while (contentGroup.children.length) {
const c = contentGroup.children.pop();
if(c.geometry) c.geometry.dispose();
if(c.material) {
if(c.material.map) c.material.map.dispose();
c.material.dispose();
}
}
contentGroup.userData.updateFn = null;
}, 150);
}
const sectionFunctions = []; // To be populated below
// Rolodex Navigation Logic
const navLeft = document.getElementById('nav-left');
const navRight = document.getElementById('nav-right');
const navTitle = document.getElementById('nav-title');
let currentIndex = 0;
function showSection(index, direction = 1) {
clearContent();
currentIndex = (index + sectionFunctions.length) % sectionFunctions.length;
const section = sectionFunctions[currentIndex];
setTimeout(() => {
section.fn();
navTitle.textContent = section.name;
// Animate in
if (contentGroup.children.length > 0) {
const newPlane = contentGroup.children[0];
newPlane.position.x = 15 * direction;
newPlane.visible = true;
contentGroup.userData.targetX = 0;
}
}, 150);
}
navLeft.addEventListener('click', () => showSection(currentIndex - 1, -1));
navRight.addEventListener('click', () => showSection(currentIndex + 1, 1));
window.addEventListener('keydown', (e) => {
if (game.active) return;
if (e.key === 'ArrowRight') showSection(currentIndex + 1, 1);
if (e.key === 'ArrowLeft') showSection(currentIndex - 1, -1);
});
/* ---------- Section Content Definitions ---------- */
function sectionOverview() {
const tex = makeTex(800, 400, (ctx, w, h) => {
ctx.fillStyle = "#ff990020"; ctx.fillRect(0, 0, w, h);
ctx.fillStyle = `var(--primary-color)`;
ctx.textAlign = "center";
ctx.font = `700 48px ${FONT_TITLE}`; ctx.fillText("A. J. Klein", w / 2, 100);
ctx.font = `400 28px ${FONT_BODY}`; ctx.fillText("Poway, California, USA", w / 2, 170);
ctx.font = `300 22px ${FONT_BODY}`; ctx.fillText("Local Time:", w / 2, 240);
});
const plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 4), new THREE.MeshBasicMaterial({ map: tex, transparent: true, opacity: 0.9 }));
contentGroup.add(plane);
contentGroup.userData.updateFn = () => {
const ctx = tex.image.getContext("2d");
ctx.clearRect(0, 250, 800, 50);
ctx.fillStyle = `var(--primary-color)`;
ctx.textAlign = "center";
ctx.font = `400 32px ${FONT_BODY}`; ctx.fillText(new Date().toLocaleTimeString(), 400, 280);
tex.needsUpdate = true;
};
}
sectionFunctions.push({ name: 'Overview', fn: sectionOverview });
function sectionSkills() {
const tex = makeTex(800, 600, (ctx, w, h) => {
ctx.fillStyle = "#ff990020"; ctx.fillRect(0, 0, w, h);
const skills = ["Python","React","Node.js","Typescript","Rust","Solidity","Foundry","SQL","Postgres","Docker","Kotlin","Three.js","Linux","Bash","CUDA"];
ctx.textAlign = "center";
ctx.font = `400 24px ${FONT_BODY}`;
ctx.fillStyle = `#f0f0f0`;
const colWidth = 200;
const rowHeight = 70;
let x = 100, y = 100;
skills.forEach(skill => {
ctx.fillText(skill, x, y);
x += colWidth;
if (x >= w) { x = 100; y += rowHeight; }
});
});
const plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 6), new THREE.MeshBasicMaterial({ map: tex, transparent: true, opacity: 0.9 }));
contentGroup.add(plane);
}
sectionFunctions.push({ name: 'Skills', fn: sectionSkills });
function sectionProjects() {
const knot = new THREE.Mesh( new THREE.TorusKnotGeometry(1.5, 0.4, 128, 16), new THREE.MeshBasicMaterial({ color: PRIMARY_COLOR, wireframe: true }) );
knot.position.set(-4, 0, 1);
contentGroup.add(knot);
const tex = makeTex(500, 600, (ctx, w, h) => {
ctx.fillStyle = "#ff990020"; ctx.fillRect(0, 0, w, h);
ctx.fillStyle = `var(--primary-color)`; ctx.font = `bold 32px ${FONT_TITLE}`; ctx.fillText("PROJECTS", 40, 80);
ctx.fillStyle = `#f0f0f0`; ctx.font = `bold 22px ${FONT_BODY}`; ctx.fillText("NV-STREAM", 40, 150);
ctx.font = `300 18px ${FONT_BODY}`;
ctx.fillText("TYPE: Real-time Data Pipeline", 40, 200);
ctx.fillText("STACK: Rust, CUDA, WebSockets", 40, 230);
ctx.fillText("‣ GPU-accelerated object detection", 45, 290);
ctx.fillText("‣ Dockerized edge deployment", 45, 320);
ctx.fillText("‣ Live dashboard w/ WebGL HUD", 45, 350);
});
const plane = new THREE.Mesh( new THREE.PlaneGeometry(5, 6), new THREE.MeshBasicMaterial({ map: tex, transparent: true, opacity: 0.9 }) );
plane.position.set(2, 0, 0);
contentGroup.add(plane);
contentGroup.userData.updateFn = () => { knot.rotation.x += 0.003; knot.rotation.y += 0.005; };
}
sectionFunctions.push({ name: 'Projects', fn: sectionProjects });
function sectionLive() {
const tex = makeTex(800, 400, (ctx, w, h) => {
ctx.fillStyle = "#ff990020"; ctx.fillRect(0, 0, w, h);
ctx.fillStyle = `var(--primary-color)`; ctx.textAlign = "center";
ctx.font = `bold 32px ${FONT_TITLE}`; ctx.fillText("LIVE ACTIVITY", w / 2, 100);
ctx.font = `300 24px ${FONT_BODY}`; ctx.fillStyle = `#f0f0f0`; ctx.fillText("Connecting to GitHub API...", w / 2, 220);
});
contentGroup.add(new THREE.Mesh(new THREE.PlaneGeometry(8, 4), new THREE.MeshBasicMaterial({ map: tex, transparent: true, opacity: 0.9 })));
(async () => {
try {
const d = await (await fetch(`https://api.github.com/users/${GITHUB}`)).json();
const ctx = tex.image.getContext("2d");
ctx.clearRect(0, 150, 800, 200);
ctx.fillStyle = `#f0f0f0`; ctx.textAlign = "center"; ctx.font = `400 28px ${FONT_BODY}`;
ctx.fillText(`Public Repos: ${d.public_repos} | Followers: ${d.followers}`, 400, 220);
ctx.fillText(`Last Update: ${new Date(d.updated_at).toLocaleDateString()}`, 400, 270);
tex.needsUpdate = true;
} catch { /* ignore */ }
})();
}
sectionFunctions.push({ name: 'Live', fn: sectionLive });
function sectionMarkets() {
const tex = makeTex(800, 400, (ctx, w, h) => {
ctx.fillStyle = "#ff990020"; ctx.fillRect(0, 0, w, h);
ctx.fillStyle = `var(--primary-color)`; ctx.textAlign = "center";
ctx.font = `bold 32px ${FONT_TITLE}`; ctx.fillText("MARKET STATS", w / 2, 100);
ctx.font = `300 24px ${FONT_BODY}`; ctx.fillStyle = `#f0f0f0`; ctx.fillText("Connecting to CoinGecko API...", w / 2, 220);
});
contentGroup.add(new THREE.Mesh(new THREE.PlaneGeometry(8, 4), new THREE.MeshBasicMaterial({ map: tex, transparent: true, opacity: 0.9 })));
(async () => {
try {
const resp = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=ethereum,solana&vs_currencies=usd");
const data = await resp.json();
const eth = data.ethereum.usd.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
const sol = data.solana.usd.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
const ctx = tex.image.getContext("2d");
ctx.clearRect(0, 150, 800, 200);
ctx.fillStyle = `#f0f0f0`; ctx.textAlign = "center"; ctx.font = `400 32px ${FONT_BODY}`;
ctx.fillText(`ETH ▸ ${eth}`, 400, 220); ctx.fillText(`SOL ▸ ${sol}`, 400, 280);
tex.needsUpdate = true;
} catch { /* ignore */ }
})();
}
sectionFunctions.push({ name: 'Markets', fn: sectionMarkets });
function sectionBlog() {
const tex = makeTex(900, 400, (ctx, w, h) => {
ctx.fillStyle = "#ff990020"; ctx.fillRect(0, 0, w, h);
ctx.fillStyle = `var(--primary-color)`; ctx.textAlign = "center";
ctx.font = `bold 32px ${FONT_TITLE}`; ctx.fillText("BLOG", w / 2, 100);
});
const plane = new THREE.Mesh(new THREE.PlaneGeometry(9, 4), new THREE.MeshBasicMaterial({ map: tex, transparent: true, opacity: 0.9 }));
contentGroup.add(plane);
let off = 0;
const posts = ["2025-07-20 — Thunderbolt 5 Secure-Storage Prototype", "2025-07-10 — Optimizing On-Device TTS for OnePlus 12", "2025-06-30 — Merkle-Proof Gas Optimisation"];
const fullText = posts.join(" // ");
contentGroup.userData.updateFn = () => {
const ctx = tex.image.getContext("2d");
ctx.clearRect(0, 150, 900, 200);
ctx.fillStyle = "#f0f0f0"; ctx.font = `400 24px ${FONT_BODY}`;
const textWidth = ctx.measureText(fullText).width + 100;
ctx.fillText(fullText, -off, 250);
ctx.fillText(fullText, -off + textWidth, 250);
off = (off + 0.8) % textWidth;
tex.needsUpdate = true;
};
}
sectionFunctions.push({ name: 'Blog', fn: sectionBlog });
// Initial call
showSection(0);
/* ===================== 5. RESIZE & MOUSE PARALLAX ===================== */
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
composer.setSize(innerWidth, innerHeight);
});
const mouse = new THREE.Vector2();
addEventListener("mousemove", e => {
mouse.x = (e.clientX / innerWidth) * 2 - 1;
mouse.y = -(e.clientY / innerHeight) * 2 + 1;
});
/* ===================== 6. MINI-GAME LOGIC ===================== */
const game = { active: false, idleTimer: null, spawnInterval: null, score: 0, enemies: [], particles: [], lasers: [], starSpeed: 0 };
const gameHud = document.getElementById("game-hud");
const scoreEl = document.getElementById("hud-score");
const standDownBtn = document.getElementById("hud-stand-down");
const raycaster = new THREE.Raycaster();
function startGameIdleTimer() { clearTimeout(game.idleTimer); game.idleTimer = setTimeout(startGame, 30000); }
function startGame() {
if (game.active) return;
game.active = true; game.score = 0;
scoreEl.textContent = "SCORE: 0"; gameHud.style.display = "block";
document.body.classList.add('attack-mode');
[navLeft, navRight, navTitle].forEach(el => el.style.display = 'none');
clearContent();
geometryGroup.visible = false;
game.starSpeed = 50;
bloomPass.strength = 1.8;
stars.material.color.set(0xff0000);
game.spawnInterval = setInterval(spawnEnemy, 2000); spawnEnemy();
}
function endGame() {
game.active = false;
clearTimeout(game.idleTimer); clearInterval(game.spawnInterval);
gameHud.style.display = "none";
document.body.classList.remove('attack-mode');
[navLeft, navRight, navTitle].forEach(el => el.style.display = 'block');
geometryGroup.visible = true; game.starSpeed = 0;
bloomPass.strength = 1.0; stars.material.color.set(PRIMARY_COLOR);
[...game.enemies, ...game.lasers, ...game.particles].forEach(obj => scene.remove(obj));
game.enemies = []; game.lasers = []; game.particles = [];
showSection(currentIndex);
startGameIdleTimer();
}
// Game functions (spawn, fire, etc.) remain largely the same, logic is sound
function spawnEnemy() { if (!game.active) return; const geo = new THREE.TetrahedronGeometry(0.5); const mat = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }); const enemy = new THREE.Mesh(geo, mat); enemy.position.set((Math.random() - 0.5) * 15, (Math.random() - 0.5) * 10, -20 - Math.random() * 20); enemy.userData.velocity = new THREE.Vector3((Math.random() - 0.5) * 0.01, (Math.random() - 0.5) * 0.01, 0); scene.add(enemy); game.enemies.push(enemy); }
function fireLaser() { raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(game.enemies); if (intersects.length > 0) { const target = intersects[0].object; scene.remove(target); game.enemies = game.enemies.filter(e => e !== target); game.score++; scoreEl.textContent = `SCORE: ${game.score}`; } const laserMat = new THREE.LineBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 1.0 }); const points = [new THREE.Vector3(0, -camera.position.y, camera.position.z-1), raycaster.ray.at(100, new THREE.Vector3())]; const laserGeo = new THREE.BufferGeometry().setFromPoints(points); const laser = new THREE.Line(laserGeo, laserMat); laser.userData.lifespan = 0.2; scene.add(laser); game.lasers.push(laser); }
window.addEventListener('click', () => { if (game.active) { fireLaser(); } });
standDownBtn.addEventListener('click', endGame);
startGameIdleTimer();
/* ===================== 7. ANIMATION LOOP ===================== */
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const t = clock.getElapsedTime();
// Animate background geometry
geometryGroup.rotation.y = t * 0.05;
geometryGroup.rotation.x = t * 0.03;
crystal.rotation.y = -t * 0.1;
knot.rotation.y = -t * 0.2;
knot.rotation.z = t * 0.15;
geometryGroup.children.forEach(child => {
if(child.userData.rotationSpeed) child.rotation.z += child.userData.rotationSpeed * delta;
});
// Animate content plane sliding
if (contentGroup.children.length > 0 && contentGroup.userData.targetX !== undefined) {
const plane = contentGroup.children[0];
plane.position.x += (contentGroup.userData.targetX - plane.position.x) * 0.1;
}
// Mouse parallax
camera.position.x += (mouse.x * 1.5 - camera.position.x) * 0.04;
camera.position.y += (mouse.y * 1.5 - camera.position.y) * 0.04;
camera.lookAt(scene.position);
// Update dynamic content
contentGroup.userData.updateFn?.();
// Mini-game animations
if (game.active) {
game.enemies.forEach(e => { e.position.add(e.userData.velocity); e.rotation.x += 0.01; e.rotation.y += 0.02; });
game.lasers.forEach((l, i) => { l.userData.lifespan -= delta; l.material.opacity = l.userData.lifespan * 5; if(l.userData.lifespan <= 0) { scene.remove(l); l.geometry.dispose(); l.material.dispose(); game.lasers.splice(i, 1); }});
}
// Trench run effect for game mode
if (game.starSpeed > 0) {
const positions = stars.geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
positions[i + 2] += game.starSpeed * delta;
if (positions[i + 2] > camera.position.z) {
positions[i + 2] = -200 - Math.random() * 50;
positions[i] = (Math.random() - 0.5) * 150;
positions[i + 1] = (Math.random() - 0.5) * 150;
}
}
stars.geometry.attributes.position.needsUpdate = true;
}
composer.render();
}
animate();
/* ---------- UI TOGGLE ---------- */
const uiToggle = document.getElementById('ui-toggle');
const domUIElems = [navLeft, navRight, navTitle, document.getElementById('socials'), gameHud];
let uiHidden = false;
function applyUIVisibility() {
domUIElems.forEach(el => el.style.display = uiHidden ? 'none' : '');
geometryGroup.visible = !uiHidden; // leave the glow‑rings if you prefer -> true
contentGroup.visible = !uiHidden;
}
function toggleUI() {
uiHidden = !uiHidden;
document.body.classList.toggle('ui-hidden', uiHidden);
applyUIVisibility();
}
uiToggle.addEventListener('click', toggleUI);
window.addEventListener('keydown', e => { if (e.key.toLowerCase()==='h') toggleUI(); });
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment