// ==UserScript==
// @name Easy Compare
// @description Compare images
// @version 0.9.5
// @author Secant (TYT@NexusHD)
// @license GPL-3.0-or-later
// @supportURL [email protected]
// @contributionURL https://i.loli.net/2020/02/28/JPGgHc3UMwXedhv.jpg
// @contributionAmount 10
// @include *
// @require https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js
// @require https://greasyfork.org/scripts/401377-pixelmatch/code/pixelmatch.js
// @resource PixelMatchCore https://greasyfork.org/scripts/401377-pixelmatch/code/pixelmatch.js
// @namespace https://greasyfork.org/users/152136
// @icon data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23008000'%3E%3Cpath id='ld' d='M20 6H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h10v4h4V2h-4v4zm0 30H10l10-12v12zM38 6H28v4h10v26L28 24v18h10c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4z'/%3E%3C/svg%3E
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_getResourceText
// @grant unsafewindow
// @connect hdbits.org
// @connect awesome-hd.me
// @connect ptpimg.me
// @connect imgbox.com
// @connect malzo.com
// @connect imagebam.com
// @connect pixhost.to
// @connect loli.net
// @connect funkyimg.com
// @connect ilikeshots.club
// @connect z4a.net
// @connect picgd.com
// @connect tu.totheglory.im
// @connect tpimg.ccache.org
// @connect pterclub.com
// @connect catbox.moe
// @connect sm.ms
// @connect broadcasthe.net
// @connect *
// ==/UserScript==
// jshint esversion:8, -W054
(async function ($, Mousetrap, pixelmatch, URL) {
'use strict';
/*--- Preparation ---*/
// Mousetrap Pause Plugin
if (Mousetrap) {
let target = Mousetrap.prototype || Mousetrap;
const _originalStopCallback = target.stopCallback;
target.stopCallback = function (e, element, combo) {
var self = this;
if (self.paused) {
return true;
}
return _originalStopCallback.call(self, e, element, combo);
};
target.pause = function () {
var self = this;
self.paused = true;
};
target.unpause = function () {
var self = this;
self.paused = false;
};
try {
Mousetrap.init();
} catch (_) { }
}
/*--- Global Contexts ---*/
// A global timeout ID holder
let timeout;
// A global scale factor
let scale = 10;
// Regex replacement array that converts thumbs to originals
const t2oLib = [
[/\.thumb\.jpe?g$/, ''], // nexusphp
[/\.md\.png$/, '.png'], // m-team
[/\.th\.png$/, '.png'], // pterclub
[/_thumb\.png$/, '.png'], // totheglory
[/img\.awesome\-hd\.me\/t(\/\d+)?\//, 'img.awesome-hd.me/images/'], // awesome-hd
[/thumbs((?:\d+)?\.imgbox\.com\/.+_)t\.png$/, 'images$1o.png'], // imgbox
[/t((?:\d+)?\.pixhost\.to\/)thumbs\//, 'img$1images/'], // pixhost
[/t(\.hdbits\.org\/.+)\.jpg$/, 'i$1.png'], // hdbits
[/^.*?imagecache\.php\?url=(https?)%3A%2F%2Fthumbs(\d+)?\.imgbox\.com%2F(\w+)%2F(\w+)%2F(\w+)_t\.png/, '$1://images$2.imgbox.com/$3/$4/$5_o.png']
];
// Skip redirections
const skipRedirLib = [
[/^https?:\/\/anonym\.to\/\?(.*)$/, (_, p1) => decodeURIComponent(p1)],
[/^https?:\/\/www\.dereferer\.org\/\?(.*)$/, (_, p1) => decodeURIComponent(p1)],
[/^(?:https?:\/\/pterclub\.com)?\/link\.php\?sign=.+?&target=(.*)$/, (_, p1) => decodeURIComponent(p1.replace(/\+/g, ' ')).replace(/ /g, '%20')],
[/^.*?imagecache\.php\?url=(.*)$/, (_, p1) => decodeURIComponent(p1.replace(/\+/g, ' ')).replace(/ /g, '%20')]
];
// Probable original image selectors on a view page
const guessSelectorLib = [
'#image-viewer-container>img',
'.image-container img',
'div.img.big>img',
'img.mainimage',
'img.main-image',
'img#img'
];
// Filter function mapping
const filterImage = {
'solar': img => rgbImage(img, solarWorker || rgbSolarCurve),
's2lar': img => rgbImage(img, s2larWorker || rgbS2larCurve)
};
/*--- Workers Initialization ---*/
// Solar Curve
function solarCurve(x, t = 5, k = 5.5) {
const m = (k * Math.PI - 128 / t);
const A = -1 / 4194304 * m;
const B = 3 / 32768 * m;
const C = 1 / t;
return Math.round(
127.9999 * Math.sin(
A * x ** 3 + B * x ** 2 + C * x - Math.PI / 2
) + 127.5
) || 0;
}
let rgbSolarCurve = GM_getValue('solarCurve');
let rgbS2larCurve = GM_getValue('s2larCurve');
if (!rgbSolarCurve) {
rgbSolarCurve = [
Array.from({ length: 256 }, (_, x) => solarCurve(x)),
Array.from({ length: 256 }, (_, x) => solarCurve(x - 5)),
Array.from({ length: 256 }, (_, x) => solarCurve(x + 5))
];
GM_setValue('solarCurve', JSON.stringify(rgbSolarCurve));
rgbS2larCurve = [
Array.from({ length: 256 }, (_, x) => rgbSolarCurve[0][[rgbSolarCurve[0][x]]]),
Array.from({ length: 256 }, (_, x) => rgbSolarCurve[1][[rgbSolarCurve[1][x]]]),
Array.from({ length: 256 }, (_, x) => rgbSolarCurve[2][[rgbSolarCurve[2][x]]])
];
GM_setValue('s2larCurve', JSON.stringify(rgbS2larCurve));
} else {
rgbSolarCurve = JSON.parse(rgbSolarCurve);
rgbS2larCurve = JSON.parse(rgbS2larCurve);
}
rgbSolarCurve = rgbSolarCurve.map(e => new Uint8Array(e));
rgbS2larCurve = rgbS2larCurve.map(e => new Uint8Array(e));
async function loadBuffer(worker, [R, G, B]) {
return new Promise((resolve) => {
worker.onmessage = (e) => {
resolve(e.data.result);
};
worker.postMessage({
R: R.buffer,
G: G.buffer,
B: B.buffer
}, [R.buffer, G.buffer, B.buffer]);
});
}
// Diff, Solar, S2lar Worker Initialization
function diffWork(f) {
f.apply(self);
const u = Uint8ClampedArray;
self.onmessage = ({ data: { key, img1, img2, width, height, init } }) => {
img1 = new u(img1);
img2 = new u(img2);
const diff = new u(img1);
try {
self.pixelmatch(img1, img2, diff, width, height, init);
self.postMessage({
diff: diff.buffer,
width: width,
height: height,
key: key
}, [diff.buffer]);
} catch (err) {
console.warn(err);
self.postMessage({
diff: null,
key: key
});
}
};
}
function rgbWork(f) {
const u = Uint8ClampedArray;
self.onmessage = ({ data: { key, R, G, B, img, width, height } }) => {
if (R && G && B) {
self.RGB = [new u(R), new u(G), new u(B)];
self.postMessage({ result: true });
} else {
img = new u(img);
const filter = new u(img);
try {
f.apply(self, [img, filter, width, height, self.RGB]);
self.postMessage({
filter: filter.buffer,
width: width,
height: height,
key: key
}, [filter.buffer]);
} catch (err) {
console.warn(err);
self.postMessage({
filter: null,
key: key
});
}
}
};
}
function stringifyWork(workFun, arg) {
return `(${workFun.toString()})(${arg})`;
}
let diffWorker, solarWorker, s2larWorker;
let loadBufferPromise;
try {
const diffWorkerBlob = new Blob([
stringifyWork(diffWork, new Function(
GM_getResourceText('PixelMatchCore')
))
], { type: 'application/javascript' });
diffWorker = new Worker(URL.createObjectURL(diffWorkerBlob));
diffWorker.keyPool = {};
URL.revokeObjectURL(diffWorkerBlob);
const rgbWorkerBlob = new Blob([stringifyWork(rgbWork, rgbRemap)], { type: 'application/javascript' });
const rgbWorkerURL = URL.createObjectURL(rgbWorkerBlob);
solarWorker = new Worker(rgbWorkerURL);
solarWorker.keyPool = {};
const transSo = loadBuffer(solarWorker, rgbSolarCurve);
s2larWorker = new Worker(rgbWorkerURL);
s2larWorker.keyPool = {};
const transS2 = loadBuffer(s2larWorker, rgbS2larCurve);
URL.revokeObjectURL(rgbWorkerURL);
loadBufferPromise = Promise.all([transSo, transS2]);
} catch (e) {
try {
const diffWorkerDataURI = `data:application/javascript,${
encodeURIComponent(
stringifyWork(diffWork, new Function(
GM_getResourceText('PixelMatchCore')
))
)}`;
diffWorker = new Worker(diffWorkerDataURI);
diffWorker.keyPool = {};
const rgbWorkerDataURI = `data:application/javascript,${
encodeURIComponent(
stringifyWork(rgbWork, rgbRemap)
)}`;
solarWorker = new Worker(rgbWorkerDataURI);
solarWorker.keyPool = {};
const transSo = loadBuffer(solarWorker, rgbSolarCurve);
s2larWorker = new Worker(rgbWorkerDataURI);
s2larWorker.keyPool = {};
const transS2 = loadBuffer(s2larWorker, rgbS2larCurve);
loadBufferPromise = Promise.all([transSo, transS2]);
} catch (e) {
diffWorker = null;
solarWorker = null;
}
}
/*--- Helper Functions ---*/
// Virtual DOM for selection without fetching images
function $$(htmlString) {
return $(htmlString, document.implementation.createHTMLDocument('virtual'));
}
// Function to make an <canvas/> element
function makeCanvas(outlineColor = 'red') {
const $figure = $('<figure/>').css({
'width': 'fit-content',
'position': 'fixed',
'top': '50%',
'left': '50%',
'margin': '0',
'vertical-align': 'middle'
});
const $canvas = $(`<canvas/>`).css({
'display': 'none',
'transform': 'translate(-50%, -50%)',
'opacity': '1',
'outline': '3px solid ' + outlineColor,
'outline-offset': '2px',
});
$figure.append($canvas);
return $canvas[0];
}
// Draw text on canvas
function drawText(canvas, text, font = '16px sans serif', fillStyle = 'rgba(255,255,255,255)') {
const context = canvas.getContext('2d');
context.font = font;
canvas.width = context.measureText(text).width;
canvas.height = 20;
context.font = font;
context.fillStyle = fillStyle;
context.fillText(text, 0, 15);
}
// Draw image on canvas
function drawImage(canvas, imageData) {
canvas.width = imageData.width;
canvas.height = imageData.height;
canvas.getContext('2d').putImageData(imageData, 0, 0);
}
// Guess original image src from view page
function guessOriginalImage(url) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
url: url,
method: 'GET',
timeout: 6000,
onload: (x) => {
if (x.status === 200) {
try {
const $e = $$(x.responseText);
const src = $e.find(guessSelectorLib.join(','))[0].src;
let realSrc = src;
for (let pairs of t2oLib) {
realSrc = realSrc.replace(pairs[0], pairs[1]);
if (realSrc !== src) {
break;
}
}
resolve(realSrc);
}
catch (e) {
console.warn(e);
resolve(null);
}
}
else {
console.warn(x);
resolve(null);
}
},
ontimeout: (e) => {
console.warn(e);
resolve(null);
}
});
});
}
// RGB channel remap function (lowlevel)
function rgbRemap(raw, filter, width, height, rgb) {
const [R, G, B] = rgb;
for (let row = 0; row < height; ++row) {
for (let col = 0; col < width; ++col) {
let ind = col * 4 + row * width * 4;
filter[ind] = R[raw[ind]];
filter[ind + 1] = G[raw[ind + 1]];
filter[ind + 2] = B[raw[ind + 2]];
filter[ind + 3] = raw[ind + 3];
}
}
}
// Get ImageData from src with an optional update hook
// Cross origin is supported
async function GM_getImageData(src, fn) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
url: src,
method: 'GET',
// Blob or Arraybuffer responseType will slow down the page noticeably,
// so we text type with x-user-defined charset to get raw binaries
overrideMimeType: 'text/plain; charset=x-user-defined',
// Progress update hook
onprogress: (e) => {
if (typeof (fn) == 'function') {
if (e.total !== -1) {
fn(e.loaded / e.total);
}
else {
fn(-e.loaded);
}
}
},
onload: (e) => {
if (e.status === 200) {
// Get binary from text
const imageResponseText = e.responseText;
const l = imageResponseText.length;
const bytes = new Uint8Array(l);
for (let i = 0; i < l; i++) {
bytes[i] = imageResponseText.charCodeAt(i) & 0xff;
}
// Decode png binary and resolve the image data arraybuffer,
// createImageBitmap is a multi-thread operation,
// and won't complain about CSP img-src errors when using Image object
const type = (e.responseHeaders.match(/content\-type: *(.+)$/m) || ['', 'image/png'])[1];
let ext;
switch (type) {
case 'image/apng':
ext = '.apng';
break;
case 'image/bmp':
ext = '.bmp';
break;
case 'image/gif':
ext = '.gif';
break;
case 'image/x-icon':
ext = '.ico';
break;
case 'image/jpeg':
ext = '.jpg';
break;
case 'image/png':
ext = '.png';
break;
case 'image/svg+xml':
ext = '.svg';
break;
case 'image/tiff':
ext = '.tiff';
break;
case 'image/webp':
ext = '.webp';
break;
default:
if (type.slice(0, 5) === 'image') {
let temp = type.match(/\/(.*)/);
if (temp) {
ext = '.' + temp;
} else {
ext = '';
}
} else {
ext = (src.match(/\.[^\.]+$/) || [''])[0];
}
break;
}
createImageBitmap(new Blob([bytes], { type: type }))
.then((e) => {
const [width, height] = [e.width, e.height];
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
context.drawImage(e, 0, 0);
e.close();
resolve({
imageData: new ImageData(
context.getImageData(0, 0, width, height).data,
width,
height
),
extension: ext
});
});
}
else {
console.warn(e);
resolve(null);
}
},
onerror: (e) => {
console.warn(e);
resolve(null);
}
});
});
}
/*--- Diff and Filter Core Function ---*/
// Diff images
async function diffImage(img1, img2, init = { alpha: 0.5, threshold: 0.007 }, worker = diffWorker) {
if (
img1 && img2 &&
img1.width === img2.width &&
img1.height === img2.height
) {
if (worker) {// async diff
const [
raw1,
raw2,
width,
height
] = [
img1.data.buffer,
img2.data.buffer,
img1.width,
img1.height
];
const key = '' + Date.now();
worker.onmessage = (e) => {
const returnKey = e.data.key;
const resolve = worker.keyPool[returnKey];
if (resolve) {
resolve(
new ImageData(
new Uint8ClampedArray(e.data.diff),
e.data.width,
e.data.height
)
);
}
};
worker.postMessage({
img1: raw1,
img2: raw2,
width: width,
height: height,
init: init,
key: key
}, [raw1, raw2]);
return new Promise((res) => {
worker.keyPool[key] = res;
});
} else {// sync diff
const [data1, data2, width, height] = [
img1.data,
img2.data,
img1.width,
img1.height
];
const res = new Uint8ClampedArray(data1);
pixelmatch(data1, data2, res, width, height, init);
return (
new ImageData(
res,
width,
height
)
);
}
} else {
return null;
}
}
// RGB channel remap filter image
async function rgbImage(img, argument) {
if (img) {
if (argument instanceof Worker) {
const worker = argument;
const [raw, width, height] = [img.data.buffer, img.width, img.height];
const key = '' + Date.now();
worker.onmessage = (e) => {
const returnKey = e.data.key;
const resolve = worker.keyPool[returnKey];
if (resolve) {
resolve(
new ImageData(
new Uint8ClampedArray(e.data.filter),
e.data.width,
e.data.height
)
);
}
};
await loadBufferPromise;
worker.postMessage({
img: raw,
width: width,
height: height,
key: key
}, [raw]);
return new Promise((res) => {
worker.keyPool[key] = res;
});
} else {
const rgb = argument;
const [data, width, height] = [img.data, img.width, img.height];
const res = new Uint8ClampedArray(data);
rgbRemap(data, res, width, height, rgb);
return (
new ImageData(
res,
width,
height
)
);
}
} else {
return null;
}
}
function reRenderImage(image, scale) {
if (scale > 10) {
image.style['image-rendering'] = 'pixelated';
} else {
image.style['image-rendering'] = 'auto';
}
}
/*--- Get Images: Original, Diffed or Filtered ---*/
// Get original image function
function getOriginalImage(target, $overlay) {
if (target.easyCompare && target.easyCompare.originalImage) {
const originalImage = target.easyCompare.originalImage;
if (originalImage.ready) {
originalImage.style.width = `${scale * 10}%`;
reRenderImage(originalImage, scale);
}
return originalImage;
} else {
const originalCanvas = makeCanvas();
const updateProgress = (p) => {
if (p !== null && p >= 0) {
drawText(originalCanvas, `Loading ${(p * 100).toFixed(1)}%`);
} else if (p < 0) {
drawText(originalCanvas, `Loading...`);
}
};
const resolveOriginal = (src, onprogress, resolve) => {
GM_getImageData(src, onprogress).then(({ imageData: originalImageData, extension }) => {
resolve(originalImageData);
originalCanvas.src = src;
originalCanvas.ext = extension;
drawImage(originalCanvas, originalImageData);
originalCanvas.style.width = `${scale * 10}%`;
reRenderImage(originalCanvas, scale);
originalCanvas.ready = true;
});
};
drawText(originalCanvas, `Loading...`);
originalCanvas.ready = false;
originalCanvas.targetImage = target;
$overlay.append(originalCanvas.parentElement);
if (!target.easyCompare) {
target.easyCompare = {};
}
target.easyCompare.originalImage = originalCanvas;
target.easyCompare.originalImagePromise = onprogress => new Promise(async (resolve) => {
let realSrc = target.src;
// Parse original src from thumb src
for (let pairs of t2oLib) {
realSrc = realSrc.replace(pairs[0], pairs[1]);
if (realSrc !== target.src) {
resolveOriginal(realSrc, onprogress, resolve);
return;
}
}
// Guess original src from hyper link
let href, hrefOriginal;
if ((hrefOriginal = target.parentElement.href, href = hrefOriginal)) {
for (let pairs of skipRedirLib) {
href = href.replace(pairs[0], pairs[1]);
if (href !== hrefOriginal) {
break;
}
}
if (href.match(/\.png$|\.jpe?g$|\.webp|\.gif|\.bmp|\.svg$/)) {
resolveOriginal(href, onprogress, resolve);
return;
} else {
guessOriginalImage(href).then(src => {
resolveOriginal(src || realSrc, onprogress, resolve);
return;
});
}
} else {
resolveOriginal(realSrc, onprogress, resolve);
return;
}
});
target.easyCompare.originalImagePromise(updateProgress);
return originalCanvas;
}
}
// Get diffed image function
function getDiffedImage(target, base, $overlay) {
if (target.src === base.src) {
return getOriginalImage(target);
}
if (target.easyCompare && target.easyCompare[base.src]) {
target.easyCompare[base.src].targetImage = target;
target.easyCompare[base.src].baseImage = base;
const diffedCanvas = target.easyCompare[base.src];
if (diffedCanvas.ready) {
diffedCanvas.style.width = `${scale * 10}%`;
reRenderImage(diffedCanvas, scale);
}
return diffedCanvas;
} else {
const diffedCanvas = makeCanvas();
drawText(diffedCanvas, 'Loading...');
diffedCanvas.ready = false;
diffedCanvas.targetImage = target;
diffedCanvas.baseImage = base;
diffedCanvas.threshold = -1;
diffedCanvas.step = 0.001;
$overlay.append(diffedCanvas.parentElement);
if (!target.easyCompare) {
target.easyCompare = {};
}
target.easyCompare[base.src] = diffedCanvas;
if (!base.easyCompare) {
base.easyCompare = {};
}
base.easyCompare[target.src] = diffedCanvas;
let progress = [0, 0];
// Progress update function
const updateProgress = (p, ind) => {
if (p !== null && p >= 0 && ind !== null) {
progress[ind] = p;
drawText(diffedCanvas, `Loading ${((progress[0] + progress[1]) * 50).toFixed(1)}%`);
}
else if (p < 0) {
drawText(diffedCanvas, 'Loading...');
}
else {
drawText(diffedCanvas, 'Diffing...');
}
};
getOriginalImage(target, $overlay);
getOriginalImage(base, $overlay);
Promise.all([
target.easyCompare.originalImagePromise((p) => updateProgress(p, 0)),
base.easyCompare.originalImagePromise((p) => updateProgress(p, 1))
]).then(imageData => {
updateProgress(null, null);
return diffImage(...imageData, {
alpha: 0.5,
threshold: 0.007
});
}).then((diffedImageData) => {
if (diffedImageData === null) {
drawText(diffedCanvas, 'Sizes Not Match');
} else {
drawImage(diffedCanvas, diffedImageData);
diffedCanvas.ext = '.png';
diffedCanvas.threshold = 0.007;
diffedCanvas.style.width = `${scale * 10}%`;
reRenderImage(diffedCanvas, scale);
diffedCanvas.ready = true;
}
}).catch((err) => {
console.warn(err);
drawText(diffedCanvas, 'Something Went Wrong');
});
return diffedCanvas;
}
}
// Get filtered image function
function getFilteredImage(target, ftType, $overlay) {
if (target.easyCompare && target.easyCompare[ftType]) {
const filteredCanvas = target.easyCompare[ftType];
if (filteredCanvas.ready) {
filteredCanvas.style.width = `${scale * 10}%`;
reRenderImage(filteredCanvas, scale);
}
return filteredCanvas;
} else {
const filteredCanvas = makeCanvas();
drawText(filteredCanvas, 'Loading...');
filteredCanvas.ready = false;
filteredCanvas.targetImage = target;
$overlay.append(filteredCanvas.parentElement);
if (!target.easyCompare) {
target.easyCompare = {};
}
target.easyCompare[ftType] = filteredCanvas;
// Progress Update Function
const updateProgress = (p) => {
if (p !== null && p >= 0) {
drawText(filteredCanvas, `Loading ${(p * 100).toFixed(1)}%`);
} else if (p < 0) {
drawText(filteredCanvas, 'Loading...');
} else {
drawText(filteredCanvas, 'Filtering...');
}
};
// Wait original image and filter the original image
getOriginalImage(target, $overlay);
target.easyCompare
.originalImagePromise(updateProgress).then((imageData) => {
updateProgress(null);
return filterImage[ftType](imageData);
}).then(filterdImageData => {
drawImage(filteredCanvas, filterdImageData);
filteredCanvas.ext = '.png';
filteredCanvas.style.width = `${scale * 10}%`;
reRenderImage(filteredCanvas, scale);
filteredCanvas.ready = true;
});
return filteredCanvas;
}
}
/*--- UI Response Functions ---*/
// Function to acquire active image
function getActive($overlay) {
return $overlay.find('canvas:visible');
}
// Function fired when compare button is activated
function activateCompare($target) {
$target.attr({
'fill': '#008000'
}).css({
'cursor': 'pointer',
'opacity': '1'
})[0].state = true;
}
// Function fired when leaving image
function leaveImage($overlay, target = undefined) {
const original = getActive($overlay).hide()[0];
if (((original && (target = original.targetImage)) || target) &&
target.easyCompare && target.easyCompare.boxShadow !== undefined) {
$(target).css('box-shadow', target.easyCompare.boxShadow);
}
}
// Function fired when compare button is clicked and toggled on
// (Main UI Logic)
function enterCompare($overlay, $images, $message) {
if (Mousetrap) {
Mousetrap.pause();
}
$overlay.show()[0].state = true;
let colors = ['red', 'blue'];
let step = 1, baseImage;
let ftType = 'none';
let fadingTime = 300;
// Mouse enter event
$images.on('mouseenter.compare', (e, triggeredShiftKey) => {
const target = e.currentTarget;
clearTimeout(timeout);
leaveImage($overlay);
if (!target.easyCompare) {
target.easyCompare = {};
target.easyCompare.boxShadow = target.style['box-shadow'];
}
$(target).css({
'box-shadow': '0px 0px 8px ' + colors[0]
});
let displayedImage;
if ((e.shiftKey || triggeredShiftKey) && baseImage) {
displayedImage = $(getDiffedImage(target, baseImage, $overlay))
.css('outline-color', colors[0])
.show();
} else {
switch (ftType) {
case 'none':
displayedImage = $(getOriginalImage(target, $overlay))
.css('outline-color', colors[0])
.show();
break;
default:
displayedImage = $(getFilteredImage(target, ftType, $overlay))
.css('outline-color', colors[0])
.show();
break;
}
}
colors.push(colors.shift());
//Mouse leave event
}).on('mouseleave.compare', (e) => {
const target = e.currentTarget;
timeout = setTimeout(() => {
leaveImage($overlay, target);
}, 200);
});
// KeyBoard functions
function setBaseImage() {
try {
baseImage = getActive($overlay)[0].targetImage;
} catch (err) {
baseImage = undefined;
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
function downloadImage(name = 'easycompare') {
try {
const target = getActive($overlay)[0];
const url = target.src || target.toDataURL('image/png').replace(/^data:image\/[^;]/, 'data:application/octet-stream');
const ext = target.ext || '';
GM_download({
url: url,
name: name + ext
});
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
function toggleFilter(filter) {
ftType = (ftType === filter ? 'none' : filter);
try {
const target = getActive($overlay).hide()[0];
let $displayImage;
if (ftType === 'none') {
$displayImage = $(getOriginalImage(target.targetImage, $overlay));
} else {
$displayImage = $(getFilteredImage(target.targetImage, ftType, $overlay));
}
$displayImage
.css('outline-color', target.style['outline-color'])
.show();
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
function adjustView(up) {
try {
if (up && scale < 10) {
scale = scale + 1;
} else if (up && scale < 30) {
scale = scale + 2;
} else if (!up && scale > 10) {
scale = scale - 2;
} else if (!up && scale > 1) {
scale = scale - 1;
}
const target = getActive($overlay)[0];
if (target.ready) {
target.style.width = `${scale * 10}%`;
reRenderImage(target, scale);
}
$message.text(`Zoom: ${parseInt(scale * 10)}%`).css('opacity', '1');
setTimeout(() => {
$message.css('opacity', '0');
}, fadingTime);
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
function setView(scl) {
try {
if (scale !== scl) {
scale = scl;
const target = getActive($overlay)[0];
if (target.ready) {
target.style.width = `${scale * 10}%`;
reRenderImage(target, scale);
}
$message.text(`Zoom: ${parseInt(scale * 10)}%`).css('opacity', '1');
setTimeout(() => {
$message.css('opacity', '0');
}, fadingTime);
}
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
function adjustThreshold(up) {
try {
const target = getActive($overlay)[0];
let threshold = target.threshold;
if (threshold !== undefined && threshold >= 0) {
const thresholdPrev = threshold;
$message.text(`Threshold: ${thresholdPrev.toFixed(4)}`).css('opacity', '1');
if (up) {
threshold += target.step;
if (threshold > 1) {
threshold = 1;
}
} else {
threshold -= target.step;
if (threshold < 0) {
threshold = 0;
}
}
target.threshold = -1;
const [
baseCanvas,
targetCanvas
] = [
target.baseImage.easyCompare.originalImage,
target.targetImage.easyCompare.originalImage
];
diffImage(
baseCanvas.getContext('2d').getImageData(0, 0, baseCanvas.width, baseCanvas.height),
targetCanvas.getContext('2d').getImageData(0, 0, targetCanvas.width, targetCanvas.height),
{
alpha: 0.5,
threshold: threshold
}
).then((imageData) => {
target.getContext('2d').putImageData(imageData, 0, 0);
$message.text(`Threshold: ${threshold.toFixed(4)}`).css('opacity', '1');
setTimeout(() => {
target.threshold = threshold;
$message.css('opacity', '0');
}, fadingTime);
});
}
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
function adjustStep(left) {
try {
const target = getActive($overlay)[0];
let step = target.step;
if (step) {
if (left && step <= 0.1) {
target.step = step * 10;
} else if (left) {
target.step = 1.0;
} else if (!left && step >= 0.001) {
target.step = step / 10;
} else {
target.step = 0.0001;
}
$message.text(`Step: ${target.step.toFixed(4)}`).css('opacity', '1');
setTimeout(() => $message.css('opacity', '0'), fadingTime);
}
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
function clearCache() {
try {
leaveImage($overlay, getActive($overlay)[0].targetImage);
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
$overlay.find('canvas').toArray().forEach(e => {
const target = e.targetImage;
delete target.easyCompare;
e.parentElement.remove();
});
}
function switchImage(left, shiftKey) {
try {
const targetImage = getActive($overlay)[0].targetImage;
const index = $images.index(targetImage);
leaveImage($overlay, targetImage);
const nextElem = $images[left ? index - step : index + step] || $images[index];
$(nextElem).trigger('mouseenter', [shiftKey]);
} catch (err) {
if (!(err instanceof TypeError)) {
console.warn(err);
}
}
}
// Scroll and Keyboard event
$(document).on('scroll.compare', (e) => {
const temp = getActive($overlay)[0];
if (temp) {
const $prev = $(temp.targetImage);
if (!$prev.is(':hover')) {
leaveImage($overlay, $prev[0]);
$images.find('img:hover').trigger('mousenter');
}
}// Hot-Keys
}).on('keydown.compare', (e) => {
e.preventDefault();
e.stopImmediatePropagation();
switch (e.key) {
case 'Escape':
exitCompare($overlay, $images);
break;
case 'Shift':
setBaseImage();
break;
case '+': case '=':
if (e.ctrlKey) {
adjustView(true);
}
break;
case '-': case '_':
if (e.ctrlKey) {
adjustView(false);
}
break;
case 'O': case 'o':
if (e.ctrlKey) {
setView(10);
}
break;
case 'P': case 'p':
if (e.ctrlKey) {
setView(30);
}
break;
case 'S': case 's':
if (e.ctrlKey) {
downloadImage();
} else {
toggleFilter('solar');
}
break;
case 'A': case 'a':
toggleFilter('s2lar');
break;
case 'I': case 'i':
if (e.ctrlKey) {
setView(1);
} else {
adjustThreshold(true);
}
break;
case 'ArrowUp':
adjustThreshold(true);
break;
case 'K': case 'k': case 'ArrowDown':
adjustThreshold(false);
break;
case 'J': case 'j': case 'ArrowLeft':
adjustStep(true);
break;
case 'L': case 'l': case 'ArrowRight':
if (e.ctrlKey) {
clearCache();
} else {
adjustStep(false);
}
break;
case 'Q':
case 'q':
$overlay.css('opacity', 0.5);
break;
case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
step = parseInt(e.key);
break;
case '0':
step = 10;
break;
case 'W': case 'w':
switchImage(true, e.shiftKey);
break;
case 'E': case 'e':
switchImage(false, e.shiftKey);
break;
}
return false;
}).on('keyup.compare', (e) => {
e.preventDefault();
e.stopImmediatePropagation();
switch (e.key) {
case 'Q':
case 'q':
$overlay.css('opacity', '');
break;
}
return false;
});
}
// Function fired when compare button is clicked and toggled off
// or quit via keyboard 'esc'
function exitCompare($overlay, $images) {
if (Mousetrap) {
Mousetrap.unpause();
}
leaveImage($overlay);
$overlay.hide()[0].state = false;
$images
.off('mouseenter.compare')
.off('mouseleave.compare');
$(document)
.off('scroll.compare')
.off('keydown.compare');
}
/*--- Building Blocks ---*/
// A message on the whole page
const $message = $('<div>').css({
'top': '50%',
'left': '50%',
'z-index': 2147483647,
'position': 'fixed',
'transform': 'translate(-50%, -50%)',
'opacity': '0',
'vertical-align': 'middle',
'pointer-events': 'none',
'transition': 'all 0.1s',
'font-size': '500%',
'color': 'yellow',
'font-weight': 'bold'
});
// An overlay on the whole page
const $overlay = $('<div/>').css({
'id': 'easy-compare-overlay',
'position': 'fixed',
'top': 0,
'right': 0,
'bottom': 0,
'left': 0,
'z-index': 2147483646,
'background-color': 'rgba(0, 0, 0, 0.75)',
'pointer-events': 'none',
'display': 'none'
}).append($message);
// The compare button
const $compareButton = $(`<svg xmlns="http://www.w3.org/2000/svg">
<path id="ld" d="M20 6H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h10v4h4V2h-4v4zm0 30H10l10-12v12zM38 6H28v4h10v26L28 24v18h10c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4z"/>
</svg>`).attr({
'width': '30',
'height': '30',
'viewBox': '0 0 48 48',
'stroke': 'white',
'stroke-width': '5px',
'fill': 'gray'
}).css({
'position': 'fixed',
'top': '0px',
'right': '0px',
'padding': '15px',
'box-sizing': 'content-box',
'z-index': 2147483647,
'paint-order': 'stroke',
'opacity': 0,
'transition': 'all 0.2s',
'cursor': 'auto'
}).on('mouseenter', (e) => {
const $target = $(e.currentTarget);
if ($target[0].manualFlag) {
$target.attr({
'fill': 'gray'
}).css({
'opacity': 0.2,
'pointer-events': 'none'
});
$target[0].manualFlag = false;
const clientWidth = document.documentElement.clientWidth;
$(document).on('mousemove.compare', ({ clientX, clientY }) => {
if (clientX < clientWidth - 61 || clientY > 61) {
$target[0].insideFlag = 0;
clearTimeout(timeout);
$target.attr({
'fill': 'gray'
}).css({
'cursor': 'auto',
'opacity': 0,
'pointer-events': 'auto'
})[0].state = false;
$(document).off('mousemove.compare');
$target[0].manualFlag = true;
} else if (clientX >= clientWidth - 45 && clientX <= clientWidth - 15 && clientY >= 15 && clientY <= 45) {
if (!$target[0].insideFlag) {
$target[0].insideFlag = 1;
timeout = setTimeout(() => {
activateCompare($target);
$target.css({
'pointer-events': 'auto'
});
}, $overlay[0].state ? 0 : 1000);
}
} else if (clientX < clientWidth - 45 || clientX > clientWidth - 15 || clientY < 15 || clientY > 45) {
$target[0].insideFlag = 0;
clearTimeout(timeout);
$target.attr({
'fill': 'gray'
}).css({
'cursor': 'auto',
'opacity': 0.2,
'pointer-events': 'none'
})[0].state = false;
}
});
}
}).click((e) => {
if (e.currentTarget.state) {
switch ($overlay[0].state) {
case false:
enterCompare($overlay, $(':not("#easy-compare-overlay") img:visible'), $message);
break;
case true:
exitCompare($overlay, $(':not("#easy-compare-overlay") img:visible'));
break;
}
}
else {
let x = e.clientX;
let y = e.clientY;
const lowerElement = document
.elementsFromPoint(x, y)
.find(e => !['svg', 'path'].includes(e.tagName));
lowerElement.click();
}
}).mousedown((e) => {
if (e.currentTarget.state) {
$(e.currentTarget).attr({
'fill': '#006000'
});
}
}).mouseup((e) => {
if (e.currentTarget.state) {
$(e.currentTarget).attr({
'fill': '#008000'
});
}
});
$compareButton[0].manualFlag = true;
$compareButton[0].insideFlag = false;
/*--- Insert to Document ---*/
$overlay[0].state = false;
$compareButton[0].state = false;
$('body').append($compareButton).append($overlay);
})(window.$.noConflict(true),
unsafeWindow.Mousetrap,
window.pixelmatch,
unsafeWindow.URL.createObjectURL ?
unsafeWindow.URL :
unsafeWindow.webkitURL);