@chromospace/chromospace@0.0.10
latest
Library for visualization of 3D genomic data.
This package works with BrowsersIt is unknown whether this package works with Cloudflare Workers, Deno, Bun
JSR Score
94%
Published
4 months ago (0.0.10)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278// @ts-ignore import { N8AOPostPass } from "npm:n8ao@^1.8.2"; import { EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset, } from "npm:postprocessing@^6.35.4"; import * as THREE from "npm:three@^0.164.1"; import { OrbitControls } from "npm:three@^0.164.1/addons/controls/OrbitControls.js"; import { decideColor, estimateBestSphereSize } from "../utils.ts"; import { computeTubes, decideGeometry } from "./render-utils.ts"; import type { DrawableMarkSegment } from "./renderer-types.ts"; import type { Color as ChromaColor, Scale as ChromaScale } from "npm:chroma-js@^2.4.2"; import type { vec3 } from "npm:gl-matrix@^3.4.3"; /** * Basic implementation of a 3d chromatin renderer. Essentially just wraps THREE.WebGLRenderer but provides semantics for building chromatin visualization. * * Important methods: * - addSegments: adding segments of chromatin with unified visual properties (e.g., specified by a grammar) * - buildStructures, buildPart: turns segments with specific visual attributes into THREE primitives */ export class ChromatinBasicRenderer { markSegments: DrawableMarkSegment[] = []; //~ threejs stuff renderer: THREE.WebGLRenderer; scene: THREE.Scene; camera: THREE.PerspectiveCamera; composer: EffectComposer; ssaoPasses: [N8AOPostPass, N8AOPostPass]; //~ dom redrawRequest = 0; alwaysRedraw = false; constructor(params?: { canvas?: HTMLCanvasElement; alwaysRedraw?: boolean; }) { const { canvas = undefined, alwaysRedraw = true } = params || {}; this.renderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", antialias: false, stencil: false, depth: false, canvas, }); this.renderer.setClearColor("#eeeeee"); this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera(25, 2, 0.1, 1000); const controls = new OrbitControls(this.camera, this.renderer.domElement); this.camera.position.z = 3.0; controls.update(); const lightA = new THREE.DirectionalLight(); lightA.position.set(3, 10, 10); lightA.castShadow = true; const lightB = new THREE.DirectionalLight(); lightB.position.set(-3, 10, -10); lightB.intensity = 0.2; const lightC = new THREE.AmbientLight(); lightC.intensity = 0.2; this.scene.add(lightA, lightB, lightC); this.composer = new EffectComposer(this.renderer); this.composer.addPass(new RenderPass(this.scene, this.camera)); // N8AOPass replaces RenderPass const w = 1920; const h = 1080; const n8aopass = new N8AOPostPass(this.scene, this.camera, w, h); n8aopass.configuration.aoRadius = 0.1; n8aopass.configuration.distanceFalloff = 1.0; n8aopass.configuration.intensity = 2.0; this.composer.addPass(n8aopass); const n8aopassBigger = new N8AOPostPass(this.scene, this.camera, w, h); n8aopassBigger.configuration.aoRadius = 1.0; n8aopassBigger.configuration.distanceFalloff = 1.0; n8aopassBigger.configuration.intensity = 2.0; this.composer.addPass(n8aopass); this.ssaoPasses = [n8aopass, n8aopassBigger]; /* SMAA Recommended */ this.composer.addPass( new EffectPass( this.camera, new SMAAEffect({ preset: SMAAPreset.ULTRA, }), ), ); this.render = this.render.bind(this); this.getCanvasElement = this.getCanvasElement.bind(this); this.startDrawing = this.startDrawing.bind(this); this.endDrawing = this.endDrawing.bind(this); this.resizeRendererToDisplaySize = this.resizeRendererToDisplaySize.bind(this); //~ setting size of canvas to fill parent const c = this.getCanvasElement(); c.style.width = "100%"; c.style.height = "100%"; this.alwaysRedraw = alwaysRedraw; if (!alwaysRedraw) { controls.addEventListener("change", this.render); } } getCanvasElement(): HTMLCanvasElement { return this.renderer.domElement; } /** * Entrypoint for adding actual data to show */ addSegments(newSegments: DrawableMarkSegment[]) { this.markSegments = [...this.markSegments, ...newSegments]; this.buildStructures(); } /** * Turns all drawable segments into THREE objects to be rendered */ buildStructures() { for (const segment of this.markSegments) { this.buildPart(segment); } } /** * Meant to be called directly from client (eg, Observable notebook) to request redraw */ updateViewConfig() { this.scene.clear(); this.buildStructures(); this.redrawRequest = requestAnimationFrame(this.render); } /** * Turns a singular segment (ie, position+mark+attributes) into THREEjs objects for rendering */ buildPart(segment: DrawableMarkSegment) { const { color = undefined, colorMap = undefined, size = undefined, makeLinks = true, } = segment.attributes; const sphereRadius = size ? size : estimateBestSphereSize(segment.positions); const tubeSize = 0.4 * sphereRadius; const geometry = decideGeometry(segment.mark, segment.attributes); const material = new THREE.MeshBasicMaterial({ color: "#FFFFFF" }); //~ bin spheres const meshInstcedSpheres = new THREE.InstancedMesh( geometry, material, segment.positions.length, ); const dummyObj = new THREE.Object3D(); const colorObj = new THREE.Color(); for (let [i, b] of segment.positions.entries()) { dummyObj.position.set(b[0], b[1], b[2]); dummyObj.updateMatrix(); meshInstcedSpheres.setMatrixAt(i, dummyObj.matrix); decideColor(colorObj, i, segment.positions.length, color, colorMap); meshInstcedSpheres.setColorAt(i, colorObj); i += 1; } this.scene.add(meshInstcedSpheres); if (makeLinks) { this.buildLinks(segment.positions, tubeSize, color, colorMap); } } /** * Utility function for building links between marks (optional) */ buildLinks( positions: vec3[], tubeSize: number, color?: ChromaColor, colorMap?: ChromaScale, ) { //~ tubes between tubes const tubes = computeTubes(positions); const tubeGeometry = new THREE.CylinderGeometry( tubeSize, tubeSize, 1.0, 10, 1, ); const material = new THREE.MeshBasicMaterial({ color: "#FFFFFF" }); const meshInstcedTubes = new THREE.InstancedMesh( tubeGeometry, material, tubes.length, ); const dummyObj = new THREE.Object3D(); const colorObj = new THREE.Color(); for (const [i, tube] of tubes.entries()) { dummyObj.position.set(tube.position.x, tube.position.y, tube.position.z); dummyObj.rotation.set( tube.rotation.x, tube.rotation.y, tube.rotation.z, tube.rotation.order, ); dummyObj.scale.setY(tube.scale); dummyObj.updateMatrix(); decideColor(colorObj, i, positions.length, color, colorMap); meshInstcedTubes.setMatrixAt(i, dummyObj.matrix); meshInstcedTubes.setColorAt(i, colorObj); } this.scene.add(meshInstcedTubes); } startDrawing() { this.redrawRequest = requestAnimationFrame(this.render); } endDrawing() { cancelAnimationFrame(this.redrawRequest); this.renderer.dispose(); } resizeRendererToDisplaySize(renderer: THREE.WebGLRenderer): boolean { const canvas = renderer.domElement; const pixelRatio = window.devicePixelRatio; const width = Math.floor(canvas.clientWidth * pixelRatio); const height = Math.floor(canvas.clientHeight * pixelRatio); const needResize = canvas.width !== width || canvas.height !== height; if (needResize) { renderer.setSize(width, height, false); this.composer.setSize(width, height); const [pass1, pass2] = this.ssaoPasses; pass1.setSize(width, height); pass2.setSize(width, height); } return needResize; } render() { if (this.alwaysRedraw) { this.redrawRequest = requestAnimationFrame(this.render); } console.log("drawing"); //~ from: https://threejs.org/manual/#en/responsive if (this.resizeRendererToDisplaySize(this.renderer)) { const canvas = this.renderer.domElement; this.camera.aspect = canvas.clientWidth / canvas.clientHeight; this.camera.updateProjectionMatrix(); } this.composer.render(); } }