diff --git a/imgxis.user.js b/imgxis.user.js new file mode 100644 index 0000000..b0b00ef --- /dev/null +++ b/imgxis.user.js @@ -0,0 +1,422 @@ +// ==UserScript== +// @name IMGXIS +// @namespace http://yellowafterlife.itch.io/imgxis +// @version 1.07 +// @description A userscript for better image viewing experience in browsers. +// @include *://*.png* +// @include *://*.jpg* +// @include *://*.jpeg* +// @include *://*.gif* +// @include *://*.bmp* +// @include *://*.webp* +// @connect * +// @include *://textures.minecraft.net/texture/* +// @grant GM_getValue +// @grant GM_setValue +// @copyright 2015+, YellowAfterlife +// ==/UserScript== + +// Revisions: +// 1.06 (Sep 28, 2015): No longer blocking the default behaviour for alt+dragging the image. +// 1.05 (Sep 26, 2015): Added a color picker (Ctrl+C to copy a color under the cursor); +// 1.04 (Sep 25, 2015): Image are now "smoothed" when zoomed out; Fixed Firefox beta compatibilty. +// 1.03 (Sep 24, 2015): Added a button to view the image in tiled form. +// 1.02 (Jul 19, 2015): F5 now refreshes image without reloading the page. Shift+F5 for timer. +// 1.01 (Apr 01, 2015): Added color presets (instead of a single color for all slots). +// 1.00 (Mar 31, 2015): Initial release. + +// parameter group: +var menuButtonWidth = 32; // in pixels +var menuColors = 10; // how many color buttons to show in the menu +var colorPickerEnabled = true; // whether to allow to Ctrl+C colors from the image +var colorPickerMode = 0; // 0: hex, 1: rgb, 2: rgba + +// ensure that the page contains nothing but a single : +if (document.body.children.length != 1) return; +var q = document.body.children[0]; +if (q.tagName != "IMG") return; + +// stylesheet: +var css = document.createElement("style"); +css.type = "text/css"; +css.innerHTML = [ + ":root {", + " background: none !important;", + "}", + "body {", + " overflow: hidden;", + "}", + // panner area + ".imgxis-panner {", + " position: absolute;", + " left: 0; width: 100%;", + " top: 0; height: 100%;", + "}", + // cursors + ".imgxis-panner, .imgxis-panner img {", + " cursor: move;", + "}", + ".imgxis-panner.colorpick, .imgxis-panner.colorpick img {", + " cursor: default;", + " cursor: copy;", + "}", + // disable interpolation: + ".imgxis-panner.zoomed, .imgxis-panner.zoomed img {", + " -ms-interpolation-mode: nearest-neighbor;", + " image-rendering: optimizeSpeed;", + " image-rendering: -moz-crisp-edges;", + " image-rendering: -webkit-optimize-contrast;", + " image-rendering: -o-crisp-edges;", + " image-rendering: pixelated;", + "}", + // actual image: + ".imgxis-panner img {", + // disable Firefox' centering: + " position: relative;", + " margin: 0;", + " background-color: transparent !important;", + "}", + // menu pane on top: + ".imgxis-menu {", + " position: absolute;", + " left: 0; width: 100%;", + " top: 0; height: 24px;", + " background: rgba(0, 0, 0, 0.7);", + " padding: 4px;", + " color: white;", + " font: 16px sans-serif;", + " line-height: 24px;", + " min-width: 1024px;", + "}", + // style the buttons in the menu: + ".imgxis-menu input {", + " width: " + menuButtonWidth + "px;", + " height: 24px;", + " padding: 0px;", + " margin-right: 2px;", + " float: left;", + "}", +].join("\n"); +document.body.appendChild(css); + +// the thing is that browser has it's own zoom in controls, and they are +// linked to that particular natively. So we need to make a copy of it, strip +// it of any unneeded styles, and put it in place of the original: +var o = q.cloneNode(); +o.removeAttribute("width"); +o.removeAttribute("height"); +var panner = document.createElement("div"); +var pannerZoomed = false; +panner.className = "imgxis-panner"; +panner.appendChild(o); +q.parentNode.appendChild(panner); +q.parentNode.removeChild(q); +var s = o.style; +s.transformOrigin = "top left"; +q = null; +var tile = false; +// +var w = 0, + h = 0, + x = 0, + y = 0, + z = 0, + m = 1; + +// pan and zoom: +function update() { + var pz = m > 1; + if (pz != pannerZoomed) { + pannerZoomed = pz; + var cl = panner.classList; + if (pz) cl.add("zoomed"); + else cl.remove("zoomed"); + } + if (tile) { + var p = panner.style; + p.backgroundPosition = -x + "px " + -y + "px"; + p.backgroundSize = o.width * m + "px"; + } + s.transform = "matrix(" + m + ",0,0," + m + "," + -x + "," + -y + ")"; +} +function zoomto(zx, zy, d) { + var p = m; + m = Math.pow(2, (z += d)); + var f = m / p; + x = (zx + x) * f - zx; + y = (zy + y) * f - zy; + menu_zoom.innerHTML = (0 | (m * 100)) + "%"; + update(); +} + +// color picker: +var colorPickerCanvas = document.createElement("canvas"); +var colorPickerContext = colorPickerCanvas.getContext("2d"); +var colorPickerColor = document.createElement("input"); +colorPickerColor.type = "color"; +colorPickerColor.style.width = "24px"; +var colorPickerField = document.createElement("input"); +colorPickerField.type = "text"; +colorPickerField.style.width = "64px"; +colorPickerColor.title = colorPickerField.title = + "Ctrl+C to copy a color under the cursor in here."; +var colorPickerActive = false; +function colorPickerUpdate() { + var w = o.width; + var h = o.height; + if (w <= 0 || h <= 0) return; + if (colorPickerCanvas.width != w) { + colorPickerCanvas.width = w; + } else if (colorPickerCanvas.height != h) { + colorPickerCanvas.height = h; + } else colorPickerContext.clearRect(0, 0, w, h); + colorPickerContext.drawImage(o, 0, 0); + var rx = (mx + x) / m; + rx %= w; + if (rx < 0) rx += w; + var ry = (my + y) / m; + ry %= h; + if (ry < 0) ry += h; + try { + var d = colorPickerContext.getImageData(~~rx, ~~ry, 1, 1).data; + var hex = "0123456789ABCDEF"; + var chx = "#"; + for (k = 0; k < 3; k++) + chx += hex.charAt(d[k] >> 4) + hex.charAt(d[k] & 15); + // + var out = "", + i, + k; + switch (colorPickerMode) { + case 1: + out = d[0] + "," + d[1] + "," + d[2]; + break; + case 2: + out = d[0] + "," + d[1] + "," + d[2] + "," + (d[3] / 255).toFixed(3); + break; + default: + out = chx; + } + colorPickerColor.value = chx; + colorPickerField.value = out; + colorPickerField.select(); + } catch (_) { + colorPickerField.value = "(error)"; + } +} +function colorPickerDeactivate() { + colorPickerActive = false; + panner.classList.remove("colorpick"); +} + +// menu: +var menu_zoom = null, + menu; +(function () { + menu = document.createElement("div"); + menu.className = "imgxis-menu"; + function menubt(s, f) { + var bt = document.createElement("input"); + bt.type = "button"; + bt.value = s; + bt.addEventListener("click", f); + menu.appendChild(bt); + } + var defcolors = [ + "#1A1D23", + "#60C19D", + "#AB7680", + "#FFFFFF", + "#F4F2EC", + "#CAC2BD", + "#88898E", + "#4F556A", + "#1D1F2C", + "#000000", + ]; + function menucl(i) { + var bt = document.createElement("input"); + bt.type = "color"; + bt.value = GM_getValue("imgxis-color" + i, defcolors[i]); + function bt_apply() { + document.body.style.backgroundColor = bt.value; + } + bt.addEventListener("click", function (e) { + if (!e.shiftKey) { + e.preventDefault(); + bt_apply(); + } + }); + bt.addEventListener("change", function (e) { + GM_setValue("imgxis-color" + i, bt.value); + bt_apply(); + }); + bt.title = + "Color " + (i + 1) + ".\nClick to apply.\nShift+click to change."; + menu.appendChild(bt); + if (i == 0) bt_apply(); + } + for (var i = 0; i < menuColors; i++) menucl(i); + menubt("-", function (_) { + zoomto(window.innerWidth / 2, window.innerHeight / 2, -0.5); + }); + menubt("1:1", function (_) { + var iw = window.innerWidth, + ih = window.innerHeight; + zoomto(iw / 2, ih / 2, -z); + x = (w - iw) / 2; + y = (h - ih) / 2; + update(); + }); + menubt("+", function (_) { + zoomto(window.innerWidth / 2, window.innerHeight / 2, 0.5); + }); + menubt("tile", function (_) { + tile = !tile; + panner.style.backgroundImage = tile ? "url(" + o.src + ")" : ""; + update(); + }); + // + if (colorPickerEnabled) { + menu.appendChild(colorPickerColor); + menu.appendChild(colorPickerField); + } + // + menu_zoom = document.createElement("span"); + menu_zoom.innerHTML = "100%"; + menu.appendChild(menu_zoom); + document.body.appendChild(menu); +})(); + +// mouse controls: +function onmousewheel(e) { + var d = e.wheelDelta || -e.detail; + d = (d < 0 ? -1 : d > 0 ? 1 : 0) * 0.5; + zoomto(e.pageX, e.pageY, d); +} +var mx = 0, + my = 0, + mp = false; +function onmousemove(e) { + var ox = mx; + mx = e.pageX; + var dx = mx - ox; + var oy = my; + my = e.pageY; + var dy = my - oy; + if (mp) { + x -= mx - ox; + y -= my - oy; + update(); + } else if (colorPickerActive) { + colorPickerUpdate(); + } +} +function onmousedown(e) { + onmousemove(e); + if (e.which != 3 && !e.altKey) { + // not the right click + e.preventDefault(); // disable image "grab" + mp = true; + } +} +function onmouseup(e) { + onmousemove(e); + mp = false; +} + +// +var refresh_timer = null; +function refresh() { + var o_src = o.src; + // strip the previous timestamp parameter: + var pos = o_src.indexOf("imgxis-time"); + if (pos >= 0) { + switch (o_src.charAt(pos - 1)) { + case "&": + case "?": + pos--; + break; + } + o_src = o_src.substring(0, pos); + } + // add a timestamp parameter: + o_src += o_src.indexOf("?") >= 0 ? "&" : "?"; + o_src += "imgxis-time=" + Date.now(); + // + o.src = o_src; + if (tile) panner.style.backgroundImage = "url(" + o.src + ")"; +} + +// +function onkeydown(e) { + switch (e.keyCode) { + case 17: + if (colorPickerEnabled) { + colorPickerActive = true; + colorPickerUpdate(); + panner.classList.add("colorpick"); + } + break; + case 116: + e.preventDefault(); + if (e.shiftKey) { + if (refresh_timer == null) { + var t = parseFloat(prompt("Refresh interval (seconds)", "15")); + if (!isNaN(t)) + refresh_timer = setInterval(refresh, Math.max(100, t * 1000)); + } else clearInterval(refresh_timer); + } else refresh(); + break; + } +} +function onkeyup(e) { + switch (e.keyCode) { + case 17: + if (colorPickerActive) colorPickerDeactivate(); + break; + } +} +function onblur(e) { + if (colorPickerActive) colorPickerDeactivate(); +} +window.addEventListener("mousemove", onmousemove); +panner.addEventListener("mousedown", onmousedown); +window.addEventListener("mouseup", onmouseup); +window.addEventListener("mousewheel", onmousewheel); +window.addEventListener("DOMMouseScroll", onmousewheel); +window.addEventListener("keydown", onkeydown); +window.addEventListener("keyup", onkeyup); +window.addEventListener("blur", onblur); +// consider that the image dimensions may not be available instantly: +var onwait_t = 1000; +var onwait = function onwait() { + w = o.width; + h = o.height; + if (w > 0 && h > 0) { + var iw = window.innerWidth, + lw = w; + var ih = window.innerHeight, + lh = h; + if (lw < iw && lh < ih) + for (var k = 0; k < 3; k++) { + if (lw * 2 < iw && lh * 2 < ih) { + z++; + lw *= 2; + lh *= 2; + } + } + else + while (lw > iw || lh > ih) { + z--; + lw /= 2; + lh /= 2; + } + m = Math.pow(2, z); + menu_zoom.innerHTML = (0 | (m * 100)) + "%"; + x = -(iw - lw) / 2; + y = -(ih - lh) / 2; + update(); + } else if (--onwait_t > 0) setTimeout(onwait, 10); +}; +onwait();