pinch-zoom.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927
  1. /*
  2. PinchZoom.js
  3. Copyright (c) Manuel Stofer 2013 - today
  4. Author: Manuel Stofer (mst@rtp.ch)
  5. Version: 2.3.5
  6. Permission is hereby granted, free of charge, to any person obtaining a copy
  7. of this software and associated documentation files (the "Software"), to deal
  8. in the Software without restriction, including without limitation the rights
  9. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. copies of the Software, and to permit persons to whom the Software is
  11. furnished to do so, subject to the following conditions:
  12. The above copyright notice and this permission notice shall be included in
  13. all copies or substantial portions of the Software.
  14. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. THE SOFTWARE.
  21. */
  22. // polyfills
  23. if (typeof Object.assign != 'function') {
  24. // Must be writable: true, enumerable: false, configurable: true
  25. Object.defineProperty(Object, "assign", {
  26. value: function assign(target, varArgs) { // .length of function is 2
  27. if (target == null) { // TypeError if undefined or null
  28. throw new TypeError('Cannot convert undefined or null to object');
  29. }
  30. var to = Object(target);
  31. for (var index = 1; index < arguments.length; index++) {
  32. var nextSource = arguments[index];
  33. if (nextSource != null) { // Skip over if undefined or null
  34. for (var nextKey in nextSource) {
  35. // Avoid bugs when hasOwnProperty is shadowed
  36. if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
  37. to[nextKey] = nextSource[nextKey];
  38. }
  39. }
  40. }
  41. }
  42. return to;
  43. },
  44. writable: true,
  45. configurable: true
  46. });
  47. }
  48. if (typeof Array.from != 'function') {
  49. Array.from = function (object) {
  50. return [].slice.call(object);
  51. };
  52. }
  53. // utils
  54. var buildElement = function(str) {
  55. // empty string as title argument required by IE and Edge
  56. var tmp = document.implementation.createHTMLDocument('');
  57. tmp.body.innerHTML = str;
  58. return Array.from(tmp.body.children)[0];
  59. };
  60. var triggerEvent = function(el, name) {
  61. var event = document.createEvent('HTMLEvents');
  62. event.initEvent(name, true, false);
  63. el.dispatchEvent(event);
  64. };
  65. var definePinchZoom = function () {
  66. /**
  67. * Pinch zoom
  68. * @param el
  69. * @param options
  70. * @constructor
  71. */
  72. var PinchZoom = function (el, options) {
  73. this.el = el;
  74. this.zoomFactor = 1;
  75. this.lastScale = 1;
  76. this.offset = {
  77. x: 0,
  78. y: 0
  79. };
  80. this.initialOffset = {
  81. x: 0,
  82. y: 0,
  83. };
  84. this.options = Object.assign({}, this.defaults, options);
  85. this.setupMarkup();
  86. this.bindEvents();
  87. this.update();
  88. // The image may already be loaded when PinchZoom is initialized,
  89. // and then the load event (which trigger update) will never fire.
  90. if (this.isImageLoaded(this.el)) {
  91. this.updateAspectRatio();
  92. this.setupOffsets();
  93. }
  94. this.enable();
  95. },
  96. sum = function (a, b) {
  97. return a + b;
  98. },
  99. isCloseTo = function (value, expected) {
  100. return value > expected - 0.01 && value < expected + 0.01;
  101. };
  102. PinchZoom.prototype = {
  103. defaults: {
  104. tapZoomFactor: 2,
  105. zoomOutFactor: 1.3,
  106. animationDuration: 300,
  107. maxZoom: 4,
  108. minZoom: 0.5,
  109. draggableUnzoomed: true,
  110. lockDragAxis: false,
  111. setOffsetsOnce: false,
  112. use2d: true,
  113. zoomStartEventName: 'pz_zoomstart',
  114. zoomUpdateEventName: 'pz_zoomupdate',
  115. zoomEndEventName: 'pz_zoomend',
  116. dragStartEventName: 'pz_dragstart',
  117. dragUpdateEventName: 'pz_dragupdate',
  118. dragEndEventName: 'pz_dragend',
  119. doubleTapEventName: 'pz_doubletap',
  120. verticalPadding: 0,
  121. horizontalPadding: 0,
  122. onZoomStart: null,
  123. onZoomEnd: null,
  124. onZoomUpdate: null,
  125. onDragStart: null,
  126. onDragEnd: null,
  127. onDragUpdate: null,
  128. onDoubleTap: null
  129. },
  130. /**
  131. * Event handler for 'dragstart'
  132. * @param event
  133. */
  134. handleDragStart: function (event) {
  135. triggerEvent(this.el, this.options.dragStartEventName);
  136. if(typeof this.options.onDragStart == "function"){
  137. this.options.onDragStart(this, event)
  138. }
  139. this.stopAnimation();
  140. this.lastDragPosition = false;
  141. this.hasInteraction = true;
  142. this.handleDrag(event);
  143. },
  144. /**
  145. * Event handler for 'drag'
  146. * @param event
  147. */
  148. handleDrag: function (event) {
  149. var touch = this.getTouches(event)[0];
  150. this.drag(touch, this.lastDragPosition);
  151. this.offset = this.sanitizeOffset(this.offset);
  152. this.lastDragPosition = touch;
  153. },
  154. handleDragEnd: function () {
  155. triggerEvent(this.el, this.options.dragEndEventName);
  156. if(typeof this.options.onDragEnd == "function"){
  157. this.options.onDragEnd(this, event)
  158. }
  159. this.end();
  160. },
  161. /**
  162. * Event handler for 'zoomstart'
  163. * @param event
  164. */
  165. handleZoomStart: function (event) {
  166. triggerEvent(this.el, this.options.zoomStartEventName);
  167. if(typeof this.options.onZoomStart == "function"){
  168. this.options.onZoomStart(this, event)
  169. }
  170. this.stopAnimation();
  171. this.lastScale = 1;
  172. this.nthZoom = 0;
  173. this.lastZoomCenter = false;
  174. this.hasInteraction = true;
  175. },
  176. /**
  177. * Event handler for 'zoom'
  178. * @param event
  179. */
  180. handleZoom: function (event, newScale) {
  181. // a relative scale factor is used
  182. var touchCenter = this.getTouchCenter(this.getTouches(event)),
  183. scale = newScale / this.lastScale;
  184. this.lastScale = newScale;
  185. // the first touch events are thrown away since they are not precise
  186. this.nthZoom += 1;
  187. if (this.nthZoom > 3) {
  188. this.scale(scale, touchCenter);
  189. this.drag(touchCenter, this.lastZoomCenter);
  190. }
  191. this.lastZoomCenter = touchCenter;
  192. },
  193. handleZoomEnd: function () {
  194. triggerEvent(this.el, this.options.zoomEndEventName);
  195. if(typeof this.options.onZoomEnd == "function"){
  196. this.options.onZoomEnd(this, event)
  197. }
  198. this.end();
  199. },
  200. /**
  201. * Event handler for 'doubletap'
  202. * @param event
  203. */
  204. handleDoubleTap: function (event) {
  205. var center = this.getTouches(event)[0],
  206. zoomFactor = this.zoomFactor > 1 ? 1 : this.options.tapZoomFactor,
  207. startZoomFactor = this.zoomFactor,
  208. updateProgress = (function (progress) {
  209. this.scaleTo(startZoomFactor + progress * (zoomFactor - startZoomFactor), center);
  210. }).bind(this);
  211. if (this.hasInteraction) {
  212. return;
  213. }
  214. this.isDoubleTap = true;
  215. if (startZoomFactor > zoomFactor) {
  216. center = this.getCurrentZoomCenter();
  217. }
  218. this.animate(this.options.animationDuration, updateProgress, this.swing);
  219. triggerEvent(this.el, this.options.doubleTapEventName);
  220. if(typeof this.options.onDoubleTap == "function"){
  221. this.options.onDoubleTap(this, event)
  222. }
  223. },
  224. /**
  225. * Compute the initial offset
  226. *
  227. * the element should be centered in the container upon initialization
  228. */
  229. computeInitialOffset: function () {
  230. this.initialOffset = {
  231. x: -Math.abs(this.el.offsetWidth * this.getInitialZoomFactor() - this.container.offsetWidth) / 2,
  232. y: -Math.abs(this.el.offsetHeight * this.getInitialZoomFactor() - this.container.offsetHeight) / 2,
  233. };
  234. },
  235. /**
  236. * Reset current image offset to that of the initial offset
  237. */
  238. resetOffset: function() {
  239. this.offset.x = this.initialOffset.x;
  240. this.offset.y = this.initialOffset.y;
  241. },
  242. /**
  243. * Determine if image is loaded
  244. */
  245. isImageLoaded: function (el) {
  246. if (el.nodeName === 'IMG') {
  247. return el.complete && el.naturalHeight !== 0;
  248. } else {
  249. return Array.from(el.querySelectorAll('img')).every(this.isImageLoaded);
  250. }
  251. },
  252. setupOffsets: function() {
  253. if (this.options.setOffsetsOnce && this._isOffsetsSet) {
  254. return;
  255. }
  256. this._isOffsetsSet = true;
  257. this.computeInitialOffset();
  258. this.resetOffset();
  259. },
  260. /**
  261. * Max / min values for the offset
  262. * @param offset
  263. * @return {Object} the sanitized offset
  264. */
  265. sanitizeOffset: function (offset) {
  266. var elWidth = this.el.offsetWidth * this.getInitialZoomFactor() * this.zoomFactor;
  267. var elHeight = this.el.offsetHeight * this.getInitialZoomFactor() * this.zoomFactor;
  268. var maxX = elWidth - this.getContainerX() + this.options.horizontalPadding,
  269. maxY = elHeight - this.getContainerY() + this.options.verticalPadding,
  270. maxOffsetX = Math.max(maxX, 0),
  271. maxOffsetY = Math.max(maxY, 0),
  272. minOffsetX = Math.min(maxX, 0) - this.options.horizontalPadding,
  273. minOffsetY = Math.min(maxY, 0) - this.options.verticalPadding;
  274. return {
  275. x: Math.min(Math.max(offset.x, minOffsetX), maxOffsetX),
  276. y: Math.min(Math.max(offset.y, minOffsetY), maxOffsetY)
  277. };
  278. },
  279. /**
  280. * Scale to a specific zoom factor (not relative)
  281. * @param zoomFactor
  282. * @param center
  283. */
  284. scaleTo: function (zoomFactor, center) {
  285. this.scale(zoomFactor / this.zoomFactor, center);
  286. },
  287. /**
  288. * Scales the element from specified center
  289. * @param scale
  290. * @param center
  291. */
  292. scale: function (scale, center) {
  293. scale = this.scaleZoomFactor(scale);
  294. this.addOffset({
  295. x: (scale - 1) * (center.x + this.offset.x),
  296. y: (scale - 1) * (center.y + this.offset.y)
  297. });
  298. triggerEvent(this.el, this.options.zoomUpdateEventName);
  299. if(typeof this.options.onZoomUpdate == "function"){
  300. this.options.onZoomUpdate(this, event)
  301. }
  302. },
  303. /**
  304. * Scales the zoom factor relative to current state
  305. * @param scale
  306. * @return the actual scale (can differ because of max min zoom factor)
  307. */
  308. scaleZoomFactor: function (scale) {
  309. var originalZoomFactor = this.zoomFactor;
  310. this.zoomFactor *= scale;
  311. this.zoomFactor = Math.min(this.options.maxZoom, Math.max(this.zoomFactor, this.options.minZoom));
  312. return this.zoomFactor / originalZoomFactor;
  313. },
  314. /**
  315. * Determine if the image is in a draggable state
  316. *
  317. * When the image can be dragged, the drag event is acted upon and cancelled.
  318. * When not draggable, the drag event bubbles through this component.
  319. *
  320. * @return {Boolean}
  321. */
  322. canDrag: function () {
  323. return this.options.draggableUnzoomed || !isCloseTo(this.zoomFactor, 1);
  324. },
  325. /**
  326. * Drags the element
  327. * @param center
  328. * @param lastCenter
  329. */
  330. drag: function (center, lastCenter) {
  331. if (lastCenter) {
  332. if(this.options.lockDragAxis) {
  333. // lock scroll to position that was changed the most
  334. if(Math.abs(center.x - lastCenter.x) > Math.abs(center.y - lastCenter.y)) {
  335. this.addOffset({
  336. x: -(center.x - lastCenter.x),
  337. y: 0
  338. });
  339. }
  340. else {
  341. this.addOffset({
  342. y: -(center.y - lastCenter.y),
  343. x: 0
  344. });
  345. }
  346. }
  347. else {
  348. this.addOffset({
  349. y: -(center.y - lastCenter.y),
  350. x: -(center.x - lastCenter.x)
  351. });
  352. }
  353. triggerEvent(this.el, this.options.dragUpdateEventName);
  354. if(typeof this.options.onDragUpdate == "function"){
  355. this.options.onDragUpdate(this, event)
  356. }
  357. }
  358. },
  359. /**
  360. * Calculates the touch center of multiple touches
  361. * @param touches
  362. * @return {Object}
  363. */
  364. getTouchCenter: function (touches) {
  365. return this.getVectorAvg(touches);
  366. },
  367. /**
  368. * Calculates the average of multiple vectors (x, y values)
  369. */
  370. getVectorAvg: function (vectors) {
  371. return {
  372. x: vectors.map(function (v) { return v.x; }).reduce(sum) / vectors.length,
  373. y: vectors.map(function (v) { return v.y; }).reduce(sum) / vectors.length
  374. };
  375. },
  376. /**
  377. * Adds an offset
  378. * @param offset the offset to add
  379. * @return return true when the offset change was accepted
  380. */
  381. addOffset: function (offset) {
  382. this.offset = {
  383. x: this.offset.x + offset.x,
  384. y: this.offset.y + offset.y
  385. };
  386. },
  387. sanitize: function () {
  388. if (this.zoomFactor < this.options.zoomOutFactor) {
  389. this.zoomOutAnimation();
  390. } else if (this.isInsaneOffset(this.offset)) {
  391. this.sanitizeOffsetAnimation();
  392. }
  393. },
  394. /**
  395. * Checks if the offset is ok with the current zoom factor
  396. * @param offset
  397. * @return {Boolean}
  398. */
  399. isInsaneOffset: function (offset) {
  400. var sanitizedOffset = this.sanitizeOffset(offset);
  401. return sanitizedOffset.x !== offset.x ||
  402. sanitizedOffset.y !== offset.y;
  403. },
  404. /**
  405. * Creates an animation moving to a sane offset
  406. */
  407. sanitizeOffsetAnimation: function () {
  408. var targetOffset = this.sanitizeOffset(this.offset),
  409. startOffset = {
  410. x: this.offset.x,
  411. y: this.offset.y
  412. },
  413. updateProgress = (function (progress) {
  414. this.offset.x = startOffset.x + progress * (targetOffset.x - startOffset.x);
  415. this.offset.y = startOffset.y + progress * (targetOffset.y - startOffset.y);
  416. this.update();
  417. }).bind(this);
  418. this.animate(
  419. this.options.animationDuration,
  420. updateProgress,
  421. this.swing
  422. );
  423. },
  424. /**
  425. * Zooms back to the original position,
  426. * (no offset and zoom factor 1)
  427. */
  428. zoomOutAnimation: function () {
  429. if (this.zoomFactor === 1) {
  430. return;
  431. }
  432. var startZoomFactor = this.zoomFactor,
  433. zoomFactor = 1,
  434. center = this.getCurrentZoomCenter(),
  435. updateProgress = (function (progress) {
  436. this.scaleTo(startZoomFactor + progress * (zoomFactor - startZoomFactor), center);
  437. }).bind(this);
  438. this.animate(
  439. this.options.animationDuration,
  440. updateProgress,
  441. this.swing
  442. );
  443. },
  444. /**
  445. * Updates the container aspect ratio
  446. *
  447. * Any previous container height must be cleared before re-measuring the
  448. * parent height, since it depends implicitly on the height of any of its children
  449. */
  450. updateAspectRatio: function () {
  451. this.unsetContainerY();
  452. this.setContainerY(this.container.parentElement.offsetHeight);
  453. },
  454. /**
  455. * Calculates the initial zoom factor (for the element to fit into the container)
  456. * @return {number} the initial zoom factor
  457. */
  458. getInitialZoomFactor: function () {
  459. var xZoomFactor = this.container.offsetWidth / this.el.offsetWidth;
  460. var yZoomFactor = this.container.offsetHeight / this.el.offsetHeight;
  461. return Math.min(xZoomFactor, yZoomFactor);
  462. },
  463. /**
  464. * Calculates the aspect ratio of the element
  465. * @return the aspect ratio
  466. */
  467. getAspectRatio: function () {
  468. return this.el.offsetWidth / this.el.offsetHeight;
  469. },
  470. /**
  471. * Calculates the virtual zoom center for the current offset and zoom factor
  472. * (used for reverse zoom)
  473. * @return {Object} the current zoom center
  474. */
  475. getCurrentZoomCenter: function () {
  476. var offsetLeft = this.offset.x - this.initialOffset.x;
  477. var centerX = -1 * this.offset.x - offsetLeft / (1 / this.zoomFactor - 1);
  478. var offsetTop = this.offset.y - this.initialOffset.y;
  479. var centerY = -1 * this.offset.y - offsetTop / (1 / this.zoomFactor - 1);
  480. return {
  481. x: centerX,
  482. y: centerY
  483. };
  484. },
  485. /**
  486. * Returns the touches of an event relative to the container offset
  487. * @param event
  488. * @return array touches
  489. */
  490. getTouches: function (event) {
  491. var rect = this.container.getBoundingClientRect();
  492. var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  493. var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
  494. var posTop = rect.top + scrollTop;
  495. var posLeft = rect.left + scrollLeft;
  496. return Array.prototype.slice.call(event.touches).map(function (touch) {
  497. return {
  498. x: touch.pageX - posLeft,
  499. y: touch.pageY - posTop,
  500. };
  501. });
  502. },
  503. /**
  504. * Animation loop
  505. * does not support simultaneous animations
  506. * @param duration
  507. * @param framefn
  508. * @param timefn
  509. * @param callback
  510. */
  511. animate: function (duration, framefn, timefn, callback) {
  512. var startTime = new Date().getTime(),
  513. renderFrame = (function () {
  514. if (!this.inAnimation) { return; }
  515. var frameTime = new Date().getTime() - startTime,
  516. progress = frameTime / duration;
  517. if (frameTime >= duration) {
  518. framefn(1);
  519. if (callback) {
  520. callback();
  521. }
  522. this.update();
  523. this.stopAnimation();
  524. this.update();
  525. } else {
  526. if (timefn) {
  527. progress = timefn(progress);
  528. }
  529. framefn(progress);
  530. this.update();
  531. requestAnimationFrame(renderFrame);
  532. }
  533. }).bind(this);
  534. this.inAnimation = true;
  535. requestAnimationFrame(renderFrame);
  536. },
  537. /**
  538. * Stops the animation
  539. */
  540. stopAnimation: function () {
  541. this.inAnimation = false;
  542. },
  543. /**
  544. * Swing timing function for animations
  545. * @param p
  546. * @return {Number}
  547. */
  548. swing: function (p) {
  549. return -Math.cos(p * Math.PI) / 2 + 0.5;
  550. },
  551. getContainerX: function () {
  552. return this.container.offsetWidth;
  553. },
  554. getContainerY: function () {
  555. return this.container.offsetHeight;
  556. },
  557. setContainerY: function (y) {
  558. return this.container.style.height = y + 'px';
  559. },
  560. unsetContainerY: function () {
  561. this.container.style.height = null;
  562. },
  563. /**
  564. * Creates the expected html structure
  565. */
  566. setupMarkup: function () {
  567. this.container = buildElement('<div class="pinch-zoom-container"></div>');
  568. this.el.parentNode.insertBefore(this.container, this.el);
  569. this.container.appendChild(this.el);
  570. this.container.style.overflow = 'hidden';
  571. this.container.style.position = 'relative';
  572. this.el.style.webkitTransformOrigin = '0% 0%';
  573. this.el.style.mozTransformOrigin = '0% 0%';
  574. this.el.style.msTransformOrigin = '0% 0%';
  575. this.el.style.oTransformOrigin = '0% 0%';
  576. this.el.style.transformOrigin = '0% 0%';
  577. this.el.style.position = 'absolute';
  578. },
  579. end: function () {
  580. this.hasInteraction = false;
  581. this.sanitize();
  582. this.update();
  583. },
  584. /**
  585. * Binds all required event listeners
  586. */
  587. bindEvents: function () {
  588. var self = this;
  589. detectGestures(this.container, this);
  590. this.resizeHandler = this.update.bind(this)
  591. window.addEventListener('resize', this.resizeHandler);
  592. Array.from(this.el.querySelectorAll('img')).forEach(function(imgEl) {
  593. imgEl.addEventListener('load', self.update.bind(self));
  594. });
  595. if (this.el.nodeName === 'IMG') {
  596. this.el.addEventListener('load', this.update.bind(this));
  597. }
  598. },
  599. /**
  600. * Updates the css values according to the current zoom factor and offset
  601. */
  602. update: function (event) {
  603. if (event && event.type === 'resize') {
  604. this.updateAspectRatio();
  605. this.setupOffsets();
  606. }
  607. if (event && event.type === 'load') {
  608. this.updateAspectRatio();
  609. this.setupOffsets();
  610. }
  611. if (this.updatePlanned) {
  612. return;
  613. }
  614. this.updatePlanned = true;
  615. window.setTimeout((function () {
  616. this.updatePlanned = false;
  617. var zoomFactor = this.getInitialZoomFactor() * this.zoomFactor,
  618. offsetX = -this.offset.x / zoomFactor,
  619. offsetY = -this.offset.y / zoomFactor,
  620. transform3d = 'scale3d(' + zoomFactor + ', ' + zoomFactor + ',1) ' +
  621. 'translate3d(' + offsetX + 'px,' + offsetY + 'px,0px)',
  622. transform2d = 'scale(' + zoomFactor + ', ' + zoomFactor + ') ' +
  623. 'translate(' + offsetX + 'px,' + offsetY + 'px)',
  624. removeClone = (function () {
  625. if (this.clone) {
  626. this.clone.parentNode.removeChild(this.clone);
  627. delete this.clone;
  628. }
  629. }).bind(this);
  630. // Scale 3d and translate3d are faster (at least on ios)
  631. // but they also reduce the quality.
  632. // PinchZoom uses the 3d transformations during interactions
  633. // after interactions it falls back to 2d transformations
  634. if (!this.options.use2d || this.hasInteraction || this.inAnimation) {
  635. this.is3d = true;
  636. removeClone();
  637. this.el.style.webkitTransform = transform3d;
  638. this.el.style.mozTransform = transform2d;
  639. this.el.style.msTransform = transform2d;
  640. this.el.style.oTransform = transform2d;
  641. this.el.style.transform = transform3d;
  642. } else {
  643. // When changing from 3d to 2d transform webkit has some glitches.
  644. // To avoid this, a copy of the 3d transformed element is displayed in the
  645. // foreground while the element is converted from 3d to 2d transform
  646. if (this.is3d) {
  647. this.clone = this.el.cloneNode(true);
  648. this.clone.style.pointerEvents = 'none';
  649. this.container.appendChild(this.clone);
  650. window.setTimeout(removeClone, 200);
  651. }
  652. this.el.style.webkitTransform = transform2d;
  653. this.el.style.mozTransform = transform2d;
  654. this.el.style.msTransform = transform2d;
  655. this.el.style.oTransform = transform2d;
  656. this.el.style.transform = transform2d;
  657. this.is3d = false;
  658. }
  659. }).bind(this), 0);
  660. },
  661. /**
  662. * Enables event handling for gestures
  663. */
  664. enable: function() {
  665. this.enabled = true;
  666. },
  667. /**
  668. * Disables event handling for gestures
  669. */
  670. disable: function() {
  671. this.enabled = false;
  672. },
  673. /**
  674. * Unmounts the zooming container and global event listeners
  675. */
  676. destroy: function () {
  677. window.removeEventListener('resize', this.resizeHandler);
  678. if (this.container) {
  679. this.container.remove();
  680. this.container = null;
  681. }
  682. }
  683. };
  684. var detectGestures = function (el, target) {
  685. var interaction = null,
  686. fingers = 0,
  687. lastTouchStart = null,
  688. startTouches = null,
  689. setInteraction = function (newInteraction, event) {
  690. if (interaction !== newInteraction) {
  691. if (interaction && !newInteraction) {
  692. switch (interaction) {
  693. case "zoom":
  694. target.handleZoomEnd(event);
  695. break;
  696. case 'drag':
  697. target.handleDragEnd(event);
  698. break;
  699. }
  700. }
  701. switch (newInteraction) {
  702. case 'zoom':
  703. target.handleZoomStart(event);
  704. break;
  705. case 'drag':
  706. target.handleDragStart(event);
  707. break;
  708. }
  709. }
  710. interaction = newInteraction;
  711. },
  712. updateInteraction = function (event) {
  713. if (fingers === 2) {
  714. setInteraction('zoom');
  715. } else if (fingers === 1 && target.canDrag()) {
  716. setInteraction('drag', event);
  717. } else {
  718. setInteraction(null, event);
  719. }
  720. },
  721. targetTouches = function (touches) {
  722. return Array.from(touches).map(function (touch) {
  723. return {
  724. x: touch.pageX,
  725. y: touch.pageY
  726. };
  727. });
  728. },
  729. getDistance = function (a, b) {
  730. var x, y;
  731. x = a.x - b.x;
  732. y = a.y - b.y;
  733. return Math.sqrt(x * x + y * y);
  734. },
  735. calculateScale = function (startTouches, endTouches) {
  736. var startDistance = getDistance(startTouches[0], startTouches[1]),
  737. endDistance = getDistance(endTouches[0], endTouches[1]);
  738. return endDistance / startDistance;
  739. },
  740. cancelEvent = function (event) {
  741. event.stopPropagation();
  742. event.preventDefault();
  743. },
  744. detectDoubleTap = function (event) {
  745. var time = (new Date()).getTime();
  746. if (fingers > 1) {
  747. lastTouchStart = null;
  748. }
  749. if (time - lastTouchStart < 300) {
  750. cancelEvent(event);
  751. target.handleDoubleTap(event);
  752. switch (interaction) {
  753. case "zoom":
  754. target.handleZoomEnd(event);
  755. break;
  756. case 'drag':
  757. target.handleDragEnd(event);
  758. break;
  759. }
  760. } else {
  761. target.isDoubleTap = false;
  762. }
  763. if (fingers === 1) {
  764. lastTouchStart = time;
  765. }
  766. },
  767. firstMove = true;
  768. el.addEventListener('touchstart', function (event) {
  769. if(target.enabled) {
  770. firstMove = true;
  771. fingers = event.touches.length;
  772. detectDoubleTap(event);
  773. }
  774. }, { passive: false });
  775. el.addEventListener('touchmove', function (event) {
  776. if(target.enabled && !target.isDoubleTap) {
  777. if (firstMove) {
  778. updateInteraction(event);
  779. if (interaction) {
  780. cancelEvent(event);
  781. }
  782. startTouches = targetTouches(event.touches);
  783. } else {
  784. switch (interaction) {
  785. case 'zoom':
  786. if (startTouches.length == 2 && event.touches.length == 2) {
  787. target.handleZoom(event, calculateScale(startTouches, targetTouches(event.touches)));
  788. }
  789. break;
  790. case 'drag':
  791. target.handleDrag(event);
  792. break;
  793. }
  794. if (interaction) {
  795. cancelEvent(event);
  796. target.update();
  797. }
  798. }
  799. firstMove = false;
  800. }
  801. }, { passive: false });
  802. el.addEventListener('touchend', function (event) {
  803. if(target.enabled) {
  804. fingers = event.touches.length;
  805. updateInteraction(event);
  806. }
  807. });
  808. };
  809. return PinchZoom;
  810. };
  811. var PinchZoom = definePinchZoom();
  812. export default PinchZoom;