488 lines
16 KiB
JavaScript
488 lines
16 KiB
JavaScript
/**
|
|
* body-render.js — Canvas Render Katmani
|
|
* ---------------------------------------------------------------
|
|
* Sadece OKUR (state, anatomy, particles) ve CIZER.
|
|
* Hicbir fizyoloji hesabi burada yapilmaz.
|
|
* ---------------------------------------------------------------
|
|
*/
|
|
(function (root) {
|
|
'use strict';
|
|
|
|
const A = root.NumexAnatomy;
|
|
const P = root.NumexParticles;
|
|
|
|
const COLORS = {
|
|
bg: '#05070d',
|
|
grid: 'rgba(56, 189, 248, 0.055)',
|
|
bodyFill: 'rgba(30, 41, 59, 0.55)',
|
|
bodyStroke: 'rgba(94, 234, 212, 0.55)',
|
|
organFill: 'rgba(148, 163, 184, 0.20)',
|
|
organStroke: 'rgba(94, 234, 212, 0.65)',
|
|
artery: 'rgba(239, 68, 68, 0.30)',
|
|
vein: 'rgba(59, 130, 246, 0.30)',
|
|
nerve: 'rgba(168, 85, 247, 0.32)',
|
|
motor: '#f59e0b',
|
|
sensory: '#22d3ee',
|
|
bloodO2: '#ef4444',
|
|
bloodDeO2: '#3b82f6',
|
|
text: '#e2e8f0',
|
|
dim: 'rgba(226, 232, 240, 0.45)'
|
|
};
|
|
|
|
/** t'nin renkli SpO2 gostergesi (dusukse morarir/donuklasir). */
|
|
function bloodTint(spo2, isArtery) {
|
|
// SpO2 98 → parlak kirmizi; 80 → koyu kizil/morumsu
|
|
const health = Math.min(1, Math.max(0, (spo2 - 70) / 28));
|
|
if (isArtery) {
|
|
const r = Math.round(120 + 135 * health);
|
|
const g = Math.round(20 + 48 * health);
|
|
const b = Math.round(40 + 30 * health);
|
|
return `rgb(${r},${g},${b})`;
|
|
}
|
|
// Ven: dusuk oksijende koyu mavi → cok dusukse mora kayar
|
|
const r = Math.round(30 + 60 * (1 - health));
|
|
const g = Math.round(80 + 50 * health);
|
|
const b = Math.round(160 + 60 * health);
|
|
return `rgb(${r},${g},${b})`;
|
|
}
|
|
|
|
function createRenderer(canvas) {
|
|
const ctx = canvas.getContext('2d');
|
|
const R = {
|
|
canvas, ctx,
|
|
w: 0, h: 0, dpr: 1,
|
|
pad: 24,
|
|
|
|
/** Canvas boyutunu ayarla (high-DPI). */
|
|
resize() {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
R.dpr = dpr;
|
|
canvas.width = Math.round(rect.width * dpr);
|
|
canvas.height = Math.round(rect.height * dpr);
|
|
R.w = rect.width;
|
|
R.h = rect.height;
|
|
},
|
|
|
|
/** Normalize (0..1) → ekran pikseli. Vucut dikey sigdirilir. */
|
|
sx(nx) {
|
|
const scale = R.drawH / 1.0;
|
|
const cx = R.w / 2;
|
|
return cx + (nx - 0.5) * scale;
|
|
},
|
|
sy(ny) { return R.pad + ny * R.drawH; },
|
|
|
|
/** Cizim olcegi: yukseklige gore. */
|
|
computeScale() {
|
|
R.drawH = R.h - R.pad * 2;
|
|
R.drawScale = R.drawH;
|
|
},
|
|
|
|
clear() {
|
|
const g = ctx.createLinearGradient(0, 0, 0, R.h);
|
|
g.addColorStop(0, '#05070d');
|
|
g.addColorStop(0.5, '#070b14');
|
|
g.addColorStop(1, '#05070d');
|
|
ctx.fillStyle = g;
|
|
ctx.fillRect(0, 0, R.w, R.h);
|
|
R.drawGrid();
|
|
},
|
|
|
|
drawGrid() {
|
|
ctx.save();
|
|
ctx.strokeStyle = COLORS.grid;
|
|
ctx.lineWidth = 1;
|
|
const step = 40;
|
|
for (let x = 0; x < R.w; x += step) {
|
|
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, R.h); ctx.stroke();
|
|
}
|
|
for (let y = 0; y < R.h; y += step) {
|
|
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(R.w, y); ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
},
|
|
|
|
/** Govde silueti. */
|
|
drawBody() {
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
A.BODY_OUTLINE.forEach((p, i) => {
|
|
const x = R.sx(p[0]), y = R.sy(p[1]);
|
|
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
});
|
|
ctx.closePath();
|
|
|
|
const g = ctx.createLinearGradient(0, R.pad, 0, R.h - R.pad);
|
|
g.addColorStop(0, 'rgba(30, 41, 59, 0.65)');
|
|
g.addColorStop(0.5, 'rgba(21, 30, 45, 0.75)');
|
|
g.addColorStop(1, 'rgba(30, 41, 59, 0.65)');
|
|
ctx.fillStyle = g;
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = COLORS.bodyStroke;
|
|
ctx.lineWidth = 1.6;
|
|
ctx.shadowColor = 'rgba(94, 234, 212, 0.35)';
|
|
ctx.shadowBlur = 14;
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
},
|
|
|
|
/** Damarlar (arka plan cizgileri). */
|
|
drawVessels() {
|
|
ctx.save();
|
|
A.VESSELS.forEach(v => {
|
|
ctx.beginPath();
|
|
v.pts.forEach((p, i) => {
|
|
const x = R.sx(p[0]), y = R.sy(p[1]);
|
|
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
});
|
|
ctx.strokeStyle = v.type === 'artery' ? COLORS.artery : COLORS.vein;
|
|
ctx.lineWidth = Math.max(1.2, v.calibre * R.drawScale / 620);
|
|
ctx.lineCap = 'round';
|
|
ctx.lineJoin = 'round';
|
|
ctx.stroke();
|
|
});
|
|
ctx.restore();
|
|
},
|
|
|
|
/** Sinir yollari (arka plan). */
|
|
drawNerves(state) {
|
|
ctx.save();
|
|
A.NERVES.forEach(n => {
|
|
const active = state.lastTarget === n.target;
|
|
ctx.beginPath();
|
|
n.pts.forEach((p, i) => {
|
|
const x = R.sx(p[0]), y = R.sy(p[1]);
|
|
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
});
|
|
ctx.strokeStyle = active
|
|
? (n.type === 'motor' ? COLORS.motor : COLORS.sensory)
|
|
: COLORS.nerve;
|
|
ctx.lineWidth = active ? 2.2 : 1.4;
|
|
if (active) {
|
|
ctx.shadowColor = n.type === 'motor' ? COLORS.motor : COLORS.sensory;
|
|
ctx.shadowBlur = 12;
|
|
}
|
|
ctx.setLineDash(active ? [] : [4, 5]);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
ctx.shadowBlur = 0;
|
|
});
|
|
ctx.restore();
|
|
},
|
|
|
|
/** Organlar. Kalp kasilma fazina gore buyur/kucukler. */
|
|
drawOrgans(state) {
|
|
const o = A.ORGANS;
|
|
|
|
// --- Akcigerler (solunum fazina gore sisip iner) ---
|
|
const breath = Math.sin(state.respPhase * Math.PI * 2);
|
|
const expand = 1 + breath * 0.07;
|
|
[o.lungL, o.lungR].forEach(lung => {
|
|
const rx = lung.rx * R.drawScale * (0.5 + 0.5 * expand - 0.5 + 0.5);
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.ellipse(
|
|
R.sx(lung.cx), R.sy(lung.cy),
|
|
lung.rx * R.drawScale, lung.ry * R.drawScale * expand,
|
|
0, 0, Math.PI * 2
|
|
);
|
|
const g = ctx.createRadialGradient(
|
|
R.sx(lung.cx), R.sy(lung.cy), 2,
|
|
R.sx(lung.cx), R.sy(lung.cy), lung.rx * R.drawScale
|
|
);
|
|
const o2 = Math.min(1, (state.spo2 - 60) / 40);
|
|
g.addColorStop(0, `rgba(148, 163, 184, ${0.30 + 0.25 * o2})`);
|
|
g.addColorStop(1, 'rgba(71, 85, 105, 0.18)');
|
|
ctx.fillStyle = g;
|
|
ctx.fill();
|
|
ctx.strokeStyle = 'rgba(94, 234, 212, 0.5)';
|
|
ctx.lineWidth = 1.2;
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
});
|
|
|
|
// --- Diyafram ---
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.ellipse(
|
|
R.sx(o.diaphragm.cx), R.sy(o.diaphragm.cy + breath * 0.012),
|
|
o.diaphragm.rx * R.drawScale, o.diaphragm.ry * R.drawScale,
|
|
0, 0, Math.PI * 2
|
|
);
|
|
ctx.strokeStyle = 'rgba(94, 234, 212, 0.35)';
|
|
ctx.lineWidth = 1.4;
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
|
|
// --- Omurilik ---
|
|
ctx.save();
|
|
const sc = o.spinalCord;
|
|
ctx.beginPath();
|
|
ctx.moveTo(R.sx(sc.x), R.sy(sc.y1));
|
|
ctx.lineTo(R.sx(sc.x), R.sy(sc.y2));
|
|
ctx.strokeStyle = 'rgba(168, 85, 247, 0.55)';
|
|
ctx.lineWidth = 2.4 + state.spinalLoad * 3.5;
|
|
ctx.shadowColor = 'rgba(168, 85, 247, 0.8)';
|
|
ctx.shadowBlur = 6 + state.spinalLoad * 18;
|
|
ctx.stroke();
|
|
ctx.shadowBlur = 0;
|
|
ctx.restore();
|
|
|
|
// --- Beyin ---
|
|
ctx.save();
|
|
const br = o.brain;
|
|
ctx.beginPath();
|
|
ctx.ellipse(
|
|
R.sx(br.cx), R.sy(br.cy),
|
|
br.rx * R.drawScale, br.ry * R.drawScale, 0, 0, Math.PI * 2
|
|
);
|
|
const bg = ctx.createRadialGradient(
|
|
R.sx(br.cx), R.sy(br.cy), 1,
|
|
R.sx(br.cx), R.sy(br.cy), br.rx * R.drawScale
|
|
);
|
|
const brainGlow = 0.25 + state.spinalLoad * 0.5;
|
|
bg.addColorStop(0, `rgba(168, 85, 247, ${0.35 + brainGlow * 0.4})`);
|
|
bg.addColorStop(1, 'rgba(88, 28, 135, 0.25)');
|
|
ctx.fillStyle = bg;
|
|
ctx.shadowColor = 'rgba(168, 85, 247, 0.7)';
|
|
ctx.shadowBlur = 8 + state.spinalLoad * 20;
|
|
ctx.fill();
|
|
ctx.strokeStyle = 'rgba(216, 180, 254, 0.75)';
|
|
ctx.lineWidth = 1.4;
|
|
ctx.stroke();
|
|
ctx.shadowBlur = 0;
|
|
|
|
// beyin kirisik dokusu
|
|
ctx.strokeStyle = 'rgba(216, 180, 254, 0.28)';
|
|
ctx.lineWidth = 0.9;
|
|
for (let i = -1; i <= 1; i++) {
|
|
ctx.beginPath();
|
|
ctx.ellipse(R.sx(br.cx), R.sy(br.cy + i * 0.012),
|
|
br.rx * R.drawScale * (0.7 - Math.abs(i) * 0.15),
|
|
br.ry * R.drawScale * 0.5, 0, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
|
|
// --- Kalp (kasilma ile buyur) ---
|
|
ctx.save();
|
|
const h = o.heart;
|
|
const c = state.contract; // 0..1
|
|
const rBase = h.r * R.drawScale;
|
|
const r = rBase * (1 - c * 0.16); // sistolde kuculur
|
|
const hx = R.sx(h.cx), hy = R.sy(h.cy);
|
|
|
|
// kasilma halo
|
|
if (c > 0.02) {
|
|
ctx.beginPath();
|
|
ctx.arc(hx, hy, rBase * (1.5 + c * 0.7), 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(239, 68, 68, ${c * 0.14})`;
|
|
ctx.fill();
|
|
}
|
|
|
|
ctx.beginPath();
|
|
// kalp sekli: iki daire + konik ucta birlestirme
|
|
ctx.arc(hx - r * 0.34, hy - r * 0.22, r * 0.62, 0, Math.PI * 2);
|
|
ctx.arc(hx + r * 0.34, hy - r * 0.22, r * 0.62, 0, Math.PI * 2);
|
|
ctx.moveTo(hx - r * 0.94, hy - r * 0.14);
|
|
ctx.lineTo(hx, hy + r * 1.25);
|
|
ctx.lineTo(hx + r * 0.94, hy - r * 0.14);
|
|
ctx.closePath();
|
|
|
|
const hg = ctx.createRadialGradient(hx, hy, 1, hx, hy, r * 1.5);
|
|
const heat = Math.min(1, state.hr / 180);
|
|
hg.addColorStop(0, `rgba(${220 + 35 * heat}, ${60 + 30 * (1 - heat)}, ${70}, 0.85)`);
|
|
hg.addColorStop(1, 'rgba(120, 20, 35, 0.55)');
|
|
ctx.fillStyle = hg;
|
|
ctx.shadowColor = 'rgba(239, 68, 68, 0.85)';
|
|
ctx.shadowBlur = 10 + c * 30;
|
|
ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
ctx.strokeStyle = `rgba(254, 202, 202, ${0.5 + c * 0.5})`;
|
|
ctx.lineWidth = 1.6;
|
|
ctx.stroke();
|
|
|
|
// EKG cizgisi kalp uzerine bindirme
|
|
ctx.beginPath();
|
|
const ecgW = rBase * 2.2;
|
|
const ecgX = hx - ecgW / 2, ecgY = hy + rBase * 1.75;
|
|
ctx.moveTo(ecgX, ecgY);
|
|
for (let i = 0; i <= 40; i++) {
|
|
const p = i / 40;
|
|
const x = ecgX + ecgW * p;
|
|
let v = 0;
|
|
// QRS dalgasi: beatPhase ile senkron
|
|
const d = p - state.beatPhase;
|
|
if (d > -0.06 && d < 0.06) v = Math.sin((d + 0.06) * 26) * 12;
|
|
else if (d > 0.10 && d < 0.22) v = Math.sin((d - 0.10) * 26) * 4;
|
|
ctx.lineTo(x, ecgY - v);
|
|
}
|
|
ctx.strokeStyle = `rgba(74, 222, 128, ${0.4 + c * 0.5})`;
|
|
ctx.lineWidth = 1.3;
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
},
|
|
|
|
/** Kan parcaciklari. */
|
|
drawBlood(blood, state) {
|
|
ctx.save();
|
|
const arteryColor = bloodTint(state.spo2, true);
|
|
const veinColor = bloodTint(state.spo2, false);
|
|
|
|
for (let i = 0; i < blood.particles.length; i++) {
|
|
const p = blood.particles[i];
|
|
const v = blood.vesselMap[p.vesselId];
|
|
if (!v) continue;
|
|
const pt = P.pointAt(v.pts, p.t);
|
|
const x = R.sx(pt.x + p.jitter);
|
|
const y = R.sy(pt.y);
|
|
|
|
const rad = Math.max(1.1, p.size * (v.calibre / 2.2) * (R.drawScale / 420));
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, rad, 0, Math.PI * 2);
|
|
ctx.fillStyle = p.type === 'artery' ? arteryColor : veinColor;
|
|
ctx.globalAlpha = p.alpha;
|
|
ctx.shadowColor = p.type === 'artery' ? '#ef4444' : '#3b82f6';
|
|
ctx.shadowBlur = 6;
|
|
ctx.fill();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
ctx.shadowBlur = 0;
|
|
ctx.restore();
|
|
},
|
|
|
|
/** Nabiz dalgalari. */
|
|
drawPulses(pulses) {
|
|
ctx.save();
|
|
pulses.forEach(w => {
|
|
const v = A.VESSELS.find(x => x.id === w.vesselId);
|
|
if (!v) return;
|
|
ctx.beginPath();
|
|
v.pts.forEach((p, i) => {
|
|
const x = R.sx(p[0]), y = R.sy(p[1]);
|
|
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
});
|
|
ctx.strokeStyle = `rgba(254, 240, 138, ${Math.max(0, 0.5 - w.life * 0.4)})`;
|
|
ctx.lineWidth = 3.5;
|
|
ctx.lineCap = 'round';
|
|
ctx.stroke();
|
|
});
|
|
ctx.restore();
|
|
},
|
|
|
|
/** Sinir sinyal paketleri. */
|
|
drawSignals(signals) {
|
|
ctx.save();
|
|
signals.forEach(s => {
|
|
const n = A.NERVES.find(x => x.id === s.nerveId);
|
|
if (!n) return;
|
|
const pt = P.pointAt(n.pts, s.t);
|
|
const x = R.sx(pt.x), y = R.sy(pt.y);
|
|
const color = s.type === 'motor' ? COLORS.motor : COLORS.sensory;
|
|
const fade = s.done ? Math.max(0, 1 - s.life / 0.55) : 1;
|
|
|
|
// hale
|
|
const g = ctx.createRadialGradient(x, y, 0, x, y, 16);
|
|
g.addColorStop(0, color);
|
|
g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
ctx.globalAlpha = 0.55 * fade;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 16, 0, Math.PI * 2);
|
|
ctx.fillStyle = g;
|
|
ctx.fill();
|
|
|
|
// cekirdek
|
|
ctx.globalAlpha = fade;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, s.done ? 6 : 4.2, 0, Math.PI * 2);
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.shadowColor = color;
|
|
ctx.shadowBlur = 14;
|
|
ctx.fill();
|
|
|
|
// hedefe varinca halka
|
|
if (s.done) {
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 8 + s.life * 40, 0, Math.PI * 2);
|
|
ctx.strokeStyle = color;
|
|
ctx.globalAlpha = fade * 0.7;
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
}
|
|
ctx.shadowBlur = 0;
|
|
});
|
|
ctx.globalAlpha = 1;
|
|
ctx.restore();
|
|
},
|
|
|
|
/** Tiklanabilir bolge vurgusu (hover). */
|
|
drawHover(zone, label) {
|
|
if (!zone) return;
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.ellipse(
|
|
R.sx(zone.cx), R.sy(zone.cy),
|
|
zone.rx * R.drawScale, zone.ry * R.drawScale,
|
|
0, 0, Math.PI * 2
|
|
);
|
|
ctx.strokeStyle = 'rgba(250, 204, 21, 0.75)';
|
|
ctx.setLineDash([5, 4]);
|
|
ctx.lineWidth = 1.6;
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
ctx.fillStyle = 'rgba(250, 204, 21, 0.10)';
|
|
ctx.fill();
|
|
|
|
if (label) {
|
|
const x = R.sx(zone.cx), y = R.sy(zone.cy) - zone.ry * R.drawScale - 12;
|
|
ctx.font = '600 12px ui-sans-serif, system-ui, sans-serif';
|
|
const w = ctx.measureText(label).width + 14;
|
|
ctx.fillStyle = 'rgba(15, 23, 42, 0.92)';
|
|
ctx.strokeStyle = 'rgba(250, 204, 21, 0.6)';
|
|
roundRect(ctx, x - w / 2, y - 14, w, 20, 5);
|
|
ctx.fill(); ctx.stroke();
|
|
ctx.fillStyle = '#fde68a';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(label, x, y - 4);
|
|
}
|
|
ctx.restore();
|
|
},
|
|
|
|
/** Olcek cubugu + bilgi. */
|
|
drawHud(state) {
|
|
ctx.save();
|
|
ctx.font = '11px ui-monospace, monospace';
|
|
ctx.fillStyle = COLORS.dim;
|
|
ctx.textAlign = 'left';
|
|
ctx.textBaseline = 'top';
|
|
ctx.fillText(`t = ${state.t.toFixed(1)}s`, 12, 10);
|
|
ctx.fillText(`atış #${state.beatCount}`, 12, 26);
|
|
ctx.restore();
|
|
}
|
|
};
|
|
|
|
return R;
|
|
}
|
|
|
|
function roundRect(ctx, x, y, w, h, r) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + r, y);
|
|
ctx.lineTo(x + w - r, y);
|
|
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
ctx.lineTo(x + w, y + h - r);
|
|
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
ctx.lineTo(x + r, y + h);
|
|
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
ctx.lineTo(x, y + r);
|
|
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
ctx.closePath();
|
|
}
|
|
|
|
const api = { createRenderer, COLORS, bloodTint };
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
root.NumexBodyRender = api;
|
|
})(typeof window !== 'undefined' ? window : globalThis);
|