<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Cutive+Mono&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/addons/p5.sound.min.js"></script>
<script language="javascript" type="text/javascript" src="./sketch.js" defer></script>
</head>
<body class="wrapper">
<div class="grid-container">
<div class="patient-info">
David Omar Flores Chávez
</div>
<div class="date text-right">
<span id="date-value" ></span>
</div>
<div id="sketch-holder" class="sketch-holder"></div>
<div class="bpm">
Heart rate: <span id="heart-rate-value">60</span> bpm
</div>
<div class="temperature text-right">
Temperature: <span id="temperature-value">98.6</span> °F
</div>
<div class="pressure">
Blood Pressure: <span id="pressure-value">132/88</span> mmHg
</div>
<div class="hb-levels text-right">
Hemoglobin: <span id="hemoglobin-value">14.1</span> g/dl
</div>
<div class="minute-ventilation">
Minute ventilation: <span id="minute-ventilation-value"> 6.14 </span> L/min
</div>
<div class="other">
</div>
</div>
</body>
</html>
body{
background-color: #151515;
font-family: 'Cutive Mono', monospace;
font-size: 15px;
color: white;
text-shadow: 0 0 4px #14B51B;
&:after{ /* the video interlace effect I loved and didn't mind to delete */
background:linear-gradient(0deg, rgba(0,0,0,0) 40%, rgba(0,0,0,.2) 80%);
background-size: 5px 3px;
display: block;
position: absolute;
content: '';
width: 100%;
height: 100%;
top: 0;
left: 0;
}
}
.wrapper {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 100vh;
align-items: center;
justify-items: center;
}
.grid-container {
display: grid;
grid-template-columns: 300px 300px;
grid-template-rows: 1fr 150px 1fr 1fr 1fr;
grid-row-gap: 10px;
grid-template-areas: "patient-info date""sketch-holder sketch-holder""bpm minute-ventilation""temperature hb-levels""pressure other";
}
.patient-info {
grid-area: patient-info;
}
.text-right {
text-align: right;
}
.date {
grid-area: date;
}
.sketch-holder {
grid-area: sketch-holder;
}
.bpm {
grid-area: bpm;
}
View Compiled
// 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.
*/
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 = 60;
this.nextBeatIn = 60;
this.bpm = [];
this.voltage = 0;
}
/**
* 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;
}
// 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;
// Stop the sound, going from 0.5 to 0 volume in 0.01 secs
osc.amp(0, 0.01);
}
// 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);
ecg.drawBPM(round(this.bpm.reduce((p, c) => p + c, 0) / this.bpm.length));
}
/**
* Decrease this.nextBeatIn to simulate the pass of time.
* If necessary, create a new this.nextBeat value
*/
updateTimeToNextBeat() {
// This indicates that the next beat will begin in the next iteration
if (this.nextBeatIn-- === 0) {
// Then calculate a new "remaining time" for the next beat.
// Use the x coordinates of the mouse position to modify the heart frequency
this.nextBeat = abs(ceil(randomGaussian((900 - mouseX) / 10, 3)));
// 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(121, 239, 150, alpha);
fill(121, 239, 150, 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();
}
//// The following methods update the values represented as html elements
updateInfo() {
this.updateDate();
if(draw_i % 50 === 0){
this.updateBloodPressure();
this.updateVentilation();
this.updateTemperature();
this.updateHemoglobin();
}
}
updateHemoglobin(){
document.getElementById("hemoglobin-value").innerHTML = randomGaussian(14, .1).toFixed(1)
}
updateTemperature(){
document.getElementById("temperature-value").innerHTML = randomGaussian(98.6, .1).toFixed(1)
}
updateVentilation(){
document.getElementById("minute-ventilation-value").innerHTML = randomGaussian(6, .5).toFixed(2)
}
updateBloodPressure(){
document.getElementById("pressure-value").innerHTML =
"" + round(randomGaussian(130, 1)) + "/" + round(randomGaussian(90, 1));
}
updateDate() {
let date = new Date();
date =
"" +
date.getFullYear() +
"-" +
date.getMonth() +
"-" +
date.getDate() +
" " +
date.getHours() +
":" +
date.getMinutes() +
":" +
date.getSeconds();
document.getElementById("date-value").innerHTML = date;
}
/**
* 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("sketch-holder");
// 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);
// Set the frequency the ecg will emit every heartbeat.
osc = new p5.Oscillator();
osc.setType("sine");
osc.freq(445);
osc.amp(0);
osc.start();
}
/**
* 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(121, 239, 150, 1);
rect(0, 0, 599, 149);
pop();
}
/**
* Function to be called until the page is closed
* Part of p5js
*/
function draw() {
// 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();
// Update the information values of the ECG
ecg.updateInfo();
}
// p5.sound variables
let osc;
function touchStarted() {
// To prevent sound bugs by browsers asking for permission
getAudioContext().resume();
}
View Compiled
This Pen doesn't use any external JavaScript resources.