643 lines
22 KiB
JavaScript
643 lines
22 KiB
JavaScript
'use strict';
|
|
|
|
/*@
|
|
Ox.ImageViewer <f> Image Viewer
|
|
options <o> Options
|
|
area <[n]> [x0, y0, x1, y1], if set this will override center and zoom
|
|
center <[n]|s|'auto'> Center ([x, y] or 'auto')
|
|
elasticity <n|0> Number of pixels to scroll/zoom beyond min/max
|
|
height <n|384> Viewer height in px
|
|
imageHeight <n|0> Image height in px
|
|
imagePreviewURL <s|''> URL of smaller preview image
|
|
imageURL <s|''> Image URL
|
|
imageWidth <n|0> Image width in px
|
|
maxZoom <n|16> Maximum zoom (minimum zoom is 'fit')
|
|
overviewSize <n|128> Size of overview image in px
|
|
width <n|512> Viewer width in px
|
|
zoom <n|s|'fit'> Zoom (number or 'fit' or 'fill')
|
|
self <o> Shared private variable
|
|
([options[, self]]) -> <o:Ox.Element> Image Viewer
|
|
center <!> Center changed
|
|
center <[n]|s> Center
|
|
zoom <!> Zoom changed
|
|
zoom <n|s> Zoom
|
|
@*/
|
|
|
|
Ox.ImageViewer = function(options, self) {
|
|
|
|
self = self || {};
|
|
var that = Ox.Element({}, self)
|
|
.defaults({
|
|
area: [],
|
|
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
|
|
});
|
|
|
|
if (self.options.area && self.options.area.length == 4) {
|
|
var centerAndZoom = getCenterAndZoom(self.options.area);
|
|
self.options.center = centerAndZoom.center;
|
|
self.options.zoom = centerAndZoom.zoom;
|
|
}
|
|
|
|
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('<img>')
|
|
.addClass('OxImage')
|
|
.attr({src: self.options.imagePreviewURL})
|
|
//.css(getImageCSS())
|
|
.appendTo(that);
|
|
|
|
Ox.$('<img>')
|
|
.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') + ' <span class="OxBright">[1]</span>'
|
|
},
|
|
{
|
|
id: 'fill',
|
|
title: 'fill',
|
|
tooltip: Ox._('Zoom to Fill') + ' <span class="OxBright">[2]</span>'
|
|
}
|
|
],
|
|
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') + ' <span class="OxBright">[-]</span>'
|
|
},
|
|
{
|
|
id: 'original',
|
|
title: 'equal',
|
|
tooltip: Ox._('Original Size') + ' <span class="OxBright">[0]</span>'
|
|
},
|
|
{
|
|
id: 'in',
|
|
title: 'add',
|
|
tooltip: Ox._('Zoom In') + ' <span class="OxBright">[=]</span>'
|
|
}
|
|
],
|
|
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'
|
|
})
|
|
.hide()
|
|
.on({
|
|
mouseenter: function() {
|
|
self.mouseIsInInterface = true;
|
|
},
|
|
mouseleave: function() {
|
|
self.mouseIsInInterface = false;
|
|
}
|
|
})
|
|
.appendTo(that);
|
|
|
|
self.$overviewImage = Ox.Element('<img>')
|
|
.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();
|
|
|
|
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 getArea(center, zoom) {
|
|
center = Ox.clone(center);
|
|
zoom = Ox.clone(zoom);
|
|
self.viewerRatio = self.options.width / self.options.height;
|
|
self.imageIsWider = self.imageRatio > self.viewerRatio;
|
|
if (center == 'auto') {
|
|
center = [
|
|
Math.round(self.options.imageWidth / 2),
|
|
Math.round(self.options.imageHeight / 2)
|
|
];
|
|
}
|
|
if (zoom == 'fit') {
|
|
zoom = self.imageIsWider
|
|
? self.options.width / self.options.imageWidth
|
|
: self.options.height / self.options.imageHeight;
|
|
} else if (zoom == 'fill') {
|
|
zoom = self.imageIsWider
|
|
? self.options.height / self.options.imageHeight
|
|
: self.options.width / self.options.imageWidth;
|
|
}
|
|
return [
|
|
Math.max(center[0] - self.options.width / 2 / zoom, 0),
|
|
Math.max(center[1] - self.options.height / 2 / zoom, 0),
|
|
Math.min(center[0] + self.options.width / 2 / zoom, self.options.imageWidth),
|
|
Math.min(center[1] + self.options.height / 2 / zoom, self.options.imageHeight)
|
|
];
|
|
}
|
|
|
|
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 getCenterAndZoom(area) {
|
|
if (!area || !area.length) {
|
|
return {center: 'auto', zoom: 'fit'};
|
|
}
|
|
var areaWidth = area[2] - area[0],
|
|
areaHeight = area[3] - area[1],
|
|
areaRatio = areaWidth / areaHeight;
|
|
self.viewerRatio = self.options.width / self.options.height;
|
|
return {
|
|
center: [
|
|
(area[0] + area[2]) / 2,
|
|
(area[1] + area[3]) / 2
|
|
],
|
|
zoom: self.viewerRatio < areaRatio
|
|
? self.options.width / areaWidth
|
|
: self.options.height / areaHeight
|
|
};
|
|
}
|
|
|
|
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) {
|
|
// FIXME: elastic maxZoom is still wrong
|
|
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;
|
|
if (e.deltaX == 0 && Math.abs(e.deltaY) && !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());
|
|
}
|
|
updateInterface();
|
|
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 updateInterface() {
|
|
var isFitZoom = self.zoom == self.fitZoom;
|
|
self.$scaleButton[
|
|
isFitZoom ? '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[
|
|
isFitZoom ? 'disableButton' : 'enableButton'
|
|
]('out');
|
|
self.$zoomButton[
|
|
self.zoom == 1 ? 'disableButton' : 'enableButton'
|
|
]('original');
|
|
self.$zoomButton[
|
|
self.zoom == self.maxZoom ? 'disableButton' : 'enableButton'
|
|
]('in');
|
|
!isFitZoom && self.$overview.show();
|
|
self.$overview.stop().animate({
|
|
opacity: isFitZoom ? 0 : 1
|
|
}, 250, function() {
|
|
isFitZoom && self.$overview.hide();
|
|
});
|
|
}
|
|
|
|
function updateSize() {
|
|
if (!self.updateTimeout) {
|
|
self.updateTimeout = setTimeout(function() {
|
|
self.updateTimeout = null;
|
|
setSize();
|
|
});
|
|
}
|
|
}
|
|
|
|
that.getArea = function() {
|
|
return getArea(self.options.center, self.options.zoom);
|
|
};
|
|
|
|
that.setArea = function(area) {
|
|
var centerAndZoom = getCenterAndZoom(area);
|
|
that.options({center: centerAndZoom.center, zoom: centerAndZoom.zoom});
|
|
return that;
|
|
};
|
|
|
|
return that;
|
|
|
|
};
|