CHRONOS // TEMPORAL CARTOGRAPHY INTERFACE
@import url(‘
https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=VT323&display=swap’);
:root{
–bg:#040604;
–bg-2:#081008;
–phosphor:#3CFF7A;
–phosphor-dim:#12622e;
–grid:#0e2b18;
–amber:#FFB627;
–red:#FF3B3B;
–cyan:#39e6c8;
–panel:#050b06;
–text-dim:#5f9a72;
}
*{box-sizing:border-box; margin:0; padding:0;}
html,body{
width:100%; height:100%;
background:var(–bg);
overflow:hidden;
font-family:’Share Tech Mono’, monospace;
color:var(–phosphor);
cursor:crosshair;
user-select:none;
}
/* ———- CRT / SCANLINE / VIGNETTE LAYERS ———- */
#crt-overlay{
position:fixed; inset:0; pointer-events:none; z-index:900;
background:
repeating-linear-gradient(
to bottom,
rgba(0,0,0,0) 0px,
rgba(0,0,0,0) 1px,
rgba(0,0,0,0.18) 2px,
rgba(0,0,0,0.18) 3px
);
mix-blend-mode:multiply;
animation:flicker 6s infinite;
}
#vignette{
position:fixed; inset:0; pointer-events:none; z-index:899;
background:radial-gradient(ellipse at center, rgba(0,0,0,0) 55%, rgba(0,0,0,0.75) 100%);
}
@keyframes flicker{
0%,100%{opacity:1;}
92%{opacity:1;}
93%{opacity:0.82;}
94%{opacity:1;}
96%{opacity:0.9;}
97%{opacity:1;}
}
.glitch-active{ animation: glitchjump 0.15s steps(2) 2; }
@keyframes glitchjump{
0%{ transform:translate(0,0); filter:none; }
25%{ transform:translate(-2px,1px); filter:hue-rotate(20deg); }
50%{ transform:translate(2px,-1px); filter:hue-rotate(-15deg); }
75%{ transform:translate(-1px,0); }
100%{ transform:translate(0,0); filter:none; }
}
/* ———- BOOT SEQUENCE ———- */
#boot{
position:fixed; inset:0; z-index:1000; background:#000;
color:var(–phosphor); font-size:16px; padding:40px;
line-height:1.6; white-space:pre-wrap;
}
#boot .prompt-line{opacity:0;}
#boot-cursor{display:inline-block; width:9px; height:16px; background:var(–phosphor); animation:blink 1s steps(1) infinite; vertical-align:middle;}
@keyframes blink{ 50%{opacity:0;} }
/* ———- HUD ———- */
#hud-top{
position:fixed; top:0; left:0; right:0; z-index:50;
display:flex; justify-content:space-between; align-items:center;
padding:10px 22px;
border-bottom:1px solid var(–phosphor-dim);
background:linear-gradient(to bottom, rgba(4,10,4,0.95), rgba(4,10,4,0.6));
font-size:12px; letter-spacing:2px;
}
#hud-top .brand{ color:var(–phosphor); font-weight:bold; text-shadow:0 0 6px var(–phosphor); }
#hud-top .sub{ color:var(–text-dim); }
#hud-clock{ color:var(–amber); text-shadow:0 0 6px var(–amber); }
#hud-readout{
position:fixed; top:44px; left:22px; z-index:50;
font-family:’VT323′, monospace; font-size:34px; color:var(–phosphor);
text-shadow:0 0 10px rgba(60,255,122,0.6);
letter-spacing:2px;
}
#hud-readout .range-label{
font-family:’Share Tech Mono’, monospace; font-size:11px; color:var(–text-dim);
letter-spacing:2px; display:block; margin-top:-6px;
}
/* ———- VIEWPORT ———- */
#viewport{
position:absolute; top:0; left:0; width:100%; height:100%;
overflow:hidden;
}
#stage{
position:absolute; top:0; left:0; width:100%; height:100%;
transform-origin: 0 0;
}
svg#timeline-svg{ position:absolute; top:0; left:0; overflow:visible; }
.grid-line{ stroke:var(–grid); stroke-width:1; }
.main-axis{ stroke:var(–phosphor-dim); stroke-width:2; }
.now-line{ stroke:var(–amber); stroke-width:1.5; stroke-dasharray:4 4; opacity:0.8;}
.tick-label{
font-family:’Share Tech Mono’, monospace; fill:var(–text-dim);
font-size:11px; letter-spacing:1px;
}
.tick-label.major{ fill:var(–phosphor); font-size:12px; }
.branch-line{ stroke-width:1.5; opacity:0.75; }
.connector-line{ stroke-width:1; stroke-dasharray:2 3; opacity:0.6; }
.node-circle{ cursor:pointer; transition:r 0.15s ease; }
.node-circle:hover{ }
.node-label{
font-family:’Share Tech Mono’, monospace; font-size:11px;
pointer-events:none; letter-spacing:0.5px;
}
.branch-tag{
font-family:’Share Tech Mono’, monospace; font-size:10px;
letter-spacing:2px; opacity:0.85;
}
/* ———- BOTTOM DOCK ———- */
#dock{
position:fixed; bottom:0; left:0; right:0; z-index:60;
border-top:1px solid var(–phosphor-dim);
background:linear-gradient(to top, rgba(4,10,4,0.97), rgba(4,10,4,0.75));
padding:12px 18px;
display:flex; align-items:center; gap:18px; flex-wrap:wrap;
}
.btn{
background:transparent; border:1px solid var(–phosphor-dim);
color:var(–phosphor); font-family:’Share Tech Mono’, monospace;
font-size:11px; letter-spacing:2px; padding:8px 14px; cursor:pointer;
text-transform:uppercase; transition:all .12s ease;
}
.btn:hover{ background:var(–phosphor); color:#000; box-shadow:0 0 12px var(–phosphor); }
.btn.amber{ border-color:#7a5a10; color:var(–amber); }
.btn.amber:hover{ background:var(–amber); color:#000; box-shadow:0 0 12px var(–amber); }
.btn.red{ border-color:#7a1a1a; color:var(–red); }
.btn.red:hover{ background:var(–red); color:#000; box-shadow:0 0 12px var(–red); }
#zoom-group{ display:flex; align-items:center; gap:6px; }
#zoom-readout{ font-size:11px; color:var(–text-dim); width:90px; text-align:center; }
#scrubber-wrap{
flex:1; min-width:220px; height:26px; position:relative;
border:1px solid var(–phosphor-dim); background:var(–panel);
}
#scrubber-fill{
position:absolute; top:0; bottom:0; left:0;
background:rgba(60,255,122,0.18); border-right:1px solid var(–phosphor);
cursor:grab;
}
/* ———- DETAIL PANEL ———- */
#detail-panel{
position:fixed; top:0; right:-380px; width:360px; height:100%; z-index:80;
background:rgba(3,8,4,0.97); border-left:1px solid var(–phosphor-dim);
padding:70px 24px 24px 24px; transition:right 0.25s ease;
overflow-y:auto;
}
#detail-panel.open{ right:0; }
#detail-panel h2{
font-family:’VT323′, monospace; font-size:28px; color:var(–phosphor);
text-shadow:0 0 8px rgba(60,255,122,0.5); letter-spacing:1px; margin-bottom:4px;
}
#detail-panel .yr{ color:var(–amber); font-size:13px; letter-spacing:2px; margin-bottom:14px; display:block;}
#detail-panel .branch-name{ color:var(–cyan); font-size:11px; letter-spacing:2px; display:block; margin-bottom:18px;}
#detail-panel p{ font-size:13px; line-height:1.7; color:#cfe9d8; }
#detail-panel .close-btn{
position:absolute; top:22px; right:22px; cursor:pointer; color:var(–red);
font-size:18px; border:1px solid #7a1a1a; width:28px; height:28px;
text-align:center; line-height:26px;
}
#detail-panel .del-btn{ margin-top:24px; }
/* ———- ADD NODE MODAL ———- */
#modal-overlay{
position:fixed; inset:0; z-index:200; background:rgba(0,0,0,0.75);
display:none; align-items:center; justify-content:center;
}
#modal-overlay.open{ display:flex; }
#modal{
width:420px; max-width:92vw; background:var(–panel);
border:1px solid var(–phosphor); box-shadow:0 0 30px rgba(60,255,122,0.25);
padding:26px; cursor:default;
}
#modal h3{
font-family:’VT323′, monospace; font-size:24px; letter-spacing:2px;
color:var(–phosphor); margin-bottom:18px; text-shadow:0 0 6px rgba(60,255,122,0.5);
}
.field{ margin-bottom:14px; }
.field label{
display:block; font-size:10px; letter-spacing:2px; color:var(–text-dim); margin-bottom:5px;
}
.field input, .field textarea, .field select{
width:100%; background:#000; border:1px solid var(–phosphor-dim); color:var(–phosphor);
font-family:’Share Tech Mono’, monospace; font-size:13px; padding:8px 10px; outline:none;
}
.field input:focus, .field textarea:focus, .field select:focus{ border-color:var(–phosphor); box-shadow:0 0 8px rgba(60,255,122,0.3); }
.field textarea{ resize:vertical; min-height:60px; }
.color-swatches{ display:flex; gap:8px; margin-top:4px; }
.swatch{ width:24px; height:24px; border:2px solid transparent; cursor:pointer; }
.swatch.selected{ border-color:#fff; }
.modal-actions{ display:flex; justify-content:flex-end; gap:10px; margin-top:20px; }
/* ———- HELP / LEGEND ———- */
#legend{
position:fixed; top:44px; right:22px; z-index:50; font-size:10px;
color:var(–text-dim); text-align:right; letter-spacing:1px; line-height:1.8;
}
#legend b{ color:var(–phosphor); }
::-webkit-scrollbar{ width:6px; }
::-webkit-scrollbar-thumb{ background:var(–phosphor-dim); }
@media (max-width:720px){
#hud-readout{ font-size:24px; }
#detail-panel{ width:100%; right:-100%; }
#legend{ display:none; }
.btn{ font-size:9px; padding:6px 8px; }
}
CHRONOS
// TEMPORAL CARTOGRAPHY INTERFACE REV 3.3.0
SYSTEM NOMINAL
-6000
VIEWPORT ANCHOR // SCROLL+DRAG TO NAVIGATE
LMB DRAG — pan timeline
SCROLL — zoom in / out
CLICK NODE — open record
X
MAIN SEQUENCE
EVENT
0 AD
OVERVIEW
// NEW TEMPORAL RECORD
DESIGNATION (TITLE)
YEAR (use negative for BC, e.g. -332)
BRANCH / LANE (leave blank for MAIN SEQUENCE)
LOG ENTRY
(function(){
/* ======================= CONFIG ======================= */
const YEAR_MIN = -6000;
const YEAR_MAX = 3033;
const NOW_YEAR = 2026;
const COLORS = [‘#3CFF7A’, ‘#FFB627’, ‘#39e6c8’, ‘#FF3B3B’, ‘#c9c9ff’];
const STORAGE_KEY = ‘chronos_timeline_data_v1’;
const SVG_NS = ‘
http://www.w3.org/2000/svg’;
const LANE_GAP = 70; // vertical distance between lanes
const NODE_R = 6;
const AXIS_Y = 0; // main axis local y (stage coords), viewport centers it
/* ======================= SEED DATA ======================= */
const seedEvents = [
{id:’e1′, year:-6000, branch:’MAIN’, title:’Neolithic Settlements’, desc:’Early agricultural communities take root across the Fertile Crescent, marking humanity\’s shift from foraging to farming.’, color:’#3CFF7A’},
{id:’e2′, year:-3100, branch:’MAIN’, title:’Unification of Egypt’, desc:’Upper and Lower Egypt are unified under a single ruler, beginning the era of the Pharaohs.’, color:’#3CFF7A’},
{id:’e3′, year:-2560, branch:’MAIN’, title:’Great Pyramid of Giza’, desc:’Construction of the Great Pyramid concludes, the last surviving Ancient Wonder of the World.’, color:’#3CFF7A’},
{id:’e4′, year:-776, branch:’MAIN’, title:’First Olympic Games’, desc:’Athletic competition begins at Olympia, Greece, inaugurating a tradition that spans millennia.’, color:’#3CFF7A’},
{id:’e5′, year:-509, branch:’MAIN’, title:’Roman Republic Founded’, desc:’Rome overthrows its monarchy, establishing a republic that will grow into an empire.’, color:’#3CFF7A’},
{id:’e6′, year:-221, branch:’MAIN’, title:’Unification of China’, desc:’Qin Shi Huang unifies warring states, becoming the first emperor of a unified China.’, color:’#3CFF7A’},
{id:’e7′, year:0, branch:’MAIN’, title:’Common Era Begins’, desc:’The reference point of the modern calendar system.’, color:’#FFB627′},
{id:’e8′, year:476, branch:’MAIN’, title:’Fall of Western Rome’, desc:’The Western Roman Empire collapses, reshaping the political map of Europe.’, color:’#3CFF7A’},
{id:’e9′, year:1453, branch:’MAIN’, title:’Fall of Constantinople’, desc:’The Ottoman conquest ends the Byzantine Empire and closes the medieval era.’, color:’#3CFF7A’},
{id:’e10′, year:1969, branch:’MAIN’, title:’Apollo 11 Moon Landing’, desc:’Humanity sets foot on another world for the first time.’, color:’#39e6c8′},
{id:’e11′, year:NOW_YEAR, branch:’MAIN’, title:’Present Signal’, desc:’Current temporal anchor point of this interface.’, color:’#FFB627′},
{id:’b1′, year:2032, branch:’OFF-WORLD COLONIES’, title:’Weyland-Yutani Lunar Charter’, desc:’Fictional projection: a commercial charter establishes permanent lunar extraction operations.’, color:’#c9c9ff’},
{id:’b2′, year:2079, branch:’OFF-WORLD COLONIES’, title:’Deep-Space Commercial Towing Est.’, desc:’Fictional projection: long-haul freight vessels begin routine deep-space refinery towing routes.’, color:’#c9c9ff’},
{id:’b3′, year:2122, branch:’OFF-WORLD COLONIES’, title:’Nostromo-Class Vessels Commissioned’, desc:’Fictional projection: a new class of commercial towing vessel enters service.’, color:’#c9c9ff’},
{id:’c1′, year:1859, branch:’SCIENTIFIC REVOLUTIONS’, title:’Origin of Species Published’, desc:’Darwin\’s theory of evolution by natural selection reframes the life sciences.’, color:’#FF3B3B’},
{id:’c2′, year:1905, branch:’SCIENTIFIC REVOLUTIONS’, title:’Special Relativity’, desc:’Einstein publishes the theory that redefines space, time, and energy.’, color:’#FF3B3B’},
{id:’c3′, year:1953, branch:’SCIENTIFIC REVOLUTIONS’, title:’DNA Structure Described’, desc:’Watson and Crick describe the double-helix structure of DNA.’, color:’#FF3B3B’},
];
/* ======================= STATE ======================= */
let events = [];
let scale = 0; // px per year
let minScale = 0;
let maxScale = 0;
let camYear = 0; // year at left edge of viewport
let vw = 0, vh = 0;
let dragging = false, dragStartX = 0, dragStartCam = 0;
let laneOrder = []; // branch names in order of first appearance (excluding MAIN)
const svg = document.getElementById(‘timeline-svg’);
const stage = document.getElementById(‘stage’);
const viewport = document.getElementById(‘viewport’);
/* ======================= PERSISTENCE ======================= */
function loadData(){
try{
const raw = localStorage.getItem(STORAGE_KEY);
if(raw){ events = JSON.parse(raw); return; }
}catch(e){}
events = JSON.parse(JSON.stringify(seedEvents));
saveData();
}
function saveData(){
try{ localStorage.setItem(STORAGE_KEY, JSON.stringify(events)); }catch(e){}
}
/* ======================= SCALE HELPERS ======================= */
function computeLaneOrder(){
laneOrder = [];
events.forEach(ev=>{
if(ev.branch && ev.branch !== ‘MAIN’ && !laneOrder.includes(ev.branch)){
laneOrder.push(ev.branch);
}
});
}
function laneY(branch){
if(!branch || branch === ‘MAIN’) return AXIS_Y;
const idx = laneOrder.indexOf(branch);
const side = idx % 2 === 0 ? 1 : -1;
const tier = Math.floor(idx/2) + 1;
return AXIS_Y + side * tier * LANE_GAP;
}
function yearToX(year){ return (year – YEAR_MIN) * scale; }
function xToYear(x){ return x / scale + YEAR_MIN; }
function formatYear(y){
y = Math.round(y);
if(y = rawYears) return s; }
return 5000;
}
function render(){
clearSvg();
computeLaneOrder();
const totalWidth = (YEAR_MAX – YEAR_MIN) * scale;
const centerY = vh/2;
svg.setAttribute(‘width’, totalWidth);
svg.setAttribute(‘height’, vh);
stage.style.height = vh + ‘px’;
const g = svgEl(‘g’, {transform:`translate(0, ${centerY})`});
svg.appendChild(g);
/* grid + ticks */
const step = niceStep(scale);
const minorStep = step / 5;
let startTick = Math.floor(YEAR_MIN/minorStep)*minorStep;
for(let y = startTick; y <= YEAR_MAX; y += minorStep){
const x = yearToX(y);
const isMajor = Math.abs(Math.round(y/step)*step – y) {
const evs = events.filter(e=>e.branch===branch).sort((a,b)=>a.year-b.year);
if(!evs.length) return;
const y = laneY(branch);
const x1 = yearToX(evs[0].year);
const x2 = yearToX(evs[evs.length-1].year);
const col = evs[0].color || ‘#39e6c8’;
g.appendChild(svgEl(‘line’, {
x1: Math.min(x1,x2), y1:y, x2:Math.max(x1,x2)+40, y2:y,
class:’branch-line’, stroke:col
}));
const tag = svgEl(‘text’, {x:Math.min(x1,x2), y:y-10, class:’branch-tag’, fill:col});
tag.textContent = ‘// ‘ + branch;
g.appendChild(tag);
// connector from main axis at first branch event’s year
g.appendChild(svgEl(‘line’, {
x1:x1, y1:AXIS_Y, x2:x1, y2:y, class:’connector-line’, stroke:col
}));
});
/* nodes */
events.forEach(ev=>{
const x = yearToX(ev.year);
const y = laneY(ev.branch);
const col = ev.color || ‘#3CFF7A’;
if(ev.branch && ev.branch !== ‘MAIN’){
g.appendChild(svgEl(‘line’, {x1:x, y1:AXIS_Y, x2:x, y2:y, class:’connector-line’, stroke:col}));
}
const node = svgEl(‘circle’, {
cx:x, cy:y, r:NODE_R, fill:’#000′, stroke:col, ‘stroke-width’:2,
class:’node-circle’, ‘data-id’:ev.id
});
node.style.filter = `drop-shadow(0 0 4px ${col})`;
node.addEventListener(‘click’, (e)=>{ e.stopPropagation(); openDetail(ev.id); });
node.addEventListener(‘mouseenter’, ()=> node.setAttribute(‘r’, NODE_R+3));
node.addEventListener(‘mouseleave’, ()=> node.setAttribute(‘r’, NODE_R));
g.appendChild(node);
const lbl = svgEl(‘text’, {
x:x+10, y:y – (ev.branch===’MAIN’||!ev.branch ? -22 : (laneY(ev.branch)>0? 18:-14)),
class:’node-label’, fill:col
});
lbl.textContent = ev.title;
g.appendChild(lbl);
});
updateReadouts();
updateScrubber();
}
/* ======================= CAMERA / ZOOM / PAN ======================= */
function applyCamera(){
stage.style.transform = `translateX(${ -camYear * scale }px)`;
}
function clampCamera(){
const viewYears = vw / scale;
const maxCam = YEAR_MAX – viewYears;
const minCam = YEAR_MIN;
if(camYear maxCam) camYear = Math.max(minCam, maxCam);
}
function setScale(newScale, anchorClientX){
newScale = Math.max(minScale, Math.min(maxScale, newScale));
const anchorYear = anchorClientX!==undefined ? xToYear((anchorClientX) / 1 + camYear*scale) : camYear + (vw/2)/scale;
// recompute anchor in world using OLD scale
const worldXBefore = anchorClientX!==undefined ? (anchorClientX + camYear*scale) : (vw/2 + camYear*scale);
const yearAtAnchor = worldXBefore/scale;
scale = newScale;
const worldXAfterTarget = yearAtAnchor*scale;
camYear = (worldXAfterTarget – (anchorClientX!==undefined?anchorClientX:vw/2)) / scale;
clampCamera();
applyCamera();
render();
updateZoomReadout();
}
function updateZoomReadout(){
const ratio = scale/minScale;
const el = document.getElementById(‘zoom-readout’);
if(ratio < 1.5) el.textContent = 'OVERVIEW';
else if(ratio < 8) el.textContent = 'REGIONAL x' + ratio.toFixed(1);
else if(ratio e.id===id);
if(!ev) return;
selectedId = id;
document.getElementById(‘detail-branch’).textContent = (ev.branch && ev.branch!==’MAIN’) ? ev.branch : ‘MAIN SEQUENCE’;
document.getElementById(‘detail-title’).textContent = ev.title;
document.getElementById(‘detail-year’).textContent = formatYear(ev.year);
document.getElementById(‘detail-desc’).textContent = ev.desc || ‘[NO LOG DATA]’;
document.getElementById(‘detail-panel’).classList.add(‘open’);
}
function closeDetail(){
document.getElementById(‘detail-panel’).classList.remove(‘open’);
selectedId = null;
}
document.getElementById(‘detail-close’).addEventListener(‘click’, closeDetail);
document.getElementById(‘detail-delete’).addEventListener(‘click’, ()=>{
if(!selectedId) return;
events = events.filter(e=>e.id!==selectedId);
saveData();
closeDetail();
render();
});
/* ======================= MODAL ======================= */
const modalOverlay = document.getElementById(‘modal-overlay’);
let chosenColor = COLORS[0];
function buildSwatches(){
const wrap = document.getElementById(‘color-swatches’);
wrap.innerHTML = ”;
COLORS.forEach((c,i)=>{
const sw = document.createElement(‘div’);
sw.className = ‘swatch’ + (i===0?’ selected’:”);
sw.style.background = c;
sw.addEventListener(‘click’, ()=>{
chosenColor = c;
[…wrap.children].forEach(ch=>ch.classList.remove(‘selected’));
sw.classList.add(‘selected’);
});
wrap.appendChild(sw);
});
}
buildSwatches();
document.getElementById(‘btn-add’).addEventListener(‘click’, ()=>{
document.getElementById(‘f-title’).value=”;
document.getElementById(‘f-year’).value=”;
document.getElementById(‘f-branch’).value=”;
document.getElementById(‘f-desc’).value=”;
chosenColor = COLORS[0];
buildSwatches();
modalOverlay.classList.add(‘open’);
});
document.getElementById(‘modal-cancel’).addEventListener(‘click’, ()=>modalOverlay.classList.remove(‘open’));
modalOverlay.addEventListener(‘click’, (e)=>{ if(e.target===modalOverlay) modalOverlay.classList.remove(‘open’); });
document.getElementById(‘modal-save’).addEventListener(‘click’, ()=>{
const title = document.getElementById(‘f-title’).value.trim();
const yearRaw = document.getElementById(‘f-year’).value.trim();
const branch = document.getElementById(‘f-branch’).value.trim().toUpperCase() || ‘MAIN’;
const desc = document.getElementById(‘f-desc’).value.trim();
if(!title || yearRaw===”){
flashInvalid();
return;
}
const year = Math.max(YEAR_MIN, Math.min(YEAR_MAX, parseInt(yearRaw,10)));
const id = ‘u’ + Date.now();
events.push({id, year, branch, title, desc, color:chosenColor});
saveData();
modalOverlay.classList.remove(‘open’);
render();
focusOnYear(year);
});
function flashInvalid(){
const modal = document.getElementById(‘modal’);
modal.classList.add(‘glitch-active’);
setTimeout(()=>modal.classList.remove(‘glitch-active’), 300);
}
/* ======================= RESET ======================= */
document.getElementById(‘btn-reset’).addEventListener(‘click’, ()=>{
if(!confirm(‘RESET ALL TEMPORAL RECORDS TO FACTORY SEED DATA?’)) return;
events = JSON.parse(JSON.stringify(seedEvents));
saveData();
render();
});
/* ======================= NAV CONTROLS ======================= */
function focusOnYear(year){
camYear = year – (vw/2)/scale;
clampCamera();
applyCamera();
updateReadouts();
updateScrubber();
}
document.getElementById(‘btn-jump-origin’).addEventListener(‘click’, ()=>focusOnYear(YEAR_MIN+ (vw/2)/scale));
document.getElementById(‘btn-jump-zero’).addEventListener(‘click’, ()=>focusOnYear(0));
document.getElementById(‘btn-jump-now’).addEventListener(‘click’, ()=>focusOnYear(NOW_YEAR));
document.getElementById(‘btn-jump-end’).addEventListener(‘click’, ()=>focusOnYear(YEAR_MAX – (vw/2)/scale));
document.getElementById(‘btn-zoom-in’).addEventListener(‘click’, ()=>setScale(scale*1.6, vw/2));
document.getElementById(‘btn-zoom-out’).addEventListener(‘click’, ()=>setScale(scale/1.6, vw/2));
/* wheel zoom */
viewport.addEventListener(‘wheel’, (e)=>{
e.preventDefault();
const factor = e.deltaY {
if(e.target.closest(‘#detail-panel’)) return;
dragging = true;
dragStartX = e.clientX;
dragStartCam = camYear;
viewport.style.cursor = ‘grabbing’;
});
window.addEventListener(‘mousemove’, (e)=>{
if(!dragging) return;
const dx = e.clientX – dragStartX;
camYear = dragStartCam – dx/scale;
clampCamera();
applyCamera();
updateReadouts();
updateScrubber();
});
window.addEventListener(‘mouseup’, ()=>{ dragging = false; viewport.style.cursor=’crosshair’; });
/* touch support (basic pan) */
let touchStartX=0, touchStartCam=0;
viewport.addEventListener(‘touchstart’, (e)=>{
if(e.touches.length!==1) return;
touchStartX = e.touches[0].clientX;
touchStartCam = camYear;
}, {passive:true});
viewport.addEventListener(‘touchmove’, (e)=>{
if(e.touches.length!==1) return;
const dx = e.touches[0].clientX – touchStartX;
camYear = touchStartCam – dx/scale;
clampCamera();
applyCamera();
updateReadouts();
updateScrubber();
}, {passive:true});
/* scrubber drag */
const scrubberWrap = document.getElementById(‘scrubber-wrap’);
let scrubDragging=false;
scrubberWrap.addEventListener(‘mousedown’, (e)=>{
scrubDragging = true;
moveScrubTo(e);
});
window.addEventListener(‘mousemove’, (e)=>{ if(scrubDragging) moveScrubTo(e); });
window.addEventListener(‘mouseup’, ()=> scrubDragging=false);
function moveScrubTo(e){
const rect = scrubberWrap.getBoundingClientRect();
const pct = Math.max(0, Math.min(1, (e.clientX-rect.left)/rect.width));
const total = YEAR_MAX-YEAR_MIN;
camYear = YEAR_MIN + pct*total – (vw/2)/scale;
clampCamera();
applyCamera();
updateReadouts();
updateScrubber();
render();
}
/* ======================= RESIZE ======================= */
function handleResize(){
vw = viewport.clientWidth;
vh = viewport.clientHeight;
const totalYears = YEAR_MAX – YEAR_MIN;
minScale = vw / totalYears;
maxScale = minScale * 300;
if(!scale) scale = minScale;
clampCamera();
applyCamera();
render();
}
window.addEventListener(‘resize’, handleResize);
/* ======================= CLOCK ======================= */
function tickClock(){
const d = new Date();
document.getElementById(‘hud-clock’).textContent =
‘SYS TIME ‘ + d.toTimeString().slice(0,8) + ‘ // RECORDS: ‘ + events.length;
}
setInterval(tickClock, 1000);
/* ======================= BOOT SEQUENCE ======================= */
function boot(){
const bootEl = document.getElementById(‘boot’);
const textEl = document.getElementById(‘boot-text’);
const lines = [
‘CHRONOS TEMPORAL CARTOGRAPHY INTERFACE’,
‘REV 3.3.0 — COPYRIGHT UNKNOWN’,
”,
‘INITIALIZING CORE………..OK’,
‘LOADING TEMPORAL INDEX…….OK’,
‘CALIBRATING AXIS -6000 > 3033…….OK’,
‘MOUNTING BRANCH REGISTRY…..OK’,
”,
‘INTERFACE READY.’
];
let full = lines.join(‘\n’);
let i = 0;
textEl.style.opacity = 1;
const iv = setInterval(()=>{
textEl.textContent = full.slice(0, i);
i += 3;
if(i > full.length){
clearInterval(iv);
setTimeout(()=>{
bootEl.classList.add(‘glitch-active’);
setTimeout(()=>{ bootEl.style.display=’none’; init(); }, 180);
}, 400);
}
}, 12);
}
/* ======================= INIT ======================= */
function init(){
loadData();
handleResize();
focusOnYear(YEAR_MIN + (vw/2)/scale);
}
document.addEventListener(‘DOMContentLoaded’, boot);
})();