trackball.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  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/mrdoob/three.js/,
  8. * copyright (c) 2010-2018 three.js authors. MIT License
  9. */
  10. import { Subject } from 'rxjs';
  11. import { Quat, Vec2, Vec3, EPSILON } from 'mol-math/linear-algebra';
  12. import { clamp } from 'mol-math/interpolate';
  13. import InputObserver from 'mol-util/input/input-observer';
  14. import { cameraLookAt } from '../camera/util';
  15. export const DefaultTrackballControlsProps = {
  16. rotateSpeed: 3.0,
  17. zoomSpeed: 2.2,
  18. panSpeed: 0.1,
  19. staticMoving: false,
  20. dynamicDampingFactor: 0.2,
  21. minDistance: 0,
  22. maxDistance: Infinity
  23. }
  24. export type TrackballControlsProps = Partial<typeof DefaultTrackballControlsProps>
  25. const enum STATE {
  26. NONE = - 1,
  27. ROTATE = 0,
  28. ZOOM = 1,
  29. PAN = 2,
  30. TOUCH_ROTATE = 3,
  31. TOUCH_ZOOM_PAN = 4
  32. }
  33. interface Object {
  34. position: Vec3,
  35. direction: Vec3,
  36. up: Vec3,
  37. }
  38. interface Screen {
  39. left: number
  40. top: number
  41. width: number
  42. height: number
  43. }
  44. interface TrackballControls {
  45. change: Subject<void>
  46. start: Subject<void>
  47. end: Subject<void>
  48. dynamicDampingFactor: number
  49. rotateSpeed: number
  50. zoomSpeed: number
  51. panSpeed: number
  52. update: () => void
  53. handleResize: () => void
  54. reset: () => void
  55. dispose: () => void
  56. }
  57. namespace TrackballControls {
  58. export function create (element: Element, object: Object, props: TrackballControlsProps = {}): TrackballControls {
  59. const p = { ...DefaultTrackballControlsProps, ...props }
  60. const screen: Screen = { left: 0, top: 0, width: 0, height: 0 }
  61. let { rotateSpeed, zoomSpeed, panSpeed } = p
  62. let { staticMoving, dynamicDampingFactor } = p
  63. let { minDistance, maxDistance } = p
  64. const change = new Subject<void>()
  65. const start = new Subject<void>()
  66. const end = new Subject<void>()
  67. // internals
  68. const target = Vec3.zero()
  69. const lastPosition = Vec3.zero()
  70. let _state = STATE.NONE
  71. let _prevState = STATE.NONE
  72. const _eye = Vec3.zero()
  73. const _movePrev = Vec2.zero()
  74. const _moveCurr = Vec2.zero()
  75. const _lastAxis = Vec3.zero()
  76. let _lastAngle = 0
  77. const _zoomStart = Vec2.zero()
  78. const _zoomEnd = Vec2.zero()
  79. let _touchZoomDistanceStart = 0
  80. let _touchZoomDistanceEnd = 0
  81. const _panStart = Vec2.zero()
  82. const _panEnd = Vec2.zero()
  83. // for reset
  84. const target0 = Vec3.clone(target)
  85. const position0 = Vec3.clone(object.position)
  86. const up0 = Vec3.clone(object.up)
  87. // methods
  88. function handleResize () {
  89. if ( element instanceof Document ) {
  90. screen.left = 0;
  91. screen.top = 0;
  92. screen.width = window.innerWidth;
  93. screen.height = window.innerHeight;
  94. } else {
  95. const box = element.getBoundingClientRect();
  96. // adjustments come from similar code in the jquery offset() function
  97. const d = element.ownerDocument.documentElement;
  98. screen.left = box.left + window.pageXOffset - d.clientLeft;
  99. screen.top = box.top + window.pageYOffset - d.clientTop;
  100. screen.width = box.width;
  101. screen.height = box.height;
  102. }
  103. }
  104. const mouseOnScreenVec2 = Vec2.zero()
  105. function getMouseOnScreen(pageX: number, pageY: number) {
  106. Vec2.set(
  107. mouseOnScreenVec2,
  108. (pageX - screen.left) / screen.width,
  109. (pageY - screen.top) / screen.height
  110. );
  111. return mouseOnScreenVec2;
  112. }
  113. const mouseOnCircleVec2 = Vec2.zero()
  114. function getMouseOnCircle(pageX: number, pageY: number) {
  115. Vec2.set(
  116. mouseOnCircleVec2,
  117. ((pageX - screen.width * 0.5 - screen.left) / (screen.width * 0.5)),
  118. ((screen.height + 2 * (screen.top - pageY)) / screen.width) // screen.width intentional
  119. );
  120. return mouseOnCircleVec2;
  121. }
  122. const rotAxis = Vec3.zero()
  123. const rotQuat = Quat.zero()
  124. const rotEyeDir = Vec3.zero()
  125. const rotObjUpDir = Vec3.zero()
  126. const rotObjSideDir = Vec3.zero()
  127. const rotMoveDir = Vec3.zero()
  128. function rotateCamera() {
  129. Vec3.set(rotMoveDir, _moveCurr[0] - _movePrev[0], _moveCurr[1] - _movePrev[1], 0);
  130. let angle = Vec3.magnitude(rotMoveDir);
  131. if (angle) {
  132. Vec3.copy(_eye, object.position)
  133. Vec3.sub(_eye, _eye, target)
  134. Vec3.normalize(rotEyeDir, Vec3.copy(rotEyeDir, _eye))
  135. Vec3.normalize(rotObjUpDir, Vec3.copy(rotObjUpDir, object.up))
  136. Vec3.normalize(rotObjSideDir, Vec3.cross(rotObjSideDir, rotObjUpDir, rotEyeDir))
  137. Vec3.setMagnitude(rotObjUpDir, rotObjUpDir, _moveCurr[1] - _movePrev[1])
  138. Vec3.setMagnitude(rotObjSideDir, rotObjSideDir, _moveCurr[0] - _movePrev[0])
  139. Vec3.add(rotMoveDir, Vec3.copy(rotMoveDir, rotObjUpDir), rotObjSideDir)
  140. Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye))
  141. angle *= rotateSpeed;
  142. Quat.setAxisAngle(rotQuat, rotAxis, angle )
  143. Vec3.transformQuat(_eye, _eye, rotQuat)
  144. Vec3.transformQuat(object.up, object.up, rotQuat)
  145. Vec3.copy(_lastAxis, rotAxis)
  146. _lastAngle = angle;
  147. } else if (!staticMoving && _lastAngle) {
  148. _lastAngle *= Math.sqrt(1.0 - dynamicDampingFactor);
  149. Vec3.sub(_eye, Vec3.copy(_eye, object.position), target)
  150. Quat.setAxisAngle(rotQuat, _lastAxis, _lastAngle)
  151. Vec3.transformQuat(_eye, _eye, rotQuat)
  152. Vec3.transformQuat(object.up, object.up, rotQuat)
  153. }
  154. Vec2.copy(_movePrev, _moveCurr)
  155. }
  156. function zoomCamera () {
  157. if (_state === STATE.TOUCH_ZOOM_PAN) {
  158. const factor = _touchZoomDistanceStart / _touchZoomDistanceEnd
  159. _touchZoomDistanceStart = _touchZoomDistanceEnd;
  160. Vec3.scale(_eye, _eye, factor)
  161. } else {
  162. const factor = 1.0 + ( _zoomEnd[1] - _zoomStart[1] ) * zoomSpeed
  163. if (factor !== 1.0 && factor > 0.0) {
  164. Vec3.scale(_eye, _eye, factor)
  165. }
  166. if (staticMoving) {
  167. Vec2.copy(_zoomStart, _zoomEnd)
  168. } else {
  169. _zoomStart[1] += ( _zoomEnd[1] - _zoomStart[1] ) * dynamicDampingFactor
  170. }
  171. }
  172. }
  173. const panMouseChange = Vec2.zero()
  174. const panObjUp = Vec3.zero()
  175. const panOffset = Vec3.zero()
  176. function panCamera() {
  177. Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, _panEnd), _panStart)
  178. if (Vec2.squaredMagnitude(panMouseChange)) {
  179. Vec2.scale(panMouseChange, panMouseChange, Vec3.magnitude(_eye) * panSpeed)
  180. Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), object.up)
  181. Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0])
  182. Vec3.setMagnitude(panObjUp, object.up, panMouseChange[1])
  183. Vec3.add(panOffset, panOffset, panObjUp)
  184. Vec3.add(object.position, object.position, panOffset)
  185. Vec3.add(target, target, panOffset)
  186. if (staticMoving) {
  187. Vec2.copy(_panStart, _panEnd)
  188. } else {
  189. Vec2.sub(panMouseChange, _panEnd, _panStart)
  190. Vec2.scale(panMouseChange, panMouseChange, dynamicDampingFactor)
  191. Vec2.add(_panStart, _panStart, panMouseChange)
  192. }
  193. }
  194. }
  195. function checkDistances() {
  196. if (Vec3.squaredMagnitude(_eye) > maxDistance * maxDistance) {
  197. Vec3.setMagnitude(_eye, _eye, maxDistance)
  198. Vec3.add(object.position, target, _eye)
  199. Vec2.copy(_zoomStart, _zoomEnd)
  200. }
  201. if (Vec3.squaredMagnitude(_eye) < minDistance * minDistance) {
  202. Vec3.setMagnitude(_eye, _eye, minDistance)
  203. Vec3.add(object.position, target, _eye)
  204. Vec2.copy(_zoomStart, _zoomEnd)
  205. }
  206. }
  207. function update() {
  208. Vec3.sub( _eye, object.position, target)
  209. rotateCamera()
  210. zoomCamera()
  211. panCamera()
  212. Vec3.add(object.position, target, _eye)
  213. checkDistances()
  214. cameraLookAt(object.position, object.up, object.direction, target)
  215. if (Vec3.squaredDistance(lastPosition, object.position) > EPSILON.Value) {
  216. change.next()
  217. Vec3.copy(lastPosition, object.position)
  218. }
  219. }
  220. function reset() {
  221. _state = STATE.NONE;
  222. _prevState = STATE.NONE;
  223. Vec3.copy(target, target0)
  224. Vec3.copy(object.position, position0)
  225. Vec3.copy(object.up, up0)
  226. Vec3.sub(_eye, object.position, target)
  227. cameraLookAt(object.position, object.up, object.direction, target)
  228. change.next()
  229. Vec3.copy(lastPosition, object.position)
  230. }
  231. // listeners
  232. function mousedown(event: MouseEvent) {
  233. event.preventDefault();
  234. event.stopPropagation();
  235. if (_state === STATE.NONE) {
  236. _state = event.button;
  237. }
  238. if (_state === STATE.ROTATE) {
  239. Vec2.copy(_moveCurr, getMouseOnCircle(event.pageX, event.pageY))
  240. Vec2.copy(_movePrev, _moveCurr)
  241. } else if (_state === STATE.ZOOM) {
  242. Vec2.copy(_zoomStart, getMouseOnScreen(event.pageX, event.pageY))
  243. Vec2.copy(_zoomEnd, _zoomStart)
  244. } else if (_state === STATE.PAN) {
  245. Vec2.copy(_panStart, getMouseOnScreen(event.pageX, event.pageY))
  246. Vec2.copy(_panEnd, _panStart)
  247. }
  248. document.addEventListener('mousemove', mousemove, false);
  249. document.addEventListener('mouseup', mouseup, false);
  250. start.next()
  251. }
  252. function mousemove(event: MouseEvent) {
  253. event.preventDefault();
  254. event.stopPropagation();
  255. if (_state === STATE.ROTATE) {
  256. Vec2.copy(_movePrev, _moveCurr)
  257. Vec2.copy(_moveCurr, getMouseOnCircle(event.pageX, event.pageY))
  258. } else if (_state === STATE.ZOOM) {
  259. Vec2.copy(_zoomEnd, getMouseOnScreen(event.pageX, event.pageY))
  260. } else if (_state === STATE.PAN) {
  261. Vec2.copy(_panEnd, getMouseOnScreen(event.pageX, event.pageY))
  262. }
  263. }
  264. function mouseup(event: MouseEvent) {
  265. event.preventDefault();
  266. event.stopPropagation();
  267. _state = STATE.NONE;
  268. document.removeEventListener( 'mousemove', mousemove );
  269. document.removeEventListener( 'mouseup', mouseup );
  270. end.next()
  271. }
  272. function mousewheel( event: MouseWheelEvent ) {
  273. event.preventDefault();
  274. event.stopPropagation();
  275. switch ( event.deltaMode ) {
  276. case 2:
  277. // Zoom in pages
  278. _zoomStart[1] -= event.deltaY * 0.025;
  279. break;
  280. case 1:
  281. // Zoom in lines
  282. _zoomStart[1] -= event.deltaY * 0.01;
  283. break;
  284. default:
  285. // undefined, 0, assume pixels
  286. _zoomStart[1] -= event.deltaY * 0.00025;
  287. break;
  288. }
  289. start.next()
  290. end.next()
  291. }
  292. function touchstart(event: TouchEvent ) {
  293. switch ( event.touches.length ) {
  294. case 1:
  295. _state = STATE.TOUCH_ROTATE;
  296. Vec2.copy(_moveCurr, getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY))
  297. Vec2.copy(_movePrev, _moveCurr)
  298. break;
  299. default: // 2 or more
  300. _state = STATE.TOUCH_ZOOM_PAN;
  301. const dx = event.touches[0].pageX - event.touches[1].pageX;
  302. const dy = event.touches[0].pageY - event.touches[1].pageY;
  303. _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy);
  304. const x = ( event.touches[0].pageX + event.touches[1].pageX) / 2;
  305. const y = ( event.touches[0].pageY + event.touches[1].pageY) / 2;
  306. Vec2.copy(_panStart, getMouseOnScreen(x, y))
  307. Vec2.copy(_panEnd, _panStart)
  308. break;
  309. }
  310. start.next()
  311. }
  312. function touchmove(event: TouchEvent) {
  313. event.preventDefault();
  314. event.stopPropagation();
  315. switch ( event.touches.length ) {
  316. case 1:
  317. Vec2.copy(_movePrev, _moveCurr)
  318. Vec2.copy(_moveCurr, getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY))
  319. break;
  320. default: // 2 or more
  321. const dx = event.touches[0].pageX - event.touches[1].pageX;
  322. const dy = event.touches[0].pageY - event.touches[1].pageY;
  323. _touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy);
  324. const x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
  325. const y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
  326. Vec2.copy(_panEnd, getMouseOnScreen(x, y))
  327. break;
  328. }
  329. }
  330. function touchend(event: TouchEvent) {
  331. switch ( event.touches.length ) {
  332. case 0:
  333. _state = STATE.NONE;
  334. break;
  335. case 1:
  336. _state = STATE.TOUCH_ROTATE;
  337. Vec2.copy(_moveCurr, getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY))
  338. Vec2.copy(_movePrev, _moveCurr)
  339. break;
  340. }
  341. end.next()
  342. }
  343. function contextmenu(event: Event) {
  344. event.preventDefault();
  345. }
  346. function dispose() {
  347. element.removeEventListener( 'contextmenu', contextmenu, false );
  348. element.removeEventListener( 'mousedown', mousedown as any, false );
  349. element.removeEventListener( 'wheel', mousewheel, false );
  350. element.removeEventListener( 'touchstart', touchstart as any, false );
  351. element.removeEventListener( 'touchend', touchend as any, false );
  352. element.removeEventListener( 'touchmove', touchmove as any, false );
  353. document.removeEventListener( 'mousemove', mousemove, false );
  354. document.removeEventListener( 'mouseup', mouseup, false );
  355. }
  356. element.addEventListener( 'contextmenu', contextmenu, false );
  357. element.addEventListener( 'mousedown', mousedown as any, false );
  358. element.addEventListener( 'wheel', mousewheel, false );
  359. element.addEventListener( 'touchstart', touchstart as any, false );
  360. element.addEventListener( 'touchend', touchend as any, false );
  361. element.addEventListener( 'touchmove', touchmove as any, false );
  362. handleResize();
  363. // force an update at start
  364. update();
  365. return {
  366. change,
  367. start,
  368. end,
  369. get dynamicDampingFactor() { return dynamicDampingFactor },
  370. set dynamicDampingFactor(value: number ) { dynamicDampingFactor = value },
  371. get rotateSpeed() { return rotateSpeed },
  372. set rotateSpeed(value: number ) { rotateSpeed = value },
  373. get zoomSpeed() { return zoomSpeed },
  374. set zoomSpeed(value: number ) { zoomSpeed = value },
  375. get panSpeed() { return panSpeed },
  376. set panSpeed(value: number ) { panSpeed = value },
  377. update,
  378. handleResize,
  379. reset,
  380. dispose
  381. }
  382. }
  383. }
  384. export default TrackballControls