Manager
View Site
Name
Type
React (.jsx)
HTML (.html)
Icon
Description
Code Editor
Revision History (0)
import React, { useState, useEffect, useRef } from 'react'; // Three.js CDN const THREE_CDN = "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"; const DEFAULT_EARTH_MAP = "https://static.vecteezy.com/system/resources/thumbnails/053/312/125/small_2x/3d-relief-world-map-global-topography-earth-texture-geography-photo.jpeg"; // ----------------------------------------------------------------------------- // HELPER: Generate City Lights (Emissive Map) // We keep this to ensure the night side isn't pitch black but has civilization // ----------------------------------------------------------------------------- const generateCityLights = () => { const size = 1024; const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); // Black base ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, size, size); // Random Cities (Yellow/Gold dots) // Concentrated bands to simulate latitudinal population density roughly for (let i = 0; i < 4000; i++) { const x = Math.random() * size; const y = Math.random() * size; // Simple bias to avoid extreme poles for cities if (y < 100 || y > size - 100) continue; const r = Math.random(); ctx.fillStyle = `rgba(255, 220, 150, ${r})`; ctx.fillRect(x, y, r > 0.9 ? 3 : 1, r > 0.9 ? 3 : 1); } return canvas; }; // ----------------------------------------------------------------------------- // DIGIT TOPOLOGY // ----------------------------------------------------------------------------- const DIGITS = { 0: [[1,1,1],[1,0,1],[1,0,1],[1,0,1],[1,1,1]], 1: [[0,1,0],[0,1,0],[0,1,0],[0,1,0],[0,1,0]], 2: [[1,1,1],[0,0,1],[1,1,1],[1,0,0],[1,1,1]], 3: [[1,1,1],[0,0,1],[1,1,1],[0,0,1],[1,1,1]], 4: [[1,0,1],[1,0,1],[1,1,1],[0,0,1],[0,0,1]], 5: [[1,1,1],[1,0,0],[1,1,1],[0,0,1],[1,1,1]], 6: [[1,1,1],[1,0,0],[1,1,1],[1,0,1],[1,1,1]], 7: [[1,1,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1]], 8: [[1,1,1],[1,0,1],[1,1,1],[1,0,1],[1,1,1]], 9: [[1,1,1],[1,0,1],[1,1,1],[0,0,1],[1,1,1]], }; const COLON = [[0,0,0],[0,1,0],[0,0,0],[0,1,0],[0,0,0]]; const ModernClock = () => { const [libLoaded, setLibLoaded] = useState(false); const [modeIndex, setModeIndex] = useState(4); // Default to SOLAR DIAL const [appMode, setAppMode] = useState('CLOCK'); const [timerDuration, setTimerDuration] = useState(0); const [timeLeft, setTimeLeft] = useState(0); const [isTimerRunning, setIsTimerRunning] = useState(false); const [location, setLocation] = useState(null); // Texture State const [textureUrl, setTextureUrl] = useState(DEFAULT_EARTH_MAP); const MODES = [ 'PARTICLE_MIST', 'VOXEL_TUMBLE', 'GLITCH_MATRIX', 'NEON_RINGS', 'SOLAR_DIAL', 'ARC_REACTOR' ]; const clockDataRef = useRef({ h: 0, m: 0, s: 0, ms: 0, isTimer: false, themeColor: 0x44aadd }); // Load Persistence & Location useEffect(() => { // Load Map const savedMap = localStorage.getItem('clock_earth_map'); if (savedMap) setTextureUrl(savedMap); // Load Loc if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (pos) => { setLocation({ lat: pos.coords.latitude, lon: pos.coords.longitude }); }, (err) => console.log("Location error:", err), { enableHighAccuracy: false, timeout: 5000 } ); } }, []); const handleChangeMap = () => { const newUrl = prompt("Enter URL for Earth Texture (JPG/PNG):", textureUrl); if (newUrl) { setTextureUrl(newUrl); localStorage.setItem('clock_earth_map', newUrl); } }; const handleResetMap = () => { setTextureUrl(DEFAULT_EARTH_MAP); localStorage.removeItem('clock_earth_map'); }; useEffect(() => { if (window.THREE) { setLibLoaded(true); return; } const script = document.createElement('script'); script.src = THREE_CDN; script.async = true; script.onload = () => setLibLoaded(true); document.body.appendChild(script); }, []); useEffect(() => { const interval = setInterval(() => { if (appMode === 'CLOCK') { const now = new Date(); const rawH = now.getHours(); const h = rawH % 12 || 12; const m = now.getMinutes(); const s = now.getSeconds(); const ms = now.getMilliseconds(); clockDataRef.current = { h, m, s, ms, isTimer: false, themeColor: 0x00ddee }; } else { if (isTimerRunning && timeLeft > 0) { setTimeLeft(prev => Math.max(0, prev - 1)); } else if (isTimerRunning && timeLeft === 0) { setIsTimerRunning(false); } const h = Math.floor(timeLeft / 3600); const m = Math.floor((timeLeft % 3600) / 60); const s = timeLeft % 60; clockDataRef.current = { h, m, s, ms: 0, isTimer: true, themeColor: timeLeft === 0 && timerDuration > 0 ? 0xff0000 : 0xffaa00 }; } }, 1000); return () => clearInterval(interval); }, [appMode, isTimerRunning, timeLeft, timerDuration]); const addTime = (seconds) => { setTimeLeft(prev => prev + seconds); setTimerDuration(prev => prev + seconds); }; const resetTimer = () => { setIsTimerRunning(false); setTimeLeft(0); setTimerDuration(0); }; const toggleTimer = () => setIsTimerRunning(!isTimerRunning); if (!libLoaded) return <div className="bg-black text-white h-screen flex items-center justify-center font-mono">LOADING ENGINE...</div>; const now = new Date(); return ( <div className="relative w-full h-screen bg-black overflow-hidden font-sans select-none"> <ThreeClockScene visualMode={MODES[modeIndex]} dataRef={clockDataRef} appMode={appMode} userLocation={location} textureUrl={textureUrl} /> {/* ---------------- UI OVERLAYS ---------------- */} <div className="absolute top-6 right-6 z-20 cursor-pointer group" onClick={() => setModeIndex(prev => (prev + 1) % MODES.length)} > <div className="flex flex-col items-end"> <div className="text-[10px] text-white/40 mb-1 font-mono tracking-widest">VISUAL ENGINE</div> <div className="text-sm font-bold text-white group-hover:text-cyan-400 transition-colors"> {MODES[modeIndex].replace('_', ' ')} </div> </div> </div> {appMode === 'CLOCK' && ( <div className="absolute top-[80%] left-1/2 -translate-x-1/2 z-20 flex flex-col items-center pointer-events-none opacity-80 mix-blend-screen"> <div className="flex items-baseline gap-2"> <div className="text-3xl font-bold text-white tracking-tight"> {now.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} </div> <div className="text-xl text-cyan-400 font-mono font-bold"> {now.getHours() >= 12 ? 'PM' : 'AM'} </div> </div> <div className="text-[10px] text-white/50 mt-1 font-mono tracking-[0.3em]"> {now.toLocaleDateString(undefined, { weekday: 'long' }).toUpperCase()} </div> {location && modeIndex === 4 && ( <div className="text-[8px] text-cyan-500/50 mt-1 font-mono"> LOC: {location.lat.toFixed(2)}, {location.lon.toFixed(2)} </div> )} </div> )} {/* Map Control for Solar Dial - BOTTOM LEFT */} {modeIndex === 4 && ( <div className="absolute left-6 bottom-10 z-30 flex gap-2 pointer-events-auto"> <button onClick={handleChangeMap} className="text-[10px] text-cyan-400/80 hover:text-white hover:bg-cyan-900/50 font-mono bg-black/60 px-3 py-2 rounded border border-cyan-900 backdrop-blur-md transition-all">CHANGE MAP</button> {textureUrl !== DEFAULT_EARTH_MAP && ( <button onClick={handleResetMap} className="text-[10px] text-white/30 hover:text-red-400 font-mono bg-black/60 px-3 py-2 rounded backdrop-blur-md transition-all">RESET</button> )} </div> )} <div className="absolute bottom-10 left-0 right-0 z-20 flex flex-col items-center justify-center gap-6"> {appMode === 'TIMER' && ( <div className="flex gap-4 animate-in slide-in-from-bottom-4 duration-300"> <button onClick={() => addTime(60)} className="px-4 py-2 bg-white/5 hover:bg-white/20 border border-white/10 rounded-lg text-white text-xs font-mono backdrop-blur-md transition-all active:scale-95"> +1 MIN </button> <button onClick={() => addTime(300)} className="px-4 py-2 bg-white/5 hover:bg-white/20 border border-white/10 rounded-lg text-white text-xs font-mono backdrop-blur-md transition-all active:scale-95"> +5 MIN </button> <button onClick={toggleTimer} className={`px-6 py-2 rounded-lg text-white text-xs font-bold tracking-wider backdrop-blur-md transition-all active:scale-95 border ${isTimerRunning ? 'bg-orange-500/20 border-orange-500 text-orange-400' : 'bg-green-500/20 border-green-500 text-green-400'}`}> {isTimerRunning ? 'PAUSE' : 'START'} </button> <button onClick={resetTimer} className="px-4 py-2 bg-red-500/10 hover:bg-red-500/30 border border-red-500/30 rounded-lg text-red-400 text-xs font-mono backdrop-blur-md transition-all active:scale-95"> RESET </button> </div> )} <div className="flex p-1 bg-white/10 backdrop-blur-xl rounded-full border border-white/5"> <button onClick={() => setAppMode('CLOCK')} className={`px-6 py-2 rounded-full text-xs font-bold tracking-widest transition-all duration-300 ${appMode === 'CLOCK' ? 'bg-white text-black shadow-lg' : 'text-white/50 hover:text-white'}`} > CLOCK </button> <button onClick={() => setAppMode('TIMER')} className={`px-6 py-2 rounded-full text-xs font-bold tracking-widest transition-all duration-300 ${appMode === 'TIMER' ? 'bg-orange-500 text-white shadow-lg shadow-orange-900/20' : 'text-white/50 hover:text-white'}`} > TIMER </button> </div> </div> </div> ); }; // ----------------------------------------------------------------------------- // THREE.JS SCENE COMPONENT // ----------------------------------------------------------------------------- const ThreeClockScene = ({ visualMode, dataRef, appMode, userLocation, textureUrl }) => { const mountRef = useRef(null); const cleanupRef = useRef(null); useEffect(() => { if (!mountRef.current || !window.THREE) return; const THREE = window.THREE; const scene = new THREE.Scene(); // Camera const width = window.innerWidth; const height = window.innerHeight; const aspect = width / height; const baseDistance = aspect < 1 ? 70 : 40; const camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000); camera.position.set(0, 0, baseDistance); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(width, height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; mountRef.current.appendChild(renderer.domElement); const handleResize = () => { const w = window.innerWidth; const h = window.innerHeight; const newAspect = w / h; camera.aspect = newAspect; camera.position.z = newAspect < 1 ? 70 : 40; camera.updateProjectionMatrix(); renderer.setSize(w, h); }; window.addEventListener('resize', handleResize); // --- ORBIT CONTROLS (INVERTED + ZOOM) --- let cameraRotation = { x: 0, y: 0 }; let zoomLevel = 1.0; let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; let initialPinchDistance = null; let initialZoom = 1.0; const onMouseDown = (e) => { isDragging = true; const x = e.touches ? e.touches[0].clientX : e.clientX; const y = e.touches ? e.touches[0].clientY : e.clientY; previousMousePosition = { x, y }; if (e.touches && e.touches.length === 2) { isDragging = false; const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; initialPinchDistance = Math.sqrt(dx*dx + dy*dy); initialZoom = zoomLevel; } }; const onMouseUp = () => { isDragging = false; initialPinchDistance = null; }; const onMouseMove = (e) => { if (isDragging) { const x = e.touches ? e.touches[0].clientX : e.clientX; const y = e.touches ? e.touches[0].clientY : e.clientY; const deltaX = x - previousMousePosition.x; const deltaY = y - previousMousePosition.y; // INVERTED CONTROLS cameraRotation.x -= deltaX * 0.005; cameraRotation.y -= deltaY * 0.005; // Inverted Vertical cameraRotation.y = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, cameraRotation.y)); previousMousePosition = { x, y }; } if (e.touches && e.touches.length === 2 && initialPinchDistance) { const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const currentDistance = Math.sqrt(dx*dx + dy*dy); const ratio = initialPinchDistance / currentDistance; zoomLevel = Math.min(Math.max(initialZoom * ratio, 0.3), 3.0); } }; const onWheel = (e) => { e.preventDefault(); const delta = e.deltaY * 0.001; zoomLevel = Math.min(Math.max(zoomLevel + delta, 0.3), 3.0); }; window.addEventListener('mousedown', onMouseDown); window.addEventListener('mouseup', onMouseUp); window.addEventListener('mousemove', onMouseMove); window.addEventListener('touchstart', onMouseDown, {passive: false}); window.addEventListener('touchend', onMouseUp); window.addEventListener('touchmove', onMouseMove, {passive: false}); window.addEventListener('wheel', onWheel, {passive: false}); // DIGITAL LAYOUT CONSTANTS const DIGIT_POS_X = [-11, -7.5, -2.5, 1.5, 6.5, 10]; const COLON_POS_X = [-5, 4]; const getDigitPoints = (number, xOffset, isColon = false) => { const grid = isColon ? COLON : (DIGITS[number] || DIGITS[0]); const points = []; const scale = 0.5; grid.forEach((row, y) => { row.forEach((val, x) => { if (val) { const px = (x - 1) * scale + xOffset; const py = -(y - 2) * scale; points.push(new THREE.Vector3(px, py, 0)); } }); }); return points; }; let updateFn = null; // 1. PARTICLE MIST if (visualMode === 'PARTICLE_MIST') { const systems = []; const PARTICLE_COUNT = 1200; for(let i=0; i<8; i++) { const isColon = i >= 6; const pCount = isColon ? 200 : PARTICLE_COUNT; const geo = new THREE.BufferGeometry(); const pos = new Float32Array(pCount * 3); const vel = new Float32Array(pCount * 3); const target = new Float32Array(pCount * 3); for(let k=0; k<pCount*3; k++) pos[k] = (Math.random()-0.5)*30; geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); const mat = new THREE.PointsMaterial({ size: 0.12, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, color: 0xffffff }); const mesh = new THREE.Points(geo, mat); scene.add(mesh); systems.push({ mesh, pos, vel, target, currentDigit: -1, phase: 0, isColon }); } updateFn = (h, m, s, themeColor) => { const digits = [Math.floor(h/10), h%10, Math.floor(m/10), m%10, Math.floor(s/10), s%10, 10, 10]; systems.forEach((sys, idx) => { let col = themeColor; if (dataRef.current.isTimer) col = idx > 3 && idx < 6 ? 0xffffff : themeColor; else if (idx >= 4 && idx < 6) col = 0xff3366; if (sys.isColon) col = 0x888888; sys.mesh.material.color.setHex(col); const num = digits[idx]; const xPos = sys.isColon ? COLON_POS_X[idx-6] : DIGIT_POS_X[idx]; if(sys.currentDigit !== num) { sys.currentDigit = num; sys.phase = 1.0; const pts = getDigitPoints(num, xPos, sys.isColon); const pLen = sys.pos.length / 3; for(let p=0; p<pLen; p++) { const pt = pts[p % pts.length]; sys.target[p*3] = pt.x + (Math.random()-0.5)*0.3; sys.target[p*3+1] = pt.y + (Math.random()-0.5)*0.3; sys.target[p*3+2] = pt.z + (Math.random()-0.5)*0.5; if(sys.phase > 0) { sys.vel[p*3] = (Math.random()-0.5)*1.5; sys.vel[p*3+1] = (Math.random()-0.5)*1.5; sys.vel[p*3+2] = (Math.random()-0.5)*2.0; } } } const pLen = sys.pos.length / 3; for(let p=0; p<pLen; p++) { const ix = p*3; if(sys.phase > 0) { sys.pos[ix] += sys.vel[ix]; sys.pos[ix+1] += sys.vel[ix+1]; sys.pos[ix+2] += sys.vel[ix+2]; sys.vel[ix] *= 0.92; sys.vel[ix+1] *= 0.92; sys.vel[ix+2] *= 0.92; } sys.pos[ix] += (sys.target[ix] - sys.pos[ix]) * 0.12; sys.pos[ix+1] += (sys.target[ix+1] - sys.pos[ix+1]) * 0.12; sys.pos[ix+2] += (sys.target[ix+2] - sys.pos[ix+2]) * 0.12; sys.pos[ix] += Math.sin(Date.now()*0.003 + p)*0.004; } sys.mesh.geometry.attributes.position.needsUpdate = true; if(sys.isColon) sys.mesh.material.opacity = 0.5 + Math.sin(Date.now()*0.005)*0.4; if(sys.phase>0) sys.phase -= 0.03; }); }; } else if (visualMode === 'VOXEL_TUMBLE') { const geo = new THREE.BoxGeometry(0.35, 0.35, 0.35); const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.2, metalness: 0.5 }); const hotMat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffaa00, emissiveIntensity: 2.0 }); const light = new THREE.DirectionalLight(0xffffff, 1.5); light.position.set(0, 10, 10); scene.add(light); scene.add(new THREE.AmbientLight(0x202020)); scene.add(new THREE.PointLight(0x00ddee, 1, 50)); const groups = []; for(let i=0; i<8; i++) { const isColon = i >= 6; const g = new THREE.Group(); g.position.x = isColon ? COLON_POS_X[i-6] : DIGIT_POS_X[i]; scene.add(g); groups.push({ group: g, currentDigit: -1, voxels: [], isColon }); } updateFn = (h, m, s, themeColor) => { const digits = [Math.floor(h/10), h%10, Math.floor(m/10), m%10, Math.floor(s/10), s%10, 10, 10]; mat.color.setHex(themeColor); groups.forEach((gObj, idx) => { const num = digits[idx]; if (!gObj.isColon && idx >= 4 && idx < 6 && !dataRef.current.isTimer) mat.color.setHex(0xff3366); else mat.color.setHex(themeColor); if(gObj.currentDigit !== num) { gObj.currentDigit = num; gObj.voxels.forEach(v => { v.active = false; v.vel = new THREE.Vector3((Math.random()-0.5)*0.8, Math.random()*0.8, (Math.random()-0.5)*2.0); v.rot = new THREE.Vector3(Math.random()*0.5, Math.random()*0.5, Math.random()*0.5); v.mesh.material = hotMat; }); const pts = getDigitPoints(num, 0, gObj.isColon); pts.forEach(pt => { const m = new THREE.Mesh(geo, mat); m.position.set(pt.x, pt.y - 10, -5); m.scale.set(0,0,0); gObj.group.add(m); gObj.voxels.push({ mesh: m, target: pt, active: true, vel: new THREE.Vector3() }); }); } gObj.voxels = gObj.voxels.filter(v => { if(!v.active) { v.mesh.position.add(v.vel); v.mesh.rotation.x += v.rot.x; v.mesh.rotation.y += v.rot.y; v.vel.y -= 0.03; v.mesh.scale.multiplyScalar(0.92); if(v.mesh.position.y < -15) { gObj.group.remove(v.mesh); return false; } return true; } else { v.mesh.position.lerp(v.target, 0.2); v.mesh.scale.lerp(new THREE.Vector3(1,1,1), 0.2); v.mesh.rotation.set(0,0,0); return true; } }); }); }; } else if (visualMode === 'GLITCH_MATRIX') { const boxGeo = new THREE.PlaneGeometry(0.4, 0.4); const boxMat = new THREE.MeshBasicMaterial({ color: 0x00ff00, side: THREE.DoubleSide }); const digitMeshes = []; for(let i=0; i<8; i++) { const isColon = i >= 6; const g = new THREE.Group(); g.position.x = isColon ? COLON_POS_X[i-6] : DIGIT_POS_X[i]; const pixels = []; for(let y=0; y<5; y++) { for(let x=0; x<3; x++) { const m = new THREE.Mesh(boxGeo, boxMat.clone()); const ox = (x-1)*0.45; const oy = -(y-2)*0.45; m.position.set(ox, oy, 0); m.userData = { ox, oy }; g.add(m); pixels.push(m); } } scene.add(g); digitMeshes.push({ pixels, currentDigit: -1, glitchFrames: 0, isColon }); } updateFn = (h, m, s, themeColor) => { const digits = [Math.floor(h/10), h%10, Math.floor(m/10), m%10, Math.floor(s/10), s%10, 10, 10]; digitMeshes.forEach((d, idx) => { const num = digits[idx]; if(d.currentDigit !== num) { d.currentDigit = num; d.glitchFrames = 25; } const grid = d.isColon ? COLON : DIGITS[num]; const flat = grid.flat(); d.pixels.forEach((p, pIdx) => { const on = flat[pIdx] === 1; if(d.glitchFrames > 0) { p.visible = Math.random() > 0.3; p.material.color.setHex(Math.random()>0.5 ? 0x00ffff : 0xff00ff); p.position.set(p.userData.ox + (Math.random()-0.5)*0.5, p.userData.oy + (Math.random()-0.5)*0.5, (Math.random()-0.5)*2.0); } else { p.visible = on; if (!d.isColon && idx >= 4 && idx < 6 && !dataRef.current.isTimer) p.material.color.setHex(0xff3366); else if (d.isColon) p.material.color.setHex(0x888888); else p.material.color.setHex(themeColor); p.position.set(p.userData.ox, p.userData.oy, 0); } }); if(d.glitchFrames > 0) d.glitchFrames--; }); }; } else if (visualMode === 'NEON_RINGS') { const group = new THREE.Group(); scene.add(group); const createRing = (radius, color, thickness) => { const rGroup = new THREE.Group(); rGroup.add(new THREE.Mesh(new THREE.TorusGeometry(radius, 0.05, 16, 100), new THREE.MeshBasicMaterial({ color: 0x333333 }))); const cursor = new THREE.Mesh(new THREE.TorusGeometry(radius, thickness, 16, 32, Math.PI / 4), new THREE.MeshBasicMaterial({ color: color })); rGroup.add(cursor); for(let i=0; i<12; i++) { const tick = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.2, 0.2), new THREE.MeshBasicMaterial({color: 0x555555})); const a = (i/12)*Math.PI*2; tick.position.set(Math.cos(a)*radius, Math.sin(a)*radius, 0); rGroup.add(tick); } return { rGroup, cursor, radius }; }; const rings = { h: createRing(8, 0x00ffff, 0.4), m: createRing(11, 0xff00ff, 0.3), s: createRing(14, 0xffffff, 0.15) }; group.add(rings.h.rGroup); group.add(rings.m.rGroup); group.add(rings.s.rGroup); updateFn = (h, m, s, themeColor) => { let time; if (appMode === 'CLOCK') { const d = new Date(); time = { h: d.getHours()%12, m: d.getMinutes(), s: d.getSeconds(), ms: d.getMilliseconds() }; } else { time = { h, m, s, ms: 0 }; } rings.s.cursor.rotation.z = -((time.s + time.ms/1000) / 60) * Math.PI * 2 + Math.PI/2; rings.m.cursor.rotation.z = -((time.m + time.s/60) / 60) * Math.PI * 2 + Math.PI/2; rings.h.cursor.rotation.z = -((time.h + time.m/60) / 12) * Math.PI * 2 + Math.PI/2; }; } // 5. SOLAR DIAL (UPDATED: Real Texture + Custom Map) else if (visualMode === 'SOLAR_DIAL') { const group = new THREE.Group(); scene.add(group); // Load Custom/Default Map const textureLoader = new THREE.TextureLoader(); textureLoader.setCrossOrigin('anonymous'); const earthTex = textureLoader.load(textureUrl); // City Lights for Emissive const cityMap = generateCityLights(); const cityTex = new THREE.CanvasTexture(cityMap); // B. The Planet const sphereGeo = new THREE.SphereGeometry(6, 64, 64); const sphereMat = new THREE.MeshStandardMaterial({ map: earthTex, roughness: 0.8, metalness: 0.1, emissive: 0x000000, emissiveMap: cityTex, emissiveIntensity: 2.0 }); const sphere = new THREE.Mesh(sphereGeo, sphereMat); sphere.receiveShadow = true; sphere.castShadow = true; group.add(sphere); // Atmosphere const atmoGeo = new THREE.SphereGeometry(6.2, 64, 64); const atmoMat = new THREE.MeshBasicMaterial({ color: 0x4488ff, transparent: true, opacity: 0.1, side: THREE.BackSide, blending: THREE.AdditiveBlending }); const atmo = new THREE.Mesh(atmoGeo, atmoMat); group.add(atmo); // Stars const starsGeo = new THREE.BufferGeometry(); const starCount = 2000; const starPos = new Float32Array(starCount * 3); for(let i=0; i<starCount*3; i++) starPos[i] = (Math.random() - 0.5) * 200; starsGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3)); const starsMat = new THREE.PointsMaterial({color: 0xffffff, size: 0.2, transparent: true, opacity: 0.8}); const stars = new THREE.Points(starsGeo, starsMat); scene.add(stars); // C. User Marker const markerGroup = new THREE.Group(); if (userLocation) { const latRad = userLocation.lat * (Math.PI / 180); const lonRad = -userLocation.lon * (Math.PI / 180); const r = 6; const x = r * Math.cos(latRad) * Math.cos(lonRad); const y = r * Math.sin(latRad); const z = r * Math.cos(latRad) * Math.sin(lonRad); markerGroup.position.set(x, y, z); markerGroup.lookAt(x*2, y*2, z*2); // Align Camera initially cameraRotation.x = lonRad + Math.PI/2; cameraRotation.y = latRad; } else { markerGroup.position.set(0, 6, 0); } const pinHead = new THREE.Mesh(new THREE.DodecahedronGeometry(0.3), new THREE.MeshBasicMaterial({ color: 0xff3366 })); pinHead.position.z = 0.8; markerGroup.add(pinHead); const pinStick = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.8), new THREE.MeshBasicMaterial({ color: 0xffffff })); pinStick.rotation.x = Math.PI/2; pinStick.position.z = 0.4; markerGroup.add(pinStick); group.add(markerGroup); // D. Sun Mechanism const sunPivot = new THREE.Group(); group.add(sunPivot); const sunDist = 25; const sunMesh = new THREE.Mesh(new THREE.SphereGeometry(1.5, 32, 32), new THREE.MeshBasicMaterial({ color: 0xffaa00 })); sunMesh.position.set(sunDist, 0, 0); sunPivot.add(sunMesh); const sunLight = new THREE.DirectionalLight(0xffffff, 2.5); sunLight.position.set(sunDist, 0, 0); sunLight.castShadow = true; sunLight.shadow.mapSize.width = 2048; sunLight.shadow.mapSize.height = 2048; sunLight.shadow.camera.near = 0.5; sunLight.shadow.camera.far = 100; sunLight.shadow.camera.left = -10; sunLight.shadow.camera.right = 10; sunLight.shadow.camera.top = 10; sunLight.shadow.camera.bottom = -10; sunPivot.add(sunLight); const ambient = new THREE.AmbientLight(0x000000); scene.add(ambient); updateFn = (h, m, s) => { const d = new Date(); const timeVal = d.getUTCHours() + d.getUTCMinutes()/60; const sunAngle = -((timeVal - 12) / 24) * Math.PI * 2; sunPivot.rotation.y = sunAngle; sunMesh.material.color.setHex(dataRef.current.isTimer ? 0xff0000 : 0xffaa00); }; } else if (visualMode === 'ARC_REACTOR') { const group = new THREE.Group(); scene.add(group); const makeArc = (radius, color, width) => { const g = new THREE.Group(); g.add(new THREE.Mesh(new THREE.RingGeometry(radius-width/2, radius+width/2, 64), new THREE.MeshBasicMaterial({ color: 0x111111, side: THREE.DoubleSide }))); const active = new THREE.Mesh(new THREE.BufferGeometry(), new THREE.MeshBasicMaterial({ color: color, side: THREE.DoubleSide })); g.add(active); return { active, radius, width, color }; }; const arcH = makeArc(7, 0x44aadd, 1); const arcM = makeArc(9.5, 0x44aadd, 0.8); const arcS = makeArc(12, 0xffffff, 0.5); group.add(arcH.active.parent); group.add(arcM.active.parent); group.add(arcS.active.parent); updateFn = (h, m, s) => { let time; if (appMode === 'CLOCK') { const d = new Date(); time = { h: d.getHours()%12, m: d.getMinutes(), s: d.getSeconds(), ms: d.getMilliseconds() }; } else { time = { h, m, s, ms: 0 }; } const updateGeo = (mesh, radius, width, pct) => { if(mesh.geometry) mesh.geometry.dispose(); mesh.geometry = new THREE.RingGeometry(radius-width/2, radius+width/2, 64, 1, Math.PI/2, -(Math.PI * 2 * pct)); }; updateGeo(arcS.active, arcS.radius, arcS.width, (time.s + time.ms/1000)/60); updateGeo(arcM.active, arcM.radius, arcM.width, (time.m + time.s/60)/60); updateGeo(arcH.active, arcH.radius, arcH.width, (time.h + time.m/60)/12); }; } const animate = () => { cleanupRef.current = requestAnimationFrame(animate); const { h, m, s, themeColor } = dataRef.current; if (updateFn) updateFn(h, m, s, themeColor); const dist = baseDistance * zoomLevel; camera.position.x = dist * Math.sin(cameraRotation.x) * Math.cos(cameraRotation.y); camera.position.z = dist * Math.cos(cameraRotation.x) * Math.cos(cameraRotation.y); camera.position.y = dist * Math.sin(cameraRotation.y); camera.lookAt(0, 0, 0); renderer.render(scene, camera); }; animate(); return () => { window.removeEventListener('resize', handleResize); window.removeEventListener('mousedown', onMouseDown); window.removeEventListener('mouseup', onMouseUp); window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('touchstart', onMouseDown); window.removeEventListener('touchend', onMouseUp); window.removeEventListener('touchmove', onMouseMove); window.removeEventListener('wheel', onWheel); cancelAnimationFrame(cleanupRef.current); if (mountRef.current && renderer.domElement) { mountRef.current.removeChild(renderer.domElement); } renderer.dispose(); while(scene.children.length > 0){ scene.remove(scene.children[0]); } }; }, [visualMode, userLocation, textureUrl]); return <div ref={mountRef} className="w-full h-full cursor-move active:cursor-grabbing" />; }; export default ModernClock;
No revisions found. Save the file to create a backup.
Delete
Update App