[claude] Floating bookshelves with spine labels of merged PRs (#264) (#335)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
This commit was merged in pull request #335.
This commit is contained in:
220
app.js
220
app.js
@@ -1174,6 +1174,12 @@ function animate() {
|
||||
loraPanelSprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22;
|
||||
}
|
||||
|
||||
// Animate floating bookshelves — gentle slow bob
|
||||
for (const shelf of bookshelfGroups) {
|
||||
const ud = shelf.userData;
|
||||
shelf.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.18;
|
||||
}
|
||||
|
||||
// Animate Timmy speech bubble — fade in, hold, fade out
|
||||
if (timmySpeechState) {
|
||||
const age = elapsed - timmySpeechState.startTime;
|
||||
@@ -1807,6 +1813,9 @@ window.addEventListener('beforeunload', () => {
|
||||
// === COMMIT BANNERS ===
|
||||
const commitBanners = [];
|
||||
|
||||
/** @type {THREE.Group[]} */
|
||||
const bookshelfGroups = [];
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2016,6 +2025,217 @@ async function initCommitBanners() {
|
||||
initCommitBanners();
|
||||
loadPortals();
|
||||
|
||||
// === FLOATING BOOKSHELVES ===
|
||||
// Floating bookshelves display merged PR history as books with spine labels.
|
||||
// Each book spine shows a PR number and truncated title rendered via canvas texture.
|
||||
|
||||
/**
|
||||
* Creates a canvas texture for a book spine.
|
||||
* @param {number} prNum
|
||||
* @param {string} title
|
||||
* @param {string} bgColor CSS color string for book cover
|
||||
* @returns {THREE.CanvasTexture}
|
||||
*/
|
||||
function createSpineTexture(prNum, title, bgColor) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128;
|
||||
canvas.height = 512;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Background — book cover color
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fillRect(0, 0, 128, 512);
|
||||
|
||||
// Accent border
|
||||
ctx.strokeStyle = '#4488ff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeRect(3, 3, 122, 506);
|
||||
|
||||
// PR number — accent blue, near top
|
||||
ctx.font = 'bold 32px "Courier New", monospace';
|
||||
ctx.fillStyle = '#4488ff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`#${prNum}`, 64, 58);
|
||||
|
||||
// Divider line
|
||||
ctx.strokeStyle = '#4488ff';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(12, 78);
|
||||
ctx.lineTo(116, 78);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1.0;
|
||||
|
||||
// Title — rotated 90° to read bottom-to-top (spine convention)
|
||||
ctx.save();
|
||||
ctx.translate(64, 300);
|
||||
ctx.rotate(-Math.PI / 2);
|
||||
const displayTitle = title.length > 30 ? title.slice(0, 30) + '\u2026' : title;
|
||||
ctx.font = '21px "Courier New", monospace';
|
||||
ctx.fillStyle = '#ccd6f6';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(displayTitle, 0, 0);
|
||||
ctx.restore();
|
||||
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a single floating bookshelf group and adds it to the scene.
|
||||
* @param {Array<{prNum: number, title: string}>} books
|
||||
* @param {THREE.Vector3} position
|
||||
* @param {number} rotationY
|
||||
*/
|
||||
function buildBookshelf(books, position, rotationY) {
|
||||
const group = new THREE.Group();
|
||||
group.position.copy(position);
|
||||
group.rotation.y = rotationY;
|
||||
|
||||
const SHELF_W = books.length * 0.52 + 0.6;
|
||||
const SHELF_THICKNESS = 0.12;
|
||||
const SHELF_DEPTH = 0.72;
|
||||
const ENDPANEL_H = 2.0;
|
||||
|
||||
// Dark metallic shelf material
|
||||
const shelfMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x0d1520,
|
||||
metalness: 0.6,
|
||||
roughness: 0.5,
|
||||
emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.02),
|
||||
});
|
||||
|
||||
// Shelf plank (horizontal)
|
||||
const plank = new THREE.Mesh(new THREE.BoxGeometry(SHELF_W, SHELF_THICKNESS, SHELF_DEPTH), shelfMat);
|
||||
group.add(plank);
|
||||
|
||||
// End panels
|
||||
const endGeo = new THREE.BoxGeometry(0.1, ENDPANEL_H, SHELF_DEPTH);
|
||||
const leftEnd = new THREE.Mesh(endGeo, shelfMat);
|
||||
leftEnd.position.set(-SHELF_W / 2, ENDPANEL_H / 2 - SHELF_THICKNESS / 2, 0);
|
||||
group.add(leftEnd);
|
||||
|
||||
const rightEnd = new THREE.Mesh(endGeo.clone(), shelfMat);
|
||||
rightEnd.position.set(SHELF_W / 2, ENDPANEL_H / 2 - SHELF_THICKNESS / 2, 0);
|
||||
group.add(rightEnd);
|
||||
|
||||
// Accent glow strip along front edge of shelf
|
||||
const glowStrip = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(SHELF_W, 0.035, 0.035),
|
||||
new THREE.MeshBasicMaterial({ color: NEXUS.colors.accent, transparent: true, opacity: 0.55 })
|
||||
);
|
||||
glowStrip.position.set(0, SHELF_THICKNESS / 2 + 0.017, SHELF_DEPTH / 2);
|
||||
group.add(glowStrip);
|
||||
|
||||
// Book cover colors — dark tones with slight variation
|
||||
const BOOK_COLORS = [
|
||||
'#0f0818', '#080f18', '#0f1108', '#07120e',
|
||||
'#130c06', '#060b12', '#120608', '#080812',
|
||||
];
|
||||
|
||||
// Spine thickness (X), book height (Y), cover depth (Z)
|
||||
// +Z face (index 4) = spine visible to viewer
|
||||
const bookStartX = -(SHELF_W / 2) + 0.36;
|
||||
books.forEach((book, i) => {
|
||||
const spineW = 0.34 + (i % 3) * 0.05; // slight width variation
|
||||
const bookH = 1.35 + (i % 4) * 0.13; // slight height variation
|
||||
const coverD = 0.58;
|
||||
|
||||
const bgColor = BOOK_COLORS[i % BOOK_COLORS.length];
|
||||
const spineTexture = createSpineTexture(book.prNum, book.title, bgColor);
|
||||
|
||||
const plainMat = new THREE.MeshStandardMaterial({
|
||||
color: new THREE.Color(bgColor),
|
||||
roughness: 0.85,
|
||||
metalness: 0.05,
|
||||
});
|
||||
const spineMat = new THREE.MeshBasicMaterial({ map: spineTexture });
|
||||
|
||||
// Material array: +X, -X, +Y, -Y, +Z (spine), -Z
|
||||
const bookMats = [plainMat, plainMat, plainMat, plainMat, spineMat, plainMat];
|
||||
|
||||
const bookGeo = new THREE.BoxGeometry(spineW, bookH, coverD);
|
||||
const bookMesh = new THREE.Mesh(bookGeo, bookMats);
|
||||
bookMesh.position.set(
|
||||
bookStartX + i * 0.5,
|
||||
SHELF_THICKNESS / 2 + bookH / 2,
|
||||
0
|
||||
);
|
||||
bookMesh.userData.zoomLabel = `PR #${book.prNum}: ${book.title.slice(0, 40)}`;
|
||||
group.add(bookMesh);
|
||||
});
|
||||
|
||||
// Soft point light beneath shelf for ambient glow
|
||||
const shelfLight = new THREE.PointLight(NEXUS.colors.accent, 0.25, 5);
|
||||
shelfLight.position.set(0, -0.4, 0);
|
||||
group.add(shelfLight);
|
||||
|
||||
group.userData.zoomLabel = 'PR Archive — Merged Contributions';
|
||||
group.userData.baseY = position.y;
|
||||
group.userData.floatPhase = bookshelfGroups.length * Math.PI;
|
||||
group.userData.floatSpeed = 0.17 + bookshelfGroups.length * 0.06;
|
||||
|
||||
scene.add(group);
|
||||
bookshelfGroups.push(group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches merged PRs and spawns floating bookshelves in the scene.
|
||||
*/
|
||||
async function initBookshelves() {
|
||||
let prs = [];
|
||||
try {
|
||||
const res = await fetch(
|
||||
'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/pulls?state=closed&limit=20',
|
||||
{ headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } }
|
||||
);
|
||||
if (!res.ok) throw new Error('fetch failed');
|
||||
const data = await res.json();
|
||||
prs = data
|
||||
.filter(/** @type {(p: any) => boolean} */ p => p.merged)
|
||||
.map(/** @type {(p: any) => {prNum: number, title: string}} */ p => ({
|
||||
prNum: p.number,
|
||||
// Strip "[claude]" prefix and trailing "(#N)" suffix for cleaner spine labels
|
||||
title: p.title
|
||||
.replace(/^\[[\w\s]+\]\s*/i, '')
|
||||
.replace(/\s*\(#\d+\)\s*$/, ''),
|
||||
}));
|
||||
} catch {
|
||||
// Fallback if API unreachable
|
||||
prs = [
|
||||
{ prNum: 324, title: 'Model training status — LoRA adapters' },
|
||||
{ prNum: 323, title: 'The Oath — interactive SOUL.md reading' },
|
||||
{ prNum: 320, title: 'Hermes session save/load' },
|
||||
{ prNum: 304, title: 'Session export as markdown' },
|
||||
{ prNum: 303, title: 'Procedural Web Audio ambient soundtrack' },
|
||||
{ prNum: 301, title: 'Warp tunnel effect for portals' },
|
||||
{ prNum: 296, title: 'Procedural terrain for floating island' },
|
||||
{ prNum: 294, title: 'Northern lights flash on PR merge' },
|
||||
];
|
||||
}
|
||||
|
||||
if (prs.length === 0) return;
|
||||
|
||||
// Split PRs across two shelves — left and right of the scene background
|
||||
const mid = Math.ceil(prs.length / 2);
|
||||
|
||||
buildBookshelf(
|
||||
prs.slice(0, mid),
|
||||
new THREE.Vector3(-8.5, 1.5, -4.5),
|
||||
Math.PI * 0.1, // slight angle toward scene center
|
||||
);
|
||||
|
||||
if (prs.slice(mid).length > 0) {
|
||||
buildBookshelf(
|
||||
prs.slice(mid),
|
||||
new THREE.Vector3(8.5, 1.5, -4.5),
|
||||
-Math.PI * 0.1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
initBookshelves();
|
||||
|
||||
// === THE OATH ===
|
||||
// Interactive reading of SOUL.md with dramatic lighting.
|
||||
// Trigger: press 'O' or double-click the tome object in the scene.
|
||||
|
||||
Reference in New Issue
Block a user