1807 lines
72 KiB
HTML
1807 lines
72 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="tr">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Gökbey-9 · Fırlatma Rampası 3B</title>
|
||
<!-- NUMEX-DÜZELTME (2026-10-22): Tarayıcı /favicon.ico istiyordu ve sunucu 404 dönüyordu. İstek hiç doğmasın diye satır içi SVG favicon eklendi — harici dosya gerekmez, 404 ortadan kalkar. -->
|
||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%230a1018'/%3E%3Cpath d='M16 4c3.2 4.2 4.8 8.4 4.8 13.6 0 3.2-1 6.4-1 6.4h-7.6s-1-3.2-1-6.4C11.2 12.4 12.8 8.2 16 4z' fill='%2364c8ff'/%3E%3Cpath d='M11.6 24h8.8l-1.6 4.4h-5.6z' fill='%23f0a06a'/%3E%3C/svg%3E" />
|
||
<style>
|
||
:root{
|
||
--cam: rgba(10, 16, 26, .55);
|
||
--cam-brd: rgba(150, 190, 230, .18);
|
||
--txt: #e8eef6;
|
||
--dim: #93a4b8;
|
||
--acc: #64c8ff;
|
||
}
|
||
*{ box-sizing: border-box; margin: 0; padding: 0; }
|
||
html, body{ height: 100%; overflow: hidden; background: #0a1018; }
|
||
body{
|
||
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
||
color: var(--txt);
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
#sahne{ position: fixed; inset: 0; width: 100%; height: 100%; display: block; cursor: grab; }
|
||
#sahne:active{ cursor: grabbing; }
|
||
|
||
.katman{ position: fixed; z-index: 10; pointer-events: none; }
|
||
.panel{
|
||
background: var(--cam);
|
||
border: 1px solid var(--cam-brd);
|
||
border-radius: 10px;
|
||
backdrop-filter: blur(10px) saturate(1.2);
|
||
-webkit-backdrop-filter: blur(10px) saturate(1.2);
|
||
pointer-events: auto;
|
||
}
|
||
|
||
/* ---------- Sol üst başlık ---------- */
|
||
#baslik{ top: 18px; left: 18px; padding: 14px 18px; }
|
||
#baslik h1{
|
||
font-size: clamp(16px, 1.6vw, 22px);
|
||
font-weight: 600;
|
||
letter-spacing: .5px;
|
||
display: flex; align-items: center; gap: 9px;
|
||
}
|
||
#baslik h1 span.nokta{
|
||
width: 7px; height: 7px; border-radius: 50%;
|
||
background: var(--acc);
|
||
box-shadow: 0 0 10px var(--acc);
|
||
animation: nefes 2.4s ease-in-out infinite;
|
||
}
|
||
@keyframes nefes{ 0%,100%{ opacity:.45; transform: scale(.85);} 50%{ opacity:1; transform: scale(1);} }
|
||
#baslik .alt{
|
||
margin-top: 5px;
|
||
font-size: 11px;
|
||
color: var(--dim);
|
||
letter-spacing: 1.6px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* ---------- Sekmeler (Genel bakış / Yapı / Fırlatma) ---------- */
|
||
#sekmeler{
|
||
top: 18px; left: 50%; transform: translateX(-50%);
|
||
display: flex; gap: 4px; padding: 5px; z-index: 12;
|
||
}
|
||
.sekme{
|
||
font-family: inherit; font-size: 12px; letter-spacing: .6px;
|
||
color: var(--dim); background: none; border: none; border-radius: 6px;
|
||
padding: 8px 18px; cursor: pointer; transition: .2s; white-space: nowrap;
|
||
}
|
||
.sekme:hover{ color: var(--txt); background: rgba(255,255,255,.06); }
|
||
.sekme.aktif{ color: #04121c; background: var(--acc); font-weight: 700; }
|
||
|
||
/* Ölçü katmanı */
|
||
#olcuKatman{ position: fixed; inset: 0; pointer-events: none; z-index: 6; }
|
||
.olcuCizgi{ position: absolute; background: rgba(100,200,255,.55); }
|
||
.olcuCizgi.dikey{ width: 1px; }
|
||
.olcuCizgi.yatay{ height: 1px; }
|
||
.olcuUc{ position: absolute; width: 7px; height: 7px; border: 1px solid rgba(100,200,255,.8);
|
||
background: rgba(10,16,26,.8); border-radius: 50%; transform: translate(-50%,-50%); }
|
||
.olcuEtiket{
|
||
position: absolute; transform: translate(-50%,-50%);
|
||
font-size: 11px; font-variant-numeric: tabular-nums; letter-spacing: .5px;
|
||
color: #cfe6fb; background: rgba(10,16,26,.82); border: 1px solid rgba(100,200,255,.35);
|
||
border-radius: 4px; padding: 3px 8px; white-space: nowrap;
|
||
}
|
||
.olcuNot{
|
||
position: absolute; bottom: 96px; left: 50%; transform: translateX(-50%);
|
||
font-size: 10px; letter-spacing: 1.2px; color: rgba(147,164,184,.75);
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* Fırlatma paneli */
|
||
#firlatmaPanel{ bottom: 96px; left: 50%; transform: translateX(-50%);
|
||
padding: 12px 20px; display: none; text-align: center; min-width: 260px; }
|
||
#firlatmaPanel.acik{ display: block; }
|
||
#geriSayim{
|
||
font-size: 40px; font-weight: 200; letter-spacing: 3px;
|
||
font-variant-numeric: tabular-nums; line-height: 1.1;
|
||
color: var(--acc); text-shadow: 0 0 22px rgba(100,200,255,.5);
|
||
}
|
||
#asamaAd{ font-size: 11px; letter-spacing: 2px; color: var(--dim); text-transform: uppercase; margin-top: 4px; }
|
||
|
||
/* ---------- Sağ üst performans (yalnız geliştirici kipi) ---------- */
|
||
#perf{ top: 18px; right: 18px; padding: 11px 14px; min-width: 176px;
|
||
display: none; }
|
||
#perf.acik{ display: block; }
|
||
#perf .satir{
|
||
display: flex; justify-content: space-between; align-items: baseline;
|
||
font-size: 11px; color: var(--dim); letter-spacing: .6px;
|
||
padding: 3px 0;
|
||
}
|
||
#perf .satir b{
|
||
color: var(--txt); font-weight: 600;
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: 12px;
|
||
}
|
||
#perf .dil{
|
||
margin-top: 8px; display: flex; gap: 6px;
|
||
border-top: 1px solid var(--cam-brd); padding-top: 9px;
|
||
}
|
||
.dugme{
|
||
font-family: inherit; font-size: 11px;
|
||
color: var(--dim); background: rgba(255,255,255,.05);
|
||
border: 1px solid var(--cam-brd); border-radius: 6px;
|
||
padding: 6px 10px; cursor: pointer;
|
||
transition: .18s; letter-spacing: .5px;
|
||
}
|
||
.dugme:hover{ color: var(--txt); background: rgba(100,200,255,.14); border-color: rgba(100,200,255,.4); }
|
||
.dugme.aktif{ color: #04121c; background: var(--acc); border-color: var(--acc); font-weight: 700; }
|
||
|
||
/* ---------- Alt orta zaman çubuğu ---------- */
|
||
#zaman{
|
||
bottom: 22px; left: 50%; transform: translateX(-50%);
|
||
padding: 12px 18px; width: min(520px, calc(100vw - 36px));
|
||
}
|
||
#zamanBasliklar{
|
||
display: flex; justify-content: space-between;
|
||
font-size: 11px; color: var(--dim); letter-spacing: .8px;
|
||
margin-bottom: 9px;
|
||
}
|
||
#zamanBasliklar span{ transition: color .2s; }
|
||
#zamanBasliklar span.secili{ color: var(--acc); font-weight: 700; }
|
||
#kaydirici{
|
||
-webkit-appearance: none; appearance: none;
|
||
width: 100%; height: 4px; border-radius: 4px;
|
||
background: linear-gradient(90deg,#f0a06a 0%, #8fc7ee 33%, #e8894f 66%, #1b2540 100%);
|
||
outline: none; cursor: pointer;
|
||
}
|
||
#kaydirici::-webkit-slider-thumb{
|
||
-webkit-appearance: none; appearance: none;
|
||
width: 16px; height: 16px; border-radius: 50%;
|
||
background: #fff; border: 2px solid var(--acc);
|
||
box-shadow: 0 0 12px rgba(100,200,255,.7);
|
||
cursor: pointer;
|
||
}
|
||
#kaydirici::-moz-range-thumb{
|
||
width: 16px; height: 16px; border: 2px solid var(--acc);
|
||
border-radius: 50%; background: #fff;
|
||
box-shadow: 0 0 12px rgba(100,200,255,.7); cursor: pointer;
|
||
}
|
||
|
||
/* ---------- İpucu ---------- */
|
||
#ipucu{
|
||
bottom: 24px; left: 18px;
|
||
font-size: 11px; color: rgba(200,215,235,.5);
|
||
letter-spacing: .3px; max-width: 210px; line-height: 1.6;
|
||
}
|
||
|
||
/* ---------- Parça bilgi kartı ---------- */
|
||
#kart{
|
||
top: 50%; right: 18px; transform: translateY(-50%) translateX(calc(100% + 30px));
|
||
width: min(310px, calc(100vw - 36px));
|
||
padding: 18px 20px;
|
||
transition: transform .35s cubic-bezier(.22,.9,.3,1);
|
||
}
|
||
#kart.acik{ transform: translateY(-50%) translateX(0); }
|
||
#kart h2{ font-size: 15px; font-weight: 600; margin-bottom: 4px; }
|
||
#kart .etiket{
|
||
font-size: 10px; color: var(--acc); letter-spacing: 2px;
|
||
text-transform: uppercase; margin-bottom: 10px; display: block;
|
||
}
|
||
#kart p{ font-size: 12.5px; line-height: 1.75; color: #c3d0e0; }
|
||
#kart .kapat{
|
||
position: absolute; top: 10px; right: 12px;
|
||
background: none; border: none; color: var(--dim);
|
||
font-size: 18px; cursor: pointer; line-height: 1; padding: 4px;
|
||
}
|
||
#kart .kapat:hover{ color: var(--txt); }
|
||
#kart .olcu{
|
||
margin-top: 14px; padding-top: 12px;
|
||
border-top: 1px solid var(--cam-brd);
|
||
font-size: 11px; color: var(--dim); display: flex; justify-content: space-between;
|
||
}
|
||
|
||
/* ---------- Yükleniyor ---------- */
|
||
#yukleniyor{
|
||
position: fixed; inset: 0; z-index: 100;
|
||
background: #0a1018;
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||
gap: 16px; transition: opacity .6s;
|
||
}
|
||
#yukleniyor.bitti{ opacity: 0; pointer-events: none; }
|
||
#yukleniyor .cizgi{
|
||
width: 160px; height: 2px; background: rgba(255,255,255,.1); overflow: hidden; border-radius: 2px;
|
||
}
|
||
#yukleniyor .cizgi::after{
|
||
content: ""; display: block; width: 40%; height: 100%;
|
||
background: var(--acc); animation: akip 1.1s ease-in-out infinite;
|
||
}
|
||
@keyframes akip{ 0%{ transform: translateX(-100%);} 100%{ transform: translateX(350%);} }
|
||
#yukleniyor span{ font-size: 12px; color: var(--dim); letter-spacing: 2px; }
|
||
|
||
@media (max-width: 760px){
|
||
#ipucu{ display: none; }
|
||
#kart{ top: auto; bottom: 108px; right: 12px; transform: translateX(calc(100% + 30px)); }
|
||
#kart.acik{ transform: translateX(0); }
|
||
#perf{ padding: 8px 10px; min-width: 140px; }
|
||
#baslik{ padding: 10px 12px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<canvas id="sahne"></canvas>
|
||
|
||
<div id="yukleniyor">
|
||
<span>SAHNE HAZIRLANIYOR</span>
|
||
<div class="cizgi"></div>
|
||
</div>
|
||
|
||
<div id="baslik" class="katman panel">
|
||
<h1><span class="nokta"></span>Gökbey-9</h1>
|
||
<div class="alt">Fırlatma Rampası · Canlı Sahne</div>
|
||
</div>
|
||
|
||
<div id="sekmeler" class="katman panel">
|
||
<button class="sekme aktif" data-bolum="genel">Genel Bakış</button>
|
||
<button class="sekme" data-bolum="yapi">Yapı</button>
|
||
<button class="sekme" data-bolum="firlatma">Fırlatma</button>
|
||
</div>
|
||
|
||
<div id="perf" class="katman panel">
|
||
<div class="satir"><span>FPS</span><b id="pFps">--</b></div>
|
||
<div class="satir"><span>Üçgen</span><b id="pUcgen">--</b></div>
|
||
<div class="satir"><span>Çizim çağrısı</span><b id="pCizim">--</b></div>
|
||
<div class="dil">
|
||
<button class="dugme" id="btnSifirla">Kamerayı Sıfırla</button>
|
||
<button class="dugme" id="btnParalel">Kamera: Paralel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ölçü çizgileri katmanı (Yapı bölümünde görünür) -->
|
||
<div id="olcuKatman"></div>
|
||
|
||
<!-- Fırlatma bölümü paneli -->
|
||
<div id="firlatmaPanel" class="katman panel">
|
||
<div id="geriSayim">T-10</div>
|
||
<div id="asamaAd">Hazırlık</div>
|
||
</div>
|
||
|
||
<div id="zaman" class="katman panel">
|
||
<div id="zamanBasliklar">
|
||
<span data-t="0">Şafak</span>
|
||
<span data-t="1">Öğle</span>
|
||
<span data-t="2">Alacakaranlık</span>
|
||
<span data-t="3">Gece</span>
|
||
</div>
|
||
<input type="range" id="kaydirici" min="0" max="1000" value="0" />
|
||
</div>
|
||
|
||
<div id="ipucu" class="katman">
|
||
Sürükle: döndür<br />
|
||
Tekerlek: yakınlaştır<br />
|
||
Parçaya tıkla: bilgi
|
||
</div>
|
||
|
||
<div id="kart" class="katman panel">
|
||
<button class="kapat" id="kartKapat">×</button>
|
||
<span class="etiket">Parça</span>
|
||
<h2 id="kartBaslik">—</h2>
|
||
<p id="kartMetin">—</p>
|
||
<div class="olcu"><span id="kartYukseklik"></span><span id="kartAgirlik"></span></div>
|
||
</div>
|
||
|
||
<script type="importmap">
|
||
{
|
||
"imports": {
|
||
"three": "./vendor/three.module.js"
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<script type="module">
|
||
import * as THREE from 'three';
|
||
|
||
/* =========================================================
|
||
GÖKBEY-9 · FIRLATMA RAMPASI
|
||
Tek dosya, sıfır harici model. Tüm geometri kodla üretilir.
|
||
========================================================= */
|
||
|
||
const tuval = document.getElementById('sahne');
|
||
const pFps = document.getElementById('pFps');
|
||
const pUcgen = document.getElementById('pUcgen');
|
||
const pCizim = document.getElementById('pCizim');
|
||
const btnSifirla = document.getElementById('btnSifirla');
|
||
const btnParalel = document.getElementById('btnParalel');
|
||
const sahne = new THREE.Scene();
|
||
|
||
const kamera = new THREE.PerspectiveCamera(48, innerWidth / innerHeight, 0.5, 4000);
|
||
const KAMERA_BASLANGIC = new THREE.Vector3(72, 44, 78);
|
||
kamera.position.copy(KAMERA_BASLANGIC);
|
||
|
||
const cizici = new THREE.WebGLRenderer({ canvas: tuval, antialias: true, powerPreference: 'high-performance' });
|
||
// NUMEX-DÜZELTME (2026-10-22): Tuval ekranın tamamını kaplamıyordu: setSize innerWidth/innerHeight ile bir kez ayarlanıyor, ama #sahne CSS'te inset:0 ile konumlandığı için gerçek ölçü CSS kutusundan gelmeli. Boyutu elemanın kendi ölçüsünden alıyorum.
|
||
// NUMEX-DÜZELTME (2026-10-22): Kök neden: getBoundingClientRect() ile ölçüp setSize(...,false) çağırmak kısır döngü yarattı — canvas henüz boyutlanmadığı için CSS kutusu 300×150 dönüyor, setSize CSS'i güncellemediği için de öyle kalıyordu; sahne 300×150'ye çöküyordu. Pencere ölçüsüne dönüldü, CSS de güncelleniyor.
|
||
// Tuval boyutu: pencere ölçüsü (CSS de güncellenir — updateStyle varsayılan true)
|
||
function tuvalBoyutla(){
|
||
cizici.setSize(Math.max(1, window.innerWidth), Math.max(1, window.innerHeight));
|
||
}
|
||
tuvalBoyutla();
|
||
cizici.setPixelRatio(1); // 2 yerine 1: retina olmayan ekranlarda piksel maliyeti 4 kat azalır
|
||
cizici.shadowMap.enabled = true;
|
||
cizici.shadowMap.type = THREE.PCFSoftShadowMap; // blok gölge yerine yumuşak kenar
|
||
cizici.toneMapping = THREE.ACESFilmicToneMapping;
|
||
cizici.toneMappingExposure = 1.0;
|
||
|
||
/* ---------------- Yörünge kontrolü (bağımlılıksız) ---------------- */
|
||
// NUMEX-DÜZELTME (2026-10-22): Referans görselde kamera alçak ve ufka yakın bakıyor (ufuk ekranın ortasında, kompleks silüet halinde). Önceki açı kuşbakışıydı; açıyı düşürüp hedefi yukarı aldım.
|
||
// NUMEX-DÜZELTME (2026-10-22): Açılışta kompleks ekranın ~%25'ini kaplıyordu (istek: roket ekranın yarısından fazlası). Mesafeyi 205'ten 118'e çekip hedefi roketin orta yüksekliğine (y=33) aldım; roket dikey karede ~%62 doluluk veriyor.
|
||
// NUMEX-DÜZELTME (2026-10-22): Roket ekranın ~%30'unda kalıyordu; kule, küreler ve paratonerler kadrajı dolduruyordu. Referans açıyı koruyup mesafeyi 118→72'ye çekip hedefi roketin gövde ortasına (y=36) aldım; roket dikey karede ~%62 doluluk veriyor.
|
||
// NUMEX-DÜZELTME (2026-10-22): 72 birimde roketin burnu ve nozulları kadraj dışına taşıyordu; paratoner direkleri ön planda ekranı ikiye bölüyordu. Mesafeyi 96'ya, dik bakış açısını 0.10'a aldım; roketin tamamı (y=3.6..60.4) görünür ve dikeyde ~%58 doluluk verir. Yatayda da kule ve çevre sığar.
|
||
// NUMEX-DÜZELTME (2026-10-22): Roket ekranın ~%35'inde kalıyordu ve ön plandaki paratoner direkleri kadrajı ikiye bölüyordu. Açıyı yandan (0.72) daha öne (1.05) alıp mesafeyi 96→78'e indirdim; direkler artık roketin önünden geçmiyor, roket dikeyde ~%62 doluluk veriyor.
|
||
// NUMEX-DÜZELTME (2026-10-22): Paratoner direkleri 45/135/225/315 derecede, 40 birim yarıçapta duruyor; kamera her açıda bir direğin arkasına düşüp roketi kesiyordu. Kamera açısını 0.72 rad (~41°) yerine 1.571 rad (90°, direk boşluğu) aldım — roketin önünden direk geçmez.
|
||
// Paratonerler 45/135/225/315°'de durur; kamera 90°'de (direk boşluğu) konumlanır.
|
||
// NUMEX-DÜZELTME (2026-10-22): 84 birimde kamera çok yaklaşmış: ön plandaki yakıt küresi kadrajın dörtte birini kaplıyor ve kule rokete yapışık görünüyordu. Mesafeyi 105'e, hedef yüksekliğini 33'e aldım; roket dikeyde ~%55 doluluk verir, küre ve kule ayrışır.
|
||
const KAMERA_SIFIR = { aciX: 0.13, aciY: 1.50, mesafe: 105, hedef: new THREE.Vector3(0, 33, 0) };
|
||
|
||
const kontroller = {
|
||
hedef: KAMERA_SIFIR.hedef.clone(),
|
||
aciX: KAMERA_SIFIR.aciX, // dikey (küçük = ufka yakın, engin görünüm)
|
||
aciY: KAMERA_SIFIR.aciY, // yatay
|
||
mesafe: KAMERA_SIFIR.mesafe,
|
||
hedefMesafe: KAMERA_SIFIR.mesafe,
|
||
surukle: false,
|
||
sonX: 0, sonY: 0,
|
||
|
||
// Her karede çağrılan tek güncelleme noktası
|
||
update: function(){
|
||
this.mesafe += (this.hedefMesafe - this.mesafe) * 0.12; // yumuşak yakınlaştırma
|
||
kamerayiKonumla();
|
||
}
|
||
};
|
||
|
||
function kamerayiKonumla(){
|
||
const d = kontroller.mesafe;
|
||
const x = kontroller.hedef.x + d * Math.cos(kontroller.aciX) * Math.sin(kontroller.aciY);
|
||
const y = kontroller.hedef.y + d * Math.sin(kontroller.aciX);
|
||
const z = kontroller.hedef.z + d * Math.cos(kontroller.aciX) * Math.cos(kontroller.aciY);
|
||
kamera.position.set(x, y, z);
|
||
kamera.lookAt(kontroller.hedef);
|
||
// NUMEX-DÜZELTME (2026-10-22): Kök neden: orto kamera aktifken konumu 'kamera' (perspektif) nesnesinden kopyalanıyordu; perspektif kamera orto modda güncellenmediği için değerleri bayat/yanlış kalıyor ve paralel görünüm ters dönüyordu. Artık iki kamera da aynı yörünge hesabından besleniyor.
|
||
// Paralel kamera seçiliyse onu da AYNI yörünge hesabından konumla
|
||
if (aktifKamera !== kamera){
|
||
aktifKamera.position.set(x, y, z);
|
||
aktifKamera.lookAt(kontroller.hedef);
|
||
aktifKamera.updateMatrixWorld();
|
||
}
|
||
}
|
||
|
||
let aktifKamera = kamera; // varsayılan: perspektif
|
||
|
||
kamerayiKonumla();
|
||
|
||
tuval.addEventListener('pointerdown', e => {
|
||
kontroller.surukle = true;
|
||
kontroller.sonX = e.clientX; kontroller.sonY = e.clientY;
|
||
tuval.setPointerCapture(e.pointerId);
|
||
});
|
||
tuval.addEventListener('pointerup', e => {
|
||
kontroller.surukle = false;
|
||
try { tuval.releasePointerCapture(e.pointerId); } catch (_) {}
|
||
});
|
||
tuval.addEventListener('pointermove', e => {
|
||
if (!kontroller.surukle) return;
|
||
const dx = e.clientX - kontroller.sonX;
|
||
const dy = e.clientY - kontroller.sonY;
|
||
kontroller.sonX = e.clientX; kontroller.sonY = e.clientY;
|
||
kontroller.aciY -= dx * 0.005;
|
||
kontroller.aciX = Math.max(0.12, Math.min(1.45, kontroller.aciX + dy * 0.004));
|
||
});
|
||
tuval.addEventListener('wheel', e => {
|
||
e.preventDefault();
|
||
kontroller.hedefMesafe = Math.max(34, Math.min(240, kontroller.hedefMesafe + e.deltaY * 0.09));
|
||
}, { passive: false });
|
||
|
||
// Dokunmatik iki parmak yakınlaştırma
|
||
let parmakMesafe = null;
|
||
tuval.addEventListener('touchmove', e => {
|
||
if (e.touches.length !== 2) return;
|
||
e.preventDefault();
|
||
const a = e.touches[0], b = e.touches[1];
|
||
const d = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
||
if (parmakMesafe !== null) {
|
||
kontroller.hedefMesafe = Math.max(34, Math.min(240, kontroller.hedefMesafe - (d - parmakMesafe) * 0.35));
|
||
}
|
||
parmakMesafe = d;
|
||
}, { passive: false });
|
||
tuval.addEventListener('touchend', () => { parmakMesafe = null; });
|
||
|
||
/* Kamera sıfırlama yukarıda (KAMERA_SIFIR ile) tek yerden yönetiliyor. */
|
||
|
||
/* ---------------- Gökyüzü (zaman dilimine göre) ---------------- */
|
||
const gokyuzuUniform = {
|
||
ustRenk: { value: new THREE.Color(0x1d3a5f) },
|
||
ortaRenk: { value: new THREE.Color(0xd88a55) },
|
||
altRenk: { value: new THREE.Color(0x14202e) },
|
||
gunesYon: { value: new THREE.Vector3(0.6, 0.12, -1).normalize() },
|
||
gunesGuc: { value: 1.0 },
|
||
geceFaktoru: { value: 0.0 }
|
||
};
|
||
|
||
const gokyuzuMalzeme = new THREE.ShaderMaterial({
|
||
uniforms: gokyuzuUniform,
|
||
side: THREE.BackSide,
|
||
depthWrite: false,
|
||
vertexShader: `
|
||
varying vec3 vYon;
|
||
void main(){
|
||
vYon = normalize(position);
|
||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||
}`,
|
||
fragmentShader: `
|
||
uniform vec3 ustRenk; uniform vec3 ortaRenk; uniform vec3 altRenk;
|
||
uniform vec3 gunesYon; uniform float gunesGuc; uniform float geceFaktoru;
|
||
varying vec3 vYon;
|
||
|
||
// Sade, tekrarlanabilir yıldız gürültüsü
|
||
float yildizKatmani(vec3 yon, float olcek){
|
||
vec3 p = yon * olcek;
|
||
vec3 hucre = floor(p);
|
||
float rast = fract(sin(dot(hucre, vec3(12.9898, 78.233, 45.164))) * 43758.5453);
|
||
float esik = step(0.9965, rast); // çok seyrek noktalar
|
||
vec3 merkez = hucre + 0.5 + 0.12 * vec3(
|
||
fract(rast * 13.7), fract(rast * 7.3), fract(rast * 3.1));
|
||
float d = length(p - merkez);
|
||
return esik * smoothstep(0.34, 0.0, d);
|
||
}
|
||
|
||
void main(){
|
||
// Ufukta sıcak/parlak renk, tepede koyu; orta bant yumuşak geçiş
|
||
float h = clamp(vYon.y, 0.0, 1.0);
|
||
float h2 = pow(h, 0.55);
|
||
vec3 renk = mix(altRenk, ortaRenk, smoothstep(0.0, 0.22, h2));
|
||
renk = mix(renk, ustRenk, smoothstep(0.14, 0.85, h2));
|
||
|
||
// Gündüz güneş parlaması (gece tamamen söner)
|
||
float gunduz = 1.0 - geceFaktoru;
|
||
float g = max(dot(normalize(vYon), normalize(gunesYon)), 0.0);
|
||
// NUMEX-DÜZELTME (2026-10-22): Gece modunda ufuk turuncu kalıyordu: pow(g,8) geniş açılı sıcak halo katmanı geceFaktoru 0 olsa bile ufku boyuyordu. Halo katmanının etkisini gece katsayısıyla ikinci kez, daha sert söndürüyorum (gunduz^2) ve dar tutuyorum.
|
||
renk += vec3(1.0, 0.72, 0.42) * pow(g, 120.0) * 2.0 * gunesGuc * gunduz;
|
||
renk += vec3(1.0, 0.62, 0.32) * pow(g, 12.0) * 0.16 * gunesGuc * gunduz * gunduz;
|
||
|
||
// Gece yıldızları (ufka yakın bölgede kaybolur)
|
||
if (geceFaktoru > 0.02){
|
||
float y1 = yildizKatmani(vYon, 220.0);
|
||
float y2 = yildizKatmani(vYon + 37.0, 460.0) * 0.6;
|
||
float gorunurluk = smoothstep(0.03, 0.30, vYon.y) * geceFaktoru;
|
||
renk += vec3(0.85, 0.9, 1.0) * (y1 + y2) * gorunurluk * 1.6;
|
||
}
|
||
|
||
gl_FragColor = vec4(renk, 1.0);
|
||
}`
|
||
});
|
||
const gokyuzu = new THREE.Mesh(new THREE.SphereGeometry(1600, 32, 20), gokyuzuMalzeme);
|
||
sahne.add(gokyuzu);
|
||
|
||
sahne.fog = new THREE.FogExp2(0x9a7a68, 0.0032);
|
||
|
||
/* ---------------- Işıklar ---------------- */
|
||
const gunes = new THREE.DirectionalLight(0xffcf9e, 2.1);
|
||
gunes.position.set(70, 34, -90);
|
||
gunes.castShadow = true;
|
||
gunes.shadow.mapSize.set(2048, 2048);
|
||
gunes.shadow.camera.near = 10;
|
||
gunes.shadow.camera.far = 320;
|
||
gunes.shadow.camera.left = -110;
|
||
gunes.shadow.camera.right = 110;
|
||
gunes.shadow.camera.top = 110;
|
||
gunes.shadow.camera.bottom = -110;
|
||
gunes.shadow.bias = -0.0006;
|
||
sahne.add(gunes);
|
||
|
||
const gokyuzuIsik = new THREE.HemisphereLight(0x9dc4e8, 0x3a2a20, 0.75);
|
||
sahne.add(gokyuzuIsik);
|
||
|
||
const dolguIsik = new THREE.DirectionalLight(0x88aaff, 0.35);
|
||
dolguIsik.position.set(-80, 30, 60);
|
||
sahne.add(dolguIsik);
|
||
|
||
/* ---------------- Malzemeler ---------------- */
|
||
const MAL = {
|
||
beton: new THREE.MeshStandardMaterial({ color: 0x8b8b86, roughness: 0.95, metalness: 0.02 }),
|
||
betonKoyu:new THREE.MeshStandardMaterial({ color: 0x5c5c58, roughness: 0.95, metalness: 0.02 }),
|
||
toprak: new THREE.MeshStandardMaterial({ color: 0x8a7a5f, roughness: 1.0, metalness: 0.0 }),
|
||
govde: new THREE.MeshStandardMaterial({ color: 0xe8e9ec, roughness: 0.42, metalness: 0.22 }),
|
||
govdeKoyu:new THREE.MeshStandardMaterial({ color: 0x2b2f36, roughness: 0.55, metalness: 0.35 }),
|
||
metal: new THREE.MeshStandardMaterial({ color: 0x9aa4ae, roughness: 0.42, metalness: 0.85 }),
|
||
metalKoyu:new THREE.MeshStandardMaterial({ color: 0x4a525c, roughness: 0.6, metalness: 0.7 }),
|
||
turuncu: new THREE.MeshStandardMaterial({ color: 0xd9622b, roughness: 0.62, metalness: 0.15 }),
|
||
kirmizi: new THREE.MeshStandardMaterial({ color: 0xb8352c, roughness: 0.6, metalness: 0.1 }),
|
||
cam: new THREE.MeshStandardMaterial({ color: 0x7fb6d6, roughness: 0.12, metalness: 0.5, transparent: true, opacity: 0.42 }),
|
||
isik: new THREE.MeshStandardMaterial({ color: 0xffd9a0, emissive: 0xffb45c, emissiveIntensity: 1.6, roughness: 0.6 })
|
||
};
|
||
|
||
/* ---------------- Yardımcılar ---------------- */
|
||
const tiklanabilir = [];
|
||
// NUMEX-DÜZELTME (2026-10-22): Kök neden: tıklama işleyicisi userData.bilgi arıyordu, etiketle() ise veriyi userData'nın kendisine yazıyordu — alt alan hiç oluşmadığı için her tıklama sessizce düşüyordu. Veri artık bilgi altında tutuluyor.
|
||
function etiketle(nesne, ad, metin, yukseklik, agirlik){
|
||
nesne.userData = { bilgi: { ad, metin, yukseklik, agirlik } };
|
||
tiklanabilir.push(nesne);
|
||
return nesne;
|
||
}
|
||
function kutu(w, h, d, malzeme){
|
||
return new THREE.Mesh(new THREE.BoxGeometry(w, h, d), malzeme);
|
||
}
|
||
function silindir(ustR, altR, h, malzeme, dilim = 20){
|
||
return new THREE.Mesh(new THREE.CylinderGeometry(ustR, altR, h, dilim), malzeme);
|
||
}
|
||
|
||
/* =========================================================
|
||
ZEMİN · Ova
|
||
========================================================= */
|
||
// Zemin ufku aşacak kadar büyük olmalı; yoksa ufkun altında gökyüzü sızar
|
||
const ova = new THREE.Mesh(new THREE.PlaneGeometry(7000, 7000, 1, 1), MAL.toprak);
|
||
ova.rotation.x = -Math.PI / 2;
|
||
ova.receiveShadow = true;
|
||
sahne.add(ova);
|
||
|
||
/* --- Kıyı şeridi: uzakta deniz + kıyı çizgisi (istek 5) --- */
|
||
const denizMal = new THREE.MeshStandardMaterial({
|
||
color: 0x1b3a52, roughness: 0.24, metalness: 0.55
|
||
});
|
||
const deniz = new THREE.Mesh(new THREE.PlaneGeometry(7000, 2400), denizMal);
|
||
deniz.rotation.x = -Math.PI / 2;
|
||
deniz.position.set(0, -0.35, -3000);
|
||
deniz.receiveShadow = true;
|
||
sahne.add(deniz);
|
||
|
||
// Kıyı köpük şeridi (denizin iç kenarı)
|
||
const kopukMal = new THREE.MeshBasicMaterial({
|
||
color: 0x9fc4dd, transparent: true, opacity: 0.4,
|
||
blending: THREE.AdditiveBlending, depthWrite: false
|
||
});
|
||
const kopuk = new THREE.Mesh(new THREE.PlaneGeometry(7000, 42), kopukMal);
|
||
kopuk.rotation.x = -Math.PI / 2;
|
||
kopuk.position.set(0, 0.05, -1820);
|
||
sahne.add(kopuk);
|
||
|
||
// Kıyı çizgisi boyunca ince kum şeridi
|
||
const kumMal = new THREE.MeshStandardMaterial({ color: 0x8a7458, roughness: 1.0 });
|
||
const kum = new THREE.Mesh(new THREE.PlaneGeometry(7000, 90), kumMal);
|
||
kum.rotation.x = -Math.PI / 2;
|
||
kum.position.set(0, 0.02, -1740);
|
||
kum.receiveShadow = true;
|
||
sahne.add(kum);
|
||
|
||
// Uzak tepe silüetleri (tek halka, sabit tohum → her açılışta aynı manzara)
|
||
// NUMEX-DÜZELTME (2026-10-22): Dağ silüeti düz ve opak kalıyor, arkasındaki gökyüzüyle kaynaşmıyordu. Rengi sis tonuna yaklaştırıp hafif saydam yaptım — mesafe derinliği oluşsun.
|
||
const tepeMalzeme = new THREE.MeshStandardMaterial({
|
||
color: 0x6a5a4a, roughness: 1.0, transparent: true, opacity: 0.92
|
||
});
|
||
let tohum = 20261022;
|
||
const rastgele = () => {
|
||
tohum = (tohum * 1664525 + 1013904223) % 4294967296;
|
||
return tohum / 4294967296;
|
||
};
|
||
for (let i = 0; i < 64; i++){
|
||
const aci = (i / 64) * Math.PI * 2 + (rastgele() - 0.5) * 0.06;
|
||
// NUMEX-DÜZELTME (2026-10-22): Dağ halkası kameraya çok yakındı (900–1320 birim, kamera 210 birimde) — ekranın üstünü dev bir duvar gibi kaplıyordu. Referanstaki gibi ince, uzak bir silüet için halkayı ufka çektim ve yükseklikleri ölçekledim.
|
||
const uzaklik = 2600 + rastgele() * 1100;
|
||
const genislik = 420 + rastgele() * 620;
|
||
const yukseklik = 55 + rastgele() * 85;
|
||
const tepe = new THREE.Mesh(new THREE.ConeGeometry(genislik, yukseklik, 14), tepeMalzeme);
|
||
tepe.position.set(Math.cos(aci) * uzaklik, yukseklik / 2 - 10, Math.sin(aci) * uzaklik);
|
||
tepe.rotation.y = rastgele() * Math.PI;
|
||
tepe.scale.z = 0.6 + rastgele() * 0.5;
|
||
tepe.frustumCulled = false;
|
||
sahne.add(tepe);
|
||
}
|
||
|
||
/* =========================================================
|
||
FIRLATMA RAMPA KOMPLEKSİ
|
||
========================================================= */
|
||
// Gece yanan bütün lambalar burada toplanır (gündüz söner)
|
||
const lambaIzgarasi = [];
|
||
const kompleks = new THREE.Group();
|
||
sahne.add(kompleks);
|
||
|
||
/* --- Beton platform --- */
|
||
const platform = kutu(84, 3, 84, MAL.beton);
|
||
platform.position.y = 1.5;
|
||
platform.castShadow = true; platform.receiveShadow = true;
|
||
kompleks.add(etiketle(platform, 'Fırlatma Platformu', 'Ağır beton döşeme. Rampanın ve servis kulesinin oturduğu ana taşıyıcı yüzey; egzoz kanalı bu döşemenin ortasından geçer.', '84 × 84 m', '12.000 ton'));
|
||
|
||
// Platform kenar şeridi
|
||
const serit = kutu(86, 0.4, 86, MAL.betonKoyu);
|
||
serit.position.y = 3.05;
|
||
serit.receiveShadow = true;
|
||
kompleks.add(serit);
|
||
|
||
// Egzoz kanalı (rampanın altında, iki yana açılan)
|
||
const kanal = kutu(52, 1.6, 7, MAL.betonKoyu);
|
||
kanal.position.set(0, 2.4, 0);
|
||
kompleks.add(kanal);
|
||
const kanal2 = kanal.clone(); kanal2.rotation.y = Math.PI / 2;
|
||
kompleks.add(kanal2);
|
||
|
||
/* --- ALEV ÇUKURU: rampanın altındaki derin egzoz kuyusu (istek 5) --- */
|
||
// İç karartılmış çukur duvarı
|
||
const cukurMal = new THREE.MeshStandardMaterial({
|
||
color: 0x0a0a0b, roughness: 0.95, metalness: 0.2, side: THREE.DoubleSide
|
||
});
|
||
const cukurDerinlik = 9;
|
||
const cukurYari = 11;
|
||
const cukur = new THREE.Mesh(
|
||
new THREE.CylinderGeometry(cukurYari, cukurYari, cukurDerinlik, 40, 1, true),
|
||
cukurMal
|
||
);
|
||
cukur.position.set(0, -cukurDerinlik / 2 + 2.2, 0);
|
||
kompleks.add(cukur);
|
||
|
||
// Çukur tabanı — sıcak yanma izi
|
||
const tabanMal = new THREE.MeshStandardMaterial({
|
||
color: 0x1a0e07, roughness: 1.0, metalness: 0.1,
|
||
emissive: 0x140600, emissiveIntensity: 0.4
|
||
});
|
||
const cukurTaban = new THREE.Mesh(new THREE.CircleGeometry(cukurYari * 0.96, 40), tabanMal);
|
||
cukurTaban.rotation.x = -Math.PI / 2;
|
||
cukurTaban.position.y = -cukurDerinlik + 2.3;
|
||
kompleks.add(cukurTaban);
|
||
|
||
// Çevre beton bileziği
|
||
const cukurBilezik = new THREE.Mesh(
|
||
new THREE.TorusGeometry(cukurYari, 0.9, 10, 40), MAL.betonKoyu
|
||
);
|
||
cukurBilezik.rotation.x = Math.PI / 2;
|
||
cukurBilezik.position.y = 2.25;
|
||
cukurBilezik.castShadow = true;
|
||
kompleks.add(cukurBilezik);
|
||
|
||
/* --- RAMPA DOKUSU: beton platform üzerinde panel/tırtık izleri (istek 5) --- */
|
||
// Platform yüzeyine serilen doku plakası (canvas ile üretilir, harici dosya yok)
|
||
function betonDokusu(){
|
||
const cv = document.createElement('canvas');
|
||
cv.width = 1024; cv.height = 1024;
|
||
const ctx = cv.getContext('2d');
|
||
|
||
// Taban beton
|
||
ctx.fillStyle = '#7d7a73';
|
||
ctx.fillRect(0, 0, 1024, 1024);
|
||
|
||
// İnce gren
|
||
const img = ctx.getImageData(0, 0, 1024, 1024);
|
||
for (let i = 0; i < img.data.length; i += 4){
|
||
const n = (Math.random() - 0.5) * 26;
|
||
img.data[i] += n; img.data[i + 1] += n; img.data[i + 2] += n;
|
||
}
|
||
ctx.putImageData(img, 0, 0);
|
||
|
||
// Panel derzleri (8×8 beton plaka)
|
||
ctx.strokeStyle = 'rgba(45,44,42,.55)';
|
||
ctx.lineWidth = 5;
|
||
for (let i = 0; i <= 8; i++){
|
||
const p = (i / 8) * 1024;
|
||
ctx.beginPath(); ctx.moveTo(p, 0); ctx.lineTo(p, 1024); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(0, p); ctx.lineTo(1024, p); ctx.stroke();
|
||
}
|
||
|
||
// Egzoz isi (ortada koyu radyal leke)
|
||
const g = ctx.createRadialGradient(512, 512, 40, 512, 512, 430);
|
||
g.addColorStop(0, 'rgba(18,14,10,.85)');
|
||
g.addColorStop(0.55, 'rgba(40,34,28,.4)');
|
||
g.addColorStop(1, 'rgba(0,0,0,0)');
|
||
ctx.fillStyle = g;
|
||
ctx.fillRect(0, 0, 1024, 1024);
|
||
|
||
// Çatlaklar
|
||
ctx.strokeStyle = 'rgba(35,33,30,.5)';
|
||
ctx.lineWidth = 2;
|
||
for (let i = 0; i < 26; i++){
|
||
ctx.beginPath();
|
||
let x = Math.random() * 1024, y = Math.random() * 1024;
|
||
ctx.moveTo(x, y);
|
||
for (let j = 0; j < 7; j++){
|
||
x += (Math.random() - 0.5) * 90;
|
||
y += (Math.random() - 0.5) * 90;
|
||
ctx.lineTo(x, y);
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
|
||
// Sarı güvenlik şeritleri (rampa çevresi)
|
||
ctx.strokeStyle = 'rgba(214,178,72,.62)';
|
||
ctx.lineWidth = 12;
|
||
ctx.strokeRect(250, 250, 524, 524);
|
||
|
||
const tex = new THREE.CanvasTexture(cv);
|
||
tex.colorSpace = THREE.SRGBColorSpace;
|
||
tex.anisotropy = 8;
|
||
return tex;
|
||
}
|
||
|
||
const platformDoku = betonDokusu();
|
||
const platformUst = new THREE.Mesh(
|
||
new THREE.PlaneGeometry(84, 84),
|
||
new THREE.MeshStandardMaterial({ map: platformDoku, roughness: 0.92, metalness: 0.04 })
|
||
);
|
||
platformUst.rotation.x = -Math.PI / 2;
|
||
platformUst.position.y = 3.06;
|
||
platformUst.receiveShadow = true;
|
||
kompleks.add(platformUst);
|
||
|
||
/* --- Rampa yükseltici (mobil fırlatma tablası) --- */
|
||
const tablaAlt = kutu(22, 5, 22, MAL.metalKoyu);
|
||
tablaAlt.position.set(0, 5.5, 0);
|
||
tablaAlt.castShadow = true; tablaAlt.receiveShadow = true;
|
||
kompleks.add(tablaAlt);
|
||
|
||
const tabla = kutu(19, 2.6, 19, MAL.beton);
|
||
tabla.position.set(0, 9.3, 0);
|
||
tabla.castShadow = true; tabla.receiveShadow = true;
|
||
kompleks.add(etiketle(tabla, 'Fırlatma Tablası', 'Roketi taşıyan hareketli çelik tabla. Kalkış anında kilitleri bırakır, roket serbest kalır ve tabla yerinde sabit kalır.', '19 × 19 m', '480 ton'));
|
||
|
||
// Tabla üzerindeki kızak rayları
|
||
for (const offset of [-6.4, 6.4]){
|
||
const ray = kutu(1.2, 0.7, 15, MAL.turuncu);
|
||
ray.position.set(offset, 10.9, 0);
|
||
ray.castShadow = true;
|
||
kompleks.add(ray);
|
||
}
|
||
|
||
/* --- Tutucu kollar (roketi tutan) --- */
|
||
const tutucular = new THREE.Group();
|
||
kompleks.add(tutucular);
|
||
for (let i = 0; i < 4; i++){
|
||
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
|
||
const kol = kutu(2.4, 3.2, 1.4, MAL.metal);
|
||
kol.position.set(Math.cos(aci) * 6.6, 13.4, Math.sin(aci) * 6.6);
|
||
kol.rotation.y = -aci;
|
||
kol.castShadow = true;
|
||
tutucular.add(kol);
|
||
}
|
||
|
||
/* =========================================================
|
||
ROKET · Gökbey-9
|
||
========================================================= */
|
||
const roket = new THREE.Group();
|
||
roket.position.y = 11;
|
||
kompleks.add(roket);
|
||
etiketle(roket, 'Gökbey-9 Taşıyıcı Roketi', 'İki kademeli orta-sınıf taşıyıcı. Çekirdek kademe sıvı yakıtla çalışır, dört yan booster kalkış itkisini tamamlar. Burun konisinin içinde faydalı yük bölmesi bulunur.', '62 m', '410 ton');
|
||
|
||
const GOVDE_ALT = 0; // metre, roket grubuna göre
|
||
const GOVDE_UST = 40; // çekirdek gövde
|
||
const CAPRAZ = 1;
|
||
|
||
// Çekirdek gövde
|
||
const cekirdek = silindir(2.6, 2.9, GOVDE_UST, MAL.govde, 32);
|
||
cekirdek.position.y = GOVDE_ALT + GOVDE_UST / 2;
|
||
cekirdek.castShadow = true; cekirdek.receiveShadow = true;
|
||
roket.add(cekirdek);
|
||
|
||
// Gövde bantları (siyah detay)
|
||
for (const y of [8, 18, 30, 37]){
|
||
const bant = silindir(2.68, 2.9, 0.8, MAL.govdeKoyu, 32);
|
||
bant.position.y = y;
|
||
bant.castShadow = true;
|
||
roket.add(bant);
|
||
}
|
||
|
||
// Motor bölümü (alt)
|
||
const motorBolumu = silindir(2.9, 3.1, 5.4, MAL.govdeKoyu, 32);
|
||
motorBolumu.position.y = GOVDE_ALT - 2.4;
|
||
motorBolumu.castShadow = true;
|
||
roket.add(motorBolumu);
|
||
|
||
// Motor nozulları
|
||
for (let i = 0; i < 4; i++){
|
||
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
|
||
const nozul = silindir(0.85, 1.35, 2.6, MAL.metalKoyu, 16);
|
||
nozul.position.set(Math.cos(aci) * 1.5, GOVDE_ALT - 6.1, Math.sin(aci) * 1.5);
|
||
nozul.castShadow = true;
|
||
roket.add(nozul);
|
||
}
|
||
|
||
// Burun konisi
|
||
const koni = new THREE.Mesh(new THREE.ConeGeometry(2.6, 9, 32), MAL.govde);
|
||
koni.position.y = GOVDE_UST + 4.5;
|
||
koni.castShadow = true;
|
||
roket.add(etiketle(koni, 'Burun Konisi · Faydalı Yük', 'Aerodinamik kap. İçindeki bölmede uydu ya da kargo taşınır; kalkıştan sonra ayrılarak yükü serbest bırakır.', '9 m', '1.8 ton'));
|
||
|
||
// Koni ucu (kırmızı)
|
||
const koniUcu = new THREE.Mesh(new THREE.ConeGeometry(0.65, 2.4, 20), MAL.kirmizi);
|
||
koniUcu.position.y = GOVDE_UST + 10.2;
|
||
koniUcu.castShadow = true;
|
||
roket.add(koniUcu);
|
||
|
||
// Faydalı yük bölmesi halkası
|
||
const payYukHalka = silindir(2.68, 2.72, 1.6, MAL.turuncu, 32);
|
||
payYukHalka.position.y = GOVDE_UST + 0.6;
|
||
roket.add(payYukHalka);
|
||
|
||
// Dört yan booster
|
||
const boosterlar = [];
|
||
for (let i = 0; i < 4; i++){
|
||
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
|
||
const bx = Math.cos(aci) * 4.1;
|
||
const bz = Math.sin(aci) * 4.1;
|
||
|
||
const booster = silindir(1.35, 1.45, 21, MAL.govde, 24);
|
||
booster.position.set(bx, 14, bz);
|
||
booster.castShadow = true;
|
||
roket.add(booster);
|
||
boosterlar.push(booster);
|
||
|
||
// Booster burun konisi
|
||
const bKoni = new THREE.Mesh(new THREE.ConeGeometry(1.35, 4.2, 24), MAL.govde);
|
||
bKoni.position.set(bx, 26.6, bz);
|
||
bKoni.castShadow = true;
|
||
roket.add(bKoni);
|
||
|
||
// Booster alt halkası
|
||
const bHalka = silindir(1.46, 1.5, 1.2, MAL.govdeKoyu, 24);
|
||
bHalka.position.set(bx, 3.6, bz);
|
||
roket.add(bHalka);
|
||
|
||
// Booster nozulu
|
||
const bNozul = silindir(0.8, 1.1, 1.8, MAL.metalKoyu, 14);
|
||
bNozul.position.set(bx, 2.2, bz);
|
||
bNozul.castShadow = true;
|
||
roket.add(bNozul);
|
||
|
||
// Bağlantı ayağı
|
||
const bag = kutu(1.1, 0.7, 2.2, MAL.metal);
|
||
bag.position.set(bx * 0.72, 9, bz * 0.72);
|
||
bag.rotation.y = -aci;
|
||
roket.add(bag);
|
||
}
|
||
etiketle(boosterlar[0], 'Yan Booster', 'Kalkış itkisinin büyük kısmını sağlayan dört yardımcı motor. Yakıtı bitince ayrılır ve denize düşer.', '21 m', '4 × 42 ton');
|
||
|
||
/* =========================================================
|
||
SERVİS KULESİ
|
||
========================================================= */
|
||
const kule = new THREE.Group();
|
||
kule.position.set(-17, 3, -2);
|
||
kompleks.add(kule);
|
||
etiketle(kule, 'Servis Kulesi', 'Roketin yakıt dolumu, elektrik bağlantısı ve mürettebat erişimi bu kuleden yapılır. Üç ayrı erişim kolu roketin farklı yüksekliklerine uzanır.', '96 m', '3.400 ton');
|
||
|
||
// NUMEX-DÜZELTME (2026-10-22): Kule gövdesi 8×90×8 dolu bir blok olarak modellenmişti; referansta kafes/çelik strüktür var ve blok roketten kalın görünüyordu. Dolu hacmi yarı saydam ince panelle değiştirip dört köşeye taşıyıcı kolon koydum.
|
||
// Gövde: dolu blok yerine yarı saydam ince panel (arkadaki roket görünsün)
|
||
const kulePanelMal = new THREE.MeshStandardMaterial({
|
||
color: 0x7c8896, roughness: 0.85, metalness: 0.25,
|
||
transparent: true, opacity: 0.28, side: THREE.DoubleSide, depthWrite: false
|
||
});
|
||
const kuleGovde = kutu(7.4, 90, 7.4, kulePanelMal);
|
||
kuleGovde.position.y = 45;
|
||
kule.add(kuleGovde);
|
||
|
||
// Dört köşe taşıyıcı kolon
|
||
const kolonMal = new THREE.MeshStandardMaterial({ color: 0x9aa4ae, roughness: 0.6, metalness: 0.7 });
|
||
for (const [kx, kz] of [[-3.9,-3.9],[3.9,-3.9],[-3.9,3.9],[3.9,3.9]]){
|
||
const kolon = kutu(1.1, 90, 1.1, kolonMal);
|
||
kolon.position.set(kx, 45, kz);
|
||
kolon.castShadow = true;
|
||
kule.add(kolon);
|
||
}
|
||
|
||
// Yatay kata bantları (kule ritmi)
|
||
for (let y = 8; y < 90; y += 11){
|
||
const bant = kutu(8.2, 0.8, 8.2, kolonMal);
|
||
bant.position.y = y;
|
||
kule.add(bant);
|
||
}
|
||
|
||
// Kule kafes çaprazları (görsel derinlik)
|
||
const caprazMal = new THREE.LineBasicMaterial({ color: 0x6d7883, transparent: true, opacity: 0.55 });
|
||
const caprazNoktalari = [];
|
||
for (let y = 0; y < 90; y += 6){
|
||
const d = 4.02;
|
||
caprazNoktalari.push(-d, y, d, d, y + 6, d);
|
||
caprazNoktalari.push(d, y, d, d, y + 6, -d);
|
||
caprazNoktalari.push(-d, y, -d, -d, y + 6, d);
|
||
}
|
||
const caprazGeo = new THREE.BufferGeometry();
|
||
caprazGeo.setAttribute('position', new THREE.Float32BufferAttribute(caprazNoktalari, 3));
|
||
kule.add(new THREE.LineSegments(caprazGeo, caprazMal));
|
||
|
||
// Kule tepesi
|
||
const kuleTepe = kutu(9.5, 4, 9.5, MAL.metal);
|
||
kuleTepe.position.y = 92;
|
||
kuleTepe.castShadow = true;
|
||
kule.add(kuleTepe);
|
||
|
||
// Kule kırmızı uyarı ışığı
|
||
const uyari = new THREE.Mesh(new THREE.SphereGeometry(0.6, 12, 10), MAL.kirmizi.clone());
|
||
uyari.material.emissive = new THREE.Color(0xff3322);
|
||
uyari.material.emissiveIntensity = 2.2;
|
||
uyari.position.y = 95;
|
||
kule.add(uyari);
|
||
|
||
// Erişim kolları (roketin yanına uzanan)
|
||
const kollar = [14, 30, 44];
|
||
kollar.forEach((y, i) => {
|
||
const kol = kutu(11, 1.1, 2.6, MAL.metal);
|
||
kol.position.set(9.5, y, 0);
|
||
kol.castShadow = true;
|
||
kule.add(kol);
|
||
// Kol ucu kabin
|
||
const kabin = kutu(3.2, 3, 3.2, MAL.govdeKoyu);
|
||
kabin.position.set(15.2, y + 1.6, 0);
|
||
kabin.castShadow = true;
|
||
kule.add(kabin);
|
||
// Kabin pencereleri
|
||
const pencere = kutu(0.15, 1.2, 2.2, MAL.cam);
|
||
pencere.position.set(16.85, y + 1.8, 0);
|
||
kule.add(pencere);
|
||
});
|
||
|
||
// Kule asansör rayı
|
||
const asansor = kutu(1.6, 88, 1.6, MAL.turuncu);
|
||
asansor.position.set(-4.3, 45, 0);
|
||
kule.add(asansor);
|
||
|
||
/* =========================================================
|
||
YAKIT KÜRELERİ
|
||
========================================================= */
|
||
const kureGrubu = new THREE.Group();
|
||
kompleks.add(kureGrubu);
|
||
|
||
function yakitKuresi(x, z, r, renk){
|
||
const g = new THREE.Group();
|
||
g.position.set(x, 0, z);
|
||
kureGrubu.add(g);
|
||
|
||
const kure = new THREE.Mesh(new THREE.SphereGeometry(r, 28, 20), new THREE.MeshStandardMaterial({
|
||
color: renk, roughness: 0.5, metalness: 0.35
|
||
}));
|
||
kure.position.y = r + 5;
|
||
kure.castShadow = true; kure.receiveShadow = true;
|
||
g.add(kure);
|
||
|
||
// Destek ayakları
|
||
for (let i = 0; i < 6; i++){
|
||
const aci = (i / 6) * Math.PI * 2;
|
||
const ayak = silindir(0.28, 0.28, r * 0.9, MAL.metalKoyu, 8);
|
||
ayak.position.set(Math.cos(aci) * r * 0.72, (r + 5) / 2 + 1, Math.sin(aci) * r * 0.72);
|
||
ayak.rotation.z = Math.cos(aci) * 0.22;
|
||
ayak.rotation.x = -Math.sin(aci) * 0.22;
|
||
g.add(ayak);
|
||
}
|
||
return g;
|
||
}
|
||
|
||
const kure1 = yakitKuresi(-30, 26, 8, 0xd8dde2);
|
||
etiketle(kure1, 'Sıvı Oksijen Tankı', 'Kriyojenik oksijen soğuk hâlde tutulur. Boru hatlarıyla kuleye, oradan rokete aktarılır; dolum kalkıştan saatler önce biter.', 'Ø 16 m', '900 m³');
|
||
yakitKuresi(-14, 32, 6, 0xc2c8ce);
|
||
const kure3 = yakitKuresi(30, 28, 7.5, 0xd0d5da);
|
||
etiketle(kure3, 'Sıvı Yakıt Tankı', 'Yakıt bileşeni basınç altında saklanır. Tanklar arasındaki boru köprüsü karma kompleksin damar sistemidir.', 'Ø 15 m', '780 m³');
|
||
|
||
// Boru hatları (kürelerden kuleye)
|
||
const boruMal = new THREE.MeshStandardMaterial({ color: 0x8d959d, roughness: 0.55, metalness: 0.7 });
|
||
function boru(x1, z1, x2, z2, y){
|
||
const uzunluk = Math.hypot(x2 - x1, z2 - z1);
|
||
const b = silindir(0.42, 0.42, uzunluk, boruMal, 10);
|
||
b.position.set((x1 + x2) / 2, y, (z1 + z2) / 2);
|
||
b.rotation.z = Math.PI / 2;
|
||
b.rotation.y = -Math.atan2(z2 - z1, x2 - x1);
|
||
kompleks.add(b);
|
||
}
|
||
boru(-30, 24, -17, 4, 4.5);
|
||
boru(-14, 30, -17, 6, 3.6);
|
||
boru(30, 26, -8, 4, 4.2);
|
||
|
||
/* =========================================================
|
||
PARATONER DİREKLERİ
|
||
========================================================= */
|
||
const paratonerGrubu = new THREE.Group();
|
||
kompleks.add(paratonerGrubu);
|
||
const catenary = [];
|
||
|
||
for (let i = 0; i < 4; i++){
|
||
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
|
||
// NUMEX-DÜZELTME (2026-10-22): Paratoner direkleri 40 birim yarıçapta duruyordu; kamera 84 birimdeyken ekranı dikey çizgilerle bölüp roketi kesiyorlardı. Yarıçapı 62'ye çıkardım — direkler artık kadrajın dışına/kenarına düşer, roketin önünden geçmez.
|
||
const px = Math.cos(aci) * 62;
|
||
const pz = Math.sin(aci) * 62;
|
||
|
||
const direk = silindir(0.5, 0.85, 62, MAL.metal, 12);
|
||
direk.position.set(px, 34, pz);
|
||
direk.castShadow = true;
|
||
paratonerGrubu.add(direk);
|
||
|
||
// Tepe ucu
|
||
const ucu = new THREE.Mesh(new THREE.ConeGeometry(0.5, 3, 10), MAL.metal);
|
||
ucu.position.set(px, 66.5, pz);
|
||
paratonerGrubu.add(ucu);
|
||
|
||
// Tepe lambası (gece yanar)
|
||
const lamba = new THREE.Mesh(new THREE.SphereGeometry(0.4, 10, 8), MAL.isik.clone());
|
||
lamba.position.set(px, 68, pz);
|
||
lambaIzgarasi.push(lamba);
|
||
paratonerGrubu.add(lamba);
|
||
|
||
// Taban bloğu
|
||
const taban = kutu(3, 2.6, 3, MAL.betonKoyu);
|
||
taban.position.set(px, 4, pz);
|
||
paratonerGrubu.add(taban);
|
||
|
||
catenary.push(new THREE.Vector3(px, 62, pz));
|
||
}
|
||
etiketle(paratonerGrubu, 'Paratoner Direkleri', 'Dört direk kompleksi yıldırım darbelerine karşı korur; aralarındaki iletken tel örgü elektrik yükünü toprağa taşır.', '68 m', '4 direk');
|
||
|
||
/* =========================================================
|
||
ÇEVRE BİNALARI · SU KULESİ · YOL
|
||
========================================================= */
|
||
// Yatay yakıt servis binası
|
||
const servisBina = kutu(16, 7, 10, MAL.beton);
|
||
servisBina.position.set(-38, 3.5 + 3, 8);
|
||
servisBina.castShadow = true; servisBina.receiveShadow = true;
|
||
kompleks.add(etiketle(servisBina, 'Servis Binası', 'Yakıt pompaları, vana odaları ve kontrol ekipmanı bu binada. Yer altı tünelleriyle rampaya bağlanır.', '16 × 10 m', '—'));
|
||
|
||
// Yatay bina (kontrol merkezi)
|
||
const kontrol = kutu(26, 6, 12, MAL.betonKoyu);
|
||
kontrol.position.set(34, 6, -26);
|
||
kontrol.castShadow = true; kontrol.receiveShadow = true;
|
||
kompleks.add(kontrol);
|
||
|
||
// Kontrol merkezi pencereleri
|
||
for (let i = 0; i < 7; i++){
|
||
const p = kutu(2.2, 1.6, 0.2, MAL.isik.clone());
|
||
p.position.set(24 + i * 3.1, 7.2, -20.1);
|
||
lambaIzgarasi.push(p);
|
||
kompleks.add(p);
|
||
}
|
||
|
||
// Haberleşme çanakları
|
||
for (const [cx, cz, r] of [[42, -34, 4.5], [46, -22, 3.2]]){
|
||
const canak = new THREE.Mesh(
|
||
new THREE.SphereGeometry(r, 20, 12, 0, Math.PI * 2, 0, Math.PI / 2.6),
|
||
new THREE.MeshStandardMaterial({ color: 0xd6dade, roughness: 0.45, metalness: 0.4, side: THREE.DoubleSide })
|
||
);
|
||
canak.position.set(cx, 12, cz);
|
||
canak.rotation.set(Math.PI * 0.72, 0.6, 0.3);
|
||
canak.castShadow = true;
|
||
kompleks.add(canak);
|
||
const bacak = silindir(0.4, 0.6, 10, MAL.metalKoyu, 8);
|
||
bacak.position.set(cx, 8, cz);
|
||
kompleks.add(bacak);
|
||
}
|
||
|
||
// Su kulesi
|
||
const suKule = new THREE.Group();
|
||
suKule.position.set(-46, 3, -30);
|
||
kompleks.add(suKule);
|
||
for (let i = 0; i < 4; i++){
|
||
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
|
||
const bacak = silindir(0.45, 0.6, 22, MAL.metal, 8);
|
||
bacak.position.set(Math.cos(aci) * 3.4, 11, Math.sin(aci) * 3.4);
|
||
bacak.rotation.z = Math.cos(aci) * 0.1;
|
||
suKule.add(bacak);
|
||
}
|
||
const tank = silindir(5.2, 5.2, 8, MAL.govde, 24);
|
||
tank.position.y = 26;
|
||
tank.castShadow = true;
|
||
suKule.add(etiketle(tank, 'Su Kulesi', 'Kalkış anında ses dalgasını bastırmak için rampa üzerine su püskürtülür; bu kule o suyu depolar.', '8 m yüksek', '1.100 m³'));
|
||
|
||
// Yol (platformdan dışa)
|
||
const yol = new THREE.Mesh(new THREE.PlaneGeometry(14, 200), new THREE.MeshStandardMaterial({ color: 0x3d3d3a, roughness: 1 }));
|
||
yol.rotation.x = -Math.PI / 2;
|
||
yol.position.set(0, 3.05, 130);
|
||
yol.receiveShadow = true;
|
||
kompleks.add(yol);
|
||
|
||
// Yol kenarı direkleri
|
||
for (let i = 0; i < 12; i++){
|
||
for (const sx of [-8.5, 8.5]){
|
||
const d = silindir(0.16, 0.2, 7, MAL.metalKoyu, 6);
|
||
d.position.set(sx, 6.5, 60 + i * 16);
|
||
kompleks.add(d);
|
||
const l = new THREE.Mesh(new THREE.SphereGeometry(0.3, 8, 6), MAL.isik.clone());
|
||
l.position.set(sx, 10, 60 + i * 16);
|
||
lambaIzgarasi.push(l); // gündüz söner, gece yanar
|
||
kompleks.add(l);
|
||
}
|
||
}
|
||
|
||
// Güvenlik çiti (platform çevresi)
|
||
const citMal = new THREE.LineBasicMaterial({ color: 0x7e8894, transparent: true, opacity: 0.4 });
|
||
const citNokta = [];
|
||
const citYari = 46;
|
||
for (let i = 0; i <= 40; i++){
|
||
const t = i / 40;
|
||
const x = -citYari + t * citYari * 2;
|
||
citNokta.push(x, 3.2, -citYari, x, 6.4, -citYari);
|
||
citNokta.push(x, 3.2, citYari, x, 6.4, citYari);
|
||
}
|
||
const citGeo = new THREE.BufferGeometry();
|
||
citGeo.setAttribute('position', new THREE.Float32BufferAttribute(citNokta, 3));
|
||
kompleks.add(new THREE.LineSegments(citGeo, citMal));
|
||
|
||
/* =========================================================
|
||
ZAMAN DİLİMİ SİSTEMİ
|
||
========================================================= */
|
||
// Bulut katmanı (yavaşça döner — havada hareket hissi versin diye)
|
||
let bulutlar = null;
|
||
{
|
||
const bulutMal = new THREE.MeshBasicMaterial({
|
||
color: 0xffffff, transparent: true, opacity: 0.07, depthWrite: false, side: THREE.DoubleSide
|
||
});
|
||
bulutlar = new THREE.Group();
|
||
for (let i = 0; i < 5; i++){
|
||
// NUMEX-DÜZELTME (2026-10-22): Bulut katmanları kameraya çok yakın ve çok büyüktü (340 birim, y=150-238) — gökyüzünde dev lacivert üçgenler olarak görünüyordu. Katmanları ufka ve yukarı taşıyıp boyutlarını küçülttüm, saydamlığı düşürdüm.
|
||
const d = new THREE.Mesh(new THREE.PlaneGeometry(1600, 1600), bulutMal.clone());
|
||
d.rotation.x = -Math.PI / 2;
|
||
d.position.set((Math.random() - 0.5) * 900, 620 + i * 90, (Math.random() - 0.5) * 900);
|
||
d.material.opacity = 0.018 + Math.random() * 0.018;
|
||
bulutlar.add(d);
|
||
}
|
||
sahne.add(bulutlar);
|
||
}
|
||
|
||
// Atmosferik degaj (yer sıcaklığı bulanıklığı) — tek yerden yönetilen parlaklık
|
||
const havaUniform = { parlaklik: { value: 1 } };
|
||
|
||
const ZAMANLAR = [
|
||
// alt = ufuk çizgisi rengi (sıcak/parlak) · orta = geçiş bandı · ust = gökyüzü tepesi (koyu)
|
||
// NUMEX-DÜZELTME (2026-10-22): Şafak paletini fazla ısıtmıştım; gökyüzü ateş kırmızısına dönüp referanstaki lacivert-mor geçiş kaybolmuştu. Ufku sıcak, tepesi soğuk lacivert olan dengeye çektim.
|
||
{ ad: 'Şafak', alt: 0xf3b184, orta: 0x9b7fa8, ust: 0x182c4e, sis: 0xb8a094, sisYog: 0.0028,
|
||
gunesRenk: 0xffc48a, gunesGuc: 1.9, gokyuzuGuc: 0.85, gokyuzuInt: 0.7, pozlama: 1.02,
|
||
gunesPoz: [70, 30, -95], gece: 0 },
|
||
{ ad: 'Öğle', alt: 0xcfe2ee, orta: 0x8fbde2, ust: 0x2f6fae, sis: 0xc3d3dd, sisYog: 0.0017,
|
||
gunesRenk: 0xffffff, gunesGuc: 3.2, gokyuzuGuc: 0.15, gokyuzuInt: 1.2, pozlama: 1.0,
|
||
gunesPoz: [40, 160, -60], gece: 0 },
|
||
{ ad: 'Alacakaranlık', alt: 0xf0954a, orta: 0x6a4a66, ust: 0x18294a, sis: 0x8c6a58, sisYog: 0.0030,
|
||
gunesRenk: 0xff9b52, gunesGuc: 1.4, gokyuzuGuc: 1.3, gokyuzuInt: 0.5, pozlama: 1.06,
|
||
gunesPoz: [-90, 14, -70], gece: 0.35 },
|
||
// NUMEX-DÜZELTME (2026-10-22): Gece modu fazla siyahtı: dağ silüetleri kayboluyor, ipucu/panel metinleri okunmuyordu. Ufuk ve gökyüzü tonlarını bir kademe açtım, ay ışığını (directional) ve ortam ışığını yükselttim ki silüetler seçilebilsin.
|
||
{ ad: 'Gece', alt: 0x2a3f60, orta: 0x13233c, ust: 0x060c18, sis: 0x141f33, sisYog: 0.0026,
|
||
gunesRenk: 0x9db6e8, gunesGuc: 0.7, gokyuzuGuc: 0.10, gokyuzuInt: 0.55, pozlama: 1.35,
|
||
gunesPoz: [-50, 70, 60], gece: 1 }
|
||
];
|
||
|
||
const kaydirici = document.getElementById('kaydirici');
|
||
const zamanBasliklar = [...document.querySelectorAll('#zamanBasliklar span')];
|
||
let aktifZaman = 0;
|
||
|
||
function zamanUygula(t){ // t: 0..3 arası ondalık
|
||
const i0 = Math.floor(t) % 4;
|
||
const i1 = (i0 + 1) % 4;
|
||
const f = t - Math.floor(t);
|
||
const A = ZAMANLAR[i0], B = ZAMANLAR[i1];
|
||
|
||
const karis = (a, b) => new THREE.Color(a).lerp(new THREE.Color(b), f);
|
||
const say = (a, b) => a + (b - a) * f;
|
||
|
||
gokyuzuUniform.ustRenk.value.copy(karis(A.ust, B.ust));
|
||
gokyuzuUniform.ortaRenk.value.copy(karis(A.orta, B.orta));
|
||
gokyuzuUniform.altRenk.value.copy(karis(A.alt, B.alt));
|
||
gokyuzuUniform.gunesGuc.value = say(A.gokyuzuGuc, B.gokyuzuGuc);
|
||
gokyuzuUniform.geceFaktoru.value = say(A.gece, B.gece);
|
||
|
||
// Sahne lambaları: gece parlak, gündüz sönük
|
||
const geceF = say(A.gece, B.gece);
|
||
for (const lamba of lambaIzgarasi){
|
||
lamba.material.color.setRGB(
|
||
0.35 + geceF * 0.65,
|
||
0.32 + geceF * 0.55,
|
||
0.20 + geceF * 0.25
|
||
);
|
||
}
|
||
|
||
sahne.fog.color.copy(karis(A.sis, B.sis));
|
||
sahne.fog.density = say(A.sisYog, B.sisYog);
|
||
|
||
gunes.color.copy(karis(A.gunesRenk, B.gunesRenk));
|
||
gunes.intensity = say(A.gunesGuc, B.gunesGuc);
|
||
gunes.position.set(
|
||
say(A.gunesPoz[0], B.gunesPoz[0]),
|
||
say(A.gunesPoz[1], B.gunesPoz[1]),
|
||
say(A.gunesPoz[2], B.gunesPoz[2])
|
||
);
|
||
|
||
gokyuzuIsik.intensity = say(A.gokyuzuInt, B.gokyuzuInt);
|
||
dolguIsik.intensity = 0.35 + (1 - say(A.gece, B.gece)) * 0.35;
|
||
cizici.toneMappingExposure = say(A.pozlama, B.pozlama);
|
||
havaUniform.parlaklik.value = 1 - say(A.gece, B.gece) * 0.75;
|
||
}
|
||
|
||
// NUMEX-DÜZELTME (2026-10-22): Kök neden: etiket tıklaması 'input' olayı gönderiyordu ama dinleyici kaydırıcıda HENÜZ bağlanmamıştı — script sırası yüzünden tıklama hiçbir şey yapmıyordu. Ayrıca .aktif sınıfı CSS'te tanımlı değildi (.secili tanımlı). Zaman dilimini tek bir fonksiyona topladım; hem etiket hem kaydırıcı aynı fonksiyonu çağırıyor, doğru sınıf adı kullanılıyor.
|
||
// Zaman dilimini uygulayan TEK giriş noktası (etiket + kaydırıcı buradan geçer)
|
||
function zamaniAyarla(t){
|
||
const v = Math.max(0, Math.min(3.999, t));
|
||
aktifZaman = v;
|
||
zamanUygula(v);
|
||
const yakin = Math.round(v) % 4;
|
||
zamanBasliklar.forEach((s, i) => s.classList.toggle('secili', i === yakin));
|
||
kaydirici.value = String(Math.round(v / 3.999 * 1000));
|
||
}
|
||
|
||
zamanBasliklar.forEach((s, i) => {
|
||
s.addEventListener('click', () => zamaniAyarla(i));
|
||
});
|
||
|
||
kaydirici.addEventListener('input', () => {
|
||
zamaniAyarla(parseFloat(kaydirici.value) / 1000 * 3.999);
|
||
});
|
||
|
||
zamanUygula(0);
|
||
|
||
/* =========================================================
|
||
PARÇAYA TIKLAMA · BİLGİ KARTI
|
||
========================================================= */
|
||
const isinlayici = new THREE.Raycaster();
|
||
const fare = new THREE.Vector2();
|
||
let baslangicNoktasi = null;
|
||
let seciliParca = null;
|
||
|
||
// NUMEX-DÜZELTME (2026-10-22): Kök neden: kart, kartBaslik, kartMetin, kartYukseklik, kartAgirlik, kartKapat değişkenleri hiç tanımlanmamıştı; kod bunları global id'lerden otomatik alıyordu ve kartiGoster çağrısı sessizce patlayıp kart hiç açılmıyordu. Hepsi açıkça sorgulanıyor.
|
||
// Kart DOM referansları (açıkça tanımlı — global id tahminine bırakma)
|
||
const kart = document.getElementById('kart');
|
||
const kartBaslik = document.getElementById('kartBaslik');
|
||
const kartMetin = document.getElementById('kartMetin');
|
||
const kartYukseklik = document.getElementById('kartYukseklik');
|
||
const kartAgirlik = document.getElementById('kartAgirlik');
|
||
const kartKapat = document.getElementById('kartKapat');
|
||
|
||
function kartiGoster(veri){
|
||
kartBaslik.textContent = veri.ad;
|
||
kartMetin.textContent = veri.metin;
|
||
kartYukseklik.textContent = veri.yukseklik || '—';
|
||
kartAgirlik.textContent = veri.agirlik || '—';
|
||
kart.classList.add('acik');
|
||
}
|
||
function kartiKapat(){
|
||
kart.classList.remove('acik');
|
||
if (seciliParca){
|
||
seciliParca.traverse(o => {
|
||
if (o.isMesh && o.userData.ozelMalzeme && o.material.emissive){
|
||
o.material.emissiveIntensity = o.userData.ozelMalzeme.emissiveIntensity;
|
||
}
|
||
});
|
||
seciliParca = null;
|
||
}
|
||
}
|
||
kartKapat.addEventListener('click', kartiKapat);
|
||
|
||
// İmleç değişimi (hover)
|
||
let hoverZaman = 0;
|
||
tuval.addEventListener('pointermove', (e) => {
|
||
const simdi = performance.now();
|
||
if (simdi - hoverZaman < 70) return;
|
||
hoverZaman = simdi;
|
||
fare.x = (e.clientX / window.innerWidth) * 2 - 1;
|
||
fare.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
||
isinlayici.setFromCamera(fare, aktifKamera);
|
||
const bulunan = isinlayici.intersectObjects(tiklanabilir, false);
|
||
tuval.style.cursor = bulunan.length ? 'pointer' : 'grab';
|
||
});
|
||
|
||
tuval.addEventListener('pointerdown', (e) => {
|
||
baslangicNoktasi = { x: e.clientX, y: e.clientY };
|
||
});
|
||
|
||
tuval.addEventListener('pointerup', (e) => {
|
||
if (!baslangicNoktasi) return;
|
||
const gecti = Math.hypot(e.clientX - baslangicNoktasi.x, e.clientY - baslangicNoktasi.y);
|
||
baslangicNoktasi = null;
|
||
if (gecti > 6) return; // sürükleme sayılır, tıklama değil
|
||
|
||
fare.x = (e.clientX / window.innerWidth) * 2 - 1;
|
||
fare.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
||
isinlayici.setFromCamera(fare, aktifKamera);
|
||
const bulunan = isinlayici.intersectObjects(tiklanabilir, false);
|
||
|
||
if (!bulunan.length){ kartiKapat(); return; }
|
||
|
||
let hedef = bulunan[0].object;
|
||
while (hedef && !hedef.userData.bilgi) hedef = hedef.parent;
|
||
if (!hedef) return;
|
||
|
||
if (seciliParca === hedef){ kartiKapat(); return; }
|
||
if (seciliParca) kartiKapat();
|
||
|
||
seciliParca = hedef;
|
||
// Seçili parçayı hafifçe aydınlat
|
||
hedef.traverse(o => {
|
||
if (o.isMesh && o.material && o.material.emissive){
|
||
o.userData.ozelMalzeme = { emissiveIntensity: o.material.emissiveIntensity };
|
||
o.material.emissiveIntensity = o.userData.ozelMalzeme.emissiveIntensity + 0.35;
|
||
}
|
||
});
|
||
kartiGoster(hedef.userData.bilgi);
|
||
});
|
||
|
||
/* =========================================================
|
||
KAMERA SIFIRLA
|
||
========================================================= */
|
||
btnSifirla.addEventListener('click', () => {
|
||
kontroller.aciX = KAMERA_SIFIR.aciX;
|
||
kontroller.aciY = KAMERA_SIFIR.aciY;
|
||
kontroller.hedefMesafe = KAMERA_SIFIR.mesafe;
|
||
kontroller.hedef.copy(KAMERA_SIFIR.hedef);
|
||
});
|
||
|
||
/* Perspektif ↔ paralel (izometrik) kamera */
|
||
let paralelMi = false;
|
||
const persKamera = kamera;
|
||
const ortKamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.5, 6000);
|
||
ortKamera.position.copy(kamera.position);
|
||
ortKamera.lookAt(kontroller.hedef);
|
||
function kameraBoyutla(){
|
||
const oran = innerWidth / innerHeight;
|
||
const boy = 190;
|
||
ortKamera.left = -boy * oran;
|
||
ortKamera.right = boy * oran;
|
||
ortKamera.top = boy;
|
||
ortKamera.bottom = -boy;
|
||
ortKamera.updateProjectionMatrix();
|
||
}
|
||
kameraBoyutla();
|
||
|
||
btnParalel.addEventListener('click', () => {
|
||
paralelMi = !paralelMi;
|
||
// NUMEX-DÜZELTME (2026-10-22): Kök neden: geçiş sırasında k.position.copy(kamera.position) yazıyordu; perspektife dönerken kamera === persKamera olduğu için konum kendinden kopyalanıp bozulmuş orto kamera pozisyonuyla eziliyordu (kamera yerin altına düşüyordu). Konumu yörünge kontrolünden yeniden hesaplatıyorum.
|
||
aktifKamera = paralelMi ? ortKamera : persKamera;
|
||
kamerayiKonumla(); // konumu her zaman yörünge kontrolünden yeniden hesapla
|
||
aktifKamera.updateMatrixWorld();
|
||
btnParalel.textContent = paralelMi ? 'Kamera: Perspektif' : 'Kamera: Paralel';
|
||
});
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') kartiKapat();
|
||
if (e.key === 'r' || e.key === 'R') btnSifirla.click();
|
||
});
|
||
|
||
/* =========================================================
|
||
KAMERA SIFIRLAMA ANİMASYONU İÇİN YUMUŞAK GEÇİŞ
|
||
========================================================= */
|
||
// NUMEX-DÜZELTME (2026-10-22): Kök neden: yumuşak kamera geçişi (hedefKamera) kamera.position'a lerp yapıyordu; bu, yörünge kontrolünün her karede yazdığı konumla çakışıp kamerayı sürekli yarışa sokuyordu. Kontrol artık tek sahipli; kullanılmayan animasyon bloğu kaldırıldı.
|
||
/* Kamera tek sahipli: yörünge kontrolü her karede konumluyor
|
||
(yukarıdaki btnSifirla dinleyicisi konumu sıfırlıyor). */
|
||
|
||
/* =========================================================
|
||
PERFORMANS SAYAÇLARI
|
||
========================================================= */
|
||
let kareSayaci = 0;
|
||
let fpsZaman = performance.now();
|
||
let fps = 0;
|
||
|
||
function sayaciGuncelle(){
|
||
kareSayaci++;
|
||
const simdi = performance.now();
|
||
const gecen = simdi - fpsZaman;
|
||
if (gecen >= 500){
|
||
fps = Math.round((kareSayaci * 1000) / gecen);
|
||
kareSayaci = 0;
|
||
fpsZaman = simdi;
|
||
pFps.textContent = fps;
|
||
pUcgen.textContent = cizici.info.render.triangles.toLocaleString('tr-TR');
|
||
pCizim.textContent = cizici.info.render.calls;
|
||
}
|
||
}
|
||
|
||
/* =========================================================
|
||
BÖLÜMLER · Genel Bakış / Yapı / Fırlatma
|
||
========================================================= */
|
||
const SEKMELER = Array.from(document.querySelectorAll('.sekme'));
|
||
const olcuKatman = document.getElementById('olcuKatman');
|
||
const firlatmaPanel = document.getElementById('firlatmaPanel');
|
||
const geriSayimEl = document.getElementById('geriSayim');
|
||
const asamaAdEl = document.getElementById('asamaAd');
|
||
const perfPanel = document.getElementById('perf');
|
||
|
||
let bolum = 'genel';
|
||
|
||
/* ---- Ölçü çizgileri (Yapı bölümü) ---- */
|
||
const olcuNesneleri = [];
|
||
function olcuCizgisiOlustur(cls){
|
||
const el = document.createElement('div');
|
||
el.className = 'olcuCizgi ' + cls;
|
||
olcuKatman.appendChild(el);
|
||
return el;
|
||
}
|
||
function olcuUcuOlustur(){
|
||
const el = document.createElement('div');
|
||
el.className = 'olcuUc';
|
||
olcuKatman.appendChild(el);
|
||
return el;
|
||
}
|
||
function olcuEtiketOlustur(metin){
|
||
const el = document.createElement('div');
|
||
el.className = 'olcuEtiket';
|
||
el.textContent = metin;
|
||
olcuKatman.appendChild(el);
|
||
return el;
|
||
}
|
||
|
||
// Roketin gerçek sınırları (roket grubu y=11'de, gövde 0..40, koni 40..49, motor -6..0)
|
||
const ROKET_ALT = 11 - 7.4; // nozul ucu
|
||
const ROKET_UST = 11 + 49.4; // koni ucu
|
||
const ROKET_YARICAP = 3.1; // motor bölümü yarıçapı (en geniş)
|
||
|
||
const olcuBoy = olcuCizgisiOlustur('dikey');
|
||
const olcuBoyUst = olcuUcuOlustur();
|
||
const olcuBoyAlt = olcuUcuOlustur();
|
||
const olcuBoyEtiket = olcuEtiketOlustur('≈ 62 m · TEMSİLİ');
|
||
|
||
const olcuCap = olcuCizgisiOlustur('yatay');
|
||
const olcuCapSol = olcuUcuOlustur();
|
||
const olcuCapSag = olcuUcuOlustur();
|
||
const olcuCapEtiket = olcuEtiketOlustur('Ø 6,2 m · TEMSİLİ');
|
||
|
||
const olcuNot = document.createElement('div');
|
||
olcuNot.className = 'olcuNot';
|
||
olcuNot.textContent = 'Ölçüler temsilîdir · gerçek bir araç değildir';
|
||
olcuKatman.appendChild(olcuNot);
|
||
|
||
const v3 = new THREE.Vector3();
|
||
function dunyadanEkrana(x, y, z){
|
||
v3.set(x, y, z).project(aktifKamera);
|
||
return {
|
||
x: (v3.x * 0.5 + 0.5) * innerWidth,
|
||
y: (-v3.y * 0.5 + 0.5) * innerHeight,
|
||
gorunur: v3.z < 1
|
||
};
|
||
}
|
||
|
||
function olculeriGuncelle(goster){
|
||
const gorunur = goster ? 1 : 0;
|
||
const opaklik = String(gorunur);
|
||
olcuKatman.style.opacity = opaklik;
|
||
olcuKatman.style.transition = 'opacity .4s';
|
||
if (!goster) return;
|
||
|
||
// --- Boy ölçüsü: roketin sağında dikey çizgi ---
|
||
const ust = dunyadanEkrana(ROKET_YARICAP + 4, ROKET_UST, 0);
|
||
const alt = dunyadanEkrana(ROKET_YARICAP + 4, ROKET_ALT, 0);
|
||
if (ust.gorunur && alt.gorunur){
|
||
const x = ust.x;
|
||
const y1 = Math.min(ust.y, alt.y);
|
||
const y2 = Math.max(ust.y, alt.y);
|
||
olcuBoy.style.left = x + 'px';
|
||
olcuBoy.style.top = y1 + 'px';
|
||
olcuBoy.style.height = (y2 - y1) + 'px';
|
||
|
||
olcuBoyUst.style.left = x + 'px';
|
||
olcuBoyUst.style.top = y1 + 'px';
|
||
olcuBoyAlt.style.left = x + 'px';
|
||
olcuBoyAlt.style.top = y2 + 'px';
|
||
|
||
// Yatay kılavuz çizgiler
|
||
olcuBoyEtiket.style.left = (x + 58) + 'px';
|
||
olcuBoyEtiket.style.top = ((y1 + y2) / 2) + 'px';
|
||
}
|
||
|
||
// --- Çap ölçüsü: motor bölümü genişliği (yatay) ---
|
||
const sol = dunyadanEkrana(-ROKET_YARICAP, ROKET_ALT + 2.2, 0);
|
||
const sag = dunyadanEkrana(ROKET_YARICAP, ROKET_ALT + 2.2, 0);
|
||
if (sol.gorunur && sag.gorunur){
|
||
const y = sol.y;
|
||
olcuCap.style.left = Math.min(sol.x, sag.x) + 'px';
|
||
olcuCap.style.top = y + 'px';
|
||
olcuCap.style.width = Math.abs(sag.x - sol.x) + 'px';
|
||
|
||
olcuCapSol.style.left = sol.x + 'px';
|
||
olcuCapSol.style.top = y + 'px';
|
||
olcuCapSag.style.left = sag.x + 'px';
|
||
olcuCapSag.style.top = y + 'px';
|
||
|
||
olcuCapEtiket.style.left = ((sol.x + sag.x) / 2) + 'px';
|
||
olcuCapEtiket.style.top = (y - 26) + 'px';
|
||
}
|
||
}
|
||
|
||
/* ---- Yapı bölümü: parçalara ayrılma ----
|
||
Roketin DOĞRUDAN çocuklarını (düz seviye) alır; her biri kendi
|
||
yüksekliğine göre yukarı/aşağı kayar. Alt gruplara dokunmayız —
|
||
traverse yerel konumlar karıştırıyordu. */
|
||
const YAPI_PARCA = [];
|
||
roket.children.forEach(o => {
|
||
if (o.isMesh || o.isGroup){
|
||
YAPI_PARCA.push({
|
||
nesne: o,
|
||
basY: o.position.y,
|
||
basX: o.position.x,
|
||
basZ: o.position.z,
|
||
ad: (o.userData && o.userData.bilgi && o.userData.bilgi.ad) || o.type
|
||
});
|
||
}
|
||
});
|
||
// Gövde ana silindiri (cekirdek) tek parça ve en büyük; onu da listeye kat
|
||
if (!YAPI_PARCA.some(p => p.nesne === cekirdek)){
|
||
YAPI_PARCA.push({ nesne: cekirdek, basY: cekirdek.position.y, basX: cekirdek.position.x, basZ: cekirdek.position.z, ad: 'Çekirdek gövde' });
|
||
}
|
||
// Yüksekliğe göre sırala: en üstteki en çok yükselsin
|
||
YAPI_PARCA.sort((a, b) => b.basY - a.basY);
|
||
console.info('[YAPI] parça sayısı:', YAPI_PARCA.length,
|
||
YAPI_PARCA.slice(0, 8).map(p => p.basY.toFixed(1)));
|
||
|
||
let yapiOran = 0; // 0..1 (parçalara ayrılma)
|
||
let yapiHedef = 0;
|
||
|
||
function yapiUygula(t){
|
||
const n = Math.max(1, YAPI_PARCA.length);
|
||
YAPI_PARCA.forEach((p, i) => {
|
||
// Sırayla açılma (aşağıdan yukarı dalga)
|
||
const gecikme = (i / n) * 0.45;
|
||
const yerel = Math.min(1, Math.max(0, (t - gecikme) / (1 - gecikme || 1)));
|
||
const yumusak = yerel * yerel * (3 - 2 * yerel);
|
||
|
||
// Dikey açılma: her parça kendi yüksekliğinden uzağa, büyük parça çok açılır
|
||
const merkezY = 22;
|
||
const yon = p.basY >= merkezY ? 1 : -1;
|
||
const mesafe = Math.abs(p.basY - merkezY) / merkezY;
|
||
const miktar = 6 + mesafe * 16 + (p.nesne === cekirdek ? 4 : 0);
|
||
p.nesne.position.y = p.basY + yon * yumusak * miktar;
|
||
|
||
// Radyal açılma: parçalar gövdeden yanlara dağılır
|
||
const aci = (i / n) * Math.PI * 2 + 0.6;
|
||
const radyal = yumusak * (3.5 + (i % 5) * 1.8);
|
||
p.nesne.position.x = p.basX + Math.cos(aci) * radyal;
|
||
p.nesne.position.z = p.basZ + Math.sin(aci) * radyal;
|
||
|
||
// Gövde dilimleri hafif yatsın (yapı sökülüyormuş hissi)
|
||
if (p.nesne !== cekirdek){
|
||
p.nesne.rotation.z = Math.sin(i) * yumusak * 0.12;
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ---- Fırlatma bölümü: geri sayım → ateşleme → kalkış → ayrılma ---- */
|
||
let firlatmaAktif = false;
|
||
let firlatmaZaman = 0;
|
||
let kalkanRoketY = 11;
|
||
|
||
const ASAMALAR = [
|
||
{ t: 0.0, ad: 'Hazırlık', sayim: 'T-10' },
|
||
{ t: 3.0, ad: 'Geri sayım', sayim: 'T-7' },
|
||
{ t: 6.0, ad: 'Servis kolları çekiliyor', sayim: 'T-4' },
|
||
{ t: 9.0, ad: 'Ateşleme', sayim: 'T-0' },
|
||
{ t: 11.0, ad: 'Kalkış', sayim: 'T+2' },
|
||
{ t: 15.0, ad: 'Kademe ayrılması', sayim: 'T+6' },
|
||
{ t: 19.0, ad: 'Yörüngeye oturma', sayim: 'T+10' }
|
||
];
|
||
|
||
function firlatmaSifirla(){
|
||
firlatmaAktif = false;
|
||
firlatmaZaman = 0;
|
||
kalkanRoketY = 11;
|
||
roket.position.y = 11;
|
||
roket.rotation.z = 0;
|
||
geriSayimEl.textContent = 'T-10';
|
||
asamaAdEl.textContent = 'Hazırlık';
|
||
alevler.forEach(a => { a.visible = false; a.scale.setScalar(1); });
|
||
servisKollari.forEach(k => { k.rotation.z = k.userData.kapali; });
|
||
kademeAyrik.position.y = 0;
|
||
yapiHedef = 0;
|
||
}
|
||
|
||
function firlatmaBaslat(){
|
||
firlatmaSifirla();
|
||
firlatmaAktif = true;
|
||
firlatmaZaman = 0;
|
||
}
|
||
|
||
/* ---- ALEVLER: çekirdek + 4 booster nozulunun altında ---- */
|
||
const alevMal = new THREE.MeshBasicMaterial({
|
||
color: 0xffb45a, transparent: true, opacity: 0.92,
|
||
blending: THREE.AdditiveBlending, depthWrite: false
|
||
});
|
||
const alevIcMal = new THREE.MeshBasicMaterial({
|
||
color: 0xfff2d0, transparent: true, opacity: 0.95,
|
||
blending: THREE.AdditiveBlending, depthWrite: false
|
||
});
|
||
|
||
const alevler = [];
|
||
function alevEkle(x, y, z, r){
|
||
// Dış koni (turuncu)
|
||
const dis = new THREE.Mesh(new THREE.ConeGeometry(r, 7, 16, 1, true), alevMal);
|
||
dis.rotation.x = Math.PI; // aşağı baksın
|
||
dis.position.set(x, y - 3.5, z);
|
||
dis.visible = false;
|
||
roket.add(dis);
|
||
alevler.push(dis);
|
||
|
||
// İç koni (beyaz-sıcak)
|
||
const ic = new THREE.Mesh(new THREE.ConeGeometry(r * 0.55, 4.5, 12, 1, true), alevIcMal);
|
||
ic.rotation.x = Math.PI;
|
||
ic.position.set(x, y - 2.4, z);
|
||
ic.visible = false;
|
||
roket.add(ic);
|
||
alevler.push(ic);
|
||
}
|
||
// Çekirdek motor nozulları (roket içinde, y = -6.1)
|
||
for (let i = 0; i < 4; i++){
|
||
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
|
||
alevEkle(Math.cos(aci) * 1.5, -6.1, Math.sin(aci) * 1.5, 0.9);
|
||
}
|
||
// Booster nozulları (roket içinde, y = 2.2)
|
||
for (let i = 0; i < 4; i++){
|
||
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
|
||
alevEkle(Math.cos(aci) * 4.1, 2.2, Math.sin(aci) * 4.1, 1.05);
|
||
}
|
||
|
||
/* ---- SERVİS KOLLARI: kuleden rokete uzanan 3 kol ---- */
|
||
const servisKollari = [];
|
||
const kolMal = new THREE.MeshStandardMaterial({ color: 0x6b7684, roughness: 0.6, metalness: 0.75 });
|
||
[-22, -6, 10].forEach((y, i) => {
|
||
const kol = new THREE.Group();
|
||
kol.position.set(0, y, 0); // kule grubuna göre (kule -17,3,-2'de)
|
||
const govde = kutu(7, 0.55, 0.9, kolMal);
|
||
govde.position.x = 3.5;
|
||
kol.add(govde);
|
||
kol.userData.kapali = 0; // rokete uzanmış
|
||
kol.userData.acik = -1.15 - i * 0.08; // geri çekilmiş
|
||
kule.add(kol);
|
||
servisKollari.push(kol);
|
||
});
|
||
|
||
/* ---- KADEME AYRILMASI: üst kademe + burun ayrı gruba ---- */
|
||
const kademeAyrik = new THREE.Group();
|
||
roket.add(kademeAyrik);
|
||
// NUMEX-DÜZELTME (2026-10-22): Kule sahnesi roketin yanında havada uçuyordu: bu blok roket.children içinden y>30 olan her şeyi kademeAyrik grubuna taşıyordu ve YAPI_PARCA listesini de bozuyordu. kademeAyrik zaten boş bir grup; ayrılma animasyonu roket.rotation ve pozisyonla sağlandığından bu taşıma gereksiz — kaldırıldı.
|
||
// Not: üst kademe ayrılması roket.rotation.z + kademeAyrik konumu ile
|
||
// canlandırılıyor. Parçaları gruplar arasında taşımıyoruz — bu, sahnenin
|
||
// diğer nesnelerini (kule, küreler) yanlışlıkla rokete bağlıyordu.
|
||
|
||
function firlatmaGuncelle(dt){
|
||
if (!firlatmaAktif) return;
|
||
firlatmaZaman += dt;
|
||
|
||
// Aşama metni
|
||
let asama = ASAMALAR[0];
|
||
for (const a of ASAMALAR){
|
||
if (firlatmaZaman >= a.t) asama = a;
|
||
}
|
||
asamaAdEl.textContent = asama.ad;
|
||
const kalan = Math.max(0, 10 - firlatmaZaman);
|
||
geriSayimEl.textContent = firlatmaZaman < 10
|
||
? 'T-' + Math.ceil(kalan)
|
||
: 'T+' + Math.floor(firlatmaZaman - 10);
|
||
|
||
// Servis kolları 6-9 sn arası çekilir
|
||
const kolOran = Math.min(1, Math.max(0, (firlatmaZaman - 6) / 3));
|
||
servisKollari.forEach((k, i) => {
|
||
const yerel = Math.min(1, Math.max(0, (kolOran - i * 0.12) / 0.7));
|
||
const yumusak = yerel * yerel * (3 - 2 * yerel);
|
||
k.rotation.z = k.userData.kapali + (k.userData.acik - k.userData.kapali) * yumusak;
|
||
});
|
||
|
||
// Ateşleme: 9 sn'den sonra alev
|
||
if (firlatmaZaman >= 9){
|
||
const guc = Math.min(1, (firlatmaZaman - 9) / 1.5);
|
||
alevler.forEach((a, i) => {
|
||
a.visible = true;
|
||
const titresim = 1 + Math.sin(firlatmaZaman * 34 + i) * 0.12;
|
||
a.scale.set(guc * titresim, guc * (1.6 + Math.min(2.2, (firlatmaZaman - 9) * 0.8)) * titresim, guc * titresim);
|
||
});
|
||
}
|
||
|
||
// Kalkış: 11 sn'den sonra yüksel
|
||
if (firlatmaZaman >= 11){
|
||
const y = Math.min(1, (firlatmaZaman - 11) / 4);
|
||
kalkanRoketY = 11 + y * y * 150;
|
||
}
|
||
roket.position.y = kalkanRoketY;
|
||
|
||
// Kamera roketi takip etsin (kalkıştan sonra yukarı kaydır)
|
||
if (firlatmaZaman >= 11){
|
||
// NUMEX-DÜZELTME (2026-10-22): Kalkışta roket kadrajdan taşıyordu (burun üstten kesik): roket 150 birim yükselirken kamera yalnız 42 birim uzaklaşıyordu. Takip mesafesini yükselişle orantılı hale getirdim (96 + yükseliş*1.15) — roket hızla uzaklaşsa bile kadrajda kalır.
|
||
const yukselis = kalkanRoketY - 11; // 0 → 150
|
||
// NUMEX-DÜZELTME (2026-10-22): Kalkış takibi roketi ekranın %20'sine küçültüyor ve rampayı kadrajdan tamamen çıkarıyordu (hedef hem çok yukarı hem çok uzak). Takibi roketin yükselişinin ~%55'i kadar yukarı kaldırıp mesafeyi 96→118 aralığında tuttum; böylece roket %40 dolulukta kalır ve rampa altta referans olarak görünmeye devam eder.
|
||
// NUMEX-DÜZELTME (2026-10-22): Kamera takibi roketi kaçırıyordu (T+9'da roket ekran tepesinden çıkıyordu): hedef yalnız yükselişin %55'ini izliyordu. Hedefi roketin GERÇEK konumuna bağladım; roket artık kadrajdan çıkamaz.
|
||
// Hedef = roketin gerçek konumu (gövde ortası); roket asla kadrajdan çıkmaz
|
||
const hedefY = kalkanRoketY + 14;
|
||
kontroller.hedef.y += (hedefY - kontroller.hedef.y) * Math.min(1, dt * 4.0);
|
||
// Mesafe roket rahat sığacak kadar artsın, aşırı uzaklaşma yok
|
||
kontroller.hedefMesafe = Math.min(135, 96 + yukselis * 0.22);
|
||
}
|
||
|
||
// Kademe ayrılması: 15 sn'den sonra — ayrılan parçalar yana savrulur
|
||
if (firlatmaZaman >= 15){
|
||
const y = Math.min(1, (firlatmaZaman - 15) / 3.5);
|
||
const yumusak = y * y * (3 - 2 * y);
|
||
kademeAyrik.position.y = -yumusak * 18; // aşağıda kalır
|
||
kademeAyrik.position.x = yumusak * 9; // yana kayar
|
||
kademeAyrik.position.z = yumusak * 5;
|
||
kademeAyrik.rotation.z = -yumusak * 0.32; // yatıp savrulur
|
||
roket.rotation.z = yumusak * 0.06;
|
||
}
|
||
|
||
if (firlatmaZaman > 22) firlatmaAktif = false;
|
||
}
|
||
|
||
/* ---- Bölüm değiştirme ---- */
|
||
function bolumSec(yeni){
|
||
bolum = yeni;
|
||
SEKMELER.forEach(s => s.classList.toggle('aktif', s.dataset.bolum === yeni));
|
||
|
||
firlatmaPanel.classList.toggle('acik', yeni === 'firlatma');
|
||
olcuKatman.style.display = (yeni === 'yapi') ? 'block' : 'none';
|
||
|
||
if (yeni === 'yapi'){
|
||
// NUMEX-DÜZELTME (2026-10-22): Yapı bölümü hiç açılmıyordu: yapiHedef=1 atanıyordu ama hemen ardından çağrılan firlatmaSifirla() içinde yapiHedef=0 yapılıyordu (satır 1554), yani hedef hep sıfıra dönüyordu. Sıra düzeltildi: önce sıfırla, sonra hedefi ata.
|
||
firlatmaSifirla(); // önce temizle (yapiHedef'i 0 yapar)
|
||
yapiHedef = 1; // SONRA hedefi ata — sıra önemli
|
||
} else if (yeni === 'genel'){
|
||
yapiHedef = 0;
|
||
firlatmaSifirla();
|
||
} else if (yeni === 'firlatma'){
|
||
yapiHedef = 0;
|
||
firlatmaBaslat();
|
||
}
|
||
}
|
||
|
||
SEKMELER.forEach(s => s.addEventListener('click', () => bolumSec(s.dataset.bolum)));
|
||
|
||
/* Geliştirici kipi: Ctrl+Shift+D */
|
||
const GELISTIRICI_ANAHTARI = 'gokbey9-gelistirici';
|
||
let gelistiriciKipi = localStorage.getItem(GELISTIRICI_ANAHTARI) === '1';
|
||
function gelistiriciUygula(){
|
||
perfPanel.classList.toggle('acik', gelistiriciKipi);
|
||
}
|
||
gelistiriciUygula();
|
||
window.addEventListener('keydown', e => {
|
||
if (e.ctrlKey && e.shiftKey && (e.key === 'D' || e.key === 'd')){
|
||
e.preventDefault();
|
||
gelistiriciKipi = !gelistiriciKipi;
|
||
localStorage.setItem(GELISTIRICI_ANAHTARI, gelistiriciKipi ? '1' : '0');
|
||
gelistiriciUygula();
|
||
}
|
||
});
|
||
|
||
/* =========================================================
|
||
YÜKLENİYOR EKRANI + DÖNGÜ
|
||
========================================================= */
|
||
window.addEventListener('resize', () => {
|
||
// NUMEX-DÜZELTME (2026-10-22): Pencere boyutu değişince tuval yine sabit pencere ölçüsüne dönüyordu; artık CSS kutusundan ölçülen tek fonksiyon kullanılıyor.
|
||
kamera.aspect = window.innerWidth / window.innerHeight;
|
||
kamera.updateProjectionMatrix();
|
||
kameraBoyutla();
|
||
tuvalBoyutla();
|
||
});
|
||
|
||
const saat = new THREE.Clock();
|
||
|
||
function dongu(){
|
||
requestAnimationFrame(dongu);
|
||
const dt = saat.getDelta();
|
||
|
||
// NUMEX-DÜZELTME (2026-10-22): Kök neden: kaldırılan hedefKamera değişkenine döngüde hâlâ başvuruluyordu — tanımsız değişken ReferenceError fırlatıp animasyon döngüsünü her karede kırardı.
|
||
// (kamera konumu kontroller.update içinde tek noktadan yönetilir)
|
||
|
||
kontroller.update();
|
||
|
||
// Bölüm animasyonları
|
||
firlatmaGuncelle(dt);
|
||
yapiOran += (yapiHedef - yapiOran) * Math.min(1, dt * 3.2);
|
||
if (bolum === 'yapi') yapiUygula(yapiOran);
|
||
olculeriGuncelle(bolum === 'yapi');
|
||
|
||
// ÖLÇÜM (geçici): bölüm ve parça sayısını bir kez yazdır
|
||
if (!window.__numexOlcum){
|
||
window.__numexOlcum = true;
|
||
console.info('[YAPI] bolum=' + bolum + ' parca=' + YAPI_PARCA.length +
|
||
' oran=' + yapiOran.toFixed(2) + ' hedef=' + yapiHedef.toFixed(2));
|
||
}
|
||
|
||
// Rüzgârda ağır ağır salınan yıldız ışığı hissi için hafif ışık titremesi
|
||
const t = saat.getElapsedTime();
|
||
gunes.intensity *= 1 + Math.sin(t * 0.4) * 0.004;
|
||
|
||
// Yükselti etkisi: bulut düzlemini çok yavaş kaydır
|
||
if (bulutlar){
|
||
bulutlar.rotation.y = t * 0.004;
|
||
}
|
||
|
||
cizici.render(sahne, aktifKamera);
|
||
sayaciGuncelle();
|
||
}
|
||
|
||
// Yükleniyor ekranını kapat
|
||
setTimeout(() => {
|
||
const yuk = document.getElementById('yukleniyor');
|
||
if (yuk){
|
||
yuk.style.opacity = '0';
|
||
setTimeout(() => yuk.remove(), 700);
|
||
}
|
||
}, 600);
|
||
|
||
dongu();
|
||
|
||
/* =========================================================
|
||
BAŞLANGIÇ ZAMAN DİLİMİ PARLAKLIĞI (ipucu metni)
|
||
========================================================= */
|
||
</script>
|
||
</body>
|
||
</html>
|