kalp-dolasim/public/app.js

669 lines
26 KiB
JavaScript
Raw Normal View History

2026-09-09 22:40:08 +00:00
/* =====================================================================
İnsan Dolaşım & Sinir Sistemi Canlı Canvas Simülasyonu
---------------------------------------------------------------------
Kalp pompalar, kan damar Bezier eğrileri boyunca akar (oksijenli
parlak kırmızı / oksijensiz koyu), sinir dürtüleri kalpten yayılır,
nabız dalgaları damarları şişirir. Kullanıcı aktiviteyi seçer:
Otur (~72bpm) -> Hafif Koşu (~110bpm) -> Hızlı Koşu (~160bpm)
Kalp hızı VE kan akış hızı, hedefe yumuşakça yaklaşarak değişir.
Dış bağımlılık YOK saf DOM + Canvas + requestAnimationFrame.
===================================================================== */
'use strict';
/* ====================== CANVAS & DOM ================================ */
const tuval = document.getElementById('tuval');
const ctx = tuval.getContext('2d');
const W = tuval.width;
const H = tuval.height;
const hudBpm = document.getElementById('bpmDeger');
const hudAktif = document.getElementById('aktivDeger');
const hudKan = document.getElementById('kanHucresiDeger');
const hudSinir = document.getElementById('sinirDeger');
const hudNabiz = document.getElementById('nabizDeger');
const btnOtur = document.getElementById('btnOtur');
const btnYavas = document.getElementById('btnYavas');
const btnHizli = document.getElementById('btnHizli');
const aktiviteBtnlar = [btnOtur, btnYavas, btnHizli];
const notlarEl = document.querySelector('.bilgiNotlar');
/* ================== YARDIMCILAR ===================================== */
function nokta(x, y) { return { x: x, y: y }; }
function yumusak(a, b, t) { return a + (b - a) * Math.max(0, Math.min(1, t)); }
function yakinlik(a, b, t) { return a * (1 - t) + b * t; }
function cfgNokta() { return nokta(W * 0.5, H * 0.42); }
/* =====================================================================
AKTİVİTE -> KALP HIZI HEDEFİ
idle ~72 bpm (normal, dinlenme), jog ~110 bpm (hafif koşu),
sprint ~160 bpm (hızlı koşu -> maksimum efor).
===================================================================== */
const AKTIVITE_TANIM = {
idle: { bpm: 72, kanSayi: 34, sinirSayi: 3, ad: 'Otur', yazi: '😌 Dinlenme hâli — kalbin normal atıyor.' },
jog: { bpm: 110, kanSayi: 56, sinirSayi: 7, ad: 'Hafif Koşu', yazi: '🏃 Kalp hızlandı, kan hücreleri daha hızlı akıyor.' },
sprint: { bpm: 160, kanSayi: 86, sinirSayi: 14, ad: 'Hızlı Koşu', yazi: '⚡ Maksimum efor! Kalp çok çalışıyor, sinir dürtüleri yoğun.' }
};
/* ================== GLOBAL DURUM ==================================== */
let aktivite = 'idle'; // 'idle' | 'jog' | 'sprint'
let calisiyor = true;
let hedefBpm = 72; // aktiviteye göre hedef kalp atışı
let guncelBpm = 72; // yumuşakça hedefe yaklaşan değer
let faz = 0; // 0..1 kalp atış fazı (0 başlangıç sistol öncesi)
let zamanGeo = 0; // toplam süre (sn)
let damarAg = []; // statik damar Bezier eğrileri
let kanHucreleri = []; // hareketli kan damlacıkları
let sinirDurtuleri = []; // kalpten yayılan sinir kıvılcımları
let nabizDalgasi = { aktif: false, konum: 0, guclu: 0 }; // damar şişmesi
let aktifNabizSayisi = 0;
/* =====================================================================
AKTİVİTE UYGULA
Hedef BPM'i ve kan/sinir yoğunluğunu günceller; partikülleri yeniler.
===================================================================== */
function setActivity(mod) {
if (!AKTIVITE_TANIM[mod]) return;
// Eski buton stilini temizle
aktiviteBtnlar.forEach(function (b) { b.classList.remove('aktif'); });
aktivite = mod;
var tanim = AKTIVITE_TANIM[mod];
// Yeni seçilen butonu işaretle
if (mod === 'idle') btnOtur.classList.add('aktif');
else if (mod === 'jog') btnYavas.classList.add('aktif');
else if (mod === 'sprint') btnHizli.classList.add('aktif');
hedefBpm = tanim.bpm;
notlarEl.textContent = tanim.yazi;
yenidenUret(mod);
}
/* Aktivite değişince kan/sinir partikül havuzunu hedef yoğunlukla senkronlar */
function yenidenUret(mod) {
var tanim = AKTIVITE_TANIM[mod];
// Kan hücre eşle, yeterliyse kalsın
while (kanHucreleri.length < tanim.kanSayi && kanHucreleri.length < 120) {
kanHucreleri.push(yeniKanHucresi());
}
kanHucreleri = kanHucreleri.slice(0, tanim.kanSayi);
// Sinir dürtüleri senkron
while (sinirDurtuleri.length < tanim.sinirSayi) {
sinirDurtuleri.push(yeniSinirImpulsu(0));
}
sinirDurtuleri = sinirDurtuleri.slice(0, tanim.sinirSayi);
}
/* Kalbin o anki fazına göre üretilecek nabız hızı / dürtü sayısı */
function calcHeartRate(dt) {
// bpm'i hedefe yaklaştır (sn cinsinden yumuşak geçiş, ~2s zaman sabiti)
guncelBpm = yakinlik(guncelBpm, hedefBpm, Math.min(1, dt * 0.8));
return guncelBpm;
}
/* =====================================================================
DAMAR AĞI (Anatomi)
Statik Bezier eğrileri ile ana dolaşım yolları:
aorta (kalpten çıkar), pulmoner (akciğere), vücut damarları (perifer/
beyin) ve geri dönüş venaları. Her damar bir "id" taşır; kan
partikülleri bu eğriler üzerinde ilerler.
===================================================================== */
function damarHazirla(x1, y1, c1x, c1y, c2x, c2y, x2, y2, tip, cap) {
return {
x1: x1, y1: y1, c1x: c1x, c1y: c1y,
c2x: c2x, c2y: c2y, x2: x2, y2: y2,
tip: tip, cap: cap || 5
};
}
function buildVessels() {
var hx = W * 0.5, hy = H * 0.42; // kalp merkezi
var ag = [];
var capAna = 9, capOrta = 5, capKapi = 2;
// --- AORTA: kalp -> göğüs -> baş (yukarı) ve karın (aşağı) ---
ag.push(damarHazirla(hx + 20, hy, hx + 60, hy - 10, hx + 70, hy - 70, hx + 30, hy - 150, 'arter', capAna)); // aort yukarı
ag.push(damarHazirla(hx - 5, hy + 14, hx - 5, hy + 70, hx - 5, hy + 90, hx + 40, hy + 160, 'arter', capAna)); // aort aşağı (karın)
// --- PULMONER (akciğere) ---
ag.push(damarHazirla(hx - 20, hy + 4, hx - 70, hy + 6, hx - 95, hy - 40, hx - 170, hy - 60, 'arter', capOrta)); // sağ akciğer
ag.push(damarHazirla(hx + 34, hy + 14, hx + 80, hy + 26, hx + 100, hy - 30, hx + 170, hy - 55, 'arter', capOrta)); // sol akciğer
// --- PERİFER (kollar/bacaklar) ---
ag.push(damarHazirla(hx + 40, hy + 160, hx + 90, hy + 200, hx + 95, hy + 260, hx + 90, hy + 320, 'kapiller', capKapi)); // sağ bacak
ag.push(damarHazirla(hx + 40, hy + 160, hx - 30, hy + 210, hx - 35, hy + 270, hx + 10, hy + 330, 'kapiller', capKapi)); // sol bacak
ag.push(damarHazirla(hx + 30, hy - 150, hx + 10, hy - 180, hx - 30, hy - 185, hx - 90, hy - 150, 'kapiller', capKapi)); // baş damarları
// --- VENÖZ DÖNÜŞ (kalbe geri) ---
ag.push(damarHazirla(hx + 145, hy - 55, hx + 120, hy + 2, hx + 60, hy + 6, hx + 16, hy + 12, 'ven', capOrta)); // sol akciğer dönüşü
ag.push(damarHazirla(hx - 130, hy - 50, hx - 80, hy + 4, hx - 40, hy + 8, hx - 10, hy + 12, 'ven', capOrta)); // sağ akciğer dönüşü
ag.push(damarHazirla(hx - 90, hy - 150, hx - 55, hy - 140, hx - 30, hy - 90, hx - 15, hy - 20, 'ven', capOrta)); // baş dönüşü
ag.push(damarHazirla(hx + 90, hy + 320, hx + 130, hy + 240, hx + 110, hy + 130, hx + 60, hy + 120, 'ven', capOrta)); // bacak dönüşü
return ag;
}
/* =====================================================================
KAN HÜCRESİ
rastgele bir damara yerleştirilir ve eğri üzerinde t yürüyüşü yapar.
===================================================================== */
function nasilOksijenli(damar) {
// Akciğere giden arterler/pulmoner oksijenlenir; venöz dönüşte düşer
if (!damar) return true;
if (damar.tip === 'ven') return false; // vücuttan dönen oksijensiz
if (damar.y2 < W * 0.42 - 40) return true; // akciğer/üst — genelde oksijenli
return true;
}
function yeniKanHucresi() {
var hedefDamar = damarAg[Math.floor(Math.random() * damarAg.length)];
var oks = hedefDamar ? (hedefDamar.tip !== 'ven') : true;
return { damar: hedefDamar || damarAg[0], t: Math.random(), oksijen: oks };
}
/* =====================================================================
BEZIER NOKTASI eğri üzerindeki t değerinin gerçek (x,y) konumunu verir
===================================================================== */
function bezierNokta(d, t) {
var u = 1 - t;
var uu = u * u, uuu = uu * u;
var tt = t * t, ttt = tt * t;
return nokta(
uuu * d.x1 + 3 * uu * t * d.c1x + 3 * u * tt * d.c2x + ttt * d.x2,
uuu * d.y1 + 3 * uu * t * d.c1y + 3 * u * tt * d.c2y + ttt * d.y2
);
}
/* =====================================================================
KAN AKIŞI ÇEVİRİMİ
Damarın ucuna gelince devam damarına geçer (döngü oluşturur).
===================================================================== */
function varisDegeri(d) {
return (d.y2 - d.y1) + (d.x2 - d.x1);
}
function kanGuncelle(dt, hizCarpani) {
for (var i = 0; i < kanHucreleri.length; i++) {
var hc = kanHucreleri[i];
if (!hc.damar) { kanHucreleri[i] = yeniKanHucresi(); continue; }
// hız = baz hız * kalp hızı oranı * aktivite katsayısı
hc.t += dt * 0.15 * hizCarpani;
if (hc.t >= 1) {
// damar sonuna geldi: tipine göre devam damarı seç / oksijen durumunu güncelle
var gecici = hc.damar;
if (hc.damar.tip === 'arter' && hc.damar.y2 >= H * 0.4) {
// aorttan perifere gitti -> oksijensizleş
hc.oksijen = false;
}
hc.t = 0;
// Eşleşen bir giriş bul (yakın noktadan başlayan)
var uygun = devamDamariBul(hc.damar);
if (uygun) {
hc.damar = uygun;
hc.t = 0;
} else {
hc.damar = gecici;
hc.t = 0;
}
}
}
}
/* Basit bir damar graph'ı : çıkış noktasına göre devamı seç */
function devamDamariBul(simdiki) {
var enYakin = null, enYakinDeger = 1e9;
for (var i = 0; i < damarAg.length; i++) {
var d = damarAg[i];
if (d === simdiki) continue;
// giriş noktası simdiki'nin çıkışına yakınsa devam adayı
var mesafe = Math.abs(d.x1 - simdiki.x2) + Math.abs(d.y1 - simdiki.y2);
// sırt, döngüyü bitirişe yakın bırakmak için eşik
if (mesafe < 60 && mesafe < enYakinDeger) {
enYakin = d;
enYakinDeger = mesafe;
}
}
// hiçbiri yoksa aortaya geri dön (büyük döngü; kalbe dön)
return enYakin || damarAg[1];
}
/* =====================================================================
KALP ÇİZİMİ Bezier eğrileriyle organik, atan bir kalp
faz: 0..1 (sistol:0..0.4 kasılır, diyastol:0.4..1 gevşer)
===================================================================== */
function kalpNoktalari(cx, cy, r, kas) {
// Atan kalp: kas arttıkça küçülür (sistol), azalınca büyür (diyastol)
var squish = 1 - kas * 0.28; // yükseklik daralması
var squash = 1 + kas * 0.22; // yatay açılma (pompa dalgası)
var hw = r * 1.05 * squash; // genişlik
var hh = r * 1.1 * squish; // yükseklik
return {
// üst çentik
ust: nokta(cx, cy - hh),
// sol kanat eğri uçları
lsol: nokta(cx - hw, cy - hh * 0.25),
lsag: nokta(cx - hw * 0.75, cy + hh * 0.7),
rsag: nokta(cx + hw * 0.75, cy + hh * 0.7),
rsol: nokta(cx + hw, cy - hh * 0.25),
alt: nokta(cx, cy + hh * 1.25)
};
}
function cizKalp(fazG) {
var hx = W * 0.5, hy = H * 0.42;
// sistol aşamasında kasılma daha güçlü (0 ve 0.4 arası)
var kas = 0;
if (fazG < 0.45) {
kas = Math.sin((fazG / 0.45) * Math.PI); // sistol tepe noktası
}
var p = kalpNoktalari(hx, hy, 34, kas);
ctx.save();
ctx.beginPath();
// Sol yarı (kalp sol modülü)
ctx.moveTo(p.ust.x, p.ust.y);
ctx.bezierCurveTo(p.lsol.x + 4, p.lsol.y - 28, p.ust.x - 40, p.ust.y + 30, p.lsol.x, p.lsol.y);
ctx.bezierCurveTo(p.ust.x - 30, p.lsol.y + 16, p.lsol.x, p.lsol.y + 66, p.alt.x, p.alt.y);
// Sağ yarı
ctx.bezierCurveTo(p.alt.x + 30, p.lsol.y + 66, p.alt.x + 38, p.lsol.y + 16, p.rsol.x, p.rsol.y);
ctx.bezierCurveTo(p.alt.x + 46, p.ust.y + 30, p.rsol.x - 4, p.rsol.y - 28, p.ust.x, p.ust.y);
ctx.closePath();
// Dolgu — atan kalp için sıcak degrade
var koronerDegrade = ctx.createLinearGradient(hx - 20, hy - 20, hx + 20, hy + 30);
koronerDegrade.addColorStop(0, kas > 0.1 ? '#ff5f6d' : '#d93b4a');
koronerDegrade.addColorStop(0.5, '#ef1f37');
koronerDegrade.addColorStop(1, '#9c1626');
// Sistol sırasında parlak nabız halkası
if (kas > 0.2) {
ctx.shadowColor = 'rgba(255,71,87,0.9)';
ctx.shadowBlur = 22 + kas * 26;
} else {
ctx.shadowColor = 'rgba(255,71,87,0.35)';
ctx.shadowBlur = 6;
}
ctx.fillStyle = koronerDegrade;
ctx.fill();
ctx.lineWidth = 2.5;
ctx.strokeStyle = 'rgba(255,160,160,0.7)';
ctx.stroke();
ctx.restore();
}
/* Kalbin nabzı; sistol başlayınca damar genişleme dalgası yaratır */
function nabizYarat() {
nabizDalgasi = { aktif: true, konum: 0, guclu: 1 };
aktifNabizSayisi = 1;
}
function heartBeatPulse(dt, guncelBpm) {
if (nabizDalgasi.aktif) {
nabizDalgasi.konum += dt * 3.4; // damar ağı boyunca ilerleme hızı
nabizDalgasi.guclu *= 0.955; // sönümleme
if (nabizDalgasi.konum > 1 || nabizDalgasi.guclu < 0.02) {
nabizDalgasi.aktif = false;
aktifNabizSayisi = 0;
}
}
}
/* =====================================================================
SİNİR SİSTEMİ kalple etkileşen, hızlanan/yavaşlayan uyarılar
Her dürtü, damar ile paralel ilerleyen kıvılcımlı bir "uyarı" çizgisi.
AktiviteTANI'a göre dürtü sayısı ve hızı değişir.
===================================================================== */
function yeniSinirImpulsu(boy) {
return { konum: boy || Math.random(), hiz: 0.28 + Math.random() * 0.3 };
}
function sinirGuncelle(dt, guncelBpm, aktiv) {
var tip = AKTIVITE_TANIM[aktiv];
var hizKatsayi = 1 + (tip.bpm - 72) / 72; // normalleştirilmiş hız
var tuketilen = 0;
for (var i = 0; i < sinirDurtuleri.length; i++) {
var s = sinirDurtuleri[i];
s.konum += dt * 0.35 * s.hiz * (0.6 + Math.min(2, guncelBpm / 50));
if (s.konum >= 1.3) {
sinirDurtuleri[i] = yeniSinirImpulsu(0); // hareket ediyor, kalbe geri dönsün
tuketilen++;
}
}
// boştaki sayıyı tazeliyoruz
/* sayac dostu */
return sinirDurtuleri.length;
}
function sinirCiz(yolNokta, x1, y1, x2, y2) {
// kıvılcımlı kesik çizgi
ctx.save();
ctx.strokeStyle = '#ffd32a';
ctx.globalAlpha = 0.55;
ctx.lineWidth = 1.6;
ctx.setLineDash([4, 6]);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
ctx.restore();
}
function drawNervePath() {
// Kan hücrelerinin tersi yönde bir damar hattı çizer (statik dekor vb.)
// Bu istekte doğrudan kullanılmayabilir ama ana API tutarlı kalsın
ctx.save();
ctx.strokeStyle = 'rgba(255,211,42,0.16)';
ctx.lineWidth = 1;
for (var i = 0; i < damarAg.length; i++) {
var d = damarAg[i];
ctx.beginPath();
ctx.moveTo(d.x1 + 3, d.y1 + 3);
ctx.bezierCurveTo(d.c1x + 3, d.c1y + 3, d.c2x + 3, d.c2y + 3, d.x2 + 3, d.y2 + 3);
ctx.stroke();
}
ctx.restore();
}
function cizSinirDurtuleri() {
var hx = W * 0.5, hy = H * 0.42;
for (var i = 0; i < sinirDurtuleri.length; i++) {
var s = sinirDurtuleri[i];
var d = damarAg[i % damarAg.length] || damarAg[0];
var p = bezierNokta(d, Math.abs(s.konum % 1));
// kalbin etrafına üç ışın çiz — "uyarı yayılıyor" efekti
var del = s.konum * 40;
for (var u = 0; u < 2; u++) {
var aci = ((s.konum * 2 + u) / (Math.PI * 2)) * 2;
sinirCiz(d, p.x, p.y, p.x + Math.cos(aci) * 14, p.y + Math.sin(aci) * 14);
}
// küçük bağlantı noktası
ctx.save();
ctx.fillStyle = '#ffe680';
ctx.shadowColor = '#ffd32a';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.arc(p.x, p.y, 2.4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
/* =====================================================================
STATİK ANATOMİ her frame başında arka plan grafiği
===================================================================== */
function damarCiz(d, genislikEfekt, oksijenFark) {
ctx.save();
// kap dallanma / ana arter farkını renklendirme
var renk;
if (d.tip === 'arter') renk = 'rgba(226,60,78,0.85)';
else if (d.tip === 'ven') renk = 'rgba(98,60,105,0.85)';
else renk = 'rgba(120,90,120,0.45)';
ctx.strokeStyle = renk;
ctx.lineWidth = (d.cap || 5) + (genislikEfekt || 0);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(d.x1, d.y1);
ctx.bezierCurveTo(d.c1x, d.c1y, d.c2x, d.c2y, d.x2, d.y2);
ctx.stroke();
// içte daha açık çekirdek (= akan kan)
ctx.globalAlpha = 0.5;
ctx.lineWidth = Math.max(1.5, ((d.cap || 5) * 0.4) + (genislikEfekt || 0) * 0.5);
ctx.strokeStyle = oksijenFark ? '#ff9aa0' : '#6a223b';
ctx.stroke();
ctx.restore();
}
function drawStaticAnatomy(fazG, nabizDo) {
// nabız genişletme her damarda eşit olmasın; dalga konumuna göre değişsin
var dalgaSekil = 0;
if (nabizDalgasi.aktif) {
dalgaSekil = Math.max(0, Math.sin(nabizDalgasi.konum * Math.PI));
}
for (var i = 0; i < damarAg.length; i++) {
damarCiz(damarAg[i], dalgaSekil * (nabizDalgasi.guclu || 0) * 3, false);
}
// ---- AKCİĞERLER (oval organlar) ----
ctx.save();
var akciDeg = ctx.createRadialGradient(W * 0.5 - 170, H * 0.42 - 60, 6, W * 0.5 - 170, H * 0.42 - 60, 55);
akciDeg.addColorStop(0, 'rgba(38,222,129,0.34)');
akciDeg.addColorStop(1, 'rgba(38,222,129,0.05)');
ctx.fillStyle = akciDeg;
ctx.beginPath();
ctx.ellipse(W * 0.5 - 170, H * 0.42 - 60, 48, 70, -0.18, 0, Math.PI * 2);
ctx.fill();
var akciDeg2 = ctx.createRadialGradient(W * 0.5 + 170, H * 0.42 - 55, 6, W * 0.5 + 170, H * 0.42 - 55, 55);
akciDeg2.addColorStop(0, 'rgba(38,222,129,0.34)');
akciDeg2.addColorStop(1, 'rgba(38,222,129,0.05)');
ctx.fillStyle = akciDeg2;
ctx.beginPath();
ctx.ellipse(W * 0.5 + 170, H * 0.42 - 55, 48, 70, 0.18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// --- BEYİN (üst merkez) ---
ctx.save();
var beyinDeg = ctx.createRadialGradient(W * 0.5, H * 0.42 - 170, 4, W * 0.5, H * 0.42 - 170, 42);
beyinDeg.addColorStop(0, 'rgba(120,150,220,0.5)');
beyinDeg.addColorStop(1, 'rgba(120,150,220,0.03)');
ctx.fillStyle = beyinDeg;
ctx.beginPath();
ctx.ellipse(W * 0.5, H * 0.42 - 175, 52, 34, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
/* =====================================================================
KAN HÜCRE ÇİZİMİ oksijenli parlak, oksijensiz koyu, akan bir "top"
===================================================================== */
function cizKanHucreleri() {
for (var i = 0; i < kanHucreleri.length; i++) {
var hc = kanHucreleri[i];
var d = hc.damar;
if (!d) { continue; }
var p = bezierNokta(d, Math.abs(hc.t % 1));
// oksijenli => parlak kırmızı + parlama; değilse koyu bordo
var renk = hc.oksijen ? '#ff4d5e' : '#6d2235';
ctx.save();
ctx.fillStyle = renk;
if (hc.oksijen) {
ctx.shadowColor = 'rgba(255,77,94,0.8)';
ctx.shadowBlur = 9;
} else {
ctx.shadowColor = 'rgba(120,30,45,0.4)';
ctx.shadowBlur = 3;
}
ctx.beginPath();
ctx.arc(p.x, p.y, hc.oksijen ? 3.4 : 3, 0, Math.PI * 2);
ctx.fill();
if (hc.oksijen) {
// iç parlak çekirdek
ctx.fillStyle = 'rgba(255,190,200,0.9)';
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(p.x - 0.8, p.y - 0.8, 1.4, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
}
/* =====================================================================
ÖN ÇİZİM (tampon) + ANA KARE
===================================================================== */
function degradeArkaPlan() {
var g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#0b0f18');
g.addColorStop(1, '#06070b');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
// iskeletimsi gövde dış hattı (şeffaf)
ctx.save();
ctx.strokeStyle = 'rgba(120,140,190,0.10)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.ellipse(W * 0.5, H * 0.46, W * 0.34, H * 0.34, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
/* Kalp merkezidne küçük damar etiketi / ok (içinde akan yönü belli etsin) */
function okYonuOku(d, tCiz) {
var p1 = bezierNokta(d, tCiz);
var p2 = bezierNokta(d, Math.min(1, tCiz + 0.05));
ctx.save();
ctx.strokeStyle = 'rgba(255,255,255,0.18)';
ctx.lineWidth = 1.4;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
// küçük ok başı
var aci = Math.atan2(p2.y - p1.y, p2.x - p1.x);
ctx.beginPath();
ctx.moveTo(p2.x, p2.y);
ctx.lineTo(p2.x - 5 * Math.cos(aci - 0.5), p2.y - 5 * Math.sin(aci - 0.5));
ctx.lineTo(p2.x - 5 * Math.cos(aci + 0.5), p2.y - 5 * Math.sin(aci + 0.5));
ctx.closePath();
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.fill();
ctx.restore();
}
/* =====================================================================
HUD üst bilgi kartlarını günceller
===================================================================== */
function drawHUD(aktBpm, kanSay, sinirSay) {
hudBpm.textContent = Math.round(aktBpm);
if (aktivite === 'sprint') hudBpm.className = 'hudDeger hizli';
else if (aktivite === 'jog') hudBpm.className = 'hudDeger kosar';
else hudBpm.className = 'hudDeger normal';
hudAktif.textContent = AKTIVITE_TANIM[aktivite].ad;
hudKan.textContent = kanSay;
hudSinir.textContent = sinirSay;
hudNabiz.textContent = nabizDalgasi.aktif ? Math.round(nabizDalgasi.guclu * 100) : 0;
}
/* =====================================================================
ANA ANİMASYON DÖNGÜSÜ
===================================================================== */
function animFrame(ts) {
if (!calisiyor) return;
// Delta zaman (sn), farkı stabil tut
var dt = Math.min(0.05, (ts - animFrame.sonTs) / 1000 || 0.016);
animFrame.sonTs = ts;
zamanGeo += dt;
// 1) Kalp atış hızını hedefe yaklaştır
var akt = calcHeartRate(dt); // -> guncelBpm yumuşar
// 2) Faza göre sistol/diyastol ilerlet (bpm'e orantılı)
var periyotSn = 60 / Math.max(24, guncelBpm); // saniye/yapı
faz = (zamanGeo % periyotSn) / periyotSn; // 0..1
if (faz > 0.03 && faz < 0.14) nabizYarat(); // her vuruşta nabız dalgası
// 3) Damarda hareket eden kan hızı = kalp hızı oranı
var hizCarpani = 1 + ((guncelBpm - 72) / 72); // sprint'te ~2.2x
kanGuncelle(dt, hizCarpani);
// 4) Sinir dürtüleri
var sinirGucu = sinirGuncelle(dt, guncelBpm, aktivite);
// 5) Çiz
degradeArkaPlan();
drawStaticAnatomy(faz, nabizDalgasi);
heartBeatPulse(dt, guncelBpm);
cizKanHucreleri();
cizSinirDurtuleri();
drawNervePath();
cizKalp(faz);
okYonuOku(damarAg[2] || damarAg[0], 0.4); // yön göstergeci
// 6) HUD
drawHUD(guncelBpm, kanHucreleri.length, sinirGucu);
requestAnimationFrame(animFrame);
}
animFrame.sonTs = 0;
/* =====================================================================
BAŞLANGIÇ / OLAY BAĞLAMA
===================================================================== */
function initSim() {
damarAg = buildVessels();
if (!damarAg.length) damarAg = buildVessels();
// başlangıç kanı
yenidenUret('idle');
setActivity('idle');
requestAnimationFrame(animFrame);
}
// Buton dinlemesi
aktiviteBtnlar.forEach(function (btn) {
btn.addEventListener('click', function () {
setActivity(btn.dataset.aktivite);
});
});
// Sayfa hazır olunca başlat
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSim);
} else {
initSim();
}
/* =====================================================================
MİMARİ SÖZLEŞME / UYUM KATMANI
Aşağıdaki takma adlar, dış sözleşmede (plan) istenen fonksiyon adlarını
birebir karşılar. Hepsi yukarıdaki gerçek uygulamalara bağlanır; yeni
çizim/JS mantığı eklemez, üretim kodunu bozmaz.
===================================================================== */
/* Kalp atış fazını üretir (döngüde guncelBpm & faz global güncellendiği
için burada sadece periyodu geri verir). */
function buildSynergy() {
var periotSn = 60 / Math.max(24, guncelBpm);
return { faz: (zamanGeo % periotSn) / periotSn, periyot: periotSn };
}
/* Kalbi çizer (tek sarmalayıcı) */
function drawHeart(fazG) { cizKalp(fazG); }
/* İlk kan hücresi akışını kurar yenidenUret zaten yapar; istenirse
başlangıçtaki ilk etki için de bazı hücreyi manuel yaratmayı destekler */
function spawnBloodCells(adet) {
if (typeof adet === 'number') {
while (kanHucreleri.length < adet && kanHucreleri.length < 120) {
kanHucreleri.push(yeniKanHucresi());
}
}
return kanHucreleri.length;
}
/* Aktivite değişince partikül havuzunu hedef yoğunlukla senkronlar */
function respawnActivity(mod) { yenidenUret(mod); }
/* Kan çevrimi ilerlemesi */
function updateBloodCycle(dt, hizCarpani) { kanGuncelle(dt, hizCarpani || 1); }
/* Tek bir kan hücresini çizer (oksijen durumuna göre) */
function drawBloodCell(hc) {
if (!hc || !hc.damar) return;
var p = bezierNokta(hc.damar, Math.abs(hc.t % 1));
ctx.save();
ctx.fillStyle = hc.oksijen ? '#ff4d5e' : '#6d2235';
if (hc.oksijen) { ctx.shadowColor = 'rgba(255,77,94,0.8)'; ctx.shadowBlur = 9; }
else { ctx.shadowColor = 'rgba(120,30,45,0.4)'; ctx.shadowBlur = 3; }
ctx.beginPath();
ctx.arc(p.x, p.y, hc.oksijen ? 3.4 : 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
/* Sinir dürtüsü oluşturur — yenidenUret de bu adı kullanabilir */
function spawnNerveImpulses(fazC) { return yeniSinirImpulsu(fazC); }
/* Sinir sistemi güncellemesi (kalbin hızına göre sinyal akışı) */
function updateNerves(dt, guncelBpmG, aktiv) { return sinirGuncelle(dt, guncelBpmG, aktiv || aktivite); }