From 6550066667151d8ca197142a32a19f85aef92783 Mon Sep 17 00:00:00 2001 From: rolux Date: Sun, 5 Jan 2014 15:26:44 +0530 Subject: [PATCH] add ImageViewer widget --- source/Ox.UI/js/Image/ImageViewer.js | 571 +++++++++++++++++++++++++++ 1 file changed, 571 insertions(+) create mode 100644 source/Ox.UI/js/Image/ImageViewer.js diff --git a/source/Ox.UI/js/Image/ImageViewer.js b/source/Ox.UI/js/Image/ImageViewer.js new file mode 100644 index 00000000..7409a9a8 --- /dev/null +++ b/source/Ox.UI/js/Image/ImageViewer.js @@ -0,0 +1,571 @@ +'use strict'; + +/*@ +Ox.ImageViewer Image Viewer + options Options + center <[n]|s|'auto'> Center ([x, y] or 'auto') + elasticity Number of pixels to scroll/zoom beyond min/max + height Viewer height in px + imageHeight Image height in px + imagePreviewURL URL of smaller preview image + imageURL Image URL + imageWidth Image width in px + maxZoom Maximum zoom (minimum zoom is 'fit') + overviewSize Size of overview image in px + width Viewer width in px + zoom Zoom (number or 'fit' or 'fill') + self Shared private variable + ([options[, self]]) -> Image Viewer + center Center changed + center <[n]|s> Center + zoom Zoom changed + zoom Zoom +@*/ + +Ox.ImageViewer = function(options, self) { + + self = self || {}; + var that = Ox.Element({}, self) + .defaults({ + center: 'auto', + elasticity: 0, + height: 384, + imageHeight: 0, + imagePreviewURL: '', + imageURL: '', + imageWidth: 0, + maxZoom: 16, + overviewSize: 128, + width: 512, + zoom: 'fit' + }) + .options(options || {}) + .update({ + center: function() { + setCenterAndZoom(true, true); + }, + // allow for setting height and width at the same time + height: updateSize, + width: updateSize, + zoom: function() { + setCenterAndZoom(true, true); + } + }) + .addClass('OxImageViewer OxGrid') + .on({ + mousedown: function() { + that.gainFocus(); + }, + mouseenter: function() { + showInterface(); + }, + mouseleave: function() { + hideInterface(); + }, + mousemove: function() { + showInterface(); + hideInterface(); + } + }) + .bindEvent({ + doubleclick: onDoubleclick, + dragstart: onDragstart, + drag: onDrag, + dragend: onDragend, + key_0: function() { + that.options({zoom: 1}); + }, + key_1: function() { + that.options({center: 'auto', zoom: 'fit'}); + }, + key_2: function() { + that.options({center: 'auto', zoom: 'fill'}); + }, + key_down: function() { + that.options({ + center: [ + self.center[0], + self.center[1] + self.options.height / 2 / self.zoom + ] + }); + }, + key_equal: function() { + that.options({zoom: self.zoom * 2}); + }, + key_left: function() { + that.options({ + center: [ + self.center[0] - self.options.width / 2 / self.zoom, + self.center[1] + ] + }); + }, + key_minus: function() { + that.options({zoom: self.zoom / 2}); + }, + key_right: function() { + that.options({ + center: [ + self.center[0] + self.options.width / 2 / self.zoom, + self.center[1] + ] + }); + }, + key_up: function() { + that.options({ + center: [ + self.center[0], + self.center[1] - self.options.height / 2 / self.zoom + ] + }); + }, + mousewheel: onMousewheel, + singleclick: onSingleclick + }); + + self.imageRatio = self.options.imageWidth / self.options.imageHeight; + self.overviewHeight = Math.round( + self.options.overviewSize / (self.imageRatio > 1 ? self.imageRatio : 1) + ); + self.overviewWidth = Math.round( + self.options.overviewSize * (self.imageRatio > 1 ? 1 : self.imageRatio) + ); + self.overviewZoom = self.overviewWidth / self.options.imageWidth; + + self.$image = Ox.Element('') + .addClass('OxImage') + .attr({src: self.options.imagePreviewURL}) + //.css(getImageCSS()) + .appendTo(that); + + Ox.$('') + .one({ + load: function() { + self.$image.attr({src: self.options.imageURL}); + } + }) + .attr({src: self.options.imageURL}); + + self.$scaleButton = Ox.ButtonGroup({ + buttons: [ + { + id: 'fit', + title: 'fit', + tooltip: Ox._('Zoom to Fit') + ' [1]' + }, + { + id: 'fill', + title: 'fill', + tooltip: Ox._('Zoom to Fill') + ' [2]' + } + ], + style: 'overlay', + type: 'image' + }) + .addClass('OxInterface OxScaleButton') + .on({ + mouseenter: function() { + self.mouseIsInInterface = true; + }, + mouseleave: function() { + self.mouseIsInInterface = false; + } + }) + .bindEvent({ + click: function(data) { + that.options({center: 'auto', zoom: data.id}); + } + }) + .appendTo(that); + + self.$zoomButton = Ox.ButtonGroup({ + buttons: [ + { + id: 'out', + title: 'remove', + tooltip: Ox._('Zoom Out') + ' [-]' + }, + { + id: 'original', + title: 'equal', + tooltip: Ox._('Original Size') + ' [0]' + }, + { + id: 'in', + title: 'add', + tooltip: Ox._('Zoom In') + ' [=]' + } + ], + style: 'overlay', + type: 'image' + }) + .addClass('OxInterface OxZoomButton') + .on({ + mouseenter: function() { + self.mouseIsInInterface = true; + }, + mouseleave: function() { + self.mouseIsInInterface = false; + } + }) + .bindEvent({ + click: function(data) { + if (data.id == 'out') { + that.options({zoom: self.zoom / 2}); + } else if (data.id == 'original') { + that.options({zoom: 1}); + } else { + that.options({zoom: self.zoom * 2}); + } + } + }) + .appendTo(that); + + self.$overview = Ox.Element() + .addClass('OxInterface OxImageOverview') + .css({ + height: self.overviewHeight + 'px', + width: self.overviewWidth + 'px' + }) + .on({ + mouseenter: function() { + self.mouseIsInInterface = true; + }, + mouseleave: function() { + self.mouseIsInInterface = false; + } + }) + .appendTo(that); + + self.$overviewImage = Ox.Element('') + .attr({src: self.options.imagePreviewURL}) + .css({ + height: self.overviewHeight + 'px', + width: self.overviewWidth + 'px' + }) + .appendTo(self.$overview); + + self.$overlay = Ox.Element() + .addClass('OxImageOverlay') + .appendTo(self.$overview); + + self.$area = {}; + ['bottom', 'center', 'left', 'right', 'top'].forEach(function(area) { + self.$area[area] = Ox.Element() + .addClass('OxImageOverlayArea') + .attr({id: 'OxImageOverlay' + Ox.toTitleCase(area)}) + .css(getAreaCSS(area)) + .appendTo(self.$overlay); + }); + + setSize(); + setCenterAndZoom(); + + function getAreaCSS(area) { + return area == 'bottom' ? { + height: self.overviewHeight + 'px' + } : area == 'center' ? { + left: self.overviewWidth + 'px', + top: self.overviewHeight + 'px', + right: self.overviewWidth + 'px', + bottom: self.overviewHeight + 'px' + } : area == 'left' ? { + top: self.overviewHeight + 'px', + bottom: self.overviewHeight + 'px', + width: self.overviewWidth + 'px' + } : area == 'right' ? { + top: self.overviewHeight + 'px', + bottom: self.overviewHeight + 'px', + width: self.overviewWidth + 'px' + } : { + height: self.overviewHeight + 'px' + }; + } + + function getCenter(e) { + var $target = $(e.target), center, offset, offsetX, offsetY; + if ($target.is('.OxImage')) { + center = [e.offsetX / self.zoom, e.offsetY / self.zoom]; + } else if ($target.is('.OxImageOverlayArea')) { + offset = that.offset(); + offsetX = e.clientX - offset.left - self.options.width + + self.overviewWidth + 6; + offsetY = e.clientY - offset.top - self.options.height + + self.overviewHeight + 6; + center = [offsetX / self.overviewZoom, offsetY / self.overviewZoom]; + } + return center; + } + + function getImageCSS() { + return { + left: Math.round(self.options.width / 2 - self.center[0] * self.zoom) + 'px', + top: Math.round(self.options.height / 2 - self.center[1] * self.zoom) + 'px', + width: Math.round(self.options.imageWidth * self.zoom) + 'px', + height: Math.round(self.options.imageHeight * self.zoom) + 'px' + }; + } + + function getOverlayCSS() { + var centerLeft = self.center[0] / self.options.imageWidth * self.overviewWidth, + centerTop = self.center[1] / self.options.imageHeight * self.overviewHeight, + centerWidth = self.options.width / self.zoom * self.overviewZoom + 4, + centerHeight = self.options.height / self.zoom * self.overviewZoom + 4; + return { + left: Math.round(centerLeft - centerWidth / 2 - self.overviewWidth) + 'px', + top: Math.round(centerTop - centerHeight / 2 - self.overviewHeight) + 'px', + width: Math.round(2 * self.overviewWidth + centerWidth) + 'px', + height: Math.round(2 * self.overviewHeight + centerHeight) + 'px' + }; + } + + function getZoomCenter(e, factor) { + var center = getCenter(e), + delta = [ + center[0] - self.center[0], + center[1] - self.center[1] + ]; + if (factor == 0.5) { + factor = -1; + } + return [ + self.center[0] + delta[0] / factor, + self.center[1] + delta[1] / factor + ]; + } + + function hideInterface() { + clearTimeout(self.interfaceTimeout); + self.interfaceTimeout = setTimeout(function() { + if (!self.mouseIsInInterface) { + self.interfaceIsVisible = false; + self.$scaleButton.animate({opacity: 0}, 250); + self.$zoomButton.animate({opacity: 0}, 250); + self.$overview.animate({opacity: 0}, 250); + } + }, 2500); + } + + function limitCenter(elastic) { + var center, imageHeight, imageWidth, maxCenter, minCenter; + if (self.options.zoom == 'fill') { + imageWidth = self.imageIsWider + ? self.options.height * self.imageRatio + : self.options.width; + imageHeight = self.imageIsWider + ? self.options.height + : self.options.width / self.imageRatio; + } else if (self.options.zoom == 'fit') { + imageWidth = self.imageIsWider + ? self.options.width + : self.options.height * self.imageRatio; + imageHeight = self.imageIsWider + ? self.options.width / self.imageRatio + : self.options.height; + } else { + imageWidth = self.options.imageWidth * self.options.zoom; + imageHeight = self.options.imageHeight * self.options.zoom; + } + minCenter = [ + imageWidth > self.options.width + ? self.options.width / 2 / self.zoom + : self.options.imageWidth / 2, + imageHeight > self.options.height + ? self.options.height / 2 / self.zoom + : self.options.imageHeight / 2 + ].map(function(value) { + return elastic ? value - self.options.elasticity / self.zoom : value; + }); + maxCenter = [ + self.options.imageWidth - minCenter[0], + self.options.imageHeight - minCenter[1] + ]; + center = self.options.center == 'auto' ? [ + self.options.imageWidth / 2, + self.options.imageHeight / 2 + ] : [ + Ox.limit(self.options.center[0], minCenter[0], maxCenter[0]), + Ox.limit(self.options.center[1], minCenter[1], maxCenter[1]) + ]; + if (Ox.isArray(self.options.center)) { + self.options.center = center; + } + return center; + } + + function limitZoom(elastic) { + var imageSize = self.imageIsWider ? self.options.imageWidth : self.options.imageHeight, + minZoom = elastic + ? (self.fitZoom * imageSize - 2 * self.options.elasticity) / imageSize + : self.fitZoom, + maxZoom = elastic + ? (self.maxZoom * imageSize + 2 * self.options.elasticity) / imageSize + : self.maxZoom, + zoom = self.options.zoom == 'fill' ? self.fillZoom + : self.options.zoom == 'fit' ? self.fitZoom + : Ox.limit(self.options.zoom, minZoom, maxZoom); + if (Ox.isNumber(self.options.zoom)) { + self.options.zoom = zoom; + } + return zoom; + } + + function onDoubleclick(e) { + var $target = $(e.target), factor = e.shiftKey ? 0.5 : 2; + if (( + $target.is('.OxImage') || $target.is('.OxImageOverlayArea') + ) && ( + (!e.shiftKey && self.zoom < self.maxZoom) + || (e.shiftKey && self.zoom > self.fitZoom) + )) { + that.options({ + center: getZoomCenter(e, factor), + zoom: self.zoom * factor + }); + } + } + + function onDragstart(e) { + var $target = $(e.target); + if ($target.is('.OxImage') || $target.is('#OxImageOverlayCenter')) { + self.drag = { + center: self.center, + zoom: $target.is('.OxImage') ? self.zoom : -self.overviewZoom + }; + } + } + + function onDrag(e) { + if (self.drag) { + self.options.center = [ + self.drag.center[0] - e.clientDX / self.drag.zoom, + self.drag.center[1] - e.clientDY / self.drag.zoom + ]; + setCenterAndZoom(false, true); + } + } + + function onDragend() { + if (self.drag) { + self.drag = null; + setCenterAndZoom(true); + } + } + + function onMousewheel(e) { + var $target = $(e.target), + factor = e.deltaY < 0 ? 2 : 0.5; + Ox.print('MW', e.deltaY); + if (e.deltaX == 0 && Math.abs(e.deltaY) > 10 && !self.mousewheelTimeout) { + if ($target.is('.OxImage') || $target.is('.OxImageOverlayArea')) { + self.options.center = getZoomCenter(e, factor); + self.options.zoom = self.zoom * factor; + setCenterAndZoom(true, true); + self.mousewheelTimeout = setTimeout(function() { + self.mousewheelTimeout = null; + }, 250); + } + } + } + + function onSingleclick(e) { + var $target = $(e.target), offset, offsetX, offsetY; + if ($target.is('.OxImage') || $target.is('.OxImageOverlayArea')) { + that.options({center: getCenter(e)}); + } + } + + function setCenterAndZoom(animate, elastic) { + self.zoom = limitZoom(elastic); + self.center = limitCenter(elastic); + if (animate) { + self.$image.stop().animate(getImageCSS(), 250, function() { + var center = limitCenter(), + zoom = limitZoom(), + setCenter = center[0] != self.center[0] + || center[1] != self.center[1], + setZoom = zoom != self.zoom; + if (setCenter || setZoom) { + self.options.center = center; + self.options.zoom = zoom; + setCenterAndZoom(); + } + that.triggerEvent({ + center: {center: self.options.center}, + zoom: {zoom: self.options.zoom} + }); + }); + self.$overlay.stop().animate(getOverlayCSS(), 250); + } else { + self.$image.css(getImageCSS()); + self.$overlay.css(getOverlayCSS()); + } + updateButtons(); + showInterface(); + hideInterface(); + } + + function setSize() { + self.viewerRatio = self.options.width / self.options.height; + self.imageIsWider = self.imageRatio > self.viewerRatio; + self.fillZoom = ( + self.imageIsWider + ? self.options.height * self.imageRatio + : self.options.width + ) / self.options.imageWidth; + self.fitZoom = ( + self.imageIsWider + ? self.options.width + : self.options.height * self.imageRatio + ) / self.options.imageWidth; + self.maxZoom = Math.max(self.fillZoom, self.options.maxZoom); + that.css({ + width: self.options.width + 'px', + height: self.options.height + 'px' + }); + setCenterAndZoom(); + } + + function showInterface() { + clearTimeout(self.interfaceTimeout); + if (!self.interfaceIsVisible) { + self.interfaceIsVisible = true; + self.$scaleButton.animate({opacity: 1}, 250); + self.$zoomButton.animate({opacity: 1}, 250); + self.$overview.animate({opacity: 1}, 250); + } + } + + function updateButtons() { + self.$scaleButton[ + self.zoom == self.fitZoom ? 'disableButton' : 'enableButton' + ]('fit'); + self.$scaleButton[ + self.zoom == self.fillZoom + && self.center[0] == self.options.imageWidth / 2 + && self.center[1] == self.options.imageHeight / 2 + ? 'disableButton' : 'enableButton' + ]('fill'); + self.$zoomButton[ + self.zoom == self.fitZoom ? 'disableButton' : 'enableButton' + ]('out'); + self.$zoomButton[ + self.zoom == 1 ? 'disableButton' : 'enableButton' + ]('original'); + self.$zoomButton[ + self.zoom == self.maxZoom ? 'disableButton' : 'enableButton' + ]('in'); + } + + function updateSize() { + if (!self.updateTimeout) { + self.updateTimeout = setTimeout(function() { + self.updateTimeout = null; + setSize(); + }); + } + } + + return that; + +}; \ No newline at end of file