669 lines
26 KiB
JavaScript
669 lines
26 KiB
JavaScript
/* =====================================================================
|
||
İ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); }
|