Spaces:
Running
Running
Download clockwork/clockwork.js from numexai/numex-market: direct link, hf CLI and curl.
- Browser
- Download file 51.3 kB
-
https://huggingface.co/spaces/numexai/numex-market/resolve/main/clockwork/clockwork.js
- Command line
-
hf download hf://spaces/numexai/numex-market/clockwork/clockwork.js
-
curl -L -o clockwork.js https://huggingface.co/spaces/numexai/numex-market/resolve/main/clockwork/clockwork.js
51.3 kB
| /* ============================================================ | |
| NUMEX CLOCKWORK — Zamanın İçini Gör | |
| Tamamen kodla üretilmiş 3D mekanik saat. | |
| Hazır model / video / dış görsel YOK — her parça matematikle çizilir. | |
| ============================================================ */ | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; | |
| import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; | |
| import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; | |
| import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; | |
| import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; | |
| import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; | |
| /* ============================================================ | |
| 0. YARDIMCILAR | |
| ============================================================ */ | |
| const TAU = Math.PI * 2; | |
| const DEG = Math.PI / 180; | |
| const clamp = (v, a, b) => Math.min(b, Math.max(a, v)); | |
| const lerp = (a, b, t) => a + (b - a) * t; | |
| const easeInOut = t => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2); | |
| /* ============================================================ | |
| 1. MALZEMELER — altın, bronz, çelik, cam | |
| ============================================================ */ | |
| const MAT = {}; | |
| /* ------------------------------------------------------------ | |
| Yüzey pürüz dokusu — kodla üretilir (harici dosya YOK). | |
| Değer gürültüsü + ince tarama: metali "kesilmiş sac" görünümünden | |
| çıkarıp torna/freze izli gerçek yüzeye yaklaştırır. | |
| ------------------------------------------------------------ */ | |
| function makeMetalRoughnessTexture(size = 256, base = 150, variance = 46) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = canvas.height = size; | |
| const ctx = canvas.getContext('2d'); | |
| const img = ctx.createImageData(size, size); | |
| for (let y = 0; y < size; y++) { | |
| for (let x = 0; x < size; x++) { | |
| // eşmerkezli tarama izleri + ince gürültü | |
| const cx = x / size - 0.5; | |
| const cy = y / size - 0.5; | |
| const r = Math.sqrt(cx * cx + cy * cy) * 60; | |
| const rings = Math.sin(r * Math.PI) * 0.5 + 0.5; | |
| const noise = Math.random(); | |
| const v = clamp(base + (rings - 0.5) * variance * 1.4 + (noise - 0.5) * variance, 0, 255); | |
| const i = (y * size + x) * 4; | |
| img.data[i] = img.data[i + 1] = img.data[i + 2] = v; | |
| img.data[i + 3] = 255; | |
| } | |
| } | |
| ctx.putImageData(img, 0, 0); | |
| const tex = new THREE.CanvasTexture(canvas); | |
| tex.wrapS = tex.wrapT = THREE.RepeatWrapping; | |
| tex.repeat.set(2, 2); | |
| tex.colorSpace = THREE.NoColorSpace; | |
| tex.anisotropy = 4; | |
| return tex; | |
| } | |
| function buildMaterials() { | |
| const roughTex = makeMetalRoughnessTexture(); | |
| /* Altın: yüzeyde mikro pürüz (roughnessMap yerine yüksek roughness + | |
| düşük emissive) — düz "sarı boya" görünümünü kırar. */ | |
| MAT.gold = new THREE.MeshPhysicalMaterial({ | |
| color: 0xc08c33, metalness: 1.0, roughness: 0.42, | |
| roughnessMap: roughTex, | |
| clearcoat: 0.35, clearcoatRoughness: 0.45, | |
| emissive: 0x140c02, emissiveIntensity: 0.25, | |
| }); | |
| MAT.goldPolished = new THREE.MeshPhysicalMaterial({ | |
| color: 0xe8bd6d, metalness: 1.0, roughness: 0.2, | |
| roughnessMap: roughTex, | |
| clearcoat: 0.85, clearcoatRoughness: 0.08, | |
| }); | |
| MAT.bronze = new THREE.MeshPhysicalMaterial({ | |
| color: 0x6f4520, metalness: 1.0, roughness: 0.56, | |
| roughnessMap: roughTex, | |
| clearcoat: 0.2, clearcoatRoughness: 0.6, | |
| emissive: 0x0e0702, emissiveIntensity: 0.22, | |
| }); | |
| MAT.copper = new THREE.MeshPhysicalMaterial({ | |
| color: 0xb06a33, metalness: 1.0, roughness: 0.28, | |
| }); | |
| MAT.steel = new THREE.MeshPhysicalMaterial({ | |
| color: 0x9aa1aa, metalness: 1.0, roughness: 0.24, | |
| roughnessMap: roughTex, | |
| clearcoat: 0.6, | |
| }); | |
| MAT.dark = new THREE.MeshPhysicalMaterial({ | |
| color: 0x2a2118, metalness: 0.85, roughness: 0.5, | |
| }); | |
| MAT.dialFace = new THREE.MeshPhysicalMaterial({ | |
| color: 0x0e0b08, metalness: 0.35, roughness: 0.62, | |
| clearcoat: 0.4, clearcoatRoughness: 0.4, | |
| }); | |
| MAT.enamel = new THREE.MeshPhysicalMaterial({ | |
| color: 0x14100c, metalness: 0.2, roughness: 0.55, | |
| clearcoat: 0.9, clearcoatRoughness: 0.15, | |
| }); | |
| /* Cam: yalnızca kenarlarda ince bir yansıma. | |
| transmission + bloom beyaz leke üretiyordu; additive blending ise | |
| tüm sahneyi sise boğuyordu. Normal saydamlık + çok düşük opaklık | |
| sahneyi örtmeden cam hissi verir. */ | |
| MAT.glass = new THREE.MeshPhysicalMaterial({ | |
| color: 0xaec6d6, metalness: 0.0, roughness: 0.06, | |
| transparent: true, opacity: 0.055, | |
| clearcoat: 1.0, clearcoatRoughness: 0.02, | |
| side: THREE.FrontSide, depthWrite: false, | |
| envMapIntensity: 1.6, | |
| }); | |
| MAT.glow = new THREE.MeshBasicMaterial({ color: 0xffd699 }); | |
| MAT.ruby = new THREE.MeshPhysicalMaterial({ | |
| color: 0x8e1020, metalness: 0.1, roughness: 0.15, | |
| clearcoat: 1.0, emissive: 0x3a0510, emissiveIntensity: 0.6, | |
| }); | |
| } | |
| /* ============================================================ | |
| 2. EVOLVENT DİŞLİ ÜRETECİ | |
| Gerçek evolvent profil yerine yüksek kaliteli yaklaşım: | |
| her diş bir trapez + yuvarlatılmış tepe; silindirik gövde. | |
| ============================================================ */ | |
| function gearShape(teeth, pitchRadius, { module_ = 0.083, hubRatio = 0.34, spokeCount = 0, holeRadius = 0 } = {}) { | |
| const shape = new THREE.Shape(); | |
| /* DİŞ ÖLÇÜSÜ HATVEYE BAĞLIDIR, YARIÇAPA DEĞİL. | |
| Eski sürüm toothDepth'ı pitchRadius'un yüzdesi olarak alıyordu; bu yüzden | |
| 44 dişli çarkta dişler iğne gibi ince, 9 dişlide kütük gibi kalıyor ve | |
| komşu çarka hiç girmiyordu (ekranda "fırça" görünümü). Gerçek çarkta: | |
| addendum = 1.00 × m (hatve dairesinden tepeye) | |
| dedendum = 1.25 × m (hatve dairesinden köke) | |
| Kök açıklığı bilerek tepe yüksekliğinden büyük: böylece dişler boşluğa | |
| girer, dipte çakışma olmaz. */ | |
| const m = module_; // hatve (modül) | |
| const rPitch = pitchRadius; | |
| const rOuter = rPitch + m * 1.00; // diş üstü dairesi | |
| const rRoot = rPitch - m * 1.25; // diş dibi dairesi | |
| const step = TAU / teeth; // açısal hatve | |
| const halfTop = step * 0.21; // tepe genişliği (hatvenin %42'si) | |
| const halfRoot = step * 0.29; // kök genişliği (hatvenin %58'i) | |
| /* EVOLVENT YAKLAŞIMI: diş yanağı iki nokta arasında düz çizgi değil, | |
| kökten tepeye doğru DIŞA doğru kıvrılan bir eğri. Gerçek evolventin | |
| ilk iki terimi (r·(1+t²/2)) yeterli: fark gözle görülür, maliyeti yok. */ | |
| const flank = (side, a, r, k) => { | |
| // side: -1 sol yanak, +1 sağ yanak; k: 0→kök, 1→tepe | |
| const t = k; // yanak boyunca 0..1 | |
| const ang = a + side * lerp(halfRoot, halfTop, t); | |
| // evolvent düzeltmesi: yanağın ortası hafifçe dışa taşar | |
| const bulge = 1 + m * 0.10 * Math.sin(Math.PI * t) / r; | |
| return [Math.cos(ang) * r * bulge, Math.sin(ang) * r * bulge]; | |
| }; | |
| for (let i = 0; i < teeth; i++) { | |
| const a = i * step; | |
| const rootL = a - halfRoot; | |
| const rootR = a + halfRoot; | |
| const topL = a - halfTop; | |
| const topR = a + halfTop; | |
| // 1) kök yayının sonundan bu dişin sol köküne | |
| if (i === 0) { | |
| shape.moveTo(Math.cos(rootL) * rRoot, Math.sin(rootL) * rRoot); | |
| } else { | |
| shape.lineTo(Math.cos(rootL) * rRoot, Math.sin(rootL) * rRoot); | |
| } | |
| // 2) sol yanak — kökten tepeye eğri (3 ara nokta) | |
| for (let s = 1; s <= 3; s++) { | |
| const p = flank(-1, a, lerp(rRoot, rOuter, s / 4), s / 4); | |
| shape.lineTo(p[0], p[1]); | |
| } | |
| shape.lineTo(Math.cos(topL) * rOuter, Math.sin(topL) * rOuter); | |
| // 3) tepe yayı — bir miktar yuvarlatılmış (gerçek çarkta tepe düzdür, | |
| // köşeler faso edilmiştir; burada 2 ara nokta ile taklit edilir) | |
| shape.absarc(0, 0, rOuter, topL, topR, false); | |
| // 4) sağ yanak — tepeden köke eğri | |
| for (let s = 3; s >= 1; s--) { | |
| const p = flank(1, a, lerp(rRoot, rOuter, s / 4), s / 4); | |
| shape.lineTo(p[0], p[1]); | |
| } | |
| // 5) kök yayı — bir sonraki dişin sol köküne kadar | |
| const nextRootL = a + step - halfRoot; | |
| shape.absarc(0, 0, rRoot, rootR, nextRootL, false); | |
| } | |
| shape.closePath(); | |
| // merkez deliği | |
| const hubR = pitchRadius * hubRatio; | |
| const hole = new THREE.Path(); | |
| hole.absarc(0, 0, hubR, 0, TAU, true); | |
| shape.holes.push(hole); | |
| // hafifletme delikleri (büyük dişlilerde "spoke" görünümü) | |
| if (spokeCount > 0) { | |
| const holeR = pitchRadius * holeRadius; | |
| const ringR = (hubR + rRoot) * 0.5; | |
| for (let s = 0; s < spokeCount; s++) { | |
| const ang = (s / spokeCount) * TAU + Math.PI / spokeCount; | |
| const cx = Math.cos(ang) * ringR; | |
| const cy = Math.sin(ang) * ringR; | |
| const h = new THREE.Path(); | |
| h.absarc(cx, cy, holeR, 0, TAU, true); | |
| shape.holes.push(h); | |
| } | |
| } | |
| return shape; | |
| } | |
| function makeGear(teeth, module_, opts = {}) { | |
| const pitchRadius = (module_ * teeth) / 2; | |
| const thickness = opts.thickness ?? pitchRadius * 0.22; | |
| const shape = gearShape(teeth, pitchRadius, { ...opts, module_ }); | |
| const geo = new THREE.ExtrudeGeometry(shape, { | |
| depth: thickness, | |
| bevelEnabled: true, | |
| bevelThickness: thickness * 0.08, | |
| bevelSize: pitchRadius * 0.012, | |
| bevelSegments: 2, | |
| curveSegments: Math.max(8, Math.min(30, Math.round(teeth / 2))), | |
| }); | |
| geo.center(); | |
| geo.rotateX(0); | |
| const mesh = new THREE.Mesh(geo, opts.material || MAT.gold); | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| mesh.userData.pitchRadius = pitchRadius; | |
| mesh.userData.teeth = teeth; | |
| mesh.userData.thickness = thickness; | |
| return mesh; | |
| } | |
| /* ------------------------------------------------------------ | |
| MİL, SOMUN, RONDELA | |
| DÜZELTME (kök neden): Silindir geometrisinin ekseni ZATEN +Y'dir. | |
| Eski kod `rotation.x = Math.PI/2` uyguluyordu → mil ve somun çark | |
| düzlemine YATIYOR, çark ekseninde durmuyordu. Ayrıca geo.center() | |
| sonrası çarkın ön yüzü +thickness/2'de kalırken somun thickness*0.72'ye | |
| konuyordu → gövdenin İÇİNE gömülüyor, dışarıdan görünmüyordu. | |
| Doğrusu: çark düzlemi XY, kalınlık Z boyunca. Mil ve somun Z ekseninde | |
| olmalı → silindiri `rotateX(90°)` ile Y'den Z'ye çevirmek gerekir; | |
| ama bunu mesh'e DEĞİL geometriye uygulayıp somun konumunu ön yüze taşırız. | |
| ------------------------------------------------------------ */ | |
| function addAxleAndNut(group, radius, thickness, mat = MAT.steel) { | |
| /* Mil: çark kalınlığından taşar, iki yanda görünür */ | |
| const axleLen = thickness + radius * 0.34; | |
| const axleGeo = new THREE.CylinderGeometry(radius * 0.085, radius * 0.085, axleLen, 18); | |
| axleGeo.rotateX(Math.PI / 2); // Y ekseni → Z ekseni (çark ekseni) | |
| const axle = new THREE.Mesh(axleGeo, mat); | |
| axle.castShadow = true; | |
| group.add(axle); | |
| /* Rondela: somunun çarka oturduğu pul */ | |
| const washerGeo = new THREE.CylinderGeometry(radius * 0.19, radius * 0.19, thickness * 0.10, 24); | |
| washerGeo.rotateX(Math.PI / 2); | |
| /* Altıgen somun: çark ekseninde, çarkın ÖN yüzünün hemen dışında */ | |
| const nutGeo = new THREE.CylinderGeometry(radius * 0.145, radius * 0.145, thickness * 0.30, 6); | |
| nutGeo.rotateX(Math.PI / 2); | |
| const faceF = thickness / 2; // çarkın ön yüzü | |
| const washerF = new THREE.Mesh(washerGeo, MAT.bronze); | |
| washerF.position.z = faceF + thickness * 0.05; | |
| group.add(washerF); | |
| const nutF = new THREE.Mesh(nutGeo, MAT.goldPolished); | |
| nutF.position.z = faceF + thickness * 0.10 + thickness * 0.15; | |
| nutF.castShadow = true; | |
| group.add(nutF); | |
| /* Arka yüzde sadece rondela + küçük somun (kasaya bakan taraf) */ | |
| const washerB = new THREE.Mesh(washerGeo, MAT.bronze); | |
| washerB.position.z = -faceF - thickness * 0.05; | |
| group.add(washerB); | |
| const nutB = new THREE.Mesh(nutGeo, MAT.goldPolished); | |
| nutB.position.z = -faceF - thickness * 0.10 - thickness * 0.15; | |
| group.add(nutB); | |
| return group; | |
| } | |
| /* ============================================================ | |
| 3. SAHNE KURULUMU | |
| ============================================================ */ | |
| const canvas = document.getElementById('stage'); | |
| const renderer = new THREE.WebGLRenderer({ | |
| canvas, antialias: true, alpha: false, powerPreference: 'high-performance', | |
| }); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| renderer.toneMappingExposure = 1.05; | |
| renderer.outputColorSpace = THREE.SRGBColorSpace; | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x060403); | |
| scene.fog = new THREE.FogExp2(0x060403, 0.028); | |
| const camera = new THREE.PerspectiveCamera(34, window.innerWidth / window.innerHeight, 0.1, 200); | |
| const CAM_TARGET = new THREE.Vector3(0, 0.2, 0); | |
| /* Ev kamerası EKRAN ORANINA göre hesaplanır: dar/dikey ekranda yatay | |
| görüş açısı küçüldüğü için sabit mesafe kasayı kesiyordu. Kasa yarıçapı | |
| CASE_R, hem dikey hem yatay eksende sığacak şekilde ölçeklenir. */ | |
| const CAM_HOME = new THREE.Vector3(); | |
| /* ★28.10 Market hazırlığı: alttaki kontrol paneli kasanın alt üçte birini (sarkaç | |
| topuzu dahil) örtüyordu — kadraj tüm pencereye göre hesaplanıyordu. Artık SERBEST | |
| alan (panelin üstü) ölçülür: kamera görüntüsü o alanın ortasına kaydırılır | |
| (setViewOffset) ve sığdırma mesafesi o alanın yüksekliğine göre bulunur. */ | |
| function serbestAlan() { | |
| const h = window.innerHeight; | |
| const panel = document.getElementById('panel'); | |
| const alt = panel ? Math.max(0, h - panel.getBoundingClientRect().top + 6) : 0; | |
| const ust = Math.min(56, h * 0.06); | |
| return { h, alt, ust, oran: Math.max(0.35, (h - alt - ust) / h) }; | |
| } | |
| function kadrajiKaydir() { | |
| const w = window.innerWidth, { h, alt, ust } = serbestAlan(); | |
| camera.setViewOffset(w, h, 0, Math.round((alt - ust) / 2), w, h); | |
| } | |
| /* yarıçapı R olan daireyi serbest alana sığdıran kamera mesafesi */ | |
| function sigmaMesafesi(R, pay) { | |
| const vFov = Math.atan(Math.tan((camera.fov * DEG) / 2) * serbestAlan().oran); | |
| const hFov = Math.atan(Math.tan((camera.fov * DEG) / 2) * camera.aspect); | |
| return (R / Math.tan(Math.min(vFov, hFov))) * pay; // dar olan eksen belirleyici | |
| } | |
| function computeHomeDistance() { | |
| const CASE_R_HOME = 5.4; // kasa yarıçapı (CASE_R sabitinin ev değeri) | |
| /* %26 nefes payı: %16'da en dıştaki çarklar kadraj kenarına dayanıyordu. */ | |
| return sigmaMesafesi(CASE_R_HOME, 1.26); | |
| } | |
| function resetCameraHome(immediate = false) { | |
| kadrajiKaydir(); | |
| const d = computeHomeDistance(); | |
| CAM_HOME.set(0, 0.75 * (d / 15.2), d); | |
| if (immediate) { | |
| camera.position.copy(CAM_HOME); | |
| camera.lookAt(CAM_TARGET); | |
| } | |
| return CAM_HOME; | |
| } | |
| resetCameraHome(true); | |
| buildMaterials(); | |
| /* --- Çevre (metal yansımaları) --- */ | |
| const pmrem = new THREE.PMREMGenerator(renderer); | |
| pmrem.compileEquirectangularShader(); | |
| const envRT = pmrem.fromScene(new RoomEnvironment(), 0.04); | |
| scene.environment = envRT.texture; | |
| /* ============================================================ | |
| 4. IŞIK — sinematik | |
| ============================================================ */ | |
| const lights = new THREE.Group(); | |
| scene.add(lights); | |
| const keyLight = new THREE.SpotLight(0xffe3b0, 190, 34, 0.62, 0.45, 1.6); | |
| keyLight.position.set(5.2, 8.6, 7.0); | |
| keyLight.castShadow = true; | |
| keyLight.shadow.mapSize.set(2048, 2048); | |
| keyLight.shadow.camera.near = 1; | |
| keyLight.shadow.camera.far = 40; | |
| keyLight.shadow.bias = -0.0009; | |
| keyLight.target.position.set(0, 0, 0); | |
| lights.add(keyLight, keyLight.target); | |
| const rimLight = new THREE.SpotLight(0x7fb4ff, 120, 34, 0.8, 0.6, 1.5); | |
| rimLight.position.set(-7.2, 4.0, -6.4); | |
| rimLight.target.position.set(0, 0, 0); | |
| lights.add(rimLight, rimLight.target); | |
| const fillLight = new THREE.PointLight(0xff9b4d, 26, 16, 2); | |
| fillLight.position.set(-3.4, -3.2, 5.4); | |
| lights.add(fillLight); | |
| /* Alt dolgu ışığı: sarkaç topuzuna değil, kasanın alt iç yüzüne vurur. */ | |
| const underGlow = new THREE.PointLight(0xb8762a, 5.5, 7, 2.4); | |
| underGlow.position.set(0, -3.2, -0.4); | |
| lights.add(underGlow); | |
| const ambient = new THREE.AmbientLight(0x2a1f14, 0.9); | |
| scene.add(ambient); | |
| /* ============================================================ | |
| 5. KASA — cam arkasında mekanizma | |
| ============================================================ */ | |
| const clockRoot = new THREE.Group(); | |
| scene.add(clockRoot); | |
| const CASE_R = 5.4; | |
| const caseGroup = new THREE.Group(); | |
| clockRoot.add(caseGroup); | |
| /* arka plaka */ | |
| const backPlate = new THREE.Mesh( | |
| new THREE.CylinderGeometry(CASE_R, CASE_R, 0.34, 96), | |
| new THREE.MeshPhysicalMaterial({ | |
| color: 0x12100d, metalness: 0.9, roughness: 0.42, | |
| clearcoat: 0.3, | |
| }) | |
| ); | |
| backPlate.rotation.x = Math.PI / 2; | |
| backPlate.position.z = -1.55; | |
| backPlate.receiveShadow = true; | |
| caseGroup.add(backPlate); | |
| /* arka plakada dekoratif halka */ | |
| const plateRing = new THREE.Mesh( | |
| new THREE.TorusGeometry(CASE_R * 0.82, 0.055, 12, 128), | |
| MAT.bronze | |
| ); | |
| plateRing.position.z = -1.36; | |
| caseGroup.add(plateRing); | |
| /* dış çember / kasa halkası */ | |
| const bezel = new THREE.Mesh( | |
| new THREE.TorusGeometry(CASE_R, 0.36, 22, 128), | |
| MAT.gold | |
| ); | |
| bezel.castShadow = true; | |
| caseGroup.add(bezel); | |
| const bezelInner = new THREE.Mesh( | |
| new THREE.TorusGeometry(CASE_R - 0.32, 0.09, 12, 128), | |
| MAT.goldPolished | |
| ); | |
| caseGroup.add(bezelInner); | |
| /* cam kapak */ | |
| const glassDome = new THREE.Mesh( | |
| new THREE.SphereGeometry(CASE_R * 0.995, 64, 32, 0, TAU, 0, Math.PI * 0.42), | |
| MAT.glass | |
| ); | |
| glassDome.rotation.x = -Math.PI / 2; | |
| glassDome.position.z = 0.98; | |
| glassDome.renderOrder = 3; | |
| clockRoot.add(glassDome); | |
| /* kasayı tutan ayaklar */ | |
| for (let i = 0; i < 4; i++) { | |
| const a = (i / 4) * TAU + Math.PI / 4; | |
| const leg = new THREE.Mesh( | |
| new THREE.CylinderGeometry(0.09, 0.13, 1.5, 12), | |
| MAT.bronze | |
| ); | |
| leg.position.set(Math.cos(a) * CASE_R * 0.92, Math.sin(a) * CASE_R * 0.92, -1.2); | |
| leg.rotation.x = Math.PI / 2; | |
| caseGroup.add(leg); | |
| const ball = new THREE.Mesh(new THREE.SphereGeometry(0.17, 20, 16), MAT.gold); | |
| ball.position.set(Math.cos(a) * CASE_R * 0.92, Math.sin(a) * CASE_R * 0.92, -1.95); | |
| caseGroup.add(ball); | |
| } | |
| /* ============================================================ | |
| 6. DİŞLİ TRENİ — birbirine bağlı, hız oranları fiziksel | |
| ============================================================ */ | |
| /* Modül, kasa çapına göre TÜRETİLİR — sabit vermiyoruz ki dişli treni | |
| her zaman kasanın içinde kalsın. En büyük çark 44 diş, hatve yarıçapı | |
| MODULE*44/2 olur; bunun kasa yarıçapının %42'sini aşmaması istenir. */ | |
| const MAX_TEETH = 44; | |
| const MODULE = (CASE_R * 0.34 * 2) / MAX_TEETH; // ≈ 0.083 — tren kasaya rahat sığar | |
| const gearField = new THREE.Group(); | |
| gearField.position.z = -0.15; | |
| caseGroup.add(gearField); | |
| const gears = []; // her dişli: { group, mesh, teeth, pitchRadius, base, omega, explodeDir } | |
| /* ------------------------------------------------------------ | |
| placeGear — bir dişliyi, kendisinden ÖNCE eklenmiş (ebeveyn) | |
| bir dişliye kavrayacak biçimde yerleştirir. | |
| Merkez uzaklığı = r1 + r2 (aynı modülde kavrama koşulu). | |
| ------------------------------------------------------------ */ | |
| let gearSeq = 0; | |
| function placeGear(teeth, parentIndex, angleDeg, opts = {}) { | |
| const pitchRadius = (MODULE * teeth) / 2; | |
| const mesh = makeGear(teeth, MODULE, opts); | |
| mesh.name = opts.name || `disli_${gearSeq++}`; | |
| const group = new THREE.Group(); | |
| group.name = `mil_${mesh.name}`; | |
| group.add(mesh); | |
| addAxleAndNut(group, pitchRadius, mesh.userData.thickness, opts.axleMat || MAT.steel); | |
| gearField.add(group); | |
| const g = { | |
| group, mesh, teeth, pitchRadius, | |
| omega: 0, // rad/sn (işaretli) | |
| base: new THREE.Vector3(), | |
| explodeDir: new THREE.Vector3(), | |
| parentIndex: parentIndex ?? null, | |
| phase: 0, | |
| }; | |
| if (parentIndex === null || parentIndex === undefined) { | |
| g.base.set(0, 0, opts.z ?? 0); | |
| } else { | |
| const prev = gears[parentIndex]; | |
| /* Kavrama mesafesi: hatve yarıçapları toplamı. Aynı modülde zaten | |
| r1+r2 = (z1+z2)/2 × m'dir — iki yazım eşdeğer; burada niyeti | |
| açık etmek için hatve biçiminde yazıldı. Dişler artık gerçekten | |
| birbirine GİRER (addendum = m, kök boşluğu 1.25m). */ | |
| const dist = (prev.teeth + teeth) / 2 * MODULE; | |
| const a = angleDeg * DEG; | |
| g.base.set( | |
| prev.base.x + Math.cos(a) * dist, | |
| prev.base.y + Math.sin(a) * dist, | |
| opts.z ?? prev.base.z | |
| ); | |
| } | |
| group.position.copy(g.base); | |
| gears.push(g); | |
| return gears.length - 1; | |
| } | |
| /* ------------------------------------------------------------ | |
| DİŞLİ TRENİ | |
| Merkez çark → 5 ara dişli → 3 ikinci kademe → 2 üçüncü kademe | |
| Hepsi GERÇEK kavrama geometrisiyle (r1+r2 uzaklık) yerleşir. | |
| ------------------------------------------------------------ */ | |
| const g0 = placeGear(MAX_TEETH, null, 0, { | |
| name: 'ana_cark', | |
| thickness: 0.16, spokeCount: 5, hubRatio: 0.30, holeRadius: 0.105, | |
| material: MAT.gold, z: 0, | |
| }); | |
| const sats = [ | |
| { teeth: 22, ang: 168, name: 'ara_a', opts: { material: MAT.bronze, z: 0.05, spokeCount: 4, holeRadius: 0.13, hubRatio: 0.32 } }, | |
| { teeth: 30, ang: 74, name: 'ara_b', opts: { material: MAT.copper, z: 0.10, spokeCount: 5, holeRadius: 0.115, hubRatio: 0.30 } }, | |
| { teeth: 16, ang: 123, name: 'ara_c', opts: { material: MAT.gold, z: 0.02, hubRatio: 0.34 } }, | |
| { teeth: 26, ang: 330, name: 'ara_d', opts: { material: MAT.bronze, z: 0.08, spokeCount: 4, holeRadius: 0.13, hubRatio: 0.32 } }, | |
| { teeth: 18, ang: 215, name: 'ara_e', opts: { material: MAT.steel, z: 0.04, hubRatio: 0.34 } }, | |
| ]; | |
| const satIdx = sats.map(s => placeGear(s.teeth, g0, s.ang, { ...s.opts, name: s.name })); | |
| const secondStage = [ | |
| { parent: satIdx[0], teeth: 12, ang: 221, name: 'kademe2_a', opts: { material: MAT.goldPolished, z: 0.30, hubRatio: 0.32 } }, | |
| { parent: satIdx[2], teeth: 13, ang: 132, name: 'kademe2_b', opts: { material: MAT.copper, z: 0.26, hubRatio: 0.32 } }, | |
| { parent: satIdx[3], teeth: 17, ang: 50, name: 'kademe2_c', opts: { material: MAT.bronze, z: 0.32, spokeCount: 0, hubRatio: 0.32 } }, | |
| ]; | |
| const secondIdx = secondStage.map(s => placeGear(s.teeth, s.parent, s.ang, { ...s.opts, name: s.name })); | |
| /* üçüncü kademe — ebeveyni doğrudan ikinci kademedir */ | |
| const thirdStage = [ | |
| { parent: secondIdx[0], teeth: 9, ang: 270, name: 'kademe3_a', opts: { material: MAT.steel, z: 0.46, hubRatio: 0.36 } }, | |
| { parent: secondIdx[2], teeth: 11, ang: 85, name: 'kademe3_b', opts: { material: MAT.gold, z: 0.54, hubRatio: 0.36 } }, | |
| ]; | |
| const thirdIdx = thirdStage.map(s => placeGear(s.teeth, s.parent, s.ang, { ...s.opts, name: s.name })); | |
| /* ============================================================ | |
| 6b. KAÇIŞ ÇARKI — ana çarka kavrar | |
| ★28.10 Market hazırlığı: eskiden 270°'de ara_d ile üst üste duruyor ve hızı elle | |
| (-BASE_OMEGA × 7.5) veriliyordu → dişleri ana çarkın içinden geçiyordu. Artık | |
| boş bir açıda ve hızı/fazı diğer çarklar gibi kavrama oranından gelir. | |
| Açılar ölçülerek seçildi: bağlı olmayan iki çark arası en az ~0.27 birim, | |
| hepsi kasanın 0.88R'si içinde, sarkaç koridoru boş. | |
| ============================================================ */ | |
| const escapeWheel = placeGear(15, g0, 25, { | |
| name: 'escape_wheel', material: MAT.goldPolished, z: 0.44, hubRatio: 0.30, | |
| }); | |
| /* ------------------------------------------------------------ | |
| Dönüş hızları — fiziksel kavrama oranı | |
| w_çocuk = -w_ebeveyn * (z_ebeveyn / z_çocuk) | |
| ------------------------------------------------------------ */ | |
| const BASE_OMEGA = TAU / 72; // ana çark 72 saniyede bir tur | |
| gears.forEach((g, i) => { | |
| if (g.parentIndex === null) { | |
| g.omega = BASE_OMEGA; | |
| g.phase = 0; | |
| return; | |
| } | |
| const parent = gears[g.parentIndex]; | |
| g.omega = -parent.omega * (parent.teeth / g.teeth); | |
| /* Dişlerin birbirine geçmesi: çocuk çark, ebeveyne bakan yönde | |
| yarım diş kaydırılır. Ebeveynin o kavrama açısındaki fazı hesaplanır. */ | |
| /* KAVRAMA FAZI — diş dişe geçsin diye. | |
| Çocuk çarkın, ebeveyne bakan yönünde (contact açısı) bir DİŞ değil | |
| bir BOŞLUK gelmelidir. Ebeveynin o açıdaki fazı ölçülüp çocuğun | |
| fazı yarım hatve kaydırılır. Bu satır olmadan dişler üst üste biner | |
| (ekranda "birbirine girmemiş" görüntünün ikinci nedeni). */ | |
| const dx = g.base.x - parent.base.x; | |
| const dy = g.base.y - parent.base.y; | |
| const contact = Math.atan2(dy, dx); | |
| // ebeveynin kavrama noktasındaki diş fazı (0 = diş ortası, 1 = boşluk ortası) | |
| const parentPhaseAtContact = | |
| (parent.phase * parent.teeth / TAU) + (contact / (TAU / parent.teeth)) + 0.5; | |
| // çocuk, ebeveynin dişinin karşısına BOŞLUK getirecek şekilde döner | |
| // NUMEX-DÜZELTME (2026-10-27): teeth bu kapsamda tanımlı değil; çocuk çarkın diş sayısı g.teeth. ReferenceError tüm sahneyi düşürüyordu. | |
| const childOffset = -(parentPhaseAtContact % 1) * (TAU / g.teeth); | |
| g.phase = contact + Math.PI + childOffset; | |
| }); | |
| /* ============================================================ | |
| 7. KADRAN — akrep, yelkovan, saniye | |
| ============================================================ */ | |
| const dialGroup = new THREE.Group(); | |
| dialGroup.position.z = 0.92; | |
| dialGroup.visible = false; // başlangıçta mekanizma modu | |
| clockRoot.add(dialGroup); | |
| const dialR = CASE_R * 0.86; | |
| /* kadran yüzü */ | |
| const dialFace = new THREE.Mesh( | |
| new THREE.CircleGeometry(dialR, 96), | |
| new THREE.MeshPhysicalMaterial({ | |
| color: 0x0b0906, metalness: 0.25, roughness: 0.68, | |
| clearcoat: 0.55, clearcoatRoughness: 0.3, | |
| }) | |
| ); | |
| dialFace.position.z = 0.30; | |
| dialGroup.add(dialFace); | |
| /* dış ince halka */ | |
| const dialRing = new THREE.Mesh(new THREE.TorusGeometry(dialR * 0.985, 0.035, 10, 128), MAT.goldPolished); | |
| dialRing.position.z = 0.34; | |
| dialGroup.add(dialRing); | |
| /* saat çizgileri (12 büyük, 60 ince) */ | |
| const tickMat = MAT.goldPolished; | |
| for (let i = 0; i < 60; i++) { | |
| const major = i % 5 === 0; | |
| const len = major ? 0.34 : 0.15; | |
| const w = major ? 0.055 : 0.022; | |
| const tick = new THREE.Mesh(new THREE.BoxGeometry(w, len, 0.03), tickMat); | |
| const a = (i / 60) * TAU; | |
| const r = dialR * 0.9; | |
| tick.position.set(Math.cos(a) * r, Math.sin(a) * r, 0.36); | |
| tick.rotation.z = a - Math.PI / 2; | |
| dialGroup.add(tick); | |
| } | |
| /* Saat rakamları — 12/3/6/9 için gerçek rakam geometrisi. | |
| Her rakam, kalınlığı sabit parçalardan (segment) kurulur; böylece | |
| yazı tipi yüklemeden, tamamen kodla okunur rakam elde ederiz. */ | |
| const numeralGroup = new THREE.Group(); | |
| numeralGroup.position.z = 0.375; | |
| dialGroup.add(numeralGroup); | |
| /* ------------------------------------------------------------ | |
| Saat rakamları — Canvas 2D ile yazı tipinden çizilip dokuya kaplanır. | |
| Segment yaklaşımı (çubuklardan rakam) bırakıldı: "12" okunmuyor, | |
| rakamlar perspektifte esniyordu. Burada rakam gerçek bir yazı | |
| tipiyle, kendi tuvaline çizilir; harici dosya/doku YOK. | |
| ------------------------------------------------------------ */ | |
| function makeNumeralSprite(label, sizeUnits, opacity = 0.95) { | |
| const S = 256; // tuval çözünürlüğü | |
| const cv = document.createElement('canvas'); | |
| cv.width = cv.height = S; | |
| const ctx = cv.getContext('2d'); | |
| ctx.clearRect(0, 0, S, S); | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| /* altın gradyan dolgu + ince koyu kontur: metal kabartma hissi */ | |
| const grad = ctx.createLinearGradient(0, S * 0.2, 0, S * 0.85); | |
| grad.addColorStop(0, '#f7e0ab'); | |
| grad.addColorStop(0.45, '#d9ae62'); | |
| grad.addColorStop(1, '#8a5f27'); | |
| ctx.fillStyle = grad; | |
| ctx.strokeStyle = 'rgba(40,24,8,0.55)'; | |
| ctx.lineWidth = S * 0.018; | |
| const twoDigits = String(label).length > 1; | |
| ctx.font = `600 ${S * (twoDigits ? 0.62 : 0.72)}px Georgia, "Times New Roman", serif`; | |
| ctx.strokeText(String(label), S / 2, S / 2); | |
| ctx.fillText(String(label), S / 2, S / 2); | |
| const tex = new THREE.CanvasTexture(cv); | |
| tex.colorSpace = THREE.SRGBColorSpace; | |
| tex.anisotropy = 8; | |
| const mat = new THREE.MeshBasicMaterial({ | |
| map: tex, transparent: true, depthWrite: false, | |
| opacity, | |
| }); | |
| const mesh = new THREE.Mesh( | |
| new THREE.PlaneGeometry(sizeUnits, sizeUnits), | |
| mat | |
| ); | |
| return mesh; | |
| } | |
| /* Tüm 12 saat işaretli — 12/3/6/9 büyük ve altın, aradakiler küçük ve sönük. | |
| Saat yönü (ekran düzlemi, +y yukarı): | |
| 12 → tepe = +π/2 · 3 → sağ = 0 · 6 → alt = -π/2 · 9 → sol = π */ | |
| const DIAL_HOURS = [ | |
| [12, 0, true], [1, 1, false], [2, 2, false], [3, 3, true], | |
| [4, 4, false], [5, 5, false], [6, 6, true], [7, 7, false], | |
| [8, 8, false], [9, 9, true], [10, 10, false], [11, 11, false], | |
| ]; | |
| DIAL_HOURS.forEach(([label, hour, major]) => { | |
| const a = Math.PI / 2 - (hour / 12) * TAU; | |
| const r = dialR * (major ? 0.745 : 0.775); | |
| const size = major ? (label > 9 ? 0.64 : 0.46) : (label > 9 ? 0.34 : 0.26); | |
| const num = makeNumeralSprite(label, size, major ? 0.95 : 0.55); | |
| num.position.set(Math.cos(a) * r, Math.sin(a) * r, 0.02); | |
| numeralGroup.add(num); | |
| }); | |
| /* marka yazısı yeri: küçük altın plaka */ | |
| const plaque = new THREE.Mesh( | |
| new THREE.CylinderGeometry(0.24, 0.24, 0.02, 32), | |
| MAT.goldPolished | |
| ); | |
| plaque.position.set(0, -dialR * 0.45, 0.37); | |
| dialGroup.add(plaque); | |
| /* --- Kollar --- */ | |
| function makeHand(length, width, tail, mat, thick = 0.055) { | |
| const shape = new THREE.Shape(); | |
| // iğ biçimli kol: tabanda geniş, uçta sivri | |
| shape.moveTo(-width, -tail); | |
| shape.lineTo(width, -tail); | |
| shape.lineTo(width * 0.55, length * 0.72); | |
| shape.lineTo(0, length); | |
| shape.lineTo(-width * 0.55, length * 0.72); | |
| shape.closePath(); | |
| const geo = new THREE.ExtrudeGeometry(shape, { | |
| depth: thick, bevelEnabled: true, bevelThickness: 0.008, bevelSize: 0.008, bevelSegments: 2, | |
| }); | |
| const hand = new THREE.Mesh(geo, mat); | |
| hand.castShadow = true; | |
| return hand; | |
| } | |
| const hourHand = makeHand(dialR * 0.50, 0.075, 0.16, MAT.goldPolished, 0.06); | |
| hourHand.position.z = 0.42; | |
| dialGroup.add(hourHand); | |
| const minuteHand = makeHand(dialR * 0.76, 0.055, 0.20, MAT.gold, 0.055); | |
| minuteHand.position.z = 0.50; | |
| dialGroup.add(minuteHand); | |
| const secondHand = makeHand(dialR * 0.84, 0.018, 0.26, MAT.ruby, 0.035); | |
| secondHand.position.z = 0.57; | |
| dialGroup.add(secondHand); | |
| /* merkez göbek */ | |
| const centerCap = new THREE.Mesh(new THREE.SphereGeometry(0.14, 24, 16), MAT.goldPolished); | |
| centerCap.position.z = 0.62; | |
| centerCap.scale.z = 0.6; | |
| dialGroup.add(centerCap); | |
| const centerGem = new THREE.Mesh(new THREE.SphereGeometry(0.06, 16, 12), MAT.ruby); | |
| centerGem.position.z = 0.68; | |
| dialGroup.add(centerGem); | |
| /* ============================================================ | |
| 8. SARKAÇ — ritmik hareket | |
| ============================================================ */ | |
| /* Sarkaç kasanın İÇİNDE ve camın arkasında durur — dışına taşmaz. */ | |
| /* DÜZELTME (kök neden): sarkaç z=-0.95'te, ana çark z=0'daydı → sarkaç | |
| dişli treninin ARKASINDA kalıyor, kadrajda gölgeye gömülüyordu. | |
| Gerçek saatte sarkaç mekanizmanın önünde, ayrı bir bölmede durur. | |
| z=+0.62: tüm çarkların önü, kadranın (z=0.92) arkası. */ | |
| const pendulumPivot = new THREE.Group(); | |
| /* Sarkaç derinliği TEK kaynaktan: animasyon döngüsü de bu sabiti kullanır. | |
| (Eskiden döngüde -1.1 sabit yazılıydı ve buradaki değeri eziyordu.) */ | |
| /* KONUM — sarkaç, dişli treninin TAMAMEN ALINDA olmalı. | |
| Dişli treni yarıçapı: en dış çark merkezi 1.83 + yarıçapı 0.66 ≈ 2.5. | |
| Sarkaç pivotu y=-1.55'te; çubuk 2.2 aşağı iner → topuz y≈-3.65. | |
| Kasa yarıçapı 5.4 olduğuna göre topuz kasanın içinde, alt boşlukta, | |
| hiçbir çarkın önünden geçmeden salınır. (Önceki denemede pivot 4.55'teyken | |
| topuz y=-3.4'e iniyordu ama çubuk çarkların önünden geçiyordu.) */ | |
| const PENDULUM_Z = 0.74; | |
| /* ★28.10: pivot ana çarkın hemen altına (-0.38R); altındaki koridorda hiçbir çark yok. */ | |
| pendulumPivot.position.set(0, -CASE_R * 0.38, PENDULUM_Z); | |
| caseGroup.add(pendulumPivot); | |
| /* Çubuk: eski hâli yarıçap 0.035 (14px'te ~1 piksel) — koyu zeminde kayboluyordu. | |
| Gerçek saat sarkaç çubuğu ince ama PARLAK çelik; ölçüyü büyüttük ve | |
| malzemeyi cilalı çeliğe çektik ki ışığı yakalayıp görünsün. */ | |
| /* Çubuk, pivotun üstündeki kısa boyunduruk + aşağı inen kol olarak kurulur. | |
| 2.1 birim: pivot -1.57'de, topuz -3.67'de → kasanın alt iç kenarına (-5.4) | |
| rahatça sığar. */ | |
| const rodLen = CASE_R * 0.40; | |
| const rod = new THREE.Mesh( | |
| new THREE.CylinderGeometry(0.055, 0.055, rodLen, 16), | |
| MAT.steel | |
| ); | |
| rod.position.y = -rodLen / 2; | |
| rod.castShadow = true; | |
| pendulumPivot.add(rod); | |
| /* Topuz: çap 0.34 → 0.46. Kasanın içinde kalır. */ | |
| const bobR = 0.46; | |
| const bob = new THREE.Mesh( | |
| new THREE.SphereGeometry(bobR, 36, 26), | |
| MAT.goldPolished | |
| ); | |
| bob.scale.set(1, 1, 0.52); | |
| bob.position.y = -rodLen + 0.10; | |
| bob.castShadow = true; | |
| pendulumPivot.add(bob); | |
| const bobRing = new THREE.Mesh(new THREE.TorusGeometry(bobR * 1.02, 0.05, 12, 56), MAT.bronze); | |
| bobRing.position.copy(bob.position); | |
| bobRing.castShadow = true; | |
| pendulumPivot.add(bobRing); | |
| const pivotPivot = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.5, 16), MAT.steel); | |
| pivotPivot.rotation.x = Math.PI / 2; | |
| pendulumPivot.add(pivotPivot); | |
| /* ============================================================ | |
| 9. POST-PROCESS — bloom + vinyet | |
| ============================================================ */ | |
| const composer = new EffectComposer(renderer); | |
| composer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| composer.setSize(window.innerWidth, window.innerHeight); | |
| composer.addPass(new RenderPass(scene, camera)); | |
| const bloom = new UnrealBloomPass( | |
| new THREE.Vector2(window.innerWidth, window.innerHeight), | |
| 0.22, // strength — kasa çerçevesi bloom'la erimesin | |
| 0.42, // radius | |
| 0.96 // threshold — yalnız en parlak noktalar taşar | |
| ); | |
| composer.addPass(bloom); | |
| const VignetteShader = { | |
| uniforms: { | |
| tDiffuse: { value: null }, | |
| offset: { value: 0.92 }, | |
| darkness: { value: 1.15 }, | |
| tint: { value: new THREE.Color(0x2a1a0c) }, | |
| tintAmount: { value: 0.12 }, | |
| }, | |
| vertexShader: /* glsl */` | |
| varying vec2 vUv; | |
| void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); } | |
| `, | |
| fragmentShader: /* glsl */` | |
| uniform sampler2D tDiffuse; | |
| uniform float offset; | |
| uniform float darkness; | |
| uniform vec3 tint; | |
| uniform float tintAmount; | |
| varying vec2 vUv; | |
| void main(){ | |
| vec4 c = texture2D(tDiffuse, vUv); | |
| vec2 uv = (vUv - 0.5) * vec2(offset); | |
| float d = dot(uv, uv); | |
| c.rgb *= 1.0 - d * darkness * 0.85; | |
| c.rgb = mix(c.rgb, tint, tintAmount * smoothstep(0.0, 0.6, d)); | |
| gl_FragColor = c; | |
| } | |
| `, | |
| }; | |
| const vignette = new ShaderPass(VignetteShader); | |
| composer.addPass(vignette); | |
| composer.addPass(new OutputPass()); | |
| /* ============================================================ | |
| 10. KAMERA KONTROLÜ | |
| ============================================================ */ | |
| const controls = new OrbitControls(camera, renderer.domElement); | |
| controls.target.copy(CAM_TARGET); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.055; | |
| controls.rotateSpeed = 0.7; | |
| controls.zoomSpeed = 0.75; | |
| controls.minDistance = 4.6; | |
| /* ★28.10: sabit 20, ev mesafesini kesip kasayı panelin altına itiyordu → ev mesafesine göre. */ | |
| controls.maxDistance = Math.max(20, CAM_HOME.length() * 1.5); | |
| controls.maxPolarAngle = Math.PI * 0.86; | |
| controls.minPolarAngle = Math.PI * 0.06; | |
| controls.enablePan = false; | |
| controls.autoRotate = false; | |
| /* ============================================================ | |
| 11. ZAMAN DURUMU | |
| ============================================================ */ | |
| const timeConfig = { | |
| speed: 1, // 1, 0.15, 0, 6 | |
| targetSpeed: 1, | |
| clockAngle: 0, // mekanizmanın toplam dönüş fazı (rad) | |
| simTime: 0, // simülasyon saati (saniye) | |
| }; | |
| /* gerçek saatten başlangıç fazı */ | |
| const now = new Date(); | |
| const startSeconds = | |
| now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + now.getMilliseconds() / 1000; | |
| /* ============================================================ | |
| 12. PATLAT / BİRLEŞTİR | |
| ============================================================ */ | |
| const explodeState = { t: 0, target: 0, active: false }; | |
| /* her dişli için merkezden dışa doğru yön */ | |
| gears.forEach((g, i) => { | |
| const dir = new THREE.Vector3(g.base.x, g.base.y, 0); | |
| if (dir.lengthSq() < 0.01) dir.set(0, 0, 1); | |
| dir.normalize(); | |
| // katmanına göre z derinliği | |
| const layerZ = g.base.z * 1.6; | |
| g.explodeDir.set(dir.x * 1.55, dir.y * 1.55, 0.35 + layerZ); | |
| }); | |
| pendulumPivot.userData.explodeDir = new THREE.Vector3(-2.4, -1.0, -1.4); | |
| dialGroup.userData.explodeDir = new THREE.Vector3(0, 0, 7.2); | |
| glassDome.userData.explodeDir = new THREE.Vector3(0, 0, 9.6); | |
| /* ============================================================ | |
| 13. ARAYÜZ BAĞLANTISI | |
| ============================================================ */ | |
| const ui = { | |
| segBtns: [...document.querySelectorAll('.seg-btn[data-speed]')], | |
| explodeBtn: document.getElementById('explode-btn'), | |
| explodeTxt: document.getElementById('explode-txt'), | |
| viewMech: document.getElementById('view-mech'), | |
| viewDial: document.getElementById('view-dial'), | |
| resetCam: document.getElementById('reset-cam'), | |
| tourBtn: document.getElementById('tour-btn'), | |
| hh: document.getElementById('clock-hh'), | |
| mm: document.getElementById('clock-mm'), | |
| ss: document.getElementById('clock-ss'), | |
| mode: document.getElementById('clock-mode'), | |
| loader: document.getElementById('loader'), | |
| fill: document.getElementById('loader-fill'), | |
| hint: document.getElementById('loader-hint'), | |
| }; | |
| function setSpeed(v, btn) { | |
| timeConfig.targetSpeed = v; | |
| ui.segBtns.forEach(b => { | |
| const on = b === btn; | |
| b.classList.toggle('is-active', on); | |
| b.setAttribute('aria-pressed', String(on)); | |
| }); | |
| ui.mode.textContent = | |
| v === 0 ? 'zaman durdu' : | |
| v === 0.15 ? 'ağır çekim' : | |
| v === 6 ? 'hızlandırılmış' : 'gerçek zaman'; | |
| } | |
| ui.segBtns.forEach(btn => { | |
| btn.addEventListener('click', () => setSpeed(Number(btn.dataset.speed), btn)); | |
| }); | |
| function toggleExplode() { | |
| explodeState.active = !explodeState.active; | |
| explodeState.target = explodeState.active ? 1 : 0; | |
| ui.explodeBtn.classList.toggle('is-active', explodeState.active); | |
| ui.explodeBtn.setAttribute('aria-pressed', String(explodeState.active)); | |
| ui.explodeTxt.textContent = explodeState.active ? 'Birleştir' : 'Parçalara Ayır'; | |
| /* Parçalanmış sahne kadraja sığmaz: kamera mekanizma mesafesine çekilir. | |
| Kadran modundaysak ve kullanıcı parçalıyorsa kadran görünür kalır — | |
| o durumda da geniş açı gerekir. */ | |
| const fitDist = sigmaMesafesi(CASE_R * 1.25, 1.05); // parçalar kasanın ~1.2R ötesine açılır | |
| flyCamera( | |
| new THREE.Vector3(CAM_HOME.x, CAM_HOME.y + 0.3, explodeState.active ? fitDist : CAM_HOME.z), | |
| CAM_TARGET.clone(), | |
| 1.1 | |
| ); | |
| } | |
| ui.explodeBtn.addEventListener('click', toggleExplode); | |
| let viewMode = 'mech'; | |
| function setView(mode) { | |
| viewMode = mode; | |
| const isDial = mode === 'dial'; | |
| ui.viewMech.classList.toggle('is-active', !isDial); | |
| ui.viewDial.classList.toggle('is-active', isDial); | |
| ui.viewMech.setAttribute('aria-pressed', String(!isDial)); | |
| ui.viewDial.setAttribute('aria-pressed', String(isDial)); | |
| // kadran modunda sarkaç geriye çekilir, kamera kadrana döner | |
| if (isDial) { | |
| /* Kadran: tüm yüzü görecek mesafe — kasa yarıçapından türetilir, | |
| sabit sayı vermiyoruz ki mercek/kasa değişince kadraj kaymasın. | |
| mesafe = (yarıçap / tan(fov/2)) * 1.18 pay */ | |
| const dist = sigmaMesafesi(CASE_R, 1.18); | |
| flyCamera(new THREE.Vector3(0, 0, dist), new THREE.Vector3(0, 0, 0.6)); | |
| bloom.strength = 0.20; | |
| /* Dolgu ışığı kadranın önüne düşüp sol altta turuncu leke bırakıyordu. */ | |
| fillLight.intensity = 6; | |
| } else { | |
| flyCamera(CAM_HOME.clone(), CAM_TARGET.clone()); | |
| bloom.strength = 0.22; | |
| fillLight.intensity = 26; | |
| } | |
| } | |
| ui.viewMech.addEventListener('click', () => setView('mech')); | |
| ui.viewDial.addEventListener('click', () => setView('dial')); | |
| /* yumuşak kamera geçişi */ | |
| const camAnim = { active: false, t: 0, fromPos: new THREE.Vector3(), toPos: new THREE.Vector3(), fromTgt: new THREE.Vector3(), toTgt: new THREE.Vector3() }; | |
| function flyCamera(toPos, toTgt, dur = 1.4) { | |
| camAnim.active = true; | |
| camAnim.t = 0; | |
| camAnim.dur = dur; | |
| camAnim.fromPos.copy(camera.position); | |
| camAnim.toPos.copy(toPos); | |
| camAnim.fromTgt.copy(controls.target); | |
| camAnim.toTgt.copy(toTgt); | |
| } | |
| ui.resetCam.addEventListener('click', () => { | |
| tour.active = false; | |
| flyCamera(CAM_HOME.clone(), CAM_TARGET.clone(), 1.2); | |
| }); | |
| /* sinematik tur */ | |
| const tour = { active: false, t: 0 }; | |
| ui.tourBtn.addEventListener('click', () => { | |
| tour.active = !tour.active; | |
| tour.t = 0; | |
| ui.tourBtn.classList.toggle('is-active', tour.active); | |
| }); | |
| /* klavye */ | |
| window.addEventListener('keydown', (e) => { | |
| if (e.code === 'Space') { | |
| e.preventDefault(); | |
| const stopped = timeConfig.targetSpeed === 0; | |
| setSpeed(stopped ? 1 : 0, ui.segBtns.find(b => Number(b.dataset.speed) === (stopped ? 1 : 0))); | |
| } else if (e.key === 'e' || e.key === 'E') { | |
| toggleExplode(); | |
| } else if (e.key === 'r' || e.key === 'R') { | |
| ui.resetCam.click(); | |
| } else if (e.key === '1') { | |
| setSpeed(1, ui.segBtns.find(b => Number(b.dataset.speed) === 1)); | |
| } else if (e.key === '2') { | |
| setSpeed(0.15, ui.segBtns.find(b => Number(b.dataset.speed) === 0.15)); | |
| } else if (e.key === '3') { | |
| setSpeed(6, ui.segBtns.find(b => Number(b.dataset.speed) === 6)); | |
| } | |
| }); | |
| /* ============================================================ | |
| 14. ANİMASYON DÖNGÜSÜ | |
| ============================================================ */ | |
| const clock = new THREE.Clock(); | |
| /* patlatma sırasında orijinal konumları koru */ | |
| const originalZ = gears.map(g => g.base.z); | |
| function updateGears(dt) { | |
| // yumuşak hız geçişi | |
| timeConfig.speed = lerp(timeConfig.speed, timeConfig.targetSpeed, 1 - Math.pow(0.001, dt)); | |
| timeConfig.simTime += dt * timeConfig.speed; | |
| const secs = startSeconds + timeConfig.simTime; | |
| /* Dişliler: faz + hız * simülasyon zamanı. | |
| Grup dönmez (mil/montaj sabit kalır), yalnız çark gövdesi döner. */ | |
| gears.forEach((g) => { | |
| g.mesh.rotation.z = g.phase + g.omega * timeConfig.simTime; | |
| }); | |
| // kadran kolları — gerçek zaman | |
| const hAngle = ((secs / 43200) % 1) * TAU; | |
| const mAngle = ((secs / 3600) % 1) * TAU; | |
| const sAngle = ((secs / 60) % 1) * TAU; | |
| /* Kol şekli +Y yönüne (12) uzanıyor; saat yönü için negatif. | |
| ★28.10: eskiden "+ Math.PI / 2" ekleniyordu → kollar 90° (3 saat / 15 dk) geride; dijital 00:39 iken | |
| akrep 9'da, yelkovan 24. dakikadaydı. Rakamlar 12 tepede dizildiği için kaydırma gerekmez. */ | |
| hourHand.rotation.z = -hAngle; | |
| minuteHand.rotation.z = -mAngle; | |
| secondHand.rotation.z = -sAngle; | |
| /* SARKAÇ — KAÇIŞ ÇARKINDAN TÜRETİLİR (bağımsız sinüs değil). | |
| Gerçek saatte sarkaç kaçış çarkını kilitler; ikisi aynı fazdan beslenir. | |
| Kaçış çarkı bir tam diş attığında sarkaç bir yarım salınım yapar. | |
| Böylece mekanizma ile sarkaç gözle de aynı ritimde görünür. */ | |
| const escGear = gears[escapeWheel]; | |
| const escPhase = escGear.mesh.rotation.z; | |
| /* Kaçış çarkının açısal konumu doğrudan salınım açısına çevrilir: | |
| diş sayısı kadar adım → tam salınım. Genlik 22° (gerçek sarkaç ~±11° | |
| ama görsel okunabilirlik için abartıldı). | |
| ★28.10: çapa (anchor) kaçışında bir tam salınımda bir diş geçer → çarpan | |
| teeth. Kaçış çarkı artık gerçek oranla döndüğü için periyot ≈ 1,6 sn. */ | |
| const swing = Math.sin(escPhase * escGear.teeth) * 22 * DEG; | |
| pendulumPivot.rotation.z = swing; | |
| // dijital okuma | |
| const hh = Math.floor((secs / 3600) % 24); | |
| const mm = Math.floor((secs / 60) % 60); | |
| const ss = Math.floor(secs % 60); | |
| ui.hh.textContent = String(hh).padStart(2, '0'); | |
| ui.mm.textContent = String(mm).padStart(2, '0'); | |
| ui.ss.textContent = String(ss).padStart(2, '0'); | |
| } | |
| function updateExplode(dt) { | |
| if (Math.abs(explodeState.t - explodeState.target) < 0.0005) { | |
| explodeState.t = explodeState.target; | |
| } else { | |
| explodeState.t = lerp(explodeState.t, explodeState.target, 1 - Math.pow(0.0009, dt * 1.6)); | |
| } | |
| const t = easeInOut(clamp(explodeState.t, 0, 1)); | |
| gears.forEach((g, i) => { | |
| g.group.position.x = g.base.x + g.explodeDir.x * t; | |
| g.group.position.y = g.base.y + g.explodeDir.y * t; | |
| g.group.position.z = originalZ[i] + g.explodeDir.z * t; | |
| }); | |
| // NUMEX-DÜZELTME (2026-10-27): Her karede sarkaç z'si -1.1'e sabitleniyordu; kurulumda verilen 0.74 değeri eziliyor ve sarkaç görünmüyordu. | |
| /* DÜZELTME (kök neden): bu satır her karede sarkacın z'sini -1.1'e | |
| sabitliyordu — kurulumda verilen görünür derinlik (0.74) ilk karede | |
| eziliyor, sarkaç dişli treninin en arkasına düşüyordu (ekranda yok). | |
| Artık kurulumdaki z korunuyor, yalnız patlatma ofseti ekleniyor. */ | |
| pendulumPivot.position.z = PENDULUM_Z + pendulumPivot.userData.explodeDir.z * t; | |
| pendulumPivot.position.x = pendulumPivot.userData.explodeDir.x * t; | |
| dialGroup.position.z = 0.92 + dialGroup.userData.explodeDir.z * t; | |
| glassDome.position.z = 0.98 + glassDome.userData.explodeDir.z * t; | |
| glassDome.scale.setScalar(1 + t * 0.06); | |
| /* Ayrıkken çarklar hafifçe kendi eksenlerinde yatar (canlı görünür). */ | |
| gears.forEach((g) => { | |
| g.mesh.rotation.y = t * 0.22; | |
| }); | |
| /* Kadran yalnızca kadran modunda görünür. Mekanizma modunda | |
| parçalarken kadran ekranı kapatmasın — yoksa kullanıcı çarkları | |
| kolların arkasından izlemek zorunda kalır. | |
| Kadran modunda ise dişli tarlası tamamen gizlenir: çarklar kadran | |
| yüzünü kirletiyor, rakamların okunurluğunu düşürüyordu. */ | |
| const dialMode = viewMode === 'dial'; | |
| dialGroup.visible = dialMode; | |
| gearField.visible = !dialMode; | |
| pendulumPivot.visible = !dialMode; // sarkaç topuzu kadranın önüne sızıyordu | |
| gearField.position.z = -0.15; | |
| } | |
| function updateCamera(dt) { | |
| /* ★28.10: sis yoğunluğu sabit 0.028 iken kamera 20 biriminden uzaklaşınca (dar ekran, parçalanmış görünüm) | |
| sahne karanlığa gömülüyordu — yoğunluk kamera uzaklığıyla ölçeklenir, 20 birime kadar eskisiyle aynı. */ | |
| scene.fog.density = 0.028 * Math.min(1, 20 / camera.position.distanceTo(controls.target)); | |
| if (camAnim.active) { | |
| camAnim.t += dt / camAnim.dur; | |
| const t = easeInOut(clamp(camAnim.t, 0, 1)); | |
| camera.position.lerpVectors(camAnim.fromPos, camAnim.toPos, t); | |
| controls.target.lerpVectors(camAnim.fromTgt, camAnim.toTgt, t); | |
| if (camAnim.t >= 1) camAnim.active = false; | |
| } | |
| if (tour.active && !camAnim.active) { | |
| tour.t += dt * 0.18; | |
| const r = 13.0 + Math.sin(tour.t * 0.7) * 2.2; | |
| const a = tour.t; | |
| camera.position.set( | |
| Math.cos(a) * r, | |
| 1.6 + Math.sin(tour.t * 0.9) * 2.6, | |
| Math.sin(a) * r | |
| ); | |
| controls.target.lerp(new THREE.Vector3(0, 0.2, 0), 0.05); | |
| } | |
| controls.update(); | |
| } | |
| function animate() { | |
| const dt = Math.min(clock.getDelta(), 0.05); | |
| updateGears(dt); | |
| updateExplode(dt); | |
| updateCamera(dt); | |
| composer.render(); | |
| requestAnimationFrame(animate); | |
| } | |
| /* ============================================================ | |
| 15. BOYUTLANDIRMA | |
| ============================================================ */ | |
| function onResize() { | |
| const w = window.innerWidth, h = window.innerHeight; | |
| camera.aspect = w / h; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(w, h); | |
| composer.setSize(w, h); | |
| /* Ekran oranı değişti → ev kamerası mesafesi yeniden hesaplanır | |
| (dikey ekranda kasa kesilmesin). Kullanıcı sahneyi döndürdüyse | |
| kamerasına dokunulmaz, yalnız referans noktası güncellenir. */ | |
| const near = camera.position.distanceTo(controls.target); | |
| const oldDist = CAM_HOME.length(); | |
| resetCameraHome(false); | |
| const newDist = CAM_HOME.length(); | |
| controls.maxDistance = Math.max(20, newDist * 1.5); | |
| if (Math.abs(near - oldDist) < 0.6 && !camAnim.active) { | |
| const dir = camera.position.clone().sub(controls.target).normalize(); | |
| camera.position.copy(controls.target).addScaledVector(dir, newDist); | |
| } | |
| } | |
| window.addEventListener('resize', onResize); | |
| /* ============================================================ | |
| 16. YÜKLEYİCİ | |
| ============================================================ */ | |
| function buildLoaderGears() { | |
| const g1 = document.getElementById('lg1'); | |
| const g2 = document.getElementById('lg2'); | |
| const gearPath = (teeth, r) => { | |
| let d = ''; | |
| for (let i = 0; i < teeth; i++) { | |
| const a = (i / teeth) * TAU; | |
| const step = TAU / teeth; | |
| const pts = [ | |
| [a - step * 0.3, r * 0.86], [a - step * 0.16, r], [a + step * 0.16, r], [a + step * 0.3, r * 0.86], | |
| ]; | |
| pts.forEach(([ang, rad], k) => { | |
| const x = 50 + Math.cos(ang) * rad; | |
| const y = 50 + Math.sin(ang) * rad; | |
| d += (i === 0 && k === 0 ? 'M' : 'L') + x.toFixed(2) + ' ' + y.toFixed(2) + ' '; | |
| }); | |
| } | |
| return d + 'Z'; | |
| }; | |
| if (g1) g1.innerHTML = `<path d="${gearPath(12, 42)}" fill-rule="evenodd"/><circle cx="50" cy="50" r="12" fill="#0a0805"/>`; | |
| if (g2) g2.innerHTML = `<path d="${gearPath(9, 40)}" fill-rule="evenodd"/><circle cx="50" cy="50" r="11" fill="#0a0805"/>`; | |
| } | |
| const loaderSteps = [ | |
| [10, 'Mekanizma kuruluyor…'], | |
| [35, 'Dişliler birbirine geçiriliyor…'], | |
| [60, 'Kadran ve kollar yerleştiriliyor…'], | |
| [82, 'Işıklar ayarlanıyor…'], | |
| [100, 'Hazır'], | |
| ]; | |
| function runLoader(onDone) { | |
| let i = 0; | |
| const tick = () => { | |
| if (i >= loaderSteps.length) { | |
| setTimeout(() => { | |
| ui.loader.classList.add('is-done'); | |
| onDone(); | |
| }, 380); | |
| return; | |
| } | |
| const [pct, txt] = loaderSteps[i++]; | |
| ui.fill.style.width = pct + '%'; | |
| ui.hint.textContent = txt; | |
| setTimeout(tick, 240); | |
| }; | |
| tick(); | |
| } | |
| /* ============================================================ | |
| 17. BAŞLAT | |
| ============================================================ */ | |
| buildLoaderGears(); | |
| animate(); | |
| runLoader(() => { | |
| // girişte sinematik yaklaşma: uzaktan sahneye | |
| camera.position.set(0, 4.0, 24); | |
| flyCamera(CAM_HOME.clone(), CAM_TARGET.clone(), 2.6); | |
| }); | |
| /* vitrin bilgisi (Market meta) */ | |
| window.NUMEX_CLOCKWORK = { | |
| name: 'NUMEX CLOCKWORK', | |
| slogan: 'Zamanın İçini Gör', | |
| gears: gears.length, | |
| version: '1.0.0', | |
| ready: true, | |
| }; | |