2019-11-02 00:06:25 +01:00
|
|
|
const events = {
|
2019-10-25 09:15:16 +02:00
|
|
|
documentMouseup: [],
|
|
|
|
windowResize: [],
|
|
|
|
};
|
|
|
|
|
|
|
|
window.onload = function () {
|
2020-07-02 01:45:54 +02:00
|
|
|
document.body.addEventListener("mouseup", (e) => {
|
2019-10-25 09:15:16 +02:00
|
|
|
events.documentMouseup = events.documentMouseup.filter(function (event) {
|
|
|
|
// go through automatic cleanup when running events.
|
|
|
|
if (!document.body.contains(event.canvas)) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
event.callback.call(this, e);
|
|
|
|
return true;
|
2017-06-08 22:58:15 +02:00
|
|
|
});
|
2019-10-25 09:15:16 +02:00
|
|
|
});
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
window.addEventListener("resize", function (e) {
|
2020-07-02 01:45:54 +02:00
|
|
|
events.windowResize = events.windowResize.filter((event) => {
|
2019-10-25 09:15:16 +02:00
|
|
|
if (!document.body.contains(event.canvas)) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
event.callback.call(this, e);
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
return true;
|
2020-07-02 01:45:54 +02:00
|
|
|
});
|
2019-10-25 09:15:16 +02:00
|
|
|
});
|
|
|
|
};
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-26 00:26:37 +02:00
|
|
|
const funcs = {
|
2019-10-25 09:15:16 +02:00
|
|
|
setZoom: function (meta, zoom) {
|
|
|
|
// condition to handle zooming event by zoom hotkeys
|
2020-07-15 01:29:15 +02:00
|
|
|
if (zoom === "+") {
|
2019-10-25 09:15:16 +02:00
|
|
|
zoom = meta.zoom * 1.2;
|
2020-07-15 01:29:15 +02:00
|
|
|
} else if (zoom === "-") {
|
2019-10-25 09:15:16 +02:00
|
|
|
zoom = meta.zoom / 1.2;
|
|
|
|
}
|
|
|
|
// make sure the zoom is above 1 and below the maxZoom.
|
|
|
|
meta.zoom = Math.min(Math.max(zoom, 1), meta.maxZoom);
|
|
|
|
},
|
|
|
|
|
|
|
|
// this is a function given a canvas that attaches all of the events
|
|
|
|
// required to pan and zoom.
|
|
|
|
attachEvents: function (canvas, context, meta) {
|
2019-11-02 00:06:25 +01:00
|
|
|
let mousedown = false;
|
2019-10-25 09:15:16 +02:00
|
|
|
|
|
|
|
// wheelEvent.deltaMode is a value that describes what the unit is
|
|
|
|
// for the `deltaX`, `deltaY`, and `deltaZ` properties.
|
2019-11-02 00:06:25 +01:00
|
|
|
const DELTA_MODE = {
|
2019-10-25 09:15:16 +02:00
|
|
|
PIXEL: 0,
|
|
|
|
LINE: 1,
|
|
|
|
PAGE: 2,
|
|
|
|
};
|
2017-08-02 22:39:20 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// give object structure in `mousedown`, because its props are only
|
|
|
|
// ever set once `mousedown` + `mousemove` is triggered.
|
2019-11-02 00:06:25 +01:00
|
|
|
let lastPosition = {};
|
2019-10-25 09:15:16 +02:00
|
|
|
|
|
|
|
// in browsers such as Safari, the `e.movementX` and `e.movementY`
|
|
|
|
// props don't exist, so we need to create them as a difference of
|
|
|
|
// where the last `layerX` and `layerY` movements since the last
|
|
|
|
// `mousemove` event in this `mousedown` event were registered.
|
2019-11-02 00:06:25 +01:00
|
|
|
const polyfillMouseMovement = function (e) {
|
2019-10-25 09:15:16 +02:00
|
|
|
e.movementX = e.layerX - lastPosition.x || 0;
|
|
|
|
e.movementY = e.layerY - lastPosition.y || 0;
|
|
|
|
|
|
|
|
lastPosition = {
|
|
|
|
x: e.layerX,
|
|
|
|
y: e.layerY,
|
2017-07-31 22:22:59 +02:00
|
|
|
};
|
2019-10-25 09:15:16 +02:00
|
|
|
};
|
2017-07-31 22:22:59 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// use the wheel event rather than scroll because this isn't
|
|
|
|
// actually an element that can scroll. The wheel event will
|
|
|
|
// detect the *gesture* of scrolling over an element, without actually
|
|
|
|
// worrying about scrollable content.
|
2020-07-02 01:45:54 +02:00
|
|
|
canvas.addEventListener("wheel", (e) => {
|
2019-10-25 09:15:16 +02:00
|
|
|
e.preventDefault();
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2020-03-28 01:25:56 +01:00
|
|
|
// this is to reverse scrolling directions for the image.
|
2019-11-02 00:06:25 +01:00
|
|
|
let delta = meta.direction * e.deltaY;
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
if (e.deltaMode === DELTA_MODE.LINE) {
|
|
|
|
// the vertical height in pixels of an approximate line.
|
|
|
|
delta *= 15;
|
|
|
|
}
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
if (e.deltaMode === DELTA_MODE.PAGE) {
|
|
|
|
// the vertical height in pixels of an approximate page.
|
|
|
|
delta *= 300;
|
|
|
|
}
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// this is calculated as the user defined speed times the normalizer
|
|
|
|
// (which just is what it takes to take the raw delta and transform
|
|
|
|
// it to a normal speed), multiply it against the current zoom.
|
|
|
|
// Example:
|
|
|
|
// delta = 8
|
|
|
|
// normalizedDelta = delta * (1 / 20) * 1 = 0.4
|
|
|
|
// zoom = zoom * (0.4 / 100) + 1
|
2020-07-15 00:34:28 +02:00
|
|
|
const zoom =
|
|
|
|
meta.zoom * ((meta.speed * meta.internalSpeedMultiplier * delta) / 100 + 1);
|
2019-10-25 09:15:16 +02:00
|
|
|
|
|
|
|
funcs.setZoom(meta, zoom);
|
|
|
|
funcs.displayImage(canvas, context, meta);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
});
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// the only valid mousedown events should originate inside of the
|
|
|
|
// canvas.
|
2020-07-02 01:45:54 +02:00
|
|
|
canvas.addEventListener("mousedown", () => {
|
2019-10-25 09:15:16 +02:00
|
|
|
mousedown = true;
|
|
|
|
});
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// on mousemove, actually run the pan events.
|
2020-07-02 01:45:54 +02:00
|
|
|
canvas.addEventListener("mousemove", (e) => {
|
2019-10-25 09:15:16 +02:00
|
|
|
// to pan, there must be mousedown and mousemove, check if valid.
|
|
|
|
if (mousedown === true) {
|
|
|
|
polyfillMouseMovement(e);
|
|
|
|
// find the percent of movement relative to the canvas width
|
|
|
|
// since e.movementX, e.movementY are in px.
|
2019-11-02 00:06:25 +01:00
|
|
|
const percentMovement = {
|
2019-10-25 09:15:16 +02:00
|
|
|
x: e.movementX / canvas.width,
|
|
|
|
y: e.movementY / canvas.height,
|
|
|
|
};
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// add the percentMovement to the meta coordinates but divide
|
|
|
|
// out by the zoom ratio because when zoomed in 10x for example
|
|
|
|
// moving the photo by 1% will appear like 10% on the <canvas>.
|
2020-07-15 00:34:28 +02:00
|
|
|
meta.coords.x += (percentMovement.x * 2) / meta.zoom;
|
|
|
|
meta.coords.y += (percentMovement.y * 2) / meta.zoom;
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// redraw the image.
|
|
|
|
funcs.displayImage(canvas, context, meta);
|
|
|
|
}
|
|
|
|
});
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// event listener to handle zoom in and out from using keyboard keys z/Z and +/-
|
|
|
|
// in the canvas
|
|
|
|
// these hotkeys are not implemented in static/js/hotkey.js as the code in
|
|
|
|
// static/js/lightbox_canvas.js and static/js/lightbox.js isn't written a way
|
|
|
|
// that the LightboxCanvas instance created in lightbox.js can be
|
|
|
|
// accessed from hotkey.js. Major code refactoring is required in lightbox.js
|
|
|
|
// to implement these keyboard shortcuts in hotkey.js
|
2020-07-15 01:29:15 +02:00
|
|
|
document.addEventListener("keydown", (e) => {
|
2019-10-25 09:15:16 +02:00
|
|
|
if (!overlays.lightbox_open()) {
|
|
|
|
return;
|
|
|
|
}
|
2020-07-15 01:29:15 +02:00
|
|
|
if (e.key === "Z" || e.key === "+") {
|
|
|
|
funcs.setZoom(meta, "+");
|
2019-10-25 09:15:16 +02:00
|
|
|
funcs.displayImage(canvas, context, meta);
|
2020-07-15 01:29:15 +02:00
|
|
|
} else if (e.key === "z" || e.key === "-") {
|
|
|
|
funcs.setZoom(meta, "-");
|
2019-10-25 09:15:16 +02:00
|
|
|
funcs.displayImage(canvas, context, meta);
|
2020-07-15 01:29:15 +02:00
|
|
|
} else if (e.key === "v") {
|
|
|
|
overlays.close_overlay("lightbox");
|
2017-06-08 22:58:15 +02:00
|
|
|
}
|
2019-10-25 09:15:16 +02:00
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
});
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// make sure that when the mousedown is lifted on <canvas>to prevent
|
|
|
|
// panning events.
|
2020-07-02 01:45:54 +02:00
|
|
|
canvas.addEventListener("mouseup", () => {
|
2019-10-25 09:15:16 +02:00
|
|
|
mousedown = false;
|
|
|
|
// reset this to be empty so that the values will `NaN` on first
|
|
|
|
// mousemove and default to a change of (0, 0).
|
|
|
|
lastPosition = {};
|
|
|
|
});
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// do so on the document.body as well, though depending on the infra,
|
|
|
|
// these are less reliable as preventDefault may prevent these events
|
|
|
|
// from propagating all the way to the <body>.
|
|
|
|
events.documentMouseup.push({
|
|
|
|
canvas: canvas,
|
|
|
|
meta: meta,
|
|
|
|
callback: function () {
|
|
|
|
mousedown = false;
|
|
|
|
},
|
|
|
|
});
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
events.windowResize.push({
|
|
|
|
canvas: canvas,
|
|
|
|
meta: meta,
|
|
|
|
callback: function () {
|
|
|
|
funcs.sizeCanvas(canvas, meta);
|
|
|
|
funcs.displayImage(canvas, context, meta);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
},
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
imageRatio: function (image) {
|
|
|
|
return image.naturalWidth / image.naturalHeight;
|
|
|
|
},
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
displayImage: function (canvas, context, meta) {
|
|
|
|
meta.coords.x = Math.max(1 / (meta.zoom * 2), meta.coords.x);
|
|
|
|
meta.coords.x = Math.min(1 - 1 / (meta.zoom * 2), meta.coords.x);
|
|
|
|
|
|
|
|
meta.coords.y = Math.max(1 / (meta.zoom * 2), meta.coords.y);
|
|
|
|
meta.coords.y = Math.min(1 - 1 / (meta.zoom * 2), meta.coords.y);
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const c = {
|
2019-10-25 09:15:16 +02:00
|
|
|
x: meta.coords.x - 1,
|
|
|
|
y: meta.coords.y - 1,
|
2017-06-08 22:58:15 +02:00
|
|
|
};
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const x = meta.zoom * c.x * canvas.width + canvas.width / 2;
|
|
|
|
const y = meta.zoom * c.y * canvas.height + canvas.height / 2;
|
|
|
|
const w = canvas.width * meta.zoom;
|
|
|
|
const h = canvas.height * meta.zoom;
|
2019-10-25 09:15:16 +02:00
|
|
|
|
2020-05-27 00:44:47 +02:00
|
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
2019-10-25 09:15:16 +02:00
|
|
|
context.imageSmoothingEnabled = false;
|
|
|
|
|
|
|
|
context.drawImage(meta.image, x, y, w, h);
|
|
|
|
},
|
|
|
|
|
|
|
|
// the `sizeCanvas` method figures out the appropriate bounding box for
|
|
|
|
// the canvas given a parent that has constraints.
|
|
|
|
// for example, if a photo has a ration of 1.5:1 (w:h), and the parent
|
|
|
|
// box is 1:1 respectively, we want to stretch the photo to be as large
|
|
|
|
// as we can, which means that we check if having the photo width = 100%
|
|
|
|
// means that the height is less than 100% of the parent height. If so,
|
|
|
|
// then we size the photo as w = 100%, h = 100% / 1.5.
|
|
|
|
sizeCanvas: function (canvas, meta) {
|
2020-04-04 12:07:14 +02:00
|
|
|
if (canvas.parentNode === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
if (typeof meta.onresize === "function") {
|
|
|
|
meta.onresize(canvas);
|
2017-06-08 22:58:15 +02:00
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const parent = {
|
2019-10-25 09:15:16 +02:00
|
|
|
width: canvas.parentNode.clientWidth,
|
|
|
|
height: canvas.parentNode.clientHeight,
|
|
|
|
};
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
if (parent.height * meta.ratio > parent.width) {
|
|
|
|
canvas.width = parent.width * 2;
|
|
|
|
canvas.style.width = parent.width + "px";
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2020-07-15 00:34:28 +02:00
|
|
|
canvas.height = (parent.width / meta.ratio) * 2;
|
2019-10-25 09:15:16 +02:00
|
|
|
canvas.style.height = parent.width / meta.ratio + "px";
|
|
|
|
} else {
|
|
|
|
canvas.height = parent.height * 2;
|
|
|
|
canvas.style.height = parent.height + "px";
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
canvas.width = parent.height * meta.ratio * 2;
|
|
|
|
canvas.style.width = parent.height * meta.ratio + "px";
|
|
|
|
}
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
blueslip.warn("Please specify a 'data-width' or 'data-height' argument for canvas.");
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
// a class w/ prototype to create a new `LightboxCanvas` instance.
|
2019-11-02 00:06:25 +01:00
|
|
|
const LightboxCanvas = function (el) {
|
|
|
|
const self = this;
|
2019-10-25 09:15:16 +02:00
|
|
|
|
|
|
|
this.meta = {
|
|
|
|
direction: -1,
|
|
|
|
zoom: 1,
|
|
|
|
image: null,
|
|
|
|
coords: {
|
|
|
|
x: 0.5,
|
|
|
|
y: 0.5,
|
2017-06-08 22:58:15 +02:00
|
|
|
},
|
2019-10-25 09:15:16 +02:00
|
|
|
speed: 1,
|
|
|
|
// this is to normalize the speed to what I would consider to be
|
|
|
|
// "standard" zoom speed.
|
|
|
|
internalSpeedMultiplier: 0.05,
|
|
|
|
maxZoom: 10,
|
|
|
|
};
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
if (el instanceof Node) {
|
|
|
|
this.canvas = el;
|
|
|
|
} else if (typeof el === "string") {
|
|
|
|
this.canvas = document.querySelector(el);
|
|
|
|
} else {
|
|
|
|
blueslip.warn("Error. 'LightboxCanvas' accepts either string selector or node.");
|
|
|
|
return;
|
|
|
|
}
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
this.context = this.canvas.getContext("2d");
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
this.meta.image = new Image();
|
|
|
|
this.meta.image.src = this.canvas.getAttribute("data-src");
|
|
|
|
this.meta.image.onload = function () {
|
|
|
|
self.meta.ratio = funcs.imageRatio(this);
|
2017-06-08 22:58:15 +02:00
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
funcs.sizeCanvas(self.canvas, self.meta);
|
|
|
|
funcs.displayImage(self.canvas, self.context, self.meta);
|
2017-06-08 22:58:15 +02:00
|
|
|
};
|
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
this.canvas.image = this.meta.image;
|
|
|
|
|
|
|
|
funcs.attachEvents(this.canvas, this.context, self.meta);
|
|
|
|
};
|
|
|
|
|
2019-10-25 09:45:13 +02:00
|
|
|
LightboxCanvas.prototype = {
|
2019-10-25 09:15:16 +02:00
|
|
|
// set the speed at which scrolling zooms in on a photo.
|
|
|
|
speed: function (speed) {
|
|
|
|
this.meta.speed = speed;
|
|
|
|
},
|
|
|
|
|
|
|
|
// set the max zoom of the `LightboxCanvas` canvas as a mult of the total width.
|
|
|
|
maxZoom: function (maxZoom) {
|
|
|
|
this.meta.maxZoom = maxZoom;
|
|
|
|
},
|
|
|
|
|
|
|
|
reverseScrollDirection: function () {
|
|
|
|
this.meta.direction = 1;
|
|
|
|
},
|
|
|
|
|
|
|
|
setZoom: function (zoom) {
|
|
|
|
funcs.setZoom(this.meta, zoom);
|
|
|
|
funcs.displayImage(this.canvas, this.context, this.meta);
|
|
|
|
},
|
|
|
|
|
|
|
|
resize: function (callback) {
|
|
|
|
this.meta.onresize = callback;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2019-10-25 09:45:13 +02:00
|
|
|
module.exports = LightboxCanvas;
|
2018-07-06 15:24:05 +02:00
|
|
|
window.LightboxCanvas = LightboxCanvas;
|