Main/resources/[jobs]/[medic]/visn_are/nui/ecg.js
2025-06-07 08:51:21 +02:00

368 lines
No EOL
13 KiB
JavaScript

// Keep track of the times draw() has been called
let draw_i = 0;
/**
* A Heart object will beat, and generate voltage values according to the time
* the beat started
*
* "Duration" values are really pixels. 1 pixel represents 1/60 of a second.
*/
var HEART_RATE = 0;
var beepPlayed = false;
let ecgVolume = 0.3;
function updateHeartRate(newHeartRate) {
heart.updateHeartRate(newHeartRate);
ecg.drawBPM(newHeartRate);
}
function updateBloodPressure(highBP, lowBP) {
ecg.updateBloodPressure(highBP, lowBP);
}
class Heart {
/**
* Creates an instance of Heart
* @param {number} adDuration Duration in pixels of the atria depolarization
* @param {number} vdDuration Duration in pixels of the ventricle depolarization
* @param {number} vrDuration Duration in pixels of the ventricle repolarization
*
* @property {number} this.beatDuration Duration in pixels of the whole beat
* @property {number} this.nextBeat Time between last beat, and next beat
* @property {number} this.nextBeatIn Time remaining for next beat
* @property {number[]} this.bpm Time between two particular beats
* @property {number} this.voltage Current voltage value. No units used.
*/
constructor(adDuration, vdDuration, vrDuration) {
this.adDuration = adDuration;
this.vdDuration = vdDuration;
this.vrDuration = vrDuration;
this.beatDuration = adDuration + vdDuration + vrDuration;
this.nextBeat = HEART_RATE;
this.nextBeatIn = HEART_RATE;
this.bpm = [];
this.voltage = 0;
this.heartRate = HEART_RATE;
}
updateHeartRate(heartRate) {
if (this.heartRate == heartRate) return;
this.heartRate = heartRate;
this.nextBeat = abs(45 / (this.heartRate / 80));
this.nextBeatIn = abs(45 / (this.heartRate / 80));
}
/**
* Assign the heart a new voltage value, and report that value to the ECG
* the heart is connected to.
* @param {number} voltage
*/
setVoltage(voltage) {
this.voltage = voltage;
ecg.addValue({ y: this.voltage });
}
/**
* Generates the voltage values corresponding to the atria depolarization process.
* This is the process that generates the first part of the curve of every beat.
*
* @param {number} time Time in pixels since the atria depolarization process started
*/
atriaDepolarization(time) {
// This process is not close to what reality does, but here it is generated using a
// sin function where only the positive values remain, making a bump followed by a
// flat section
let y = randomGaussian(5, 1) * sin(time * (360 / this.adDuration));
// To compensate for the y-axis inverted direction, return -y when y is over 0
y = y > 0 ? -y : 0.2 * (1 - y);
// Update the voltage to whatever value was calculated
this.setVoltage(y + noise(time));
}
/**
* Generates the voltage values corresponding to the ventricle depolarization process.
* This is the process that generates the spiky part of the curve of every beat.
*
* @param {number} time Time in pixels since the ventricle depolarization process started
*/
ventricleDepolarization(time) {
let y;
// In the first third, the curve has a spike going down
if (time <= this.vdDuration / 3)
y = (randomGaussian(8, 2) * (this.vdDuration - time)) / 6;
// In the second third, the curve has a big spike going up
else if (time < (2 * this.vdDuration) / 3) {
// Start producing a sound, going from 0 to 0.5 volume in 0.01 seconds
//osc.amp(0.5, 0.01);
y = (randomGaussian(70, 2) * abs(1.5 - (this.vdDuration - time))) / 3;
y = -y;
if (!beepPlayed) {
let audio = new Audio("assets/sounds/hm_beep.mp3");
audio.volume = ecgVolume;
audio.play();
}
beepPlayed = true;
}
// In the last third, the curve has another spike (bigger than the first one) going down
else {
y = (randomGaussian(20, 2) * (this.vdDuration - time)) / 3;
beepPlayed = false;
}
// Update the voltage to whatever value was calculated
this.setVoltage(y);
}
/**
* Generates the voltage values corresponding to the ventricle repolarization process.
* This is the process that generates the last part of the curve of every beat.
*
* @param {number} time Time in pixels since the ventricle repolarization process started
*/
ventricleRepolarization(time) {
// This process is not close to what reality does, but here it is generated using a
// sin function where only the positive values remain, but displaced half a turn to
// make a flat section followed by a bump
let y = randomGaussian(8, 2) * sin(180 + time * (360 / this.vrDuration));
// To compensate for the y-axis inverted direction, return -y when y is over 0
y = y < 0 ? 0.2 * (1 - y) : -y;
// Update the voltage to whatever value was calculated
this.setVoltage(y + noise(time));
}
updateBPM() {
// bpm = 3600 / pixel-distance
this.bpm.push(3600 / this.nextBeat);
// To make rapid frequency changes meaningful, get the average bpm using only the
// last 5 values of time, not all of them. So dispose the oldest one when the list
// length is over 5.
if (this.bpm.length > 5) this.bpm.splice(0, 1);
}
/**
* Decrease this.nextBeatIn to simulate the pass of time.
* If necessary, create a new this.nextBeat value
*/
updateTimeToNextBeat() {
if (this.nextBeatIn == Infinity && this.heartRate > 0) {
this.nextBeat = abs(45 / (this.heartRate / 80));
this.nextBeatIn = abs(45 / (this.heartRate / 80));
}
if (this.nextBeatIn-- <= 0) {
this.nextBeat = abs(45 / (this.heartRate / 80));
// It the pixel time between beat and beat is less than 18, force it to be
// 18. This value makes to a bpm of 200.
if (this.nextBeat < 18) this.nextBeat = 18;
// Get new bpm values using the last this.nextBeat
this.updateBPM();
// Reset the remaining time to the new calculated time
this.nextBeatIn = this.nextBeat;
}
}
/**
* Get voltage values for every second of the beat, even at rest (no-beating time
* after the ventricle repolarization finished, and before the next atria depolarization)
* @param {*} time Time in pixels after the atria depolarization started
*/
beat(time) {
// Update the time left for the start of the next beat
this.updateTimeToNextBeat();
// If according to time, beat is in the atria depolarization process, call that function
if (time <= this.adDuration) {
this.atriaDepolarization(time);
return;
}
// If according to time, beat is in the ventricle depolarization process, call that function
// Update the time so the value sent is relative to the start of the ventricle
// depolarization process
time -= this.adDuration;
if (time <= this.vdDuration) {
this.ventricleDepolarization(time);
return;
}
// If according to time, beat is in the ventricle repolarization process, call that function
// Update the time so the value sent is relative to the start of the ventricle
// repolarization process
time -= this.vdDuration;
if (time <= this.vrDuration) {
this.ventricleRepolarization(time);
return;
}
// If function reached this point, it's not in any of the beat processes, and it's resting.
// Add a noisy voltage value
this.setVoltage(0 + noise(draw_i * 0.5) * 5);
}
}
// Initialize a heart
let heart = new Heart(12, 8, 12);
/**
* ECG will receive, process, and draw the health information
*/
class ECG {
/**
* @param {Object} graphZero Coordinates of the {0, 0} value of the graph
* @param {Object[]} values Array of {x, y} objects. x plots time, y plots voltage
* @param {number} maxValuesHistory Maximum number of values before wiping oldest one
*/
constructor(graphZero, values, maxValuesHistory) {
this.graphZero = graphZero;
this.values = values;
this.maxValuesHistory = maxValuesHistory;
this.maximumX = maxValuesHistory;
}
/**
* Add a new voltage value to the values array. If it exceeds the maximum number of
* values allowed to store, remove the oldest one before.
* @param {Object} value {x, y} object. x represents time, y represents voltage
*/
addValue(value) {
// If no x (time) value is received, assume it is the sucessor of the last value
// in the values array. If the new x exceeds the maximum allowed, make x = 0
if (this.values.length >= this.maxValuesHistory) this.values.splice(0, 1);
if (value.x === undefined) {
value.x = (this.values[this.values.length - 1].x + 1) % this.maximumX;
}
this.values.push(value);
}
/**
* Draw lines joining every voltage value throughout time in the screen
*/
plotValues() {
push();
for (let i = 1; i < this.values.length; i++) {
// If the previous value has a X coordinate higher than the current one,
// don't draw it, to avoid lines crossing from end to start of the ECG plot area.
if (this.values[i - 1].x > this.values[i].x) continue;
// Older values are drawn with a lower alpha
let alpha = i / this.values.length;
// Set the color of the drawing
stroke(255, 168, 0, alpha);
fill(255, 168, 0, alpha);
// Line from previous value to current value
line(
this.graphZero.x + this.values[i - 1].x,
this.graphZero.y + this.values[i - 1].y,
this.graphZero.x + this.values[i].x,
this.graphZero.y + this.values[i].y
);
// For the last 5 values, draw a circle with a radius going in function to
// its index. This to make the leading line thicker
if (i + 5 > this.values.length) {
circle(
this.graphZero.x + this.values[i].x,
this.graphZero.y + this.values[i].y,
this.values.length / i
);
}
}
pop();
}
updateBloodPressure(highBP, lowBP) {
document.getElementById("pressure-value").innerHTML = "" + highBP + "/" + lowBP;
}
/**
* Update the html content of the span containing the bpm info
* @param {number} bpm
*/
drawBPM(bpm) {
document.getElementById("heart-rate-value").innerHTML = bpm;
}
}
// Initialize the ecg
let ecg = new ECG({ x: 0, y: 110 }, [{ x: 0, y: 0 }], 600);
/**
* Set the general configuration for the p5js canvas
*/
function setup() {
// Create a 600x150 canvas and place it inside the div with id "sketch-holder"
let myCanvas = createCanvas(600, 150);
myCanvas.parent("ecg-display");
// Set the color mode to allow calling RGBA without converting to string
colorMode(RGB, 255, 255, 255, 1);
// Work with degrees instead of Radians (sin function used inside Heart Class)
angleMode(DEGREES);
}
/**
* Draw a rectangle of size (canvas.width - 1, canvas.height - 1) with dark background
* and a brilliant green border.
*
* The -1 is to allow the border to be seen in the final page.
*/
function drawECGScreenBackground() {
push();
fill("#201D1D");
stroke(255, 255, 255, 0.2);
rect(0, 0, 599, 149);
pop();
}
/**
* Function to be called until the page is closed
* Part of p5js
*/
function draw() {
if($(".ecg").css("display") == "none") return;
// Keep track of the number of times draw has been called
draw_i++;
// Hide previous ECG line by drawing a background
drawECGScreenBackground();
// Get the new voltage values for the ECG from the heart
heart.beat(heart.nextBeat - heart.nextBeatIn);
// Draw the line of voltage values over time in the ECG screen
ecg.plotValues();
}
function touchStarted() {
getAudioContext().resume();
}
$(".mute i").click(function() {
if($(this).hasClass("fa-volume")) {
$(this).removeClass("fa-volume");
$(this).addClass("fa-volume-mute");
ecgVolume = 0;
} else {
$(this).removeClass("fa-volume-mute");
$(this).addClass("fa-volume");
ecgVolume = 0.3;
}
});