Files
2026-10-27 01:05:45 +00:00

659 lines
27 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// NUMEX CORE — EVENT HORIZON
// Denetleyici: WebGL2 kurulumu, kamera durumu, UI bağlantısı, FPS ölçümü, adaptif kalite.
// Ana fizik hesabı shader.js içindeki fragment shader'dadır; bu dosya ona veri sağlar.
// GLOBAL HATA YAKALAYICI — her şeyden önce kurulur, hatayı ekranda gösterir
window.addEventListener('error', function (ev) {
var box = document.getElementById('errbox');
var ov = document.getElementById('overlay');
var ld = document.getElementById('ldbox');
if (box && ov) {
ov.classList.remove('gone');
ov.style.visibility = 'visible';
ov.style.opacity = '1';
if (ld) ld.style.display = 'none';
box.style.display = 'block';
var pre = document.getElementById('errtext');
if (pre) pre.textContent = (ev.message || '') + '\n' + (ev.filename || '') + ':' + (ev.lineno || '');
}
console.error('[NUMEX HATA]', ev.message, ev.filename, ev.lineno);
});
(function () {
'use strict';
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl2', {
antialias: false, alpha: false, depth: false, stencil: false,
preserveDrawingBuffer: true, powerPreference: 'high-performance'
});
// ---------------- HATA GÖSTERİMİ ----------------
const overlay = document.getElementById('overlay');
const ldbox = document.getElementById('ldbox');
const errbox = document.getElementById('errbox');
const errtext = document.getElementById('errtext');
function fail(title, detail) {
overlay.classList.remove('gone');
ldbox.style.display = 'none';
errbox.style.display = 'block';
errbox.querySelector('h2').textContent = title;
errtext.textContent = detail;
// Sayfa boş kalmasın: canvas'a okunabilir bir mesaj zemini çiz
try {
const ctx2 = canvas.getContext('2d');
if (ctx2) {
canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight;
ctx2.fillStyle = '#080a10'; ctx2.fillRect(0, 0, canvas.width, canvas.height);
ctx2.fillStyle = '#f87171'; ctx2.font = '16px monospace';
ctx2.fillText('GPU hatası — simülasyon çalışmıyor', 40, 60);
}
} catch (e) { /* yok say */ }
}
if (!gl) {
fail('WEBGL2 DESTEĞİ YOK', 'Bu tarayıcı WebGL2 sağlamıyor. Chrome/Edge güncel bir sürüm deneyin ya da donanım hızlandırmayı açın.');
return;
}
// ---- ÖLÇÜM API'si: sahne tamponundan gerçek piksel değerleri okur (test/doğrulama için)
window.numexProbe = function (nx, ny) {
// nx, ny ∈ [0,1] normalize ekran koordinatı (sol-alt orijin)
if (!window.__sceneT || !gl) return null;
const t = window.__sceneT;
const px = new Uint8Array(4);
// HDR (RGBA16F) hedefte readPixels UNSIGNED_BYTE çalışmaz → küçük bir kopya FBO kullan
try {
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, t.fbo);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, window.__probeFBO);
const x = Math.floor(nx * (t.w - 1)), y = Math.floor(ny * (t.h - 1));
gl.blitFramebuffer(x, y, x + 1, y + 1, 0, 0, 1, 1, gl.COLOR_BUFFER_BIT, gl.NEAREST);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, window.__probeFBO);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
return [px[0], px[1], px[2], px[3]];
} catch (e) {
return 'err:' + e.message;
}
};
// ---------------- SHADER DERLEME ----------------
function compile(type, src, label) {
const sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(sh) || '(boş log)';
throw new Error('[' + label + '] ' + log);
}
return sh;
}
function program(vs, fs, label) {
const p = gl.createProgram();
const v = compile(gl.VERTEX_SHADER, vs, label + ':vs');
const f = compile(gl.FRAGMENT_SHADER, fs, label + ':fs');
gl.attachShader(p, v); gl.attachShader(p, f);
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
throw new Error('[' + label + ':link] ' + (gl.getProgramInfoLog(p) || '(boş log)'));
}
gl.deleteShader(v); gl.deleteShader(f);
return p;
}
let mainProg, brightProg, blurProg, compProg;
try {
mainProg = program(window.VS_SOURCE, window.FS_SOURCE, 'raytracer');
brightProg = program(window.POST_VS, window.BRIGHT_FS, 'bright');
blurProg = program(window.POST_VS, window.BLUR_FS, 'blur');
compProg = program(window.POST_VS, window.COMPOSITE_FS, 'composite');
} catch (e) {
fail('SHADER DERLEME HATASI', (e && e.message) ? e.message : String(e));
return;
}
// uniform konumları önbelleğe al
const U = {};
const names = ['uRes', 'uTime', 'uCamPos', 'uCamBasis', 'uTanHalfFov',
'uDiskInner', 'uDiskOuter', 'uTempScale', 'uSpin', 'uTurb', 'uDoppler',
'uLensing', 'uExposure', 'uMode', 'uRadii', 'uQuality', 'uMaxSteps'];
names.forEach(n => U[n] = gl.getUniformLocation(mainProg, n));
const UB = {
tex: gl.getUniformLocation(brightProg, 'uTex'),
threshold: gl.getUniformLocation(brightProg, 'uThreshold'),
soft: gl.getUniformLocation(brightProg, 'uSoft')
};
const UL = {
tex: gl.getUniformLocation(blurProg, 'uTex'),
dir: gl.getUniformLocation(blurProg, 'uDir')
};
const UC = {
scene: gl.getUniformLocation(compProg, 'uScene'),
bloom: gl.getUniformLocation(compProg, 'uBloom'),
amt: gl.getUniformLocation(compProg, 'uBloomAmt'),
mode: gl.getUniformLocation(compProg, 'uMode')
};
const vao = gl.createVertexArray();
// Adaptif kalite çarpanı (0.25–1.0). resize() bunu kullanır → TANIMI ÖNCE olmalı.
let qualityLevel = 1.0;
// ---------------- PİKSEL TAMPONLARI ----------------
function makeTarget(w, h) {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, w, h, 0, gl.RGBA, gl.HALF_FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
const st = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return { tex, fbo, w, h, ok: st === gl.FRAMEBUFFER_COMPLETE };
}
function freeTarget(t) {
if (!t) return;
gl.deleteTexture(t.tex); gl.deleteFramebuffer(t.fbo);
}
// HDR desteklenmiyorsa düşük duyarlıklı hedef
let hdrOK = true;
let sceneT = makeTarget(8, 8);
if (!sceneT.ok) {
hdrOK = false;
freeTarget(sceneT);
sceneT = { tex: gl.createTexture(), fbo: gl.createFramebuffer(), w: 8, h: 8, ok: true, ldr: true };
}
function allocTarget(w, h) {
if (hdrOK) {
const t = makeTarget(w, h);
if (t.ok) return t;
hdrOK = false;
freeTarget(t);
}
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return { tex, fbo, w, h, ok: true, ldr: true };
}
let bloomA = null, bloomB = null;
// ölçüm için 1x1 RGBA8 kopya hedefi
(function initProbe() {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
window.__probeFBO = fbo;
})();
function resize(dpr) {
const scale = state.renderScale;
const w = Math.max(64, Math.floor(canvas.clientWidth * dpr * scale));
const h = Math.max(64, Math.floor(canvas.clientHeight * dpr * scale));
// Boyut gerçekten değişmediyse yeniden ayırma (her resize'da doku yenilemek pahalı)
if (sceneT && sceneT.w === w && sceneT.h === h && canvas.width === w && canvas.height === h) return;
canvas.width = w; canvas.height = h;
freeTarget(sceneT); freeTarget(bloomA); freeTarget(bloomB);
sceneT = allocTarget(w, h);
window.__sceneT = sceneT;
// Bloom hedefleri 1/8 çözünürlük: 4× daha az piksel.
// Bloom zaten bulanık bir efekt — tam çözünürlük gereksiz maliyet.
const bw = Math.max(16, w >> 3), bh = Math.max(16, h >> 3);
bloomA = allocTarget(bw, bh);
bloomB = allocTarget(bw, bh);
document.getElementById('hud-res').textContent = w + '×' + h;
}
// ---------------- DURUM ----------------
window.__state = null; // teşhis için (render içinde atanır)
const state = {
mass: 1.0,
dist: 18.0, // rs birimi
incl: 12, // derece
azim: 0.6, // radyan
fov: 52,
inner: 3.0,
outer: 12.0,
temp: 1.0,
spin: 1.0,
turb: 0.55,
doppler: true,
lensing: true,
steps: 130,
renderScale: 0.32,
bloom: 1.0,
exposure: 0.75,
adaptive: true,
radii: false,
speed: 1.0,
paused: false,
mode: 0, // 0 bilimsel, 1 sinematik
quality: 0.5
};
// kamera yumuşatma hedefleri (sıçramasız geçiş — TEST 7)
const cam = { dist: state.dist, incl: state.incl, azim: state.azim, fov: state.fov };
const camTarget = { dist: state.dist, incl: state.incl, azim: state.azim, fov: state.fov };
// ---------------- KAMERA ----------------
function updateCamera(dt) {
const k = 1 - Math.pow(0.001, dt); // üstel yumuşatma
cam.dist += (camTarget.dist - cam.dist) * k;
cam.incl += (camTarget.incl - cam.incl) * k;
cam.azim += (camTarget.azim - cam.azim) * k;
cam.fov += (camTarget.fov - cam.fov) * k;
state.dist = cam.dist; state.incl = cam.incl; state.azim = cam.azim; state.fov = cam.fov;
// Shader tüm uzunlukları r_s = 1 biriminde kullanır → kamera konumu da r_s cinsinden.
const th = cam.incl * Math.PI / 180;
const rRs = Math.max(cam.dist, 2.15);
camPos = [
Math.cos(th) * Math.cos(cam.azim) * rRs,
Math.sin(th) * rRs,
Math.cos(th) * Math.sin(cam.azim) * rRs
];
}
let camPos = [0, 2, 18];
function cameraBasis(pos) {
// ileri = -normalize(pos) (merkeze bak), sağ/yukarı dünya Y ekseni ile
const f = norm([-pos[0], -pos[1], -pos[2]]);
let upWorld = [0, 1, 0];
let rgt = cross(f, upWorld);
if (len(rgt) < 1e-4) rgt = [1, 0, 0];
rgt = norm(rgt);
const up = norm(cross(rgt, f));
return [rgt, up, f];
}
const len = v => Math.hypot(v[0], v[1], v[2]);
const norm = v => { const l = len(v) || 1; return [v[0] / l, v[1] / l, v[2] / l]; };
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
// ---------------- RENDER ----------------
let simTime = 0;
let lastT = performance.now();
let fpsAcc = 0, fpsFrames = 0, fps = 0, msAcc = 0;
let lastAdapt = 0;
function drawQuad() {
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
function render(dt) {
const w = canvas.width, h = canvas.height;
const th = cam.incl * Math.PI / 180;
const rs = state.mass;
// shader rs = 1 birimini kullanır: kamera konumu rs cinsinden
const rRs = Math.max(cam.dist, 2.15);
const pos = [
Math.cos(th) * Math.cos(cam.azim) * rRs,
Math.sin(th) * rRs,
Math.cos(th) * Math.sin(cam.azim) * rRs
];
const [R, Uv, F] = cameraBasis(pos);
// 1) ana ışın izleme → sceneT
gl.bindFramebuffer(gl.FRAMEBUFFER, sceneT.fbo);
gl.viewport(0, 0, sceneT.w, sceneT.h);
gl.useProgram(mainProg);
gl.uniform2f(U.uRes, sceneT.w, sceneT.h);
gl.uniform1f(U.uTime, simTime);
gl.uniform3f(U.uCamPos, pos[0], pos[1], pos[2]);
// WebGL matrisleri SÜTUN-öncelikli: her sütun bir eksen → mat3(e1,e2,e3)*v = e1*v.x + e2*v.y + e3*v.z
gl.uniformMatrix3fv(U.uCamBasis, false, new Float32Array([
R[0], R[1], R[2],
Uv[0], Uv[1], Uv[2],
F[0], F[1], F[2]
]));
gl.uniform1f(U.uTanHalfFov, Math.tan((cam.fov * Math.PI / 180) * 0.5));
gl.uniform1f(U.uDiskInner, Math.max(1.55, state.inner));
gl.uniform1f(U.uDiskOuter, Math.max(state.inner + 0.5, state.outer));
gl.uniform1f(U.uTempScale, state.temp);
gl.uniform1f(U.uSpin, state.spin);
gl.uniform1f(U.uTurb, state.turb);
gl.uniform1f(U.uDoppler, state.doppler ? 1 : 0);
gl.uniform1f(U.uLensing, state.lensing ? 1 : 0);
gl.uniform1f(U.uExposure, state.exposure);
gl.uniform1f(U.uMode, state.mode);
gl.uniform1f(U.uRadii, state.radii ? 1 : 0);
gl.uniform1i(U.uMaxSteps, Math.round(state.steps * qualityLevel));
window.__state = state; window.__pos = pos; window.__cam = cam;
drawQuad();
// 2) bloom: bright pass
gl.bindFramebuffer(gl.FRAMEBUFFER, bloomA.fbo);
gl.viewport(0, 0, bloomA.w, bloomA.h);
gl.useProgram(brightProg);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, sceneT.tex);
gl.uniform1i(UB.tex, 0);
// Bloom eşiği: gölgeye taşmayı azaltmak için yüksek tutulur
gl.uniform1f(UB.threshold, state.mode > 0.5 ? 0.62 : 0.75);
gl.uniform1f(UB.soft, 0.35);
drawQuad();
// 3) ayrılabilir blur (2 geçiş)
gl.useProgram(blurProg);
gl.uniform1i(UL.tex, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, bloomB.fbo);
gl.viewport(0, 0, bloomB.w, bloomB.h);
gl.bindTexture(gl.TEXTURE_2D, bloomA.tex);
gl.uniform2f(UL.dir, 1.6 / bloomA.w, 0);
drawQuad();
gl.bindFramebuffer(gl.FRAMEBUFFER, bloomA.fbo);
gl.bindTexture(gl.TEXTURE_2D, bloomB.tex);
gl.uniform2f(UL.dir, 0, 1.6 / bloomB.h);
drawQuad();
// 4) kompozit → ekran
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, w, h);
gl.useProgram(compProg);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, sceneT.tex);
gl.uniform1i(UC.scene, 0);
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, bloomA.tex);
gl.uniform1i(UC.bloom, 1);
gl.uniform1f(UC.amt, state.bloom * (state.mode > 0.5 ? 0.9 : 0.55));
gl.uniform1f(UC.mode, state.mode);
drawQuad();
}
// ---------------- ANA DÖNGÜ ----------------
const hudFps = document.getElementById('hud-fps');
const hudMs = document.getElementById('hud-ms');
const hudSteps = document.getElementById('hud-steps');
const hudQuality = document.getElementById('hud-quality');
const hudCam = document.getElementById('hud-cam');
const hudMode = document.getElementById('hud-mode');
function qualityLabel() {
const s = Math.round(state.steps * qualityLevel);
if (s >= 300) return 'YÜKSEK';
if (s >= 180) return 'ORTA';
if (s >= 90) return 'DÜŞÜK';
return 'MİNİMUM';
}
function frame(now) {
const dtReal = Math.min(0.1, (now - lastT) / 1000);
lastT = now;
updateCamera(dtReal * 2.2);
if (!state.paused) simTime += dtReal * state.speed;
const t0 = performance.now();
try {
render(dtReal);
} catch (e) {
if (!window.__renderErrShown) {
window.__renderErrShown = true;
fail('RENDER HATASI', (e && e.stack) ? e.stack : String(e));
}
window.__stopLoop = true; // döngüyü durdur, ekranda hata kalsın
return;
}
const t1 = performance.now();
// ölçümler
const frameMs = t1 - t0;
msAcc = msAcc * 0.9 + frameMs * 0.1;
fpsAcc += dtReal; fpsFrames++;
if (fpsAcc >= 0.5 && fpsFrames > 0) {
fps = fpsFrames / fpsAcc;
fpsAcc = 0; fpsFrames = 0;
hudFps.textContent = fps.toFixed(0);
hudMs.textContent = msAcc.toFixed(1) + ' ms';
hudSteps.textContent = Math.round(state.steps * qualityLevel);
hudQuality.textContent = 'KALİTE: ' + qualityLabel();
hudQuality.className = 'pill ' + (fps >= 45 ? 'ok' : fps >= 25 ? 'warn' : 'err');
hudCam.textContent = 'r = ' + cam.dist.toFixed(1) + ' r_s · θ = ' + cam.incl.toFixed(0) + '°';
}
// adaptif kalite (TEST 5): 60 FPS mümkün değilse örneklemeyi azalt
if (state.adaptive && now - lastAdapt > 1200 && fps > 0) {
lastAdapt = now;
// Hedef 60 FPS: altına düşünce adımları kıs, üstüne çıkınca geri al
if (fps < 50 && qualityLevel > 0.22) {
qualityLevel = Math.max(0.22, qualityLevel - 0.18);
} else if (fps > 58 && qualityLevel < 1.0) {
qualityLevel = Math.min(1.0, qualityLevel + 0.06);
}
}
// DİKKAT: burada requestAnimationFrame YOK — zinciri loop() yürütür.
}
// ---------------- UI BAĞLANTISI ----------------
function bindSlider(id, key, fmt, cb) {
const el = document.getElementById(id);
const vEl = document.getElementById('v-' + (id === 'speed' ? 'speed' : id));
const apply = () => {
const v = parseFloat(el.value);
state[key] = v;
if (vEl) vEl.textContent = fmt ? fmt(v) : v.toFixed(2);
if (cb) cb(v);
};
el.addEventListener('input', apply);
apply();
}
bindSlider('mass', 'mass', v => v.toFixed(2), v => {
document.getElementById('rs-val').textContent = v.toFixed(3);
document.getElementById('rph-val').textContent = (1.5 * v).toFixed(3);
document.getElementById('isco-val').textContent = (3 * v).toFixed(3);
});
bindSlider('dist', 'dist', v => v.toFixed(1), v => camTarget.dist = v);
bindSlider('incl', 'incl', v => v.toFixed(0), v => camTarget.incl = v);
bindSlider('fov', 'fov', v => v.toFixed(0), v => camTarget.fov = v);
bindSlider('inner', 'inner', v => v.toFixed(2));
bindSlider('outer', 'outer', v => v.toFixed(1));
bindSlider('temp', 'temp', v => v.toFixed(2));
bindSlider('spin', 'spin', v => v.toFixed(2));
bindSlider('turb', 'turb', v => v.toFixed(2));
bindSlider('steps', 'steps', v => v.toFixed(0));
bindSlider('scale', 'renderScale', v => v.toFixed(2), () => resize(devicePixelRatio));
bindSlider('bloom', 'bloom', v => v.toFixed(2));
bindSlider('exposure', 'exposure', v => v.toFixed(2));
bindSlider('speed', 'speed', v => v.toFixed(2));
document.getElementById('doppler').addEventListener('change', e => state.doppler = e.target.checked);
document.getElementById('adaptive').addEventListener('change', e => { state.adaptive = e.target.checked; });
document.getElementById('lensing-on').addEventListener('change', e => state.lensing = e.target.checked);
document.getElementById('radii-on').addEventListener('change', e => state.radii = e.target.checked);
// mod düğmeleri
const bsci = document.getElementById('mode-sci');
const bcin = document.getElementById('mode-cin');
function setMode(m) {
state.mode = m;
bsci.classList.toggle('on', m === 0);
bcin.classList.toggle('on', m === 1);
hudMode.textContent = m === 0 ? 'BİLİMSEL MOD' : 'SİNEMATİK MOD';
// sinematik mod yalnız sunum parametrelerini değiştirir (geometri aynı)
const bloomEl = document.getElementById('bloom');
state.bloom = m === 0 ? 0.7 : 1.35;
bloomEl.value = state.bloom;
document.getElementById('v-bloom').textContent = state.bloom.toFixed(2);
state.exposure = m === 0 ? 0.95 : 1.25;
document.getElementById('exposure').value = state.exposure;
document.getElementById('v-exposure').textContent = state.exposure.toFixed(2);
}
bsci.addEventListener('click', () => setMode(0));
bcin.addEventListener('click', () => setMode(1));
// kamera konumları
const presets = {
far: { dist: 26, incl: 14, fov: 52 },
orbit: { dist: 9.5, incl: 4, fov: 62 },
horizon: { dist: 3.0, incl: 2, fov: 86 }
};
function goPreset(p, name) {
tour.active = false;
document.getElementById('cam-tour').classList.remove('on');
document.getElementById('tour2').classList.remove('on');
camTarget.dist = p.dist;
camTarget.incl = p.incl;
camTarget.fov = p.fov;
document.getElementById('dist').value = p.dist;
document.getElementById('v-dist').textContent = p.dist.toFixed(1);
document.getElementById('incl').value = p.incl;
document.getElementById('v-incl').textContent = p.incl.toFixed(0);
document.getElementById('fov').value = p.fov;
document.getElementById('v-fov').textContent = p.fov.toFixed(0);
state.dist = cam.dist; // hedefe yumuşak geçiş
if (name) hudCam.textContent = name;
}
document.getElementById('cam-far').addEventListener('click', () => goPreset(presets.far, 'UZAK GÖZLEMCİ'));
document.getElementById('cam-orbit').addEventListener('click', () => goPreset(presets.orbit, 'DİSK YÖRÜNGESİ'));
document.getElementById('cam-horizon').addEventListener('click', () => goPreset(presets.horizon, 'OLAY UFKUNA YAKLAŞMA'));
document.getElementById('cam-reset').addEventListener('click', () => goPreset(presets.far, 'UZAK GÖZLEMCİ'));
// sinematik tur
const tour = { active: false, t: 0, start: null };
function toggleTour() {
tour.active = !tour.active;
document.getElementById('cam-tour').classList.toggle('on', tour.active);
document.getElementById('tour2').classList.toggle('on', tour.active);
if (tour.active) {
tour.t = 0;
tour.start = { dist: cam.dist, incl: cam.incl, azim: cam.azim };
}
}
document.getElementById('cam-tour').addEventListener('click', toggleTour);
document.getElementById('tour2').addEventListener('click', toggleTour);
// duraklat
const pauseBtn = document.getElementById('pause');
pauseBtn.addEventListener('click', () => {
state.paused = !state.paused;
pauseBtn.textContent = state.paused ? 'DEVAM ET' : 'DURAKLAT';
pauseBtn.classList.toggle('on', state.paused);
});
// panel katlama
document.getElementById('toggle-panel').addEventListener('click', () => {
const p = document.getElementById('panel');
p.classList.toggle('hide');
document.getElementById('toggle-panel').textContent = p.classList.contains('hide') ? '›' : '‹';
});
document.querySelectorAll('.sec h3').forEach(h => {
h.addEventListener('click', () => h.parentElement.classList.toggle('closed'));
});
// ---------------- FARE / DOKUNMA ----------------
let drag = null;
canvas.addEventListener('pointerdown', e => {
drag = { x: e.clientX, y: e.clientY, incl: camTarget.incl, azim: camTarget.azim, shift: e.shiftKey };
canvas.setPointerCapture(e.pointerId);
});
canvas.addEventListener('pointermove', e => {
if (!drag) return;
const dx = e.clientX - drag.x, dy = e.clientY - drag.y;
if (drag.shift) {
camTarget.incl = Math.max(-89, Math.min(89, drag.incl - dy * 0.25));
document.getElementById('incl').value = camTarget.incl;
document.getElementById('v-incl').textContent = camTarget.incl.toFixed(0);
} else {
camTarget.azim = drag.azim - dx * 0.005;
if (tour.active) { tour.active = false; document.getElementById('cam-tour').classList.remove('on'); document.getElementById('tour2').classList.remove('on'); }
}
});
canvas.addEventListener('pointerup', e => { drag = null; try { canvas.releasePointerCapture(e.pointerId); } catch (err) { } });
canvas.addEventListener('wheel', e => {
e.preventDefault();
camTarget.dist = Math.max(2.2, Math.min(60, camTarget.dist * (1 + Math.sign(e.deltaY) * 0.08)));
document.getElementById('dist').value = camTarget.dist;
document.getElementById('v-dist').textContent = camTarget.dist.toFixed(1);
if (tour.active) { tour.active = false; document.getElementById('cam-tour').classList.remove('on'); document.getElementById('tour2').classList.remove('on'); }
}, { passive: false });
// ---------------- SİNEMATİK TUR GÜNCELLEME ----------------
const tourFrames = [
{ dist: 26, incl: 14, fov: 52 },
{ dist: 12, incl: 6, fov: 58 },
{ dist: 7, incl: 42, fov: 66 },
{ dist: 4, incl: -8, fov: 80 },
{ dist: 18, incl: 22, fov: 54 }
];
let tourIdx = 0;
function updateTour(dt) {
if (!tour.active) return;
tour.t += dt;
camTarget.azim += dt * 0.12;
if (tour.t > 5.0) {
tour.t = 0;
tourIdx = (tourIdx + 1) % tourFrames.length;
const f = tourFrames[tourIdx];
camTarget.dist = f.dist; camTarget.incl = f.incl; camTarget.fov = f.fov;
document.getElementById('dist').value = f.dist;
document.getElementById('v-dist').textContent = f.dist.toFixed(1);
document.getElementById('incl').value = f.incl;
document.getElementById('v-incl').textContent = f.incl.toFixed(0);
document.getElementById('fov').value = f.fov;
document.getElementById('v-fov').textContent = f.fov.toFixed(0);
}
}
// Ana animasyon zinciri: loop() → tur güncellemesi + frame().
// frame() KENDİ İÇİNDE requestAnimationFrame ÇAĞIRMAZ; zinciri loop() yürütür
// (çift zincir = çift simülasyon hızı olurdu).
function loop(now) {
const dtReal = Math.min(0.1, (now - lastT) / 1000);
updateTour(dtReal);
frame(now);
if (!window.__stopLoop) requestAnimationFrame(loop);
}
// ---------------- BAŞLAT ----------------
function start() {
// Hata olsa bile perde kalksın; hata varsa ekranda görünsün
window.addEventListener('error', ev => {
fail('ÇALIŞMA HATASI', (ev.message || '') + '\n' + (ev.filename || '') + ':' + (ev.lineno || ''));
});
resize(devicePixelRatio || 1);
window.addEventListener('resize', () => resize(devicePixelRatio || 1));
// yöntem metni
document.getElementById('method-text').innerHTML =
'<b>Model:</b> Dönmeyen (Schwarzschild) kara delik. Işınlar kameradan geriye izlenir.<br><br>' +
'<b>Yörünge denklemi:</b> u″ = −u + 1.5·r<sub>s</sub>·u², u = 1/r. RK4 ile değişken adımda (Δφ ≈ 0.0015–0.06) integre edilir.<br><br>' +
'<b>Olay ufku:</b> u ≥ 1/r<sub>s</sub> bölgesine giren ışın yutulur → siyah piksel.<br>' +
'<b>Foton küresi:</b> r<sub>ph</sub> = 1.5 r<sub>s</sub>; görüntüdeki foton halkası bu küreye teğet geçen ışınlardan doğar — ikisi aynı şey değildir.<br><br>' +
'<b>Disk:</b> ışının y=0 düzlemiyle kesişimi üzerinden örneklenir (halka geometrisi değil). T(r) ∝ r<sup>−3/4</sup>(1−√(r<sub>in</sub>/r))<sup>1/4</sup>.<br><br>' +
'<b>Yaklaşımlar:</b> (1) Doppler/beaming faktörü basitleştirilmiş Kepler hızıyla (β = √(r<sub>s</sub>/2r)) uygulanır — tam relativistik ışın izleme değildir; (2) disk ince ve geometrik olarak opaktır, yükseklik yoktur; (3) ışık yolu sonlu adımda integre edilir (sayısal hata ~10⁻³ mertebesinde); (4) kara delik dönmüyor (spin a = 0).';
lastT = performance.now();
// ?norender=1 → tek kare bile çizmeden dur (performans teşhisi)
if (!new URLSearchParams(location.search).has('norender')) {
requestAnimationFrame(loop);
}
}
// Perdeyi kaldırmayı GARANTİ et: hata olsa bile sahne/arayüz görünsün.
overlay.classList.add('gone');
try {
start();
} catch (e) {
fail('BAŞLATMA HATASI', (e && e.stack) ? e.stack : String(e));
}
})();