Universal Horizon Sync
CHRONOS STATION
High-precision temporal tracking for global synchronization. Retro aesthetics meeting cutting-edge real-time data.
0
0
0
0
:
0
0
0
0
:
0
0
0
0
—
·
—
·
—
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
gsap.registerPlugin(ScrollTrigger);
const STORAGE_KEY = 'tp_world_clocks';
const DEFAULT_CLOCKS = [
{ id: '1', name: 'LONDON', tz: 'Europe/London' },
{ id: '2', name: 'NEW YORK', tz: 'America/New_York' },
{ id: '3', name: 'TOKYO', tz: 'Asia/Tokyo' },
{ id: '4', name: 'KOLKATA', tz: 'Asia/Kolkata' }
];
const TZ_ALIASES = {
// Asia
'Asia/Kolkata': 'India Kolkata Delhi Mumbai Chennai IST',
'Asia/Calcutta': 'India Kolkata Delhi Mumbai Chennai IST',
'Asia/Tokyo': 'Japan JST Tokyo Osaka Kyoto',
'Asia/Shanghai': 'China Shanghai Beijing CST',
'Asia/Hong_Kong': 'Hong Kong HK',
'Asia/Singapore': 'Singapore SGT',
'Asia/Seoul': 'Korea South Korea Seoul KST',
'Asia/Dubai': 'UAE United Arab Emirates Dubai GST Abu Dhabi',
'Asia/Jakarta': 'Indonesia Jakarta WIB',
'Asia/Bangkok': 'Thailand Bangkok ICT',
'Asia/Manila': 'Philippines Manila PHT',
'Asia/Ho_Chi_Minh': 'Vietnam Ho Chi Minh City Saigon',
'Asia/Kuala_Lumpur': 'Malaysia Kuala Lumpur MYT',
'Asia/Taipei': 'Taiwan Taipei CST',
'Asia/Jerusalem': 'Israel Jerusalem IST',
'Asia/Riyadh': 'Saudi Arabia Riyadh AST',
'Asia/Tehran': 'Iran Tehran IRST',
'Asia/Kabul': 'Afghanistan Kabul AFT',
'Asia/Tashkent': 'Uzbekistan Tashkent UZT',
// Europe
'Europe/London': 'UK United Kingdom London Great Britain BST GMT GB',
'Europe/Paris': 'France Paris CET CEST',
'Europe/Berlin': 'Germany Berlin CET CEST Munich Frankfurt Hamburg',
'Europe/Rome': 'Italy Rome Milan CET CEST',
'Europe/Madrid': 'Spain Madrid Barcelona CET CEST',
'Europe/Amsterdam': 'Netherlands Amsterdam CET CEST',
'Europe/Brussels': 'Belgium Brussels CET CEST',
'Europe/Zurich': 'Switzerland Zurich CET CEST',
'Europe/Vienna': 'Austria Vienna CET CEST',
'Europe/Stockholm': 'Sweden Stockholm CET CEST',
'Europe/Oslo': 'Norway Oslo CET CEST',
'Europe/Copenhagen': 'Denmark Copenhagen CET CEST',
'Europe/Helsinki': 'Finland Helsinki EET EEST',
'Europe/Athens': 'Greece Athens EET EEST',
'Europe/Istanbul': 'Turkey Istanbul TRT',
'Europe/Moscow': 'Russia Moscow MSK Saint Petersburg',
'Europe/Kiev': 'Ukraine Kyiv Kiev EET EEST',
'Europe/Warsaw': 'Poland Warsaw CET CEST',
'Europe/Prague': 'Czechia Prague CET CEST',
'Europe/Lisbon': 'Portugal Lisbon WET WEST',
'Europe/Dublin': 'Ireland Dublin GMT IST',
// Americas
'America/New_York': 'USA United States NYC NY East Coast EST EDT Washington',
'America/Chicago': 'USA United States Chicago Central Time CST CDT',
'America/Denver': 'USA United States Denver Mountain Time MST MDT',
'America/Los_Angeles': 'USA United States LA California CA West Coast PST PDT',
'America/Anchorage': 'USA Alaska AK AKT AKST',
'America/Honolulu': 'USA Hawaii HI HST',
'America/Toronto': 'Canada Toronto Ontario EST EDT',
'America/Vancouver': 'Canada Vancouver BC PST PDT',
'America/Mexico_City': 'Mexico Mexico City CST CDT',
'America/Sao_Paulo': 'Brazil Sao Paulo BRT Sao Paulo Rio Janeiro',
'America/Argentina/Buenos_Aires': 'Argentina Buenos Aires ART',
'America/Santiago': 'Chile Santiago CLT CLST',
'America/Bogota': 'Colombia Bogota COT',
'America/Lima': 'Peru Lima PET',
'America/Caracas': 'Venezuela Caracas VET',
// Oceania
'Australia/Sydney': 'Australia Sydney Melbourne Canberra AEDT AEST Victoria NSW',
'Australia/Perth': 'Australia Perth AWST Western Australia',
'Australia/Adelaide': 'Australia Adelaide ACDT ACST South Australia',
'Pacific/Auckland': 'New Zealand Auckland NZDT NZST',
'Pacific/Fiji': 'Fiji FJT FJST',
// Africa
'Africa/Johannesburg': 'South Africa Johannesburg Cape Town SAST',
'Africa/Cairo': 'Egypt Cairo EET EEST',
'Africa/Nairobi': 'Kenya Nairobi East Africa EAT',
'Africa/Lagos': 'Nigeria Lagos WAT',
'Africa/Casablanca': 'Morocco Casablanca WET WEST',
'Africa/Addis_Ababa': 'Ethiopia Addis Ababa EAT'
};
const DISPLAY_NAMES = {
'Asia/Kolkata': 'KOLKATA',
'Asia/Calcutta': 'KOLKATA',
'Asia/Saigon': 'HO CHI MINH CITY',
'Asia/Katmandu': 'KATHMANDU',
'America/New_York': 'NEW YORK',
'Europe/London': 'LONDON'
};
const ALL_TIMEZONES = Intl.supportedValuesOf('timeZone');
// Ensure common timezones are present (some browsers/environments omit aliases)
['Asia/Kolkata', 'Asia/Calcutta', 'Asia/Shanghai', 'America/New_York', 'Europe/London'].forEach(tz => {
if (!ALL_TIMEZONES.includes(tz)) {
try {
new Intl.DateTimeFormat('en-US', { timeZone: tz });
ALL_TIMEZONES.push(tz);
} catch (e) {}
}
});
// Deduplicate Kolkata/Calcutta and other common aliases to avoid double entries in search
if (ALL_TIMEZONES.includes('Asia/Kolkata') && ALL_TIMEZONES.includes('Asia/Calcutta')) {
const idx = ALL_TIMEZONES.indexOf('Asia/Calcutta');
if (idx > -1) ALL_TIMEZONES.splice(idx, 1);
}
let activeClocks = JSON.parse(localStorage.getItem(STORAGE_KEY)) || DEFAULT_CLOCKS;
let heroTz = 'UTC';
let selectedClockId = null;
// Pre-calculate short codes for searchability
const TZ_SHORT_CODES = {};
ALL_TIMEZONES.forEach(tz => {
try {
const parts = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'short' }).formatToParts(new Date());
const short = parts.find(p => p.type === 'timeZoneName')?.value;
if (short) TZ_SHORT_CODES[tz] = short;
} catch (e) {}
});
const clockGrid = document.getElementById('clockGrid');
const tzSearch = document.getElementById('tzSearch');
const searchResults = document.getElementById('searchResults');
const bgGlow = document.getElementById('bg-glow');
// Mouse Glow
document.addEventListener('mousemove', (e) => {
gsap.to(bgGlow, { x: e.clientX, y: e.clientY, duration: 2, ease: 'power2.out' });
});
// Three.js Scene Setup (Big Ben Background)
const canvasBen = document.querySelector('#big-ben-canvas');
const renderer = new THREE.WebGLRenderer({ canvas: canvasBen, alpha: true, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a0a, 0.005); // High-end mist for realism
const cameraBen = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
cameraBen.position.set(0, 35, 75);
cameraBen.lookAt(0, 40, 0); // Explicitly set initial focus
// Glowing River Thames (Winding through terrain)
const riverPoints = [];
for (let i = 0; i < 20; i++) {
const x = -200 + i * 20;
const z = Math.sin(i * 0.5) * 40 - 50;
riverPoints.push(new THREE.Vector3(x, -2, z));
}
const riverCurve = new THREE.CatmullRomCurve3(riverPoints);
const riverGeo = new THREE.TubeGeometry(riverCurve, 64, 22, 2, false);
const riverMat = new THREE.MeshPhongMaterial({
color: 0x0088ff,
emissive: 0x0044ff,
emissiveIntensity: 1.8,
transparent: true,
opacity: 0.6,
shininess: 100
});
const river = new THREE.Mesh(riverGeo, riverMat);
river.scale.y = 0.08;
scene.add(river);
const riverGlowMat = new THREE.MeshBasicMaterial({ color: 0x0044ff, transparent: true, opacity: 0.15 });
const riverGlow = new THREE.Mesh(new THREE.TubeGeometry(riverCurve, 64, 30, 2, false), riverGlowMat);
riverGlow.scale.y = 0.04;
scene.add(riverGlow);
// Refined Starry Sky System
const starCount = 4000;
const starGeo = new THREE.BufferGeometry();
const starPos = new Float32Array(starCount * 3);
const starColors = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const i3 = i * 3;
const r = 350 + Math.random() * 250;
const theta = Math.acos(Math.random() * 2 - 1);
const phi = Math.random() * Math.PI * 2;
starPos[i3] = r * Math.sin(theta) * Math.cos(phi);
starPos[i3+1] = Math.abs(r * Math.sin(theta) * Math.sin(phi));
starPos[i3+2] = r * Math.cos(theta);
const color = new THREE.Color();
const rand = Math.random();
if (rand > 0.94) color.setHex(0xa0c0ff); // Vibrant Blue
else if (rand > 0.88) color.setHex(0xffd0a0); // Stellar Orange
else if (rand > 0.82) color.setHex(0xd0a0ff); // Cosmic Purple
else color.setHex(0xffffff); // Pure White
starColors[i3] = color.r;
starColors[i3+1] = color.g;
starColors[i3+2] = color.b;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
starGeo.setAttribute('color', new THREE.BufferAttribute(starColors, 3));
const starMat = new THREE.PointsMaterial({
size: 4.2,
vertexColors: true,
transparent: true,
opacity: 0.8,
sizeAttenuation: true,
fog: false
});
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
// Subtle Sky Rotation for depth
function animateStars() {
stars.rotation.y += 0.0002;
stars.rotation.x += 0.0001;
// Twinkle effect
starMat.opacity = 0.6 + Math.random() * 0.4;
}
// Premium Procedural 3D Moon
const moonCanvas = document.createElement('canvas');
moonCanvas.width = 1024; moonCanvas.height = 1024;
const mCtx = moonCanvas.getContext('2d');
mCtx.fillStyle = '#f0f0f5'; // Bright Moon Base
mCtx.fillRect(0, 0, 1024, 1024);
// Draw Lunar Maria & Craters
for(let i=0; i<100; i++) {
const x = Math.random() * 1024;
const y = Math.random() * 1024;
const r = 10 + Math.random() * 60;
const grad = mCtx.createRadialGradient(x, y, 0, x, y, r);
grad.addColorStop(0, 'rgba(0,0,0,0.25)');
grad.addColorStop(0.8, 'rgba(0,0,0,0.1)');
grad.addColorStop(1, 'rgba(255,255,255,0.05)');
mCtx.fillStyle = grad;
mCtx.beginPath();
mCtx.arc(x, y, r, 0, Math.PI * 2);
mCtx.fill();
}
// Fine Surface Grain
for(let i=0; i<10000; i++) {
mCtx.fillStyle = `rgba(0,0,0,${Math.random() * 0.1})`;
mCtx.fillRect(Math.random()*1024, Math.random()*1024, 1, 1);
}
const moonTexture = new THREE.CanvasTexture(moonCanvas);
const moonGeo = new THREE.SphereGeometry(18, 64, 64);
const moonMat = new THREE.MeshPhongMaterial({
map: moonTexture,
emissive: 0xffffff,
emissiveMap: moonTexture,
emissiveIntensity: 0.7,
shininess: 0,
fog: false
});
const moon = new THREE.Mesh(moonGeo, moonMat);
moon.position.set(-200, 180, -350);
scene.add(moon);
// Atmospheric Moon Halo (The Glow)
const moonHaloGeo = new THREE.SphereGeometry(22, 32, 32);
const moonHaloMat = new THREE.MeshBasicMaterial({
color: 0xdae8ff,
transparent: true,
opacity: 0.15,
side: THREE.BackSide,
blending: THREE.AdditiveBlending
});
const moonHalo = new THREE.Mesh(moonHaloGeo, moonHaloMat);
moon.add(moonHalo);
// Soft Moon Glow Light
const moonLightSrc = new THREE.PointLight(0xd0e0ff, 2, 800);
moonLightSrc.position.copy(moon.position);
scene.add(moonLightSrc);
const benGroup = new THREE.Group();
scene.add(benGroup);
// Realistic Ground Base for Stability
const groundGeo = new THREE.CircleGeometry(200, 32);
const groundMat = new THREE.MeshPhongMaterial({ color: 0x0a0a0a, shininess: 5, flatShading: true });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -2;
scene.add(ground);
// Procedural Nature System (High Reliability)
function createPineTree(x, z) {
const treeGrp = new THREE.Group();
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.4, 2), new THREE.MeshPhongMaterial({ color: 0x1a0f0a }));
trunk.position.y = 1;
treeGrp.add(trunk);
for(let i=0; i<3; i++) {
const foliage = new THREE.Mesh(new THREE.ConeGeometry(2 - i*0.5, 3, 8), new THREE.MeshPhongMaterial({ color: 0x0a220a, flatShading: true }));
foliage.position.y = 2 + i*1.5;
treeGrp.add(foliage);
}
treeGrp.position.set(x, -2, z);
treeGrp.scale.setScalar(0.5 + Math.random()*1.5);
scene.add(treeGrp);
}
for (let i = 0; i < 150; i++) {
const rad = 25 + Math.random() * 100;
const ang = Math.random() * Math.PI * 2;
createPineTree(Math.cos(ang) * rad, Math.sin(ang) * rad);
}
// Materials
const bodyMat = new THREE.MeshPhongMaterial({ color: 0x4d4532, flatShading: true, shininess: 30 });
// Canvas-based Clock Face Texture for Realism
const faceCanvas = document.createElement('canvas');
faceCanvas.width = 512;
faceCanvas.height = 512;
const ctx = faceCanvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, 512, 512);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#000000';
ctx.font = 'bold 50px "Times New Roman", serif';
const romans = ['XII', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI'];
for(let i=0; i<12; i++) {
const angle = (i * 30) * Math.PI / 180;
const x = 256 + Math.sin(angle) * 200;
const y = 256 - Math.cos(angle) * 200;
ctx.fillText(romans[i], x, y);
}
const faceTexture = new THREE.CanvasTexture(faceCanvas);
const faceMat = new THREE.MeshPhongMaterial({
map: faceTexture,
emissive: 0xffffff,
emissiveMap: faceTexture,
emissiveIntensity: 0.6,
shininess: 100
});
const handMat = new THREE.MeshPhongMaterial({ color: 0x000000, shininess: 50 });
// High-Detail Elizabeth Tower (Big Ben)
const towerBody = new THREE.Mesh(new THREE.BoxGeometry(6, 45, 6), bodyMat);
towerBody.position.y = 17.5;
benGroup.add(towerBody);
// Corner Detailing (Pillars)
const pillarGeo = new THREE.BoxGeometry(0.8, 45.2, 0.8);
const pillarPositions = [[3,17.5,3], [3,17.5,-3], [-3,17.5,3], [-3,17.5,-3]];
pillarPositions.forEach(p => {
const pillar = new THREE.Mesh(pillarGeo, bodyMat);
pillar.position.set(p[0], p[1], p[2]);
benGroup.add(pillar);
});
// Clock Platform (Wider)
const clockSection = new THREE.Mesh(new THREE.BoxGeometry(7.5, 8, 7.5), bodyMat);
clockSection.position.y = 40;
benGroup.add(clockSection);
// Clock Face Pinnacles (Decorative corner spikes)
const pinGeo = new THREE.ConeGeometry(0.6, 3, 4);
const pinPos = [[3.5,41.5,3.5], [3.5,41.5,-3.5], [-3.5,41.5,3.5], [-3.5,41.5,-3.5]];
pinPos.forEach(p => {
const pin = new THREE.Mesh(pinGeo, bodyMat);
pin.position.set(p[0], p[1]+2.5, p[2]);
pin.rotation.y = Math.PI / 4;
benGroup.add(pin);
});
// Lantern Room (Above clock)
const lantern = new THREE.Mesh(new THREE.BoxGeometry(4, 5, 4), bodyMat);
lantern.position.y = 46.5;
benGroup.add(lantern);
// Faces & Hands
const faceHands = [];
for(let i=0; i<4; i++) {
const faceGroup = new THREE.Group();
faceGroup.rotation.y = (Math.PI / 2) * i;
const d = 3.76;
if (i===0) faceGroup.position.z = d;
if (i===1) faceGroup.position.x = d;
if (i===2) faceGroup.position.z = -d;
if (i===3) faceGroup.position.x = -d;
faceGroup.position.y = 40;
benGroup.add(faceGroup);
const face = new THREE.Mesh(new THREE.CircleGeometry(3, 32), faceMat);
faceGroup.add(face);
const hHand = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.05), handMat);
hHand.position.y = 0.75;
const hPivot = new THREE.Group();
hPivot.add(hHand);
hPivot.position.z = 0.1;
faceGroup.add(hPivot);
const mHand = new THREE.Mesh(new THREE.BoxGeometry(0.12, 2.2, 0.05), handMat);
mHand.position.y = 1.1;
const mPivot = new THREE.Group();
mPivot.add(mHand);
mPivot.position.z = 0.15;
faceGroup.add(mPivot);
faceHands.push({ h: hPivot, m: mPivot });
}
// Main Spire
const roofMesh = new THREE.Mesh(new THREE.ConeGeometry(5.5, 12, 4), bodyMat);
roofMesh.position.y = 55;
roofMesh.rotation.y = Math.PI / 4;
benGroup.add(roofMesh);
// High-End Natural Lighting System
const hemiLight = new THREE.HemisphereLight(0x444488, 0x111122, 1.2); // Sky-Ground soft fill
scene.add(hemiLight);
const moonLight = new THREE.DirectionalLight(0xd0d0ff, 0.8); // Subtle, cool moonlit key
moonLight.position.set(40, 100, 50);
scene.add(moonLight);
const rimLight = new THREE.SpotLight(0x223344, 5, 200, 0.5, 1, 1);
rimLight.position.set(-30, 20, -50);
rimLight.target = towerBody;
scene.add(rimLight);
// Dynamic Searchlights System
const searchlights = [];
const beamCount = 4;
const beamColors = [0xffffff, 0xd0e0ff, 0xffffff, 0xe0f0ff];
for (let i = 0; i < beamCount; i++) {
const ang = (i / beamCount) * Math.PI * 2;
const x = Math.cos(ang) * 15;
const z = Math.sin(ang) * 15;
const spot = new THREE.SpotLight(beamColors[i], 12, 180, 0.1, 0.6, 1.5);
spot.position.set(x, -1, z);
scene.add(spot);
const targetObj = new THREE.Object3D();
scene.add(targetObj);
spot.target = targetObj;
// Grounded Bulb Glow
const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.8, 16, 16), new THREE.MeshBasicMaterial({ color: beamColors[i] }));
bulb.position.set(x, -1.8, z);
scene.add(bulb);
// Volumetric Beam Mesh (Subtler)
const beamGeo = new THREE.ConeGeometry(2, 120, 32, 1, true);
const beamMat = new THREE.MeshBasicMaterial({
color: beamColors[i],
transparent: true,
opacity: 0.08,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false
});
const beam = new THREE.Mesh(beamGeo, beamMat);
scene.add(beam);
searchlights.push({ spot, target: targetObj, beam });
}
function animateBeam(s) {
const tx = (Math.random() - 0.5) * 180;
const ty = 30 + Math.random() * 90;
const tz = (Math.random() - 0.5) * 180;
gsap.to(s.target.position, {
x: tx, y: ty, z: tz,
duration: 3 + Math.random() * 4, // Faster, more cinematic sweep
ease: "sine.inOut",
onUpdate: () => {
const pos = new THREE.Vector3(s.spot.position.x, -1.8, s.spot.position.z);
const target = new THREE.Vector3(s.target.position.x, s.target.position.y, s.target.position.z);
const mid = new THREE.Vector3().addVectors(pos, target).multiplyScalar(0.5);
s.beam.position.copy(mid);
s.beam.lookAt(target);
s.beam.rotateX(Math.PI / 2);
const dist = pos.distanceTo(target);
s.beam.scale.set(1, dist / 120, 1);
},
onComplete: () => animateBeam(s)
});
}
searchlights.forEach(s => animateBeam(s));
const camAngles = [
{ pos: [60, 45, 60], look: [0, 35, 0], up: [0.3, 1, 0] }, // Wide Landscape Hero
{ pos: [12, 42, 12], look: [0, 40, 0], up: [0.2, 1, 0] }, // Balanced Face Macro
{ pos: [-80, 20, 40], look: [0, 30, 0], up: [-0.1, 1, 0] }, // River-Side Panorama
{ pos: [150, 40, 150], look: [-100, 80, -150], up: [0, 1, 0] }, // Lunar Silhouette View
{ pos: [45, 65, 45], look: [0, 30, 0], up: [0.5, 1, 0.5] }, // Side-Top Terrain View
{ pos: [-45, 55, -45], look: [5, 40, 5], up: [-0.3, 1, 0] }, // Angled Forest Observer
{ pos: [0, 65, 120], look: [0, 20, 0], up: [0, 1, 0] }, // The Grand Journey
{ pos: [40, 20, -120], look: [-150, 140, -300], up: [0, 1, 0] }, // Celestial Framing
{ pos: [-15, 42, -8], look: [0, 40, 0], up: [-0.2, 1, 0] }, // Western Face Silhouette
{ pos: [55, 50, -55], look: [10, 20, 10], up: [0.4, 1, -0.4] }, // Aerial River Sweep
{ pos: [70, 12, 70], look: [0, 45, 0], up: [-0.1, 1, -0.1] }, // Distant Majesty
{ pos: [5, 43, 8], look: [0, 40, 0], up: [0, 1, 0.2] }, // Close Architectural Glow
{ pos: [100, 60, 0], look: [0, 30, 0], up: [0.6, 1, 0] }, // God-View Panorama
{ pos: [0, 5, -130], look: [0, 50, 0], up: [0, 1, 0] } // Long-Distance Cinematic
];
// Persistent Camera State to prevent Jitter
const currentLook = { x: 0, y: 40, z: 0 };
const currentUp = { x: 0, y: 1, z: 0 };
function updateBigBenHands() {
const now = new Date();
const heroDate = new Date(now.toLocaleString('en-US', { timeZone: heroTz }));
const h = heroDate.getHours() % 12;
const m = heroDate.getMinutes();
const s = heroDate.getSeconds();
faceHands.forEach(f => {
f.h.rotation.z = -((h + m/60) * (Math.PI * 2 / 12));
f.m.rotation.z = -((m + s/60) * (Math.PI * 2 / 60));
});
}
function changeBenAngle() {
const a = camAngles[Math.floor(Math.random() * camAngles.length)];
// Kill any active transitions
gsap.killTweensOf(cameraBen.position);
gsap.killTweensOf(currentLook);
gsap.killTweensOf(currentUp);
const duration = 1.8 + Math.random() * 1.2;
const ease = "expo.inOut";
gsap.to(cameraBen.position, { x: a.pos[0], y: a.pos[1], z: a.pos[2], duration, ease });
gsap.to(currentLook, {
x: a.look[0], y: a.look[1], z: a.look[2],
duration, ease,
onUpdate: () => {
cameraBen.lookAt(currentLook.x, currentLook.y, currentLook.z);
}
});
gsap.to(currentUp, {
x: a.up[0], y: a.up[1], z: a.up[2],
duration, ease,
onUpdate: () => {
cameraBen.up.set(currentUp.x, currentUp.y, currentUp.z);
}
});
}
function animateBen() {
requestAnimationFrame(animateBen);
updateBigBenHands();
animateStars();
benGroup.rotation.y += 0.003;
renderer.render(scene, cameraBen);
}
animateBen();
window.addEventListener('resize', () => {
cameraBen.aspect = window.innerWidth / window.innerHeight;
cameraBen.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Flip Clock Logic
function updateFlipDigit(id, val) {
const unit = document.getElementById(id);
if(!unit) return;
const topStatic = unit.querySelector('.flip-card-top');
const bottomStatic = unit.querySelector('.flip-card-bottom');
const currentVal = topStatic.innerText;
if (currentVal === val) return;
const leaf = document.createElement('div');
leaf.className = 'flip-leaf';
leaf.innerHTML = `<div class="flip-leaf-front">${currentVal}</div><div class="flip-leaf-back">${val}</div>`;
unit.appendChild(leaf);
topStatic.innerText = val;
gsap.to(leaf, {
rotateX: -180, duration: 0.3, force3D: true, z: 0.1, ease: "power2.inOut",
onComplete: () => { bottomStatic.innerText = val; leaf.remove(); }
});
if (id === 's2' && parseInt(val) % 6 === 0) changeBenAngle();
}
function updateClocks() {
const now = new Date();
const heroDate = new Date(now.toLocaleString('en-US', { timeZone: heroTz }));
// Update Flip Hero
const h = heroDate.getHours().toString().padStart(2, '0');
const m = heroDate.getMinutes().toString().padStart(2, '0');
const s = heroDate.getSeconds().toString().padStart(2, '0');
updateFlipDigit('h1', h[0]); updateFlipDigit('h2', h[1]);
updateFlipDigit('m1', m[0]); updateFlipDigit('m2', m[1]);
updateFlipDigit('s1', s[0]); updateFlipDigit('s2', s[1]);
document.getElementById('localDay').innerText = heroDate.toLocaleDateString('en-US', { weekday: 'long' }).toUpperCase();
document.getElementById('localDate').innerText = heroDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).toUpperCase();
document.getElementById('localTz').innerText = heroTz === 'UTC' ? 'UNIVERSAL COORDINATED TIME' : heroTz.split('/').pop().replace(/_/g, ' ').toUpperCase();
// Update Cards
activeClocks.forEach(clock => {
const card = document.getElementById(`clock-${clock.id}`);
if (!card) return;
const clockDate = new Date(now.toLocaleString('en-US', { timeZone: clock.tz }));
card.querySelector('.card-time').innerText = clockDate.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
card.querySelector('.card-date').innerText = clockDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }).toUpperCase();
const hC = clockDate.getHours(); const mC = clockDate.getMinutes(); const sC = clockDate.getSeconds();
card.querySelector('.hand-hour').style.transform = `translateX(-50%) rotate(${(hC % 12) * 30 + mC * 0.5}deg)`;
card.querySelector('.hand-minute').style.transform = `translateX(-50%) rotate(${mC * 6}deg)`;
card.querySelector('.hand-second').style.transform = `translateX(-50%) rotate(${sC * 6}deg)`;
});
}
function renderGrid() {
clockGrid.innerHTML = '';
activeClocks.forEach((clock, index) => {
const card = document.createElement('div');
card.className = `clock-card ${selectedClockId === clock.id ? 'active' : ''}`;
card.id = `clock-${clock.id}`;
card.onclick = (e) => {
if (e.target.closest('.remove-btn')) return;
selectClock(clock.id, clock.tz);
};
card.innerHTML = `
<button class="remove-btn" onclick="removeClock('${clock.id}')" title="Remove Horizon">
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<div class="card-city">${clock.name}<span class="card-short-tz">${TZ_SHORT_CODES[clock.tz] || ''}</span></div>
<div class="card-timezone">${clock.tz.replace(/_/g, ' ')}</div>
<div class="card-time">00:00:00</div>
<div class="card-date">—</div>
<div><span class="card-offset">—</span></div>
<div class="analog-clock-face">
<div class="analog-hand hand-hour"></div>
<div class="analog-hand hand-minute"></div>
<div class="analog-hand hand-second"></div>
<div class="analog-hand hand-center"></div>
</div>
`;
clockGrid.appendChild(card);
// Manual Offset calc for render time
const now = new Date();
const localOffset = -now.getTimezoneOffset();
const tzOffset = (new Date(now.toLocaleString('en-US', { timeZone: clock.tz })) - new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }))) / 60000;
const diff = tzOffset - localOffset;
const dH = Math.floor(Math.abs(diff) / 60); const dM = Math.abs(diff) % 60;
card.querySelector('.card-offset').innerText = diff === 0 ? 'SYNCHRONIZED' : `${diff > 0 ? '+' : '-'}${dH}${dM ? `:${dM}` : ''} HRS FROM LOCAL`;
});
gsap.from(".clock-card", {
y: 20, rotateX: -10, duration: 0.6, stagger: 0.05, ease: "power4.out",
scrollTrigger: { trigger: ".clock-grid", start: "top 95%" }
});
updateClocks();
}
function addClock(tz) {
const name = DISPLAY_NAMES[tz] || tz.split('/').pop().replace(/_/g, ' ').toUpperCase();
const newClock = { id: Date.now().toString(), name, tz };
activeClocks.push(newClock);
saveClocks();
renderGrid();
tzSearch.value = '';
searchResults.style.display = 'none';
}
function selectClock(id, tz) {
if (selectedClockId === id) {
selectedClockId = null;
heroTz = 'UTC';
} else {
selectedClockId = id;
heroTz = tz;
}
renderGrid();
}
function removeClock(id) {
const el = document.getElementById(`clock-${id}`);
gsap.to(el, {
scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.in",
onComplete: () => {
el.remove();
activeClocks = activeClocks.filter(c => c.id !== id);
saveClocks();
}
});
}
function saveClocks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(activeClocks));
}
tzSearch.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
if (query.length < 2) { searchResults.style.display = 'none'; return; }
const matches = ALL_TIMEZONES.filter(tz => {
const alias = TZ_ALIASES[tz] || '';
const short = TZ_SHORT_CODES[tz] || '';
return tz.toLowerCase().includes(query) ||
tz.split('/').pop().toLowerCase().includes(query) ||
short.toLowerCase().includes(query) ||
alias.toLowerCase().includes(query);
}).slice(0, 15);
if (matches.length > 0) {
searchResults.innerHTML = matches.map(tz => {
const display = DISPLAY_NAMES[tz] || tz.split('/').pop().replace(/_/g, ' ').toUpperCase();
const short = TZ_SHORT_CODES[tz] || '';
return `
<div class="search-item" onclick="addClock('${tz}')">
<div style="display:flex; align-items:center; width:100%; justify-content:between;">
<span style="flex:1;">${display}</span>
<span style="font-size:10px; color:var(--accent); background:var(--accent-dim); padding:2px 8px; border-radius:4px; margin-left:12px; font-family:var(--mono); opacity: 0.9;">${short}</span>
</div>
<span style="opacity: 0.3; margin-left: 20px; font-size: 11px;">${tz.split('/')[0]}</span>
</div>
`;
}).join('');
searchResults.style.display = 'block';
gsap.from(".search-item", { x: -10, duration: 0.3 });
} else { searchResults.style.display = 'none'; }
});
document.addEventListener('click', (e) => { if (!tzSearch.contains(e.target) && !searchResults.contains(e.target)) searchResults.style.display = 'none'; });
const fsBtn = document.getElementById('fullscreen-btn');
fsBtn.addEventListener('click', () => {
document.body.classList.toggle('cinematic-mode');
const isCinematic = document.body.classList.contains('cinematic-mode');
fsBtn.querySelector('span').innerText = isCinematic ? 'EXIT CINEMATIC' : 'CHANGE VIEW';
if (isCinematic) {
if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(e => {});
} else {
if (document.fullscreenElement) document.exitFullscreen();
}
});
// Handle ESC key or other ways out
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
document.body.classList.remove('cinematic-mode');
fsBtn.querySelector('span').innerText = 'CHANGE VIEW';
}
});
renderGrid();
setInterval(updateClocks, 1000);