648 lines
22 KiB
JavaScript
648 lines
22 KiB
JavaScript
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
|
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
|
import { BokehPass } from 'three/addons/postprocessing/BokehPass.js';
|
|
import { LoadingManager } from 'three';
|
|
|
|
// === COLOR PALETTE ===
|
|
const NEXUS = {
|
|
colors: {
|
|
bg: 0x000008,
|
|
starCore: 0xffffff,
|
|
starDim: 0x8899cc,
|
|
constellationLine: 0x334488,
|
|
constellationFade: 0x112244,
|
|
accent: 0x4488ff,
|
|
}
|
|
};
|
|
|
|
// === ASSET LOADER ===
|
|
const loadedAssets = new Map();
|
|
|
|
const loadingManager = new THREE.LoadingManager(() => {
|
|
document.getElementById('loading-bar').style.width = '100%';
|
|
document.getElementById('loading').style.display = 'none';
|
|
animate();
|
|
});
|
|
|
|
loadingManager.onProgress = (url, itemsLoaded, itemsTotal) => {
|
|
const progress = (itemsLoaded / itemsTotal) * 100;
|
|
document.getElementById('loading-bar').style.width = `${progress}%`;
|
|
};
|
|
|
|
// Simulate loading a texture for demonstration
|
|
const textureLoader = new THREE.TextureLoader(loadingManager);
|
|
textureLoader.load('placeholder-texture.jpg', (texture) => {
|
|
loadedAssets.set('placeholder-texture', texture);
|
|
});
|
|
|
|
// === SCENE SETUP ===
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(NEXUS.colors.bg);
|
|
|
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
|
|
camera.position.set(0, 6, 11);
|
|
|
|
// === LIGHTING ===
|
|
// Required for MeshStandardMaterial / MeshPhysicalMaterial used on the platform.
|
|
const ambientLight = new THREE.AmbientLight(0x0a1428, 1.4);
|
|
scene.add(ambientLight);
|
|
|
|
const overheadLight = new THREE.PointLight(0x8899bb, 0.6, 60);
|
|
overheadLight.position.set(0, 25, 0);
|
|
scene.add(overheadLight);
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// === STAR FIELD ===
|
|
const STAR_COUNT = 800;
|
|
const STAR_SPREAD = 400;
|
|
const CONSTELLATION_DISTANCE = 30; // max distance to draw a line between stars
|
|
|
|
const starPositions = [];
|
|
const starGeo = new THREE.BufferGeometry();
|
|
const posArray = new Float32Array(STAR_COUNT * 3);
|
|
const sizeArray = new Float32Array(STAR_COUNT);
|
|
|
|
for (let i = 0; i < STAR_COUNT; i++) {
|
|
const x = (Math.random() - 0.5) * STAR_SPREAD;
|
|
const y = (Math.random() - 0.5) * STAR_SPREAD;
|
|
const z = (Math.random() - 0.5) * STAR_SPREAD;
|
|
posArray[i * 3] = x;
|
|
posArray[i * 3 + 1] = y;
|
|
posArray[i * 3 + 2] = z;
|
|
sizeArray[i] = Math.random() * 2.5 + 0.5;
|
|
starPositions.push(new THREE.Vector3(x, y, z));
|
|
}
|
|
|
|
starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
|
|
starGeo.setAttribute('size', new THREE.BufferAttribute(sizeArray, 1));
|
|
|
|
const starMaterial = new THREE.PointsMaterial({
|
|
color: NEXUS.colors.starCore,
|
|
size: 0.6,
|
|
sizeAttenuation: true,
|
|
transparent: true,
|
|
opacity: 0.9,
|
|
});
|
|
|
|
const stars = new THREE.Points(starGeo, starMaterial);
|
|
scene.add(stars);
|
|
|
|
// === CONSTELLATION LINES ===
|
|
// Connect nearby stars with faint lines, limited to avoid clutter
|
|
/**
|
|
* Builds constellation line segments connecting nearby stars.
|
|
* @returns {THREE.LineSegments}
|
|
*/
|
|
function buildConstellationLines() {
|
|
const linePositions = [];
|
|
const MAX_CONNECTIONS_PER_STAR = 3;
|
|
const connectionCount = new Array(STAR_COUNT).fill(0);
|
|
|
|
for (let i = 0; i < STAR_COUNT; i++) {
|
|
if (connectionCount[i] >= MAX_CONNECTIONS_PER_STAR) continue;
|
|
|
|
// Find nearest neighbors
|
|
const neighbors = [];
|
|
for (let j = i + 1; j < STAR_COUNT; j++) {
|
|
if (connectionCount[j] >= MAX_CONNECTIONS_PER_STAR) continue;
|
|
const dist = starPositions[i].distanceTo(starPositions[j]);
|
|
if (dist < CONSTELLATION_DISTANCE) {
|
|
neighbors.push({ j, dist });
|
|
}
|
|
}
|
|
|
|
// Sort by distance and connect closest ones
|
|
neighbors.sort((/** @type {{j: number, dist: number}} */ a, /** @type {{j: number, dist: number}} */ b) => a.dist - b.dist);
|
|
const toConnect = neighbors.slice(0, MAX_CONNECTIONS_PER_STAR - connectionCount[i]);
|
|
|
|
for (const { j } of toConnect) {
|
|
linePositions.push(
|
|
starPositions[i].x, starPositions[i].y, starPositions[i].z,
|
|
starPositions[j].x, starPositions[j].y, starPositions[j].z
|
|
);
|
|
connectionCount[i]++;
|
|
connectionCount[j]++;
|
|
}
|
|
}
|
|
|
|
const lineGeo = new THREE.BufferGeometry();
|
|
lineGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(linePositions), 3));
|
|
|
|
const lineMat = new THREE.LineBasicMaterial({
|
|
color: NEXUS.colors.constellationLine,
|
|
transparent: true,
|
|
opacity: 0.18,
|
|
});
|
|
|
|
return new THREE.LineSegments(lineGeo, lineMat);
|
|
}
|
|
|
|
const constellationLines = buildConstellationLines();
|
|
scene.add(constellationLines);
|
|
|
|
// === GLASS PLATFORM ===
|
|
// Central floating platform with transparent glass-floor sections revealing the void (star field) below.
|
|
|
|
const glassPlatformGroup = new THREE.Group();
|
|
|
|
// Dark metallic frame material
|
|
const platformFrameMat = new THREE.MeshStandardMaterial({
|
|
color: 0x0a1828,
|
|
metalness: 0.9,
|
|
roughness: 0.1,
|
|
emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.06),
|
|
});
|
|
|
|
// Outer solid rim (flat ring)
|
|
const platformRimGeo = new THREE.RingGeometry(4.7, 5.3, 64);
|
|
const platformRim = new THREE.Mesh(platformRimGeo, platformFrameMat);
|
|
platformRim.rotation.x = -Math.PI / 2;
|
|
glassPlatformGroup.add(platformRim);
|
|
|
|
// Raised border torus for visible 3-D thickness
|
|
const borderTorusGeo = new THREE.TorusGeometry(5.0, 0.1, 6, 64);
|
|
const borderTorus = new THREE.Mesh(borderTorusGeo, platformFrameMat);
|
|
borderTorus.rotation.x = Math.PI / 2;
|
|
glassPlatformGroup.add(borderTorus);
|
|
|
|
// Glass tile material — highly transmissive to reveal the void below
|
|
const glassTileMat = new THREE.MeshPhysicalMaterial({
|
|
color: new THREE.Color(NEXUS.colors.accent),
|
|
transparent: true,
|
|
opacity: 0.09,
|
|
roughness: 0.0,
|
|
metalness: 0.0,
|
|
transmission: 0.92,
|
|
thickness: 0.06,
|
|
side: THREE.DoubleSide,
|
|
depthWrite: false,
|
|
});
|
|
|
|
// Edge glow — bright accent outline on each tile
|
|
const glassEdgeBaseMat = new THREE.LineBasicMaterial({
|
|
color: NEXUS.colors.accent,
|
|
transparent: true,
|
|
opacity: 0.55,
|
|
});
|
|
|
|
const GLASS_TILE_SIZE = 0.85;
|
|
const GLASS_TILE_GAP = 0.14;
|
|
const GLASS_TILE_STEP = GLASS_TILE_SIZE + GLASS_TILE_GAP;
|
|
const GLASS_RADIUS = 4.55;
|
|
|
|
const tileGeo = new THREE.PlaneGeometry(GLASS_TILE_SIZE, GLASS_TILE_SIZE);
|
|
const tileEdgeGeo = new THREE.EdgesGeometry(tileGeo);
|
|
|
|
/** @type {Array<{mat: THREE.LineBasicMaterial, distFromCenter: number}>} */
|
|
|
|
const glassEdgeMaterials = [];
|
|
/** @type {Array<number>} */
|
|
const heatmapValues = []; // Stores normalized heat for each tile
|
|
|
|
for (let row = -5; row <= 5; row++) {
|
|
for (let col = -5; col <= 5; col++) {
|
|
const x = col * GLASS_TILE_STEP;
|
|
const z = row * GLASS_TILE_STEP;
|
|
const distFromCenter = Math.sqrt(x * x + z * z);
|
|
if (distFromCenter > GLASS_RADIUS) continue;
|
|
|
|
// Transparent glass tile
|
|
const tile = new THREE.Mesh(tileGeo, glassTileMat.clone());
|
|
tile.rotation.x = -Math.PI / 2;
|
|
tile.position.set(x, 0, z);
|
|
glassPlatformGroup.add(tile);
|
|
|
|
// Glowing edge lines
|
|
const mat = glassEdgeBaseMat.clone();
|
|
const edges = new THREE.LineSegments(tileEdgeGeo, mat);
|
|
edges.rotation.x = -Math.PI / 2;
|
|
edges.position.set(x, 0.002, z);
|
|
glassPlatformGroup.add(edges);
|
|
glassEdgeMaterials.push({ mat, distFromCenter, x, z });
|
|
heatmapValues.push(0); // Initialize heatmap value for each tile
|
|
}
|
|
}
|
|
|
|
// Map commit activity to tile positions
|
|
/**
|
|
* Calculates heatmap values based on commit data and updates the heatmapValues array.
|
|
* @param {Array<{sha: string, commit: {committer: {date: string}, message: string}}>} commits - Array of commit objects.
|
|
*/
|
|
function calculateHeatmap(commits) {
|
|
if (commits.length === 0) return;
|
|
|
|
// Group commits by week and count them
|
|
const weeklyCommitCounts = new Map(); // Week-start-date (ISO string) -> count
|
|
let maxCommitsInAWeek = 0;
|
|
|
|
commits.forEach(commit => {
|
|
const commitDate = new Date(commit.commit.committer.date);
|
|
// Get the start of the week (e.g., Sunday)
|
|
const weekStartDate = new Date(commitDate);
|
|
weekStartDate.setDate(commitDate.getDate() - commitDate.getDay());
|
|
weekStartDate.setHours(0, 0, 0, 0);
|
|
const weekKey = weekStartDate.toISOString().split('T')[0];
|
|
|
|
const currentCount = weeklyCommitCounts.get(weekKey) || 0;
|
|
weeklyCommitCounts.set(weekKey, currentCount + 1);
|
|
maxCommitsInAWeek = Math.max(maxCommitsInAWeek, currentCount + 1);
|
|
});
|
|
|
|
// Sort weeks by date (oldest to newest)
|
|
const sortedWeeks = Array.from(weeklyCommitCounts.keys()).sort();
|
|
|
|
// Determine a mapping for spatial distribution based on chronological order.
|
|
// We'll try to map older commits to outer tiles and newer commits to inner tiles.
|
|
// The glassEdgeMaterials array is roughly ordered by insertion, which means
|
|
// it doesn't have a direct spatial ordering like "inner to outer".
|
|
// Let's create a spatial mapping based on distFromCenter.
|
|
|
|
// Create a sorted list of tiles by distance from center
|
|
const sortedTilesByDistance = glassEdgeMaterials
|
|
.map((tile, index) => ({ index, distFromCenter: tile.distFromCenter }))
|
|
.sort((a, b) => a.distFromCenter - b.distFromCenter); // Inner to outer
|
|
|
|
// Distribute weeks across tiles based on their distance from center
|
|
const totalTiles = sortedTilesByDistance.length;
|
|
sortedWeeks.forEach((weekKey, weekIndex) => {
|
|
const commitsInWeek = weeklyCommitCounts.get(weekKey);
|
|
const normalizedCommits = commitsInWeek / maxCommitsInAWeek; // 0 to 1
|
|
|
|
// Assign heatmap value to tiles.
|
|
// Simple distribution: spread activity across all tiles,
|
|
// potentially weighting more recent weeks to central tiles.
|
|
// For now, let's just spread it out.
|
|
// Each tile can get a share of the overall "heat".
|
|
// A simple approach is to map the week index to a tile index.
|
|
const tileIndex = Math.floor((weekIndex / sortedWeeks.length) * totalTiles);
|
|
if (tileIndex < totalTiles) {
|
|
heatmapValues[sortedTilesByDistance[tileIndex].index] = normalizedCommits;
|
|
}
|
|
});
|
|
|
|
// Smooth out heatmap values with neighbors for a more organic look
|
|
const SMOOTHING_FACTOR = 0.5; // How much to blend with neighbors
|
|
const newHeatmapValues = [...heatmapValues];
|
|
|
|
for (let i = 0; i < glassEdgeMaterials.length; i++) {
|
|
let sumNeighbors = heatmapValues[i];
|
|
let neighborCount = 1;
|
|
|
|
// Find nearby tiles. This is not efficient, but for a small number of tiles it's okay.
|
|
// For a real-time system, a spatial hash or grid would be better.
|
|
const currentTile = glassEdgeMaterials[i];
|
|
for (let j = 0; j < glassEdgeMaterials.length; j++) {
|
|
if (i === j) continue;
|
|
const neighborTile = glassEdgeMaterials[j];
|
|
const dist = Math.sqrt(
|
|
(currentTile.x - neighborTile.x)**2 +
|
|
(currentTile.z - neighborTile.z)**2
|
|
);
|
|
if (dist < GLASS_TILE_STEP * 1.5) { // Check tiles within ~1.5 tile steps
|
|
sumNeighbors += heatmapValues[j];
|
|
neighborCount++;
|
|
}
|
|
}
|
|
const averagedHeat = sumNeighbors / neighborCount;
|
|
newHeatmapValues[i] = heatmapValues[i] * (1 - SMOOTHING_FACTOR) + averagedHeat * SMOOTHING_FACTOR;
|
|
}
|
|
for (let i = 0; i < heatmapValues.length; i++) {
|
|
heatmapValues[i] = newHeatmapValues[i];
|
|
}
|
|
}
|
|
|
|
// ... inside animate function ...
|
|
// Glass platform — heatmap edge glow
|
|
for (let i = 0; i < glassEdgeMaterials.length; i++) {
|
|
const { mat, distFromCenter } = glassEdgeMaterials[i];
|
|
const heat = heatmapValues[i] || 0; // Get pre-calculated heat value
|
|
const baseOpacity = 0.15; // Minimum opacity
|
|
const maxOpacity = 0.7; // Maximum opacity
|
|
const pulseStrength = 0.15; // How much the heatmap pulsates
|
|
const colorBlendFactor = heat; // How much to blend towards accent color
|
|
|
|
// Apply base opacity + heatmap + subtle pulse
|
|
mat.opacity = baseOpacity + (maxOpacity - baseOpacity) * heat + Math.sin(elapsed * 4 + distFromCenter) * pulseStrength * heat;
|
|
|
|
// Blend color towards accent based on heat
|
|
const baseColor = new THREE.Color(NEXUS.colors.constellationLine); // A slightly darker blue
|
|
const accentColor = new THREE.Color(NEXUS.colors.accent);
|
|
mat.color.copy(baseColor).lerp(accentColor, colorBlendFactor);
|
|
}
|
|
|
|
animate();
|
|
|
|
// === DEBUG MODE ===
|
|
let debugMode = false;
|
|
|
|
document.getElementById('debug-toggle').addEventListener('click', () => {
|
|
debugMode = !debugMode;
|
|
document.getElementById('debug-toggle').style.backgroundColor = debugMode
|
|
? 'var(--color-text-muted)'
|
|
: 'var(--color-secondary)';
|
|
console.log(`Debug mode ${debugMode ? 'enabled' : 'disabled'}`);
|
|
|
|
if (debugMode) {
|
|
// Example: Visualize all collision boxes and light sources
|
|
// Replace with actual logic when available
|
|
document.querySelectorAll('.collision-box').forEach((/** @type {HTMLElement} */ el) => el.style.outline = '2px solid red');
|
|
document.querySelectorAll('.light-source').forEach((/** @type {HTMLElement} */ el) => el.style.outline = '2px dashed yellow');
|
|
} else {
|
|
document.querySelectorAll('.collision-box, .light-source').forEach((/** @type {HTMLElement} */ el) => {
|
|
el.style.outline = 'none';
|
|
});
|
|
}
|
|
});
|
|
|
|
// === WEBSOCKET CLIENT ===
|
|
import { wsClient } from './ws-client.js';
|
|
|
|
wsClient.connect();
|
|
|
|
window.addEventListener('player-joined', (/** @type {CustomEvent} */ event) => {
|
|
console.log('Player joined:', event.detail);
|
|
});
|
|
|
|
window.addEventListener('player-left', (/** @type {CustomEvent} */ event) => {
|
|
console.log('Player left:', event.detail);
|
|
});
|
|
|
|
window.addEventListener('chat-message', (/** @type {CustomEvent} */ event) => {
|
|
console.log('Chat message:', event.detail);
|
|
if (typeof event.detail?.text === 'string' && event.detail.text.toLowerCase().includes('sovereignty')) {
|
|
triggerSovereigntyEasterEgg();
|
|
}
|
|
});
|
|
|
|
// === SOVEREIGNTY EASTER EGG ===
|
|
const SOVEREIGNTY_WORD = 'sovereignty';
|
|
let sovereigntyBuffer = '';
|
|
let sovereigntyBufferTimer = /** @type {ReturnType<typeof setTimeout>|null} */ (null);
|
|
|
|
const sovereigntyMsg = document.getElementById('sovereignty-msg');
|
|
|
|
/**
|
|
* Triggers the sovereignty Easter egg: stars pulse gold, message flashes.
|
|
*/
|
|
function triggerSovereigntyEasterEgg() {
|
|
// Flash constellation lines gold
|
|
const originalLineColor = constellationLines.material.color.getHex();
|
|
constellationLines.material.color.setHex(0xffd700);
|
|
constellationLines.material.opacity = 0.9;
|
|
|
|
// Stars burst gold
|
|
const originalStarColor = starMaterial.color.getHex();
|
|
const originalStarOpacity = starMaterial.opacity;
|
|
starMaterial.color.setHex(0xffd700);
|
|
starMaterial.opacity = 1.0;
|
|
|
|
// Show overlay message
|
|
if (sovereigntyMsg) {
|
|
sovereigntyMsg.classList.remove('visible');
|
|
// Force reflow so animation restarts
|
|
void sovereigntyMsg.offsetWidth;
|
|
sovereigntyMsg.classList.add('visible');
|
|
}
|
|
|
|
// Animate gold fade-out over 2.5s
|
|
const startTime = performance.now();
|
|
const DURATION = 2500;
|
|
|
|
function fadeBack() {
|
|
const t = Math.min((performance.now() - startTime) / DURATION, 1);
|
|
const eased = t * t; // ease in: slow start, fast end
|
|
|
|
// Interpolate star color back
|
|
const goldR = 1.0, goldG = 0.843, goldB = 0;
|
|
const origColor = new THREE.Color(originalStarColor);
|
|
starMaterial.color.setRGB(
|
|
goldR + (origColor.r - goldR) * eased,
|
|
goldG + (origColor.g - goldG) * eased,
|
|
goldB + (origColor.b - goldB) * eased
|
|
);
|
|
starMaterial.opacity = 1.0 + (originalStarOpacity - 1.0) * eased;
|
|
|
|
// Interpolate line color back
|
|
const origLineColor = new THREE.Color(originalLineColor);
|
|
constellationLines.material.color.setRGB(
|
|
1.0 + (origLineColor.r - 1.0) * eased,
|
|
0.843 + (origLineColor.g - 0.843) * eased,
|
|
0 + origLineColor.b * eased
|
|
);
|
|
|
|
if (t < 1) {
|
|
requestAnimationFrame(fadeBack);
|
|
} else {
|
|
// Restore originals exactly
|
|
starMaterial.color.setHex(originalStarColor);
|
|
starMaterial.opacity = originalStarOpacity;
|
|
constellationLines.material.color.setHex(originalLineColor);
|
|
if (sovereigntyMsg) sovereigntyMsg.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(fadeBack);
|
|
}
|
|
|
|
// Detect 'sovereignty' typed anywhere on the page (cheat-code style)
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
if (e.key.length !== 1) {
|
|
// Non-printable key resets buffer
|
|
sovereigntyBuffer = '';
|
|
return;
|
|
}
|
|
|
|
sovereigntyBuffer += e.key.toLowerCase();
|
|
|
|
// Keep only the last N chars needed
|
|
if (sovereigntyBuffer.length > SOVEREIGNTY_WORD.length) {
|
|
sovereigntyBuffer = sovereigntyBuffer.slice(-SOVEREIGNTY_WORD.length);
|
|
}
|
|
|
|
if (sovereigntyBuffer === SOVEREIGNTY_WORD) {
|
|
sovereigntyBuffer = '';
|
|
triggerSovereigntyEasterEgg();
|
|
}
|
|
|
|
// Reset buffer after 3s of inactivity
|
|
if (sovereigntyBufferTimer) clearTimeout(sovereigntyBufferTimer);
|
|
sovereigntyBufferTimer = setTimeout(() => { sovereigntyBuffer = ''; }, 3000);
|
|
});
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
wsClient.disconnect();
|
|
});
|
|
|
|
// === COMMIT BANNERS ===
|
|
const commitBanners = [];
|
|
|
|
/**
|
|
* Creates a canvas texture for a commit banner.
|
|
* @param {string} hash - Short commit hash
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
* Fetches recent commits and spawns floating banner sprites.
|
|
*/
|
|
|
|
const GITEA_API_BASE = 'http://143.198.27.163:3000/api/v1';
|
|
const REPO_OWNER = 'Timmy_Foundation';
|
|
const REPO_NAME = 'the-nexus';
|
|
const GITEA_TOKEN = 'f7bcdaf878d479ad7747873ff6739a9bb89e3f80'; // Provided token
|
|
|
|
/**
|
|
* Fetches commits from the Gitea API for a specified time range.
|
|
* @param {Date} since - Start date for commits.
|
|
* @param {Date} until - End date for commits.
|
|
* @param {number} limit - Maximum number of commits to fetch.
|
|
* @returns {Promise<Array<{sha: string, commit: {committer: {date: string}, message: string}}>>}
|
|
*/
|
|
async function fetchCommits(since, until, limit = 100) {
|
|
const sinceISO = since.toISOString();
|
|
const untilISO = until.toISOString();
|
|
const url = `${GITEA_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/commits?since=${sinceISO}&until=${untilISO}&limit=${limit}`;
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
headers: { 'Authorization': `token ${GITEA_TOKEN}` }
|
|
});
|
|
if (!res.ok) throw new Error(`Gitea API fetch failed: ${res.statusText}`);
|
|
return await res.json();
|
|
} catch (error) {
|
|
console.error('Error fetching commits:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function initCommitBanners() {
|
|
let commitsData;
|
|
const oneYearAgo = new Date();
|
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
const now = new Date();
|
|
|
|
// Fetch up to 200 commits from the last year for heatmap and banners
|
|
commitsData = await fetchCommits(oneYearAgo, now, 200);
|
|
|
|
let bannerCommits;
|
|
if (commitsData.length === 0) {
|
|
// Fallback to mock data if API fails or no commits are found
|
|
bannerCommits = [
|
|
{ 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' },
|
|
];
|
|
} else {
|
|
// Take the 5 most recent commits for banners
|
|
bannerCommits = commitsData.slice(0, 5).map(c => ({
|
|
hash: c.sha.slice(0, 7),
|
|
message: c.commit.message.split('\n')[0],
|
|
}));
|
|
}
|
|
|
|
// Only create banners if we have data for them
|
|
if (bannerCommits.length > 0) {
|
|
const spreadX = [-7, -3.5, 0, 3.5, 7];
|
|
const spreadY = [1.0, -1.5, 2.2, -0.8, 1.6];
|
|
const spreadZ = [-1.5, -2.5, -1.0, -2.0, -1.8];
|
|
|
|
bannerCommits.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);
|
|
sprite.position.set(
|
|
spreadX[i % spreadX.length],
|
|
spreadY[i % spreadY.length],
|
|
spreadZ[i % spreadZ.length]
|
|
);
|
|
sprite.userData = {
|
|
baseY: spreadY[i % spreadY.length],
|
|
floatPhase: (i / bannerCommits.length) * Math.PI * 2,
|
|
floatSpeed: 0.25 + i * 0.07,
|
|
startDelay: i * 2.5,
|
|
lifetime: 12 + i * 1.5,
|
|
spawnTime: /** @type {number|null} */ (null),
|
|
};
|
|
scene.add(sprite);
|
|
commitBanners.push(sprite);
|
|
});
|
|
}
|
|
|
|
// Proceed with heatmap calculation using all fetched commits
|
|
calculateHeatmap(commitsData);
|
|
}
|
|
|
|
const spreadX = [-7, -3.5, 0, 3.5, 7];
|
|
const spreadY = [1.0, -1.5, 2.2, -0.8, 1.6];
|
|
const spreadZ = [-1.5, -2.5, -1.0, -2.0, -1.8];
|
|
|
|
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);
|
|
sprite.position.set(
|
|
spreadX[i % spreadX.length],
|
|
spreadY[i % spreadY.length],
|
|
spreadZ[i % spreadZ.length]
|
|
);
|
|
sprite.userData = {
|
|
baseY: spreadY[i % spreadY.length],
|
|
floatPhase: (i / commits.length) * Math.PI * 2,
|
|
floatSpeed: 0.25 + i * 0.07,
|
|
startDelay: i * 2.5,
|
|
lifetime: 12 + i * 1.5,
|
|
spawnTime: /** @type {number|null} */ (null),
|
|
};
|
|
scene.add(sprite);
|
|
commitBanners.push(sprite);
|
|
});
|
|
}
|
|
|
|
initCommitBanners();
|