Displays a base image and an "x-ray" view of a second image where the mouse is pointing
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

507 lines
16 KiB

  1. "use strict";
  2. let overlayLoaded = false;
  3. let baseLoaded = false;
  4. let running = false;
  5. let radius = 200;
  6. let softness = 25;
  7. let width;
  8. let height;
  9. let border = false;
  10. let fitScreen = true;
  11. let paintMode = false;
  12. let scale;
  13. document.addEventListener("DOMContentLoaded", e => {
  14. document.querySelector("#reset-button").addEventListener("click", reset);
  15. document.querySelector("#load-button").addEventListener("click", e => {
  16. console.log("Trying to load...");
  17. const baseInput = document.querySelector("#base-url").value;
  18. const overlayInput = document.querySelector("#overlay-url").value;
  19. let success = true;
  20. try {
  21. let baseURL = new URL(baseInput)
  22. console.log(baseURL);
  23. } catch {
  24. document.querySelector("#base-url").value = "";
  25. document.querySelector("#base-url").placeholder = "Invalid URL...";
  26. success = false;
  27. }
  28. try {
  29. let overlayURL = new URL(overlayInput)
  30. console.log(overlayURL);
  31. } catch {
  32. document.querySelector("#overlay-url").value = "";
  33. document.querySelector("#overlay-url").placeholder = "Invalid URL...";
  34. success = false;
  35. }
  36. if (!success) {
  37. return;
  38. }
  39. const artistLink = document.querySelector("#artist");
  40. let artistURL = document.querySelector("#artist-url").value;
  41. if (artistURL) {
  42. artistLink.href = artistURL;
  43. artistLink.style.removeProperty("display");
  44. } else {
  45. artistLink.style.display = "none";
  46. }
  47. const overlayImg = document.querySelector("#overlay-img");
  48. const baseImg = document.querySelector("#base-img");
  49. overlayImg.src = overlayInput;
  50. baseImg.src = baseInput;
  51. setURL();
  52. load();
  53. try {
  54. localStorage.setItem("base", baseInput);
  55. localStorage.setItem("overlay", overlayInput);
  56. } catch {
  57. console.error("Couldn't set something in local storage :(")
  58. }
  59. });
  60. let url = new URL(window.location);
  61. const overlay = document.querySelector("#overlay");
  62. document.addEventListener("mousedown", e => {
  63. let x = e.clientX - e.target.getBoundingClientRect().x;
  64. let y = e.clientY - e.target.getBoundingClientRect().y;
  65. updateOverlay([[x,y]], e.buttons % 2 != 0);
  66. });
  67. document.addEventListener("mousemove", e => {
  68. let x = e.clientX - e.target.getBoundingClientRect().x;
  69. let y = e.clientY - e.target.getBoundingClientRect().y;
  70. updateOverlay([[x,y]], e.buttons % 2 != 0);
  71. });
  72. document.addEventListener("touchstart", e => {
  73. let offsetX = e.target.getBoundingClientRect().x;
  74. let offsetY = e.target.getBoundingClientRect().y;
  75. let touches = [];
  76. for (let i=0; i < e.touches.length; i++) {
  77. let x = e.touches[i].clientX - offsetX;
  78. let y = e.touches[i].clientY - offsetY;
  79. touches.push([x,y]);
  80. }
  81. updateOverlay(touches, true);
  82. });
  83. document.addEventListener("touchmove", e => {
  84. let offsetX = e.target.getBoundingClientRect().x;
  85. let offsetY = e.target.getBoundingClientRect().y;
  86. let touches = [];
  87. for (let i=0; i < e.touches.length; i++) {
  88. let x = e.touches[i].clientX - offsetX;
  89. let y = e.touches[i].clientY - offsetY;
  90. touches.push([x,y]);
  91. }
  92. updateOverlay(touches, true);
  93. });
  94. document.querySelector("#radius-slider").addEventListener("input", e => {
  95. try {
  96. radius = parseInt(e.target.value);
  97. document.querySelector("#radius-input").value = radius;
  98. } catch {
  99. console.warn("That wasn't a valid radius: " + e.target.value);
  100. }
  101. });
  102. document.querySelector("#radius-slider").addEventListener("change", e => {
  103. try {
  104. radius = parseInt(e.target.value);
  105. document.querySelector("#radius-input").value = radius;
  106. } catch {
  107. console.warn("That wasn't a valid radius: " + e.target.value);
  108. }
  109. setURL();
  110. });
  111. document.querySelector("#radius-input").addEventListener("input", e => {
  112. try {
  113. radius = parseInt(e.target.value);
  114. document.querySelector("#radius-slider").value = radius;
  115. } catch {
  116. console.warn("That wasn't a valid radius: " + e.target.value);
  117. }
  118. });
  119. document.querySelector("#radius-input").addEventListener("change", e => {
  120. try {
  121. radius = parseInt(e.target.value);
  122. document.querySelector("#radius-slider").value = radius;
  123. } catch {
  124. console.warn("That wasn't a valid radius: " + e.target.value);
  125. }
  126. setURL();
  127. });
  128. document.querySelector("#softness-slider").addEventListener("input", e => {
  129. try {
  130. softness = parseInt(e.target.value);
  131. document.querySelector("#softness-input").value = softness;
  132. } catch {
  133. console.warn("That wasn't a valid softness: " + e.target.value);
  134. }
  135. });
  136. document.querySelector("#softness-slider").addEventListener("change", e => {
  137. try {
  138. softness = parseInt(e.target.value);
  139. document.querySelector("#softness-input").value = softness;
  140. } catch {
  141. console.warn("That wasn't a valid softness: " + e.target.value);
  142. }
  143. setURL();
  144. });
  145. document.querySelector("#softness-input").addEventListener("input", e => {
  146. try {
  147. softness = parseInt(e.target.value);
  148. document.querySelector("#softness-slider").value = softness;
  149. } catch {
  150. console.warn("That wasn't a valid softness: " + e.target.value);
  151. }
  152. });
  153. document.querySelector("#softness-input").addEventListener("change", e => {
  154. try {
  155. softness = parseInt(e.target.value);
  156. document.querySelector("#softness-slider").value = softness;
  157. } catch {
  158. console.warn("That wasn't a valid softness: " + e.target.value);
  159. }
  160. setURL();
  161. });
  162. // see if we have params already; if so, use them!
  163. const overlayImg = document.querySelector("#overlay-img");
  164. const baseImg = document.querySelector("#base-img");
  165. const baseInput = document.querySelector("#base-url");
  166. const overlayInput = document.querySelector("#overlay-url");
  167. const artistInput = document.querySelector("#artist-url");
  168. const artistLink = document.querySelector("#artist");
  169. if (url.searchParams.has("base") && url.searchParams.has("overlay")) {
  170. let baseURL = url.searchParams.get("base");
  171. let overlayURL = url.searchParams.get("overlay");
  172. let artistURL = null;
  173. if (url.searchParams.has("artist")) {
  174. artistURL = url.searchParams.get("artist");
  175. }
  176. baseImg.src = baseURL;
  177. overlayImg.src = overlayURL;
  178. baseInput.value = baseURL;
  179. overlayInput.value = overlayURL;
  180. if (artistURL) {
  181. artistLink.href = artistURL;
  182. artistInput.value = artistURL;
  183. artistLink.style.removeProperty("display");
  184. } else {
  185. artistLink.style.display = "none";
  186. }
  187. load();
  188. } else {
  189. try {
  190. baseInput.value = localStorage.getItem("base");
  191. overlayInput.value = localStorage.getItem("overlay");
  192. } catch {
  193. console.error("Couldn't get something from local storage :(")
  194. }
  195. }
  196. if (url.searchParams.has("radius")) {
  197. try {
  198. radius = parseInt(url.searchParams.get("radius"));
  199. document.querySelector("#radius-slider").value = radius;
  200. document.querySelector("#radius-input").value = radius;
  201. } catch {
  202. console.warn("That was a bogus radius...");
  203. }
  204. }
  205. if (url.searchParams.has("softness")) {
  206. try {
  207. softness = parseInt(url.searchParams.get("softness"));
  208. document.querySelector("#softness-slider").value = softness;
  209. document.querySelector("#softness-input").value = softness;
  210. } catch {
  211. console.warn("That was a bogus softness...");
  212. }
  213. }
  214. if (url.searchParams.has("border")) {
  215. try {
  216. border = 1 == parseInt(url.searchParams.get("border"));
  217. document.querySelector("#show-border").checked = border;
  218. } catch {
  219. console.warn("That was a bogus softness...");
  220. }
  221. }
  222. window.addEventListener("resize", e => {
  223. if (running) {
  224. setup();
  225. }
  226. })
  227. document.querySelector("#fullscreen-button").addEventListener("click", function toggleFullScreen() {
  228. var doc = window.document;
  229. var docEl = doc.documentElement;
  230. var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
  231. var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
  232. if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
  233. requestFullScreen.call(docEl);
  234. }
  235. else {
  236. cancelFullScreen.call(doc);
  237. }
  238. });
  239. document.querySelector("#show-border").addEventListener("change", e => {
  240. border = e.target.checked;
  241. setURL();
  242. });
  243. document.querySelector("#paint-mode").addEventListener("change", e => {
  244. paintMode = e.target.checked;
  245. });
  246. document.querySelector("#fit-screen").addEventListener("change", e => {
  247. fitScreen = e.target.checked;
  248. setup();
  249. });
  250. });
  251. function load() {
  252. document.querySelector("#menu").classList.remove("start");
  253. const overlayImg = document.querySelector("#overlay-img");
  254. const baseImg = document.querySelector("#base-img");
  255. overlayImg.addEventListener("load", function overlayLoad() {
  256. console.log("The overlay is loaded");
  257. overlayLoaded = true;
  258. if (overlayLoaded && baseLoaded) {
  259. setup();
  260. }
  261. overlayImg.removeEventListener("load", overlayLoad);
  262. })
  263. baseImg.addEventListener("load", function baseLoad() {
  264. console.log("The base is loaded");
  265. baseLoaded = true;
  266. if (overlayLoaded && baseLoaded) {
  267. setup();
  268. }
  269. baseImg.removeEventListener("load", baseLoad);
  270. })
  271. }
  272. function reset() {
  273. running = false;
  274. baseLoaded = false;
  275. overlayLoaded = false;
  276. const overlay = document.querySelector("#overlay");
  277. const base = document.querySelector("#base");
  278. const overlayResized = document.querySelector("#overlay-resized");
  279. const baseResized = document.querySelector("#base-resized");
  280. document.querySelector("#menu").classList.add("start");
  281. overlay.classList.add("hidden");
  282. base.classList.add("hidden");
  283. }
  284. function setup() {
  285. running = true;
  286. const overlay = document.querySelector("#overlay");
  287. const base = document.querySelector("#base");
  288. const overlayResized = document.querySelector("#overlay-resized");
  289. const baseResized = document.querySelector("#base-resized");
  290. overlay.classList.remove("hidden");
  291. base.classList.remove("hidden");
  292. const overlayImg = document.querySelector("#overlay-img");
  293. const baseImg = document.querySelector("#base-img");
  294. /** @type {CanvasRenderingContext2D} */
  295. const overlayCtx = overlay.getContext("2d");
  296. /** @type {CanvasRenderingContext2D} */
  297. const baseCtx = base.getContext("2d");
  298. /** @type {CanvasRenderingContext2D} */
  299. const overlayCtxResized = overlayResized.getContext("2d");
  300. /** @type {CanvasRenderingContext2D} */
  301. const baseCtxResized = baseResized.getContext("2d");
  302. const availableWidth = document.querySelector("#fill-div").getBoundingClientRect().width;
  303. const availableHeight = document.querySelector("#fill-div").getBoundingClientRect().height;
  304. const scaleW = availableWidth / baseImg.width;
  305. const scaleH = availableHeight / baseImg.height;
  306. scale = fitScreen ? Math.min(scaleW, scaleH) : 1;
  307. width = fitScreen ? Math.floor(availableWidth * scale / scaleW) : baseImg.width;
  308. height = fitScreen ? Math.floor(availableHeight * scale / scaleH) : baseImg.height;
  309. [baseCtx, baseCtxResized, overlayCtx, overlayCtxResized].forEach(ctx => {
  310. ctx.canvas.width = width;
  311. ctx.canvas.height = height;
  312. ctx.canvas.style.left = fitScreen ? (availableWidth - width) / 2 + "px" : 0;
  313. ctx.canvas.style.top = fitScreen ? (availableHeight - height) / 2 + "px" : 0;
  314. });
  315. baseCtxResized.drawImage(baseImg, 0, 0, width, height);
  316. baseCtx.drawImage(baseResized, 0, 0, width, height);
  317. overlayCtxResized.drawImage(overlayImg, 0, 0, width, height);
  318. console.log("Done");
  319. }
  320. function ease(t, k) {
  321. return 1 - Math.pow(2, -k * (1 - t));
  322. }
  323. function updateOverlay(points, clicked) {
  324. const overlay = document.querySelector("#overlay");
  325. const overlayResized = document.querySelector("#overlay-resized");
  326. /** @type {CanvasRenderingContext2D} */
  327. const overlayCtx = overlay.getContext("2d");
  328. /** @type {CanvasRenderingContext2D} */
  329. const overlayCtxResized = overlay.getContext("2d");
  330. const w = overlayCtx.canvas.width;
  331. const h = overlayCtx.canvas.height;
  332. overlayCtx.save();
  333. overlayCtx.globalCompositeOperation = "source-over";
  334. if (!paintMode)
  335. overlayCtx.clearRect(0, 0, w, h);
  336. if (!paintMode || clicked) {
  337. points.forEach(point => {
  338. const [x,y] = point;
  339. overlayCtx.beginPath();
  340. overlayCtx.ellipse(x, y, radius * scale, radius * scale, 0, 0, 2 * Math.PI);
  341. const gradient = overlayCtx.createRadialGradient(x, y, 0, x, y, Math.floor(radius * scale));
  342. const maxOpacity = ease(0, 1 / (0.00001 + softness / 100));
  343. const steps = 20;
  344. for (let t=0 ; t <= steps; t+= 1) {
  345. let eased = ease(t/steps, 1 / (0.00001 + softness / 100)) / maxOpacity;
  346. gradient.addColorStop(t/steps, `rgba(0, 0, 0, ${eased}`);
  347. }
  348. let eased = ease(0.999, 1 / (0.00001 + softness / 100)) / maxOpacity;
  349. gradient.addColorStop(0.999, `rgba(0, 0, 0, ${eased}`);
  350. overlayCtx.fillStyle = gradient;
  351. overlayCtx.fill();
  352. })
  353. }
  354. overlayCtx.globalCompositeOperation = "source-in";
  355. overlayCtx.drawImage(overlayResized, 0, 0);
  356. overlayCtx.globalCompositeOperation = "source-over";
  357. if (!paintMode && border) {
  358. points.forEach(point => {
  359. const [x, y] = point;
  360. overlayCtx.strokeStyle = "#000";
  361. overlayCtx.lineWidth = 3;
  362. overlayCtx.beginPath();
  363. overlayCtx.ellipse(x, y, radius * scale, radius * scale, 0, 0, 2 * Math.PI);
  364. overlayCtx.stroke();
  365. });
  366. }
  367. overlayCtx.restore();
  368. }
  369. function setURL() {
  370. let shareURL = new URL(window.location);
  371. // for some reason, the parser gets confused by urlencoded urls...
  372. // so, to get rid of all parameters, we do this
  373. let keys = Array.from(shareURL.searchParams.keys());
  374. do {
  375. keys = Array.from(shareURL.searchParams.keys());
  376. keys.forEach(key => {
  377. shareURL.searchParams.delete(key);
  378. });
  379. } while (keys.length > 0)
  380. const artistLink = document.querySelector("#artist");
  381. const overlayImg = document.querySelector("#overlay-img");
  382. const baseImg = document.querySelector("#base-img");
  383. shareURL.searchParams.append("base", baseImg.src);
  384. shareURL.searchParams.append("overlay", overlayImg.src);
  385. if (artistLink.href) {
  386. shareURL.searchParams.append("artist", artistLink.href);
  387. }
  388. shareURL.searchParams.append("radius", radius);
  389. shareURL.searchParams.append("softness", softness);
  390. if (border) {
  391. shareURL.searchParams.append("border", 1);
  392. }
  393. window.history.replaceState(null, "X-Ray Viewer", shareURL);
  394. }