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 |
