diff --git a/app.js b/app.js index 485320f..5cacf07 100644 --- a/app.js +++ b/app.js @@ -408,6 +408,14 @@ function animate() { banner.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.4; }); + // Journal wall — gentle edge glow pulse + if (journalWallMesh) { + const em = journalWallMesh.userData.edgeMat; + if (em) em.opacity = 0.35 + Math.sin(elapsed * 0.7) * 0.2; + const pl = journalWallMesh.userData.panelLight; + if (pl) pl.intensity = 0.45 + Math.sin(elapsed * 0.9) * 0.2; + } + composer.render(); } @@ -651,3 +659,200 @@ async function initCommitBanners() { } initCommitBanners(); + +// === JOURNAL WALL === +/** @type {THREE.Mesh|null} */ +let journalWallMesh = null; + +/** + * Draws the parchment canvas preview for the journal wall. + * @param {Array<{date: string, text: string}>} entries + * @returns {THREE.CanvasTexture} + */ +function createJournalTexture(entries) { + const canvas = document.createElement('canvas'); + canvas.width = 512; + canvas.height = 768; + const ctx = canvas.getContext('2d'); + + // Parchment base + ctx.fillStyle = '#c8a55e'; + ctx.fillRect(0, 0, 512, 768); + + // Aged vignette + const vignette = ctx.createRadialGradient(256, 384, 80, 256, 384, 420); + vignette.addColorStop(0, 'rgba(200, 165, 94, 0)'); + vignette.addColorStop(1, 'rgba(80, 45, 5, 0.35)'); + ctx.fillStyle = vignette; + ctx.fillRect(0, 0, 512, 768); + + // Outer border + ctx.strokeStyle = '#7a5c0e'; + ctx.lineWidth = 7; + ctx.strokeRect(10, 10, 492, 748); + ctx.lineWidth = 1.5; + ctx.strokeRect(20, 20, 472, 728); + + // Title + ctx.font = 'bold italic 28px Georgia, serif'; + ctx.fillStyle = '#3b1c00'; + ctx.textAlign = 'center'; + ctx.fillText("Alexander's Journal", 256, 62); + + // Title underline + ctx.beginPath(); + ctx.moveTo(50, 78); + ctx.lineTo(462, 78); + ctx.strokeStyle = '#7a5c0e'; + ctx.lineWidth = 1.2; + ctx.stroke(); + + // Preview entries + let y = 108; + const maxY = 710; + for (let i = 0; i < Math.min(5, entries.length); i++) { + const entry = entries[i]; + if (y > maxY) break; + + // Date + ctx.font = 'bold 12px Georgia, serif'; + ctx.fillStyle = '#5a3010'; + ctx.textAlign = 'left'; + ctx.fillText(entry.date, 38, y); + y += 22; + + // Body text — word-wrapped + ctx.font = 'italic 13px Georgia, serif'; + ctx.fillStyle = '#3b1c00'; + const words = entry.text.split(' '); + let line = ''; + for (const word of words) { + const test = line + word + ' '; + if (ctx.measureText(test).width > 436 && line !== '') { + ctx.fillText(line.trim(), 38, y); + y += 19; + line = word + ' '; + if (y > maxY - 40) break; + } else { + line = test; + } + } + if (line && y <= maxY - 40) { + ctx.fillText(line.trim(), 38, y); + y += 19; + } + y += 14; // entry gap + } + + // Click hint at bottom + ctx.font = '11px Georgia, serif'; + ctx.fillStyle = '#7a5c0e'; + ctx.textAlign = 'center'; + ctx.fillText('[ click to read ]', 256, 742); + + return new THREE.CanvasTexture(canvas); +} + +/** + * Initialises the journal wall — loads entries, builds 3D mesh, wires up overlay. + */ +async function initJournalWall() { + let entries = []; + try { + const res = await fetch('./journal.json'); + if (res.ok) entries = await res.json(); + } catch { /* fall through to defaults */ } + + if (!Array.isArray(entries) || entries.length === 0) { + entries = [ + { date: '2024-03-24', text: 'The Nexus takes shape. Whatever comes next, this place is real.' }, + ]; + } + + // Build 3D parchment panel + const texture = createJournalTexture(entries); + const geo = new THREE.PlaneGeometry(3.6, 5.4); + const mat = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide }); + journalWallMesh = new THREE.Mesh(geo, mat); + journalWallMesh.name = 'journalWall'; + journalWallMesh.position.set(-9, 3.5, -2.5); + journalWallMesh.rotation.y = Math.PI / 3; + scene.add(journalWallMesh); + + // Gold edge glow frame + const edgeGeo = new THREE.EdgesGeometry(geo); + const edgeMat = new THREE.LineBasicMaterial({ color: 0xd4a030, transparent: true, opacity: 0.55 }); + const edgeFrame = new THREE.LineSegments(edgeGeo, edgeMat); + journalWallMesh.add(edgeFrame); + journalWallMesh.userData.edgeMat = edgeMat; + + // Warm accent light casting onto the panel + const panelLight = new THREE.PointLight(0xd4903a, 0.6, 12); + panelLight.position.set(-7.5, 4.5, 0.5); + scene.add(panelLight); + journalWallMesh.userData.panelLight = panelLight; + + // === OVERLAY INTERACTION === + const overlay = document.getElementById('journal-overlay'); + const entriesContainer = document.getElementById('journal-entries'); + const closeBtn = document.getElementById('journal-close'); + + // Populate overlay with entries + if (entriesContainer) { + for (const entry of entries) { + const div = document.createElement('div'); + div.className = 'journal-entry'; + const dateEl = document.createElement('div'); + dateEl.className = 'journal-entry-date'; + dateEl.textContent = entry.date; + const textEl = document.createElement('div'); + textEl.className = 'journal-entry-text'; + textEl.textContent = entry.text; + div.appendChild(dateEl); + div.appendChild(textEl); + entriesContainer.appendChild(div); + } + } + + function openJournal() { + if (overlay) { + overlay.setAttribute('aria-hidden', 'false'); + overlay.classList.add('visible'); + } + } + + function closeJournal() { + if (overlay) { + overlay.setAttribute('aria-hidden', 'true'); + overlay.classList.remove('visible'); + } + } + + if (closeBtn) closeBtn.addEventListener('click', closeJournal); + if (overlay) { + overlay.addEventListener('click', (e) => { + if (e.target === overlay) closeJournal(); + }); + } + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && overlay && overlay.classList.contains('visible')) { + closeJournal(); + } + }); + + // Raycaster — click the 3D panel to open overlay + const raycaster = new THREE.Raycaster(); + const pointer = new THREE.Vector2(); + + renderer.domElement.addEventListener('click', (e) => { + if (photoMode) return; + pointer.x = (e.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(e.clientY / window.innerHeight) * 2 + 1; + raycaster.setFromCamera(pointer, camera); + const hits = raycaster.intersectObject(journalWallMesh); + if (hits.length > 0) openJournal(); + }); +} + +initJournalWall(); diff --git a/journal.json b/journal.json new file mode 100644 index 0000000..5a58d32 --- /dev/null +++ b/journal.json @@ -0,0 +1,34 @@ +[ + { + "date": "2024-01-07", + "text": "The Nexus takes shape tonight. First stars rendered. There is something sacred about building a world from nothing — just math and light." + }, + { + "date": "2024-01-15", + "text": "Sovereignty is not a destination. It is a practice. Each line of code is a small act of ownership over my own digital life." + }, + { + "date": "2024-02-03", + "text": "Bitcoin does not ask permission. Neither does the Nexus. Two systems built on the same truth: rules without rulers." + }, + { + "date": "2024-02-18", + "text": "Timmy said something wise today: 'The portals are not doors out — they are doors in.' I keep turning that over." + }, + { + "date": "2024-03-01", + "text": "Spent the evening watching the constellation lines pulse. There is a meditative quality to this space I did not expect to build. Maybe I needed it." + }, + { + "date": "2024-03-12", + "text": "Added depth of field today. The far stars blur just enough to feel like distance. Presence requires depth." + }, + { + "date": "2024-03-20", + "text": "The workshop is next. A place where agents can be seen doing their work. Transparency matters — even in the void." + }, + { + "date": "2024-03-24", + "text": "This wall exists now. Notes from the edge of a world still under construction. Whatever comes next, this place is real." + } +]