kalp-dolasim/public/app.js

669 lines
26 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.

/* =====================================================================
İ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); }