molstar
Version:
A comprehensive macromolecular library.
330 lines • 14 kB
JavaScript
/**
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Mat4, Vec3, Vec4, EPSILON } from '../mol-math/linear-algebra';
import { Viewport, cameraProject, cameraUnproject } from './camera/util';
import { CameraTransitionManager } from './camera/transition';
import { BehaviorSubject } from 'rxjs';
export { Camera };
var tmpPos1 = Vec3();
var tmpPos2 = Vec3();
var tmpClip = Vec4();
var Camera = /** @class */ (function () {
function Camera(state, viewport, props) {
if (viewport === void 0) { viewport = Viewport.create(0, 0, 128, 128); }
if (props === void 0) { props = {}; }
this.view = Mat4.identity();
this.projection = Mat4.identity();
this.projectionView = Mat4.identity();
this.inverseProjectionView = Mat4.identity();
this.state = Camera.createDefaultSnapshot();
this.viewOffset = Camera.ViewOffset();
this.near = 1;
this.far = 10000;
this.fogNear = 5000;
this.fogFar = 10000;
this.zoom = 1;
this.transition = new CameraTransitionManager(this);
this.stateChanged = new BehaviorSubject(this.state);
this.prevProjection = Mat4.identity();
this.prevView = Mat4.identity();
this.deltaDirection = Vec3();
this.newPosition = Vec3();
this.viewport = viewport;
this.pixelScale = props.pixelScale || 1;
Camera.copySnapshot(this.state, state);
}
Object.defineProperty(Camera.prototype, "pixelRatio", {
get: function () {
var dpr = (typeof window !== 'undefined') ? window.devicePixelRatio : 1;
return dpr * this.pixelScale;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Camera.prototype, "position", {
get: function () { return this.state.position; },
set: function (v) { Vec3.copy(this.state.position, v); },
enumerable: false,
configurable: true
});
Object.defineProperty(Camera.prototype, "up", {
get: function () { return this.state.up; },
set: function (v) { Vec3.copy(this.state.up, v); },
enumerable: false,
configurable: true
});
Object.defineProperty(Camera.prototype, "target", {
get: function () { return this.state.target; },
set: function (v) { Vec3.copy(this.state.target, v); },
enumerable: false,
configurable: true
});
Camera.prototype.update = function () {
var snapshot = this.state;
if (snapshot.radiusMax === 0) {
return false;
}
var height = 2 * Math.tan(snapshot.fov / 2) * Vec3.distance(snapshot.position, snapshot.target);
this.zoom = this.viewport.height / height;
updateClip(this);
switch (this.state.mode) {
case 'orthographic':
updateOrtho(this);
break;
case 'perspective':
updatePers(this);
break;
default: throw new Error('unknown camera mode');
}
var changed = !Mat4.areEqual(this.projection, this.prevProjection, EPSILON) || !Mat4.areEqual(this.view, this.prevView, EPSILON);
if (changed) {
Mat4.mul(this.projectionView, this.projection, this.view);
if (!Mat4.tryInvert(this.inverseProjectionView, this.projectionView)) {
Mat4.copy(this.view, this.prevView);
Mat4.copy(this.projection, this.prevProjection);
Mat4.mul(this.projectionView, this.projection, this.view);
return false;
}
Mat4.copy(this.prevView, this.view);
Mat4.copy(this.prevProjection, this.projection);
}
return changed;
};
Camera.prototype.setState = function (snapshot, durationMs) {
this.transition.apply(snapshot, durationMs);
this.stateChanged.next(snapshot);
};
Camera.prototype.getSnapshot = function () {
return Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state);
};
Camera.prototype.getTargetDistance = function (radius) {
return Camera.targetDistance(radius, this.state.fov, this.viewport.width, this.viewport.height);
};
Camera.prototype.getFocus = function (target, radius, up, dir) {
var r = Math.max(radius, 0.01);
var targetDistance = this.getTargetDistance(r);
Vec3.sub(this.deltaDirection, this.target, this.position);
if (dir)
Vec3.matchDirection(this.deltaDirection, dir, this.deltaDirection);
Vec3.setMagnitude(this.deltaDirection, this.deltaDirection, targetDistance);
Vec3.sub(this.newPosition, target, this.deltaDirection);
var state = Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state);
state.target = Vec3.clone(target);
state.radius = r;
state.position = Vec3.clone(this.newPosition);
if (up)
Vec3.matchDirection(state.up, up, state.up);
return state;
};
Camera.prototype.getInvariantFocus = function (target, radius, up, dir) {
var r = Math.max(radius, 0.01);
var targetDistance = this.getTargetDistance(r);
Vec3.copy(this.deltaDirection, dir);
Vec3.setMagnitude(this.deltaDirection, this.deltaDirection, targetDistance);
Vec3.sub(this.newPosition, target, this.deltaDirection);
var state = Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state);
state.target = Vec3.clone(target);
state.radius = r;
state.position = Vec3.clone(this.newPosition);
Vec3.copy(state.up, up);
return state;
};
Camera.prototype.focus = function (target, radius, durationMs, up, dir) {
if (radius > 0) {
this.setState(this.getFocus(target, radius, up, dir), durationMs);
}
};
/** Transform point into 2D window coordinates. */
Camera.prototype.project = function (out, point) {
return cameraProject(out, point, this.viewport, this.projectionView);
};
/**
* Transform point from screen space to 3D coordinates.
* The point must have `x` and `y` set to 2D window coordinates
* and `z` between 0 (near) and 1 (far); the optional `w` is not used.
*/
Camera.prototype.unproject = function (out, point) {
return cameraUnproject(out, point, this.viewport, this.inverseProjectionView);
};
/** World space pixel size at given `point` */
Camera.prototype.getPixelSize = function (point) {
// project -> unproject of `point` does not exactly return the same
// to get a sufficiently accurate measure we unproject the original
// clip position in addition to the one shifted bey one pixel
this.project(tmpClip, point);
this.unproject(tmpPos1, tmpClip);
tmpClip[0] += 1;
this.unproject(tmpPos2, tmpClip);
return Vec3.distance(tmpPos1, tmpPos2);
};
return Camera;
}());
(function (Camera) {
function ViewOffset() {
return {
enabled: false,
fullWidth: 1, fullHeight: 1,
offsetX: 0, offsetY: 0,
width: 1, height: 1
};
}
Camera.ViewOffset = ViewOffset;
function setViewOffset(out, fullWidth, fullHeight, offsetX, offsetY, width, height) {
out.fullWidth = fullWidth;
out.fullHeight = fullHeight;
out.offsetX = offsetX;
out.offsetY = offsetY;
out.width = width;
out.height = height;
}
Camera.setViewOffset = setViewOffset;
function copyViewOffset(out, view) {
out.enabled = view.enabled;
out.fullWidth = view.fullWidth;
out.fullHeight = view.fullHeight;
out.offsetX = view.offsetX;
out.offsetY = view.offsetY;
out.width = view.width;
out.height = view.height;
}
Camera.copyViewOffset = copyViewOffset;
function targetDistance(radius, fov, width, height) {
var r = Math.max(radius, 0.01);
var aspect = width / height;
var aspectFactor = (height < width ? 1 : aspect);
return Math.abs((r / aspectFactor) / Math.sin(fov / 2));
}
Camera.targetDistance = targetDistance;
function createDefaultSnapshot() {
return {
mode: 'perspective',
fov: Math.PI / 4,
position: Vec3.create(0, 0, 100),
up: Vec3.create(0, 1, 0),
target: Vec3.create(0, 0, 0),
radius: 0,
radiusMax: 10,
fog: 50,
clipFar: true
};
}
Camera.createDefaultSnapshot = createDefaultSnapshot;
function copySnapshot(out, source) {
if (!source)
return out;
if (typeof source.mode !== 'undefined')
out.mode = source.mode;
if (typeof source.fov !== 'undefined')
out.fov = source.fov;
if (typeof source.position !== 'undefined')
Vec3.copy(out.position, source.position);
if (typeof source.up !== 'undefined')
Vec3.copy(out.up, source.up);
if (typeof source.target !== 'undefined')
Vec3.copy(out.target, source.target);
if (typeof source.radius !== 'undefined')
out.radius = source.radius;
if (typeof source.radiusMax !== 'undefined')
out.radiusMax = source.radiusMax;
if (typeof source.fog !== 'undefined')
out.fog = source.fog;
if (typeof source.clipFar !== 'undefined')
out.clipFar = source.clipFar;
return out;
}
Camera.copySnapshot = copySnapshot;
function areSnapshotsEqual(a, b) {
return a.mode === b.mode
&& a.fov === b.fov
&& a.radius === b.radius
&& a.radiusMax === b.radiusMax
&& a.fog === b.fog
&& a.clipFar === b.clipFar
&& Vec3.exactEquals(a.position, b.position)
&& Vec3.exactEquals(a.up, b.up)
&& Vec3.exactEquals(a.target, b.target);
}
Camera.areSnapshotsEqual = areSnapshotsEqual;
})(Camera || (Camera = {}));
function updateOrtho(camera) {
var viewport = camera.viewport, zoom = camera.zoom, near = camera.near, far = camera.far, viewOffset = camera.viewOffset;
var fullLeft = -viewport.width / 2;
var fullRight = viewport.width / 2;
var fullTop = viewport.height / 2;
var fullBottom = -viewport.height / 2;
var dx = (fullRight - fullLeft) / (2 * zoom);
var dy = (fullTop - fullBottom) / (2 * zoom);
var cx = (fullRight + fullLeft) / 2;
var cy = (fullTop + fullBottom) / 2;
var left = cx - dx;
var right = cx + dx;
var top = cy + dy;
var bottom = cy - dy;
if (viewOffset.enabled) {
var zoomW = zoom / (viewOffset.width / viewOffset.fullWidth);
var zoomH = zoom / (viewOffset.height / viewOffset.fullHeight);
var scaleW = (fullRight - fullLeft) / viewOffset.width;
var scaleH = (fullTop - fullBottom) / viewOffset.height;
left += scaleW * (viewOffset.offsetX / zoomW);
right = left + scaleW * (viewOffset.width / zoomW);
top -= scaleH * (viewOffset.offsetY / zoomH);
bottom = top - scaleH * (viewOffset.height / zoomH);
}
// build projection matrix
Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
// build view matrix
Mat4.lookAt(camera.view, camera.position, camera.target, camera.up);
}
function updatePers(camera) {
var aspect = camera.viewport.width / camera.viewport.height;
var near = camera.near, far = camera.far, viewOffset = camera.viewOffset;
var top = near * Math.tan(0.5 * camera.state.fov);
var height = 2 * top;
var width = aspect * height;
var left = -0.5 * width;
if (viewOffset.enabled) {
left += viewOffset.offsetX * width / viewOffset.fullWidth;
top -= viewOffset.offsetY * height / viewOffset.fullHeight;
width *= viewOffset.width / viewOffset.fullWidth;
height *= viewOffset.height / viewOffset.fullHeight;
}
// build projection matrix
Mat4.perspective(camera.projection, left, left + width, top, top - height, near, far);
// build view matrix
Mat4.lookAt(camera.view, camera.position, camera.target, camera.up);
}
function updateClip(camera) {
var _a = camera.state, radius = _a.radius, radiusMax = _a.radiusMax, mode = _a.mode, fog = _a.fog, clipFar = _a.clipFar;
if (radius < 0.01)
radius = 0.01;
var normalizedFar = clipFar ? radius : radiusMax;
var cameraDistance = Vec3.distance(camera.position, camera.target);
var near = cameraDistance - radius;
var far = cameraDistance + normalizedFar;
var fogNearFactor = -(50 - fog) / 50;
var fogNear = cameraDistance - (normalizedFar * fogNearFactor);
var fogFar = far;
if (mode === 'perspective') {
// set at least to 5 to avoid slow sphere impostor rendering
near = Math.max(Math.min(radiusMax, 5), near);
far = Math.max(5, far);
}
else {
// not too close to 0 as it causes issues with outline rendering
near = Math.max(Math.min(radiusMax, 5), near);
far = Math.max(5, far);
}
if (near === far) {
// make sure near and far are not identical to avoid Infinity in the projection matrix
far = near + 0.01;
}
camera.near = near;
camera.far = 2 * far; // avoid precision issues distingushing far objects from background
camera.fogNear = fogNear;
camera.fogFar = fogFar;
}
//# sourceMappingURL=camera.js.map