// ==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();