FlyTool.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. /*
  2. * FlyTool.js
  3. *
  4. * @author realor
  5. */
  6. import { Tool } from './Tool.js'
  7. import { Dialog } from '../ui/Dialog.js'
  8. import { I18N } from '../i18n/I18N.js'
  9. import * as THREE from '../lib/three.module.js'
  10. class FlyTool extends Tool {
  11. static SUBPANELS = [
  12. {
  13. name: 'leftWheel',
  14. xControl: 'lateralControl',
  15. yControl: 'elevationControl'
  16. },
  17. {
  18. name: 'pitchPanel'
  19. },
  20. {
  21. name: 'rightWheel',
  22. xControl: 'yawControl',
  23. yControl: 'forwardControl'
  24. }
  25. ]
  26. static ACTIONS = [
  27. {
  28. name: 'ascend',
  29. subpanel: 'leftWheel',
  30. keys: [87, 33], // W & page up
  31. control: 'elevationControl',
  32. value: 1
  33. },
  34. {
  35. name: 'descend',
  36. subpanel: 'leftWheel',
  37. keys: [83, 34], // S & page down
  38. control: 'elevationControl',
  39. value: -1
  40. },
  41. {
  42. name: 'moveLeft',
  43. subpanel: 'leftWheel',
  44. keys: [65, 45], // A & NP 0
  45. control: 'lateralControl',
  46. value: -1
  47. },
  48. {
  49. name: 'moveRight',
  50. subpanel: 'leftWheel',
  51. keys: [68, 46], // A & NP 0
  52. control: 'lateralControl',
  53. value: 1
  54. },
  55. {
  56. name: 'lookUp',
  57. subpanel: 'pitchPanel',
  58. keys: [82, 36], // R & home
  59. control: 'pitchControl',
  60. value: 1
  61. },
  62. {
  63. name: 'lookDown',
  64. subpanel: 'pitchPanel',
  65. keys: [70, 35], // F & end
  66. control: 'pitchControl',
  67. value: -1
  68. },
  69. {
  70. name: 'forward',
  71. subpanel: 'rightWheel',
  72. keys: [38], // cursor down
  73. control: 'forwardControl',
  74. value: 1,
  75. text: '' // use background image
  76. },
  77. {
  78. name: 'backward',
  79. subpanel: 'rightWheel',
  80. keys: [40], // cursor up
  81. control: 'forwardControl',
  82. value: -1,
  83. text: ''
  84. },
  85. {
  86. name: 'rotateLeft',
  87. subpanel: 'rightWheel',
  88. keys: [37], // cursor left
  89. control: 'yawControl',
  90. value: -1,
  91. text: ''
  92. },
  93. {
  94. name: 'rotateRight',
  95. subpanel: 'rightWheel',
  96. keys: [39], // cursor right
  97. control: 'yawControl',
  98. value: 1,
  99. text: ''
  100. },
  101. {
  102. name: 'options'
  103. }
  104. ]
  105. constructor(application, options) {
  106. super(application)
  107. this.name = 'fly'
  108. this.label = 'tool.fly.label'
  109. this.help = 'tool.fly.help'
  110. this.className = 'fly'
  111. this.setOptions(options)
  112. this.linearVelocity = 2 // meters/s
  113. this.angularVelocity = THREE.MathUtils.degToRad(20) // radians/s
  114. this.linearAccel = 5 // meters/s2
  115. this.angularAccel = THREE.MathUtils.degToRad(50) // radians/s2
  116. this.linearDecel = 2 // meters/s2
  117. this.angularDecel = THREE.MathUtils.degToRad(20) // radians/s2
  118. this.yaw = 0 // radians (0: north)
  119. this.pitch = 0 // radians
  120. this.stopMovement()
  121. // fly options
  122. this.mode = 'buttons'
  123. this.detectCollision = false
  124. this.groundDistanceControlEnabled = false
  125. this.groundDistance = 1.7
  126. // internals
  127. this.position = new THREE.Vector3(0, 0, 0)
  128. this.target = new THREE.Vector3(0, 0, 0)
  129. this.vector = new THREE.Vector3(0, 0, 0)
  130. this.raycaster = new THREE.Raycaster()
  131. this.auxVector = new THREE.Vector3(0, 0, 0)
  132. this._onKeyUp = this.onKeyUp.bind(this)
  133. this._onKeyDown = this.onKeyDown.bind(this)
  134. this._animate = this.animate.bind(this)
  135. this._onScene = this.onScene.bind(this)
  136. this.EPS = 0.00001
  137. this.createPanel()
  138. this.createKeyMap()
  139. }
  140. createPanel() {
  141. this.panel = this.application.createPanel(this.label, 'left')
  142. this.panel.preferredHeight = 120
  143. this.panel.mininumHeight = 120
  144. this.panel.bodyElem.classList.add('fly_panel')
  145. const keypad = document.createElement('div')
  146. keypad.className = 'keypad'
  147. this.panel.bodyElem.appendChild(keypad)
  148. this.subpanels = {}
  149. for (let subpanelDef of FlyTool.SUBPANELS) {
  150. let subpanelElem = document.createElement('div')
  151. subpanelElem.className = subpanelDef.name
  152. keypad.appendChild(subpanelElem)
  153. const subpanel = {
  154. name: subpanelDef.name,
  155. element: subpanelElem
  156. }
  157. this.subpanels[subpanel.name] = subpanel
  158. if (subpanelDef.xControl && subpanelDef.yControl) {
  159. subpanel.stick = new Stick(this, subpanel)
  160. subpanel.xControl = subpanelDef.xControl
  161. subpanel.yControl = subpanelDef.yControl
  162. }
  163. }
  164. this.buttons = {}
  165. for (let action of FlyTool.ACTIONS) {
  166. const button = document.createElement('button')
  167. button.name = action.name
  168. let text = action.text
  169. if (text === undefined) {
  170. if (action.keys) {
  171. text = String.fromCharCode(action.keys[0])
  172. } else {
  173. text = ''
  174. }
  175. }
  176. button.innerHTML = text
  177. button.className = action.name
  178. I18N.set(button, 'title', 'tool.fly.' + action.name)
  179. I18N.set(button, 'alt', 'tool.fly.' + action.name)
  180. if (action.control) {
  181. let onPressed = event => {
  182. this[action.control] = action.value
  183. button.setPointerCapture(event.pointerId)
  184. }
  185. let onReleased = event => {
  186. this[action.control] = 0
  187. button.setPointerCapture(event.pointerId)
  188. }
  189. button.addEventListener('pointerdown', onPressed)
  190. button.addEventListener('pointerup', onReleased)
  191. button.addEventListener('contextmenu', event => event.preventDefault())
  192. } else {
  193. button.addEventListener('click', () => this.onButtonClick(action))
  194. }
  195. if (action.subpanel) {
  196. this.subpanels[action.subpanel].element.appendChild(button)
  197. } else {
  198. keypad.appendChild(button)
  199. }
  200. this.buttons[action.name] = button
  201. }
  202. }
  203. createKeyMap() {
  204. this.keyMap = new Map()
  205. for (let action of FlyTool.ACTIONS) {
  206. if (action.keys) {
  207. for (let key of action.keys) {
  208. this.keyMap[key] = action
  209. }
  210. }
  211. }
  212. }
  213. activate() {
  214. this.panel.visible = true
  215. this.resetParameters()
  216. const application = this.application
  217. document.addEventListener('keyup', this._onKeyUp, false)
  218. document.addEventListener('keydown', this._onKeyDown, false)
  219. application.addEventListener('animation', this._animate)
  220. application.addEventListener('scene', this._onScene)
  221. }
  222. deactivate() {
  223. this.panel.visible = false
  224. this.stopMovement()
  225. const application = this.application
  226. document.removeEventListener('keyup', this._onKeyUp, false)
  227. document.removeEventListener('keydown', this._onKeyDown, false)
  228. application.removeEventListener('animation', this._animate)
  229. application.removeEventListener('scene', this._onScene)
  230. }
  231. stopMovement() {
  232. this.forwardControl = 0
  233. this.forwardAccel = 0
  234. this.forwardVelocity = 0
  235. this.lateralControl = 0
  236. this.lateralAccel = 0
  237. this.lateralVelocity = 0
  238. this.elevationControl = 0
  239. this.elevationAccel = 0
  240. this.elevationVelocity = 0
  241. this.yawControl = 0
  242. this.yawAccel = 0
  243. this.yawVelocity = 0
  244. this.pitchControl = 0
  245. this.pitchAccel = 0
  246. this.pitchVelocity = 0
  247. }
  248. switchMode() {
  249. if (this.mode === 'buttons') {
  250. this.mode = 'stick'
  251. this.panel.bodyElem.classList.add('stick')
  252. for (let subpanelName in this.subpanels) {
  253. let subpanel = this.subpanels[subpanelName]
  254. if (subpanel.stick) {
  255. subpanel.stick.activate()
  256. }
  257. }
  258. } // mode === "stick"
  259. else {
  260. this.mode = 'buttons'
  261. this.panel.bodyElem.classList.remove('stick')
  262. for (let subpanelName in this.subpanels) {
  263. let subpanel = this.subpanels[subpanelName]
  264. if (subpanel.stick) {
  265. subpanel.stick.deactivate()
  266. }
  267. }
  268. }
  269. }
  270. onKeyDown(event) {
  271. if (event.srcElement.nodeName.toUpperCase() === 'INPUT' || event.srcElement.classList.contains('cm-content')) return
  272. event.preventDefault()
  273. let action = this.keyMap[event.keyCode]
  274. if (action) {
  275. this[action.control] = action.value
  276. let button = this.buttons[action.name]
  277. button.classList.add('pressed')
  278. button.focus()
  279. }
  280. }
  281. onKeyUp(event) {
  282. if (event.srcElement.nodeName.toUpperCase() === 'INPUT') return
  283. event.preventDefault()
  284. let action = this.keyMap[event.keyCode]
  285. if (action) {
  286. this[action.control] = 0
  287. let button = this.buttons[action.name]
  288. button.classList.remove('pressed')
  289. }
  290. }
  291. onButtonClick(action) {
  292. if (action.name === 'options') {
  293. const minHeight = 0.5
  294. const maxHeight = 10
  295. const dialog = new Dialog('tool.fly.options')
  296. dialog.setClassName('fly_options')
  297. dialog.setSize(240, 180)
  298. dialog.addButton('close', 'button.close', () => {
  299. dialog.hide()
  300. })
  301. const stickControlElem = dialog.addCheckBoxField('stick', 'tool.fly.stick_control', this.mode === 'stick')
  302. const detectCollisionElem = dialog.addCheckBoxField('collision', 'tool.fly.detect_collision', this.detectCollision)
  303. const groundDistanceControlElem = dialog.addCheckBoxField('ground_control', 'tool.fly.ground_distance_control', this.groundDistanceControlEnabled)
  304. const groundDistanceElem = dialog.addNumberField('ground', 'tool.fly.ground_distance', this.groundDistance, 'ground')
  305. stickControlElem.addEventListener('change', () => this.switchMode())
  306. detectCollisionElem.addEventListener('change', () => (this.detectCollision = !this.detectCollision))
  307. groundDistanceControlElem.addEventListener('change', () => {
  308. this.groundDistanceControlEnabled = !this.groundDistanceControlEnabled
  309. groundDistanceElem.disabled = !this.groundDistanceControlEnabled
  310. })
  311. groundDistanceElem.min = minHeight
  312. groundDistanceElem.max = maxHeight
  313. groundDistanceElem.step = '0.01'
  314. groundDistanceElem.addEventListener('change', () => {
  315. let distance = groundDistanceElem.value
  316. distance = Math.max(distance, minHeight)
  317. distance = Math.min(distance, maxHeight)
  318. this.groundDistance = distance
  319. groundDistanceElem.value = distance
  320. })
  321. const unitsText = document.createElement('span')
  322. unitsText.innerHTML = this.application.units
  323. groundDistanceElem.parentElement.appendChild(unitsText)
  324. groundDistanceElem.disabled = !this.groundDistanceControlEnabled
  325. dialog.setI18N(this.application.i18n)
  326. dialog.show()
  327. }
  328. }
  329. animate(event) {
  330. const delta = event.delta
  331. const application = this.application
  332. const camera = application.camera
  333. this.forwardAccel = this.forwardControl * this.linearAccel
  334. if (this.forwardVelocity > 0) {
  335. this.forwardAccel -= this.linearDecel
  336. } else if (this.forwardVelocity < 0) {
  337. this.forwardAccel += this.linearDecel
  338. }
  339. this.lateralAccel = this.lateralControl * this.linearAccel
  340. if (this.lateralVelocity > 0) {
  341. this.lateralAccel -= this.linearDecel
  342. } else if (this.lateralVelocity < 0) {
  343. this.lateralAccel += this.linearDecel
  344. }
  345. let groundDistanceControl = 0
  346. if (this.elevationControl === 0 && this.groundDistanceControlEnabled) {
  347. groundDistanceControl = this.groundDistanceControl()
  348. this.elevationAccel = groundDistanceControl * this.linearAccel
  349. } else {
  350. this.elevationAccel = this.elevationControl * this.linearAccel
  351. }
  352. if (this.elevationVelocity > 0) {
  353. this.elevationAccel -= this.linearDecel
  354. } else if (this.elevationVelocity < 0) {
  355. this.elevationAccel += this.linearDecel
  356. }
  357. this.yawAccel = this.yawControl * this.angularAccel
  358. if (this.yawVelocity > 0) {
  359. this.yawAccel -= this.angularDecel
  360. } else if (this.yawVelocity < 0) {
  361. this.yawAccel += this.angularDecel
  362. }
  363. this.pitchAccel = this.pitchControl * this.angularAccel
  364. if (this.pitchVelocity > 0) {
  365. this.pitchAccel -= this.angularDecel
  366. } else if (this.pitchVelocity < 0) {
  367. this.pitchAccel += this.angularDecel
  368. }
  369. this.forwardVelocity += this.forwardAccel * delta
  370. this.lateralVelocity += this.lateralAccel * delta
  371. this.elevationVelocity += this.elevationAccel * delta
  372. this.yawVelocity += this.yawAccel * delta
  373. this.pitchVelocity += this.pitchAccel * delta
  374. if (this.forwardControl === 0) {
  375. if ((this.forwardAccel > 0 && this.forwardVelocity > 0) || (this.forwardAccel < 0 && this.forwardVelocity < 0)) {
  376. this.forwardAccel = 0
  377. this.forwardVelocity = 0
  378. }
  379. }
  380. if (this.lateralControl === 0) {
  381. if ((this.lateralAccel > 0 && this.lateralVelocity > 0) || (this.lateralAccel < 0 && this.lateralVelocity < 0)) {
  382. this.lateralAccel = 0
  383. this.lateralVelocity = 0
  384. }
  385. }
  386. if (this.elevationControl === 0 && groundDistanceControl === 0) {
  387. if ((this.elevationAccel > 0 && this.elevationVelocity > 0) || (this.elevationAccel < 0 && this.elevationVelocity < 0)) {
  388. this.elevationAccel = 0
  389. this.elevationVelocity = 0
  390. }
  391. }
  392. if (this.yawControl === 0) {
  393. if ((this.yawAccel > 0 && this.yawVelocity > 0) || (this.yawAccel < 0 && this.yawVelocity < 0)) {
  394. this.yawAccel = 0
  395. this.yawVelocity = 0
  396. }
  397. }
  398. if (this.pitchControl === 0) {
  399. if ((this.pitchAccel > 0 && this.pitchVelocity > 0) || (this.pitchAccel < 0 && this.pitchVelocity < 0)) {
  400. this.pitchAccel = 0
  401. this.pitchVelocity = 0
  402. }
  403. }
  404. const residualMove = 0.00001
  405. if (
  406. this.updateCamera ||
  407. Math.abs(this.yawVelocity) > residualMove ||
  408. Math.abs(this.pitchVelocity) > residualMove ||
  409. Math.abs(this.forwardVelocity) > residualMove ||
  410. Math.abs(this.lateralVelocity) > residualMove ||
  411. Math.abs(this.elevationVelocity) > residualMove
  412. ) {
  413. this.yaw += this.yawVelocity * delta
  414. this.pitch += this.pitchVelocity * delta
  415. if (this.pitch <= -0.5 * Math.PI) {
  416. this.pitch = -0.5 * Math.PI + this.EPS
  417. this.pitchVelocity = 0
  418. this.pitchAccel = 0
  419. } else if (this.pitch >= 0.5 * Math.PI) {
  420. this.pitch = 0.5 * Math.PI - this.EPS
  421. this.pitchVelocity = 0
  422. this.pitchAccel = 0
  423. }
  424. const sinYaw = Math.sin(this.yaw)
  425. const cosYaw = Math.cos(this.yaw)
  426. const sinPitch = Math.sin(this.pitch)
  427. const cosPitch = Math.cos(this.pitch)
  428. const position = camera.position
  429. this.position.copy(position)
  430. const me = camera.matrixWorld.elements
  431. const scale = this.vector.set(me[8], me[9], me[10]).length()
  432. position.x += (this.forwardVelocity * sinYaw * delta) / scale
  433. position.y += (this.forwardVelocity * cosYaw * delta) / scale
  434. position.z += (this.elevationVelocity * delta) / scale
  435. camera.translateX((this.lateralVelocity * delta) / scale)
  436. if (this.detectCollision && !this.position.equals(position)) {
  437. if (this.collide(this.position, position)) {
  438. this.stopMovement()
  439. }
  440. }
  441. this.target.x = position.x + 100 * cosPitch * sinYaw
  442. this.target.y = position.y + 100 * cosPitch * cosYaw
  443. this.target.z = position.z + 100 * sinPitch
  444. camera.updateMatrix()
  445. camera.lookAt(this.target)
  446. camera.updateMatrix()
  447. application.notifyObjectsChanged(camera, this)
  448. this.updateCamera = false
  449. }
  450. }
  451. onScene(event) {
  452. const application = this.application
  453. if (event.source !== this) {
  454. const camera = application.camera
  455. if (event.type === 'nodeChanged' && event.objects.includes(camera)) {
  456. this.resetParameters()
  457. } else if (event.type === 'cameraActivated') {
  458. this.resetParameters()
  459. }
  460. }
  461. }
  462. resetParameters() {
  463. const application = this.application
  464. const container = application.container
  465. const camera = application.camera
  466. if (camera instanceof THREE.PerspectiveCamera) {
  467. camera.aspect = container.clientWidth / container.clientHeight
  468. camera.updateProjectionMatrix()
  469. }
  470. camera.updateMatrix()
  471. const matrix = camera.matrix
  472. const me = matrix.elements
  473. const vz = new THREE.Vector3()
  474. vz.x = me[8]
  475. vz.y = me[9]
  476. vz.z = me[10]
  477. vz.normalize()
  478. this.pitch = Math.asin(-vz.z)
  479. if (Math.abs(vz.x) > 0.01 && Math.abs(vz.y) > 0.01) {
  480. this.yaw = Math.atan2(-vz.x, -vz.y)
  481. } else {
  482. const vx = new THREE.Vector3()
  483. vx.x = me[0]
  484. vx.y = me[1]
  485. vx.z = me[2]
  486. vx.normalize()
  487. this.yaw = Math.atan2(-vx.y, vx.x)
  488. }
  489. this.updateCamera = true
  490. }
  491. collide(oldPosition, newPosition) {
  492. const vector = this.auxVector
  493. vector.copy(newPosition)
  494. vector.sub(oldPosition)
  495. const margin = 0.3
  496. const vectorLength = vector.length() + margin
  497. vector.normalize()
  498. const distance = this.measureDistance(oldPosition, vector, vectorLength)
  499. if (distance !== undefined) {
  500. let stopDistance = distance - margin
  501. vector.multiplyScalar(stopDistance)
  502. newPosition.x = oldPosition.x + vector.x
  503. newPosition.y = oldPosition.y + vector.y
  504. newPosition.z = oldPosition.z + vector.z
  505. return true
  506. }
  507. return false
  508. }
  509. groundDistanceControl() {
  510. const position = this.position
  511. const groundVector = this.auxVector
  512. const groundDistance = this.groundDistance
  513. const margin = 0.01
  514. const maxHeight = 100
  515. groundVector.set(0, 0, -1)
  516. const currentGroundDistance = this.measureDistance(position, groundVector, maxHeight)
  517. if (currentGroundDistance !== undefined) {
  518. const targetDistance = currentGroundDistance - groundDistance
  519. if (targetDistance > margin) {
  520. if (this.elevationVelocity < 0) {
  521. const stopDistance = Math.abs((this.elevationVelocity * this.elevationVelocity) / (2 * this.linearAccel))
  522. if (stopDistance + margin > targetDistance) return 1
  523. }
  524. return -1
  525. } else if (targetDistance < -margin) {
  526. if (this.elevationVelocity > 0) {
  527. const stopDistance = Math.abs((this.elevationVelocity * this.elevationVelocity) / (2 * this.linearAccel))
  528. if (stopDistance + margin > -targetDistance) return -1
  529. }
  530. return 1
  531. }
  532. }
  533. return 0
  534. }
  535. measureDistance(origin, unitVector, length) {
  536. const scene = this.application.scene
  537. this.raycaster.set(origin, unitVector)
  538. this.raycaster.far = length
  539. const intersects = this.raycaster.intersectObjects(scene.children, true)
  540. let i = 0
  541. let found = false
  542. let object = null
  543. let intersect = null
  544. while (i < intersects.length && !found) {
  545. intersect = intersects[i]
  546. object = intersect.object
  547. if (this.isCollidable(object)) {
  548. found = true
  549. } else i++
  550. }
  551. return found ? intersect.distance : undefined
  552. }
  553. isCollidable(object) {
  554. let collidable = true
  555. while (object && collidable) {
  556. collidable = object.visible && (object.userData.collision === undefined || object.userData.collision.enabled)
  557. object = object.parent
  558. }
  559. return collidable
  560. }
  561. }
  562. class Stick {
  563. constructor(tool, subpanel) {
  564. this.subpanel = subpanel
  565. const element = document.createElement('div')
  566. this.element = element
  567. element.className = 'stick'
  568. element.style.touchAction = 'none'
  569. subpanel.element.style.touchAction = 'none'
  570. subpanel.element.appendChild(element)
  571. this.onPointerDown = event => {
  572. subpanel.element.addEventListener('pointermove', this.onPointerMove)
  573. subpanel.element.setPointerCapture(event.pointerId)
  574. const control = this.updatePosition(event.clientX, event.clientY)
  575. tool[subpanel.xControl] = control.x
  576. tool[subpanel.yControl] = control.y
  577. }
  578. this.onPointerUp = event => {
  579. subpanel.element.removeEventListener('pointermove', this.onPointerMove)
  580. subpanel.element.releasePointerCapture(event.pointerId)
  581. element.style.left = ''
  582. element.style.top = ''
  583. tool[subpanel.xControl] = 0
  584. tool[subpanel.yControl] = 0
  585. }
  586. this.onPointerMove = event => {
  587. let control = this.updatePosition(event.clientX, event.clientY)
  588. tool[subpanel.xControl] = control.x
  589. tool[subpanel.yControl] = control.y
  590. }
  591. subpanel.element.addEventListener('contextmenu', event => event.preventDefault())
  592. }
  593. activate() {
  594. const subpanel = this.subpanel
  595. subpanel.element.addEventListener('pointerdown', this.onPointerDown)
  596. subpanel.element.addEventListener('pointerup', this.onPointerUp)
  597. }
  598. deactivate() {
  599. const subpanel = this.subpanel
  600. subpanel.element.removeEventListener('pointerdown', this.onPointerDown)
  601. subpanel.element.removeEventListener('pointerup', this.onPointerUp)
  602. }
  603. updatePosition(clientX, clientY) {
  604. const stickElem = this.element
  605. const size = stickElem.offsetWidth
  606. const subpanelElem = this.subpanel.element
  607. const rect = subpanelElem.getBoundingClientRect()
  608. let layerX = clientX - rect.left
  609. let layerY = clientY - rect.top
  610. let x = 2 * (layerX / rect.width) - 1
  611. let y = -2 * (layerY / rect.height) + 1
  612. let radius = (rect.width - size) / rect.width
  613. if (Math.sqrt(x * x + y * y) > radius) {
  614. let angle = Math.atan2(y, x)
  615. x = radius * Math.cos(angle)
  616. y = radius * Math.sin(angle)
  617. layerX = (x + 1) * 0.5 * rect.width
  618. layerY = (1 - y) * 0.5 * rect.height
  619. }
  620. stickElem.style.left = -1 + (layerX - 0.5 * size) + 'px'
  621. stickElem.style.top = -1 + (layerY - 0.5 * size) + 'px'
  622. x = x.toFixed(2)
  623. y = y.toFixed(2)
  624. return { x, y }
  625. }
  626. }
  627. export { FlyTool }