first commit

This commit is contained in:
bs-sensei
2026-03-24 20:30:43 -04:00
commit de259dba5b
27 changed files with 3242 additions and 0 deletions

152
static/js/main.js Normal file
View File

@@ -0,0 +1,152 @@
/* ============================================================
CODEX OBSCURA — main.js
Dust motes drifting in candlelight.
============================================================ */
(function () {
'use strict';
document.documentElement.classList.remove('no-js');
document.documentElement.classList.add('js');
/* ── Uptime Counter ──────────────────────────────────────── */
const uptimeEl = document.getElementById('js-uptime');
if (uptimeEl) {
const startTime = Date.now();
function updateUptime() {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const h = String(Math.floor(elapsed / 3600)).padStart(2, '0');
const m = String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0');
const s = String(elapsed % 60).padStart(2, '0');
uptimeEl.textContent = h + ':' + m + ':' + s;
}
setInterval(updateUptime, 1000);
updateUptime();
}
/* ── Keyboard shortcuts ──────────────────────────────────── */
document.addEventListener('keydown', function (e) {
if (e.key === '/' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
const search = document.getElementById('search-input');
if (search) { e.preventDefault(); search.focus(); }
}
if (e.key === 'Escape' && document.activeElement) {
document.activeElement.blur();
}
});
/* ── Dust Motes ──────────────────────────────────────────── */
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const canvas = document.createElement('canvas');
canvas.style.cssText = [
'position:absolute', 'top:0', 'left:0',
'width:100%', 'height:100%',
'pointer-events:none', 'z-index:2'
].join(';');
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
let W = document.body.scrollWidth;
let H = document.body.scrollHeight;
canvas.width = W;
canvas.height = H;
window.addEventListener('resize', function () {
W = canvas.width = document.body.scrollWidth;
H = canvas.height = document.body.scrollHeight;
});
// Warm amber/parchment dust colours
const COLORS = [
'212, 162, 40', // amber
'201, 176, 122', // parchment warm
'196, 96, 28', // orange
'138, 96, 48', // umber
'232, 213, 163', // parchment light
' 78, 120, 48', // forest
];
const MOTE_COUNT = 500;
function rand(min, max) { return min + Math.random() * (max - min); }
function makeMote(scatterY) {
// Each mote has its own independent fade cycle driven by a sine wave
// so they breathe in and out at different rates rather than all
// appearing and disappearing together.
const peakAlpha = rand(0.06, 0.22);
return {
x: rand(0, W),
y: scatterY !== undefined ? scatterY : rand(0, H),
// very slow drift — tiny random velocity
vx: rand(-0.12, 0.12),
vy: rand(-0.08, 0.06), // slight upward bias like warm air
// size: 12.5px radius, soft and small
r: rand(1.2, 2.4),
color: COLORS[Math.floor(Math.random() * COLORS.length)],
peakAlpha,
// sine-wave phase and speed for opacity breathing
phase: rand(0, Math.PI * 2),
phaseSpeed: rand(0.003, 0.009), // full breath every ~1235 seconds
// gentle Brownian wobble
wobbleX: rand(0, Math.PI * 2),
wobbleY: rand(0, Math.PI * 2),
wobbleSpeedX: rand(0.002, 0.007),
wobbleSpeedY: rand(0.002, 0.006),
wobbleAmp: rand(0.04, 0.18), // pixels of wobble per frame
};
}
// Seed motes scattered across the whole viewport
let motes = Array.from({ length: MOTE_COUNT }, function () { return makeMote(rand(0, H)); });
function animate() {
ctx.clearRect(0, 0, W, H);
const now = performance.now() * 0.001; // seconds, for wobble
for (let i = 0; i < motes.length; i++) {
const m = motes[i];
// Advance fade phase
m.phase += m.phaseSpeed;
// Opacity: sine wave between 0 and peakAlpha, always non-negative
const alpha = m.peakAlpha * (0.5 + 0.5 * Math.sin(m.phase));
// Drift position
m.x += m.vx + Math.sin(m.wobbleX) * m.wobbleAmp;
m.y += m.vy + Math.cos(m.wobbleY) * m.wobbleAmp * 0.6;
// Advance wobble phases independently of main phase
m.wobbleX += m.wobbleSpeedX;
m.wobbleY += m.wobbleSpeedY;
// Wrap around edges with a small buffer so motes never pop in visibly
const buf = 4;
if (m.x < -buf) m.x = W + buf;
if (m.x > W + buf) m.x = -buf;
if (m.y < -buf) m.y = H + buf;
if (m.y > H + buf) m.y = -buf;
// Draw as a soft radial-gradient circle (blurred disc, not a hard pixel)
const grad = ctx.createRadialGradient(m.x, m.y, 0, m.x, m.y, m.r * 2.5);
grad.addColorStop(0, 'rgba(' + m.color + ', ' + alpha + ')');
grad.addColorStop(0.5, 'rgba(' + m.color + ', ' + (alpha * 0.4) + ')');
grad.addColorStop(1, 'rgba(' + m.color + ', 0)');
ctx.beginPath();
ctx.arc(m.x, m.y, m.r * 2.5, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
}
requestAnimationFrame(animate);
}
animate();
})();