Created
July 30, 2025 06:58
-
-
Save mewmix/7986d98167400b3fd245e5ea963ca70d to your computer and use it in GitHub Desktop.
Untitled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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