speck-renderer
Version:
Browser-based WebGL molecule renderer with the goal of producing figures that are as attractive as they are practical.
354 lines (325 loc) • 10.9 kB
JavaScript
"use strict";
import dat from 'dat.gui/build/dat.gui';
import Renderer from './renderer';
import View from './view';
import System from './system';
import xyz from './xyz';
import datGUISettings from './config/dat-gui';
import {caffeine} from './molecules/caffeine';
class Speck {
constructor({canvasContainerID, canvasID, options}) {
this.canvasContainer = document.getElementById(canvasContainerID);
this.canvas = document.getElementById(canvasID);
this.system = System.new();
this.view = View.new();
const defaultOptions = {
zoomRatio: 1
};
this.options = {
...defaultOptions,
...options
};
this.gui = new dat.GUI({load: datGUISettings});
this.renderer = new Speck.Renderer({canvas: this.canvas, canvasContainer: this.canvasContainer, system: this.system, view: this.view, options: this.options});
this._init();
this.loadStructure(caffeine);
this.renderer.init();
}
setOptions(options) {
const newOptions = {
...this.options,
...options
}
this.options = newOptions;
this.renderer.options = newOptions;
}
getOptions() {
return this.options;
}
_init() {
let speck = this.renderer;
this.gui.remember(speck);
let controllers = [];
let atomsFolder = this.gui.addFolder('Atoms');
controllers.push(atomsFolder.add(speck, 'atomRadius', 0, 100));
controllers.push(atomsFolder.add(speck, 'relativeAtomRadius', 0, 100));
controllers.push(atomsFolder.add(speck, 'atomShade', 0, 100));
let bondsFolder = this.gui.addFolder('Bonds');
controllers.push(bondsFolder.add(speck, 'displayBonds'));
controllers.push(bondsFolder.add(speck, 'bondRadius', 0, 100));
controllers.push(bondsFolder.add(speck, 'bondThreshold').min(0));
controllers.push(bondsFolder.add(speck, 'bondShade', 0, 100));
let shadingFolder = this.gui.addFolder('Shading');
controllers.push(shadingFolder.add(speck, 'ambientOcclusion', 0, 100));
controllers.push(shadingFolder.add(speck, 'brightness', 0, 100));
controllers.push(shadingFolder.add(speck, 'AOResolution', [
16,
64,
128,
256,
512,
1024,
2048
]));
controllers.push(shadingFolder.add(speck, 'samplesPerFrame', [
0,
1,
2,
4,
8,
16,
32,
64,
128
]));
let depthOfFieldFolder = this.gui.addFolder('Depth of Field');
controllers.push(depthOfFieldFolder.add(speck, 'depthOfFieldStrength', 0, 100));
controllers.push(depthOfFieldFolder.add(speck, 'depthOfFieldPosition', 0, 100));
let detailFolder = this.gui.addFolder('Detail');
controllers.push(detailFolder.add(speck, 'outlineStrength', 0, 100));
controllers.push(detailFolder.add(speck, 'antialiasingPasses').min(1).step(1));
controllers.push(detailFolder.add(speck, 'resolution', [
64,
128,
256,
512,
1024,
2048
]));
this.gui.add(speck, 'center');
this.gui.add(speck, 'downloadImage');
for (let i = 0, len = controllers.length; i < len; i++) {
let controller = controllers[i];
controller.onChange((value) => {
switch (controller.property) {
case "atomRadius":
this.renderer.view.atomScale = value / 100;
break;
case "relativeAtomRadius":
this.renderer.view.relativeAtomScale = value / 100;
break;
case "atomShade":
this.renderer.view.atomShade = value / 100;
break;
case "displayBonds":
this.renderer.view.bonds = value;
this.renderer.renderer.setSystem(this.renderer.system, this.renderer.view);
break;
case "bondRadius":
this.renderer.view.bondScale = value / 100;
break;
case "bondThreshold":
this.renderer.view.bondThreshold = value;
this.renderer.renderer.setSystem(this.renderer.system, this.renderer.view);
break;
case "bondShade":
this.renderer.view.bondShade = value / 100;
break;
case "ambientOcclusion":
this.renderer.view.ao = value / 100;
break;
case "brightness":
this.renderer.view.brightness = value / 100;
break;
case "AOResolution":
this.renderer.renderer.setResolution(speck.resolution, value);
break;
case "samplesPerFrame":
this.renderer.view.spf = value;
break;
case "depthOfFieldStrength":
this.renderer.view.dofStrength = value / 100;
break;
case "depthOfFieldPosition":
this.renderer.view.dofPosition = value / 100;
break;
case "outlineStrength":
this.renderer.view.outline = value / 100;
break;
case "antialiasingPasses":
this.renderer.view.fxaa = value;
break;
case "resolution":
this.renderer.renderer.setResolution(value, speck.AOResolution);
break;
}
View.resolve(this.renderer.view);
this.renderer.reset();
});
}
this.gui.closed = true;
}
loadStructure(molecule) {
let structure = xyz(molecule)[0]
this.system = System.new();
for (let i = 0; i < structure.length; i++) {
let a = structure[i];
let x = a.position[0];
let y = a.position[1];
let z = a.position[2];
System.addAtom(this.system, a.symbol, x, y, z);
}
System.center(this.system);
System.calculateBonds(this.system);
this.renderer.system = this.system;
this.renderer.renderer.setSystem(this.system, this.view);
View.center(this.view, this.system, this.options.zoomRatio);
this.renderer.reset();
}
destroy() {
this.gui.destroy();
this.renderer.destroy();
}
}
Speck.Renderer = class {
constructor({canvas, canvasContainer, system, view, options}) {
this.atomRadius = 60
this.relativeAtomRadius = 100
this.atomShade = 50
this.displayBonds = true
this.bondRadius = 50
this.bondThreshold = 1.2
this.bondShade = 50
this.ambientOcclusion = 75
this.brightness = 50
this.AOResolution = 512
this.samplesPerFrame = 32
this.depthOfFieldStrength = 0
this.depthOfFieldPosition = 50
this.outlineStrength = 0
this.antialiasingPasses = 1
this.resolution = 512
this.lastX = 0.0;
this.lastY = 0.0;
this.mouseDown = false;
this.needReset = false;
this.system = system;
this.view = view;
this.canvas = canvas;
this.canvasContainer = canvasContainer;
this.options = options;
this.renderer = new Renderer(this.canvas, this.view.resolution, this.view.aoRes);
}
init() {
this._createListeners();
this._loop();
this.view.atomScale = this.atomRadius / 100;
this.view.relativeAtomScale = this.relativeAtomRadius / 100;
this.view.atomShade = this.atomShade / 100;
this.view.bonds = this.displayBonds;
this.view.bondScale = this.bondRadius / 100;
this.view.bondThreshold = this.bondThreshold;
this.view.bondShade = this.bondShade / 100;
this.view.ao = this.ambientOcclusion / 100;
this.view.brightness = this.brightness / 100;
this.view.spf = this.samplesPerFrame;
this.view.dofStrength = this.depthOfFieldStrength / 100;
this.view.dofPosition = this.depthOfFieldPosition / 100;
this.view.outline = this.outlineStrength / 100;
this.view.fxaa = this.antialiasingPasses;
this.renderer.setSystem(this.system, this.view);
this.renderer.setResolution(this.resolution, this.AOResolution);
View.resolve(this.view);
this.reset();
}
get dataURL() {
this.renderer.render(this.view);
return this.canvas.toDataURL("image/png");
}
reset() {
this.needReset = true;
}
center() {
View.center(this.view, this.system, this.options.zoomRatio);
this.needReset = true;
}
downloadImage() {
this.renderer.render(this.view);
let imgURL = this.canvas.toDataURL("image/png");
let iframe = "<iframe width='100%' height='100%' style={border: none, overflow: auto} src='" + imgURL + "'></iframe>"
let x = window.open();
x.document.open();
x.document.write(iframe);
x.document.body.style.margin = "0";
x.document.body.style.height = "100%";
x.document.body.style.width = "100%";
x.document.close();
}
destroy() {
this._destroyListeners();
this._endLoop();
}
_canvasScrollListener(event) {
let wd = 0;
if (event.deltaY < 0) {
wd = 1;
} else {
wd = -1;
}
this.view.zoom = this.view.zoom * (
wd === 1
? 1 / 0.9
: 0.9);
this.needReset = true;
event.preventDefault();
}
_canvasMousedownListener(event) {
document.body.style.cursor = "none";
this.mouseDown = true;
this.lastX = event.clientX;
this.lastY = event.clientY;
}
_windowMouseupListener(event) {
document.body.style.cursor = "default";
this.mouseDown = false;
}
_windowMousemoveListener(event) {
if (!this.mouseDown) {
return;
}
let dx = event.clientX - this.lastX;
let dy = event.clientY - this.lastY;
if (dx == 0 && dy == 0) {
return;
}
if (event.shiftKey) {
View.translate(this.view, dx, dy);
} else {
View.rotate(this.view, dx, dy);
}
this.needReset = true;
this.lastX = event.clientX;
this.lastY = event.clientY;
}
_createListeners() {
this.canvasContainer.addEventListener("wheel", this._canvasScrollListener.bind(this));
this.canvasContainer.addEventListener("mousedown", this._canvasMousedownListener.bind(this));
window.addEventListener("mouseup", this._windowMouseupListener.bind(this));
window.addEventListener("mousemove", this._windowMousemoveListener.bind(this));
this.mouseCheckInterval = setInterval(function() {
if (!this.mouseDown) {
document.body.style.cursor = "default";
}
}, 20);
}
_destroyListeners() {
this.canvasContainer.removeEventListener("wheel", this._canvasScrollListener.bind(this));
this.canvasContainer.removeEventListener("mousedown", this._canvasMousedownListener.bind(this));
window.removeEventListener("mouseup", this._windowMouseupListener.bind(this));
window.removeEventListener("mousemove", this._windowMousemoveListener.bind(this));
clearInterval(this.mouseCheckInterval);
}
_loop() {
// document.getElementById("ao-indicator").style.width = Math.min(100, (renderer.getAOProgress() * 100)) + "%";
if (this.needReset) {
this.renderer.reset();
}
this.needReset = false;
this.renderer.render(this.view);
this.reqAnimationFrame = requestAnimationFrame(this._loop.bind(this));
}
_endLoop() {
cancelAnimationFrame(this.reqAnimationFrame)
}
}
export default Speck;