1 Commits

Author SHA1 Message Date
Alexander Whitestone
d759416fc6 feat: ASCII art Timmy logo with progressive loading reveal
Replace plain "INITIALIZING..." text with a block-character ASCII art
TIMMY logo that fills in character-by-character as each init step
completes. Shows a progress bar and percentage counter.

- js/loading.js: new module — ASCII art, initLoadingArt(), setLoadingProgress()
- js/main.js: import loading module; call setLoadingProgress() at each
  init stage (world 10%, effects 30%, agents 50%, controls 70%,
  UI 80%, WebSocket 90%, visitor 95%, done 100%)
- index.html: loading screen now uses <pre id="ascii-logo">, progress
  bar track/fill, percent label, and status message; CSS styles added
  for lit/unlit characters with green glow

Fixes #7

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:06:41 -04:00
3 changed files with 161 additions and 7 deletions

View File

@@ -22,13 +22,56 @@
position: fixed; inset: 0; z-index: 100;
display: flex; align-items: center; justify-content: center;
background: #000;
color: #00ff41; font-size: 14px; letter-spacing: 4px;
text-shadow: 0 0 12px #00ff41;
font-family: 'Courier New', monospace;
}
#loading-screen.hidden { display: none; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
#loading-screen span { animation: blink 1.2s ease-in-out infinite; }
.loading-content {
display: flex; flex-direction: column; align-items: center; gap: 18px;
padding: 24px;
}
/* ASCII logo */
#ascii-logo {
white-space: pre;
font-family: 'Courier New', monospace;
font-size: clamp(6px, 1.1vw, 13px);
line-height: 1.25;
color: #002200;
text-shadow: none;
user-select: none;
}
/* Lit characters glow green */
#ascii-logo .ac { color: #002200; transition: color 0.15s, text-shadow 0.15s; }
#ascii-logo .ac.lit {
color: #00ff41;
text-shadow: 0 0 8px #00ff41, 0 0 20px #00cc33;
}
/* Progress bar */
#loading-bar-track {
width: clamp(220px, 50vw, 500px);
height: 3px;
background: #001800;
border: 1px solid #003300;
border-radius: 2px;
overflow: hidden;
}
#loading-bar-fill {
height: 100%; width: 0%;
background: linear-gradient(90deg, #00aa22, #00ff41);
box-shadow: 0 0 10px #00ff41;
transition: width 0.3s ease-out;
}
/* Percent + status */
#loading-percent {
color: #00ff41; font-size: 11px; letter-spacing: 3px;
text-shadow: 0 0 8px #00ff41;
}
#loading-msg {
color: #007722; font-size: 10px; letter-spacing: 4px;
}
#ui-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
@@ -162,7 +205,14 @@
</style>
</head>
<body>
<div id="loading-screen"><span>INITIALIZING...</span></div>
<div id="loading-screen">
<div class="loading-content">
<pre id="ascii-logo"></pre>
<div id="loading-bar-track"><div id="loading-bar-fill"></div></div>
<div id="loading-percent">0%</div>
<div id="loading-msg">INITIALIZING...</div>
</div>
</div>
<!-- WebGL context loss overlay (iPad PWA, GPU resets) -->
<div id="webgl-recovery-overlay" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.9);color:#00ff41;font-family:monospace;align-items:center;justify-content:center;flex-direction:column">
<p style="font-size:1.4rem">RECOVERING WebGL CONTEXT…</p>

86
js/loading.js Normal file
View File

@@ -0,0 +1,86 @@
/**
* Loading screen — ASCII art Timmy logo with progressive character reveal.
*
* Usage:
* import { initLoadingArt, setLoadingProgress } from './loading.js';
* initLoadingArt(); // call once on page load
* setLoadingProgress(50, 'MSG'); // 0100, optional status message
*/
const TIMMY_ASCII = `
████████╗██╗███╗ ███╗███╗ ███╗██╗ ██╗
╚══██╔══╝██║████╗ ████║████╗ ████║╚██╗ ██╔╝
██║ ██║██╔████╔██║██╔████╔██║ ╚████╔╝
██║ ██║██║╚██╔╝██║██║╚██╔╝██║ ╚██╔╝
██║ ██║██║ ╚═╝ ██║██║ ╚═╝ ██║ ██║
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
┌───────────────────────────────┐
│ SOVEREIGN AI · SOUL ON BTC │
└───────────────────────────────┘
`.trimStart();
/** All non-whitespace char indices in the ASCII art, randomised for scatter reveal */
let _charSpans = [];
let _totalChars = 0;
/**
* Initialise the loading screen: build character spans for the ASCII art.
* Call once before the first setLoadingProgress().
*/
export function initLoadingArt() {
const logoEl = document.getElementById('ascii-logo');
if (!logoEl) return;
// Build span-per-char markup — whitespace stays as plain text nodes for layout.
const chars = [...TIMMY_ASCII];
let html = '';
let idx = 0;
for (const ch of chars) {
if (ch === '\n') {
html += '\n';
} else if (ch === ' ') {
html += ' ';
} else {
html += `<span class="ac" data-i="${idx}">${ch}</span>`;
idx++;
}
}
_totalChars = idx;
logoEl.innerHTML = html;
// Cache span elements for fast access
_charSpans = Array.from(logoEl.querySelectorAll('.ac'));
}
/**
* Update loading progress.
* @param {number} percent 0100
* @param {string} [msg] Optional status line text
*/
export function setLoadingProgress(percent, msg) {
const pct = Math.max(0, Math.min(100, percent));
// How many chars should be "lit" at this progress level
const litCount = Math.round((_totalChars * pct) / 100);
for (let i = 0; i < _charSpans.length; i++) {
if (i < litCount) {
_charSpans[i].classList.add('lit');
}
}
// Progress bar fill
const bar = document.getElementById('loading-bar-fill');
if (bar) bar.style.width = `${pct}%`;
// Percentage label
const pctEl = document.getElementById('loading-percent');
if (pctEl) pctEl.textContent = `${Math.round(pct)}%`;
// Status message
if (msg) {
const msgEl = document.getElementById('loading-msg');
if (msgEl) msgEl.textContent = msg;
}
}

View File

@@ -8,6 +8,7 @@ import { initUI, updateUI } from './ui.js';
import { initInteraction, updateControls, disposeInteraction } from './interaction.js';
import { initWebSocket, getConnectionState, getJobCount } from './websocket.js';
import { initVisitor } from './visitor.js';
import { initLoadingArt, setLoadingProgress } from './loading.js';
let running = false;
let canvas = null;
@@ -22,26 +23,40 @@ let canvas = null;
* Agent state map captured just before teardown; reapplied after initAgents.
*/
function buildWorld(firstInit, stateSnapshot) {
if (firstInit) setLoadingProgress(10, 'RENDERING ENGINE...');
const { scene, camera, renderer } = initWorld(canvas);
canvas = renderer.domElement;
if (firstInit) setLoadingProgress(30, 'EFFECTS SYSTEM...');
initEffects(scene);
if (firstInit) setLoadingProgress(50, 'SPAWNING AGENTS...');
initAgents(scene);
if (stateSnapshot) {
applyAgentStates(stateSnapshot);
}
if (firstInit) setLoadingProgress(70, 'ARMING CONTROLS...');
initInteraction(camera, renderer);
if (firstInit) {
setLoadingProgress(80, 'LOADING INTERFACE...');
initUI();
setLoadingProgress(90, 'CONNECTING TO GRID...');
initWebSocket(scene);
setLoadingProgress(95, 'INITIALIZING VISITOR...');
initVisitor();
// Dismiss loading screen
setLoadingProgress(100, 'WELCOME TO TOWER WORLD');
// Dismiss loading screen after a brief moment so 100% is visible
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) loadingScreen.classList.add('hidden');
if (loadingScreen) {
setTimeout(() => loadingScreen.classList.add('hidden'), 400);
}
}
// Debounce resize to 1 call per frame
@@ -117,6 +132,9 @@ function teardown({ scene, renderer, ac }) {
function main() {
const $overlay = document.getElementById('webgl-recovery-overlay');
initLoadingArt();
setLoadingProgress(0, 'BOOTING MATRIX...');
let handle = buildWorld(true, null);
// WebGL context loss recovery (iPad PWA, GPU driver reset, etc.)