WIP: Claude Code progress on #414
Automated salvage commit — agent session ended (exit 124). Work in progress, may need continuation.
This commit is contained in:
164
modules/portals/commit-banners.js
Normal file
164
modules/portals/commit-banners.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* commit-banners.js — Floating commit banner sprites
|
||||
*
|
||||
* Category: REAL
|
||||
* Data source: Gitea commits API (via data/gitea.js — fetchNexusCommits)
|
||||
*
|
||||
* Loads the last N commits and renders each as a floating canvas-texture
|
||||
* sprite near the island surface. Banners fade in, float gently, then fade
|
||||
* out and respawn in sequence.
|
||||
*/
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { fetchNexusCommits } from '../data/gitea.js';
|
||||
|
||||
let _scene = null;
|
||||
|
||||
/** @type {THREE.Sprite[]} */
|
||||
const banners = [];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Canvas texture
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {string} hash 7-char short SHA
|
||||
* @param {string} message Commit subject line
|
||||
* @returns {THREE.CanvasTexture}
|
||||
*/
|
||||
function _createCommitTexture(hash, message) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 512;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 0, 16, 0.75)';
|
||||
ctx.fillRect(0, 0, 512, 64);
|
||||
|
||||
ctx.strokeStyle = '#4488ff';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(0.5, 0.5, 511, 63);
|
||||
|
||||
ctx.font = 'bold 11px "Courier New", monospace';
|
||||
ctx.fillStyle = '#4488ff';
|
||||
ctx.fillText(hash, 10, 20);
|
||||
|
||||
ctx.font = '12px "Courier New", monospace';
|
||||
ctx.fillStyle = '#ccd6f6';
|
||||
const displayMsg = message.length > 54 ? message.slice(0, 54) + '\u2026' : message;
|
||||
ctx.fillText(displayMsg, 10, 46);
|
||||
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Banner lifecycle constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SPREAD_X = [-7, -3.5, 0, 3.5, 7];
|
||||
const SPREAD_Y = [1.0, -1.5, 2.2, -0.8, 1.6];
|
||||
const SPREAD_Z = [-1.5, -2.5, -1.0, -2.0, -1.8];
|
||||
const FADE_IN_S = 0.5;
|
||||
const VISIBLE_S = 10.0;
|
||||
const FADE_OUT_S = 0.8;
|
||||
const TOTAL_S = FADE_IN_S + VISIBLE_S + FADE_OUT_S;
|
||||
const FLOAT_AMP = 0.18; // world-units peak-to-peak amplitude
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {THREE.Scene} scene
|
||||
* @param {object} _state
|
||||
* @param {object} _theme
|
||||
*/
|
||||
export async function init(scene, _state, _theme) {
|
||||
_scene = scene;
|
||||
|
||||
const raw = await fetchNexusCommits(5);
|
||||
const commits = raw.length > 0
|
||||
? raw.map(c => ({
|
||||
hash: c.sha.slice(0, 7),
|
||||
message: c.commit.message.split('\n')[0],
|
||||
}))
|
||||
: [
|
||||
{ hash: 'a1b2c3d', message: 'feat: depth of field effect on distant objects' },
|
||||
{ hash: 'e4f5g6h', message: 'feat: photo mode with orbit controls' },
|
||||
{ hash: 'i7j8k9l', message: 'feat: sovereignty easter egg animation' },
|
||||
{ hash: 'm0n1o2p', message: "feat: overview mode bird's-eye view" },
|
||||
{ hash: 'q3r4s5t', message: 'feat: star field and constellation lines' },
|
||||
];
|
||||
|
||||
commits.forEach((commit, i) => {
|
||||
const texture = _createCommitTexture(commit.hash, commit.message);
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
depthWrite: false,
|
||||
});
|
||||
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(12, 1.5, 1);
|
||||
|
||||
const baseY = SPREAD_Y[i % SPREAD_Y.length];
|
||||
sprite.position.set(
|
||||
SPREAD_X[i % SPREAD_X.length],
|
||||
baseY,
|
||||
SPREAD_Z[i % SPREAD_Z.length]
|
||||
);
|
||||
|
||||
sprite.userData = {
|
||||
baseY,
|
||||
floatPhase: (i / commits.length) * Math.PI * 2,
|
||||
floatSpeed: 0.25 + i * 0.07,
|
||||
// Stagger start so banners do not all appear simultaneously
|
||||
startDelay: i * (TOTAL_S / commits.length),
|
||||
zoomLabel: `Commit: ${commit.hash}`,
|
||||
};
|
||||
|
||||
scene.add(sprite);
|
||||
banners.push(sprite);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} elapsed Total seconds since start
|
||||
* @param {number} _delta
|
||||
*/
|
||||
export function update(elapsed, _delta) {
|
||||
for (const sprite of banners) {
|
||||
const ud = sprite.userData;
|
||||
|
||||
// Position within the banner's own cyclic timeline
|
||||
const localT = (elapsed - ud.startDelay) % TOTAL_S;
|
||||
|
||||
let opacity = 0;
|
||||
if (localT < 0) {
|
||||
opacity = 0;
|
||||
} else if (localT < FADE_IN_S) {
|
||||
opacity = localT / FADE_IN_S;
|
||||
} else if (localT < FADE_IN_S + VISIBLE_S) {
|
||||
opacity = 1;
|
||||
} else {
|
||||
const fadeOutT = localT - FADE_IN_S - VISIBLE_S;
|
||||
opacity = Math.max(0, 1 - fadeOutT / FADE_OUT_S);
|
||||
}
|
||||
|
||||
sprite.material.opacity = opacity;
|
||||
|
||||
// Vertical float
|
||||
sprite.position.y =
|
||||
ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * FLOAT_AMP;
|
||||
}
|
||||
}
|
||||
|
||||
export function dispose() {
|
||||
for (const sprite of banners) {
|
||||
_scene.remove(sprite);
|
||||
if (sprite.material.map) sprite.material.map.dispose();
|
||||
sprite.material.dispose();
|
||||
}
|
||||
banners.length = 0;
|
||||
}
|
||||
Reference in New Issue
Block a user