diff options
| author | Paul Buetow <paul@buetow.org> | 2026-08-02 08:59:11 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-08-02 08:59:11 +0300 |
| commit | ccf28b078907a2ee89d84e635f1455914823e756 (patch) | |
| tree | 24d767955a1448cd69d4ba2c2e1f33cdac0a5bef | |
| parent | a1157e8f8b0623afc3bc4f37e7a939ad6879dde5 (diff) | |
Add arcade themes and release v0.19.0
Amp-Thread-ID: https://ampcode.com/threads/T-019fbeb1-4c14-7775-baa1-5cd8579b1dfd
Co-authored-by: Amp <amp@ampcode.com>
25 files changed, 651 insertions, 1 deletions
diff --git a/internal/generator/templates/themes/breakout/MUSIC_LICENSE.txt b/internal/generator/templates/themes/breakout/MUSIC_LICENSE.txt new file mode 100644 index 0000000..7351736 --- /dev/null +++ b/internal/generator/templates/themes/breakout/MUSIC_LICENSE.txt @@ -0,0 +1,7 @@ +Track: Insistent: background loop +Author: yd +License: CC0 1.0 Universal / public domain dedication +License URL: https://creativecommons.org/publicdomain/zero/1.0/ +Source: https://opengameart.org/content/insistent-background-loop + +Bundled as ambient.ogg for the snonux breakout theme. diff --git a/internal/generator/templates/themes/breakout/ambient.ogg b/internal/generator/templates/themes/breakout/ambient.ogg Binary files differnew file mode 100644 index 0000000..4329605 --- /dev/null +++ b/internal/generator/templates/themes/breakout/ambient.ogg diff --git a/internal/generator/templates/themes/breakout/meta.json b/internal/generator/templates/themes/breakout/meta.json new file mode 100644 index 0000000..916835f --- /dev/null +++ b/internal/generator/templates/themes/breakout/meta.json @@ -0,0 +1,7 @@ +{ + "title": "snonux.foo // BREAKOUT", + "header_html": "<div class=\"logo\"><span class=\"ball-mark\" aria-hidden=\"true\"></span><div><h1>snonux.foo</h1><p class=\"subtitle\">NOTES FROM THE MIDNIGHT ARCADE · <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><nav class=\"nav\" aria-label=\"Site\"><a href=\"atom.xml\" class=\"header-feed-link\" rel=\"alternate\" title=\"Atom feed\" type=\"application/atom+xml\">ATOM</a><a href=\"https://foo.zone/about\" class=\"transmit-btn\">SEND A NOTE</a></nav>", + "splash_inner_html": "<canvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"></canvas><div class=\"splash-inner\"><div class=\"splash-bricks\" aria-hidden=\"true\"><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div><div class=\"splash-ball\" aria-hidden=\"true\"></div><div class=\"splash-paddle\" aria-hidden=\"true\"></div><p class=\"splash-kicker\">PLAYER ONE · READY</p><div class=\"splash-title\">BREAKOUT</div><p class=\"splash-tag\">Clear a line. Leave a thought.</p><p class=\"splash-hint\">PRESS ENTER OR CLICK TO SERVE</p></div>", + "prev_page_text": "← NEWER ROUND", + "next_page_text": "OLDER ROUND →" +} diff --git a/internal/generator/templates/themes/breakout/sounds.json b/internal/generator/templates/themes/breakout/sounds.json new file mode 100644 index 0000000..a360f72 --- /dev/null +++ b/internal/generator/templates/themes/breakout/sounds.json @@ -0,0 +1,11 @@ +{ + "splash": {"freqs":[220,330,440,660],"spacing":0.06,"gain":0.07,"wave":"triangle"}, + "nav": {"freq":392,"wave":"sine","dur":0.045,"gain":0.045}, + "open": {"wave":"triangle","start":180,"end":720,"dur":0.1,"gain":0.06}, + "close": {"wave":"triangle","start":520,"end":130,"dur":0.09,"gain":0.05}, + "bounce": {"wave":"square","start":180,"end":110,"dur":0.045,"gain":0.045}, + "ambient": { + "normal": {"bpm":112,"gain":0.022,"wave":"triangle","droneFreqs":[55,110],"pulseFreqs":[220,277.18,329.63],"cutoffMin":700,"cutoffMax":2800,"attack":0.02,"release":0.12,"melody":[{"freq":220,"dur":0.12,"step":0.4},{"freq":277.18,"dur":0.1,"step":0.4},{"freq":329.63,"dur":0.12,"step":0.4}],"drums":["kick","","","snare","kick","","","snare"],"file":"ambient.ogg","volume":0.27}, + "wild": {"bpm":184,"gain":0.05,"wave":"square","droneFreqs":[55,110,220],"pulseFreqs":[440,554.37,659.25],"cutoffMin":1500,"cutoffMax":6500,"attack":0.005,"release":0.05,"melody":[{"freq":440,"dur":0.07,"step":0.13},{"freq":659.25,"dur":0.07,"step":0.13},{"freq":554.37,"dur":0.07,"step":0.13}],"drums":["kick","","snare","kick","kick","","snare","kick"],"file":"ambient.ogg","volume":0.43} + } +} diff --git a/internal/generator/templates/themes/breakout/theme.css b/internal/generator/templates/themes/breakout/theme.css new file mode 100644 index 0000000..0444ee9 --- /dev/null +++ b/internal/generator/templates/themes/breakout/theme.css @@ -0,0 +1,5 @@ +:root{--field:#080b12;--panel:#10141d;--ink:#f2eddf;--muted:#96958f;--line:#303540;--c1:#c75c5c;--c2:#d98255;--c3:#d5aa58;--c4:#77a66e;--c5:#559ca0;--c6:#657eae;--c7:#8c6f9f} +*{box-sizing:border-box;margin:0;padding:0}body{height:100vh;height:100dvh;overflow:hidden;background:var(--field);color:var(--ink);font-family:Inter,"Helvetica Neue",Arial,sans-serif}body::after{content:"";position:fixed;inset:0;z-index:2;pointer-events:none;background:linear-gradient(rgba(255,255,255,.018) 1px,transparent 1px);background-size:100% 34px}#three-canvas{position:fixed;inset:0;width:100%;height:100%;z-index:1}.overlay{position:relative;z-index:10;height:100%;display:flex;flex-direction:column}header{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:18px clamp(18px,4vw,58px);background:rgba(8,11,18,.94);border-bottom:1px solid var(--line)}.logo{display:flex;align-items:center;gap:16px;min-width:0}.ball-mark{width:13px;height:13px;border-radius:50%;background:var(--ink);box-shadow:18px 13px 0 -4px var(--c3)}.logo h1{font:650 clamp(1.25rem,2.2vw,1.8rem)/1 Georgia,serif;letter-spacing:.04em}.subtitle,.logo-host{margin-top:5px;color:var(--muted);font:600 .62rem/1.3 ui-monospace,SFMono-Regular,monospace;letter-spacing:.1em}.subtitle a,.post-text a{color:var(--c5);text-underline-offset:3px}.nav{display:flex;align-items:center;gap:20px;flex:none}.header-feed-link,.transmit-btn,.page-nav a{color:var(--ink);font:650 .68rem ui-monospace,monospace;letter-spacing:.12em;text-decoration:none}.transmit-btn,.page-nav a{padding:9px 14px;border:1px solid #666961;background:rgba(16,20,29,.9)}a:hover{color:var(--c3)}a:focus-visible,button:focus-visible,.post:focus-visible{outline:3px solid var(--c3);outline-offset:4px}.nav-hints{color:var(--muted);background:rgba(12,15,22,.95);border-bottom:1px solid #202530;font:600 .62rem ui-monospace,monospace}.nav-hints-row{min-height:28px}.nav-hints kbd{color:var(--ink);background:transparent;border:1px solid #50545b}.nav-fx-button:hover,.nav-fx-button[aria-pressed=true]{color:var(--c3);background:#1b2029}.content{flex:1;min-height:0;overflow-y:auto;padding:28px clamp(18px,7vw,110px);scrollbar-color:var(--c4) var(--field);scrollbar-width:thin}.post{position:relative;max-width:1040px;margin:0 auto 12px;padding:20px 25px;background:rgba(16,20,29,.94);border:1px solid #292f39;border-left:10px solid var(--c1);cursor:pointer;transition:transform .16s ease,background .16s}.post:nth-child(7n+2){border-left-color:var(--c2)}.post:nth-child(7n+3){border-left-color:var(--c3)}.post:nth-child(7n+4){border-left-color:var(--c4)}.post:nth-child(7n+5){border-left-color:var(--c5)}.post:nth-child(7n+6){border-left-color:var(--c6)}.post:nth-child(7n){border-left-color:var(--c7)}.post:hover{background:#171c26;transform:translateX(4px)}.post-active{background:#1a202b!important;box-shadow:inset 0 0 0 1px var(--ink);transform:translateX(5px)}.post-header{display:grid;grid-template-columns:auto 1fr auto;gap:14px;align-items:center;margin-bottom:10px;color:var(--muted);font:600 .67rem ui-monospace,monospace;letter-spacing:.06em;text-transform:uppercase}.post-header strong{color:var(--ink)}.post-time{justify-self:end}.post-text{font:400 1.02rem/1.68 Georgia,"Times New Roman",serif;max-width:78ch}.post-image{margin-top:15px;max-width:100%;border:1px solid #424650}.post-audio{width:100%;margin-top:12px}.page-nav-footer{flex:none;padding:10px;background:rgba(8,11,18,.96);border-top:1px solid var(--line)}.page-nav{display:flex;justify-content:center}.post-modal{display:none;position:fixed;inset:0;z-index:100;padding:35px 18px;overflow:auto;background:rgba(4,6,10,.78)}.post-modal.active{display:block}.modal-inner{max-width:820px;margin:auto;padding:35px;background:var(--panel);border:1px solid #656861;box-shadow:0 24px 80px #000}.modal-close{float:right;padding:9px;color:var(--ink);background:transparent;border:1px solid #656861;cursor:pointer} +[data-sno-theme=breakout] .splash-overlay{background:rgba(8,11,18,.97);font-family:ui-monospace,monospace}[data-sno-theme=breakout] .splash-inner{position:relative;z-index:2;width:min(82vw,620px);text-align:center}.splash-bricks{display:grid;grid-template-columns:repeat(7,1fr);gap:5px;margin-bottom:52px}.splash-bricks i{height:12px;background:var(--c1)}.splash-bricks i:nth-child(2){background:var(--c2)}.splash-bricks i:nth-child(3){background:var(--c3)}.splash-bricks i:nth-child(4){background:var(--c4)}.splash-bricks i:nth-child(5){background:var(--c5)}.splash-bricks i:nth-child(6){background:var(--c6)}.splash-bricks i:nth-child(7){background:var(--c7)}.splash-ball{position:absolute;top:38px;left:58%;width:13px;height:13px;border-radius:50%;background:var(--ink);animation:serve 1.8s ease-in-out infinite}.splash-paddle{width:100px;height:8px;margin:0 auto 31px;background:var(--ink)}@keyframes serve{50%{transform:translate(-90px,62px)}}.splash-kicker{color:var(--c3);font-size:.66rem;letter-spacing:.22em}.splash-title{margin:.18em 0;font:500 clamp(2.5rem,10vw,6rem)/1 Georgia,serif;letter-spacing:.08em}.splash-tag{color:var(--muted);font:400 clamp(.85rem,2vw,1rem) Georgia,serif}.splash-hint{margin-top:1.7rem;color:#777a78;font-size:.62rem;letter-spacing:.16em} +@media(max-width:640px){header{padding:12px 14px}.logo-host,.subtitle,.header-feed-link{display:none}.nav{gap:7px}.transmit-btn{padding:9px 8px}.content{padding:14px 12px}.post{padding:16px 14px;border-left-width:7px}.post-text{font-size:.96rem}.post-header{gap:7px;font-size:.59rem}.nav-hints-label,.nav-hints-spacer,.nav-hints kbd{display:none}.nav-fx-button{min-height:44px;flex:1;justify-content:center}.modal-inner{padding:22px 15px}.modal-close{min-width:44px;min-height:44px}.splash-bricks{gap:3px}} +@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important}.post{transition:none}} diff --git a/internal/generator/templates/themes/breakout/theme.js b/internal/generator/templates/themes/breakout/theme.js new file mode 100644 index 0000000..fa4947c --- /dev/null +++ b/internal/generator/templates/themes/breakout/theme.js @@ -0,0 +1,124 @@ +(function () { + 'use strict'; + var reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + var palette = [0xc75c5c, 0xd98255, 0xd5aa58, 0x77a66e, 0x559ca0, 0x657eae, 0x8c6f9f]; + + function buildScene(canvas, splash) { + if (!canvas || typeof THREE === 'undefined') return { wild: function () {}, hit: function () {}, destroy: function () {} }; + var renderer = new THREE.WebGLRenderer({canvas: canvas, alpha: true, antialias: true}); + renderer.setClearColor(0, 0); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); + var scene = new THREE.Scene(); + var camera = new THREE.OrthographicCamera(-10, 10, 7, -7, .1, 50); camera.position.z = 12; + scene.add(new THREE.AmbientLight(0xffffff, 1.4)); + var bricks = [], balls = [], rows = splash ? 3 : 6, cols = splash ? 7 : 11; + var brickGeo = new THREE.BoxGeometry(1.45, .42, .3); + for (var r = 0; r < rows; r++) for (var c = 0; c < cols; c++) { + var brick = new THREE.Mesh(brickGeo, new THREE.MeshLambertMaterial({color: palette[r % 7], transparent: true})); + brick.position.set((c - (cols - 1) / 2) * 1.65, 5.3 - r * .62, 0); + brick.userData.row = r; brick.userData.col = c; + brick.userData.homeX = brick.position.x; brick.userData.homeY = brick.position.y; + bricks.push(brick); scene.add(brick); + } + var paddle = new THREE.Mesh(new THREE.BoxGeometry(3.2, .28, .36), new THREE.MeshLambertMaterial({color: 0xf2eddf})); + paddle.position.y = -5.5; scene.add(paddle); + var ballGeo = new THREE.SphereGeometry(.22, 16, 12); + var ballMaterial = new THREE.MeshLambertMaterial({color: 0xf2eddf}); + function addBall(seed) { + var ball = new THREE.Mesh(ballGeo, ballMaterial); + ball.position.set((seed - 1) * .6, -3 + seed * .25, .35); + ball.userData.vx = (seed % 2 ? 1 : -1) * (2.8 + (seed % 6) * .35); + ball.userData.vy = 3.4 + (seed % 7) * .25; + balls.push(ball); scene.add(ball); + } + addBall(0); + var maxBalls = splash ? 10 : 18, activeBalls = 1; + var isWild = false, raf = 0, last = performance.now(), nextRow = 0, dead = false; + function resize() { + var w = canvas.clientWidth || 2, h = canvas.clientHeight || 2, aspect = w / h; + camera.left = -7 * aspect; camera.right = 7 * aspect; camera.top = 7; camera.bottom = -7; camera.updateProjectionMatrix(); renderer.setSize(w, h, false); + } + function frame(now) { + if (dead) return; + var dt = Math.min((now - last) / 1000, .04) * (isWild ? 2.8 : 1); last = now; + balls.slice(0, activeBalls).forEach(function (b) { + b.position.x += b.userData.vx * dt; b.position.y += b.userData.vy * dt; + var edge = 6.8 * camera.right / 10; + if (b.position.x > edge || b.position.x < -edge) { b.position.x = Math.max(-edge, Math.min(edge, b.position.x)); b.userData.vx *= -1; } + if (b.position.y > 6.4) b.userData.vy = -Math.abs(b.userData.vy); + if (b.position.y < -5.15 && b.position.y > -5.8 && Math.abs(b.position.x - paddle.position.x) < 1.9) b.userData.vy = Math.abs(b.userData.vy); + if (b.position.y < -6.7) { b.position.set(0, -3, .35); b.userData.vy = Math.abs(b.userData.vy); } + }); + paddle.position.x = isWild + ? (Math.sin(now * .0063) * 3.4 + Math.sin(now * .014) * 1.25) + : Math.sin(now * .00055) * 3.6; + if (isWild) { + bricks.forEach(function (b) { + var wave = now * .0048 + b.userData.col * .52 + b.userData.row * .76; + b.position.x = b.userData.homeX + Math.sin(now * .0017 + b.userData.row) * .55; + b.position.y = b.userData.homeY + Math.sin(wave) * .25; + b.scale.y = .65 + .55 * (1 + Math.sin(wave * 1.35)) / 2; + b.material.opacity = .22 + .78 * (1 + Math.sin(wave - b.userData.row)) / 2; + }); + camera.position.x = Math.sin(now * .0021) * .7; + camera.position.y = Math.cos(now * .0016) * .38; + camera.rotation.z = Math.sin(now * .00135) * .022; + } else { + bricks.forEach(function (b) { + b.position.x = b.userData.homeX; b.position.y = b.userData.homeY; b.scale.y = 1; + }); + camera.position.x = 0; camera.position.y = 0; camera.rotation.z = 0; + } + renderer.render(scene, camera); if (!reduced) raf = requestAnimationFrame(frame); + } + function hit() { + var row = nextRow++ % rows; + bricks.forEach(function (b) { if (b.userData.row === row) b.material.opacity = .08; }); + window.setTimeout(function () { if (!dead) bricks.forEach(function (b) { if (b.userData.row === row) b.material.opacity = 1; }); }, 480); + } + function wild(on) { + isWild = on && !reduced; + if (isWild) { + while (balls.length < maxBalls) addBall(balls.length); + balls.forEach(function (ball) { if (ball.parent !== scene) scene.add(ball); }); + activeBalls = maxBalls; + } else { + activeBalls = 1; + balls.forEach(function (ball, index) { if (index && ball.parent === scene) scene.remove(ball); }); + bricks.forEach(function (b) { b.material.opacity = 1; }); + } + } + function destroy() { + dead = true; window.removeEventListener('resize', resize); if (raf) cancelAnimationFrame(raf); + bricks.forEach(function (b) { b.material.dispose(); }); + brickGeo.dispose(); paddle.geometry.dispose(); paddle.material.dispose(); ballGeo.dispose(); ballMaterial.dispose(); + renderer.dispose(); if (window._snonuxSplashWebGLCleanup === destroy) window._snonuxSplashWebGLCleanup = null; + } + resize(); window.addEventListener('resize', resize); frame(last); + return {wild: wild, hit: hit, destroy: destroy}; + } + + if (!document.documentElement.classList.contains('sno-splash-skip')) { + var splashScene = buildScene(document.getElementById('splash-gl-canvas'), true); + window._snonuxSplashWebGLCleanup = splashScene.destroy; + } + var game = buildScene(document.getElementById('three-canvas'), false), wild = false; + function flash(color, vertical) { + var el = document.createElement('div'); + el.style.cssText = 'position:fixed;z-index:9000;pointer-events:none;background:' + color + ';opacity:.72;transition:transform .2s ease,opacity .28s ease;' + (vertical ? 'top:0;bottom:0;width:5px;left:50%;transform:scaleY(0)' : 'left:0;right:0;height:7px;top:50%;transform:scaleX(0)'); + document.body.appendChild(el); requestAnimationFrame(function () { el.style.transform = vertical ? 'scaleY(1)' : 'scaleX(1)'; }); + setTimeout(function () { el.style.opacity = '0'; }, 180); setTimeout(function () { el.remove(); }, 500); + } + window.snonuxOpenEffect = function () { game.hit(); flash('#d5aa58', false); }; + window.snonuxCloseEffect = function () { flash('#657eae', true); }; + window.snonuxNavEffect = function () { game.hit(); flash('#559ca0', false); }; + window.snonuxPageEffect = function () { game.hit(); flash('#8c6f9f', false); }; + window.snonuxScrollEffect = function (direction) { + var post = document.querySelector('.post-active'); if (!post) return; + post.style.transform = 'translateX(' + (direction === 'down' ? '9px' : '-3px') + ')'; setTimeout(function () { post.style.transform = ''; }, 150); + }; + window.snonuxWildToggle = function () { + wild = !wild; game.wild(wild); + var badge = document.getElementById('sno-wild-badge'); if (badge) badge.classList.toggle('sno-wild-on', wild); + document.documentElement.style.setProperty('--sno-wild-accent', wild ? '#c75c5c' : '#d5aa58'); + }; +}()); diff --git a/internal/generator/templates/themes/invaders/MUSIC_LICENSE.txt b/internal/generator/templates/themes/invaders/MUSIC_LICENSE.txt new file mode 100644 index 0000000..0f17db0 --- /dev/null +++ b/internal/generator/templates/themes/invaders/MUSIC_LICENSE.txt @@ -0,0 +1,7 @@ +Track: Scifi City - Ambient Loop +Author: TinyWorlds +License: CC0 1.0 Universal / public domain dedication +License URL: https://creativecommons.org/publicdomain/zero/1.0/ +Source: https://opengameart.org/content/scifi-city-ambient-loop + +Bundled as ambient.ogg for the snonux invaders theme. diff --git a/internal/generator/templates/themes/invaders/ambient.ogg b/internal/generator/templates/themes/invaders/ambient.ogg Binary files differnew file mode 100644 index 0000000..5813de3 --- /dev/null +++ b/internal/generator/templates/themes/invaders/ambient.ogg diff --git a/internal/generator/templates/themes/invaders/meta.json b/internal/generator/templates/themes/invaders/meta.json new file mode 100644 index 0000000..64594f1 --- /dev/null +++ b/internal/generator/templates/themes/invaders/meta.json @@ -0,0 +1,7 @@ +{ + "title": "snonux.foo // FORMATION", + "header_html": "<div class=\"logo\"><span class=\"formation-mark\" aria-hidden=\"true\"><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span><div class=\"logo-title\"><h1>snonux.foo</h1><p class=\"subtitle\">COMMAND TRANSMISSIONS · <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\">SIGNAL FEED</a><a href=\"https://foo.zone/about\" class=\"transmit-btn\">OPEN CHANNEL</a></div>", + "splash_inner_html": "<canvas class=\"splash-gl-canvas\" id=\"splash-gl-canvas\" aria-hidden=\"true\"></canvas><div class=\"splash-inner\"><div class=\"descent\" aria-hidden=\"true\"><b></b><b></b><b></b><b></b><b></b><b></b><b></b><b></b><b></b></div><div class=\"splash-kicker\">FORMATION DETECTED</div><div class=\"splash-title\">SNONUX</div><div class=\"splash-tag\">Command channel ready</div><div class=\"splash-hint\">PRESS ENTER OR CLICK TO RESPOND</div></div>", + "prev_page_text": "◀ NEWER SIGNALS", + "next_page_text": "OLDER SIGNALS ▶" +} diff --git a/internal/generator/templates/themes/invaders/sounds.json b/internal/generator/templates/themes/invaders/sounds.json new file mode 100644 index 0000000..3e608e5 --- /dev/null +++ b/internal/generator/templates/themes/invaders/sounds.json @@ -0,0 +1,11 @@ +{ + "splash": {"freqs":[164.81,196,246.94,329.63],"spacing":0.09,"gain":0.075,"wave":"square"}, + "nav": {"freq":659.25,"wave":"square","dur":0.045,"gain":0.05}, + "open": {"wave":"square","start":180,"end":720,"dur":0.1,"gain":0.065}, + "close": {"wave":"triangle","start":520,"end":130,"dur":0.11,"gain":0.06}, + "bounce": {"wave":"square","start":110,"end":70,"dur":0.07,"gain":0.06}, + "ambient": { + "normal": {"bpm":112,"gain":0.025,"wave":"square","droneFreqs":[55,110],"pulseFreqs":[220,277.18,329.63],"cutoffMin":700,"cutoffMax":3200,"attack":0.01,"release":0.09,"melody":[{"freq":220,"dur":0.1,"step":0.27},{"freq":277.18,"dur":0.1,"step":0.27},{"freq":246.94,"dur":0.1,"step":0.27},{"freq":329.63,"dur":0.1,"step":0.27}],"drums":["kick","","","snare","kick","","","snare"],"file":"ambient.ogg","volume":0.3}, + "wild": {"bpm":184,"gain":0.05,"wave":"square","droneFreqs":[55,110,220],"pulseFreqs":[440,554.37,659.25],"cutoffMin":1300,"cutoffMax":6500,"attack":0.005,"release":0.04,"melody":[{"freq":440,"dur":0.06,"step":0.13},{"freq":554.37,"dur":0.06,"step":0.13},{"freq":659.25,"dur":0.06,"step":0.13},{"freq":880,"dur":0.06,"step":0.13}],"drums":["kick","","snare","kick","kick","","snare","kick"],"file":"ambient.ogg","volume":0.44} + } +} diff --git a/internal/generator/templates/themes/invaders/theme.css b/internal/generator/templates/themes/invaders/theme.css new file mode 100644 index 0000000..814d838 --- /dev/null +++ b/internal/generator/templates/themes/invaders/theme.css @@ -0,0 +1 @@ +:root{--void:#030706;--panel:#09110f;--mint:#9fffd8;--white:#edfff8;--dim:#78988b;--line:#24473b;--alert:#ffcc70}*{box-sizing:border-box;margin:0;padding:0}body{height:100vh;height:100dvh;overflow:hidden;background:var(--void);color:var(--white);font-family:ui-monospace,"Cascadia Mono","Courier New",monospace}#three-canvas{position:fixed;inset:0;width:100%;height:100%;z-index:1}.overlay{position:relative;z-index:10;height:100vh;height:100dvh;display:flex;flex-direction:column}header{display:flex;align-items:center;justify-content:space-between;gap:22px;padding:15px 25px;background:rgba(3,7,6,.93);border-bottom:1px solid var(--mint)}.logo{display:flex;align-items:center;gap:16px;min-width:0}.formation-mark{width:39px;height:29px;display:grid;grid-template-columns:repeat(5,7px);grid-template-rows:repeat(4,7px);gap:1px}.formation-mark i{background:var(--mint)}.formation-mark i:nth-child(1){grid-column:2}.formation-mark i:nth-child(2){grid-column:4}.formation-mark i:nth-child(3){grid-column:1/6}.formation-mark i:nth-child(4){grid-column:2/5}.formation-mark i:nth-child(5){grid-column:1}.formation-mark i:nth-child(6){grid-column:3}.formation-mark i:nth-child(7){grid-column:5}.logo-title h1{font-size:clamp(1.2rem,2.5vw,1.8rem);letter-spacing:.13em}.subtitle{margin-top:5px;color:var(--dim);font-size:.65rem;letter-spacing:.08em}.subtitle a,.post-text a{color:var(--mint)}.nav{display:flex;align-items:center;gap:18px}.header-feed-link{color:var(--dim);font-size:.7rem}.transmit-btn,.page-nav a{padding:9px 15px;border:1px solid var(--mint);color:var(--mint);background:#07100d;text-decoration:none;font-size:.72rem;letter-spacing:.08em}.transmit-btn:hover,.page-nav a:hover{color:var(--void);background:var(--mint)}.nav-hints{color:var(--dim);background:rgba(3,7,6,.92);border-bottom:1px solid var(--line);font-size:.65rem}.nav-hints kbd{color:var(--mint);background:var(--panel);border:1px solid var(--line);border-radius:0}.nav-fx-button:hover,.nav-fx-button[aria-pressed=true]{color:var(--white);background:#10221c}.content{flex:1;min-height:0;overflow-y:auto;padding:22px 25px;scrollbar-color:var(--mint) var(--void);scrollbar-width:thin}.post{position:relative;max-width:960px;margin:0 auto 14px;padding:19px 21px;background:rgba(7,16,13,.92);border:1px solid var(--line);border-top-color:#3b6e5c;cursor:pointer;transition:transform .16s,border-color .16s,background .16s}.post::before{content:"TRANSMISSION";position:absolute;right:15px;top:-6px;padding:0 6px;background:var(--panel);color:var(--dim);font-size:.55rem;letter-spacing:.15em}.post:hover,.post-active{transform:translateX(4px);border-color:var(--mint);background:rgba(11,27,22,.96)}.post-active{box-shadow:inset 4px 0 var(--mint)}.post-header{display:flex;justify-content:space-between;gap:12px;margin-bottom:11px;color:var(--mint);font-size:.72rem;letter-spacing:.06em}.post-header strong::before{content:"◆ ";}.post-time{color:var(--dim)}.post-text{font-family:system-ui,-apple-system,sans-serif;font-size:1rem;line-height:1.65;color:#e5f4ee}.post-text a{text-underline-offset:3px}.post-image{margin-top:14px;border:1px solid var(--mint);border-radius:0}.post-audio{width:100%;margin-top:12px}.page-nav-footer{flex-shrink:0;padding:9px;background:rgba(3,7,6,.95);border-top:1px solid var(--line)}.page-nav{display:flex;justify-content:center}.post-modal{display:none;position:fixed;inset:0;z-index:100;padding:35px 18px;overflow:auto}.post-modal.active{display:block}.modal-inner{max-width:800px;margin:auto;padding:32px;background:#07100d;border:1px solid var(--mint);box-shadow:8px 8px 0 #18382d}.modal-close{float:right;padding:8px;color:var(--mint);background:transparent;border:1px solid var(--mint);cursor:pointer}a:focus-visible,button:focus-visible,.post:focus-visible{outline:3px solid var(--alert);outline-offset:4px}[data-sno-theme=invaders] .splash-overlay{background:#030706}[data-sno-theme=invaders] .splash-inner{position:relative;z-index:2;text-align:center}.descent{display:grid;grid-template-columns:repeat(3,25px);gap:10px 18px;width:max-content;margin:0 auto 25px}.descent b{width:25px;height:18px;background:var(--mint);clip-path:polygon(20% 0,40% 0,40% 20%,60% 20%,60% 0,80% 0,80% 20%,100% 20%,100% 80%,80% 80%,80% 100%,60% 100%,60% 80%,40% 80%,40% 100%,20% 100%,20% 80%,0 80%,0 20%,20% 20%)}.descent b:nth-child(n+4){transform:translateY(6px)}.descent b:nth-child(n+7){transform:translateY(12px)}.splash-kicker{color:var(--mint);font-size:.68rem;letter-spacing:.24em}.splash-title{margin:.18em 0;color:var(--white);font-size:clamp(2.7rem,10vw,6rem);font-weight:800;letter-spacing:.14em}.splash-tag{color:var(--dim);letter-spacing:.12em}.splash-hint{margin-top:1.5rem;color:var(--alert);font-size:.65rem;letter-spacing:.13em;animation:invBlink 1.2s steps(2) infinite}@keyframes invBlink{50%{opacity:.25}}@media(max-width:640px){header{padding:11px 13px}.logo-host,.subtitle,.header-feed-link{display:none}.formation-mark{transform:scale(.85)}.nav{gap:6px}.transmit-btn{padding:10px 8px;font-size:.62rem}.content{padding:13px}.post{padding:16px 14px}.post-text{font-size:.95rem}.nav-hints kbd,.nav-hints-label,.nav-hints-spacer{display:none}.nav-fx-button{min-height:44px;flex:1;justify-content:center}.modal-inner{padding:20px 14px}.modal-close{min-width:44px;min-height:44px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important}.post{transition:none}} diff --git a/internal/generator/templates/themes/invaders/theme.js b/internal/generator/templates/themes/invaders/theme.js new file mode 100644 index 0000000..c79975f --- /dev/null +++ b/internal/generator/templates/themes/invaders/theme.js @@ -0,0 +1,230 @@ +(function () { + var reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + var wild = false; + var patterns = [ + [[1, 0], [3, 0], [0, 1], [1, 1], [2, 1], [3, 1], [4, 1], [0, 2], [2, 2], [4, 2], [1, 3], [3, 3]], + [[0, 0], [4, 0], [1, 1], [2, 1], [3, 1], [0, 2], [1, 2], [2, 2], [3, 2], [4, 2], [1, 3], [3, 3]] + ]; + + function unit(pattern, size, material) { + var group = new THREE.Group(); + var geometry = new THREE.BoxGeometry(size, size, size * .35); + pattern.forEach(function (point) { + var mesh = new THREE.Mesh(geometry, material); + mesh.position.set((point[0] - 2) * size, (1.5 - point[1]) * size, 0); + group.add(mesh); + }); + return group; + } + + function scene(canvas, splash) { + if (!canvas || typeof THREE === 'undefined') return function () {}; + var renderer = new THREE.WebGLRenderer({canvas: canvas, antialias: false, alpha: true}); + var world = new THREE.Scene(); + var camera = new THREE.PerspectiveCamera(48, 1, .1, 100); + var formation = new THREE.Group(); + var forms = []; + var projectiles = []; + var materials = []; + var raf = 0; + var dead = false; + var last = performance.now(); + var elapsed = 0; + var marchClock = 0; + var marchX = 0; + var marchY = 0; + var direction = 1; + var localWild = false; + var rows = splash ? 3 : 5; + var cols = splash ? 5 : 8; + var spacingX = splash ? 1.45 : 2.4; + var spacingY = splash ? 1.15 : 1.8; + + camera.position.z = splash ? 13 : 26; + world.add(formation); + world.add(new THREE.AmbientLight(0x79d9bd, .48)); + var pressureLight = new THREE.PointLight(0xff486d, 0, 45); + pressureLight.position.set(0, 0, 8); + world.add(pressureLight); + + for (var y = 0; y < rows; y++) { + var material = new THREE.MeshStandardMaterial({ + color: 0x9fffd8, + emissive: 0x123c31, + emissiveIntensity: .18, + roughness: .7 + }); + materials.push(material); + for (var x = 0; x < cols; x++) { + var form = unit(patterns[y % 2], splash ? .11 : .18, material); + form.userData.homeX = (x - (cols - 1) / 2) * spacingX; + form.userData.homeY = (rows / 2 - y) * spacingY; + form.userData.row = y; + form.userData.col = x; + form.position.set(form.userData.homeX, form.userData.homeY, 0); + forms.push(form); + formation.add(form); + } + } + + /* A fixed pool makes repeated wild-mode toggles allocation-free. */ + if (!splash) { + var shotGeometry = new THREE.BoxGeometry(.075, .7, .075); + var shotMaterial = new THREE.MeshBasicMaterial({color: 0xffd477}); + materials.push(shotMaterial); + for (var p = 0; p < 18; p++) { + var shot = new THREE.Mesh(shotGeometry, shotMaterial); + shot.userData.phase = p / 18; + shot.visible = false; + projectiles.push(shot); + world.add(shot); + } + } + + function resize() { + var width = canvas.clientWidth || 2; + var height = canvas.clientHeight || 2; + renderer.setSize(width, height, false); + camera.aspect = width / height; + camera.updateProjectionMatrix(); + } + + function draw(now) { + if (dead) return; + var dt = Math.min((now - last) / 1000, .05); + last = now; + elapsed += dt; + + if (localWild && !reduced) { + marchClock += dt; + if (marchClock >= .11) { + marchClock %= .11; + marchX += direction * .38; + if (Math.abs(marchX) > 2.2) { + direction *= -1; + marchX += direction * .38; + marchY -= .32; + if (marchY < -2.2) marchY = 1.25; + } + } + formation.position.x = marchX; + formation.position.y = marchY + Math.sin(elapsed * 3.8) * .16; + var scale = 1 + Math.sin(elapsed * 2.7) * .13; + formation.scale.set(scale, scale, scale); + formation.rotation.z = Math.sin(elapsed * 1.9) * .035; + camera.position.x = Math.sin(elapsed * 1.35) * 1.5; + camera.position.y = Math.cos(elapsed * 1.7) * .7; + camera.position.z = 24 + Math.sin(elapsed * 1.1) * 2.2; + camera.lookAt(0, 0, 0); + pressureLight.intensity = 1.2 + Math.sin(elapsed * 8) * .65; + + forms.forEach(function (form) { + var row = form.userData.row; + var col = form.userData.col; + form.position.x = form.userData.homeX + Math.sin(elapsed * 5 + row * .8) * .28; + form.position.y = form.userData.homeY + Math.sin(elapsed * 6 + col * .58) * .23; + form.position.z = Math.sin(elapsed * 4.2 + col * .72 - row) * 2.25; + form.rotation.y = Math.sin(elapsed * 3.4 + col * .5) * .42; + }); + materials.slice(0, rows).forEach(function (material, index) { + var pressure = (Math.sin(elapsed * 7 + index) + 1) / 2; + material.color.setHSL(.43 - pressure * .42, .9, .63); + material.emissive.setHSL(.43 - pressure * .4, 1, .18); + material.emissiveIntensity = .5 + pressure * 1.4; + }); + projectiles.forEach(function (shot) { + var travel = (elapsed * 1.75 + shot.userData.phase) % 1; + var source = forms[Math.floor(shot.userData.phase * forms.length)]; + shot.visible = true; + shot.position.x = source.position.x + formation.position.x; + shot.position.y = 8 - travel * 18; + shot.position.z = source.position.z + Math.sin(elapsed * 9 + shot.userData.phase * 20) * .7; + shot.scale.y = 1 + travel * 2.2; + }); + } else { + formation.position.x += (0 - formation.position.x) * Math.min(1, dt * 3); + formation.position.y += (0 - formation.position.y) * Math.min(1, dt * 3); + formation.scale.set(1, 1, 1); + formation.rotation.z = 0; + camera.position.x = 0; + camera.position.y = 0; + camera.position.z = splash ? 13 : 26; + camera.lookAt(0, 0, 0); + pressureLight.intensity = 0; + forms.forEach(function (form) { + form.position.x = form.userData.homeX; + form.position.y = form.userData.homeY + (reduced ? 0 : Math.sin(elapsed * .7 + form.userData.col) * .035); + form.position.z = 0; + form.rotation.y = 0; + }); + materials.slice(0, rows).forEach(function (material) { + material.color.setHex(0x9fffd8); + material.emissive.setHex(0x123c31); + material.emissiveIntensity = .18; + }); + projectiles.forEach(function (shot) { shot.visible = false; }); + } + renderer.render(world, camera); + if (!reduced) raf = requestAnimationFrame(draw); + } + + function cleanup() { + dead = true; + window.removeEventListener('resize', resize); + if (raf) cancelAnimationFrame(raf); + world.traverse(function (object) { + if (object.geometry) object.geometry.dispose(); + }); + materials.forEach(function (material) { material.dispose(); }); + renderer.dispose(); + if (window._snonuxSplashWebGLCleanup === cleanup) window._snonuxSplashWebGLCleanup = null; + } + + resize(); + window.addEventListener('resize', resize); + draw(last); + return function (state) { + if (typeof state === 'boolean') { + localWild = state && !splash; + if (reduced) draw(performance.now()); + } else { + cleanup(); + } + }; + } + + if (!document.documentElement.classList.contains('sno-splash-skip')) { + window._snonuxSplashWebGLCleanup = scene(document.getElementById('splash-gl-canvas'), true); + } + var control = scene(document.getElementById('three-canvas'), false); + + function pulse() { + var pulseElement = document.createElement('div'); + pulseEl |
