UNPKG

3dmol

Version:

JavaScript/TypeScript molecular visualization library

892 lines (777 loc) 223 kB
<!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="Author" content="David Koes & 3Dmoljs Contributors"><meta name="Description" content="A modern, object-oriented JavaScript library for visualizing molecular data"><title>Source: GLViewer.ts</title><!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(locationPathname=document.location.pathname).substr(0,locationPathname.lastIndexOf("/")+1)</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><style>article ul li{list-style:disc}</style><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="light"><div class="sidebar-container"><div class="sidebar" id="sidebar"><a href="/" class="sidebar-title sidebar-title-anchor">3Dmol.js</a><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="mei5HJjYKLn6aua5Z5PM5"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="$3Dmol.Label.html">Label</a></div><div class="sidebar-section-children"><a href="$3Dmol.StateManager.html">StateManager</a></div><div class="sidebar-section-children"><a href="$3Dmol.UI.html">UI</a></div><div class="sidebar-section-children"><a href="Color.html">Color</a></div><div class="sidebar-section-children"><a href="CustomLinear.html">CustomLinear</a></div><div class="sidebar-section-children"><a href="Cylinder.html">Cylinder</a></div><div class="sidebar-section-children"><a href="GLModel.html">GLModel</a></div><div class="sidebar-section-children"><a href="GLShape.html">GLShape</a></div><div class="sidebar-section-children"><a href="GLShape_GLShape.html">GLShape</a></div><div class="sidebar-section-children"><a href="GLViewer.html">GLViewer</a></div><div class="sidebar-section-children"><a href="GLVolumetricRender.html">GLVolumetricRender</a></div><div class="sidebar-section-children"><a href="Matrix3.html">Matrix3</a></div><div class="sidebar-section-children"><a href="Matrix4.html">Matrix4</a></div><div class="sidebar-section-children"><a href="Quaternion.html">Quaternion</a></div><div class="sidebar-section-children"><a href="ROYGB.html">ROYGB</a></div><div class="sidebar-section-children"><a href="RWB.html">RWB</a></div><div class="sidebar-section-children"><a href="Ray.html">Ray</a></div><div class="sidebar-section-children"><a href="Sinebow.html">Sinebow</a></div><div class="sidebar-section-children"><a href="Sphere.html">Sphere</a></div><div class="sidebar-section-children"><a href="Surface.html">Surface</a></div><div class="sidebar-section-children"><a href="Triangle.html">Triangle</a></div><div class="sidebar-section-children"><a href="Vector2.html">Vector2</a></div><div class="sidebar-section-children"><a href="Vector3.html">Vector3</a></div><div class="sidebar-section-children"><a href="VolumeData.html">VolumeData</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="kv4Yx5z0THf_b8XR4Gje1"><div>Namespaces</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="GLDraw.html">GLDraw</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="ykThSufQWJcKGT1kXzr-A"><div>Tutorials</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="tutorial-code.html">Using 3Dmol within your code</a></div><div class="sidebar-section-children"><a href="tutorial-embeddable.html">Embedding a 3Dmol Viewer</a></div><div class="sidebar-section-children"><a href="tutorial-home.html">3Dmol Tutorials - Home</a></div><div class="sidebar-section-children"><a href="tutorial-learning_environment.html">Active Learning with 3Dmol.js</a></div><div class="sidebar-section-children"><a href="tutorial-url.html">Viewing Molecules with the 3Dmol Server</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="XcP4RUEg6tp7YKHbBVi9E"><div>Interfaces</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="AmbientOcclusionStyle.html">AmbientOcclusionStyle</a></div><div class="sidebar-section-children"><a href="ArrowSpec.html">ArrowSpec</a></div><div class="sidebar-section-children"><a href="AtomSelectionSpec.html">AtomSelectionSpec</a></div><div class="sidebar-section-children"><a href="AtomSpec.html">AtomSpec</a></div><div class="sidebar-section-children"><a href="AtomStyleSpec.html">AtomStyleSpec</a></div><div class="sidebar-section-children"><a href="BondStyle.html">BondStyle</a></div><div class="sidebar-section-children"><a href="BoxSpec.html">BoxSpec</a></div><div class="sidebar-section-children"><a href="CartoonStyleSpec.html">CartoonStyleSpec</a></div><div class="sidebar-section-children"><a href="ClickSphereStyleSpec.html">ClickSphereStyleSpec</a></div><div class="sidebar-section-children"><a href="CrossStyleSpec.html">CrossStyleSpec</a></div><div class="sidebar-section-children"><a href="CurveSpec.html">CurveSpec</a></div><div class="sidebar-section-children"><a href="CustomShapeSpec.html">CustomShapeSpec</a></div><div class="sidebar-section-children"><a href="CylinderSpec.html">CylinderSpec</a></div><div class="sidebar-section-children"><a href="DashedBondSpec.html">DashedBondSpec</a></div><div class="sidebar-section-children"><a href="FogSpec.html">FogSpec</a></div><div class="sidebar-section-children"><a href="IsoSurfaceSpec.html">IsoSurfaceSpec</a></div><div class="sidebar-section-children"><a href="LabelSpec.html">LabelSpec</a></div><div class="sidebar-section-children"><a href="LineSpec.html">LineSpec</a></div><div class="sidebar-section-children"><a href="LineStyleSpec.html">LineStyleSpec</a></div><div class="sidebar-section-children"><a href="OutlineStyle.html">OutlineStyle</a></div><div class="sidebar-section-children"><a href="ParserOptionsSpec.html">ParserOptionsSpec</a></div><div class="sidebar-section-children"><a href="ShapeSpec.html">ShapeSpec</a></div><div class="sidebar-section-children"><a href="SphereSpec.html">SphereSpec</a></div><div class="sidebar-section-children"><a href="SphereStyleSpec.html">SphereStyleSpec</a></div><div class="sidebar-section-children"><a href="StickStyleSpec.html">StickStyleSpec</a></div><div class="sidebar-section-children"><a href="SurfaceStyleSpec.html">SurfaceStyleSpec</a></div><div class="sidebar-section-children"><a href="UnitCellStyleSpec.html">UnitCellStyleSpec</a></div><div class="sidebar-section-children"><a href="ViewStyle.html">ViewStyle</a></div><div class="sidebar-section-children"><a href="ViewerGridSpec.html">ViewerGridSpec</a></div><div class="sidebar-section-children"><a href="ViewerSpec.html">ViewerSpec</a></div><div class="sidebar-section-children"><a href="VolumetricRendererSpec.html">VolumetricRendererSpec</a></div><div class="sidebar-section-children"><a href="WithinSelectionSpec.html">WithinSelectionSpec</a></div><div class="sidebar-section-children"><a href="global.html#XYZ">XYZ</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="jmKnIXRJjdjZ366ci7cdd"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#BCIF">BCIF</a></div><div class="sidebar-section-children"><a href="global.html#CAP">CAP</a></div><div class="sidebar-section-children"><a href="global.html#CDJSON">CDJSON</a></div><div class="sidebar-section-children"><a href="global.html#CIF">CIF</a></div><div class="sidebar-section-children"><a href="global.html#CUBE">CUBE</a></div><div class="sidebar-section-children"><a href="global.html#ColorSpec">ColorSpec</a></div><div class="sidebar-section-children"><a href="global.html#ColorschemeSpec">ColorschemeSpec</a></div><div class="sidebar-section-children"><a href="global.html#GRO">GRO</a></div><div class="sidebar-section-children"><a href="global.html#GradientSpec">GradientSpec</a></div><div class="sidebar-section-children"><a href="global.html#MMTFparser">MMTFparser</a></div><div class="sidebar-section-children"><a href="global.html#MOL2">MOL2</a></div><div class="sidebar-section-children"><a href="global.html#OFFSETS">OFFSETS</a></div><div class="sidebar-section-children"><a href="global.html#PDB">PDB</a></div><div class="sidebar-section-children"><a href="global.html#PQR">PQR</a></div><div class="sidebar-section-children"><a href="global.html#PRMTOP">PRMTOP</a></div><div class="sidebar-section-children"><a href="global.html#SDF">SDF</a></div><div class="sidebar-section-children"><a href="global.html#SurfaceType">SurfaceType</a></div><div class="sidebar-section-children"><a href="global.html#VASP">VASP</a></div><div class="sidebar-section-children"><a href="global.html#XYZ">XYZ</a></div><div class="sidebar-section-children"><a href="global.html#assignPDBBonds">assignPDBBonds</a></div><div class="sidebar-section-children"><a href="global.html#builtinColorSchemes">builtinColorSchemes</a></div><div class="sidebar-section-children"><a href="global.html#builtinGradients">builtinGradients</a></div><div class="sidebar-section-children"><a href="global.html#conversionMatrix3">conversionMatrix3</a></div><div class="sidebar-section-children"><a href="global.html#createViewer">createViewer</a></div><div class="sidebar-section-children"><a href="global.html#createViewerGrid">createViewerGrid</a></div><div class="sidebar-section-children"><a href="global.html#decode">decode</a></div><div class="sidebar-section-children"><a href="global.html#dic">dic</a></div><div class="sidebar-section-children"><a href="global.html#download">download</a></div><div class="sidebar-section-children"><a href="global.html#elementColors">elementColors</a></div><div class="sidebar-section-children"><a href="global.html#get">get</a></div><div class="sidebar-section-children"><a href="global.html#getColorFromStyle">getColorFromStyle</a></div><div class="sidebar-section-children"><a href="global.html#getbin">getbin</a></div><div class="sidebar-section-children"><a href="global.html#parseV3000">parseV3000</a></div><div class="sidebar-section-children"><a href="global.html#setSyncSurface">setSyncSurface</a></div><div class="sidebar-section-children"><a href="global.html#ssColors">ssColors</a></div><div class="sidebar-section-children"><a href="global.html#syncSurface">syncSurface</a></div><div class="sidebar-section-children"><a href="global.html#viewers">viewers</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"><div class="navbar-item"><a id="" href="https://3dmol.org/doc/index.html" target="">Documentation</a></div><div class="navbar-item"><a id="" href="tutorial-home.html" target="">Tutorials</a></div><div class="navbar-item"><a id="" href="https://github.com/3dmol/3Dmol.js" target="_blank">GitHub</a></div></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#dark-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">GLViewer.ts</h1></header><article><pre class="prettyprint source lang-js"><code>//a molecular viewer based on GLMol import { decode, encode, toRGBA8 } from 'upng-js'; import { AtomStyleSpec, GLModel, LineStyleSpec } from "./GLModel"; import { ArrowSpec, BoxSpec, CurveSpec, CustomShapeSpec, CylinderSpec, GLShape, IsoSurfaceSpec, LineSpec, ShapeSpec, SphereSpec, splitMesh } from "./GLShape"; import { getGradient, Gradient } from "./Gradient"; import { Label, LabelSpec } from "./Label"; import { ProteinSurface, SurfaceType, syncSurface } from "./ProteinSurface4"; import { VolumeData } from "./VolumeData"; import { GLVolumetricRender, VolumetricRendererSpec } from "./VolumetricRender"; import { Camera, Coloring, Fog, FrontSide, Geometry, Light, Line, LineBasicMaterial, Material, Mesh, MeshDoubleLambertMaterial, MeshLambertMaterial, Object3D, Projector, Raycaster, Renderer, Scene } from "./WebGL"; import { Matrix3, Matrix4, Quaternion, Vector3, XYZ } from "./WebGL/math"; import { CC, ColorschemeSpec, ColorSpec, elementColors } from "./colors"; import { AtomSelectionSpec, AtomSpec } from "./specs"; import { adjustVolumeStyle, extend, getColorFromStyle, getElement, getExtent, getPropertyRange, isEmptyObject, makeFunction, mergeGeos, PausableTimer } from "./utilities"; export const CONTEXTS_PER_VIEWPORT = 16; interface SurfObj { geo: Geometry; mat: Material; done: Boolean; finished: Boolean; lastGL?: any; symmetries?: any[]; style?: SurfaceStyleSpec; } /** * A surface. * * @class */ class Surface extends Array&lt;SurfObj> { style?: SurfaceStyleSpec; atomsel?: AtomSelectionSpec; allsel?: AtomSelectionSpec; focus?: AtomSelectionSpec; constructor(items: SurfObj[]) { super(...items); // spread the array elements into the Array constructor } /** * Returns list of rotational/translational matrices if there is BIOMT data * Otherwise returns a list of just the ID matrix * * @return {Array&lt;Matrix4>} * */ public getSymmetries() { //we assume all sub-objects have same symmetries if (this.length == 0) return []; let obj = this[0]; if (typeof (obj.symmetries) == 'undefined') { this.setSymmetries([new Matrix4()]); } return obj.symmetries; }; /** * Sets symmetries based on specified matrices in list * * @param {Array&lt;Matrix4>} list * */ public setSymmetries(syms) { if (typeof (syms) == "undefined") { //delete sym data syms = [new Matrix4()]; } for (let obj of this) { obj.symmetries = syms; obj.finished = false; //trigger redraw } }; } /** * WebGL-based 3Dmol.js viewer * Note: The preferred method of instantiating a GLViewer is through {@link createViewer} * * @class */ export class GLViewer { // private class variables private static numWorkers = 4; // number of threads for surface generation private static maxVolume = 64000; // how much to break up surface calculations private callback: any; private defaultcolors: any; private config: ViewerSpec; private nomouse = false; private bgColor: any; private camerax: number; private _viewer: GLViewer; private glDOM: HTMLCanvasElement | null = null; private models: GLModel[] = []; // atomistic molecular models private surfaces: Record&lt;number, Surface> = {}; private shapes = []; // Generic shapes private labels: Label[] = []; private clickables = []; //things you can click on private hoverables = []; //things you can hover over private contextMenuEnabledObjects = []; // atoms and shapes with context menu private current_hover: any = null; private hoverDuration = 500; private longTouchDuration = 1000; private viewer_frame = 0; private WIDTH: number; private HEIGHT: number; private viewChangeCallback: any = null; private stateChangeCallback: any = null; private NEAR = 1; private FAR = 800; private CAMERA_Z = 150; private fov = 20; private linkedViewers = []; private renderer: Renderer | null = null; private row: number; private col: number; private cols: number; private rows: number; private viewers: any; private control_all = false; private ASPECT: any; private camera: Camera; private lookingAt: Vector3; private raycaster: Raycaster; private projector: Projector; private scene: any = null; private rotationGroup: any = null; // which contains modelGroup private modelGroup: any = null; private fogStart = 0.4; private fogEnd = 1.0; private slabNear = -50; // relative to the center of rotationGroup private slabFar = 50; public container: HTMLElement | null; static readonly surfaceTypeMap = { "VDW": SurfaceType.VDW, "MS": SurfaceType.MS, "SAS": SurfaceType.SAS, "SES": SurfaceType.SES }; private cq = new Quaternion(0, 0, 0, 1); private dq = new Quaternion(0, 0, 0, 1); private animated = 0; private animationTimers = new Set&lt;PausableTimer>(); private isDragging = false; private mouseStartX = 0; private mouseStartY = 0; private touchDistanceStart = 0; private touchHold = false; private currentModelPos = 0; private cz = 0; private cslabNear = 0; private cslabFar = 0; private mouseButton: any; private hoverTimeout: any; private longTouchTimeout: any; private divwatcher: any; private intwatcher: any; private spinInterval: any; private getWidth() { let div = this.container; //offsetwidth accounts for scaling let w = div.offsetWidth; if (w == 0 &amp;&amp; div.style.display === 'none') { let oldpos = div.style.position; let oldvis = div.style.visibility; div.style.display = 'block'; div.style.visibility = 'hidden'; div.style.position = 'absolute'; w = div.offsetWidth; div.style.display = 'none'; div.style.visibility = oldvis; div.style.position = oldpos; } return w; }; private getHeight() { let div = this.container; let h = div.offsetHeight; if (h == 0 &amp;&amp; div.style.display === 'none') { let oldpos = div.style.position; let oldvis = div.style.visibility; div.style.display = 'block'; div.style.visibility = 'hidden'; div.style.position = 'absolute'; h = div.offsetHeight; div.style.display = 'none'; div.style.visibility = oldvis; div.style.position = oldpos; } return h; }; private setupRenderer() { let rendopt = { ...this.config, preserveDrawingBuffer: true, //so we can export images premultipliedAlpha: false,/* more traditional compositing with background */ //cannot initialize with zero size - render will start out lost containerWidth: this.WIDTH, containerHeight: this.HEIGHT }; this.renderer = new Renderer(rendopt); this.renderer.domElement.style.width = "100%"; this.renderer.domElement.style.height = "100%"; this.renderer.domElement.style.padding = "0"; this.renderer.domElement.style.position = "absolute"; //TODO: get rid of this this.renderer.domElement.style.top = "0px"; this.renderer.domElement.style.left = "0px"; this.renderer.domElement.style.zIndex = "0"; } private initializeScene() { this.scene = new Scene(); this.scene.fog = new Fog(this.bgColor, 100, 200); this.modelGroup = new Object3D(); this.rotationGroup = new Object3D(); this.rotationGroup.useQuaternion = true; this.rotationGroup.quaternion = new Quaternion(0, 0, 0, 1); this.rotationGroup.add(this.modelGroup); this.scene.add(this.rotationGroup); // setup lights var directionalLight = new Light(0xFFFFFF); directionalLight.position = new Vector3(0.2, 0.2, 1) .normalize(); directionalLight.intensity = 1.0; this.scene.add(directionalLight); }; private _handleLostContext(event) { //when contexts go missing, try to regenerate any that are visible on screen //but no more than CONTEXTS_PER_VIEWPORT (if this is set higher than the //browser limit there will be an infinity loop of refreshing contexts of //too many are on screen) const isVisible = function (cont) { const rect = cont.getBoundingClientRect(); return !( rect.right &lt; 0 || rect.bottom &lt; 0 || rect.top > (window.innerHeight || document.documentElement.clientHeight) || rect.left > (window.innerWidth || document.documentElement.clientWidth) ); }; if (isVisible(this.container)) { let restored = 0; for (let c of document.getElementsByTagName('canvas')) { if (isVisible(c) &amp;&amp; (c as any)._3dmol_viewer != undefined) { (c as any)._3dmol_viewer.resize(); restored += 1; if (restored >= CONTEXTS_PER_VIEWPORT) break; } } } } private initContainer(element) { this.container = element; this.WIDTH = this.getWidth(); this.HEIGHT = this.getHeight(); this.ASPECT = this.renderer.getAspect(this.WIDTH, this.HEIGHT); this.renderer.setSize(this.WIDTH, this.HEIGHT); this.container.append(this.renderer.domElement); this.glDOM = this.renderer.domElement; (this.glDOM as any)._3dmol_viewer = this; this.glDOM.addEventListener("webglcontextlost", this._handleLostContext.bind(this)); if (!this.nomouse) { // user can request that the mouse handlers not be installed this.glDOM.addEventListener('mousedown', this._handleMouseDown.bind(this), { passive: false }); this.glDOM.addEventListener('touchstart', this._handleMouseDown.bind(this), { passive: false }); this.glDOM.addEventListener('wheel', this._handleMouseScroll.bind(this), { passive: false }); this.glDOM.addEventListener('mousemove', this._handleMouseMove.bind(this), { passive: false }); this.glDOM.addEventListener('touchmove', this._handleMouseMove.bind(this), { passive: false }); this.glDOM.addEventListener("contextmenu", this._handleContextMenu.bind(this), { passive: false }); } }; private decAnim() { //decrement the number of animations currently this.animated--; if (this.animated &lt; 0) this.animated = 0; }; private incAnim() { this.animated++; }; private nextSurfID() { //compute the next highest surface id directly from surfaces //this is necessary to support linking of model data var max = 0; for (let i in this.surfaces) { // this is an object with possible holes if (!this.surfaces.hasOwnProperty(i)) continue; var val = parseInt(i); if (!isNaN(val)) { if (val > max) max = val; } } return max + 1; }; private setSlabAndFog() { let center = this.camera.position.z - this.rotationGroup.position.z; if (center &lt; 1) center = 1; this.camera.near = center + this.slabNear; if (!this.camera.ortho &amp;&amp; this.camera.near &lt; 1) this.camera.near = 1; this.camera.far = center + this.slabFar; if (this.camera.near + 1 > this.camera.far) this.camera.far = this.camera.near + 1; this.camera.fov = this.fov; this.camera.right = center * Math.tan(Math.PI / 180 * this.fov); this.camera.left = -this.camera.right; this.camera.top = this.camera.right / this.ASPECT; this.camera.bottom = -this.camera.top; this.camera.updateProjectionMatrix(); this.scene.fog.near = this.camera.near + this.fogStart * (this.camera.far - this.camera.near); this.scene.fog.far = this.camera.near + this.fogEnd * (this.camera.far - this.camera.near); if (this.config.disableFog) { this.scene.fog.near = this.scene.fog.far; } }; // display scene //if nolink is set/true, don't propagate changes to linked viewers private show(nolink?) { this.renderer.setViewport(); if (!this.scene) return; //let time = new Date(); this.setSlabAndFog(); this.renderer.render(this.scene, this.camera); //console.log("rendered in " + (+new Date() - (time as any)) + "ms"); //have any scene change trigger a callback if (this.viewChangeCallback) this.viewChangeCallback(this._viewer.getView()); if (!nolink &amp;&amp; this.linkedViewers.length > 0) { var view = this._viewer.getView(); for (var i = 0; i &lt; this.linkedViewers.length; i++) { var other = this.linkedViewers[i]; other.setView(view, true); } } }; //regenerate the list of clickables //also updates hoverables private updateClickables() { this.clickables.splice(0, this.clickables.length); this.hoverables.splice(0, this.hoverables.length); this.contextMenuEnabledObjects.splice(0, this.contextMenuEnabledObjects.length); for (let i = 0, il = this.models.length; i &lt; il; i++) { let model = this.models[i]; if (model) { let atoms = model.selectedAtoms({ clickable: true }); let hoverable_atoms = model.selectedAtoms({ hoverable: true }); let contextMenuEnabled_atom = model.selectedAtoms({ contextMenuEnabled: true }); // Array.prototype.push.apply(hoverables,hoverable_atoms); for (let n = 0; n &lt; hoverable_atoms.length; n++) { this.hoverables.push(hoverable_atoms[n]); } // Array.prototype.push.apply(clickables, atoms); //add atoms into clickables for (let m = 0; m &lt; atoms.length; m++) { this.clickables.push(atoms[m]); } // add atoms into contextMenuEnabledObjects for (let m = 0; m &lt; contextMenuEnabled_atom.length; m++) { this.contextMenuEnabledObjects.push(contextMenuEnabled_atom[m]); } } } for (let i = 0, il = this.shapes.length; i &lt; il; i++) { let shape = this.shapes[i]; if (shape &amp;&amp; shape.clickable) { this.clickables.push(shape); } if (shape &amp;&amp; shape.hoverable) { this.hoverables.push(shape); } if (shape &amp;&amp; shape.contextMenuEnabled) { this.contextMenuEnabledObjects.push(shape); } } }; // Checks for selection intersects on mousedown private handleClickSelection(mouseX: number, mouseY: number, event) { let intersects = this.targetedObjects(mouseX, mouseY, this.clickables); // console.log('handleClickSelection', mouseX, mouseY, intersects); if (intersects.length) { var selected = intersects[0].clickable; if (selected.callback !== undefined) { if (typeof (selected.callback) != "function") { selected.callback = makeFunction(selected.callback); } if (typeof (selected.callback) === "function") { // Suppress click callbacks when context menu will be invoked. // This only applies to clicks from "mouseup" events after right-click. // Clicks from "touchend" after longtouch contextmenu are suppressed // in _handleContextMenu. const isContextMenu = this.mouseButton === 3 &amp;&amp; this.contextMenuEnabledObjects.includes(selected) &amp;&amp; this.userContextMenuHandler; if (!isContextMenu) { selected.callback(selected, this._viewer, event, this.container, intersects); } } } } }; //return offset of container private canvasOffset() { let canvas = this.glDOM; let rect = canvas.getBoundingClientRect(); let doc = canvas.ownerDocument; let docElem = doc.documentElement; let win = doc.defaultView; return { top: rect.top + win.pageYOffset - docElem.clientTop, left: rect.left + win.pageXOffset - docElem.clientLeft }; }; //set current_hover to sel (which can be null), calling appropraite callbacks private setHover(selected, event?, intersects?) { if (this.current_hover == selected) return; if (this.current_hover) { if (typeof (this.current_hover.unhover_callback) != "function") { this.current_hover.unhover_callback = makeFunction(this.current_hover.unhover_callback); } this.current_hover.unhover_callback(this.current_hover, this._viewer, event, this.container, intersects); } this.current_hover = selected; if (selected &amp;&amp; selected.hover_callback !== undefined) { if (typeof (selected.hover_callback) != "function") { selected.hover_callback = makeFunction(selected.hover_callback); } if (typeof (selected.hover_callback) === "function") { selected.hover_callback(selected, this._viewer, event, this.container, intersects); } } }; //checks for selection intersects on hover private handleHoverSelection(mouseX, mouseY, event) { if (this.hoverables.length == 0) return; let intersects = this.targetedObjects(mouseX, mouseY, this.hoverables); if (intersects.length) { var selected = intersects[0].clickable; this.setHover(selected, event, intersects); this.current_hover = selected; } else { this.setHover(null); } }; //sees if the mouse is still on the object that invoked a hover event and if not then the unhover callback is called private handleHoverContinue(mouseX: number, mouseY: number) { let intersects = this.targetedObjects(mouseX, mouseY, this.hoverables); if (intersects.length == 0 || intersects[0] === undefined) { this.setHover(null); } if (intersects[0] !== undefined &amp;&amp; intersects[0].clickable !== this.current_hover) { this.setHover(null); } }; /** * Determine if a positioned event is "close enough" to mouseStart to be considered a click. * With a mouse, the position should be exact, but allow a slight delta for a touch interface. * @param {Event} event * @param {{ allowTolerance, tolerance: number }} options */ private closeEnoughForClick(event, { allowTolerance = event.targetTouches, tolerance = 5 } = {}) { const x = this.getX(event); const y = this.getY(event); if (allowTolerance) { const deltaX = Math.abs(x - this.mouseStartX); const deltaY = Math.abs(y - this.mouseStartY); return deltaX &lt;= tolerance &amp;&amp; deltaY &lt;= tolerance; } else { return x === this.mouseStartX &amp;&amp; y === this.mouseStartY; } } private calcTouchDistance(ev) { // distance between first two // fingers var xdiff = ev.targetTouches[0].pageX - ev.targetTouches[1].pageX; var ydiff = ev.targetTouches[0].pageY - ev.targetTouches[1].pageY; return Math.hypot(xdiff, ydiff); }; //check targetTouches as well private getX(ev) { var x = ev.pageX; if (x == undefined) x = ev.pageX; //firefox if (ev.targetTouches &amp;&amp; ev.targetTouches[0]) { x = ev.targetTouches[0].pageX; } else if (ev.changedTouches &amp;&amp; ev.changedTouches[0]) { x = ev.changedTouches[0].pageX; } return x; }; private getY(ev) { var y = ev.pageY; if (y == undefined) y = ev.pageY; if (ev.targetTouches &amp;&amp; ev.targetTouches[0]) { y = ev.targetTouches[0].pageY; } else if (ev.changedTouches &amp;&amp; ev.changedTouches[0]) { y = ev.changedTouches[0].pageY; } return y; }; //for grid viewers, return true if point is in this viewer private isInViewer(x: number, y: number) { if (this.viewers != undefined) { var width = this.WIDTH / this.cols; var height = this.HEIGHT / this.rows; var offset = this.canvasOffset(); var relx = (x - offset.left); var rely = (y - offset.top); var r = this.rows - Math.floor(rely / height) - 1; var c = Math.floor(relx / width); if (r != this.row || c != this.col) return false; } return true; }; //if the user has specify zoom limits, readjust to fit within them //also, make sure we don't go past CAMERA_Z private adjustZoomToLimits(z: number) { //a lower limit of 0 is at CAMERA_Z if (this.config.lowerZoomLimit &amp;&amp; this.config.lowerZoomLimit > 0) { let lower = this.CAMERA_Z - this.config.lowerZoomLimit; if (z > lower) z = lower; } if (this.config.upperZoomLimit &amp;&amp; this.config.upperZoomLimit > 0) { let upper = this.CAMERA_Z - this.config.upperZoomLimit; if (z &lt; upper) z = upper; } if (z > this.CAMERA_Z - 1) { z = this.CAMERA_Z - 1; //avoid getting stuck } return z; }; //interpolate between two normalized quaternions (t between 0 and 1) //https://en.wikipedia.org/wiki/Slerp private static slerp(v0: Quaternion, v1: Quaternion, t: number) { // Compute the cosine of the angle between the two vectors. //dot product if (t == 1) return v1.clone(); else if (t == 0) return v0.clone(); let dot = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z + v0.w * v1.w; if (dot > 0.9995) { // If the inputs are too close for comfort, linearly interpolate // and normalize the result. let result = new Quaternion( v0.x + t * (v1.x - v0.x), v0.y + t * (v1.y - v0.y), v0.z + t * (v1.z - v0.z), v0.w + t * (v1.w - v0.w)); result.normalize(); return result; } // If the dot product is negative, the quaternions // have opposite handed-ness and slerp won't take // the shorted path. Fix by reversing one quaternion. if (dot &lt; 0.0) { v1 = v1.clone().multiplyScalar(-1); dot = -dot; } if (dot > 1) dot = 1.0; else if (dot &lt; -1) dot = -1.0; var theta_0 = Math.acos(dot); // theta_0 = angle between input vectors var theta = theta_0 * t; // theta = angle between v0 and result var v2 = v1.clone(); v2.sub(v0.clone().multiplyScalar(dot)); v2.normalize(); // { v0, v2 } is now an orthonormal basis var c = Math.cos(theta); var s = Math.sin(theta); var ret = new Quaternion( v0.x * c + v2.x * s, v0.y * c + v2.y * s, v0.z * c + v2.z * s, v0.w * c + v2.w * s ); ret.normalize(); return ret; }; /* @param {Object} element HTML element within which to create viewer * @param {ViewerSpec} config Object containing optional configuration for the viewer */ constructor(element, c: ViewerSpec = {}) { // set variables this.config = c; this.callback = this.config.callback; this.defaultcolors = this.config.defaultcolors; if (!this.defaultcolors) this.defaultcolors = elementColors.defaultColors; this.nomouse = Boolean(this.config.nomouse); this.bgColor = 0; this.config.backgroundColor = this.config.backgroundColor || "#ffffff"; if (typeof (this.config.backgroundColor) != 'undefined') { this.bgColor = CC.color(this.config.backgroundColor).getHex(); } this.config.backgroundAlpha = this.config.backgroundAlpha == undefined ? 1.0 : this.config.backgroundAlpha; this.camerax = 0; if (typeof (this.config.camerax) != 'undefined') { this.camerax = typeof (this.config.camerax) === 'string' ? parseFloat(this.config.camerax) : this.config.camerax; } this._viewer = this; this.container = element; //we expect container to be HTMLElement if (this.config.hoverDuration != undefined) { this.hoverDuration = this.config.hoverDuration; } if (this.config.antialias === undefined) this.config.antialias = true; if (this.config.cartoonQuality === undefined) this.config.cartoonQuality = 10; this.WIDTH = this.getWidth(); this.HEIGHT = this.getHeight(); this.setupRenderer(); this.row = this.config.row == undefined ? 0 : this.config.row; this.col = this.config.col == undefined ? 0 : this.config.col; this.cols = this.config.cols; this.rows = this.config.rows; this.viewers = this.config.viewers; this.control_all = this.config.control_all; this.ASPECT = this.renderer.getAspect(this.WIDTH, this.HEIGHT); this.camera = new Camera(this.fov, this.ASPECT, this.NEAR, this.FAR, this.config.orthographic); this.camera.position = new Vector3(this.camerax, 0, this.CAMERA_Z); this.lookingAt = new Vector3(); this.camera.lookAt(this.lookingAt); this.raycaster = new Raycaster(new Vector3(0, 0, 0), new Vector3(0, 0, 0)); this.projector = new Projector(); this.initializeScene(); this.renderer.setClearColorHex(this.bgColor, this.config.backgroundAlpha); this.scene.fog.color = CC.color(this.bgColor); // this event is bound to the body element, not the container, // so no need to put it inside initContainer() document.body.addEventListener('mouseup', this._handleMouseUp.bind(this)); document.body.addEventListener('touchend', this._handleMouseUp.bind(this)); this.initContainer(this.container); if (this.config.style) { //enable setting style in constructor this.setViewStyle(this.config as ViewStyle); } window.addEventListener("resize", this.resize.bind(this)); if (typeof (window.ResizeObserver) !== "undefined") { this.divwatcher = new window.ResizeObserver(this.resize.bind(this)); this.divwatcher.observe(this.container); } if (typeof (window.IntersectionObserver) !== "undefined") { //make sure a viewer that is becoming visible is alive let intcallback = (entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { this.resize(); } }); }; this.intwatcher = new window.IntersectionObserver(intcallback); this.intwatcher.observe(this.container); } try { if (typeof (this.callback) === "function") this.callback(this); } catch (e) { // errors in callback shouldn't invalidate the viewer console.log("error with glviewer callback: " + e); } }; /** * Return a list of objects that intersect that at the specified viewer position. * * @param x - x position in screen coordinates * @param y - y position in screen coordinates * @param {Object[]} - list of objects or selection object specifying what object to check for targeting */ public targetedObjects(x: number, y: number, objects) { var mouse = { x: x, y: y, z: -1.0 }; if (!Array.isArray(objects)) { //assume selection object objects = this.selectedAtoms(objects); } if (objects.length == 0) return []; this.raycaster.setFromCamera(mouse, this.camera); return this.raycaster.intersectObjects(this.modelGroup, objects); }; /** Convert model coordinates to screen coordinates. * @param {object | list} - an object or list of objects with x,y,z attributes (e.g. an atom) * @return {object | list} - and object or list of {x: screenX, y: screenY} */ public modelToScreen(coords) { let returnsingle = false; if (!Array.isArray(coords)) { coords = [coords]; returnsingle = true; } let ratioX = this.renderer.getXRatio(); let ratioY = this.renderer.getYRatio(); let col = this.col; let row = this.row; let viewxoff = col * (this.WIDTH / ratioX); //row is from bottom let viewyoff = (ratioY - row - 1) * (this.HEIGHT / ratioY); let results = []; let offset = this.canvasOffset(); coords.forEach(coord => { let t = new Vector3(coord.x, coord.y, coord.z); t.applyMatrix4(this.modelGroup.matrixWorld); this.projector.projectVector(t, this.camera); let screenX = (this.WIDTH / ratioX) * (t.x + 1) / 2.0 + offset.left + viewxoff; let screenY = -(this.HEIGHT / ratioY) * (t.y - 1) / 2.0 + offset.top + viewyoff; results.push({ x: screenX, y: screenY }); }); if (returnsingle) results = results[0]; return results; }; /** * For a given screen (x,y) displacement return model displacement * @param{x} x displacement in screen coordinates * @param{y} y displacement in screen corodinates * @param{modelz} z coordinate in model coordinates to compute offset for, default is model axis */ public screenOffsetToModel(x: number, y: number, modelz?) { var dx = x / this.WIDTH; var dy = y / this.HEIGHT; var zpos = (modelz === undefined ? this.rotationGroup.position.z : modelz); var q = this.rotationGroup.quaternion; var t = new Vector3(0, 0, zpos); this.projector.projectVector(t, this.camera); t.x += dx * 2; t.y -= dy * 2; this.projector.unprojectVector(t, this.camera); t.z = 0; t.applyQuaternion(q); return t; }; /** * Distance from screen coordinate to model coordinate assuming screen point * is projected to the same depth as model coordinate * @param{screen} xy screen coordinate * @param{model} xyz model coordinate */ public screenToModelDistance(screen: XYZ, model) { let offset = this.canvasOffset(); //convert model to screen to get screen z let mvec = new Vector3(model.x, model.y, model.z); mvec.applyMatrix4(this.modelGroup.matrixWorld); let m = mvec.clone(); this.projector.projectVector(mvec, this.camera); let t = new Vector3((screen.x - offset.left) * 2 / this.WIDTH - 1, (screen.y - offset.top) * 2 / -this.HEIGHT + 1, mvec.z); this.projector.unprojectVector(t, this.camera); return t.distanceTo(m); }; /** * Set a callback to call when the view has potentially changed. * */ public setViewChangeCallback(callback) { if (typeof (callback) === 'function' || callback == null) this.viewChangeCallback = callback; }; /** * Set a callback to call when the view has potentially changed. * */ public setStateChangeCallback(callback) { if (typeof (callback) === 'function' || callback == null) this.stateChangeCallback = callback; }; /** * Return configuration of viewer */ public getConfig() { return this.config; }; /** * Set the configuration object. Note that some settings may only * have an effect at viewer creation time. */ public setConfig(c: ViewerSpec) { this.config = c; if (c.ambientOcclusion) { this.renderer.enableAmb