heartbeat three.js websocket fun
This commit is contained in:
BIN
heartbeat.webm
Normal file
BIN
heartbeat.webm
Normal file
Binary file not shown.
@@ -3,9 +3,20 @@
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||
<meta name="theme-color" content="#1d1850"/>
|
||||
|
||||
<title>MEDTRACE: Case - Frontend Developer</title>
|
||||
<!-- <script type="module" src="./MyCard/MyCard.js"></script> -->
|
||||
|
||||
<!-- ability to import three.js from cdn -->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js",
|
||||
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.163.0/examples/jsm/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module" src="./App/App.js"></script>
|
||||
<!-- "light" DOM styles -->
|
||||
<style>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
#cards {
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 150px);
|
||||
grid-gap: 2rem 1rem;
|
||||
grid-template-columns: repeat(auto-fill, 135px);
|
||||
grid-gap: 2rem 0.5rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<ecg-ticker name="patients" heart-rate="60"></ecg-ticker>
|
||||
<h1 name="not-found" id="not-found">Not found</h1>
|
||||
<h1 name="bad-route">Bad route</h1>
|
||||
<h1 name="not-implemented">Not implemented yet</h1>
|
||||
|
||||
9
src/ECGTicker/ECGTicker.html
Normal file
9
src/ECGTicker/ECGTicker.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<template id="ecg-ticker-template">
|
||||
<style>
|
||||
#canvas-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
</style>
|
||||
<div id="canvas-container"></div>
|
||||
</template>
|
||||
@@ -1,22 +1,26 @@
|
||||
const template = document.getElementById("my-card-template") as
|
||||
import * as THREE from "three";
|
||||
import {OrbitControls} from "three/addons/controls/OrbitControls.js";
|
||||
|
||||
|
||||
const template = document.getElementById("ecg-ticker-template") as
|
||||
HTMLTemplateElement;
|
||||
|
||||
enum Attributes {
|
||||
MESSAGE = "message",
|
||||
HEART_RATE = "heart-rate",
|
||||
}
|
||||
|
||||
export default class ECGTicker extends HTMLElement {
|
||||
static observedAttributes = [Attributes.MESSAGE];
|
||||
static observedAttributes = [Attributes.HEART_RATE];
|
||||
|
||||
private _content: DocumentFragment;
|
||||
$message: HTMLElement;
|
||||
$canvasContainer: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.attachShadow({mode: "open"});
|
||||
this._content = template.content.cloneNode(true) as DocumentFragment;
|
||||
this.$message = this._content.getElementById("message")!;
|
||||
this.$canvasContainer = this._content.getElementById("canvas-container")!;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@@ -25,18 +29,73 @@ export default class ECGTicker extends HTMLElement {
|
||||
} else {
|
||||
console.warn("No shadowRoot detected.");
|
||||
}
|
||||
|
||||
this.initECGAnimation();
|
||||
}
|
||||
|
||||
initECGAnimation() {
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
|
||||
renderer.setSize(200, 300);
|
||||
this.$canvasContainer.appendChild(renderer.domElement);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
const axesHelper = new THREE.AxesHelper(5);
|
||||
scene.add(axesHelper);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
const orbit = new OrbitControls(camera, renderer.domElement);
|
||||
|
||||
camera.position.set(1, 2, 5);
|
||||
orbit.update();
|
||||
|
||||
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
|
||||
const boxMaterial = new THREE.MeshBasicMaterial({color: 0xffff00});
|
||||
const box = new THREE.Mesh(boxGeometry, boxMaterial);
|
||||
scene.add(box);
|
||||
|
||||
let heart_amp = 0.0;
|
||||
|
||||
function animate(x) {
|
||||
let sr = 120;
|
||||
let freq = 20;
|
||||
|
||||
box.position.x = 0.01 * Math.sin(2 * Math.PI * freq * (x / sr));
|
||||
box.position.y = 0.02 * Math.sin(2 * Math.PI * (freq/2) * (x / sr));
|
||||
// box.position.z = 0.05 * Math.sin(2 * Math.PI * (freq/3) * (x / sr));
|
||||
box.position.z = heart_amp * 1;
|
||||
box.rotation.set(x/2000, x/2000, 0);
|
||||
// camera.position.x = 1 * Math.sin(2 * Math.PI * (freq/6) * (x / sr));
|
||||
// camera.position.y = 2 * Math.sin(2 * Math.PI * (freq/11) * (x / sr));
|
||||
// camera.position.z = 1 + Math.abs(15 * Math.sin(2 * Math.PI * (freq/75) * (x / sr)));
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
// const socket = new WebSocket(`${location.protocol === "https" ? "wss" : "ws"}://${location.hostname}:7890/ecg`);
|
||||
const socket = new WebSocket(`ws://localhost:7890/ecg`);
|
||||
socket.onmessage = (({data}) => {
|
||||
heart_amp = data;
|
||||
console.log(heart_amp);
|
||||
|
||||
});
|
||||
|
||||
renderer.setAnimationLoop(animate);
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _prev: string, curr: string) {
|
||||
if (Attributes.MESSAGE === name) {
|
||||
while (this.$message.firstChild) {
|
||||
this.$message.removeChild(this.$message.lastChild!);
|
||||
}
|
||||
this.$message.appendChild(document.createTextNode(curr));
|
||||
if (Attributes.HEART_RATE === name) {
|
||||
// send message via websocket to alter heart rate in real time
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.customElements.define("x-ecg-ticker", ECGTicker);
|
||||
window.customElements.define("ecg-ticker", ECGTicker);
|
||||
|
||||
export {};
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.75rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
#message {
|
||||
margin: 15px;
|
||||
|
||||
Reference in New Issue
Block a user