diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-10 09:22:06 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-10 09:22:06 +0300 |
| commit | f3155cbc866a1b261a0aafe61683da1e3c25b1ef (patch) | |
| tree | dedad4708adf0b377dd06c11c1bc957bc1bfe302 /internal | |
| parent | 3e61d09873065f5342efc414ee3ea0d5fdc4c767 (diff) | |
add WebGL scenes to all themes, sounds, image sizing, new themes
- All 11 themes now have unique Three.js WebGL backgrounds:
aurora: flowing sine-wave ribbon meshes with additive blending
brutalist: harsh rotating white/red wireframe boxes
glass: drifting crystal icosahedron shards, semi-transparent
matrix: digital rain particle columns with per-vertex colour fade
ocean: animated vertex-displaced wave surface with orbiting light
retro: amber demo-scene cube with orbiting octahedrons
synthwave: sunset sphere with scan-line rings and perspective grid
terminal: green icosahedron wireframe with orbiting torus particles
neon: existing Three.js orb and rings (unchanged)
plasma (replaces minimal): drifting additive-blend colour blobs
volcano (replaces paper): rising ember particles with lifecycle
- shared.go: distinct sounds for j/k nav (220Hz beep), Enter (ascending
triangle chime), and Esc (descending sine sweep)
- shared.go: images are now thumbnails (max-height:220px) in list view
and expand to full width inside the modal (CSS override in navmodal)
- main.go: --list-themes flag prints all theme names and exits;
--theme random picks a random theme at all generation time
- themes.go: updated registry with plasma/volcano replacing minimal/paper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/generator/shared.go | 52 | ||||
| -rw-r--r-- | internal/generator/theme_aurora.go | 109 | ||||
| -rw-r--r-- | internal/generator/theme_brutalist.go | 68 | ||||
| -rw-r--r-- | internal/generator/theme_glass.go | 114 | ||||
| -rw-r--r-- | internal/generator/theme_matrix.go | 102 | ||||
| -rw-r--r-- | internal/generator/theme_minimal.go | 180 | ||||
| -rw-r--r-- | internal/generator/theme_ocean.go | 91 | ||||
| -rw-r--r-- | internal/generator/theme_paper.go | 202 | ||||
| -rw-r--r-- | internal/generator/theme_retro.go | 87 | ||||
| -rw-r--r-- | internal/generator/theme_synthwave.go | 110 | ||||
| -rw-r--r-- | internal/generator/theme_terminal.go | 78 | ||||
| -rw-r--r-- | internal/generator/themes.go | 4 |
12 files changed, 1006 insertions, 191 deletions
diff --git a/internal/generator/shared.go b/internal/generator/shared.go index eed4de3..12a91f4 100644 --- a/internal/generator/shared.go +++ b/internal/generator/shared.go @@ -3,13 +3,12 @@ package generator // navDefs is appended to every theme template when parsing. // It defines three named sub-templates shared across all themes: // - "navhints" — keyboard shortcut hint bar HTML -// - "navmodal" — full-screen expanded-post modal HTML -// - "navscript" — keyboard navigation JavaScript +// - "navmodal" — full-screen expanded-post modal HTML + image-sizing CSS +// - "navscript" — keyboard navigation JavaScript with distinct sounds per action // // Each theme calls {{template "navhints" .}}, {{template "navmodal" .}}, and // {{template "navscript" .}} at the appropriate points in its HTML. -// All CSS for these elements (colours, borders, backdrop) lives in each theme -// so themes remain self-contained and independently styled. +// All theme-specific CSS lives in each theme file so themes stay self-contained. const navDefs = ` {{define "navhints"}} <div class="nav-hints" aria-label="keyboard shortcuts"> @@ -21,6 +20,12 @@ const navDefs = ` {{end}} {{define "navmodal"}} +<style> +/* Thumbnail sizing in list view; modal overrides to full width so images + appear larger when a post is expanded with Enter. */ +.post-image { max-height:220px; max-width:100%; object-fit:cover; cursor:pointer; } +#post-modal .post-image { max-height:none; width:100%; max-width:100%; object-fit:contain; cursor:default; } +</style> <div class="post-modal" id="post-modal"> <div class="modal-inner"> <button class="modal-close" onclick="closeModal()">[ ESC ] CLOSE</button> @@ -51,8 +56,7 @@ const navDefs = ` playNavSound(); } - // playNavSound generates a short beep via the Web Audio API. - // A fresh AudioContext per call avoids state issues across navigations. + // playNavSound: short low beep for post selection (j/k navigation). function playNavSound() { try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); @@ -60,21 +64,55 @@ const navDefs = ` const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 220; osc.type = 'sine'; - gain.gain.setValueAtTime(0.15, ctx.currentTime); + gain.gain.setValueAtTime(0.12, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.08); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.08); } catch (_) {} } + // playOpenSound: bright ascending chime when modal opens (Enter key). + function playOpenSound() { + try { + const ctx = new (window.AudioContext || window.webkitAudioContext)(); + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.connect(gain); gain.connect(ctx.destination); + osc.type = 'triangle'; + osc.frequency.setValueAtTime(440, ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(880, ctx.currentTime + 0.14); + gain.gain.setValueAtTime(0.10, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.20); + osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.20); + } catch (_) {} + } + + // playCloseSound: descending sweep when modal closes (Esc key). + function playCloseSound() { + try { + const ctx = new (window.AudioContext || window.webkitAudioContext)(); + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.connect(gain); gain.connect(ctx.destination); + osc.type = 'sine'; + osc.frequency.setValueAtTime(440, ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(110, ctx.currentTime + 0.15); + gain.gain.setValueAtTime(0.10, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.18); + osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.18); + } catch (_) {} + } + function openModal() { if (currentIndex < 0) return; document.getElementById('modal-content').innerHTML = posts[currentIndex].querySelector('.post-text').innerHTML; document.getElementById('post-modal').classList.add('active'); + playOpenSound(); } function closeModal() { document.getElementById('post-modal').classList.remove('active'); + playCloseSound(); } document.addEventListener('keydown', function(e) { diff --git a/internal/generator/theme_aurora.go b/internal/generator/theme_aurora.go index 9475320..4fa85f6 100644 --- a/internal/generator/theme_aurora.go +++ b/internal/generator/theme_aurora.go @@ -1,32 +1,21 @@ package generator -// auroraTemplate is a dark navy theme with a CSS-animated aurora borealis -// effect — shifting green/purple/teal gradients across the background sky. +// auroraTemplate is a dark navy theme with a WebGL aurora borealis effect — +// six wide ribbon meshes whose vertices are animated with overlapping sine waves, +// rendered with additive blending to create the characteristic glow. const auroraTemplate = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>snonux.foo ✦ AURORA</title> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <style> :root { --green:#00ffb3; --teal:#00cfe8; --purple:#c084fc; --navy:#050d1a; } * { margin:0; padding:0; box-sizing:border-box; } body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--navy); color:#e0f8f0; overflow:hidden; height:100vh; } - /* Animated aurora bands */ - @keyframes aurora1 { 0%,100%{opacity:0.18;transform:scaleX(1) translateY(0)} 50%{opacity:0.28;transform:scaleX(1.15) translateY(-14px)} } - @keyframes aurora2 { 0%,100%{opacity:0.12;transform:scaleX(1) translateY(0)} 50%{opacity:0.22;transform:scaleX(0.88) translateY(10px)} } - @keyframes aurora3 { 0%,100%{opacity:0.10;transform:scaleX(1) skewY(0deg)} 50%{opacity:0.18;transform:scaleX(1.08) skewY(2deg)} } - .aurora-bg { position:fixed; inset:0; z-index:0; overflow:hidden; } - .aurora-bg::before { content:''; position:absolute; left:-20%; top:5%; width:140%; height:45%; - background:radial-gradient(ellipse,rgba(0,255,179,0.38) 0%,rgba(0,207,232,0.22) 40%,transparent 70%); - filter:blur(40px); animation:aurora1 12s ease-in-out infinite; } - .aurora-bg::after { content:''; position:absolute; left:10%; top:20%; width:120%; height:55%; - background:radial-gradient(ellipse,rgba(192,132,252,0.28) 0%,rgba(0,255,179,0.18) 45%,transparent 70%); - filter:blur(50px); animation:aurora2 16s ease-in-out infinite; } - .aurora-band3 { position:fixed; left:-10%; top:35%; width:130%; height:40%; z-index:0; - background:radial-gradient(ellipse,rgba(0,207,232,0.22) 0%,rgba(192,132,252,0.14) 50%,transparent 75%); - filter:blur(45px); animation:aurora3 20s ease-in-out infinite; } + #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; } .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; } header { padding:16px 28px; background:rgba(5,13,26,0.78); backdrop-filter:blur(14px); border-bottom:1px solid rgba(0,255,179,0.25); display:flex; align-items:center; justify-content:space-between; } @@ -38,8 +27,7 @@ const auroraTemplate = `<!DOCTYPE html> .logo-title .subtitle a { color:var(--green); text-decoration:none; } .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--green); } .transmit-btn { border:1px solid var(--teal); color:var(--teal); padding:9px 20px; - border-radius:20px; text-decoration:none; font-size:0.85rem; - transition:all 0.2s; } + border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; } .transmit-btn:hover { background:var(--teal); color:var(--navy); } .nav-hints { background:rgba(5,13,26,0.6); border-bottom:1px solid rgba(0,255,179,0.15); color:rgba(224,248,240,0.45); padding:5px 28px; display:flex; gap:18px; @@ -63,7 +51,6 @@ const auroraTemplate = `<!DOCTYPE html> .post-text { line-height:1.65; font-size:0.95rem; } .post-text a { color:var(--green); text-decoration:none; } .post-text a:hover { text-shadow:0 0 8px var(--green); } - .post-image { max-width:100%; border-radius:8px; margin-top:10px; } .post-audio { width:100%; margin-top:10px; } .post-modal { display:none; position:fixed; inset:0; z-index:100; background:rgba(5,13,26,0.95); backdrop-filter:blur(20px); @@ -78,8 +65,7 @@ const auroraTemplate = `<!DOCTYPE html> </style> </head> <body> - <div class="aurora-bg"></div> - <div class="aurora-band3"></div> + <canvas id="three-canvas"></canvas> <div class="overlay"> <header> <div class="logo"> @@ -109,6 +95,87 @@ const auroraTemplate = `<!DOCTYPE html> </div> </div> {{template "navmodal" .}} + <script> + // Aurora WebGL: six wide ribbon meshes whose top-row vertices are animated + // with overlapping sine waves, rendered with additive blending so they glow + // against the dark navy sky like real aurora curtains. + (function() { + var RIBBON_COUNT = 6; + var SEG_W = 60; // horizontal segments per ribbon + var ribbonColors = [0x00ffb3, 0x00cfe8, 0xc084fc, 0x00ffb3, 0x48e8d0, 0xa855f7]; + var ribbonY = [-10, -4, 2, 8, 14, 20]; + var ribbonZ = [-40, -30, -22, -15, -10, -5]; + var ribbonFreq = [0.6, 0.9, 0.7, 1.1, 0.5, 0.8]; + var ribbonPhase = [0.0, 1.2, 2.4, 0.8, 3.1, 1.7]; + var ribbonAmp = [3.0, 2.5, 2.0, 3.5, 2.2, 2.8]; + + var scene, camera, renderer, clock; + var ribbons = []; + + function initThree() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050d1a); + scene.fog = new THREE.Fog(0x050d1a, 40, 120); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200); + camera.position.set(0, 5, 30); + camera.lookAt(0, 5, 0); + + renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + + clock = new THREE.Clock(); + + for (var r = 0; r < RIBBON_COUNT; r++) { + // Wide shallow plane; we animate the top row of vertices + var geo = new THREE.PlaneGeometry(120, 8, SEG_W, 1); + var mat = new THREE.MeshBasicMaterial({ + color: ribbonColors[r], transparent: true, opacity: 0.32, + side: THREE.DoubleSide, blending: THREE.AdditiveBlending, depthWrite: false + }); + var mesh = new THREE.Mesh(geo, mat); + mesh.position.set(0, ribbonY[r], ribbonZ[r]); + scene.add(mesh); + ribbons.push({ mesh: mesh, geo: geo, freq: ribbonFreq[r], + phase: ribbonPhase[r], amp: ribbonAmp[r] }); + } + + window.addEventListener('resize', onResize); + animate(); + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + } + + function animate() { + requestAnimationFrame(animate); + var t = clock.getElapsedTime(); + + for (var r = 0; r < ribbons.length; r++) { + var rb = ribbons[r]; + var pos = rb.geo.attributes.position; + var count = pos.count; + // PlaneGeometry vertices: (SEG_W+1)*2 total; top row is every other vertex + for (var i = 0; i < count; i++) { + var x = pos.getX(i); + // Only animate top row (y > 0 in local space) for the waving top edge + if (pos.getY(i) > 0) { + pos.setY(i, rb.amp * Math.sin(t * rb.freq + x * 0.08 + rb.phase) + + rb.amp * 0.4 * Math.cos(t * rb.freq * 0.7 + x * 0.05)); + } + } + pos.needsUpdate = true; + } + renderer.render(scene, camera); + } + + initThree(); + })(); + </script> {{template "navscript" .}} </body> </html>` diff --git a/internal/generator/theme_brutalist.go b/internal/generator/theme_brutalist.go index 214c103..aa0729d 100644 --- a/internal/generator/theme_brutalist.go +++ b/internal/generator/theme_brutalist.go @@ -1,19 +1,22 @@ package generator // brutalistTemplate is a raw brutalist theme — pure black, thick white borders, -// Impact font, red as the only accent. No rounded corners anywhere. +// Impact font, red as the only accent. WebGL scene: harsh slowly-rotating boxes, +// wireframe red and solid white, no fog — brutal clarity. const brutalistTemplate = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SNONUX.FOO</title> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <style> :root { --red:#ff2200; } * { margin:0; padding:0; box-sizing:border-box; } body { font-family:Impact,'Arial Narrow',Arial,sans-serif; background:#000; color:#fff; overflow:hidden; height:100vh; } - .overlay { height:100vh; display:flex; flex-direction:column; } + #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; } + .overlay { height:100vh; display:flex; flex-direction:column; position:relative; z-index:10; } header { padding:14px 24px; background:#000; border-bottom:4px solid #fff; display:flex; align-items:center; justify-content:space-between; } .logo { display:flex; align-items:center; gap:16px; } @@ -49,7 +52,6 @@ const brutalistTemplate = `<!DOCTYPE html> .post-time { color:#aaa; font-family:'Courier New',monospace; font-size:0.82rem; } .post-text { font-family:'Arial',sans-serif; font-size:1rem; line-height:1.5; } .post-text a { color:var(--red); text-decoration:underline; } - .post-image { max-width:100%; margin-top:10px; border:3px solid #fff; } .post-audio { width:100%; margin-top:10px; } .post-modal { display:none; position:fixed; inset:0; z-index:100; background:rgba(0,0,0,0.98); overflow-y:auto; padding:40px 20px; } @@ -63,6 +65,7 @@ const brutalistTemplate = `<!DOCTYPE html> </style> </head> <body> + <canvas id="three-canvas"></canvas> <div class="overlay"> <header> <div class="logo"> @@ -92,6 +95,65 @@ const brutalistTemplate = `<!DOCTYPE html> </div> </div> {{template "navmodal" .}} + <script> + // Brutalist WebGL: harsh slowly-rotating boxes — solid white and wireframe red. + // No fog, no softness. Pure geometric violence against the black void. + (function() { + var scene, camera, renderer, clock; + var boxes = []; + + function initThree() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x000000); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200); + camera.position.set(0, 0, 40); + + renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: false }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + clock = new THREE.Clock(); + + // Box configurations: [size, posX, posY, posZ, rotSpeedX, rotSpeedY, wireframe, color] + var configs = [ + [10, 0, 0, 0, 0.002, 0.005, false, 0xffffff], + [6, 18, -6, -8, 0.004, 0.003, true, 0xff2200], + [7, -16, 5, -10, 0.003, 0.006, true, 0xff2200], + [5, 8, 12, -5, 0.006, 0.002, false, 0xff2200], + [4, -10,-10, -3, 0.005, 0.004, false, 0xffffff], + ]; + + configs.forEach(function(c) { + var geo = new THREE.BoxGeometry(c[0], c[0], c[0]); + var mat = new THREE.MeshBasicMaterial({ color: c[7], wireframe: c[6] }); + var mesh = new THREE.Mesh(geo, mat); + mesh.position.set(c[1], c[2], c[3]); + boxes.push({ mesh: mesh, rx: c[4], ry: c[5] }); + scene.add(mesh); + }); + + window.addEventListener('resize', onResize); + animate(); + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + } + + function animate() { + requestAnimationFrame(animate); + boxes.forEach(function(b) { + b.mesh.rotation.x += b.rx; + b.mesh.rotation.y += b.ry; + }); + renderer.render(scene, camera); + } + + initThree(); + })(); + </script> {{template "navscript" .}} </body> </html>` diff --git a/internal/generator/theme_glass.go b/internal/generator/theme_glass.go index 520f9b0..598a2f3 100644 --- a/internal/generator/theme_glass.go +++ b/internal/generator/theme_glass.go @@ -1,30 +1,21 @@ package generator -// glassTemplate is a glassmorphism theme — semi-transparent frosted panels -// using backdrop-filter:blur over a blurred gradient background. -// Light mode with subtle purple/blue gradient blobs and white glass cards. +// glassTemplate is a glassmorphism theme — semi-transparent frosted panels over +// a WebGL background of slowly drifting crystal icosahedron shards. +// CSS gradient blobs are replaced by the WebGL scene. const glassTemplate = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>snonux.foo · glass</title> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <style> :root { --blue:#6366f1; --purple:#a855f7; --pink:#ec4899; --text:#1e1b4b; } * { margin:0; padding:0; box-sizing:border-box; } body { font-family:'Segoe UI',system-ui,sans-serif; overflow:hidden; height:100vh; background:#f0f4ff; color:var(--text); } - /* Blurred gradient blobs that sit behind all glass panels */ - .bg-blobs { position:fixed; inset:0; z-index:0; overflow:hidden; } - .bg-blobs::before { content:''; position:absolute; top:-20%; left:-10%; width:60%; height:70%; - border-radius:50%; background:radial-gradient(circle,rgba(99,102,241,0.35),rgba(168,85,247,0.2),transparent 70%); - filter:blur(60px); } - .bg-blobs::after { content:''; position:absolute; bottom:-10%; right:-10%; width:65%; height:65%; - border-radius:50%; background:radial-gradient(circle,rgba(236,72,153,0.28),rgba(99,102,241,0.18),transparent 70%); - filter:blur(70px); } - .blob3 { position:fixed; top:40%; left:30%; width:40%; height:50%; z-index:0; - border-radius:60% 40% 70% 30%; background:radial-gradient(circle,rgba(168,85,247,0.18),transparent 65%); - filter:blur(50px); } + #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; } .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; } header { padding:16px 28px; background:rgba(255,255,255,0.55); backdrop-filter:blur(20px); border-bottom:1px solid rgba(255,255,255,0.6); display:flex; align-items:center; justify-content:space-between; @@ -39,8 +30,7 @@ const glassTemplate = `<!DOCTYPE html> .logo-title .subtitle a:hover { text-decoration:underline; } .transmit-btn { border:1px solid rgba(99,102,241,0.4); color:var(--blue); padding:9px 20px; border-radius:20px; text-decoration:none; font-size:0.85rem; - background:rgba(255,255,255,0.5); backdrop-filter:blur(8px); - transition:all 0.2s; } + background:rgba(255,255,255,0.5); backdrop-filter:blur(8px); transition:all 0.2s; } .transmit-btn:hover { background:var(--blue); color:#fff; border-color:var(--blue); } .nav-hints { background:rgba(255,255,255,0.35); backdrop-filter:blur(10px); border-bottom:1px solid rgba(255,255,255,0.5); color:#6b7280; @@ -54,16 +44,12 @@ const glassTemplate = `<!DOCTYPE html> border-radius:20px; text-decoration:none; font-size:0.82rem; background:rgba(255,255,255,0.45); backdrop-filter:blur(8px); } .page-nav a:hover { background:var(--blue); color:#fff; } - /* Glass card */ .post { background:rgba(255,255,255,0.45); backdrop-filter:blur(18px); border:1px solid rgba(255,255,255,0.6); border-radius:14px; padding:22px; margin-bottom:14px; cursor:pointer; - box-shadow:0 4px 20px rgba(99,102,241,0.08); - transition:all 0.25s; } - .post:hover { background:rgba(255,255,255,0.6); box-shadow:0 8px 30px rgba(99,102,241,0.18); - transform:translateY(-2px); } - .post-active { border-color:var(--blue) !important; - background:rgba(238,240,255,0.75) !important; + box-shadow:0 4px 20px rgba(99,102,241,0.08); transition:all 0.25s; } + .post:hover { background:rgba(255,255,255,0.6); box-shadow:0 8px 30px rgba(99,102,241,0.18); transform:translateY(-2px); } + .post-active { border-color:var(--blue) !important; background:rgba(238,240,255,0.75) !important; box-shadow:0 0 0 2px rgba(99,102,241,0.3),0 8px 30px rgba(99,102,241,0.2), inset 3px 0 0 var(--blue) !important; } .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; } @@ -71,8 +57,6 @@ const glassTemplate = `<!DOCTYPE html> .post-text { line-height:1.65; font-size:0.95rem; } .post-text a { color:var(--blue); text-decoration:none; } .post-text a:hover { text-decoration:underline; } - .post-image { max-width:100%; border-radius:10px; margin-top:10px; - border:1px solid rgba(255,255,255,0.5); } .post-audio { width:100%; margin-top:10px; } .post-modal { display:none; position:fixed; inset:0; z-index:100; background:rgba(240,244,255,0.85); backdrop-filter:blur(28px); @@ -87,8 +71,7 @@ const glassTemplate = `<!DOCTYPE html> </style> </head> <body> - <div class="bg-blobs"></div> - <div class="blob3"></div> + <canvas id="three-canvas"></canvas> <div class="overlay"> <header> <div class="logo"> @@ -118,6 +101,83 @@ const glassTemplate = `<!DOCTYPE html> </div> </div> {{template "navmodal" .}} + <script> + // Glass WebGL: 8 crystal icosahedron shards, each rendered as a semi-transparent + // solid mesh with a wireframe overlay, drifting and rotating slowly in the light bg. + // alpha:true so the light body background (#f0f4ff) shows through the canvas. + (function() { + var scene, camera, renderer, clock; + var shards = []; + + function initThree() { + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200); + camera.position.set(0, 0, 40); + + renderer = new THREE.WebGLRenderer({ + canvas: document.getElementById('three-canvas'), + antialias: true, alpha: true + }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.setClearColor(0xf0f4ff, 1); + clock = new THREE.Clock(); + + scene.add(new THREE.AmbientLight(0xffffff, 0.8)); + var pl = new THREE.PointLight(0x7c3aed, 2, 80); + pl.position.set(10, 10, 10); + scene.add(pl); + + var colors = [0x6366f1, 0xa855f7, 0xec4899, 0x6366f1, 0x818cf8, 0xc084fc, 0xf472b6, 0x6366f1]; + var sizes = [5, 3.5, 2.5, 4, 3, 2, 4.5, 2.2]; + var details = [2, 1, 0, 2, 1, 0, 1, 2]; + var positions = [ + [-12, 6,-8], [14,-4,-12], [0,12,-6], [-8,-10,-15], + [16, 8,-4], [-14,2,-10], [6,-8,-5], [-4,10,-14] + ]; + + for (var i = 0; i < 8; i++) { + var geo = new THREE.IcosahedronGeometry(sizes[i], details[i]); + var solid = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ + color: colors[i], transparent: true, opacity: 0.12, side: THREE.DoubleSide + })); + var wire = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ + color: colors[i], wireframe: true, transparent: true, opacity: 0.28 + })); + solid.position.set(positions[i][0], positions[i][1], positions[i][2]); + wire.position.copy(solid.position); + + var rx = (Math.random() - 0.5) * 0.004; + var ry = (Math.random() - 0.5) * 0.006; + var rz = (Math.random() - 0.5) * 0.003; + shards.push({ solid: solid, wire: wire, rx: rx, ry: ry, rz: rz }); + scene.add(solid); + scene.add(wire); + } + + window.addEventListener('resize', onResize); + animate(); + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + } + + function animate() { + requestAnimationFrame(animate); + shards.forEach(function(s) { + s.solid.rotation.x += s.rx; s.solid.rotation.y += s.ry; s.solid.rotation.z += s.rz; + s.wire.rotation.copy(s.solid.rotation); + }); + renderer.render(scene, camera); + } + + initThree(); + })(); + </script> {{template "navscript" .}} </body> </html>` diff --git a/internal/generator/theme_matrix.go b/internal/generator/theme_matrix.go index 6d8b531..8d35d7c 100644 --- a/internal/generator/theme_matrix.go +++ b/internal/generator/theme_matrix.go @@ -3,22 +3,27 @@ package generator // matrixTemplate is a hacker-style theme inspired by The Matrix — black // background, bright matrix-green (#00ff41) text, monospace throughout, // no decorations beyond a faint scanline overlay. +// WebGL scene: digital rain particle columns that simulate the iconic falling +// green characters, with vertex-colour fading from bright head to dark tail. const matrixTemplate = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>snonux.foo // MATRIX</title> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <style> :root { --g:#00ff41; --g2:#008f11; --g3:#003b00; --bg:#000; } * { margin:0; padding:0; box-sizing:border-box; } body { font-family:'Courier New',Courier,monospace; background:var(--bg); color:var(--g); overflow:hidden; height:100vh; } - /* scanline overlay */ + /* scanline overlay sits above WebGL */ body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none; background:repeating-linear-gradient(0deg,transparent,transparent 3px, rgba(0,0,0,0.08) 3px,rgba(0,0,0,0.08) 4px); } @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} } + /* WebGL background canvas */ + #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; } .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; } header { padding:12px 24px; background:#000; border-bottom:1px solid var(--g2); display:flex; align-items:center; justify-content:space-between; } @@ -68,6 +73,7 @@ const matrixTemplate = `<!DOCTYPE html> </style> </head> <body> + <canvas id="three-canvas"></canvas> <div class="overlay"> <header> <div class="logo"> @@ -97,6 +103,100 @@ const matrixTemplate = `<!DOCTYPE html> </div> </div> {{template "navmodal" .}} + <script> + // Matrix WebGL scene: 80 columns of falling particles with per-vertex colour. + // Each column has a "head" that falls at a random speed; particles near the head + // are bright green and fade to near-black further behind, simulating digital rain. + (function() { + var NUM_COLS = 80; // number of rain columns + var COL_LEN = 25; // particles per column + var SPACING = 2.2; // vertical gap between particles in a column + var Y_TOP = 30; // world-space top of the rain field + var Y_BOTTOM = -30; // world-space bottom + + var scene, camera, renderer; + var points; + var posArr, colArr; + // Per-column state: x position, head y, and fall speed + var colX = [], headY = [], speed = []; + + function initThree() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x000000); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200); + camera.position.set(0, 0, 50); + + renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: false }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + + var totalPts = NUM_COLS * COL_LEN; + posArr = new Float32Array(totalPts * 3); + colArr = new Float32Array(totalPts * 3); + + // Spread columns across x: -50..50; initialise heads at random y positions + for (var c = 0; c < NUM_COLS; c++) { + colX[c] = -50 + (c / (NUM_COLS - 1)) * 100; + headY[c] = Y_TOP - Math.random() * (Y_TOP - Y_BOTTOM); + speed[c] = 0.08 + Math.random() * 0.07; // 0.08–0.15 units per frame + } + + var geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(posArr, 3)); + geo.setAttribute('color', new THREE.BufferAttribute(colArr, 3)); + + var mat = new THREE.PointsMaterial({ size: 0.35, vertexColors: true }); + points = new THREE.Points(geo, mat); + scene.add(points); + + window.addEventListener('resize', onResize); + animate(); + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + } + + function animate() { + requestAnimationFrame(animate); + + for (var c = 0; c < NUM_COLS; c++) { + // Advance the head downward each frame + headY[c] -= speed[c]; + // When the head exits the bottom, reset to a random point above the top + if (headY[c] < Y_BOTTOM - COL_LEN * SPACING) { + headY[c] = Y_TOP + Math.random() * 20; + } + + var base = c * COL_LEN; + for (var p = 0; p < COL_LEN; p++) { + var i = base + p; + var y = headY[c] + p * SPACING; // particles trail upward from head + posArr[i * 3] = colX[c]; + posArr[i * 3 + 1] = y; + posArr[i * 3 + 2] = 0; + + // Brightness falls off with distance behind the head: + // p=0 is the head (bright), p=COL_LEN-1 is the tail (dim) + var bright = Math.max(0, 1 - p / (COL_LEN * 0.7)); + // Head particle: #00ff41, tail: #003b00 + colArr[i * 3] = 0; + colArr[i * 3 + 1] = bright * (p === 0 ? 1.0 : 0.88); + colArr[i * 3 + 2] = bright * (p === 0 ? 0.255 : 0.04); + } + } + + points.geometry.attributes.position.needsUpdate = true; + points.geometry.attributes.color.needsUpdate = true; + renderer.render(scene, camera); + } + + initThree(); + })(); + </script> {{template "navscript" .}} </body> </html>` diff --git a/internal/generator/theme_minimal.go b/internal/generator/theme_minimal.go index ebad091..00e9a17 100644 --- a/internal/generator/theme_minimal.go +++ b/internal/generator/theme_minimal.go @@ -1,67 +1,72 @@ package generator -// minimalTemplate is a clean white theme — system font, subtle borders, -// no animations or decorations. Maximum readability. -const minimalTemplate = `<!DOCTYPE html> +// plasmaTemplate is a dark psychedelic plasma theme — overlapping translucent +// spheres with additive blending drift on sine paths, creating lava-lamp-like +// colour blobs against a near-black background. +const plasmaTemplate = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>snonux.foo</title> + <title>snonux.foo ◈ PLASMA</title> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <style> - :root { --accent:#0066cc; --border:#e2e2e2; --muted:#666; } + :root { --cyan:#00f0ff; --magenta:#ff00e0; --yellow:#ffee00; --bg:#050008; } * { margin:0; padding:0; box-sizing:border-box; } - body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; - background:#fff; color:#111; overflow:hidden; height:100vh; } - .overlay { height:100vh; display:flex; flex-direction:column; max-width:860px; - margin:0 auto; } - header { padding:20px 32px; border-bottom:1px solid var(--border); - display:flex; align-items:center; justify-content:space-between; } + body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--bg); + color:#e8e0ff; overflow:hidden; height:100vh; } + #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; } + .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; } + header { padding:16px 28px; background:rgba(5,0,8,0.8); backdrop-filter:blur(14px); + border-bottom:1px solid rgba(0,240,255,0.2); display:flex; align-items:center; justify-content:space-between; } .logo { display:flex; align-items:center; gap:14px; } - .logo-mark { font-size:1.5rem; font-weight:800; color:#111; letter-spacing:-1px; } - .logo-title h1 { font-size:1.35rem; font-weight:700; color:#111; letter-spacing:-0.5px; } - .logo-title .subtitle { font-size:0.78rem; color:var(--muted); margin-top:1px; } - .logo-title .subtitle a { color:var(--accent); text-decoration:none; } - .logo-title .subtitle a:hover { text-decoration:underline; } - .transmit-btn { border:1px solid var(--accent); color:var(--accent); padding:8px 18px; - border-radius:5px; text-decoration:none; font-size:0.88rem; - font-weight:500; transition:all 0.15s; } - .transmit-btn:hover { background:var(--accent); color:#fff; } - .nav-hints { padding:6px 32px; border-bottom:1px solid var(--border); - display:flex; gap:18px; font-size:0.72rem; color:var(--muted); flex-wrap:wrap; } - .nav-hints kbd { background:#f5f5f5; border:1px solid #ccc; border-radius:3px; - padding:1px 5px; font-size:0.72rem; color:#333; margin:0 2px; } - .content { flex:1; overflow-y:auto; padding:0 32px; - scrollbar-width:thin; scrollbar-color:#ccc #fff; } - .page-nav { display:flex; justify-content:center; margin:16px 0; } - .page-nav a { border:1px solid var(--border); color:var(--accent); padding:8px 20px; - border-radius:5px; text-decoration:none; font-size:0.88rem; } - .page-nav a:hover { background:var(--accent); color:#fff; border-color:var(--accent); } - .post { border-bottom:1px solid var(--border); padding:22px 0; cursor:pointer; - transition:background 0.12s; } - .post:hover { background:#f8f8f8; padding-left:8px; } - .post-active { background:#eef5ff !important; border-left:3px solid var(--accent) !important; - padding-left:16px !important; } - .post-header { display:flex; justify-content:space-between; margin-bottom:10px; - font-size:0.88rem; } - .post-time { color:var(--muted); font-size:0.82rem; } - .post-text { line-height:1.65; font-size:1rem; } - .post-text a { color:var(--accent); text-decoration:none; } - .post-text a:hover { text-decoration:underline; } - .post-image { max-width:100%; border-radius:6px; margin-top:10px; border:1px solid var(--border); } + .logo-mark { font-size:2rem; font-weight:800; + background:linear-gradient(90deg,var(--cyan),var(--magenta)); + -webkit-background-clip:text; -webkit-text-fill-color:transparent; } + .logo-title h1 { font-size:1.5rem; font-weight:700; color:#e8e0ff; } + .logo-title .subtitle { font-size:0.75rem; color:rgba(232,224,255,0.5); margin-top:2px; } + .logo-title .subtitle a { color:var(--cyan); text-decoration:none; } + .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--cyan); } + .transmit-btn { border:1px solid var(--magenta); color:var(--magenta); padding:9px 20px; + border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; } + .transmit-btn:hover { background:var(--magenta); color:var(--bg); } + .nav-hints { background:rgba(5,0,8,0.65); border-bottom:1px solid rgba(0,240,255,0.12); + color:rgba(232,224,255,0.4); padding:5px 28px; display:flex; gap:18px; + font-size:0.68rem; flex-wrap:wrap; } + .nav-hints kbd { background:rgba(0,240,255,0.1); border:1px solid rgba(0,240,255,0.3); + color:var(--cyan); border-radius:3px; padding:0 5px; margin:0 2px; } + .content { flex:1; overflow-y:auto; padding:20px 28px; + scrollbar-width:thin; scrollbar-color:var(--magenta) var(--bg); } + .page-nav { display:flex; justify-content:center; margin:14px 0; } + .page-nav a { border:1px solid var(--cyan); color:var(--cyan); padding:8px 20px; + border-radius:20px; text-decoration:none; font-size:0.82rem; } + .page-nav a:hover { background:var(--cyan); color:var(--bg); } + .post { background:rgba(10,0,20,0.75); border:1px solid rgba(0,240,255,0.18); border-radius:10px; + padding:20px; margin-bottom:14px; cursor:pointer; + transition:all 0.25s; backdrop-filter:blur(6px); } + .post:hover { border-color:var(--cyan); box-shadow:0 0 20px rgba(0,240,255,0.2); transform:translateY(-2px); } + .post-active { border-color:var(--magenta) !important; background:rgba(20,0,30,0.9) !important; + box-shadow:0 0 24px rgba(255,0,224,0.35),inset 3px 0 0 var(--magenta) !important; } + .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; } + .post-time { color:var(--cyan); font-family:monospace; font-size:0.8rem; } + .post-text { line-height:1.65; font-size:0.95rem; } + .post-text a { color:var(--cyan); text-decoration:none; } + .post-text a:hover { text-shadow:0 0 8px var(--cyan); } .post-audio { width:100%; margin-top:10px; } .post-modal { display:none; position:fixed; inset:0; z-index:100; - background:rgba(255,255,255,0.97); overflow-y:auto; padding:40px 20px; } + background:rgba(5,0,8,0.96); backdrop-filter:blur(20px); + overflow-y:auto; padding:40px 20px; } .post-modal.active { display:block; } - .modal-inner { max-width:760px; margin:0 auto; background:#fff; - border:1px solid var(--border); border-radius:6px; - box-shadow:0 4px 24px rgba(0,0,0,0.1); padding:40px; } - .modal-close { float:right; background:none; border:none; color:var(--muted); - font-size:0.9rem; cursor:pointer; } - @media(max-width:640px) { .nav-hints{display:none;} .overlay{max-width:100%;} header{padding:16px 20px;} .content{padding:0 20px;} } + .modal-inner { max-width:760px; margin:0 auto; background:rgba(10,0,25,0.98); + border:1px solid var(--magenta); border-radius:12px; + box-shadow:0 0 60px rgba(255,0,224,0.25); padding:40px; } + .modal-close { float:right; background:none; border:none; color:var(--cyan); + font-size:0.9rem; cursor:pointer; letter-spacing:1px; } + @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} } </style> </head> <body> + <canvas id="three-canvas"></canvas> <div class="overlay"> <header> <div class="logo"> @@ -72,7 +77,7 @@ const minimalTemplate = `<!DOCTYPE html> </div> </div> <div class="nav"> - <a href="https://foo.zone/about" class="transmit-btn">Transmit to Nexus</a> + <a href="https://foo.zone/about" class="transmit-btn">Transmit</a> </div> </header> {{template "navhints" .}} @@ -91,6 +96,81 @@ const minimalTemplate = `<!DOCTYPE html> </div> </div> {{template "navmodal" .}} + <script> + // Plasma WebGL: 12 large translucent spheres drifting on independent sine + // paths with additive blending — overlapping blobs mix colours and pulse + // like a lava lamp or plasma ball. Dark bg, cyan/magenta/yellow palette. + (function() { + var scene, camera, renderer, clock; + var blobs = []; + + function initThree() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050008); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200); + camera.position.set(0, 0, 40); + + renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + clock = new THREE.Clock(); + + // Each blob: [color, radius, baseX, baseY, baseZ, ampX, ampY, freqX, freqY, phaseX, phaseY] + var cfg = [ + [0x00f0ff, 10, -5, 0, -15, 8, 6, 0.30, 0.40, 0.0, 1.0], + [0xff00e0, 9, 8, -4, -18, 7, 8, 0.40, 0.30, 1.5, 0.5], + [0xffee00, 8, -8, 6, -20, 6, 7, 0.50, 0.20, 3.0, 2.0], + [0x00f0ff, 7, 4, 8, -12, 9, 5, 0.20, 0.50, 0.8, 3.5], + [0xff00e0, 9, -6, -8, -16, 8, 6, 0.35, 0.45, 2.2, 1.2], + [0xffee00, 11, 2, 2, -22, 7, 9, 0.25, 0.35, 4.0, 0.3], + [0x8800ff, 8,-12, 4, -14, 6, 7, 0.45, 0.25, 1.0, 2.5], + [0x00ff88, 7, 10, -6, -19, 8, 5, 0.30, 0.40, 3.5, 1.8], + [0xff4400, 9, 0, 10, -17, 7, 8, 0.40, 0.30, 0.5, 4.0], + [0x00f0ff, 6, -4, -4, -11, 5, 6, 0.55, 0.35, 2.8, 0.9], + [0xff00e0, 10, 6, 0, -25, 9, 5, 0.20, 0.50, 1.3, 3.2], + [0xffee00, 7,-10, -2, -13, 6, 8, 0.40, 0.30, 4.5, 1.5], + ]; + + cfg.forEach(function(c) { + var geo = new THREE.SphereGeometry(c[1], 24, 24); + var mat = new THREE.MeshBasicMaterial({ + color: c[0], transparent: true, opacity: 0.18, + blending: THREE.AdditiveBlending, depthWrite: false + }); + var mesh = new THREE.Mesh(geo, mat); + mesh.position.set(c[2], c[3], c[4]); + blobs.push({ mesh: mesh, + bx: c[2], by: c[3], + ax: c[5], ay: c[6], + fx: c[7], fy: c[8], + px: c[9], py: c[10] }); + scene.add(mesh); + }); + + window.addEventListener('resize', onResize); + animate(); + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + } + + function animate() { + requestAnimationFrame(animate); + var t = clock.getElapsedTime(); + blobs.forEach(function(b) { + b.mesh.position.x = b.bx + b.ax * Math.sin(t * b.fx + b.px); + b.mesh.position.y = b.by + b.ay * Math.cos(t * b.fy + b.py); + }); + renderer.render(scene, camera); + } + + initThree(); + })(); + </script> {{template "navscript" .}} </body> </html>` diff --git a/internal/generator/theme_ocean.go b/internal/generator/theme_ocean.go index 422cf0b..12b2151 100644 --- a/internal/generator/theme_ocean.go +++ b/internal/generator/theme_ocean.go @@ -1,25 +1,20 @@ package generator // oceanTemplate is a deep-ocean theme — dark navy/midnight blue background, -// teal/aqua/seafoam accents, subtle wave gradient at the bottom. +// WebGL animated wave surface with per-vertex sine displacement, teal/aqua accents. const oceanTemplate = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>snonux.foo ~ OCEAN</title> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <style> :root { --teal:#00b4d8; --aqua:#48cae4; --deep:#023e8a; --navy:#03045e; --foam:#caf0f8; } * { margin:0; padding:0; box-sizing:border-box; } body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--navy); color:var(--foam); overflow:hidden; height:100vh; } - /* Deep ocean gradient base */ - body { background:linear-gradient(180deg,#03045e 0%,#023e8a 60%,#0077b6 100%); } - /* Animated wave shimmer at bottom */ - @keyframes wave { 0%,100%{transform:translateX(0)} 50%{transform:translateX(-30px)} } - .wave-bottom { position:fixed; bottom:0; left:-5%; width:110%; height:120px; z-index:1; - background:radial-gradient(ellipse 80% 60% at 50% 100%,rgba(0,180,216,0.22),transparent); - animation:wave 8s ease-in-out infinite; } + #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; } .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; } header { padding:16px 28px; background:rgba(3,4,94,0.82); backdrop-filter:blur(12px); border-bottom:1px solid rgba(0,180,216,0.3); display:flex; align-items:center; justify-content:space-between; } @@ -30,8 +25,7 @@ const oceanTemplate = `<!DOCTYPE html> .logo-title .subtitle a { color:var(--aqua); text-decoration:none; } .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--teal); } .transmit-btn { border:1px solid var(--teal); color:var(--teal); padding:9px 20px; - border-radius:20px; text-decoration:none; font-size:0.85rem; - transition:all 0.2s; } + border-radius:20px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; } .transmit-btn:hover { background:var(--teal); color:var(--navy); } .nav-hints { background:rgba(3,4,94,0.65); border-bottom:1px solid rgba(0,180,216,0.18); color:rgba(202,240,248,0.45); padding:5px 28px; display:flex; gap:18px; @@ -55,7 +49,6 @@ const oceanTemplate = `<!DOCTYPE html> .post-text { line-height:1.65; font-size:0.95rem; } .post-text a { color:var(--aqua); text-decoration:none; } .post-text a:hover { text-shadow:0 0 8px var(--teal); } - .post-image { max-width:100%; border-radius:8px; margin-top:10px; } .post-audio { width:100%; margin-top:10px; } .post-modal { display:none; position:fixed; inset:0; z-index:100; background:rgba(3,4,94,0.96); backdrop-filter:blur(20px); @@ -70,7 +63,7 @@ const oceanTemplate = `<!DOCTYPE html> </style> </head> <body> - <div class="wave-bottom"></div> + <canvas id="three-canvas"></canvas> <div class="overlay"> <header> <div class="logo"> @@ -100,6 +93,80 @@ const oceanTemplate = `<!DOCTYPE html> </div> </div> {{template "navmodal" .}} + <script> + // Ocean WebGL: a large PlaneGeometry wave surface whose vertices are displaced + // each frame by two overlapping sine functions, lit by a moving teal point light. + (function() { + var scene, camera, renderer, clock; + var waveMesh, waveGeo, pointLight; + + function initThree() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x03045e); + scene.fog = new THREE.Fog(0x03045e, 30, 120); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200); + camera.position.set(0, 25, 50); + camera.lookAt(0, 0, 0); + + renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + clock = new THREE.Clock(); + + // Wave surface — high segment count so vertex displacement looks smooth + waveGeo = new THREE.PlaneGeometry(200, 200, 80, 80); + waveMesh = new THREE.Mesh(waveGeo, new THREE.MeshPhongMaterial({ + color: 0x0077b6, emissive: 0x023e8a, emissiveIntensity: 0.3, + transparent: true, opacity: 0.85, side: THREE.DoubleSide + })); + waveMesh.rotation.x = -Math.PI / 2; + waveMesh.position.y = -5; + scene.add(waveMesh); + + // Moving teal light circling above the wave + pointLight = new THREE.PointLight(0x48cae4, 2, 80); + pointLight.position.set(0, 20, 10); + scene.add(pointLight); + scene.add(new THREE.AmbientLight(0x023e8a, 0.6)); + + window.addEventListener('resize', onResize); + animate(); + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + } + + function animate() { + requestAnimationFrame(animate); + var t = clock.getElapsedTime(); + var pos = waveGeo.attributes.position; + + // Two overlapping sine waves produce realistic ocean surface chop + for (var i = 0; i < pos.count; i++) { + var x = pos.getX(i); + var z = pos.getZ(i); + pos.setY(i, + Math.sin(x * 0.05 + t * 1.2) * 1.8 + + Math.cos(z * 0.07 + t * 0.9) * 1.4 + ); + } + pos.needsUpdate = true; + waveGeo.computeVertexNormals(); + + // Light orbits lazily + pointLight.position.x = Math.cos(t * 0.3) * 30; + pointLight.position.z = Math.sin(t * 0.3) * 30; + + renderer.render(scene, camera); + } + + initThree(); + })(); + </script> {{template "navscript" .}} </body> </html>` diff --git a/internal/generator/theme_paper.go b/internal/generator/theme_paper.go index 551e224..ebf6211 100644 --- a/internal/generator/theme_paper.go +++ b/internal/generator/theme_paper.go @@ -1,69 +1,70 @@ package generator -// paperTemplate is a warm vintage newspaper theme — Georgia serif, sepia tones, -// subtle texture simulation via CSS, no animations. -const paperTemplate = `<!DOCTYPE html> +// volcanoTemplate is a dark volcanic theme — ember and spark particles rise from +// below the screen, glowing orange/red/yellow with additive blending, set against +// a deep dark-rock background with a warm lava glow at the horizon. +const volcanoTemplate = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>snonux.foo — the microblog</title> + <title>snonux.foo ▲ VOLCANO</title> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <style> - :root { --ink:#2c1810; --sepia:#c8a96e; --paper:#f5f0e8; --muted:#8b6f47; --accent:#7b2d00; } + :root { --lava:#ff4400; --ember:#ff8c00; --hot:#ffcc00; --bg:#0d0802; } * { margin:0; padding:0; box-sizing:border-box; } - body { font-family:Georgia,'Times New Roman',serif; background:var(--paper); color:var(--ink); - overflow:hidden; height:100vh; } - /* subtle paper grain simulation */ - body::after { content:''; position:fixed; inset:0; pointer-events:none; z-index:999; - background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E"); - opacity:0.4; } - .overlay { height:100vh; display:flex; flex-direction:column; max-width:800px; margin:0 auto; } - header { padding:18px 28px 14px; border-bottom:3px double var(--ink); - display:flex; align-items:center; justify-content:space-between; } + body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--bg); + color:#ffe8cc; overflow:hidden; height:100vh; } + #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; } + .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; } + header { padding:16px 28px; background:rgba(13,8,2,0.82); backdrop-filter:blur(12px); + border-bottom:1px solid rgba(255,68,0,0.3); display:flex; align-items:center; justify-content:space-between; } .logo { display:flex; align-items:center; gap:14px; } - .logo-mark { font-size:2.6rem; font-weight:700; color:var(--accent); line-height:1; font-style:italic; } - .logo-title h1 { font-size:1.6rem; font-weight:700; color:var(--ink); letter-spacing:1px; font-variant:small-caps; } - .logo-title .subtitle { font-size:0.78rem; color:var(--muted); margin-top:2px; font-style:italic; } - .logo-title .subtitle a { color:var(--accent); text-decoration:none; } - .logo-title .subtitle a:hover { text-decoration:underline; } - .transmit-btn { border:2px solid var(--ink); color:var(--ink); padding:8px 16px; - text-decoration:none; font-size:0.82rem; font-variant:small-caps; letter-spacing:1px; - transition:all 0.15s; } - .transmit-btn:hover { background:var(--ink); color:var(--paper); } - .nav-hints { background:transparent; border-bottom:1px solid var(--sepia); color:var(--muted); - padding:4px 28px; display:flex; gap:18px; font-size:0.68rem; font-family:monospace; flex-wrap:wrap; } - .nav-hints kbd { background:transparent; border:1px solid var(--muted); color:var(--ink); - padding:0 4px; font-size:0.68rem; margin:0 1px; } - .content { flex:1; overflow-y:auto; padding:16px 28px; - scrollbar-width:thin; scrollbar-color:var(--sepia) var(--paper); } + .logo-mark { font-size:2rem; font-weight:800; color:var(--ember); text-shadow:0 0 16px var(--lava); } + .logo-title h1 { font-size:1.5rem; font-weight:700; color:#ffe8cc; } + .logo-title .subtitle { font-size:0.75rem; color:rgba(255,232,204,0.5); margin-top:2px; } + .logo-title .subtitle a { color:var(--ember); text-decoration:none; } + .logo-title .subtitle a:hover { text-shadow:0 0 8px var(--lava); } + .transmit-btn { border:1px solid var(--lava); color:var(--lava); padding:9px 20px; + border-radius:4px; text-decoration:none; font-size:0.85rem; transition:all 0.2s; } + .transmit-btn:hover { background:var(--lava); color:var(--bg); } + .nav-hints { background:rgba(13,8,2,0.7); border-bottom:1px solid rgba(255,68,0,0.15); + color:rgba(255,232,204,0.4); padding:5px 28px; display:flex; gap:18px; + font-size:0.68rem; flex-wrap:wrap; } + .nav-hints kbd { background:rgba(255,68,0,0.12); border:1px solid rgba(255,68,0,0.35); + color:var(--ember); border-radius:3px; padding:0 5px; margin:0 2px; } + .content { flex:1; overflow-y:auto; padding:20px 28px; + scrollbar-width:thin; scrollbar-color:var(--lava) var(--bg); } .page-nav { display:flex; justify-content:center; margin:14px 0; } - .page-nav a { border:1px solid var(--ink); color:var(--ink); padding:7px 20px; - text-decoration:none; font-size:0.82rem; font-variant:small-caps; letter-spacing:1px; } - .page-nav a:hover { background:var(--ink); color:var(--paper); } - .post { border-bottom:1px solid var(--sepia); padding:18px 0; cursor:pointer; - transition:background 0.12s; } - .post:hover { background:#ede8dc; padding-left:8px; } - .post-active { background:#e8e0cc !important; border-left:4px solid var(--accent) !important; - padding-left:12px !important; } - .post-header { display:flex; justify-content:space-between; margin-bottom:10px; font-size:0.85rem; } - .post-time { color:var(--muted); font-family:monospace; font-size:0.78rem; } - .post-text { line-height:1.7; font-size:1rem; } - .post-text a { color:var(--accent); text-decoration:none; } - .post-text a:hover { text-decoration:underline; } - .post-image { max-width:100%; margin-top:10px; border:1px solid var(--sepia); } + .page-nav a { border:1px solid var(--ember); color:var(--ember); padding:8px 20px; + border-radius:4px; text-decoration:none; font-size:0.82rem; } + .page-nav a:hover { background:var(--lava); color:var(--bg); } + .post { background:rgba(20,8,2,0.72); border:1px solid rgba(255,68,0,0.2); border-radius:8px; + padding:20px; margin-bottom:14px; cursor:pointer; + transition:all 0.25s; backdrop-filter:blur(4px); } + .post:hover { border-color:var(--ember); box-shadow:0 0 20px rgba(255,68,0,0.25); transform:translateY(-2px); } + .post-active { border-color:var(--hot) !important; background:rgba(30,8,2,0.9) !important; + box-shadow:0 0 24px rgba(255,140,0,0.4),inset 3px 0 0 var(--hot) !important; } + .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; } + .post-time { color:var(--ember); font-family:monospace; font-size:0.8rem; } + .post-text { line-height:1.65; font-size:0.95rem; } + .post-text a { color:var(--ember); text-decoration:none; } + .post-text a:hover { text-shadow:0 0 8px var(--lava); } .post-audio { width:100%; margin-top:10px; } .post-modal { display:none; position:fixed; inset:0; z-index:100; - background:rgba(245,240,232,0.97); overflow-y:auto; padding:40px 20px; } + background:rgba(13,8,2,0.96); backdrop-filter:blur(20px); + overflow-y:auto; padding:40px 20px; } .post-modal.active { display:block; } - .modal-inner { max-width:720px; margin:0 auto; background:var(--paper); - border:2px solid var(--ink); padding:40px; - box-shadow:4px 4px 0 var(--sepia); } - .modal-close { float:right; background:none; border:none; color:var(--muted); - font-size:0.9rem; cursor:pointer; font-variant:small-caps; letter-spacing:1px; } - @media(max-width:640px) { .nav-hints{display:none;} .overlay{max-width:100%;} header{padding:14px 16px;} .content{padding:12px 16px;} } + .modal-inner { max-width:760px; margin:0 auto; background:rgba(20,8,2,0.98); + border:1px solid var(--lava); border-radius:10px; + box-shadow:0 0 60px rgba(255,68,0,0.3); padding:40px; } + .modal-close { float:right; background:none; border:none; color:var(--ember); + font-size:0.9rem; cursor:pointer; letter-spacing:1px; } + @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} .content{padding:14px 18px;} } </style> </head> <body> + <canvas id="three-canvas"></canvas> <div class="overlay"> <header> <div class="logo"> @@ -93,6 +94,107 @@ const paperTemplate = `<!DOCTYPE html> </div> </div> {{template "navmodal" .}} + <script> + // Volcano WebGL: 2000 ember particles emitted from the bottom, rising with + // drift and fade. Each particle has a randomised lifetime, speed, and hue + // shifting from hot yellow through orange to red as it rises and cools. + (function() { + var N = 2000; + var scene, camera, renderer, clock; + var points; + var posArr, colArr, alpArr; + + // Per-particle state + var px = new Float32Array(N); + var py = new Float32Array(N); + var pz = new Float32Array(N); + var vx = new Float32Array(N); // horizontal drift + var vy = new Float32Array(N); // rise speed + var life = new Float32Array(N); // 0..1, resets at 0 + var maxLife = new Float32Array(N); + + function resetParticle(i) { + // Spawn along a wide base strip at the bottom + px[i] = (Math.random() - 0.5) * 60; + py[i] = -25 + (Math.random() - 0.5) * 4; + pz[i] = (Math.random() - 0.5) * 20 - 5; + vx[i] = (Math.random() - 0.5) * 0.06; + vy[i] = 0.06 + Math.random() * 0.12; + maxLife[i] = 0.5 + Math.random() * 0.5; + life[i] = Math.random(); // stagger initial phases + } + + function initThree() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x0d0802); + scene.fog = new THREE.Fog(0x0d0802, 30, 80); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 120); + camera.position.set(0, 0, 45); + + renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: false }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + clock = new THREE.Clock(); + + posArr = new Float32Array(N * 3); + colArr = new Float32Array(N * 3); + + for (var i = 0; i < N; i++) resetParticle(i); + + var geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(posArr, 3)); + geo.setAttribute('color', new THREE.BufferAttribute(colArr, 3)); + + points = new THREE.Points(geo, new THREE.PointsMaterial({ + size: 0.25, vertexColors: true, + transparent: true, opacity: 0.9, + blending: THREE.AdditiveBlending, depthWrite: false + })); + scene.add(points); + + window.addEventListener('resize', onResize); + animate(); + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + } + + function animate() { + requestAnimationFrame(animate); + var dt = clock.getDelta(); + + for (var i = 0; i < N; i++) { + life[i] += dt / maxLife[i]; + if (life[i] > 1.0) resetParticle(i); + + py[i] += vy[i]; + px[i] += vx[i]; + + var idx = i * 3; + posArr[idx] = px[i]; + posArr[idx+1] = py[i]; + posArr[idx+2] = pz[i]; + + // Colour: young embers are yellow/hot, older ones shift to orange then red + var t = life[i]; + var fade = Math.max(0, 1 - t * 1.4); + colArr[idx] = fade; // R: always full + colArr[idx+1] = fade * Math.max(0, 1 - t * 2); // G: fades fast + colArr[idx+2] = 0; // B: never + } + + points.geometry.attributes.position.needsUpdate = true; + points.geometry.attributes.color.needsUpdate = true; + renderer.render(scene, camera); + } + + initThree(); + })(); + </script> {{template "navscript" .}} </body> </html>` diff --git a/internal/generator/theme_retro.go b/internal/generator/theme_retro.go index 82dc605..008fc92 100644 --- a/internal/generator/theme_retro.go +++ b/internal/generator/theme_retro.go @@ -3,24 +3,29 @@ package generator // retroTemplate is an amber DOS terminal theme — black background, amber // phosphor (#ffb000) text, monospace throughout, no decorations. // Distinct from terminal.go (green) — this one evokes vintage PC monitors. +// WebGL scene: spinning amber wireframe cube with orbiting octahedrons and +// dim amber star particles for a retro demo-scene feel. const retroTemplate = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SNONUX.FOO // RETRO</title> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <style> :root { --amber:#ffb000; --dim:#7a5200; --bg:#0a0800; --bg2:#050300; } * { margin:0; padding:0; box-sizing:border-box; } body { font-family:'Courier New',Courier,monospace; background:var(--bg); color:var(--amber); overflow:hidden; height:100vh; } - /* Phosphor scanlines */ + /* Phosphor scanlines overlay — sits above WebGL */ body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none; background:repeating-linear-gradient(0deg,transparent,transparent 2px, rgba(0,0,0,0.15) 2px,rgba(0,0,0,0.15) 4px); } /* Subtle glow flicker */ @keyframes amber-flicker { 0%,100%{opacity:1} 94%{opacity:0.98} 96%{opacity:0.93} } body { animation:amber-flicker 11s infinite; } + /* WebGL background canvas */ + #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; } .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; } header { padding:12px 24px; background:var(--bg2); border-bottom:2px solid var(--amber); display:flex; align-items:center; justify-content:space-between; } @@ -71,6 +76,7 @@ const retroTemplate = `<!DOCTYPE html> </style> </head> <body> + <canvas id="three-canvas"></canvas> <div class="overlay"> <header> <div class="logo"> @@ -100,6 +106,85 @@ const retroTemplate = `<!DOCTYPE html> </div> </div> {{template "navmodal" .}} + <script> + // Retro WebGL scene: amber demo-scene cube + orbiting octahedrons + star particles. + // Evokes classic 80s/90s PC demo aesthetics with amber phosphor colours. + (function() { + var scene, camera, renderer; + var mainCube, orbiters = []; + var clock = new THREE.Clock(); + + function initThree() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x0a0800); + scene.fog = new THREE.Fog(0x0a0800, 25, 90); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200); + camera.position.set(0, 0, 35); + + renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + + // Central amber wireframe cube — the demo-scene hero piece + var boxGeo = new THREE.BoxGeometry(8, 8, 8); + var boxMat = new THREE.MeshBasicMaterial({ color: 0xffb000, wireframe: true }); + mainCube = new THREE.Mesh(boxGeo, boxMat); + scene.add(mainCube); + + // 6 small octahedron wireframes evenly spaced on an orbital ring + var octoMat = new THREE.MeshBasicMaterial({ color: 0xffb000, wireframe: true }); + for (var i = 0; i < 6; i++) { + var octoGeo = new THREE.OctahedronGeometry(1.5); + var octo = new THREE.Mesh(octoGeo, octoMat.clone()); + var angle = (i / 6) * Math.PI * 2; + octo.position.set(Math.cos(angle) * 18, Math.sin(angle) * 5, Math.sin(angle) * 18); + orbiters.push({ mesh: octo, baseAngle: angle }); + scene.add(octo); + } + + // 800 dim amber background star particles + var starGeo = new THREE.BufferGeometry(); + var starPos = new Float32Array(800 * 3); + for (var j = 0; j < 800 * 3; j++) { + starPos[j] = (Math.random() - 0.5) * 120; + } + starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3)); + var starMat = new THREE.PointsMaterial({ color: 0x7a5200, size: 0.15 }); + scene.add(new THREE.Points(starGeo, starMat)); + + window.addEventListener('resize', onResize); + animate(); + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + } + + function animate() { + requestAnimationFrame(animate); + var t = clock.getElapsedTime(); + // Main cube rotates on Y and X axes for classic demo-scene look + mainCube.rotation.y = t * 0.35; + mainCube.rotation.x = t * 0.18; + // Orbiters revolve around the central cube and spin individually + for (var i = 0; i < orbiters.length; i++) { + var o = orbiters[i]; + var angle = o.baseAngle + t * 0.4; + o.mesh.position.x = Math.cos(angle) * 18; + o.mesh.position.z = Math.sin(angle) * 18; + o.mesh.position.y = Math.sin(angle * 0.7) * 4; + o.mesh.rotation.x = t * 0.9; + o.mesh.rotation.z = t * 0.6; + } + renderer.render(scene, camera); + } + + initThree(); + })(); + </script> {{template "navscript" .}} </body> </html>` diff --git a/internal/generator/theme_synthwave.go b/internal/generator/theme_synthwave.go index 8798d18..366c5b5 100644 --- a/internal/generator/theme_synthwave.go +++ b/internal/generator/theme_synthwave.go @@ -1,7 +1,8 @@ package generator -// synthwaveTemplate is the 80s retrowave theme — dark purple sky, CSS perspective -// grid floor, hot pink/orange accents, Russo One font. +// synthwaveTemplate is the 80s retrowave theme — dark purple sky, WebGL +// perspective grid with a glowing sun and scan-line rings, hot pink/orange +// accents, Russo One font. The CSS sky and grid-floor divs are replaced by WebGL. const synthwaveTemplate = `<!DOCTYPE html> <html lang="en"> <head> @@ -10,21 +11,13 @@ const synthwaveTemplate = `<!DOCTYPE html> <title>snonux.foo ⊕ SYNTHWAVE</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://fonts.googleapis.com/css2?family=Russo+One&family=Share+Tech+Mono&display=swap" rel="stylesheet"> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <style> :root { --pink:#ff2d78; --purple:#bf3fff; --orange:#ff6b2b; --bg:#0d0221; } * { margin:0; padding:0; box-sizing:border-box; } body { font-family:'Russo One','Arial Black',sans-serif; background:var(--bg); color:#fff; overflow:hidden; height:100vh; } - /* Sunset sky gradient */ - .sky { position:fixed; inset:0; z-index:0; - background:linear-gradient(180deg,#0d0221 0%,#1a0533 45%,#4a0080 72%,#8b1070 88%,#c8365a 100%); } - /* Perspective grid floor */ - .grid-floor { position:fixed; bottom:0; left:0; width:100%; height:46vh; z-index:1; - background-image:linear-gradient(rgba(255,45,120,0.35) 1px,transparent 1px), - linear-gradient(90deg,rgba(255,45,120,0.35) 1px,transparent 1px); - background-size:44px 44px; - transform:perspective(380px) rotateX(76deg); transform-origin:bottom; - mask-image:linear-gradient(to top,rgba(0,0,0,0.85) 0%,transparent 100%); } + #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; } .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; } header { padding:16px 28px; background:rgba(13,2,33,0.82); backdrop-filter:blur(10px); border-bottom:2px solid var(--pink); display:flex; align-items:center; justify-content:space-between; } @@ -61,7 +54,6 @@ const synthwaveTemplate = `<!DOCTYPE html> .post-time { color:var(--orange); font-family:'Share Tech Mono',monospace; font-size:0.85rem; } .post-text { line-height:1.6; font-size:0.95rem; font-family:'Share Tech Mono',monospace; } .post-text a { color:var(--pink); text-decoration:none; } - .post-image { max-width:100%; border-radius:6px; margin-top:10px; } .post-audio { width:100%; margin-top:10px; } .post-modal { display:none; position:fixed; inset:0; z-index:100; background:rgba(13,2,33,0.96); overflow-y:auto; padding:40px 20px; } @@ -71,12 +63,11 @@ const synthwaveTemplate = `<!DOCTYPE html> box-shadow:0 0 60px rgba(255,45,120,0.35); padding:38px; } .modal-close { float:right; background:none; border:none; color:var(--orange); font-family:'Russo One',sans-serif; font-size:0.9rem; cursor:pointer; letter-spacing:2px; } - @media(max-width:640px) { .nav-hints{display:none;} .grid-floor{height:30vh;} header{padding:12px 18px;} } + @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 18px;} } </style> </head> <body> - <div class="sky"></div> - <div class="grid-floor"></div> + <canvas id="three-canvas"></canvas> <div class="overlay"> <header> <div class="logo"> @@ -106,6 +97,93 @@ const synthwaveTemplate = `<!DOCTYPE html> </div> </div> {{template "navmodal" .}} + <script> + // Synthwave WebGL: glowing sunset sphere with horizontal scan-line rings, + // a receding grid floor, and pink star particles. Replaces CSS sky/grid. + (function() { + var scene, camera, renderer, clock; + var sun, sunRings = []; + + function initThree() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x0d0221); + scene.fog = new THREE.Fog(0x0d0221, 60, 180); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 300); + camera.position.set(0, 10, 45); + camera.lookAt(0, -5, -10); + + renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + clock = new THREE.Clock(); + + // Glowing orange sunset sphere + sun = new THREE.Mesh( + new THREE.SphereGeometry(12, 32, 16), + new THREE.MeshBasicMaterial({ color: 0xff6b2b }) + ); + sun.position.set(0, -8, -55); + scene.add(sun); + + // Horizontal scan-line rings — alternating pink/purple, stacked on the sun + var ringColors = [0xff2d78, 0xbf3fff, 0xff2d78, 0xbf3fff, 0xff2d78, 0xbf3fff, 0xff2d78, 0xbf3fff]; + for (var i = 0; i < 8; i++) { + var ring = new THREE.Mesh( + new THREE.TorusGeometry(13 + i * 1.2, 0.09, 8, 64), + new THREE.MeshBasicMaterial({ color: ringColors[i] }) + ); + ring.position.copy(sun.position); + ring.position.y += -4 + i * 1.1; + scene.add(ring); + sunRings.push(ring); + } + + // Receding grid floor + var grid = new THREE.GridHelper(200, 40, 0xff2d78, 0x4a0060); + grid.position.set(0, -18, -30); + scene.add(grid); + + // 1200 star particles scattered in a sphere shell + var starPos = new Float32Array(1200 * 3); + for (var j = 0; j < 1200 * 3; j += 3) { + var r = 80 + Math.random() * 40; + var theta = Math.random() * Math.PI * 2; + var phi = Math.acos(2 * Math.random() - 1); + starPos[j] = r * Math.sin(phi) * Math.cos(theta); + starPos[j+1] = r * Math.sin(phi) * Math.sin(theta); + starPos[j+2] = r * Math.cos(phi); + } + var starGeo = new THREE.BufferGeometry(); + starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3)); + scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({ + color: 0xff88aa, size: 0.2, transparent: true, opacity: 0.7 + }))); + + window.addEventListener('resize', onResize); + animate(); + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + } + + function animate() { + requestAnimationFrame(animate); + var t = clock.getElapsedTime(); + // Sun pulses subtly; camera drifts sideways for parallax + var pulse = 1 + 0.015 * Math.sin(t * 1.5); + sun.scale.setScalar(pulse); + sunRings.forEach(function(r) { r.scale.setScalar(pulse); }); + camera.position.x = Math.sin(t * 0.08) * 4; + renderer.render(scene, camera); + } + + initThree(); + })(); + </script> {{template "navscript" .}} </body> </html>` diff --git a/internal/generator/theme_terminal.go b/internal/generator/theme_terminal.go index 075ec25..0503d79 100644 --- a/internal/generator/theme_terminal.go +++ b/internal/generator/theme_terminal.go @@ -2,24 +2,29 @@ package generator // terminalTemplate is the green phosphor CRT terminal theme. // Monospace throughout, scanline overlay via CSS, no external dependencies. +// WebGL scene: large IcosahedronGeometry wireframe with orbiting torus particles, +// giving a rotating phosphor-green 3D orb behind the terminal interface. const terminalTemplate = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>snonux.foo // TERMINAL</title> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <style> :root { --p:#33ff33; --dim:#1a7a1a; --bg:#0a0a0a; --bg2:#050505; } * { margin:0; padding:0; box-sizing:border-box; } body { font-family:'Courier New',Courier,monospace; background:var(--bg); color:var(--p); overflow:hidden; height:100vh; position:relative; } - /* CRT scanlines */ + /* CRT scanlines sit above the WebGL canvas */ body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none; background:repeating-linear-gradient(0deg,transparent,transparent 2px, rgba(0,0,0,0.12) 2px,rgba(0,0,0,0.12) 4px); } /* Subtle screen flicker */ @keyframes flicker { 0%,100%{opacity:1} 93%{opacity:0.97} 95%{opacity:0.91} 97%{opacity:0.98} } body { animation:flicker 9s infinite; } + /* WebGL background canvas — fills the viewport behind everything */ + #three-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:1; } .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; } header { padding:12px 24px; background:var(--bg2); border-bottom:2px solid var(--p); display:flex; align-items:center; justify-content:space-between; } @@ -67,6 +72,7 @@ const terminalTemplate = `<!DOCTYPE html> </style> </head> <body> + <canvas id="three-canvas"></canvas> <div class="overlay"> <header> <div class="logo"> @@ -96,6 +102,76 @@ const terminalTemplate = `<!DOCTYPE html> </div> </div> {{template "navmodal" .}} + <script> + // Terminal WebGL scene: phosphor-green icosahedron wireframe + torus particle ring. + // The scene sits behind the CRT scanline overlay (z-index:999) and the UI (z-index:10). + (function() { + var scene, camera, renderer, icosa, particles; + var clock = new THREE.Clock(); + + function initThree() { + // Scene with pure-black background and distance fog + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x000000); + scene.fog = new THREE.Fog(0x000000, 20, 80); + + // Perspective camera positioned in front of the orb + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200); + camera.position.set(0, 0, 30); + + renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + + // Large green phosphor wireframe icosahedron — the central CRT orb + var icoGeo = new THREE.IcosahedronGeometry(8, 2); + var icoMat = new THREE.MeshBasicMaterial({ color: 0x33ff33, wireframe: true }); + icosa = new THREE.Mesh(icoGeo, icoMat); + scene.add(icosa); + + // 400 dim particles arranged on a torus path around the icosahedron + var torusGeo = new THREE.TorusGeometry(14, 3, 16, 100); + var positions = torusGeo.attributes.position; + var ptGeo = new THREE.BufferGeometry(); + var pts = new Float32Array(400 * 3); + for (var i = 0; i < 400; i++) { + // Sample vertices from the torus geometry to place particles on its surface + var idx = Math.floor(Math.random() * positions.count); + pts[i * 3] = positions.getX(idx); + pts[i * 3 + 1] = positions.getY(idx); + pts[i * 3 + 2] = positions.getZ(idx); + } + ptGeo.setAttribute('position', new THREE.BufferAttribute(pts, 3)); + var ptMat = new THREE.PointsMaterial({ color: 0x1a7a1a, size: 0.18 }); + particles = new THREE.Points(ptGeo, ptMat); + scene.add(particles); + + window.addEventListener('resize', onResize); + animate(); + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + } + + function animate() { + requestAnimationFrame(animate); + var t = clock.getElapsedTime(); + // Slow multi-axis rotation for the phosphor orb + icosa.rotation.x = t * 0.12; + icosa.rotation.y = t * 0.18; + icosa.rotation.z = t * 0.07; + // Counter-rotate particles for visual contrast + particles.rotation.y = -t * 0.08; + particles.rotation.x = t * 0.04; + renderer.render(scene, camera); + } + + initThree(); + })(); + </script> {{template "navscript" .}} </body> </html>` diff --git a/internal/generator/themes.go b/internal/generator/themes.go index 8de6193..1156982 100644 --- a/internal/generator/themes.go +++ b/internal/generator/themes.go @@ -7,9 +7,9 @@ var themeRegistry = map[string]string{ "neon": neonTemplate, "terminal": terminalTemplate, "synthwave": synthwaveTemplate, - "minimal": minimalTemplate, + "plasma": plasmaTemplate, // replaced "minimal" — psychedelic drifting blobs "brutalist": brutalistTemplate, - "paper": paperTemplate, + "volcano": volcanoTemplate, // replaced "paper" — rising ember particles "aurora": auroraTemplate, "matrix": matrixTemplate, "ocean": oceanTemplate, |
