212 lines
8.4 KiB
JavaScript
212 lines
8.4 KiB
JavaScript
window.sptb = {
|
|
downloadFile: function(fileName, contentType, base64) {
|
|
var link = document.createElement('a');
|
|
link.download = fileName;
|
|
link.href = 'data:' + contentType + ';base64,' + base64;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
},
|
|
setTheme: function(theme) {
|
|
var t = (theme || 'System').toLowerCase();
|
|
if (t === 'system') {
|
|
t = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
? 'dark' : 'light';
|
|
}
|
|
document.documentElement.setAttribute('data-theme', t);
|
|
},
|
|
scrollToBottom: function(el) {
|
|
if (el) el.scrollTop = el.scrollHeight;
|
|
}
|
|
};
|
|
|
|
// ── Easter eggs: N spaces in a row → slow full-screen fade-in ──
|
|
(function () {
|
|
// threshold (consecutive spaces) → image file in wwwroot
|
|
var EGGS = [
|
|
{ at: 5, src: 'seb-egg.jpg' },
|
|
{ at: 10, src: 'easter-egg.jpg' }
|
|
];
|
|
var count = 0;
|
|
|
|
function typingInField(t) {
|
|
if (!t) return false;
|
|
var tag = t.tagName;
|
|
return tag === 'INPUT' || tag === 'TEXTAREA' || t.isContentEditable;
|
|
}
|
|
|
|
function reveal(src) {
|
|
var id = 'sptb-egg-' + src.replace(/[^a-z0-9]/gi, '-');
|
|
if (document.getElementById(id)) return; // this egg already showing
|
|
var ov = document.createElement('div');
|
|
ov.id = id;
|
|
ov.style.cssText =
|
|
'position:fixed;inset:0;z-index:99999;background-color:#000;' +
|
|
'background-image:url("' + src + '");background-size:100% 100%;' +
|
|
'background-repeat:no-repeat;background-position:center;' +
|
|
'opacity:0;transition:opacity 10s ease;cursor:pointer';
|
|
ov.addEventListener('click', function () { ov.remove(); });
|
|
document.body.appendChild(ov);
|
|
void ov.offsetWidth; // force reflow so transition runs
|
|
ov.style.opacity = '1';
|
|
}
|
|
|
|
window.addEventListener('keydown', function (e) {
|
|
var isSpace = e.code === 'Space' || e.key === ' ' || e.keyCode === 32;
|
|
if (isSpace && !typingInField(e.target)) {
|
|
count++;
|
|
for (var i = 0; i < EGGS.length; i++) {
|
|
if (count === EGGS[i].at) { reveal(EGGS[i].src); break; }
|
|
}
|
|
} else if (!isSpace) {
|
|
count = 0;
|
|
}
|
|
}, true);
|
|
})();
|
|
|
|
// ── Easter egg: type "maze" → fake 3-level maze → screamer at the end ──
|
|
(function () {
|
|
var TRIGGER = 'maze';
|
|
var buf = '';
|
|
|
|
function typingInField(t) {
|
|
if (!t) return false;
|
|
var g = t.tagName;
|
|
return g === 'INPUT' || g === 'TEXTAREA' || t.isContentEditable;
|
|
}
|
|
|
|
// base maze: '#' wall, '.' path, 'P' start, 'G' goal. Solvable snake corridor.
|
|
var BASE = [
|
|
'#########',
|
|
'#P......#',
|
|
'#######.#',
|
|
'#.......#',
|
|
'#.#######',
|
|
'#.......#',
|
|
'#######.#',
|
|
'#......G#',
|
|
'#########'
|
|
];
|
|
function mirrorH(g) { return g.map(function (r) { return r.split('').reverse().join(''); }); }
|
|
function mirrorV(g) { return g.slice().reverse(); }
|
|
var LEVELS = [BASE, mirrorH(BASE), mirrorV(BASE)];
|
|
|
|
var active = false, lvl = 0, grid = null, px = 0, py = 0, rootEl = null, keyHandler = null;
|
|
|
|
function parse(g) {
|
|
for (var y = 0; y < g.length; y++)
|
|
for (var x = 0; x < g[y].length; x++)
|
|
if (g[y][x] === 'P') { px = x; py = y; }
|
|
}
|
|
function cellAt(x, y) { return grid[y] ? grid[y][x] : undefined; }
|
|
|
|
function render() {
|
|
var cols = grid[0].length, html = '';
|
|
for (var y = 0; y < grid.length; y++) {
|
|
for (var x = 0; x < grid[y].length; x++) {
|
|
var c = cellAt(x, y);
|
|
var bg = c === '#' ? '#222' : (c === 'G' ? '#1a7f37' : '#0b0b0b');
|
|
if (x === px && y === py) bg = '#e63946';
|
|
html += '<div style="background:' + bg + '"></div>';
|
|
}
|
|
}
|
|
var board = rootEl.querySelector('.mz-board');
|
|
board.style.gridTemplateColumns = 'repeat(' + cols + ',1fr)';
|
|
board.innerHTML = html;
|
|
rootEl.querySelector('.mz-lvl').textContent = 'Level ' + (lvl + 1) + ' / 3';
|
|
}
|
|
|
|
function move(dx, dy) {
|
|
var nx = px + dx, ny = py + dy, c = cellAt(nx, ny);
|
|
if (c === undefined || c === '#') return;
|
|
px = nx; py = ny;
|
|
if (c === 'G') { nextLevel(); return; }
|
|
render();
|
|
}
|
|
|
|
function nextLevel() {
|
|
lvl++;
|
|
if (lvl >= LEVELS.length) { end(); screamer(); return; }
|
|
grid = LEVELS[lvl].slice(); parse(grid); render();
|
|
}
|
|
|
|
function start() {
|
|
if (active) return;
|
|
active = true; lvl = 0; grid = LEVELS[0].slice(); parse(grid);
|
|
rootEl = document.createElement('div');
|
|
rootEl.id = 'sptb-maze';
|
|
rootEl.style.cssText = 'position:fixed;inset:0;z-index:99998;background:#000;display:flex;' +
|
|
'flex-direction:column;align-items:center;justify-content:center;font-family:monospace;color:#ddd;gap:12px';
|
|
rootEl.innerHTML =
|
|
'<div class="mz-lvl" style="font-size:18px;letter-spacing:1px"></div>' +
|
|
'<div class="mz-board" style="display:grid;width:min(80vmin,560px);aspect-ratio:1;gap:2px"></div>' +
|
|
'<div style="font-size:12px;color:#888">Arrows / WASD to move • Esc to quit</div>';
|
|
document.body.appendChild(rootEl);
|
|
render();
|
|
keyHandler = function (e) {
|
|
var k = e.key;
|
|
if (k === 'Escape') { end(); return; }
|
|
var dx = 0, dy = 0;
|
|
if (k === 'ArrowUp' || k === 'w' || k === 'W') dy = -1;
|
|
else if (k === 'ArrowDown' || k === 's' || k === 'S') dy = 1;
|
|
else if (k === 'ArrowLeft' || k === 'a' || k === 'A') dx = -1;
|
|
else if (k === 'ArrowRight' || k === 'd' || k === 'D') dx = 1;
|
|
else return;
|
|
e.preventDefault(); move(dx, dy);
|
|
};
|
|
window.addEventListener('keydown', keyHandler, true);
|
|
}
|
|
|
|
function end() {
|
|
active = false;
|
|
if (keyHandler) { window.removeEventListener('keydown', keyHandler, true); keyHandler = null; }
|
|
if (rootEl) { rootEl.remove(); rootEl = null; }
|
|
}
|
|
|
|
function screamer() {
|
|
if (!document.getElementById('sptb-shake-css')) {
|
|
var st = document.createElement('style'); st.id = 'sptb-shake-css';
|
|
st.textContent = '@keyframes sptbShake{' +
|
|
'0%{transform:translate(4px,-4px) scale(1.05)}' +
|
|
'25%{transform:translate(-5px,3px) scale(1.08)}' +
|
|
'50%{transform:translate(3px,5px) scale(1.04)}' +
|
|
'75%{transform:translate(-4px,-3px) scale(1.07)}' +
|
|
'100%{transform:translate(4px,4px) scale(1.05)}}';
|
|
document.head.appendChild(st);
|
|
}
|
|
var ov = document.createElement('div');
|
|
ov.style.cssText = 'position:fixed;inset:0;z-index:100000;background:#000 center/cover no-repeat ' +
|
|
'url("screamer.jpg");animation:sptbShake .05s linear infinite;cursor:pointer';
|
|
document.body.appendChild(ov);
|
|
scream();
|
|
ov.addEventListener('click', function () { ov.remove(); });
|
|
setTimeout(function () { if (ov.parentNode) ov.remove(); }, 2500);
|
|
}
|
|
|
|
function scream() {
|
|
try {
|
|
var AC = window.AudioContext || window.webkitAudioContext; if (!AC) return;
|
|
var ac = new AC(), dur = 2.0, n = Math.floor(ac.sampleRate * dur);
|
|
var b = ac.createBuffer(1, n, ac.sampleRate), d = b.getChannelData(0);
|
|
for (var i = 0; i < n; i++) d[i] = Math.random() * 2 - 1; // white noise
|
|
var src = ac.createBufferSource(); src.buffer = b;
|
|
var ng = ac.createGain(); ng.gain.value = 0.9;
|
|
var osc = ac.createOscillator(); osc.type = 'sawtooth'; osc.frequency.value = 180;
|
|
var og = ac.createGain(); og.gain.value = 0.4;
|
|
src.connect(ng).connect(ac.destination);
|
|
osc.connect(og).connect(ac.destination);
|
|
src.start(); osc.start();
|
|
src.stop(ac.currentTime + dur); osc.stop(ac.currentTime + dur);
|
|
} catch (e) { /* audio blocked — image jump still fires */ }
|
|
}
|
|
|
|
window.addEventListener('keydown', function (e) {
|
|
if (active) return;
|
|
if (typingInField(e.target)) { buf = ''; return; }
|
|
var k = e.key;
|
|
if (!k || k.length !== 1) return;
|
|
buf = (buf + k.toLowerCase()).slice(-TRIGGER.length);
|
|
if (buf === TRIGGER) { buf = ''; start(); }
|
|
});
|
|
})();
|