camera.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. /*
  7. * This code has been modified from https://github.com/regl-project/regl-camera,
  8. * copyright (c) 2016 Mikola Lysenko. MIT License
  9. */
  10. const isBrowser = typeof window !== 'undefined'
  11. import REGL = require('regl');
  12. import mouseChange, { MouseModifiers } from 'mol-util/mouse-change'
  13. import mouseWheel from 'mol-util/mouse-wheel'
  14. import { defaults } from 'mol-util'
  15. import { Mat4, Vec3 } from 'mol-math/linear-algebra/3d'
  16. import { clamp, damp } from 'mol-math/interpolate'
  17. export interface CameraUniforms {
  18. projection: Mat4,
  19. }
  20. export interface CameraState {
  21. center: Vec3,
  22. theta: number,
  23. phi: number,
  24. distance: number,
  25. eye: Vec3,
  26. up: Vec3,
  27. fovy: number,
  28. near: number,
  29. far: number,
  30. noScroll: boolean,
  31. flipY: boolean,
  32. dtheta: number,
  33. dphi: number,
  34. rotationSpeed: number,
  35. zoomSpeed: number,
  36. renderOnDirty: boolean,
  37. damping: number,
  38. minDistance: number,
  39. maxDistance: number,
  40. }
  41. export interface Camera {
  42. update: (props: any, block: any) => void,
  43. setState: (newState: CameraState) => void,
  44. isDirty: () => boolean
  45. }
  46. export namespace Camera {
  47. export function create (regl: REGL.Regl, element: HTMLElement, props: Partial<CameraState> = {}): Camera {
  48. const state: CameraState = {
  49. center: defaults(props.center, Vec3.zero()),
  50. theta: defaults(props.theta, 0),
  51. phi: defaults(props.phi, 0),
  52. distance: Math.log(defaults(props.distance, 10.0)),
  53. eye: Vec3.zero(),
  54. up: defaults(props.up, Vec3.create(0, 1, 0)),
  55. fovy: defaults(props.fovy, Math.PI / 4.0),
  56. near: defaults(props.near, 0.01),
  57. far: defaults(props.far, 1000.0),
  58. noScroll: defaults(props.noScroll, false),
  59. flipY: defaults(props.flipY, false),
  60. dtheta: 0,
  61. dphi: 0,
  62. rotationSpeed: defaults(props.rotationSpeed, 1),
  63. zoomSpeed: defaults(props.zoomSpeed, 1),
  64. renderOnDirty: defaults(props.renderOnDirty, false),
  65. damping: defaults(props.damping, 0.9),
  66. minDistance: Math.log(defaults(props.minDistance, 0.1)),
  67. maxDistance: Math.log(defaults(props.maxDistance, 1000))
  68. }
  69. const view = Mat4.identity()
  70. const projection = Mat4.identity()
  71. const right = Vec3.create(1, 0, 0)
  72. const front = Vec3.create(0, 0, 1)
  73. let dirty = false
  74. let ddistance = 0
  75. let prevX = 0
  76. let prevY = 0
  77. if (isBrowser) {
  78. const source = element || regl._gl.canvas
  79. const getWidth = function () {
  80. return element ? element.offsetWidth : window.innerWidth
  81. }
  82. const getHeight = function () {
  83. return element ? element.offsetHeight : window.innerHeight
  84. }
  85. mouseChange(source, function (buttons: number, x: number, y: number, mods: MouseModifiers) {
  86. if (buttons & 1) {
  87. const dx = (x - prevX) / getWidth()
  88. const dy = (y - prevY) / getHeight()
  89. state.dtheta += state.rotationSpeed * 4.0 * dx
  90. state.dphi += state.rotationSpeed * 4.0 * dy
  91. dirty = true;
  92. }
  93. prevX = x
  94. prevY = y
  95. })
  96. mouseWheel(source, function (dx: number, dy: number) {
  97. ddistance += dy / getHeight() * state.zoomSpeed
  98. dirty = true;
  99. }, state.noScroll)
  100. }
  101. function dampAndMarkDirty (x: number) {
  102. const xd = damp(x, state.damping)
  103. if (Math.abs(xd) < 0.1) return 0
  104. dirty = true;
  105. return xd
  106. }
  107. function setState (newState: Partial<CameraState> = {}) {
  108. Object.assign(state, newState)
  109. const { center, eye, up, dtheta, dphi } = state
  110. state.theta += dtheta
  111. state.phi = clamp(state.phi + dphi, -Math.PI / 2.0, Math.PI / 2.0)
  112. state.distance = clamp(state.distance + ddistance, state.minDistance, state.maxDistance)
  113. state.dtheta = dampAndMarkDirty(dtheta)
  114. state.dphi = dampAndMarkDirty(dphi)
  115. ddistance = dampAndMarkDirty(ddistance)
  116. const theta = state.theta
  117. const phi = state.phi
  118. const r = Math.exp(state.distance)
  119. const vf = r * Math.sin(theta) * Math.cos(phi)
  120. const vr = r * Math.cos(theta) * Math.cos(phi)
  121. const vu = r * Math.sin(phi)
  122. for (let i = 0; i < 3; ++i) {
  123. eye[i] = center[i] + vf * front[i] + vr * right[i] + vu * up[i]
  124. }
  125. Mat4.lookAt(view, eye, center, up)
  126. }
  127. const injectContext = regl({
  128. context: {
  129. view: () => view,
  130. dirty: () => dirty,
  131. projection: (context: REGL.DefaultContext) => {
  132. Mat4.perspective(
  133. projection,
  134. state.fovy,
  135. context.viewportWidth / context.viewportHeight,
  136. state.near,
  137. state.far
  138. )
  139. if (state.flipY) { projection[5] *= -1 }
  140. return projection
  141. }
  142. },
  143. uniforms: { // TODO
  144. view: regl.context('view' as any),
  145. projection: regl.context('projection' as any)
  146. }
  147. })
  148. function update (props: any, block: any) {
  149. setState()
  150. injectContext(props, block)
  151. dirty = false
  152. }
  153. return {
  154. update,
  155. setState,
  156. isDirty: () => dirty
  157. }
  158. }
  159. }