368 lines
No EOL
13 KiB
JavaScript
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;
|
|
}
|
|
}); |