TLViewport.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import { Vec } from '@tldraw/vec'
  2. import { action, computed, makeObservable, observable } from 'mobx'
  3. import { FIT_TO_SCREEN_PADDING, ZOOM_UPDATE_FACTOR } from '../constants'
  4. import type { TLBounds } from '../types'
  5. export class TLViewport {
  6. constructor() {
  7. makeObservable(this)
  8. }
  9. static readonly minZoom = 0.1
  10. static readonly maxZoom = 4
  11. /* ------------------- Properties ------------------- */
  12. @observable bounds: TLBounds = {
  13. minX: 0,
  14. minY: 0,
  15. maxX: 1080,
  16. maxY: 720,
  17. width: 1080,
  18. height: 720,
  19. }
  20. @observable camera = {
  21. point: [0, 0],
  22. zoom: 1,
  23. }
  24. /* --------------------- Actions -------------------- */
  25. @action updateBounds = (bounds: TLBounds): this => {
  26. this.bounds = bounds
  27. return this
  28. }
  29. panCamera = (delta: number[]): this => {
  30. return this.update({
  31. point: Vec.sub(this.camera.point, Vec.div(delta, this.camera.zoom)),
  32. })
  33. }
  34. @action update = ({ point, zoom }: Partial<{ point: number[]; zoom: number }>): this => {
  35. if (point !== undefined) this.camera.point = point
  36. if (zoom !== undefined) this.camera.zoom = zoom
  37. return this
  38. }
  39. private _currentView = {
  40. minX: 0,
  41. minY: 0,
  42. maxX: 1,
  43. maxY: 1,
  44. width: 1,
  45. height: 1,
  46. }
  47. @computed get currentView(): TLBounds {
  48. const {
  49. bounds,
  50. camera: { point, zoom },
  51. } = this
  52. const w = bounds.width / zoom
  53. const h = bounds.height / zoom
  54. return {
  55. minX: -point[0],
  56. minY: -point[1],
  57. maxX: w - point[0],
  58. maxY: h - point[1],
  59. width: w,
  60. height: h,
  61. }
  62. }
  63. getPagePoint = (point: number[]): number[] => {
  64. const { camera, bounds } = this
  65. return Vec.sub(Vec.div(Vec.sub(point, [bounds.minX, bounds.minY]), camera.zoom), camera.point)
  66. }
  67. getScreenPoint = (point: number[]): number[] => {
  68. const { camera } = this
  69. return Vec.mul(Vec.add(point, camera.point), camera.zoom)
  70. }
  71. pinchCamera = (point: number[], delta: number[], zoom: number): this => {
  72. const { camera } = this
  73. zoom = Math.max(TLViewport.minZoom, Math.min(TLViewport.maxZoom, zoom));
  74. const nextPoint = Vec.sub(camera.point, Vec.div(delta, camera.zoom))
  75. const p0 = Vec.sub(Vec.div(point, camera.zoom), nextPoint)
  76. const p1 = Vec.sub(Vec.div(point, zoom), nextPoint)
  77. return this.update({ point: Vec.toFixed(Vec.add(nextPoint, Vec.sub(p1, p0))), zoom })
  78. }
  79. setZoom = (zoom: number) => {
  80. const { bounds } = this
  81. const center = [bounds.width / 2, bounds.height / 2]
  82. this.pinchCamera(center, [0, 0], zoom)
  83. }
  84. zoomIn = () => {
  85. const { camera } = this
  86. this.setZoom(camera.zoom / ZOOM_UPDATE_FACTOR)
  87. }
  88. zoomOut = () => {
  89. const { camera, bounds } = this
  90. this.setZoom(camera.zoom * ZOOM_UPDATE_FACTOR)
  91. }
  92. resetZoom = (): this => {
  93. const {
  94. bounds,
  95. camera: { zoom, point },
  96. } = this
  97. const center = [bounds.width / 2, bounds.height / 2]
  98. const p0 = Vec.sub(Vec.div(center, zoom), point)
  99. const p1 = Vec.sub(Vec.div(center, 1), point)
  100. return this.update({ point: Vec.toFixed(Vec.add(point, Vec.sub(p1, p0))), zoom: 1 })
  101. }
  102. zoomToBounds = ({ width, height, minX, minY }: TLBounds): this => {
  103. const { bounds, camera } = this
  104. let zoom = Math.min(
  105. (bounds.width - FIT_TO_SCREEN_PADDING) / width,
  106. (bounds.height - FIT_TO_SCREEN_PADDING) / height
  107. )
  108. zoom = Math.min(
  109. 1,
  110. Math.max(
  111. TLViewport.minZoom,
  112. camera.zoom === zoom || camera.zoom < 1 ? Math.min(1, zoom) : zoom
  113. )
  114. )
  115. const delta = [
  116. (bounds.width - width * zoom) / 2 / zoom,
  117. (bounds.height - height * zoom) / 2 / zoom,
  118. ]
  119. return this.update({ point: Vec.add([-minX, -minY], delta), zoom })
  120. }
  121. }