trackball.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. /**
  2. * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author David Sehnal <david.sehnal@gmail.com>
  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 { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
  11. import { Viewport } from '../camera/util';
  12. import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput } from '../../mol-util/input/input-observer';
  13. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  14. import { Camera } from '../camera';
  15. import { absMax, degToRad } from '../../mol-math/misc';
  16. import { Binding } from '../../mol-util/binding';
  17. import { smoothstep } from '../../mol-math/interpolate';
  18. const B = ButtonsType;
  19. const M = ModifiersKeys;
  20. const Trigger = Binding.Trigger;
  21. export const DefaultTrackballBindings = {
  22. dragRotate: Binding([Trigger(B.Flag.Primary, M.create())], 'Rotate', 'Drag using ${triggers}'),
  23. dragRotateZ: Binding([Trigger(B.Flag.Primary, M.create({ shift: true }))], 'Rotate around z-axis', 'Drag using ${triggers}'),
  24. dragPan: Binding([Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true }))], 'Pan', 'Drag using ${triggers}'),
  25. dragZoom: Binding.Empty,
  26. dragFocus: Binding([Trigger(B.Flag.Forth, M.create())], 'Focus', 'Drag using ${triggers}'),
  27. dragFocusZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Focus and zoom', 'Drag using ${triggers}'),
  28. scrollZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Zoom', 'Scroll using ${triggers}'),
  29. scrollFocus: Binding([Trigger(B.Flag.Auxilary, M.create({ shift: true }))], 'Clip', 'Scroll using ${triggers}'),
  30. scrollFocusZoom: Binding.Empty,
  31. };
  32. export const TrackballControlsParams = {
  33. noScroll: PD.Boolean(true, { isHidden: true }),
  34. rotateSpeed: PD.Numeric(5.0, { min: 1, max: 10, step: 1 }),
  35. zoomSpeed: PD.Numeric(7.0, { min: 1, max: 15, step: 1 }),
  36. panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
  37. animate: PD.MappedStatic('off', {
  38. off: PD.EmptyGroup(),
  39. spin: PD.Group({
  40. speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
  41. }, { description: 'Spin the 3D scene around the x-axis in view space' }),
  42. rock: PD.Group({
  43. speed: PD.Numeric(2, { min: -20, max: 20, step: 1 }),
  44. angle: PD.Numeric(10, { min: 0, max: 90, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
  45. }, { description: 'Rock the 3D scene around the x-axis in view space' })
  46. }),
  47. staticMoving: PD.Boolean(true, { isHidden: true }),
  48. dynamicDampingFactor: PD.Numeric(0.2, {}, { isHidden: true }),
  49. minDistance: PD.Numeric(0.01, {}, { isHidden: true }),
  50. maxDistance: PD.Numeric(1e150, {}, { isHidden: true }),
  51. gestureScaleFactor: PD.Numeric(1, {}, { isHidden: true }),
  52. maxWheelDelta: PD.Numeric(0.02, {}, { isHidden: true }),
  53. bindings: PD.Value(DefaultTrackballBindings, { isHidden: true }),
  54. /**
  55. * minDistance = minDistanceFactor * boundingSphere.radius + minDistancePadding
  56. * maxDistance = max(maxDistanceFactor * boundingSphere.radius, maxDistanceMin)
  57. */
  58. autoAdjustMinMaxDistance: PD.MappedStatic('on', {
  59. off: PD.EmptyGroup(),
  60. on: PD.Group({
  61. minDistanceFactor: PD.Numeric(0),
  62. minDistancePadding: PD.Numeric(5),
  63. maxDistanceFactor: PD.Numeric(10),
  64. maxDistanceMin: PD.Numeric(20)
  65. })
  66. }, { isHidden: true })
  67. };
  68. export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
  69. export { TrackballControls };
  70. interface TrackballControls {
  71. readonly viewport: Viewport
  72. readonly isAnimating: boolean
  73. readonly props: Readonly<TrackballControlsProps>
  74. setProps: (props: Partial<TrackballControlsProps>) => void
  75. start: (t: number) => void
  76. update: (t: number) => void
  77. reset: () => void
  78. dispose: () => void
  79. }
  80. namespace TrackballControls {
  81. export function create(input: InputObserver, camera: Camera, props: Partial<TrackballControlsProps> = {}): TrackballControls {
  82. const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props };
  83. const viewport = Viewport.clone(camera.viewport);
  84. let disposed = false;
  85. const dragSub = input.drag.subscribe(onDrag);
  86. const interactionEndSub = input.interactionEnd.subscribe(onInteractionEnd);
  87. const wheelSub = input.wheel.subscribe(onWheel);
  88. const pinchSub = input.pinch.subscribe(onPinch);
  89. const gestureSub = input.gesture.subscribe(onGesture);
  90. let _isInteracting = false;
  91. // For internal use
  92. const lastPosition = Vec3();
  93. const _eye = Vec3();
  94. const _rotPrev = Vec2();
  95. const _rotCurr = Vec2();
  96. const _rotLastAxis = Vec3();
  97. let _rotLastAngle = 0;
  98. const _zRotPrev = Vec2();
  99. const _zRotCurr = Vec2();
  100. let _zRotLastAngle = 0;
  101. const _zoomStart = Vec2();
  102. const _zoomEnd = Vec2();
  103. const _focusStart = Vec2();
  104. const _focusEnd = Vec2();
  105. const _panStart = Vec2();
  106. const _panEnd = Vec2();
  107. // Initial values for reseting
  108. const target0 = Vec3.clone(camera.target);
  109. const position0 = Vec3.clone(camera.position);
  110. const up0 = Vec3.clone(camera.up);
  111. const mouseOnScreenVec2 = Vec2();
  112. function getMouseOnScreen(pageX: number, pageY: number) {
  113. return Vec2.set(
  114. mouseOnScreenVec2,
  115. (pageX - viewport.x) / viewport.width,
  116. (pageY - viewport.y) / viewport.height
  117. );
  118. }
  119. const mouseOnCircleVec2 = Vec2();
  120. function getMouseOnCircle(pageX: number, pageY: number) {
  121. return Vec2.set(
  122. mouseOnCircleVec2,
  123. (pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5),
  124. (viewport.height + 2 * (viewport.y - pageY)) / viewport.width // screen.width intentional
  125. );
  126. }
  127. function getRotateFactor() {
  128. const aspectRatio = input.width / input.height;
  129. return p.rotateSpeed * input.pixelRatio * aspectRatio;
  130. }
  131. const rotAxis = Vec3();
  132. const rotQuat = Quat();
  133. const rotEyeDir = Vec3();
  134. const rotObjUpDir = Vec3();
  135. const rotObjSideDir = Vec3();
  136. const rotMoveDir = Vec3();
  137. function rotateCamera() {
  138. const dx = _rotCurr[0] - _rotPrev[0];
  139. const dy = _rotCurr[1] - _rotPrev[1];
  140. Vec3.set(rotMoveDir, dx, dy, 0);
  141. const angle = Vec3.magnitude(rotMoveDir) * getRotateFactor();
  142. if (angle) {
  143. Vec3.sub(_eye, camera.position, camera.target);
  144. Vec3.normalize(rotEyeDir, _eye);
  145. Vec3.normalize(rotObjUpDir, camera.up);
  146. Vec3.normalize(rotObjSideDir, Vec3.cross(rotObjSideDir, rotObjUpDir, rotEyeDir));
  147. Vec3.setMagnitude(rotObjUpDir, rotObjUpDir, dy);
  148. Vec3.setMagnitude(rotObjSideDir, rotObjSideDir, dx);
  149. Vec3.add(rotMoveDir, rotObjUpDir, rotObjSideDir);
  150. Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye));
  151. Quat.setAxisAngle(rotQuat, rotAxis, angle);
  152. Vec3.transformQuat(_eye, _eye, rotQuat);
  153. Vec3.transformQuat(camera.up, camera.up, rotQuat);
  154. Vec3.copy(_rotLastAxis, rotAxis);
  155. _rotLastAngle = angle;
  156. } else if (!p.staticMoving && _rotLastAngle) {
  157. _rotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
  158. Vec3.sub(_eye, camera.position, camera.target);
  159. Quat.setAxisAngle(rotQuat, _rotLastAxis, _rotLastAngle);
  160. Vec3.transformQuat(_eye, _eye, rotQuat);
  161. Vec3.transformQuat(camera.up, camera.up, rotQuat);
  162. }
  163. Vec2.copy(_rotPrev, _rotCurr);
  164. }
  165. const zRotQuat = Quat();
  166. function zRotateCamera() {
  167. const dx = _zRotCurr[0] - _zRotPrev[0];
  168. const dy = _zRotCurr[1] - _zRotPrev[1];
  169. const angle = p.rotateSpeed * (-dx + dy) * -0.05;
  170. if (angle) {
  171. Vec3.sub(_eye, camera.position, camera.target);
  172. Quat.setAxisAngle(zRotQuat, _eye, angle);
  173. Vec3.transformQuat(camera.up, camera.up, zRotQuat);
  174. _zRotLastAngle = angle;
  175. } else if (!p.staticMoving && _zRotLastAngle) {
  176. _zRotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
  177. Vec3.sub(_eye, camera.position, camera.target);
  178. Quat.setAxisAngle(zRotQuat, _eye, _zRotLastAngle);
  179. Vec3.transformQuat(camera.up, camera.up, zRotQuat);
  180. }
  181. Vec2.copy(_zRotPrev, _zRotCurr);
  182. }
  183. function zoomCamera() {
  184. const factor = 1.0 + (_zoomEnd[1] - _zoomStart[1]) * p.zoomSpeed;
  185. if (factor !== 1.0 && factor > 0.0) {
  186. Vec3.scale(_eye, _eye, factor);
  187. }
  188. if (p.staticMoving) {
  189. Vec2.copy(_zoomStart, _zoomEnd);
  190. } else {
  191. _zoomStart[1] += (_zoomEnd[1] - _zoomStart[1]) * p.dynamicDampingFactor;
  192. }
  193. }
  194. function focusCamera() {
  195. const factor = (_focusEnd[1] - _focusStart[1]) * p.zoomSpeed;
  196. if (factor !== 0.0) {
  197. const radius = Math.max(1, camera.state.radius + camera.state.radius * factor);
  198. camera.setState({ radius });
  199. }
  200. if (p.staticMoving) {
  201. Vec2.copy(_focusStart, _focusEnd);
  202. } else {
  203. _focusStart[1] += (_focusEnd[1] - _focusStart[1]) * p.dynamicDampingFactor;
  204. }
  205. }
  206. const panMouseChange = Vec2();
  207. const panObjUp = Vec3();
  208. const panOffset = Vec3();
  209. function panCamera() {
  210. Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, _panEnd), _panStart);
  211. if (Vec2.squaredMagnitude(panMouseChange)) {
  212. const factor = input.pixelRatio * p.panSpeed;
  213. panMouseChange[0] *= (1 / camera.zoom) * camera.viewport.width * factor;
  214. panMouseChange[1] *= (1 / camera.zoom) * camera.viewport.height * factor;
  215. Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), camera.up);
  216. Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0]);
  217. Vec3.setMagnitude(panObjUp, camera.up, panMouseChange[1]);
  218. Vec3.add(panOffset, panOffset, panObjUp);
  219. Vec3.add(camera.position, camera.position, panOffset);
  220. Vec3.add(camera.target, camera.target, panOffset);
  221. if (p.staticMoving) {
  222. Vec2.copy(_panStart, _panEnd);
  223. } else {
  224. Vec2.sub(panMouseChange, _panEnd, _panStart);
  225. Vec2.scale(panMouseChange, panMouseChange, p.dynamicDampingFactor);
  226. Vec2.add(_panStart, _panStart, panMouseChange);
  227. }
  228. }
  229. }
  230. /**
  231. * Ensure the distance between object and target is within the min/max distance
  232. * and not too large compared to `camera.state.radiusMax`
  233. */
  234. function checkDistances() {
  235. const maxDistance = Math.min(Math.max(camera.state.radiusMax * 1000, 0.01), p.maxDistance);
  236. if (Vec3.squaredMagnitude(_eye) > maxDistance * maxDistance) {
  237. Vec3.setMagnitude(_eye, _eye, maxDistance);
  238. Vec3.add(camera.position, camera.target, _eye);
  239. Vec2.copy(_zoomStart, _zoomEnd);
  240. Vec2.copy(_focusStart, _focusEnd);
  241. }
  242. if (Vec3.squaredMagnitude(_eye) < p.minDistance * p.minDistance) {
  243. Vec3.setMagnitude(_eye, _eye, p.minDistance);
  244. Vec3.add(camera.position, camera.target, _eye);
  245. Vec2.copy(_zoomStart, _zoomEnd);
  246. Vec2.copy(_focusStart, _focusEnd);
  247. }
  248. }
  249. function outsideViewport(x: number, y: number) {
  250. x *= input.pixelRatio;
  251. y *= input.pixelRatio;
  252. return (
  253. x > viewport.x + viewport.width ||
  254. input.height - y > viewport.y + viewport.height ||
  255. x < viewport.x ||
  256. input.height - y < viewport.y
  257. );
  258. }
  259. let lastUpdated = -1;
  260. /** Update the object's position, direction and up vectors */
  261. function update(t: number) {
  262. if (lastUpdated === t) return;
  263. if (lastUpdated > 0) {
  264. if (p.animate.name === 'spin') spin(t - lastUpdated);
  265. else if (p.animate.name === 'rock') rock(t - lastUpdated);
  266. }
  267. Vec3.sub(_eye, camera.position, camera.target);
  268. rotateCamera();
  269. zRotateCamera();
  270. zoomCamera();
  271. focusCamera();
  272. panCamera();
  273. Vec3.add(camera.position, camera.target, _eye);
  274. checkDistances();
  275. if (Vec3.squaredDistance(lastPosition, camera.position) > EPSILON) {
  276. Vec3.copy(lastPosition, camera.position);
  277. }
  278. lastUpdated = t;
  279. }
  280. /** Reset object's vectors and the target vector to their initial values */
  281. function reset() {
  282. Vec3.copy(camera.target, target0);
  283. Vec3.copy(camera.position, position0);
  284. Vec3.copy(camera.up, up0);
  285. Vec3.sub(_eye, camera.position, camera.target);
  286. Vec3.copy(lastPosition, camera.position);
  287. }
  288. // listeners
  289. function onDrag({ x, y, pageX, pageY, buttons, modifiers, isStart }: DragInput) {
  290. const isOutside = outsideViewport(x, y);
  291. if (isStart && isOutside) return;
  292. if (!isStart && !_isInteracting) return;
  293. _isInteracting = true;
  294. resetRock(); // start rocking from the center after interactions
  295. const dragRotate = Binding.match(p.bindings.dragRotate, buttons, modifiers);
  296. const dragRotateZ = Binding.match(p.bindings.dragRotateZ, buttons, modifiers);
  297. const dragPan = Binding.match(p.bindings.dragPan, buttons, modifiers);
  298. const dragZoom = Binding.match(p.bindings.dragZoom, buttons, modifiers);
  299. const dragFocus = Binding.match(p.bindings.dragFocus, buttons, modifiers);
  300. const dragFocusZoom = Binding.match(p.bindings.dragFocusZoom, buttons, modifiers);
  301. getMouseOnCircle(pageX, pageY);
  302. getMouseOnScreen(pageX, pageY);
  303. if (isStart) {
  304. if (dragRotate) {
  305. Vec2.copy(_rotCurr, mouseOnCircleVec2);
  306. Vec2.copy(_rotPrev, _rotCurr);
  307. }
  308. if (dragRotateZ) {
  309. Vec2.copy(_zRotCurr, mouseOnCircleVec2);
  310. Vec2.copy(_zRotPrev, _zRotCurr);
  311. }
  312. if (dragZoom || dragFocusZoom) {
  313. Vec2.copy(_zoomStart, mouseOnScreenVec2);
  314. Vec2.copy(_zoomEnd, _zoomStart);
  315. }
  316. if (dragFocus) {
  317. Vec2.copy(_focusStart, mouseOnScreenVec2);
  318. Vec2.copy(_focusEnd, _focusStart);
  319. }
  320. if (dragPan) {
  321. Vec2.copy(_panStart, mouseOnScreenVec2);
  322. Vec2.copy(_panEnd, _panStart);
  323. }
  324. }
  325. if (dragRotate) Vec2.copy(_rotCurr, mouseOnCircleVec2);
  326. if (dragRotateZ) Vec2.copy(_zRotCurr, mouseOnCircleVec2);
  327. if (dragZoom || dragFocusZoom) Vec2.copy(_zoomEnd, mouseOnScreenVec2);
  328. if (dragFocus) Vec2.copy(_focusEnd, mouseOnScreenVec2);
  329. if (dragFocusZoom) {
  330. const dist = Vec3.distance(camera.state.position, camera.state.target);
  331. camera.setState({ radius: dist / 5 });
  332. }
  333. if (dragPan) Vec2.copy(_panEnd, mouseOnScreenVec2);
  334. }
  335. function onInteractionEnd() {
  336. _isInteracting = false;
  337. }
  338. function onWheel({ x, y, spinX, spinY, dz, buttons, modifiers }: WheelInput) {
  339. if (outsideViewport(x, y)) return;
  340. let delta = absMax(spinX * 0.075, spinY * 0.075, dz * 0.0001);
  341. if (delta < -p.maxWheelDelta) delta = -p.maxWheelDelta;
  342. else if (delta > p.maxWheelDelta) delta = p.maxWheelDelta;
  343. if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
  344. _zoomEnd[1] += delta;
  345. }
  346. if (Binding.match(p.bindings.scrollFocus, buttons, modifiers)) {
  347. _focusEnd[1] += delta;
  348. }
  349. }
  350. function onPinch({ fractionDelta, buttons, modifiers }: PinchInput) {
  351. if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
  352. _isInteracting = true;
  353. _zoomEnd[1] += p.gestureScaleFactor * fractionDelta;
  354. }
  355. }
  356. function onGesture({ deltaScale }: GestureInput) {
  357. _isInteracting = true;
  358. _zoomEnd[1] += p.gestureScaleFactor * deltaScale;
  359. }
  360. function dispose() {
  361. if (disposed) return;
  362. disposed = true;
  363. dragSub.unsubscribe();
  364. wheelSub.unsubscribe();
  365. pinchSub.unsubscribe();
  366. gestureSub.unsubscribe();
  367. interactionEndSub.unsubscribe();
  368. }
  369. const _spinSpeed = Vec2.create(0.005, 0);
  370. function spin(deltaT: number) {
  371. if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
  372. const frameSpeed = p.animate.params.speed / 1000;
  373. _spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
  374. Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
  375. }
  376. let _rockAngleSum = 0;
  377. let _rockDirection = 1;
  378. const _rockSpeed = Vec2.create(0.005, 0);
  379. function rock(deltaT: number) {
  380. if (p.animate.name !== 'rock' || p.animate.params.speed === 0 || _isInteracting) return;
  381. // TODO get rid of the 3.3 factor (compensates for using `smoothstep`)
  382. const maxAngle = degToRad(p.animate.params.angle * 3.3) / getRotateFactor();
  383. const alpha = smoothstep(0, 1, Math.abs(_rockAngleSum) / maxAngle);
  384. const frameSpeed = p.animate.params.speed / 1000;
  385. _rockSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
  386. _rockAngleSum += Math.abs(_rockSpeed[0]);
  387. _rockSpeed[0] *= _rockDirection * (1.1 - alpha);
  388. Vec2.add(_rotCurr, _rotPrev, _rockSpeed);
  389. if (_rockAngleSum >= maxAngle) {
  390. _rockDirection *= -1;
  391. _rockAngleSum = -maxAngle;
  392. }
  393. }
  394. function resetRock() {
  395. _rockAngleSum = 0;
  396. _rockDirection = 1;
  397. }
  398. function start(t: number) {
  399. lastUpdated = -1;
  400. update(t);
  401. }
  402. return {
  403. viewport,
  404. get isAnimating() { return p.animate.name !== 'off'; },
  405. get props() { return p as Readonly<TrackballControlsProps>; },
  406. setProps: (props: Partial<TrackballControlsProps>) => {
  407. if (props.animate?.name === 'rock' && p.animate.name !== 'rock') {
  408. resetRock(); // start rocking from the center
  409. }
  410. Object.assign(p, props);
  411. },
  412. start,
  413. update,
  414. reset,
  415. dispose
  416. };
  417. }
  418. }