Skip to main content

Library for visualization of 3D genomic data.

This package works with BrowsersIt is unknown whether this package works with Cloudflare Workers, Deno, Bun
It is unknown whether this package works with Cloudflare Workers
It is unknown whether this package works with Deno
It is unknown whether this package works with Bun
This package works with Browsers
JSR Score
94%
Published
4 months ago (0.0.10)
Package root>src>renderer>ChromatinBasicRenderer.ts
// @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(); } }