123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760 |
- /*
- Copyright (c) Manuel Stofer 2013 - rtp.ch - RTP.PinchZoom.js
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
- */
- /*global jQuery, console, define, setTimeout, window*/
- (function () {
- 'use strict';
- var definePinchZoom = function ($) {
- /**
- * Pinch zoom using jQuery
- * @version 0.0.2
- * @author Manuel Stofer <mst@rtp.ch>
- * @param el
- * @param options
- * @constructor
- */
- var PinchZoom = function (el, options) {
- this.el = $(el);
- this.zoomFactor = 1;
- this.lastScale = 1;
- this.offset = {
- x: 0,
- y: 0
- };
- this.options = $.extend({}, this.defaults, options);
- this.setupMarkup();
- this.bindEvents();
- this.update();
- // default enable.
- this.enable();
- },
- sum = function (a, b) {
- return a + b;
- },
- isCloseTo = function (value, expected) {
- return value > expected - 0.01 && value < expected + 0.01;
- };
- PinchZoom.prototype = {
- defaults: {
- tapZoomFactor: 2,
- zoomOutFactor: 1.3,
- animationDuration: 300,
- maxZoom: 4,
- minZoom: 0.5,
- lockDragAxis: false,
- use2d: true,
- zoomStartEventName: 'pz_zoomstart',
- zoomEndEventName: 'pz_zoomend',
- dragStartEventName: 'pz_dragstart',
- dragEndEventName: 'pz_dragend',
- doubleTapEventName: 'pz_doubletap'
- },
- /**
- * Event handler for 'dragstart'
- * @param event
- */
- handleDragStart: function (event) {
- this.el.trigger(this.options.dragStartEventName);
- this.stopAnimation();
- this.lastDragPosition = false;
- this.hasInteraction = true;
- this.handleDrag(event);
- },
- /**
- * Event handler for 'drag'
- * @param event
- */
- handleDrag: function (event) {
- if (this.zoomFactor > 1.0) {
- var touch = this.getTouches(event)[0];
- this.drag(touch, this.lastDragPosition);
- this.offset = this.sanitizeOffset(this.offset);
- this.lastDragPosition = touch;
- }
- },
- handleDragEnd: function () {
- this.el.trigger(this.options.dragEndEventName);
- this.end();
- },
- /**
- * Event handler for 'zoomstart'
- * @param event
- */
- handleZoomStart: function (event) {
- this.el.trigger(this.options.zoomStartEventName);
- this.stopAnimation();
- this.lastScale = 1;
- this.nthZoom = 0;
- this.lastZoomCenter = false;
- this.hasInteraction = true;
- },
- /**
- * Event handler for 'zoom'
- * @param event
- */
- handleZoom: function (event, newScale) {
- // a relative scale factor is used
- var touchCenter = this.getTouchCenter(this.getTouches(event)),
- scale = newScale / this.lastScale;
- this.lastScale = newScale;
- // the first touch events are thrown away since they are not precise
- this.nthZoom += 1;
- if (this.nthZoom > 3) {
- this.scale(scale, touchCenter);
- this.drag(touchCenter, this.lastZoomCenter);
- }
- this.lastZoomCenter = touchCenter;
- },
- handleZoomEnd: function () {
- this.el.trigger(this.options.zoomEndEventName);
- this.end();
- },
- /**
- * Event handler for 'doubletap'
- * @param event
- */
- handleDoubleTap: function (event) {
- var center = this.getTouches(event)[0],
- zoomFactor = this.zoomFactor > 1 ? 1 : this.options.tapZoomFactor,
- startZoomFactor = this.zoomFactor,
- updateProgress = (function (progress) {
- this.scaleTo(startZoomFactor + progress * (zoomFactor - startZoomFactor), center);
- }).bind(this);
- if (this.hasInteraction) {
- return;
- }
- if (startZoomFactor > zoomFactor) {
- center = this.getCurrentZoomCenter();
- }
- this.animate(this.options.animationDuration, updateProgress, this.swing);
- this.el.trigger(this.options.doubleTapEventName);
- },
- /**
- * Max / min values for the offset
- * @param offset
- * @return {Object} the sanitized offset
- */
- sanitizeOffset: function (offset) {
- var maxX = (this.zoomFactor - 1) * this.getContainerX(),
- maxY = (this.zoomFactor - 1) * this.getContainerY(),
- maxOffsetX = Math.max(maxX, 0),
- maxOffsetY = Math.max(maxY, 0),
- minOffsetX = Math.min(maxX, 0),
- minOffsetY = Math.min(maxY, 0);
- return {
- x:/*Math.min(Math.max(offset.y, minOffsetY), maxOffsetY),*/ Math.min(Math.max(offset.x, minOffsetX), maxOffsetX),
- y:/*Math.min(Math.max(offset.x, minOffsetX), maxOffsetX)*/ Math.min(Math.max(offset.y, minOffsetY), maxOffsetY)
- };
- },
- /**
- * Scale to a specific zoom factor (not relative)
- * @param zoomFactor
- * @param center
- */
- scaleTo: function (zoomFactor, center) {
- this.scale(zoomFactor / this.zoomFactor, center);
- },
- /**
- * Scales the element from specified center
- * @param scale
- * @param center
- */
- scale: function (scale, center) {
- scale = this.scaleZoomFactor(scale);
- this.addOffset({
- x: (scale - 1) * (center.x + this.offset.x),
- y: (scale - 1) * (center.y + this.offset.y)
- });
- },
- /**
- * Scales the zoom factor relative to current state
- * @param scale
- * @return the actual scale (can differ because of max min zoom factor)
- */
- scaleZoomFactor: function (scale) {
- var originalZoomFactor = this.zoomFactor;
- this.zoomFactor *= scale;
- this.zoomFactor = Math.min(this.options.maxZoom, Math.max(this.zoomFactor, this.options.minZoom));
- return this.zoomFactor / originalZoomFactor;
- },
- /**
- * Drags the element
- * @param center
- * @param lastCenter
- */
- drag: function (center, lastCenter) {
- if (lastCenter) {
- if(this.options.lockDragAxis) {
- //alert(111)
- // lock scroll to position that was changed the most
- if(Math.abs(center.x - lastCenter.x) > Math.abs(center.y - lastCenter.y)) {
- this.addOffset({
- x: -(center.x - lastCenter.x),
- y: 0
- });
- }
- else {
-
- this.addOffset({
- y: -(center.y - lastCenter.y),
- x: 0
- });
- }
- }
- else {
- //alert(111)
- this.addOffset({
- y: (center.x - lastCenter.x),//-(center.y - lastCenter.y),
- x: -(center.y - lastCenter.y)//-(center.x - lastCenter.x)
- });
- }
- }
- },
- /**
- * Calculates the touch center of multiple touches
- * @param touches
- * @return {Object}
- */
- getTouchCenter: function (touches) {
- return this.getVectorAvg(touches);
- },
- /**
- * Calculates the average of multiple vectors (x, y values)
- */
- getVectorAvg: function (vectors) {
- return {
- x: vectors.map(function (v) { return v.x; }).reduce(sum) / vectors.length,
- y: vectors.map(function (v) { return v.y; }).reduce(sum) / vectors.length
- };
- },
- /**
- * Adds an offset
- * @param offset the offset to add
- * @return return true when the offset change was accepted
- */
- addOffset: function (offset) {
- this.offset = {
- x: this.offset.x + offset.x,
- y: this.offset.y + offset.y
- };
- },
- sanitize: function () {
- if (this.zoomFactor < this.options.zoomOutFactor) {
- this.zoomOutAnimation();
- } else if (this.isInsaneOffset(this.offset)) {
- this.sanitizeOffsetAnimation();
- }
- },
- /**
- * Checks if the offset is ok with the current zoom factor
- * @param offset
- * @return {Boolean}
- */
- isInsaneOffset: function (offset) {
- var sanitizedOffset = this.sanitizeOffset(offset);
- return sanitizedOffset.x !== offset.x ||
- sanitizedOffset.y !== offset.y;
- },
- /**
- * Creates an animation moving to a sane offset
- */
- sanitizeOffsetAnimation: function () {
- var targetOffset = this.sanitizeOffset(this.offset),
- startOffset = {
- x: this.offset.x,
- y: this.offset.y
- },
- updateProgress = (function (progress) {
- this.offset.x = startOffset.x + progress * (targetOffset.x - startOffset.x);
- this.offset.y = startOffset.y + progress * (targetOffset.y - startOffset.y);
- this.update();
- }).bind(this);
- this.animate(
- this.options.animationDuration,
- updateProgress,
- this.swing
- );
- },
- /**
- * Zooms back to the original position,
- * (no offset and zoom factor 1)
- */
- zoomOutAnimation: function () {
- var startZoomFactor = this.zoomFactor,
- zoomFactor = 1,
- center = this.getCurrentZoomCenter(),
- updateProgress = (function (progress) {
- this.scaleTo(startZoomFactor + progress * (zoomFactor - startZoomFactor), center);
- }).bind(this);
- this.animate(
- this.options.animationDuration,
- updateProgress,
- this.swing
- );
- },
- /**
- * Updates the aspect ratio
- */
- updateAspectRatio: function () {
- this.setContainerY(this.getContainerX() / this.getAspectRatio());
- },
- /**
- * Calculates the initial zoom factor (for the element to fit into the container)
- * @return the initial zoom factor
- */
- getInitialZoomFactor: function () {
- // use .offsetWidth instead of width()
- // because jQuery-width() return the original width but Zepto-width() will calculate width with transform.
- // the same as .height()
- return this.container[0].offsetWidth / this.el[0].offsetWidth;
- },
- /**
- * Calculates the aspect ratio of the element
- * @return the aspect ratio
- */
- getAspectRatio: function () {
- return this.el[0].offsetWidth / this.el[0].offsetHeight;
- },
- /**
- * Calculates the virtual zoom center for the current offset and zoom factor
- * (used for reverse zoom)
- * @return {Object} the current zoom center
- */
- getCurrentZoomCenter: function () {
- // uses following formula to calculate the zoom center x value
- // offset_left / offset_right = zoomcenter_x / (container_x - zoomcenter_x)
- var length = this.container[0].offsetWidth * this.zoomFactor,
- offsetLeft = this.offset.x,
- offsetRight = length - offsetLeft -this.container[0].offsetWidth,
- widthOffsetRatio = offsetLeft / offsetRight,
- centerX = widthOffsetRatio * this.container[0].offsetWidth / (widthOffsetRatio + 1),
- // the same for the zoomcenter y
- height = this.container[0].offsetHeight * this.zoomFactor,
- offsetTop = this.offset.y,
- offsetBottom = height - offsetTop - this.container[0].offsetHeight,
- heightOffsetRatio = offsetTop / offsetBottom,
- centerY = heightOffsetRatio * this.container[0].offsetHeight / (heightOffsetRatio + 1);
- // prevents division by zero
- if (offsetRight === 0) { centerX = this.container[0].offsetWidth; }
- if (offsetBottom === 0) { centerY = this.container[0].offsetHeight; }
- return {
- x: centerX,
- y: centerY
- };
- },
- canDrag: function () {
- return !isCloseTo(this.zoomFactor, 1);
- },
- /**
- * Returns the touches of an event relative to the container offset
- * @param event
- * @return array touches
- */
- getTouches: function (event) {
- var position = this.container.offset();
- return Array.prototype.slice.call(event.touches).map(function (touch) {
- return {
- x: touch.pageX - position.left,
- y: touch.pageY - position.top
- };
- });
- },
- /**
- * Animation loop
- * does not support simultaneous animations
- * @param duration
- * @param framefn
- * @param timefn
- * @param callback
- */
- animate: function (duration, framefn, timefn, callback) {
- var startTime = new Date().getTime(),
- renderFrame = (function () {
- if (!this.inAnimation) { return; }
- var frameTime = new Date().getTime() - startTime,
- progress = frameTime / duration;
- if (frameTime >= duration) {
- framefn(1);
- if (callback) {
- callback();
- }
- this.update();
- this.stopAnimation();
- this.update();
- } else {
- if (timefn) {
- progress = timefn(progress);
- }
- framefn(progress);
- this.update();
- requestAnimationFrame(renderFrame);
- }
- }).bind(this);
- this.inAnimation = true;
- requestAnimationFrame(renderFrame);
- },
- /**
- * Stops the animation
- */
- stopAnimation: function () {
- this.inAnimation = false;
- },
- /**
- * Swing timing function for animations
- * @param p
- * @return {Number}
- */
- swing: function (p) {
- return -Math.cos(p * Math.PI) / 2 + 0.5;
- },
- getContainerX: function () {
- return this.container[0].offsetWidth;
- },
- getContainerY: function () {
- return this.container[0].offsetHeight;
- },
- setContainerY: function (y) {
- return this.container.height(y);
- },
- /**
- * Creates the expected html structure
- */
- setupMarkup: function () {
- this.container = $('<div class="pinch-zoom-container"></div>');
- this.el.before(this.container);
- this.container.append(this.el);
- this.container.css({
- 'overflow': 'hidden',
- 'position': 'relative'
- });
- // Zepto doesn't recognize `webkitTransform..` style
- this.el.css({
- '-webkit-transform-origin': '0% 0%',
- '-moz-transform-origin': '0% 0%',
- '-ms-transform-origin': '0% 0%',
- '-o-transform-origin': '0% 0%',
- 'transform-origin': '0% 0%',
- 'position': 'absolute'
- });
- },
- end: function () {
- this.hasInteraction = false;
- this.sanitize();
- this.update();
- },
- /**
- * Binds all required event listeners
- */
- bindEvents: function () {
- detectGestures(this.container.get(0), this);
- // Zepto and jQuery both know about `on`
- $(window).on('resize', this.update.bind(this));
- $(this.el).find('img').on('load', this.update.bind(this));
- },
- /**
- * Updates the css values according to the current zoom factor and offset
- */
- update: function () {
- if (this.updatePlaned) {
- return;
- }
- this.updatePlaned = true;
- setTimeout((function () {
- this.updatePlaned = false;
- this.updateAspectRatio();
- var zoomFactor = this.getInitialZoomFactor() * this.zoomFactor,
- offsetX = -this.offset.x / zoomFactor,
- offsetY = -this.offset.y / zoomFactor,
- transform3d = 'scale3d(' + zoomFactor + ', ' + zoomFactor + ',1) ' +
- 'translate3d(' + offsetX + 'px,' + offsetY + 'px,0px)',
- transform2d = 'scale(' + zoomFactor + ', ' + zoomFactor + ') ' +
- 'translate(' + offsetX + 'px,' + offsetY + 'px)',
- removeClone = (function () {
- if (this.clone) {
- this.clone.remove();
- delete this.clone;
- }
- }).bind(this);
- // Scale 3d and translate3d are faster (at least on ios)
- // but they also reduce the quality.
- // PinchZoom uses the 3d transformations during interactions
- // after interactions it falls back to 2d transformations
- if (!this.options.use2d || this.hasInteraction || this.inAnimation) {
- this.is3d = true;
- removeClone();
- this.el.css({
- '-webkit-transform': transform3d,
- '-o-transform': transform2d,
- '-ms-transform': transform2d,
- '-moz-transform': transform2d,
- 'transform': transform3d
- });
- } else {
- // When changing from 3d to 2d transform webkit has some glitches.
- // To avoid this, a copy of the 3d transformed element is displayed in the
- // foreground while the element is converted from 3d to 2d transform
- if (this.is3d) {
- this.clone = this.el.clone();
- this.clone.css('pointer-events', 'none');
- this.clone.appendTo(this.container);
- setTimeout(removeClone, 200);
- }
- this.el.css({
- '-webkit-transform': transform2d,
- '-o-transform': transform2d,
- '-ms-transform': transform2d,
- '-moz-transform': transform2d,
- 'transform': transform2d
- });
- this.is3d = false;
- }
- }).bind(this), 0);
- },
- /**
- * Enables event handling for gestures
- */
- enable: function() {
- this.enabled = true;
- },
- /**
- * Disables event handling for gestures
- */
- disable: function() {
- this.enabled = false;
- }
- };
- var detectGestures = function (el, target) {
- var interaction = null,
- fingers = 0,
- lastTouchStart = null,
- startTouches = null,
- setInteraction = function (newInteraction, event) {
- if (interaction !== newInteraction) {
- if (interaction && !newInteraction) {
- switch (interaction) {
- case "zoom":
- target.handleZoomEnd(event);
- break;
- case 'drag':
- target.handleDragEnd(event);
- break;
- }
- }
- switch (newInteraction) {
- case 'zoom':
- target.handleZoomStart(event);
- break;
- case 'drag':
- target.handleDragStart(event);
- break;
- }
- }
- interaction = newInteraction;
- },
- updateInteraction = function (event) {
- if (fingers === 2) {
- setInteraction('zoom');
- } else if (fingers === 1 && target.canDrag()) {
- setInteraction('drag', event);
- } else {
- setInteraction(null, event);
- }
- },
- targetTouches = function (touches) {
- return Array.prototype.slice.call(touches).map(function (touch) {
- return {
- x: touch.pageX,
- y: touch.pageY
- };
- });
- },
- getDistance = function (a, b) {
- var x, y;
- x = a.x - b.x;
- y = a.y - b.y;
- return Math.sqrt(x * x + y * y);
- },
- calculateScale = function (startTouches, endTouches) {
- var startDistance = getDistance(startTouches[0], startTouches[1]),
- endDistance = getDistance(endTouches[0], endTouches[1]);
- return endDistance / startDistance;
- },
- cancelEvent = function (event) {
- event.stopPropagation();
- event.preventDefault();
- },
- detectDoubleTap = function (event) {
- var time = (new Date()).getTime();
- if (fingers > 1) {
- lastTouchStart = null;
- }
- if (time - lastTouchStart < 300) {
- cancelEvent(event);
- target.handleDoubleTap(event);
- switch (interaction) {
- case "zoom":
- target.handleZoomEnd(event);
- break;
- case 'drag':
- target.handleDragEnd(event);
- break;
- }
- }
- if (fingers === 1) {
- lastTouchStart = time;
- }
- },
- firstMove = true;
- el.addEventListener('touchstart', function (event) {
- if(target.enabled) {
- firstMove = true;
- fingers = event.touches.length;
- detectDoubleTap(event);
- }
- });
- el.addEventListener('touchmove', function (event) {
- if(target.enabled) {
- if (firstMove) {
- updateInteraction(event);
- if (interaction) {
- cancelEvent(event);
- }
- startTouches = targetTouches(event.touches);
- } else {
- switch (interaction) {
- case 'zoom':
- target.handleZoom(event, calculateScale(startTouches, targetTouches(event.touches)));
- break;
- case 'drag':
- target.handleDrag(event);
- break;
- }
- if (interaction) {
- cancelEvent(event);
- target.update();
- }
- }
- firstMove = false;
- }
- });
- el.addEventListener('touchend', function (event) {
- if(target.enabled) {
- fingers = event.touches.length;
- updateInteraction(event);
- }
- });
- };
- return PinchZoom;
- };
- if (typeof define !== 'undefined' && define.amd) {
- define(['jquery'], function ($) {
- return definePinchZoom($);
- });
- } else {
- window.RTP = window.RTP || {};
- window.RTP.PinchZoom = definePinchZoom(window.$);
- }
- }).call(this);
|