index.html comm site with arxiv

This commit is contained in:
kai
2026-02-04 22:23:11 -05:00
parent 118df45d41
commit 0cfebb6f42
2 changed files with 374 additions and 139 deletions

View File

@@ -0,0 +1,86 @@
export class ArxivService {
constructor() {
this.baseUrl = "https://export.arxiv.org/api/query";
this.category = "cat:cs.AI";
this.maxResults = 10;
this.papers = [];
this.fetching = false;
this.nextStart = 0;
}
async fetchPapers() {
if (this.fetching) return;
this.fetching = true;
try {
// Sort by submittedDate descending (newest first)
// Use corsproxy.io to bypass CORS in browser environment
const query = `https://corsproxy.io/?${encodeURIComponent(this.baseUrl + `?search_query=${this.category}&sortBy=submittedDate&sortOrder=descending&start=${this.nextStart}&max_results=${this.maxResults}`)}`;
const response = await fetch(query);
const str = await response.text();
// Parse XML
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(str, "text/xml");
const entries = xmlDoc.getElementsByTagName("entry");
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const titleNode = entry.getElementsByTagName("title")[0];
const publishedNode = entry.getElementsByTagName("published")[0];
if (!titleNode || !publishedNode) continue;
const title = titleNode.textContent.replace(/\n/g, ' ').trim();
// Authors
const authorNodes = Array.from(entry.getElementsByTagName("author"));
let authors = "Unknown";
if (authorNodes.length > 0) {
const names = authorNodes.map(a => {
const n = a.getElementsByTagName("name")[0];
return n ? n.textContent : "";
}).filter(n => n);
if (names.length > 2) {
authors = names.slice(0, 2).join(', ') + " et al.";
} else {
authors = names.join(', ');
}
}
const published = publishedNode.textContent;
const year = new Date(published).getFullYear();
this.papers.push({
year: year,
name: title,
authors: authors,
isPaper: true,
// Use a unique ID to avoid dupes if api acts up?
id: entry.getElementsByTagName("id")[0]?.textContent
});
}
this.nextStart += this.maxResults;
} catch (e) {
console.error("Arxiv fetch failed", e);
} finally {
this.fetching = false;
}
}
async getNextPaper() {
// Buffer management
if (this.papers.length < 3) {
this.fetchPapers();
}
// Return null if empty (async wait loop handled by caller or just try next frame)
if (this.papers.length === 0) return null;
return this.papers.shift();
}
}

View File

@@ -9,7 +9,7 @@
body {
margin: 0;
overflow-x: hidden;
background-color: #f0f0f0;
background-color: #eeeee6;
font-family: 'Courier New', Courier, monospace;
}
@@ -323,14 +323,14 @@
z-index: 10;
color: #333;
text-align: center;
background: rgba(255, 255, 255, 0.85);
padding: 10px 25px;
border-radius: 30px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
background: transparent;
padding: 0;
border-radius: 0;
box-shadow: none;
pointer-events: none;
backdrop-filter: blur(5px);
backdrop-filter: none;
font-weight: bold;
border: 1px solid rgba(0,0,0,0.05);
border: none;
}
</style>
<!-- Load Three.js -->
@@ -347,6 +347,7 @@
</div>
<!-- 1. The Journey/Process Card -->
<!--
<div id="process-card" class="feed-card">
<div class="card-header">
<span>The Journey</span>
@@ -362,19 +363,22 @@
<li class="feed-item">Innovate in the AI Space</li>
</ul>
</div>
-->
<!-- 2. News Feed Card (Left) -->
<!--
<div id="news-card" class="feed-card">
<div class="card-header">
<span>Recent News</span>
<span style="font-size:0.7em; opacity:0.8;">UPDATES</span>
</div>
<ul class="feed-list" id="news-list">
<!-- Fake News Items -->
</ul>
</div>
-->
<!-- 3. Submission Feed Card (Left - After News) -->
<!-- 3. Submission Feed Card (Left - Appears after News) -->
<div id="submission-card" class="feed-card">
<div class="card-header">
<span>Recent Submissions</span>
@@ -389,8 +393,25 @@
<div id="scroll-track"></div>
<div id="markers-container"></div>
<div class="ui-overlay">
<span>Scroll to traverse the History of AI</span>
<div class="caret-up"></div>
<div style="font-size: 10px; letter-spacing: 2px; margin-top: 8px;">SCROLL</div>
</div>
<style>
.caret-up {
width: 24px;
height: 24px;
border-left: 4px solid #333;
border-top: 4px solid #333;
transform: rotate(45deg);
margin: 0 auto;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {transform: rotate(45deg) translate(0, 0);}
40% {transform: rotate(45deg) translate(-5px, -5px);}
60% {transform: rotate(45deg) translate(-3px, -3px);}
}
</style>
<!-- Sky Shader Vertex -->
<script id="sky-vs" type="x-shader/x-vertex">
@@ -406,99 +427,19 @@
<!-- Sky Shader Fragment -->
<script id="sky-fs" type="x-shader/x-fragment">
uniform float uTime;
uniform vec3 uColor;
uniform float uCloudDramatic;
varying vec2 vUv;
varying vec3 vWorldPosition;
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec3 v){
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy) );
vec3 x0 = v - i + dot(i, C.xxx) ;
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min( g.xyz, l.zxy );
vec3 i2 = max( g.xyz, l.zxy );
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute( permute( permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_ );
vec4 x = x_ *ns.x + ns.yyyy;
vec4 y = y_ *ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4( x.xy, y.xy );
vec4 b1 = vec4( x.zw, y.zw );
vec4 s0 = floor(b0)*2.0 + 1.0;
vec4 s1 = floor(b1)*2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
vec3 p0 = vec3(a0.xy,h.x);
vec3 p1 = vec3(a0.zw,h.y);
vec3 p2 = vec3(a1.xy,h.z);
vec3 p3 = vec3(a1.zw,h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.5 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
m = m * m;
return 105.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
dot(p2,x2), dot(p3,x3) ) );
}
float fbm(vec3 p) {
float value = 0.0;
float amplitude = 0.5;
for (int i = 0; i < 5; i++) {
value += amplitude * snoise(p);
p *= 2.0;
amplitude *= 0.5;
}
return value;
}
void main() {
vec3 pos = normalize(vWorldPosition);
vec3 motion = vec3(uTime * 0.02, 0.0, 0.0);
vec3 warp = vec3(
fbm(pos * 2.0 + vec3(uTime * 0.05)),
fbm(pos * 2.0 + vec3(uTime * 0.05 + 10.0)),
fbm(pos * 2.0 + vec3(uTime * 0.05 + 20.0))
);
float n = fbm(pos * 3.0 + motion + warp * 0.5);
float cloudVal = (n + 1.0) * 0.5;
float dramFactor = (1.0 - uCloudDramatic) * 0.2;
cloudVal = smoothstep(0.45 - dramFactor, 0.65 + dramFactor, cloudVal);
vec3 baseColor = uColor;
float intensity = uCloudDramatic * uCloudDramatic;
vec3 cloudHighlight = baseColor + vec3(0.2, 0.2, 0.25) * intensity;
cloudHighlight = min(cloudHighlight, vec3(1.0));
vec3 finalColor = mix(baseColor, cloudHighlight, cloudVal);
gl_FragColor = vec4(finalColor, 1.0);
gl_FragColor = vec4(uColor, 1.0);
}
</script>
<script type="module" src="app.js"></script>
<script>
<script type="module">
import { ArxivService } from './arxiv_service.js';
// --- Configuration & Data ---
const arxiv = new ArxivService();
const MILESTONES = [
{ year: 1958, name: "Perceptron", authors: "Frank Rosenblatt", z: -20 },
{ year: 1969, name: "XOR Problem", authors: "Minsky & Papert", z: -50 },
@@ -558,7 +499,7 @@
const container = document.getElementById('canvas-container');
const scene = new THREE.Scene();
// Updated background color (Brighter)
const initialBgColor = new THREE.Color(0xf0f4f8);
const initialBgColor = new THREE.Color(0xeeeee6);
// FOG Adjustment
scene.fog = new THREE.Fog(initialBgColor, 20, 90);
@@ -580,7 +521,7 @@
uniforms: {
uTime: { value: 0 },
// Updated background color (Brighter)
uColor: { value: new THREE.Color(0xf0f4f8) },
uColor: { value: new THREE.Color(0xeeeee6) },
uCloudDramatic: { value: 1.0 } // Default to dramatic
},
side: THREE.BackSide,
@@ -590,10 +531,10 @@
scene.add(skyMesh);
// --- Terrain ---
const width = 60;
const width = 50;
const depth = 80;
const widthSegments = 80;
const depthSegments = 100;
const depthSegments = 120; // Increased density slightly to help with flickering
const geometry = new THREE.PlaneGeometry(width, depth, widthSegments, depthSegments);
geometry.rotateX(-Math.PI / 2);
@@ -632,9 +573,10 @@
historyLineGeo.setAttribute('color', new THREE.BufferAttribute(new Float32Array(linePoints * 3), 3));
futureLineGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(linePoints * 3), 3));
const historyMat = new THREE.LineBasicMaterial({ vertexColors: true, linewidth: 4 });
// Start invisible (opacity 0) for scroll-triggered fade-in
const historyMat = new THREE.LineBasicMaterial({ vertexColors: true, linewidth: 4, transparent: true, opacity: 0 });
const futureMat = new THREE.LineDashedMaterial({
color: 0xff4500, dashSize: 0.5, gapSize: 0.3, scale: 1, linewidth: 2, opacity: 0.6, transparent: true
color: 0xff4500, dashSize: 0.5, gapSize: 0.3, scale: 1, linewidth: 2, opacity: 0, transparent: true
});
const historyLine = new THREE.Line(historyLineGeo, historyMat);
@@ -644,12 +586,12 @@
const glowGeo = new THREE.BufferGeometry();
glowGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(linePoints * 3), 3));
const glowMat = new THREE.PointsMaterial({
color: 0xff6600, size: 0.15, transparent: true, opacity: 0.8
color: 0xff6600, size: 0.15, transparent: true, opacity: 0
});
const glowPoints = new THREE.Points(glowGeo, glowMat);
scene.add(glowPoints);
scene.add(historyLine);
scene.add(futureLine);
// scene.add(historyLine);
// scene.add(futureLine);
// --- Markers ---
const markersContainer = document.getElementById('markers-container');
@@ -690,12 +632,12 @@
// Sky Cycle Colors (Updated for brighter start)
const skyColors = [
{ pos: 0, color: new THREE.Color(0xf0f4f8), dramatic: 1.0 }, // Morning (Bright, dramatic)
{ pos: 0, color: new THREE.Color(0xeeeee6), dramatic: 1.0 }, // Morning (Bright, dramatic)
{ pos: 150, color: new THREE.Color(0x8a929a), dramatic: 0.8 }, // Noon
{ pos: 300, color: new THREE.Color(0x5a626a), dramatic: 0.3 }, // Dusk
{ pos: 450, color: new THREE.Color(0x2a323a), dramatic: 0.1 }, // Night (Calm)
{ pos: 550, color: new THREE.Color(0x5a626a), dramatic: 0.3 }, // Dawn
{ pos: 700, color: new THREE.Color(0xf0f4f8), dramatic: 1.0 } // Day (Bright, dramatic)
{ pos: 700, color: new THREE.Color(0xeeeee6), dramatic: 1.0 } // Day (Bright, dramatic)
];
function getSkyState(y) {
@@ -730,9 +672,62 @@
return finalColor;
}
// Initialize ArXiv
let lastZ = MILESTONES[MILESTONES.length - 1].z;
const Z_INTERVAL = 20; // Distance between papers
let nextPaperZ = lastZ - Z_INTERVAL;
let spawningPapers = true;
function updateScene() {
flyOverY += (targetFlyOverY - flyOverY) * 0.08;
// --- ArXiv Paper Spawning Logic ---
// As we scroll down (flyOverY increases), negative Z milestones come closer to 0.
// When we get close to the "end" of the current list, spawn more.
// Check the last milestone's world Z position.
// wZ = lastZ + flyOverY. If wZ > -300 (it's getting closer), spawn next.
const lastMilestoneWorldZ = lastZ + flyOverY;
if (spawningPapers && lastMilestoneWorldZ > -500) {
// We are within range, try to get a paper
// We can't await in animation loop easily, so we poll
const paper = arxiv.papers.shift(); // Synchronous grab from buffer
if (paper) {
// Create marker data
const newM = {
year: paper.year,
name: paper.name,
authors: paper.authors,
z: nextPaperZ
};
// Add to data structures
MILESTONES.push(newM);
// Create DOM Element
const div = document.createElement('div');
div.className = 'marker';
div.innerHTML = `
<div class="marker-content" style="border-color: #00ccff;">
<div class="marker-year" style="color: #00ccff;">LATEST RESEARCH</div>
<div class="marker-label" style="font-size: 0.9rem; white-space: normal; max-width: 250px;">${newM.name}</div>
<div class="marker-author">${newM.authors}</div>
</div>
`;
markersContainer.appendChild(div);
markerElements.push({ data: newM, element: div });
// Update indices
lastZ = nextPaperZ;
nextPaperZ -= Z_INTERVAL;
} else {
// Trigger fetch if empty (service handles dedup/busy state)
arxiv.getNextPaper();
}
}
const skyState = getSkyState(flyOverY);
// Sync Fog, Sky Mesh
@@ -744,6 +739,23 @@
// Sync solid terrain color so the ground darkens with the sky cycle
terrainSolid.material.color.copy(skyState.color);
const Y_OFFSET = -12.0;
// Palette for patches (Vibrant, Brighter Variety)
if (!window.randomPalette) {
const hueBase = Math.random();
window.randomPalette = [];
for(let i=0; i<8; i++) {
const color = new THREE.Color();
const h = (hueBase + (Math.random() * 0.5 - 0.25) + i*0.1) % 1.0;
const s = 0.8 + Math.random() * 0.2;
const l = 0.5 + Math.random() * 0.3;
color.setHSL(h, s, l);
window.randomPalette.push(color);
}
}
const palette = window.randomPalette;
for (let i = 0; i < count; i++) {
const ix = i * 3;
@@ -751,12 +763,80 @@
const x = originalPositions[ix];
const z = originalPositions[iz];
const vZ = z - flyOverY;
// --- Geometry ---
const noiseVal = Noise.perlin2(x * noiseScale, vZ * noiseScale);
let height = noiseVal * heightScale;
if (height < -1) height = -1;
const jaggedNoise = Noise.perlin2(x * noiseScale * 4.5, vZ * noiseScale * 4.5);
// Mountain Layer: Compound noise for variety
// Base low-freq shape
const mNoise1 = Noise.perlin2(x * 0.03, vZ * 0.03);
// Secondary shape to break symmetry
const mNoise2 = Noise.perlin2(x * 0.1 + 123.4, vZ * 0.1 + 567.8);
// Combine and threshold
let mountainShape = Math.max(0, mNoise1 + mNoise2 * 0.4 - 0.45);
// Variable sharpness/height per peak (using low freq noise to map regions)
const regionVar = Noise.perlin2(x * 0.01, vZ * 0.01);
const peakHeight = 35.0 + regionVar * 15.0; // 20 to 50 height
const peakSharpness = 2.0 + (mNoise2 * 0.5); // Vary exponent
// Reduced exponent from 2.5 to 1.7 to avoid "icicle" needles
mountainShape = Math.pow(mountainShape, 1.7) * peakHeight;
// Add jaggedness SPECIFICALLY to mountains (rocky texture)
if (mountainShape > 0.5) {
mountainShape += jaggedNoise * 1.5;
}
// Jagged peaks only on higher ground
const jaggedFactor = Math.max(0, noiseVal - 0.2);
const finalHeightVal = noiseVal + (jaggedFactor * jaggedNoise * 0.8) + mountainShape;
let height = (finalHeightVal * heightScale) + Y_OFFSET;
// Clamp bottom
if (height < -1 + Y_OFFSET) height = -1 + Y_OFFSET;
positionAttribute.setY(i, height);
const c = getColorForHeight(noiseVal);
c.multiplyScalar(0.9);
// --- Color ---
// 1. Patch Base Color (Randomized patches)
const patchNoise = Noise.perlin2(x * 0.15 + 100, vZ * 0.15 + 100);
const normPatch = (patchNoise + 1) * 0.5; // 0..1
// Interpolate palette
const scaledPatch = normPatch * (palette.length - 1);
const idx0 = Math.floor(scaledPatch);
const idx1 = Math.min(idx0 + 1, palette.length - 1);
const tColor = scaledPatch - idx0;
const c = new THREE.Color().lerpColors(palette[idx0], palette[idx1], tColor);
// 2. Linear Chunks / Streaks (Visibility Noise)
// "Shorter linear patches": Higher Frequency in Z (0.2)
const visNoise = Noise.perlin2(x * 0.5 + 200, vZ * 0.2 + 200);
if (visNoise < -0.15) {
// Faint linear chunks
c.lerp(skyState.color, 0.85);
} else {
// Bright areas
c.multiplyScalar(0.95);
}
// 3. Snow Caps (Light on top)
// Check relative height of mountain part
if (mountainShape > 6.0) {
// Blend to white based on height
const snowFactor = Math.min(1, (mountainShape - 6.0) / 4.0);
c.lerp(new THREE.Color(0xffffff), snowFactor);
}
// 4. Black/Dark Peaks (Contour outlines) - only for lower rocky parts or edges
else if (finalHeightVal > 0.45 && mountainShape < 1.0) {
c.setHex(0x111111);
}
colorAttribute.setXYZ(i, c.r, c.g, c.b);
}
positionAttribute.needsUpdate = true;
@@ -769,16 +849,39 @@
const histCol = historyLine.geometry.attributes.color;
const futPos = futureLine.geometry.attributes.position;
const glowPos = glowPoints.geometry.attributes.position;
const presentZ = MILESTONES[MILESTONES.length - 1].z;
// Update presentZ dynamically as we add papers?
// Actually, keep presentZ at the "Current" date (2024ish) or let it move?
// Let's keep the timeline line strictly for the "History" part (up to DeepSeek).
// Future papers appear beyond the timeline line.
const presentZ = -480;
for (let i = 0; i < linePoints; i++) {
const worldZ = lineStartZ - (i * step);
const virtualZ = worldZ - flyOverY;
const pX = getPathX(virtualZ);
const noiseVal = Noise.perlin2(pX * noiseScale, virtualZ * noiseScale);
const pY = Math.max(noiseVal * heightScale, -1) + 0.5;
glowPos.setXYZ(i, pX, pY, worldZ);
// Adjust line height matches terrain logic (simplified)
const mNoise1 = Noise.perlin2(pX * 0.03, virtualZ * 0.03);
const mNoise2 = Noise.perlin2(pX * 0.1 + 123.4, virtualZ * 0.1 + 567.8);
let mountainShape = Math.max(0, mNoise1 + mNoise2 * 0.4 - 0.45);
const regionVar = Noise.perlin2(pX * 0.01, virtualZ * 0.01);
const peakHeight = 35.0 + regionVar * 15.0;
// Match reduced exponent
mountainShape = Math.pow(mountainShape, 1.7) * peakHeight;
if (mountainShape > 0.5) mountainShape += (Noise.perlin2(pX * noiseScale * 3.0, virtualZ * noiseScale * 3.0) * 1.5);
const jaggedNoise = Noise.perlin2(pX * noiseScale * 3.0, virtualZ * noiseScale * 3.0);
const jaggedFactor = Math.max(0, noiseVal - 0.2);
const roughness = Noise.perlin2(pX * 0.4, virtualZ * 0.4) * 0.15;
const finalHeightVal = noiseVal + (jaggedFactor * jaggedNoise * 0.8) + roughness + mountainShape;
const pY = Math.max(finalHeightVal * heightScale, -1) + 0.5 + Y_OFFSET;
glowPos.setXYZ(i, pX, pY, worldZ);
// Hide lines for now per user request, but logic remains
/*
if (virtualZ > presentZ) {
histPos.setXYZ(i, pX, pY, worldZ);
futPos.setXYZ(i, 0, 0, 99999);
@@ -791,10 +894,11 @@
histPos.setXYZ(i, 0, 0, 99999);
futPos.setXYZ(i, pX, pY, worldZ);
}
*/
}
histPos.needsUpdate = true;
histCol.needsUpdate = true;
futPos.needsUpdate = true;
// histPos.needsUpdate = true;
// histCol.needsUpdate = true;
// futPos.needsUpdate = true;
glowPos.needsUpdate = true;
futureLine.computeLineDistances();
@@ -808,7 +912,24 @@
if (wZ > -60 && wZ < 20) {
const wX = getPathX(vZ);
const noiseVal = Noise.perlin2(wX * noiseScale, vZ * noiseScale);
const wY = Math.max(noiseVal * heightScale, -1) + 2.5;
// Adjust marker height logic
const mNoise1 = Noise.perlin2(wX * 0.03, vZ * 0.03);
const mNoise2 = Noise.perlin2(wX * 0.1 + 123.4, vZ * 0.1 + 567.8);
let mountainShape = Math.max(0, mNoise1 + mNoise2 * 0.4 - 0.45);
const regionVar = Noise.perlin2(wX * 0.01, vZ * 0.01);
const peakHeight = 35.0 + regionVar * 15.0;
// Match reduced exponent
mountainShape = Math.pow(mountainShape, 1.7) * peakHeight;
if (mountainShape > 0.5) mountainShape += (Noise.perlin2(wX * noiseScale * 3.0, vZ * noiseScale * 3.0) * 1.5);
const jaggedNoise = Noise.perlin2(wX * noiseScale * 3.0, vZ * noiseScale * 3.0);
const jaggedFactor = Math.max(0, noiseVal - 0.2);
const roughness = Noise.perlin2(wX * 0.4, vZ * 0.4) * 0.15;
const finalHeightVal = noiseVal + (jaggedFactor * jaggedNoise * 0.8) + roughness + mountainShape;
const wY = Math.max(finalHeightVal * heightScale, -1) + 2.5 + Y_OFFSET;
tempV.set(wX, wY, wZ);
tempV.project(camera);
const x = (tempV.x * .5 + .5) * window.innerWidth;
@@ -821,6 +942,16 @@
let opacity = 1;
if (wZ > 12) opacity = 0;
else if (wZ < -40) opacity = Math.max(0, 1 - (Math.abs(wZ) - 40) / 10);
// --- NEW: Global Fade In for Markers (Post-Header) ---
// Header fades out by scrollY=600. targetFlyOverY = scrollY * 0.25 = 150.
// smooth step from 150 to 200
let globalFade = (flyOverY - 150) / 50;
if (globalFade < 0) globalFade = 0;
if (globalFade > 1) globalFade = 1;
opacity *= globalFade;
el.style.opacity = opacity;
el.style.left = `${x}px`;
el.style.top = `${y}px`;
@@ -846,6 +977,7 @@
const scrollY = window.scrollY;
targetFlyOverY = scrollY * 0.25;
// Hero Fade Out
const startFade = 100;
const endFade = 600;
let opacity = 1;
@@ -859,41 +991,58 @@
heroTitle.style.transform = `translate(-50%, calc(-50% - ${lift}px))`;
}
// Timeline Fade In (Start after Hero starts fading, fully visible after Hero is gone)
const tlStart = 400;
const tlEnd = 900;
let tlOpacity = 0;
if (scrollY > tlStart) {
tlOpacity = (scrollY - tlStart) / (tlEnd - tlStart);
if (tlOpacity > 1) tlOpacity = 1;
}
// Update materials
historyMat.opacity = tlOpacity;
futureMat.opacity = 0.6 * tlOpacity;
glowMat.opacity = 0.8 * tlOpacity;
// Show card logic
const processCard = document.getElementById('process-card');
const submissionCard = document.getElementById('submission-card');
const newsCard = document.getElementById('news-card');
// Process: Show 50px - 700px
if (scrollY > 50 && scrollY < 700) {
processCard.classList.add('visible');
if (processCard) {
if (scrollY > 50 && scrollY < 700) {
processCard.classList.add('visible');
// Highlight Logic
const start = 50;
const end = 700;
const range = end - start;
const progress = (scrollY - start) / range;
// Highlight Logic
const start = 50;
const end = 700;
const range = end - start;
const progress = (scrollY - start) / range;
const items = processCard.querySelectorAll('.feed-item');
const activeIndex = Math.floor(progress * items.length);
const items = processCard.querySelectorAll('.feed-item');
const activeIndex = Math.floor(progress * items.length);
items.forEach((item, index) => {
if (index === activeIndex) {
item.classList.add('active-step');
} else {
item.classList.remove('active-step');
}
});
items.forEach((item, index) => {
if (index === activeIndex) {
item.classList.add('active-step');
} else {
item.classList.remove('active-step');
}
});
} else {
processCard.classList.remove('visible');
} else {
processCard.classList.remove('visible');
}
}
// News: Show 700px - 1500px (Immediate after Process, Shortened Duration)
if (scrollY >= 700 && scrollY < 1500) {
newsCard.classList.add('visible');
} else {
newsCard.classList.remove('visible');
if (newsCard) {
if (scrollY >= 700 && scrollY < 1500) {
newsCard.classList.add('visible');
} else {
newsCard.classList.remove('visible');
}
}
// Submissions: Show 1500px - 3500px (Immediate after News)