diff --git a/app.js b/app.js index 485320f..e6240d3 100644 --- a/app.js +++ b/app.js @@ -555,6 +555,181 @@ window.addEventListener('beforeunload', () => { wsClient.disconnect(); }); +// === VISITOR MEMORY === +const VISIT_KEY = 'nexus_visit_count'; +const visitCount = (parseInt(localStorage.getItem(VISIT_KEY) || '0', 10) || 0) + 1; +localStorage.setItem(VISIT_KEY, String(visitCount)); + +/** + * Draws tally marks on a canvas context. + * Groups of 5: four vertical strokes + one diagonal slash. + * @param {CanvasRenderingContext2D} ctx + * @param {number} count - Total ticks to draw + * @param {number} startX + * @param {number} startY + * @param {number} tickH - Height of each tick stroke in px + * @param {number} tickGap - Gap between strokes + */ +function drawTallyMarks(ctx, count, startX, startY, tickH, tickGap) { + const groupW = tickGap * 4 + tickH * 0.6 + tickGap; // width of a group of 5 + let x = startX; + let groupPos = 0; // position within current group (0-4) + + ctx.strokeStyle = '#4488ff'; + ctx.lineWidth = 2; + ctx.lineCap = 'round'; + + for (let i = 0; i < count; i++) { + if (groupPos < 4) { + // Vertical stroke + ctx.beginPath(); + ctx.moveTo(x, startY); + ctx.lineTo(x, startY + tickH); + ctx.stroke(); + x += tickGap; + groupPos++; + } else { + // Diagonal slash across the group + ctx.beginPath(); + ctx.moveTo(x - tickGap * 4 - tickGap * 0.5, startY + tickH * 0.9); + ctx.lineTo(x + tickGap * 0.5, startY + tickH * 0.1); + ctx.stroke(); + x += tickGap * 1.5; // extra gap after group + groupPos = 0; + } + } +} + +/** + * Creates a canvas texture for the visit history panel. + * @param {number} count + * @returns {THREE.CanvasTexture} + */ +function createVisitPanelTexture(count) { + const W = 256, H = 96; + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + + // Background + ctx.fillStyle = 'rgba(0, 0, 16, 0.82)'; + ctx.fillRect(0, 0, W, H); + + // Border + ctx.strokeStyle = '#334488'; + ctx.lineWidth = 1; + ctx.strokeRect(0.5, 0.5, W - 1, H - 1); + + // Header + ctx.font = 'bold 10px "Courier New", monospace'; + ctx.fillStyle = '#4488ff'; + ctx.fillText('TIMMY REMEMBERS YOU', 12, 18); + + // Divider + ctx.strokeStyle = '#223366'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(12, 24); + ctx.lineTo(W - 12, 24); + ctx.stroke(); + + // Visit count label + ctx.font = '9px "Courier New", monospace'; + ctx.fillStyle = '#8899cc'; + ctx.fillText(`VISIT COUNT: ${count}`, 12, 38); + + // Tally marks — show up to 30 ticks (5 groups of 6 groups) + const displayCount = Math.min(count, 30); + if (displayCount > 0) { + drawTallyMarks(ctx, displayCount, 12, 48, 18, 10); + } + + // Overflow indicator + if (count > 30) { + ctx.font = '9px "Courier New", monospace'; + ctx.fillStyle = '#4488ff'; + ctx.fillText(`+${count - 30} more`, 12, 85); + } + + return new THREE.CanvasTexture(canvas); +} + +/** + * Creates the 3D visit history panel sprite and adds it to the scene. + * @param {number} count + */ +function spawnVisitPanel(count) { + const texture = createVisitPanelTexture(count); + const mat = new THREE.SpriteMaterial({ + map: texture, + transparent: true, + opacity: 0, + depthWrite: false, + }); + const sprite = new THREE.Sprite(mat); + // Position: to the right of platform, slightly elevated + sprite.scale.set(7, 2.6, 1); + sprite.position.set(7.5, 2.2, -1.5); + scene.add(sprite); + + // Fade in over 1.5s, hold, then persist at low opacity + const startTime = performance.now(); + const FADE_IN = 1500; + const HOLD = 3000; + const FADE_OUT = 1500; + + function tick() { + const elapsed = performance.now() - startTime; + let opacity; + if (elapsed < FADE_IN) { + opacity = elapsed / FADE_IN; + } else if (elapsed < FADE_IN + HOLD) { + opacity = 1; + } else if (elapsed < FADE_IN + HOLD + FADE_OUT) { + opacity = 1 - (elapsed - FADE_IN - HOLD) / FADE_OUT; + } else { + // Settle at a subtle persistent opacity + opacity = 0.35; + mat.opacity = opacity; + return; + } + mat.opacity = opacity * 0.9; + requestAnimationFrame(tick); + } + requestAnimationFrame(tick); +} + +/** + * Shows the visitor welcome overlay and spawns the history panel. + */ +function initVisitorMemory() { + const welcomeEl = document.getElementById('visitor-welcome'); + if (!welcomeEl) return; + + let message; + if (visitCount === 1) { + message = 'Welcome to the Nexus — first time here'; + } else { + message = `Welcome back — visit #${visitCount}`; + } + welcomeEl.innerHTML = message; + + // Show after a brief pause so the scene is visible first + setTimeout(() => { + welcomeEl.classList.add('visible'); + // Remove class after animation so it can replay on next call if needed + welcomeEl.addEventListener('animationend', () => { + welcomeEl.classList.remove('visible'); + }, { once: true }); + }, 1200); + + // Spawn 3D history panel + spawnVisitPanel(visitCount); +} + +initVisitorMemory(); + // === COMMIT BANNERS === const commitBanners = []; diff --git a/index.html b/index.html index 795f24e..3d03b06 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,8 @@