diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-23 08:12:46 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-23 08:12:46 +0300 |
| commit | 3df732933d241229be7d699f7340317b4743e49a (patch) | |
| tree | 29365f505328a7efa6ecde31d7530e2ff0dd4212 /internal | |
| parent | 925b64bc16af9a6b2daff8468e54d86e6935fe26 (diff) | |
new
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/generator/templates/shared/nav.tmpl | 112 | ||||
| -rw-r--r-- | internal/generator/templates/themes/biomech.tmpl | 217 | ||||
| -rw-r--r-- | internal/generator/templates/themes/cathedral.tmpl | 303 | ||||
| -rw-r--r-- | internal/generator/templates/themes/noir.tmpl | 308 | ||||
| -rw-r--r-- | internal/generator/templates/themes/surveillance.tmpl | 224 |
5 files changed, 1135 insertions, 29 deletions
diff --git a/internal/generator/templates/shared/nav.tmpl b/internal/generator/templates/shared/nav.tmpl index ae161dc..346294a 100644 --- a/internal/generator/templates/shared/nav.tmpl +++ b/internal/generator/templates/shared/nav.tmpl @@ -868,7 +868,13 @@ a.header-feed-link:hover { opacity:1; text-decoration:underline; } #splash-overlay .splash-inner { position:relative; z-index:2; max-width:min(520px,92vw); padding: clamp(1.15rem, 3.2vw, 1.75rem) clamp(1.3rem, 3.8vw, 1.95rem); border-radius:14px; background: rgba(0, 0, 0, 0.58); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); - box-shadow: 0 14px 44px rgba(0, 0, 0, 0.58), inset 0 1px 0 rgba(255, 255, 255, 0.07); } + box-shadow: 0 14px 44px rgba(0, 0, 0, 0.58), inset 0 1px 0 rgba(255, 255, 255, 0.07); + will-change:transform; } +.splash-controls { margin-top:0.55rem; font-size:0.58rem; letter-spacing:0.14em; + text-transform:uppercase; opacity:0.55; line-height:1.6; } +.splash-controls kbd { display:inline-block; background:rgba(255,255,255,0.08); + border:1px solid rgba(255,255,255,0.2); border-radius:3px; padding:0 4px; + font-family:monospace; font-size:0.62rem; margin:0 1px; } #splash-overlay.splash-brutalist .splash-inner.splash-frame { padding: clamp(1.4rem, 4.5vw, 2.25rem) clamp(1.1rem, 3.5vw, 1.9rem); background: rgba(0, 0, 0, 0.78); } html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidden !important; pointer-events:none !important; } @@ -1288,11 +1294,13 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde } catch (_) {} } function dismiss() { + if (typeof splashDrift !== 'undefined') splashDrift.stop(); if (el.classList.contains('splash--dismissed')) return; el.classList.add('splash--dismissed'); el.setAttribute('aria-hidden', 'true'); } function show() { + if (typeof splashDrift !== 'undefined') splashDrift.reset(); document.documentElement.classList.remove('sno-splash-skip'); el.classList.remove('splash--dismissed'); el.removeAttribute('aria-hidden'); @@ -1301,6 +1309,7 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde function openSplashFromHeader(e) { if (e.target.closest('a')) return; e.preventDefault(); + if (typeof modalDrift !== 'undefined') modalDrift.stop(); var modal = document.getElementById('post-modal'); if (modal) modal.classList.remove('active'); show(); @@ -1479,31 +1488,28 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde if (wild) snonuxPulseFlash(window._snonuxWildFlashColor, 200); } - // === MODAL DRIFT — arrow/hjkl push the modal around with momentum === - var modalDrift = (function() { - var x = 0, y = 0, vx = 0, vy = 0; - var raf = null; - var PUSH = 12; - var FRICTION = 0.92; - var BOUNCE_DAMP = 0.5; - var STOP_THRESHOLD = 0.3; - - function getInner() { - return document.querySelector('#post-modal .modal-inner'); - } + // === DRIFT PHYSICS — reusable controller for floating panels === + function makeDriftController(getEl, opts) { + var x = 0, y = 0, vx = 0, vy = 0, raf = null; + var PUSH = opts.push || 12; + var FRICTION = opts.friction || 0.92; + var BOUNCE_DAMP = opts.bounceDamp || 0.5; + var STOP_THRESHOLD = opts.stopThreshold || 0.3; function clampAndBounce() { - var mi = getInner(); - if (!mi) return; - var w = mi.offsetWidth, h = mi.offsetHeight; + var el = getEl(); + if (!el) return; + var w = el.offsetWidth, h = el.offsetHeight; var maxX = (window.innerWidth - w) / 2; var maxY = (window.innerHeight - h) / 2; if (maxX < 0) maxX = window.innerWidth * 0.3; if (maxY < 0) maxY = window.innerHeight * 0.3; - if (x > maxX) { x = maxX; vx = -vx * BOUNCE_DAMP; } - if (x < -maxX) { x = -maxX; vx = -vx * BOUNCE_DAMP; } - if (y > maxY) { y = maxY; vy = -vy * BOUNCE_DAMP; } - if (y < -maxY) { y = -maxY; vy = -vy * BOUNCE_DAMP; } + var hit = false; + if (x > maxX) { x = maxX; vx = -vx * BOUNCE_DAMP; hit = true; } + if (x < -maxX) { x = -maxX; vx = -vx * BOUNCE_DAMP; hit = true; } + if (y > maxY) { y = maxY; vy = -vy * BOUNCE_DAMP; hit = true; } + if (y < -maxY) { y = -maxY; vy = -vy * BOUNCE_DAMP; hit = true; } + if (hit && opts.onBounce) opts.onBounce(el, x, y, vx, vy); } function tick() { @@ -1512,8 +1518,11 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde x += vx; y += vy; clampAndBounce(); - var mi = getInner(); - if (mi) mi.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)'; + var el = getEl(); + if (el) { + if (opts.applyTransform) opts.applyTransform(el, x, y, vx, vy); + else el.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)'; + } if (Math.abs(vx) > STOP_THRESHOLD || Math.abs(vy) > STOP_THRESHOLD) { raf = requestAnimationFrame(tick); } else { @@ -1533,27 +1542,63 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde case 'l': case 'ArrowRight': dx = PUSH; break; case 'k': case 'ArrowUp': dy = -PUSH; break; case 'j': case 'ArrowDown': dy = PUSH; break; - default: return; + default: return false; } e.preventDefault(); vx += dx; vy += dy; - playNavSound(); ensureLoop(); + return true; }, + kick: function(dx, dy) { vx += (dx || 0); vy += (dy || 0); ensureLoop(); }, reset: function() { x = 0; y = 0; vx = 0; vy = 0; - var mi = getInner(); - if (mi) mi.style.transform = ''; + var el = getEl(); + if (el) el.style.transform = ''; if (raf) { cancelAnimationFrame(raf); raf = null; } }, stop: function() { if (raf) { cancelAnimationFrame(raf); raf = null; } - var mi = getInner(); - if (mi) mi.style.transform = ''; + var el = getEl(); + if (el) el.style.transform = ''; x = 0; y = 0; vx = 0; vy = 0; } }; + } + + // === MODAL DRIFT — arrow/hjkl push the modal around with momentum === + var modalDrift = makeDriftController( + function() { return document.querySelector('#post-modal .modal-inner'); }, + { push: 12, friction: 0.92, bounceDamp: 0.5, stopThreshold: 0.3 } + ); + + // === SPLASH DRIFT — same physics on the splash panel with velocity tilt === + var splashDrift = makeDriftController( + function() { return document.querySelector('#splash-overlay .splash-inner'); }, + { + push: 14, + friction: 0.93, + bounceDamp: 0.45, + stopThreshold: 0.25, + applyTransform: function(el, x, y, vx) { + var rot = Math.max(-5, Math.min(5, vx * 0.15)); + el.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px) rotate(' + rot.toFixed(2) + 'deg)'; + }, + onBounce: function() { + playBounceSound(); + if (window._snoWildActive) snonuxPulseFlash(window._snonuxWildFlashColor, 180); + } + } + ); + + // Inject keyboard controls hint into splash overlay (all themes) + (function enhanceSplashHint() { + var hint = document.querySelector('#splash-overlay .splash-hint'); + if (!hint || document.querySelector('#splash-overlay .splash-controls')) return; + var extra = document.createElement('div'); + extra.className = 'splash-controls'; + extra.innerHTML = '<kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> drift \u2022 <kbd>w</kbd> wild \u2022 <kbd>Enter</kbd> open'; + hint.appendChild(extra); })(); function openPostAt(index, scrollIntoView) { @@ -1612,12 +1657,21 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde if (e.key === 'Enter' || e.key === ' ' || e.key === 'Escape') { e.preventDefault(); if (window._snonuxDismissSplash) window._snonuxDismissSplash(); + } else if (e.key === 'w' && !e.repeat) { + e.preventDefault(); + window._snoWildActive = !window._snoWildActive; + if (window.snonuxWildToggle) window.snonuxWildToggle(); + snonuxSetWildState(window._snoWildActive); + snonuxWildFlash(window._snoWildActive); + splashDrift.kick((Math.random() - 0.5) * 24, -10 - Math.random() * 10); + } else if (splashDrift.keyPush(e)) { + playNavSound(); } return; } if (document.getElementById('post-modal').classList.contains('active')) { if (e.key === 'Escape') { closeModal(); e.preventDefault(); } - else { modalDrift.keyPush(e); } + else if (modalDrift.keyPush(e)) { playNavSound(); } return; } switch (e.key) { diff --git a/internal/generator/templates/themes/biomech.tmpl b/internal/generator/templates/themes/biomech.tmpl new file mode 100644 index 0000000..9773d96 --- /dev/null +++ b/internal/generator/templates/themes/biomech.tmpl @@ -0,0 +1,217 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>snonux.foo // BIOMECH</title> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Oxanium:wght@400;600;700&family=IBM+Plex+Mono:wght@400;700&display=swap" rel="stylesheet"> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> + <style> + :root { --bone:#d0c7bb; --flesh:#803f5d; --vein:#f55b7d; --acid:#93ffd8; --steel:#2d3642; --bg:#09070d; } + * { margin:0; padding:0; box-sizing:border-box; } + body { font-family:'Oxanium',system-ui,sans-serif; background:var(--bg); color:var(--bone); overflow:hidden; height:100vh; } + #three-canvas { position:fixed; inset:0; width:100%; height:100%; z-index:1; } + .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; } + header { padding:16px 26px; background:rgba(9,7,13,0.84); backdrop-filter:blur(10px); border-bottom:1px solid rgba(147,255,216,0.16); display:flex; align-items:center; justify-content:space-between; } + .logo { display:flex; align-items:center; gap:14px; } + .logo-mark { font-size:1.8rem; color:var(--acid); text-shadow:0 0 18px rgba(147,255,216,0.24); } + .logo-title h1 { font-size:1.42rem; color:var(--bone); letter-spacing:0.08em; } + .logo-title .subtitle { font-size:0.74rem; color:rgba(208,199,187,0.56); margin-top:2px; } + .logo-title .subtitle a { color:var(--acid); text-decoration:none; } + .logo-title .subtitle a:hover { color:#dffff6; } + .transmit-btn { border:1px solid rgba(147,255,216,0.22); color:var(--acid); padding:8px 16px; text-decoration:none; font-size:0.78rem; letter-spacing:0.2em; text-transform:uppercase; transition:all 0.18s; } + .transmit-btn:hover { background:rgba(147,255,216,0.1); } + a.header-feed-link { color:rgba(208,199,187,0.68); } + a.header-feed-link:hover { color:var(--acid); } + .nav-hints { background:rgba(12,10,18,0.74); border-bottom:1px solid rgba(147,255,216,0.08); color:rgba(208,199,187,0.44); padding:5px 26px; display:flex; gap:18px; font-size:0.66rem; letter-spacing:0.08em; flex-wrap:wrap; } + .nav-hints kbd { background:rgba(128,63,93,0.14); border:1px solid rgba(147,255,216,0.18); color:var(--acid); padding:0 5px; margin:0 2px; } + .content { flex:1; overflow-y:auto; padding:20px 26px; scrollbar-width:thin; scrollbar-color:#6d4a69 #120d16; } + .page-nav { display:flex; justify-content:center; margin:14px 0; } + .page-nav a { border:1px solid rgba(147,255,216,0.18); color:var(--acid); padding:8px 18px; text-decoration:none; font-size:0.78rem; letter-spacing:0.2em; text-transform:uppercase; } + .page-nav a:hover { background:rgba(147,255,216,0.08); } + .page-nav-footer { flex-shrink:0; padding:8px 26px; display:flex; justify-content:center; background:rgba(9,7,13,0.84); backdrop-filter:blur(10px); border-top:1px solid rgba(147,255,216,0.16); } + .post { background:linear-gradient(180deg, rgba(33,20,31,0.9), rgba(12,9,18,0.92)); border:1px solid rgba(147,255,216,0.08); padding:18px; margin-bottom:13px; cursor:pointer; box-shadow:0 16px 38px rgba(0,0,0,0.28); transition:border-color 0.18s, box-shadow 0.18s, transform 0.18s; } + .post:hover { border-color:rgba(147,255,216,0.22); transform:translateY(-1px); } + .post-active { border-color:rgba(245,91,125,0.28) !important; background:linear-gradient(180deg, rgba(46,18,34,0.94), rgba(13,9,17,0.95)) !important; + box-shadow:0 0 0 1px rgba(147,255,216,0.08), 0 18px 42px rgba(0,0,0,0.42), inset 4px 0 0 var(--vein) !important; } + .post-header { display:flex; justify-content:space-between; margin-bottom:10px; font-size:0.84rem; } + .post-header strong { color:var(--acid); } + .post-time { color:rgba(208,199,187,0.58); font-family:'IBM Plex Mono',monospace; } + .post-text { line-height:1.7; font-size:0.92rem; } + .post-text a { color:var(--acid); text-decoration:none; border-bottom:1px solid rgba(147,255,216,0.18); } + .post-image { margin-top:10px; border:1px solid rgba(147,255,216,0.1); filter:saturate(0.9) hue-rotate(-14deg) contrast(1.06); } + .post-audio { width:100%; margin-top:10px; filter:hue-rotate(-14deg); } + .post-modal { display:none; position:fixed; inset:0; z-index:100; overflow-y:auto; padding:40px 20px; } + .post-modal.active { display:block; } + .modal-inner { max-width:760px; margin:0 auto; background:rgba(11,9,16,0.98); border:1px solid rgba(147,255,216,0.18); padding:34px; box-shadow:0 22px 76px rgba(0,0,0,0.72); } + .modal-close { float:right; background:none; border:none; color:var(--acid); font-family:'IBM Plex Mono',monospace; font-size:0.78rem; cursor:pointer; letter-spacing:0.18em; } + @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 16px;} .content{padding:14px 16px;} .modal-inner{padding:24px 16px;} } + .splash-overlay.splash-biomech { + background: + radial-gradient(circle at 50% 22%, rgba(245,91,125,0.14) 0%, transparent 28%), + radial-gradient(circle at 50% 80%, rgba(147,255,216,0.08) 0%, transparent 42%), + linear-gradient(180deg, #100b14 0%, #050407 100%); + } + .splash-biomech .splash-pod { position:absolute; left:50%; top:10vh; width:min(34vw,220px); height:min(46vw,290px); transform:translateX(-50%); border-radius:48% 48% 42% 42% / 54% 54% 38% 38%; + background:radial-gradient(circle at 50% 35%, rgba(147,255,216,0.18) 0%, rgba(147,255,216,0.06) 28%, rgba(128,63,93,0.38) 62%, rgba(12,9,18,0.8) 100%); + box-shadow:0 0 42px rgba(245,91,125,0.14); opacity:0.72; z-index:1; } + .splash-biomech .splash-title { font-size:clamp(1.55rem,5vw,2.1rem); color:var(--bone); letter-spacing:0.12em; } + .splash-biomech .splash-tag { color:var(--acid); letter-spacing:0.22em; } + .splash-biomech .splash-hint { color:rgba(208,199,187,0.78); } + .splash-biomech .splash-inner { text-shadow:0 2px 22px rgba(0,0,0,0.95); } +{{template "navSharedCSSInner"}} + </style> +</head> +<body> + {{template "splashGate"}} + <div id="splash-overlay" class="splash-overlay splash-biomech" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1"> + <canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas> + <div class="splash-pod" aria-hidden="true"></div> + <div class="splash-inner"> + <div class="splash-title">snonux.foo</div> + <div class="splash-tag">Containment Membrane</div> + <div class="splash-hint">Click or Enter to breach the shell</div> + </div> + </div> + <script> + (function(){ + if(document.documentElement.classList.contains('sno-splash-skip'))return; + var cv=document.getElementById('splash-gl-canvas'); + if(!cv||typeof THREE==='undefined')return; + var raf,ren,sc,ca,clock,core; + function cleanup(){window.removeEventListener('resize',sz);if(raf)cancelAnimationFrame(raf);raf=null;if(ren)ren.dispose();ren=null;window._snonuxSplashWebGLCleanup=null;} + window._snonuxSplashWebGLCleanup=cleanup; + function sz(){var w=cv.clientWidth||2,h=cv.clientHeight||2;if(ren)ren.setSize(w,h,false);if(ca){ca.aspect=w/h;ca.updateProjectionMatrix();}} + ren=new THREE.WebGLRenderer({canvas:cv,antialias:true,alpha:true});ren.setClearColor(0,0);ren.setPixelRatio(Math.min(window.devicePixelRatio||1,2)); + sc=new THREE.Scene();ca=new THREE.PerspectiveCamera(48,1,0.1,60);ca.position.z=9;clock=new THREE.Clock(); + core=new THREE.Mesh(new THREE.SphereGeometry(1.2,24,24),new THREE.MeshBasicMaterial({color:0xf55b7d,transparent:true,opacity:0.76})); sc.add(core); + var shell=new THREE.Mesh(new THREE.TorusKnotGeometry(2.4,0.36,80,14),new THREE.MeshBasicMaterial({color:0x93ffd8,wireframe:true,transparent:true,opacity:0.42})); sc.add(shell); shell.userData.rot=0.006; + sz();window.addEventListener('resize',sz); + function loop(){ raf=requestAnimationFrame(loop); var t=clock.getElapsedTime(); shell.rotation.x=t*0.2; shell.rotation.y=t*0.3; core.scale.setScalar(1+Math.sin(t*3.2)*0.08); ren.render(sc,ca); } + raf=requestAnimationFrame(loop); + })(); + </script> + <canvas id="three-canvas"></canvas> + <div class="overlay"> + <header> + <div class="logo"> + <span class="logo-mark">SN</span> + <div class="logo-title"> + <h1>snonux.foo</h1> + <p class="subtitle">microblog — <a href="https://foo.zone">foo.zone</a> is the real blog</p> + <p class="logo-host">Served by NetBSD on a Raspberry Pi 3</p> + </div> + </div> + <div class="nav"> + <a href="atom.xml" class="header-feed-link" rel="alternate" title="Atom feed" type="application/atom+xml">Atom feed</a> + <a href="https://foo.zone/about" class="transmit-btn">Anatomy</a> + </div> + </header> + {{template "navhints" .}} + <div class="content" id="post-content"> + {{range $i, $post := .Posts}} + <div class="post" id="post-{{$post.ID}}" data-index="{{$i}}"> + <div class="post-header"> + <div><strong>@snonux</strong></div> + <div class="post-time">{{$post.FormattedTime}}</div> + </div> + <div class="post-text">{{$post.ContentHTML}}</div> + </div> + {{end}} + </div> + {{if or .PrevPage .NextPage}} + <footer class="page-nav-footer" aria-label="Pagination"> + <div class="page-nav page-nav-dual"> + {{if .PrevPage}}<a href="{{.PrevPage}}">← Newer</a>{{end}} + {{if .NextPage}}<a href="{{.NextPage}}">Older →</a>{{end}} + </div> + </footer> + {{end}} + </div> + {{template "navmodal" .}} + <script> + (function() { + var _wild = false, _snoTOffset = 0, _snoLastT = 0; + var scene, camera, renderer, clock, core, shellA, shellB, orbiters = []; + + function initThree() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x09070d); + scene.fog = new THREE.Fog(0x09070d, 18, 120); + camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 220); + camera.position.set(0, 6, 26); + 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(); + scene.add(new THREE.AmbientLight(0x553a47, 0.45)); + var coreLight = new THREE.PointLight(0xf55b7d, 1.6, 80); coreLight.position.set(0,0,0); scene.add(coreLight); + + core = new THREE.Mesh(new THREE.SphereGeometry(4.2, 36, 36), new THREE.MeshPhongMaterial({ color:0x803f5d, emissive:0xf55b7d, emissiveIntensity:0.52, shininess:90 })); + shellA = new THREE.Mesh(new THREE.TorusKnotGeometry(7.4, 0.45, 180, 24, 2, 5), new THREE.MeshBasicMaterial({ color:0x93ffd8, wireframe:true, transparent:true, opacity:0.34 })); + shellB = new THREE.Mesh(new THREE.TorusKnotGeometry(5.9, 0.28, 160, 16, 3, 7), new THREE.MeshBasicMaterial({ color:0xd0c7bb, wireframe:true, transparent:true, opacity:0.18 })); + scene.add(core); scene.add(shellA); scene.add(shellB); + for (var i = 0; i < 9; i++) { + var orb = new THREE.Mesh(new THREE.SphereGeometry(0.55 + Math.random() * 0.45, 14, 14), new THREE.MeshPhongMaterial({ color: i % 2 === 0 ? 0x93ffd8 : 0xf55b7d, emissive: i % 2 === 0 ? 0x24473b : 0x5b1f32, emissiveIntensity:0.45 })); + orb.userData.radius = 11 + Math.random() * 8; + orb.userData.speed = 0.2 + Math.random() * 0.5; + orb.userData.phase = Math.random() * Math.PI * 2; + orbiters.push(orb); scene.add(orb); + } + 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 realT = clock.getElapsedTime(); + _snoTOffset += (realT - _snoLastT) * (_wild ? 11 : 0); + _snoLastT = realT; + var t = realT + _snoTOffset; + core.scale.setScalar(1 + Math.sin(t * (_wild ? 6 : 1.8)) * (_wild ? 0.22 : 0.08)); + shellA.rotation.x = t * (_wild ? 0.9 : 0.25); shellA.rotation.y = t * (_wild ? 1.2 : 0.32); + shellB.rotation.y = -t * (_wild ? 1.1 : 0.22); shellB.rotation.z = t * (_wild ? 0.8 : 0.18); + shellA.material.opacity = _wild ? 0.54 : 0.34; + for (var i = 0; i < orbiters.length; i++) { + var o = orbiters[i], a = t * o.userData.speed + o.userData.phase; + o.position.set(Math.cos(a) * o.userData.radius, Math.sin(a * 1.4) * 4, Math.sin(a) * o.userData.radius * 0.7); + } + camera.position.x = Math.sin(realT * (_wild ? 1.8 : 0.35)) * (_wild ? 3.2 : 1.1); + camera.position.y = 6 + Math.sin(realT * (_wild ? 1.2 : 0.28)) * (_wild ? 1.8 : 0.4); + camera.lookAt(0, 0, 0); + renderer.render(scene, camera); + } + + initThree(); + + function flash(css, ms) { + var d=document.createElement('div'); + d.style.cssText='position:fixed;inset:0;z-index:998;pointer-events:none;'+css+';transition:opacity '+(ms||220)+'ms'; + document.body.appendChild(d); + setTimeout(function(){d.style.opacity='0';setTimeout(function(){d.remove();},ms||220);},25); + } + window.snonuxOpenEffect = function() { + var modal=document.getElementById('post-modal'); + if(modal){modal.classList.add('sno-modal-expand');setTimeout(function(){modal.classList.remove('sno-modal-expand');},400);} + flash('background:radial-gradient(circle at center,rgba(245,91,125,0.16),transparent 70%)',240); + }; + window.snonuxCloseEffect = function(){ flash('background:rgba(0,0,0,0.3)',160); }; + window.snonuxNavEffect = function(){ flash('background:linear-gradient(90deg,transparent,rgba(147,255,216,0.1),transparent)',160); }; + window.snonuxPageEffect = function(){ flash('background:radial-gradient(circle at center,rgba(147,255,216,0.12),transparent 72%)',220); }; + window.snonuxScrollEffect = function(dir){ + var d=document.createElement('div'); + d.style.cssText='position:fixed;'+(dir==='down'?'top:0;':'bottom:0;')+'left:0;right:0;height:'+(_wild?'16px':'6px')+';z-index:9000;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(245,91,125,0.8),rgba(147,255,216,0.7),transparent);transition:transform 0.32s ease,opacity 0.32s ease;'; + document.body.appendChild(d); + setTimeout(function(){d.style.transform=dir==='down'?'translateY(100vh)':'translateY(-100vh)';d.style.opacity='0';},16); + setTimeout(function(){d.remove();},380); + }; + window.snonuxWildToggle = function(){ _wild=!_wild; var b=document.getElementById('sno-wild-badge'); if(b)b.classList.toggle('sno-wild-on',_wild); }; + })(); + </script> + {{template "navscript" .}} +</body> +</html> diff --git a/internal/generator/templates/themes/cathedral.tmpl b/internal/generator/templates/themes/cathedral.tmpl new file mode 100644 index 0000000..fb519ce --- /dev/null +++ b/internal/generator/templates/themes/cathedral.tmpl @@ -0,0 +1,303 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>snonux.foo // CATHEDRAL</title> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=Spectral:wght@400;600&display=swap" rel="stylesheet"> + <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> + <style> + :root { --gold:#e0c47f; --violet:#6f4fae; --ruby:#8e2f49; --glass:#7bc2ff; --stone:#110f16; --chalk:#f0e8d9; } + * { margin:0; padding:0; box-sizing:border-box; } + body { font-family:'Spectral',serif; background:#0f0d14; color:var(--chalk); overflow:hidden; height:100vh; } + body::before { content:''; position:fixed; inset:0; z-index:998; pointer-events:none; + background: + radial-gradient(circle at 50% 4%, rgba(224,196,127,0.1) 0%, transparent 24%), + linear-gradient(90deg, rgba(123,194,255,0.05), transparent 18%, transparent 82%, rgba(142,47,73,0.06)); + mix-blend-mode:screen; opacity:0.8; } + #three-canvas { position:fixed; inset: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(11,10,16,0.84); backdrop-filter:blur(10px); border-bottom:1px solid rgba(224,196,127,0.18); display:flex; align-items:center; justify-content:space-between; } + .logo { display:flex; align-items:center; gap:14px; } + .logo-mark { font-family:'Cinzel',serif; font-size:1.9rem; color:var(--gold); text-shadow:0 0 14px rgba(224,196,127,0.22); } + .logo-mark::after { content:'✢'; margin-left:8px; color:#fff3c8; text-shadow:0 0 12px rgba(224,196,127,0.6); } + .logo-title h1 { font-family:'Cinzel',serif; font-size:1.5rem; letter-spacing:0.1em; color:var(--chalk); } + .logo-title .subtitle { font-size:0.8rem; color:rgba(240,232,217,0.6); margin-top:2px; } + .logo-title .subtitle a { color:var(--gold); text-decoration:none; } + .logo-title .subtitle a:hover { color:#fff3c8; } + .transmit-btn { border:1px solid rgba(224,196,127,0.28); color:var(--gold); padding:8px 16px; text-decoration:none; font-size:0.8rem; letter-spacing:0.2em; text-transform:uppercase; transition:all 0.18s; } + .transmit-btn:hover { background:rgba(224,196,127,0.12); border-color:rgba(224,196,127,0.52); } + a.header-feed-link { color:rgba(224,196,127,0.84); } + a.header-feed-link:hover { color:#fff3c8; } + .nav-hints { background:rgba(17,14,22,0.72); border-bottom:1px solid rgba(224,196,127,0.08); color:rgba(240,232,217,0.48); padding:5px 28px; display:flex; gap:18px; font-size:0.68rem; letter-spacing:0.08em; flex-wrap:wrap; } + .nav-hints kbd { background:rgba(111,79,174,0.16); border:1px solid rgba(224,196,127,0.2); color:var(--gold); padding:0 5px; margin:0 2px; } + .content { flex:1; overflow-y:auto; padding:20px 28px; scrollbar-width:thin; scrollbar-color:#7e6231 #18131d; } + .page-nav { display:flex; justify-content:center; margin:14px 0; } + .page-nav a { border:1px solid rgba(224,196,127,0.2); color:var(--gold); padding:8px 18px; text-decoration:none; font-size:0.8rem; letter-spacing:0.16em; text-transform:uppercase; } + .page-nav a:hover { background:rgba(224,196,127,0.08); } + .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center; background:rgba(11,10,16,0.84); backdrop-filter:blur(10px); border-top:1px solid rgba(224,196,127,0.18); } + .post { position:relative; background: + linear-gradient(180deg, rgba(30,19,30,0.93), rgba(13,10,17,0.95)), + radial-gradient(circle at 14% 0%, rgba(123,194,255,0.08), transparent 28%); + border:1px solid rgba(224,196,127,0.08); padding:20px; margin-bottom:14px; cursor:pointer; + box-shadow:0 16px 38px rgba(0,0,0,0.28); transition:border-color 0.2s, box-shadow 0.2s, transform 0.2s; } + .post::before { content:''; position:absolute; inset:0; pointer-events:none; background:linear-gradient(120deg, rgba(123,194,255,0.05), transparent 36%, rgba(142,47,73,0.06) 68%, transparent); } + .post:hover { border-color:rgba(224,196,127,0.24); transform:translateY(-1px); box-shadow:0 22px 42px rgba(0,0,0,0.42); } + .post-active { border-color:rgba(224,196,127,0.36) !important; + background:linear-gradient(180deg, rgba(42,19,33,0.96), rgba(15,10,18,0.96)) !important; + box-shadow:0 0 0 1px rgba(224,196,127,0.12), 0 22px 44px rgba(0,0,0,0.46), inset 4px 0 0 var(--gold) !important; } + .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; } + .post-header strong { color:var(--gold); font-family:'Cinzel',serif; } + .post-time { color:rgba(240,232,217,0.58); } + .post-text { line-height:1.72; font-size:1rem; } + .post-text a { color:#cfe2ff; text-decoration:none; border-bottom:1px solid rgba(207,226,255,0.22); } + .post-text a:hover { border-color:rgba(207,226,255,0.72); } + .post-image { margin-top:10px; border:1px solid rgba(224,196,127,0.12); filter:saturate(0.9) contrast(1.06); } + .post-audio { width:100%; margin-top:10px; filter:sepia(0.12) contrast(0.92); } + .post-modal { display:none; position:fixed; inset:0; z-index:100; overflow-y:auto; padding:40px 20px; } + .post-modal.active { display:block; } + .modal-inner { max-width:800px; margin:0 auto; background:rgba(15,11,18,0.98); border:1px solid rgba(224,196,127,0.2); padding:38px; box-shadow:0 28px 84px rgba(0,0,0,0.72); } + .modal-close { float:right; background:none; border:none; color:var(--gold); font-family:'Cinzel',serif; font-size:0.8rem; cursor:pointer; letter-spacing:0.14em; } + @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 16px;} .content{padding:14px 16px;} .modal-inner{padding:24px 16px;} } + .splash-overlay.splash-cathedral { + background: + radial-gradient(circle at 50% 18%, rgba(224,196,127,0.18) 0%, transparent 30%), + linear-gradient(180deg, #17111b 0%, #09080d 100%); + } + .splash-cathedral .splash-rose { position:absolute; top:6vh; left:50%; width:min(36vw,240px); height:min(36vw,240px); transform:translateX(-50%); border-radius:50%; + background: + radial-gradient(circle at center, rgba(255,244,200,0.86) 0 6%, rgba(224,196,127,0.78) 6% 12%, rgba(111,79,174,0.84) 12% 20%, rgba(123,194,255,0.78) 20% 28%, rgba(142,47,73,0.8) 28% 38%, rgba(224,196,127,0.2) 38% 42%, transparent 42%), + conic-gradient(from 0deg, rgba(123,194,255,0.68), rgba(142,47,73,0.84), rgba(224,196,127,0.74), rgba(111,79,174,0.74), rgba(123,194,255,0.68)); + box-shadow:0 0 72px rgba(224,196,127,0.22); opacity:0.78; z-index:1; animation:cathedralRoseSpin 24s linear infinite; } + @keyframes cathedralRoseSpin { to { transform:translateX(-50%) rotate(360deg); } } + .splash-cathedral .splash-pipes { position:absolute; inset:auto 0 0 0; height:42vh; z-index:1; + background: + linear-gradient(90deg, + transparent 0 6%, rgba(12,11,18,0.96) 6% 10%, transparent 10% 14%, rgba(12,11,18,0.96) 14% 18%, transparent 18% 22%, rgba(12,11,18,0.96) 22% 26%, + transparent 26% 74%, rgba(12,11,18,0.96) 74% 78%, transparent 78% 82%, rgba(12,11,18,0.96) 82% 86%, transparent 86% 90%, rgba(12,11,18,0.96) 90% 94%, transparent 94%); + opacity:0.94; } + .splash-cathedral .splash-incense { position:absolute; inset:0; z-index:1; + background: + radial-gradient(circle at 34% 72%, rgba(255,255,255,0.05) 0%, transparent 26%), + radial-gradient(circle at 68% 58%, rgba(255,255,255,0.04) 0%, transparent 26%); + animation:cathedralSmoke 8s ease-in-out infinite alternate; } + @keyframes cathedralSmoke { from { transform:translateY(0) scale(1); } to { transform:translateY(-2%) scale(1.05); } } + .splash-cathedral .splash-title { font-family:'Cinzel',serif; font-size:clamp(1.7rem,5vw,2.5rem); color:#fff4d2; letter-spacing:0.08em; } + .splash-cathedral .splash-tag { color:var(--gold); letter-spacing:0.26em; } + .splash-cathedral .splash-hint { color:rgba(240,232,217,0.82); } + .splash-cathedral .splash-inner { text-shadow:0 2px 28px rgba(0,0,0,0.94); } +{{template "navSharedCSSInner"}} + </style> +</head> +<body> + {{template "splashGate"}} + <div id="splash-overlay" class="splash-overlay splash-cathedral" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1"> + <canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas> + <div class="splash-rose" aria-hidden="true"></div> + <div class="splash-pipes" aria-hidden="true"></div> + <div class="splash-incense" aria-hidden="true"></div> + <div class="splash-inner"> + <div class="splash-title">snonux.foo</div> + <div class="splash-tag">Ritual Engine</div> + <div class="splash-hint">Click or Enter to cross the nave</div> + </div> + </div> + <script> + (function(){ + if(document.documentElement.classList.contains('sno-splash-skip'))return; + var cv=document.getElementById('splash-gl-canvas'); + if(!cv||typeof THREE==='undefined')return; + var raf,ren,sc,ca,clock,embers,rose; + function cleanup(){window.removeEventListener('resize',sz);if(raf)cancelAnimationFrame(raf);raf=null;if(ren)ren.dispose();ren=null;window._snonuxSplashWebGLCleanup=null;} + window._snonuxSplashWebGLCleanup=cleanup; + function sz(){var w=cv.clientWidth||2,h=cv.clientHeight||2;if(ren)ren.setSize(w,h,false);if(ca){ca.aspect=w/h;ca.updateProjectionMatrix();}} + ren=new THREE.WebGLRenderer({canvas:cv,antialias:true,alpha:true});ren.setClearColor(0,0);ren.setPixelRatio(Math.min(window.devicePixelRatio||1,2)); + sc=new THREE.Scene();ca=new THREE.PerspectiveCamera(46,1,0.1,80);ca.position.set(0,6,22);clock=new THREE.Clock(); + var floor=new THREE.Mesh(new THREE.PlaneGeometry(28,42),new THREE.MeshBasicMaterial({color:0x120f16,transparent:true,opacity:0.95})); floor.rotation.x=-Math.PI/2; floor.position.y=-4; sc.add(floor); + for(var i=0;i<8;i++){ var c=new THREE.Mesh(new THREE.CylinderGeometry(0.38,0.52,11,8),new THREE.MeshBasicMaterial({color:0x302737})); + c.position.set(i<4?-6.2:6.2,1,-16+(i%4)*8); sc.add(c);} + rose=new THREE.Mesh(new THREE.CircleGeometry(3.4,34),new THREE.MeshBasicMaterial({color:0xd9bf78,transparent:true,opacity:0.26})); rose.position.set(0,8,-16); sc.add(rose); + var beam=new THREE.Mesh(new THREE.ConeGeometry(3.8,12,20,1,true),new THREE.MeshBasicMaterial({color:0xd9bf78,transparent:true,opacity:0.08,side:THREE.DoubleSide})); beam.position.set(0,4,-6); beam.rotation.x=Math.PI; sc.add(beam); + var ep=new Float32Array(240*3); for(i=0;i<ep.length;i+=3){ ep[i]=(Math.random()-0.5)*14; ep[i+1]=Math.random()*8; ep[i+2]=-14+Math.random()*18; } + var eg=new THREE.BufferGeometry(); eg.setAttribute('position',new THREE.BufferAttribute(ep,3)); + embers=new THREE.Points(eg,new THREE.PointsMaterial({color:0xffb35a,size:0.12,transparent:true,opacity:0.24})); sc.add(embers); + sz();window.addEventListener('resize',sz); + function loop(){ raf=requestAnimationFrame(loop); var t=clock.getElapsedTime(),pos=embers.geometry.attributes.position; + for(var i=0;i<pos.count;i++){ var y=pos.getY(i)+0.05; pos.setY(i,y>10?0:y); } + pos.needsUpdate=true; rose.rotation.z=t*0.08; beam.material.opacity=0.08+Math.sin(t*1.5)*0.02; ren.render(sc,ca); } + raf=requestAnimationFrame(loop); + })(); + </script> + <canvas id="three-canvas"></canvas> + <div class="overlay"> + <header> + <div class="logo"> + <span class="logo-mark">SN</span> + <div class="logo-title"> + <h1>snonux.foo</h1> + <p class="subtitle">microblog — <a href="https://foo.zone">foo.zone</a> is the real blog</p> + <p class="logo-host">Served by NetBSD on a Raspberry Pi 3</p> + </div> + </div> + <div class="nav"> + <a href="atom.xml" class="header-feed-link" rel="alternate" title="Atom feed" type="application/atom+xml">Atom feed</a> + <a href="https://foo.zone/about" class="transmit-btn">Reliquary</a> + </div> + </header> + {{template "navhints" .}} + <div class="content" id="post-content"> + {{range $i, $post := .Posts}} + <div class="post" id="post-{{$post.ID}}" data-index="{{$i}}"> + <div class="post-header"> + <div><strong>@snonux</strong></div> + <div class="post-time">{{$post.FormattedTime}}</div> + </div> + <div class="post-text">{{$post.ContentHTML}}</div> + </div> + {{end}} + </div> + {{if or .PrevPage .NextPage}} + <footer class="page-nav-footer" aria-label="Pagination"> + <div class="page-nav page-nav-dual"> + {{if .PrevPage}}<a href="{{.PrevPage}}">← Newer</a>{{end}} + {{if .NextPage}}<a href="{{.NextPage}}">Older →</a>{{end}} + </div> + </footer> + {{end}} + </div> + {{template "navmodal" .}} + <script> + (function() { + var _wild = false, _snoTOffset = 0, _snoLastT = 0; + var scene, camera, renderer, clock, dust, embers, beams = [], candles = [], rose, halo, chandelier, pipes = []; + + function initThree() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x0f0d14); + scene.fog = new THREE.Fog(0x0f0d14, 16, 140); + camera = new THREE.PerspectiveCamera(56, window.innerWidth/window.innerHeight, 0.1, 260); + camera.position.set(0, 10, 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(); + + scene.add(new THREE.AmbientLight(0x50394b, 0.38)); + var floor = new THREE.Mesh(new THREE.PlaneGeometry(90, 220, 1, 1), new THREE.MeshPhongMaterial({ color:0x19141c, shininess:12 })); + floor.rotation.x = -Math.PI/2; floor.position.y = -3; scene.add(floor); + var aisle = new THREE.Mesh(new THREE.PlaneGeometry(10, 210), new THREE.MeshBasicMaterial({ color:0x3d2830, transparent:true, opacity:0.24 })); + aisle.rotation.x = -Math.PI/2; aisle.position.set(0,-2.98,-38); scene.add(aisle); + rose = new THREE.Mesh(new THREE.CircleGeometry(7.2, 48), new THREE.MeshBasicMaterial({ color:0xd9bf78, transparent:true, opacity:0.18 })); + rose.position.set(0, 16, -102); scene.add(rose); + halo = new THREE.Mesh(new THREE.CircleGeometry(10.8, 48), new THREE.MeshBasicMaterial({ color:0x71233d, transparent:true, opacity:0.08 })); + halo.position.set(0, 16, -103); scene.add(halo); + chandelier = new THREE.Mesh(new THREE.TorusGeometry(4.5, 0.18, 12, 44), new THREE.MeshBasicMaterial({ color:0xe0c47f, transparent:true, opacity:0.5 })); + chandelier.position.set(0, |
