oxjs/source/UI/js/Image/ImageViewer.js

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;
};