;(function ($, window) { "use strict"; var $window = $(window), $instances, animating = false, transformSupported = false; /** * @options * @param callback [function] <$.noop> "" * @param controls.postion [string] <"bottom"> "Position of default controls" * @param controls.zoomIn [string] <> "Custom zoom control selector" * @param controls.zoomOut [string] <> "Custom zoom control selector" * @param controls.next [string] <> "Custom pagination control selector" * @param controls.previous [string] <> "Custom pagination control selector" * @param customClass [string] <''> "Class applied to instance" * @param enertia [number] <0.2> "Zoom smoothing (0.1 = butter, 0.9 = sandpaper)" * @param increment [number] <0.01> "Zoom speed (0.01 = tortoise, 0.1 = hare)" * @param marginMin [] <> "" * @param marginMax [] <> "" * @param retina [boolean] "Flag for retina image support" * @param source [string | object] "Source image (string) or tiles (object)" */ var options = { callback: $.noop, controls: { position: "bottom", zoomIn: null, zoomOut: null, next: null, previous: null }, customClass: "", enertia: 0.2, increment: 0.01, marginMin: 30, // Min bounds marginMax: 100, // Max bounds retina: false, source: null }; // Internal data var properties = { images: [], aspect: "", action: "", lastAction: "", keyDownTime: 0, marginReal: 0, originalDOM: "", // Gallery gallery: false, index: 0, // Tiles $tiles: null, tiled: false, tilesTotal: 0, tilesLoaded: 0, tiledColumns: 0, tiledRows: 0, tiledHeight: 0, tiledWidth: 0, tiledThumbnail: null, // Frame centerLeft: 0, centerTop: 0, frameHeight: 0, frameWidth: 0, // Original image naturalHeight: 0, naturalWidth: 0, imageRatioWide: 0, imageRatioTall: 0, // Dimensions minHeight: null, minWidth: null, maxHeight: 0, maxWidth: 0, // Bounds boundsTop: 0, boundsBottom: 0, boundsLeft: 0, boundsRight: 0, // Image imageWidth: 0, imageHeight: 0, imageLeft: 0, imageTop: 0, targetImageWidth: 0, targetImageHeight: 0, targetImageLeft: 0, targetImageTop: 0, oldImageWidth: 0, oldImageHeight: 0, // Positioner positionerLeft: 0, positionerTop: 0, targetPositionerLeft: 0, targetPositionerTop: 0, // Zoom zoomPositionLeft: 0, zoomPositionTop: 0, // Touch Support offset: null, touches: [], zoomPercentage: 1, pinchStartX0: 0, pinchStartX1: 0, pinchStartY0: 0, pinchStartY1: 0, pinchEndX0: 0, pinchEndX1: 0, pinchEndY0: 0, pinchEndY1: 0, lastPinchEndX0: 0, lastPinchEndY0: 0, lastPinchEndX1: 0, lastPinchEndY1: 0, pinchDeltaStart: 0, pinchDeltaEnd: 0 }; /** * @events * @event zoomer.loaded "Source media loaded" */ var pub = { /** * @method * @name defaults * @description Sets default plugin options * @param opts [object] <{}> "Options object" * @example $.zoomer("defaults", opts); */ defaults: function(opts) { options = $.extend(options, opts || {}); return (typeof this === 'object') ? $(this) : true; }, /** * @method * @name destroy * @description Removes instance of plugin * @example $(".target").zoomer("destroy"); */ destroy: function() { var $targets = $(this).each(function(i, target) { var data = $(target).data("zoomer"); if (data) { $window.off(".zoomer"); data.$holder.off(".zoomer"); data.$zoomer.off(".zoomer"); data.controls.$zoomIn.off(".zoomer"); data.controls.$zoomOut.off(".zoomer"); data.controls.$next.off(".zoomer"); data.controls.$previous.off(".zoomer"); data.$target.removeClass("zoomer-element") .data("zoomer", null) .empty() .append(data.originalDOM); } }); $instances = $(".zoomer-element"); if ($instances.length < 1) { _clearAnimation(); } return $targets; }, /** * @method * @name load * @description Loads source media * @param source [string | object] "Source image (string) or tiles (object)" * @example $(".target").zoomer("load", "path/to/image.jpg"); */ load: function(source) { return $(this).each(function(i, target) { var data = $(target).data("zoomer"); if (data) { data.source = source; data.index = 0; data = _normalizeSource(data); _load(data); } }); }, /** * @method * @name pan * @description Pans plugin instances * @param left [int] "Percentage to pan to (50 = half)" * @param top [int] "Percentage to pan to (50 = half)" * @example $(".target").zoomer("pan", 50, 50); */ pan: function(left, top) { return $(this).each(function(i, target) { var data = $(target).data("zoomer"); if (data) { left /= 100; top /= 100; data.targetPositionerLeft = Math.round(data.centerLeft - data.targetImageLeft - (data.targetImageWidth * left)); data.targetPositionerTop = Math.round(data.centerTop - data.targetImageTop - (data.targetImageHeight * top)); } }); }, /** * @method * @name resize * @description Resizes plugin instange * @example $(".target").zoomer("resize"); */ resize: function() { return $(this).each(function(i, target) { var data = $(target).data("zoomer"); if (data) { data.frameWidth = data.$target.outerWidth(); data.frameHeight = data.$target.outerHeight(); data.centerLeft = Math.round(data.frameWidth * 0.5); data.centerTop = Math.round(data.frameHeight * 0.5); // Set minHeight and minWidth to naturals sizes data.minHeight = data.naturalHeight; data.minWidth = data.naturalWidth; // Recalculate minimum sizes only when the natural size of the image is bigger than the frame size - marginalReal if (data.naturalHeight > (data.frameHeight - data.marginReal) || data.naturalWidth > (data.frameWidth - data.marginReal)) { data = _setMinimums(data); } } }); }, /** * @method * @name unload * @description Unload image from plugins instances * @example $(".target").zoomer("unload"); */ unload: function() { return $(this).each(function() { var data = $(this).data("zoomer"); if (data && typeof data.$image !== 'undefined') { data.$image.remove(); } }); } }; /** * @method private * @name _init * @description Initializes plugin * @param opts [object] "Initialization options" */ function _init(opts) { // Settings opts = $.extend({}, options, properties, opts); transformSupported = _getTransform3DSupport(); // Apply to each element var $items = $(this); for (var i = 0, count = $items.length; i < count; i++) { _build($items.eq(i), opts); } // Start main animation loop $instances = $(".zoomer-element"); _startAnimation(); return $items; } /** * @method private * @name _build * @description Builds each instance * @param $target [jQuery object] "Target jQuery object" * @param data [object] <{}> "Options object" */ function _build($target, data) { if (!$target.data("zoomer")) { data = $.extend({}, data, $target.data("zoomer-options")); var $targetParent = $target.parent(); // have to get the parent before we empty the target below data.$target = $target; data.marginReal = data.marginMin * 2; data.originalDOM = data.$target.html(); if (data.$target.find("img").length > 0) { data.source = []; data.$target.find("img").each(function() { data.source.push($(this).attr("src")); }); data.$target.empty(); } data = _normalizeSource(data); // Assemble HTML var html = '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; data.$zoomer = $(html); data.$target.addClass("zoomer-element") .html(data.$zoomer); if (data.controls.zoomIn || data.controls.zoomOut || data.controls.next || data.controls.previous) { data.controls.$zoomIn = $(data.controls.zoomIn, $targetParent); data.controls.$zoomOut = $(data.controls.zoomOut, $targetParent); data.controls.$next = $(data.controls.next, $targetParent); data.controls.$previous = $(data.controls.previous, $targetParent); } else { html = '
'; html += ''; html += '-'; html += '+'; html += ''; html += '
'; data.$zoomer.append(html); data.controls.$default = data.$zoomer.find(".zoomer-controls"); data.controls.$zoomIn = data.$zoomer.find(".zoomer-zoom-in"); data.controls.$zoomOut = data.$zoomer.find(".zoomer-zoom-out"); data.controls.$next = data.$zoomer.find(".zoomer-next"); data.controls.$previous = data.$zoomer.find(".zoomer-previous"); } // Cache jquery objects data.$positioner = data.$zoomer.find(".zoomer-positioner"); data.$holder = data.$zoomer.find(".zoomer-holder"); // Bind events data.controls.$zoomIn.on("touchstart.zoomer mousedown.zoomer", data, _zoomIn) .on("touchend.zoomer mouseup.zoomer", data, _clearZoom); data.controls.$zoomOut.on("touchstart.zoomer mousedown.zoomer", data, _zoomOut) .on("touchend.zoomer mouseup.zoomer", data, _clearZoom); data.controls.$next.on("click.zoomer", data, _nextImage); data.controls.$previous.on("click.zoomer", data, _previousImage); data.$zoomer.on("mousedown.zoomer", data, _dragStart) .on("touchstart.zoomer MSPointerDown.zoomer", ":not(.zoomer-controls)", data, _onTouch); // Kick it off data.$target.data("zoomer", data); pub.resize.apply(data.$target); if (data.images.length > 0) { _load.apply(data.$target, [ data ]); } } } /** * @method private * @name _load * @description Delegates loading action * @param data [object] "Instance data" */ function _load(data) { // If gallery if (data.gallery) { data.$zoomer.addClass("zoomer-gallery"); } else { data.$zoomer.removeClass("zoomer-gallery"); } if (typeof data.$image !== "undefined") { data.$holder.animate({ opacity: 0 }, 300, function() { pub.unload.apply(data.$target); _loadImage.apply(data.$target, [ data, data.images[data.index] ]); }); } else { _loadImage.apply(data.$target, [ data, data.images[data.index] ]); } } /** * @method private * @name _loadImage * @description Handles loading an image or set of tiles * @param data [object] "Instance data" * @param source [string | object] "Source URL or object" */ function _loadImage(data, source) { data.loading = true; if (data.tiled) { data.tilesTotal = 0; data.tilesLoaded = 0; var html = '
'; for (var i in data.images[0]) { if (data.images[0].hasOwnProperty(i)) { for (var j in data.images[0][i]) { if (data.images[0][i].hasOwnProperty(j)) { html += ''; data.tilesTotal++; } } } } html += '
'; data.$image = $(html); data.$tiles = data.$image.find("img"); data.$tiles.each(function(i, img) { var $img = $(img); $img.one("load", data, _onTileLoad); if ($img[0].complete) { $img.trigger("load"); } }); } else { // Cache current image data.$image = $(''); data.$image.one("load.zoomer", data, _onImageLoad) .attr("src", source); // If image has already loaded into cache, trigger load event if (data.$image[0].complete) { data.$image.trigger("load"); } } } /** * @method private * @name _onTileLoad * @description Handles tile load * @param e [object] "Event data" */ function _onTileLoad(e) { var data = e.data; data.tilesLoaded++; if (data.tilesLoaded === data.tilesTotal) { data.tiledRows = data.images[0].length; data.tiledColumns = data.images[0][0].length; data.tiledHeight = data.$tiles.eq(0)[0].naturalHeight * data.tiledRows; data.tiledWidth = data.$tiles.eq(0)[0].naturalWidth * data.tiledColumns; _onImageLoad({ data: data }); } } /** * @method private * @name _onImageLoad * @description Handles image load * @param e [object] "Event data" */ function _onImageLoad(e) { var data = e.data; if (data.tiled) { data.naturalHeight = data.tiledHeight; data.naturalWidth = data.tiledWidth; } else { data.naturalHeight = data.$image[0].naturalHeight; data.naturalWidth = data.$image[0].naturalWidth; } if (data.retina) { data.naturalHeight /= 2; data.naturalWidth /= 2; } data.$holder.css({ height: data.naturalHeight, width: data.naturalWidth }); // Set target, min, max to naturals sizes data.targetImageHeight = data.minHeight = data.maxHeight = data.naturalHeight; data.targetImageWidth = data.minWidth = data.maxWidth = data.naturalWidth; data.imageRatioWide = data.naturalWidth / data.naturalHeight; data.imageRatioTall = data.naturalHeight / data.naturalWidth; // Initial sizing to fit screen if (data.naturalHeight > (data.frameHeight - data.marginReal) || data.naturalWidth > (data.frameWidth - data.marginReal)) { data = _setMinimums(data); data.targetImageHeight = data.minHeight; data.targetImageWidth = data.minWidth; } // SET INITIAL POSITIONS data.positionerLeft = data.targetPositionerLeft = data.centerLeft; data.positionerTop = data.targetPositionerTop = data.centerTop; data.imageLeft = data.targetImageLeft = Math.round(-data.targetImageWidth / 2); data.imageTop = data.targetImageTop = Math.round(-data.targetImageHeight / 2); data.imageHeight = data.targetImageHeight; data.imageWidth = data.targetImageWidth; if (transformSupported) { var scaleX = data.imageWidth / data.naturalWidth, scaleY = data.imageHeight / data.naturalHeight; data.$positioner.css( _prefix("transform", "translate3d("+data.positionerLeft+"px, "+data.positionerTop+"px, 0)") ); data.$holder.css( _prefix("transform", "translate3d(-50%, -50%, 0) scale("+scaleX+","+scaleY+")") ); } else { data.$positioner.css({ left: data.positionerLeft, top: data.positionerTop }); data.$holder.css({ left: data.imageLeft, top: data.imageTop, height: data.imageHeight, width: data.imageWidth }); } data.$holder.append(data.$image); if (data.tiled) { data.$holder.css({ background: "url(" + data.tiledThumbnail + ") no-repeat left top", backgroundSize: "100% 100%" }); data.tileHeightPercentage = 100 / data.tiledRows; data.tileWidthPercentage = 100 / data.tiledColumns; data.$tiles.css({ height: data.tileHeightPercentage + "%", width: data.tileWidthPercentage + "%" }); data.$tiles.each(function(i, tile) { var $tile = $(tile), position = $tile.data("zoomer-tile").split("-"); $tile.css({ left: (data.tileWidthPercentage * parseInt(position[1], 10)) + "%", top: (data.tileHeightPercentage * parseInt(position[0], 10)) + "%" }); }); } data.$holder.animate({ opacity: 1 }, 300); data.loading = false; // Start preloading if (data.gallery) { _preloadGallery(data); } } /** * @method private * @name _preloadGallery * @description Preloads previous and next images in gallery for faster rendering * @param data [object] "Instance Data" */ function _preloadGallery(data) { if (data.index > 0) { $(''); } if (data.index < data.images.length - 1) { $(''); } } /** * @method private * @name _setMinimums * @description Sets minimum dimensions * @param data [object] "Instance Data" */ function _setMinimums(data) { if (data.naturalHeight > data.naturalWidth) { // Tall data.aspect = "tall"; data.minHeight = Math.round(data.frameHeight - data.marginReal); data.minWidth = Math.round(data.minHeight / data.imageRatioTall); if (data.minWidth > (data.frameWidth - data.marginReal)) { data.minWidth = Math.round(data.frameWidth - data.marginReal); data.minHeight = Math.round(data.minWidth / data.imageRatioWide); } } else { // Wide data.aspect = "wide"; data.minWidth = Math.round(data.frameWidth - data.marginReal); data.minHeight = Math.round(data.minWidth / data.imageRatioWide); if (data.minHeight > (data.frameHeight - data.marginReal)) { data.minHeight = Math.round(data.frameHeight - data.marginReal); data.minWidth = Math.round(data.minHeight / data.imageRatioTall); } } return data; } /** * @method private * @name _render * @description Main animation loop */ function _render() { for (var i = 0, count = $instances.length; i < count; i++) { var data = $instances.eq(i).data("zoomer"); if (typeof data === "object") { // Update image and position values data = _updateValues(data); data.lastAction = data.action; // Update DOM if (transformSupported) { var scaleX = data.imageWidth / data.naturalWidth, scaleY = data.imageHeight / data.naturalHeight; data.$positioner.css(_prefix("transform", "translate3d(" + data.positionerLeft + "px, " + data.positionerTop + "px, 0)")); data.$holder.css(_prefix("transform", "translate3d(-50%, -50%, 0) scale(" + scaleX + "," + scaleY + ")")); } else { data.$positioner.css({ left: data.positionerLeft, top: data.positionerTop }); data.$holder.css({ left: data.imageLeft, top: data.imageTop, width: data.imageWidth, height: data.imageHeight }); } // Run callback function if (data.callback) { data.callback.apply(data.$zoomer, [ (data.imageWidth - data.minWidth) / (data.maxWidth - data.minWidth) ]); } } } } /** * @method private * @name _updateValues * @description Updates current image values * @param data [object] "Instance Data" */ function _updateValues(data) { // Update values based on current action if (data.action === "zoom_in" || data.action === "zoom_out") { // Calculate change data.keyDownTime += data.increment; var delta = ((data.action === "zoom_out") ? -1 : 1) * Math.round((data.imageWidth * data.keyDownTime) - data.imageWidth); if (data.aspect === "tall") { data.targetImageHeight += delta; data.targetImageWidth = Math.round(data.targetImageHeight / data.imageRatioTall); } else { data.targetImageWidth += delta; data.targetImageHeight = Math.round(data.targetImageWidth / data.imageRatioWide); } } // Check Max and Min image values; recenter if too small if (data.aspect === "tall") { if (data.targetImageHeight < data.minHeight) { data.targetImageHeight = data.minHeight; data.targetImageWidth = Math.round(data.targetImageHeight / data.imageRatioTall); } else if (data.targetImageHeight > data.maxHeight) { data.targetImageHeight = data.maxHeight; data.targetImageWidth = Math.round(data.targetImageHeight / data.imageRatioTall); } } else { if (data.targetImageWidth < data.minWidth) { data.targetImageWidth = data.minWidth; data.targetImageHeight = Math.round(data.targetImageWidth / data.imageRatioWide); } else if (data.targetImageWidth > data.maxWidth) { data.targetImageWidth = data.maxWidth; data.targetImageHeight = Math.round(data.targetImageWidth / data.imageRatioWide); } } // Calculate new dimensions data.targetImageLeft = Math.round(-data.targetImageWidth * 0.5); data.targetImageTop = Math.round(-data.targetImageHeight * 0.5); if (data.action === "drag" || data.action === "pinch") { data.imageWidth = data.targetImageWidth; data.imageHeight = data.targetImageHeight; data.imageLeft = data.targetImageLeft; data.imageTop = data.targetImageTop; } else { data.imageWidth += Math.round((data.targetImageWidth - data.imageWidth) * data.enertia); data.imageHeight += Math.round((data.targetImageHeight - data.imageHeight) * data.enertia); data.imageLeft += Math.round((data.targetImageLeft - data.imageLeft) * data.enertia); data.imageTop += Math.round((data.targetImageTop - data.imageTop) * data.enertia); } // Check bounds of current position and if big enough to drag // Set bounds data.boundsLeft = Math.round(data.frameWidth - (data.targetImageWidth * 0.5) - data.marginMax); data.boundsRight = Math.round((data.targetImageWidth * 0.5) + data.marginMax); data.boundsTop = Math.round(data.frameHeight - (data.targetImageHeight * 0.5) - data.marginMax); data.boundsBottom = Math.round((data.targetImageHeight * 0.5) + data.marginMax); // Check dragging bounds if (data.targetPositionerLeft < data.boundsLeft) { data.targetPositionerLeft = data.boundsLeft; } if (data.targetPositionerLeft > data.boundsRight) { data.targetPositionerLeft = data.boundsRight; } if (data.targetPositionerTop < data.boundsTop) { data.targetPositionerTop = data.boundsTop; } if (data.targetPositionerTop > data.boundsBottom) { data.targetPositionerTop = data.boundsBottom; } // Zoom to visible area of image if (data.zoomPositionTop > 0 && data.zoomPositionLeft > 0) { data.targetPositionerLeft = data.centerLeft - data.targetImageLeft - (data.targetImageWidth * data.zoomPositionLeft); data.targetPositionerTop = data.centerTop - data.targetImageTop - (data.targetImageHeight * data.zoomPositionTop); } if (data.action !== "pinch") { // Recenter when small enough if (data.targetImageWidth < data.frameWidth) { data.targetPositionerLeft = data.centerLeft; } if (data.targetImageHeight < data.frameHeight) { data.targetPositionerTop = data.centerTop; } } // Calculate new positions if (data.action === "drag" || data.action === "pinch") { data.positionerLeft = data.targetPositionerLeft; data.positionerTop = data.targetPositionerTop; } else { data.positionerLeft += Math.round((data.targetPositionerLeft - data.positionerLeft) * data.enertia); data.positionerTop += Math.round((data.targetPositionerTop - data.positionerTop) * data.enertia); } data.oldImageWidth = data.imageWidth; data.oldImageHeight = data.imageHeight; return data; } /** * @method private * @name _nextImage * @description Handles next button click * @param e [object] "Event Data" */ function _nextImage(e) { var data = e.data; if (!data.loading && data.index+1 < data.images.length) { data.index++; _load.apply(data.$target, [ data ]); } } /** * @method private * @name _previousImage * @description Handles previous button click * @param e [object] "Event Data" */ function _previousImage(e) { var data = e.data; if (!data.loading && data.index-1 >= 0) { data.index--; _load.apply(data.$target, [ data ]); } } /** * @method private * @name _zoomIn * @description Handles zoom in button click * @param e [object] "Event Data" */ function _zoomIn(e) { e.preventDefault(); e.stopPropagation(); var data = e.data; data = _setZoomPosition(data); data.keyDownTime = 1; data.action = "zoom_in"; } /** * @method private * @name _zoomOut * @description Handles zoom out button click * @param e [object] "Event Data" */ function _zoomOut(e) { e.preventDefault(); e.stopPropagation(); var data = e.data; data = _setZoomPosition(data); data.keyDownTime = 1; data.action = "zoom_out"; } /** * @method private * @name _clearZoom * @description Clears current zoom action * @param e [object] "Event Data" */ function _clearZoom(e) { e.preventDefault(); e.stopPropagation(); var data = e.data; data = _clearZoomPosition(data); data.keyDownTime = 0; data.action = ""; } /** * @method private * @name _setZoomPosition * @description Sets zoom position * @param data [object] "Instance Data" * @param left [number] "Left position" * @param top [number] "Top position" */ function _setZoomPosition(data, left, top) { left = left || (data.imageWidth * 0.5); top = top || (data.imageHeight * 0.5); data.zoomPositionLeft = ((left - (data.positionerLeft - data.centerLeft)) / data.imageWidth); data.zoomPositionTop = ((top - (data.positionerTop - data.centerTop)) / data.imageHeight); return data; } /** * @method private * @name _clearZoomPosition * @description Clears zoom position * @param data [object] "Instance Data" */ function _clearZoomPosition(data) { data.zoomPositionTop = 0; data.zoomPositionLeft = 0; return data; } /** * @method private * @name _dragStart * @description Handles drag start * @param e [object] "Event Data" */ function _dragStart(e) { if (e.preventDefault) { e.preventDefault(); e.stopPropagation(); } var data = e.data; data.action = "drag"; data.mouseX = e.pageX; data.mouseY = e.pageY; data.targetPositionerLeft = data.positionerLeft; data.targetPositionerTop = data.positionerTop; $window.on("mousemove.zoomer", data, _onDrag) .on("mouseup.zoomer", data, _dragStop); } /** * @method private * @name _onDrag * @description Handles dragging * @param e [object] "Event Data" */ function _onDrag(e) { if (e.preventDefault) { e.preventDefault(); e.stopPropagation(); } var data = e.data; if (e.pageX && e.pageY) { data.targetPositionerLeft -= Math.round(data.mouseX - e.pageX); data.targetPositionerTop -= Math.round(data.mouseY - e.pageY); data.mouseX = e.pageX; data.mouseY = e.pageY; } } /** * @method private * @name _dragStop * @description Handles drag end * @param e [object] "Event Data" */ function _dragStop(e) { if (e.preventDefault) { e.preventDefault(); e.stopPropagation(); } var data = e.data; data.action = ""; $window.off("mousemove.zoomer mouseup.zoomer"); } /** * @method private * @name _onTouch * @description Delegates touch event * @param e [object] "Event Data" */ function _onTouch(e) { if ($(e.target).parent(".zoomer-controls").length > 0) { return; } // Stop ms panning and zooming if (e.preventManipulation) { e.preventManipulation(); } e.preventDefault(); e.stopPropagation(); var data = e.data, oe = e.originalEvent; if (oe.type.match(/(up|end)$/i)) { _onTouchEnd(data, oe); return; } if (oe.pointerId) { // Normalize MS pointer events back to standard touches var activeTouch = false; for (var i in data.touches) { if (data.touches[i].identifier === oe.pointerId) { activeTouch = true; data.touches[i].pageX = oe.clientX; data.touches[i].pageY = oe.clientY; } } if (!activeTouch) { data.touches.push({ identifier: oe.pointerId, pageX: oe.clientX, pageY: oe.clientY }); } } else { // Alias normal touches data.touches = oe.touches; } // Delegate touch actions if (oe.type.match(/(down|start)$/i)) { _onTouchStart(data); } else if (oe.type.match(/move$/i)) { _onTouchMove(data); } } /** * @method private * @name _onTouchStart * @description Handles touch start * @param data [object] "Instance Data" */ function _onTouchStart(data) { // Touch events if (!data.touchEventsBound) { data.touchEventsBound = true; $window.on("touchmove.zoomer MSPointerMove.zoomer", data, _onTouch) .on("touchend.zoomer MSPointerUp.zoomer", data, _onTouch); } data.zoomPercentage = 1; if (data.touches.length >= 2) { data.offset = data.$zoomer.offset(); // Double touch - zoom data.pinchStartX0 = data.touches[0].pageX - data.offset.left; data.pinchStartY0 = data.touches[0].pageY - data.offset.top; data.pinchStartX1 = data.touches[1].pageX - data.offset.left; data.pinchStartY1 = data.touches[1].pageY - data.offset.top; data.pinchStartX = ((data.pinchStartX0 + data.pinchStartX1) / 2.0); data.pinchStartY = ((data.pinchStartY0 + data.pinchStartY1) / 2.0); data.imageWidthStart = data.imageWidth; data.imageHeightStart = data.imageHeight; _setZoomPosition(data); data.pinchDeltaStart = Math.sqrt(Math.pow((data.pinchStartX1 - data.pinchStartX0), 2) + Math.pow((data.pinchStartY1 - data.pinchStartY0), 2)); } data.mouseX = data.touches[0].pageX; data.mouseY = data.touches[0].pageY; } /** * @method private * @name _onTouchMove * @description Handles touch move * @param data [object] "Instance Data" */ function _onTouchMove(data) { if (data.touches.length === 1) { data.action = "drag"; data.targetPositionerLeft -= (data.mouseX - data.touches[0].pageX); data.targetPositionerTop -= (data.mouseY - data.touches[0].pageY); } else if (data.touches.length >= 2) { data.action = "pinch"; data.pinchEndX0 = data.touches[0].pageX - data.offset.left; data.pinchEndY0 = data.touches[0].pageY - data.offset.top; data.pinchEndX1 = data.touches[1].pageX - data.offset.left; data.pinchEndY1 = data.touches[1].pageY - data.offset.top; // Double touch - zoom // Only if we've actually move our touches if (data.pinchEndX0 !== data.lastPinchEndX0 || data.pinchEndY0 !== data.lastPinchEndY0 || data.pinchEndX1 !== data.lastPinchEndX1 || data.pinchEndY1 !== data.lastPinchEndY1) { data.pinchDeltaEnd = Math.sqrt(Math.pow((data.pinchEndX1 - data.pinchEndX0), 2) + Math.pow((data.pinchEndY1 - data.pinchEndY0), 2)); data.zoomPercentage = (data.pinchDeltaEnd / data.pinchDeltaStart); data.targetImageWidth = Math.round(data.imageWidthStart * data.zoomPercentage); data.targetImageHeight = Math.round(data.imageHeightStart * data.zoomPercentage); data.pinchEndX = ((data.pinchEndX0 + data.pinchEndX1) / 2.0); data.pinchEndY = ((data.pinchEndY0 + data.pinchEndY1) / 2.0); data.lastPinchEndX0 = data.pinchEndX0; data.lastPinchEndY0 = data.pinchEndY0; data.lastPinchEndX1 = data.pinchEndX1; data.lastPinchEndY1 = data.pinchEndY1; } } data.mouseX = data.touches[0].pageX; data.mouseY = data.touches[0].pageY; } /** * @method private * @name _onTouchEnd * @description Handles touch end * @param data [object] "Instance Data" */ function _onTouchEnd(data, oe) { data.action = ""; data.lastPinchEndX0 = data.pinchEndX0 = data.pinchStartX0 = 0; data.lastPinchEndY0 = data.pinchEndY0 = data.pinchStartY0 = 0; data.lastPinchEndX1 = data.pinchEndX1 = data.pinchStartX1 = 0; data.lastPinchEndY1 = data.pinchEndY1 = data.pinchStartY1 = 0; data.pinchStartX = data.pinchEndX = 0; data.pinchStartY = data.pinchEndX = 0; _clearZoomPosition(data); if (oe.pointerId) { for (var i in data.touches) { if (data.touches[i].identifier === oe.pointerId) { data.touches.splice(i, 1); } } } // Clear touch events /* if (data.touches.length <= 1) { */ $window.off(".zoomer"); data.touchEventsBound = false; /* } else { data.mouseX = data.touches[0].pageX; data.mouseY = data.touches[0].pageY; } */ } /** * @method private * @name _normalizeSource * @description Normalizes source string or object * @param data [object] "Instance Data" */ function _normalizeSource(data) { data.tiled = false; data.gallery = false; if (typeof data.source === "string") { data.images = [data.source]; } else { if (typeof data.source[0] === "string") { data.images = data.source; if (data.images.length > 1) { data.gallery = true; } } else { data.tiledThumbnail = data.source.thumbnail; data.images = [data.source.tiles]; data.tiled = true; } } return data; } /** * @method private * @name _startAnimation * @description Starts main animation loop */ function _startAnimation() { if (!animating) { animating = true; _onAnimate(); } } /** * @method private * @name _clearAnimation * @description End main animation loop */ function _clearAnimation() { animating = false; } /** * @method private * @name _onAnimate * @description Handles RAF */ function _onAnimate() { if (animating) { window.requestAnimationFrame(_onAnimate); _render(); } } /** * @method private * @name _prefix * @description Builds vendor-prefixed styles * @param property [string] "Property to prefix" * @param value [string] "Property value" * @return [string] "Vendor-prefixed style" */ function _prefix(property, value) { var r = {}; r["-webkit-" + property] = value; r[ "-moz-" + property] = value; r[ "-ms-" + property] = value; r[ "-o-" + property] = value; r[ property] = value; return r; } /** * @method private * @name _getTransform3DSupport * @description Determines if transforms are support * @return [boolean] "True if transforms supported" */ function _getTransform3DSupport() { /* http://stackoverflow.com/questions/11628390/how-to-detect-css-translate3d-without-the-webkit-context */ /* var prop = "transform", val = "translate3d(0px, 0px, 0px)", test = /translate3d\(0px, 0px, 0px\)/g, $div = $("
"); $div.css(_prefix(prop, val)); var check = $div[0].style.cssText.match(test); return (check !== null && check.length > 0); */ /* http://stackoverflow.com/questions/5661671/detecting-transform-translate3d-support/12621264#12621264 */ var el = document.createElement('p'), has3d, transforms = { 'webkitTransform':'-webkit-transform', 'OTransform':'-o-transform', 'msTransform':'-ms-transform', 'MozTransform':'-moz-transform', 'transform':'transform' }; document.body.insertBefore(el, null); for (var t in transforms) { if (el.style[t] !== undefined) { el.style[t] = "translate3d(1px,1px,1px)"; has3d = window.getComputedStyle(el).getPropertyValue(transforms[t]); } } document.body.removeChild(el); return (has3d !== undefined && has3d.length > 0 && has3d !== "none"); } $.fn.zoomer = function(method) { if (pub[method]) { return pub[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof method === 'object' || !method) { return _init.apply(this, arguments); } return this; }; $.zoomer = function(method) { if (method === "defaults") { pub.defaults.apply(this, Array.prototype.slice.call(arguments, 1)); } }; })(jQuery, window);