Skip to content

Instantly share code, notes, and snippets.

@VitalyErmilov
Created February 6, 2025 21:31
Show Gist options
  • Select an option

  • Save VitalyErmilov/6f875145369b97bc9c59b37581934e30 to your computer and use it in GitHub Desktop.

Select an option

Save VitalyErmilov/6f875145369b97bc9c59b37581934e30 to your computer and use it in GitHub Desktop.
ECG [TSL]
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.173.0/build/three.webgpu.js",
"three/webgpu": "https://unpkg.com/three@0.173.0/build/three.webgpu.js",
"three/tsl": "https://unpkg.com/three@0.173.0/build/three.tsl.js",
"three/addons/": "https://unpkg.com/three@0.173.0/examples/jsm/"
}
}
</script>
import * as THREE from "three";
import * as tsl from "three/tsl";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { pass, mrt, output, emissive } from "three/tsl";
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
console.clear();
// load fonts
await (async function () {
async function loadFont(fontface) {
await fontface.load();
document.fonts.add(fontface);
}
let fonts = [
new FontFace(
"Orbitron",
"url(https://fonts.gstatic.com/s/orbitron/v31/yMJRMIlzdpvBhQQL_Qq7dy0.woff2) format('woff2')"
)
];
for (let font in fonts) {
await loadFont(fonts[font]);
}
})();
class Postprocessing extends THREE.PostProcessing {
constructor(renderer) {
const scenePass = pass(scene, camera);
scenePass.setMRT(
mrt({
output,
emissive
})
);
const outputPass = scenePass.getTextureNode();
const emissivePass = scenePass.getTextureNode("emissive");
const bloomPass = bloom(emissivePass, 0.15, 0);
super(renderer);
this.outputNode = outputPass.add(bloomPass);
}
}
class DataScreen extends THREE.Mesh{
constructor(){
let g = new THREE.PlaneGeometry(3, 5);
let m = new THREE.MeshLambertMaterial({
color: new THREE.Color(0xface8d).multiplyScalar(0.1),
emissive: 0xff2200,
emissiveIntensity: 5,
side: THREE.DoubleSide,
transparent: true
});
super(g, m);
this.init();
}
init(){
let c = document.createElement("canvas");
c.width = 600;
c.height = 1000;
let ctx = c.getContext("2d");
let tex = new THREE.CanvasTexture(c);
tex.anisotropy = 8;
this.onScreenData = {
canvas: c,
ctx: ctx,
texture: tex,
cardiogramPoints: [
[-45, 0],
[-45, 0],
[-30, 0],
[-20, -10],
[-15, 0],
[-10, 10],
[0, -50],
[10, 10],
[15, 0],
[25, -10],
[30, 0],
[45, 0],
[45, 0]
].map(p => {
let v = new THREE.Vector2(...p);
v.x *= 0.5;
return v;
}),
totalLength: 0,
lineWidth: c.height * 0.01 * 3,
u: (val) => {return val * c.height * 0.01}
};
for(let i = 0; i < this.onScreenData.cardiogramPoints.length - 1; i++){
let v1 = this.onScreenData.cardiogramPoints[i];
let v2 = this.onScreenData.cardiogramPoints[i + 1];
this.onScreenData.totalLength += this.onScreenData.u(v1.distanceTo(v2));
}
this.mediators = {
v: new THREE.Vector2
}
this.material.emissiveMap = this.onScreenData.texture;
this.material.alphaMap = this.onScreenData.texture;
}
update(t){
let points = this.onScreenData.cardiogramPoints;
let cnv = this.onScreenData.canvas;
let ctx = this.onScreenData.ctx;
let u = this.onScreenData.u;
ctx.clearRect(0, 0, cnv.width, cnv.height);
ctx.strokeStyle = ctx.fillStyle = "rgba(255, 255, 255, 1)";
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.lineWidth = u(2);
ctx.save();
ctx.setLineDash([this.onScreenData.totalLength, this.onScreenData.totalLength]);
ctx.lineDashOffset = this.onScreenData.totalLength - t * (this.onScreenData.totalLength / 1.5);
ctx.translate(cnv.width * 0.5, cnv.height * 0.5);
ctx.beginPath();
let pMinusOne = points[0];
let pZero = points[1];
ctx.moveTo(u((pMinusOne.x + pZero.x) * 0.5), u((pMinusOne.y + pZero.y) * 0.5));
for(let pIdx = 1; pIdx < points.length - 1; pIdx++) {
let p = points[pIdx];
let pNext = points[pIdx + 1];
ctx.quadraticCurveTo(u(p.x), u(p.y), u((p.x + pNext.x) * 0.5), u((p.y + pNext.y) * 0.5));
};
ctx.stroke();
ctx.restore();
ctx.lineWidth = u(0.5);
ctx.beginPath();
ctx.roundRect(u(2), u(2), cnv.width - u(4), cnv.height - u(4), u(5));
ctx.stroke();
ctx.font = `${u(15)}px Orbitron`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("ECG", cnv.width * 0.5, u(75));
this.onScreenData.texture.needsUpdate = true;
}
}
let scene = new THREE.Scene();
scene.backgroundNode = tsl.color(0x000000);
let camera = new THREE.PerspectiveCamera(30, innerWidth / innerHeight, 1, 100);
camera.position.set(0, 0, 1).setLength(10);
let renderer = new THREE.WebGPURenderer({ antialias: false });
renderer.setPixelRatio(devicePixelRatio);
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", (event) => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let postprocessing = new Postprocessing(renderer);
let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
let light = new THREE.DirectionalLight(0xffffff, Math.PI);
light.position.setScalar(1);
let screenLight = new THREE.DirectionalLight(0xff2200, Math.PI);
screenLight.position.z = 1;
scene.add(light, screenLight, new THREE.AmbientLight(0xffffff, Math.PI * 0.5));
let dataScreen = new DataScreen();
//dataScreen.update(0);
scene.add(dataScreen);
let dodec = new THREE.Mesh(
new THREE.DodecahedronGeometry(1, 0),
new THREE.MeshLambertMaterial({color: 0x888888})
);
dodec.position.z = -1.5;
scene.add(dodec);
let clock = new THREE.Clock();
let t = 0;
renderer.setAnimationLoop(() => {
controls.update();
let dt = clock.getDelta();
t += dt;
dataScreen.update(t);
dodec.rotation.y = t * 0.31;
dodec.rotation.x = t * 0.27;
//renderer.render(scene, camera);
postprocessing.render();
});
body{
overflow: hidden;
margin: 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment