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.
 
 
 
 

687 lines
23 KiB

  1. "use strict";
  2. let overlayLoaded = false;
  3. let baseLoaded = false;
  4. let running = false;
  5. let radius = 200;
  6. let softness = 0;
  7. let darkness = 0;
  8. let opacity = 100;
  9. let width;
  10. let height;
  11. let border = true;
  12. let fitScreen = true;
  13. let paintMode = false;
  14. let offsetMode = false;
  15. let shadow = true;
  16. let firstTime = true;
  17. let scale;
  18. document.addEventListener("DOMContentLoaded", e => {
  19. document.querySelector("#reset-button").addEventListener("click", reset);
  20. document.querySelector("#show-toolbar").addEventListener("click", e => {
  21. document.querySelector("#menu").classList.remove("hidden")
  22. document.querySelector("#show-toolbar").classList.add("hidden")
  23. setup();
  24. })
  25. document.querySelector("#hide-toolbar").addEventListener("click", e => {
  26. document.querySelector("#menu").classList.add("hidden")
  27. document.querySelector("#show-toolbar").classList.remove("hidden")
  28. setup();
  29. })
  30. document.querySelector("#show-toolbar").classList.add("hidden")
  31. document.querySelector("#load-button").addEventListener("click", e => {
  32. console.log("Trying to load...");
  33. const baseInput = document.querySelector("#base-url").value;
  34. const overlayInput = document.querySelector("#overlay-url").value;
  35. let success = true;
  36. try {
  37. let baseURL = new URL(baseInput)
  38. console.log(baseURL);
  39. } catch {
  40. document.querySelector("#base-url").value = "";
  41. document.querySelector("#base-url").placeholder = "Invalid URL...";
  42. success = false;
  43. }
  44. try {
  45. let overlayURL = new URL(overlayInput)
  46. console.log(overlayURL);
  47. } catch {
  48. document.querySelector("#overlay-url").value = "";
  49. document.querySelector("#overlay-url").placeholder = "Invalid URL...";
  50. success = false;
  51. }
  52. if (!success) {
  53. return;
  54. }
  55. const artistLink = document.querySelector("#artist");
  56. let artistURL = document.querySelector("#artist-url").value;
  57. if (artistURL) {
  58. artistLink.href = artistURL;
  59. artistLink.style.removeProperty("display");
  60. } else {
  61. artistLink.style.display = "none";
  62. }
  63. const overlayImg = document.querySelector("#overlay-img");
  64. const baseImg = document.querySelector("#base-img");
  65. overlayImg.src = overlayInput;
  66. baseImg.src = baseInput;
  67. setURL();
  68. load();
  69. try {
  70. localStorage.setItem("base", baseInput);
  71. localStorage.setItem("overlay", overlayInput);
  72. } catch {
  73. console.error("Couldn't set something in local storage :(")
  74. }
  75. });
  76. let url = new URL(window.location);
  77. const overlay = document.querySelector("#overlay");
  78. document.addEventListener("mousedown", e => {
  79. let x = e.clientX - overlay.getBoundingClientRect().x;
  80. let y = e.clientY - overlay.getBoundingClientRect().y;
  81. updateOverlay([[x,y]], e.buttons % 2 != 0);
  82. });
  83. document.addEventListener("mousemove", e => {
  84. let x = e.clientX - overlay.getBoundingClientRect().x;
  85. let y = e.clientY - overlay.getBoundingClientRect().y;
  86. updateOverlay([[x,y]], e.buttons % 2 != 0);
  87. });
  88. document.addEventListener("touchstart", e => {
  89. let offsetX = overlay.getBoundingClientRect().x;
  90. let offsetY = overlay.getBoundingClientRect().y;
  91. let touches = [];
  92. for (let i=0; i < e.touches.length; i++) {
  93. let x = e.touches[i].clientX - offsetX;
  94. let y = e.touches[i].clientY - offsetY;
  95. touches.push([x,y]);
  96. }
  97. updateOverlay(touches, true);
  98. });
  99. document.addEventListener("touchmove", e => {
  100. let offsetX = overlay.getBoundingClientRect().x;
  101. let offsetY = overlay.getBoundingClientRect().y;
  102. let touches = [];
  103. for (let i=0; i < e.touches.length; i++) {
  104. let x = e.touches[i].clientX - offsetX;
  105. let y = e.touches[i].clientY - offsetY;
  106. touches.push([x,y]);
  107. }
  108. updateOverlay(touches, true);
  109. });
  110. document.querySelector("#radius-slider").addEventListener("input", e => {
  111. try {
  112. radius = parseInt(e.target.value);
  113. document.querySelector("#radius-input").value = radius;
  114. } catch {
  115. console.warn("That wasn't a valid radius: " + e.target.value);
  116. }
  117. });
  118. document.querySelector("#radius-slider").addEventListener("change", e => {
  119. try {
  120. radius = parseInt(e.target.value);
  121. document.querySelector("#radius-input").value = radius;
  122. } catch {
  123. console.warn("That wasn't a valid radius: " + e.target.value);
  124. }
  125. setURL();
  126. });
  127. document.querySelector("#radius-input").addEventListener("input", e => {
  128. try {
  129. radius = parseInt(e.target.value);
  130. document.querySelector("#radius-slider").value = radius;
  131. } catch {
  132. console.warn("That wasn't a valid radius: " + e.target.value);
  133. }
  134. });
  135. document.querySelector("#radius-input").addEventListener("change", e => {
  136. try {
  137. radius = parseInt(e.target.value);
  138. document.querySelector("#radius-slider").value = radius;
  139. } catch {
  140. console.warn("That wasn't a valid radius: " + e.target.value);
  141. }
  142. setURL();
  143. });
  144. document.querySelector("#softness-slider").addEventListener("input", e => {
  145. try {
  146. softness = parseInt(e.target.value);
  147. document.querySelector("#softness-input").value = softness;
  148. } catch {
  149. console.warn("That wasn't a valid softness: " + e.target.value);
  150. }
  151. });
  152. document.querySelector("#softness-slider").addEventListener("change", e => {
  153. try {
  154. softness = parseInt(e.target.value);
  155. document.querySelector("#softness-input").value = softness;
  156. } catch {
  157. console.warn("That wasn't a valid softness: " + e.target.value);
  158. }
  159. setURL();
  160. });
  161. document.querySelector("#softness-input").addEventListener("input", e => {
  162. try {
  163. softness = parseInt(e.target.value);
  164. document.querySelector("#softness-slider").value = softness;
  165. } catch {
  166. console.warn("That wasn't a valid softness: " + e.target.value);
  167. }
  168. });
  169. document.querySelector("#softness-input").addEventListener("change", e => {
  170. try {
  171. softness = parseInt(e.target.value);
  172. document.querySelector("#softness-slider").value = softness;
  173. } catch {
  174. console.warn("That wasn't a valid softness: " + e.target.value);
  175. }
  176. setURL();
  177. });
  178. document.querySelector("#darkness-slider").addEventListener("input", e => {
  179. try {
  180. darkness = parseInt(e.target.value);
  181. document.querySelector("#darkness-input").value = darkness;
  182. document.querySelector("#shadow").style.setProperty("--opacity", darkness / 100);
  183. } catch {
  184. console.warn("That wasn't a valid darkness: " + e.target.value);
  185. }
  186. });
  187. document.querySelector("#darkness-slider").addEventListener("change", e => {
  188. try {
  189. darkness = parseInt(e.target.value);
  190. document.querySelector("#darkness-input").value = darkness;
  191. document.querySelector("#shadow").style.setProperty("--opacity", darkness / 100);
  192. } catch {
  193. console.warn("That wasn't a valid darkness: " + e.target.value);
  194. }
  195. setURL();
  196. });
  197. document.querySelector("#darkness-input").addEventListener("input", e => {
  198. try {
  199. darkness = parseInt(e.target.value);
  200. document.querySelector("#darkness-slider").value = darkness;
  201. document.querySelector("#shadow").style.setProperty("--opacity", darkness / 100);
  202. } catch {
  203. console.warn("That wasn't a valid darkness: " + e.target.value);
  204. }
  205. });
  206. document.querySelector("#darkness-input").addEventListener("change", e => {
  207. try {
  208. darkness = parseInt(e.target.value);
  209. document.querySelector("#darkness-slider").value = darkness;
  210. document.querySelector("#shadow").style.setProperty("--opacity", darkness / 100);
  211. } catch {
  212. console.warn("That wasn't a valid darkness: " + e.target.value);
  213. }
  214. setURL();
  215. });
  216. document.querySelector("#opacity-slider").addEventListener("input", e => {
  217. try {
  218. opacity = parseInt(e.target.value);
  219. document.querySelector("#opacity-input").value = opacity;
  220. document.querySelector("#overlay").style.setProperty("--opacity", opacity / 100);
  221. } catch {
  222. console.warn("That wasn't a valid opacity: " + e.target.value);
  223. }
  224. });
  225. document.querySelector("#opacity-slider").addEventListener("change", e => {
  226. try {
  227. opacity = parseInt(e.target.value);
  228. document.querySelector("#opacity-input").value = opacity;
  229. document.querySelector("#overlay").style.setProperty("--opacity", opacity / 100);
  230. } catch {
  231. console.warn("That wasn't a valid opacity: " + e.target.value);
  232. }
  233. setURL();
  234. });
  235. document.querySelector("#opacity-input").addEventListener("input", e => {
  236. try {
  237. opacity = parseInt(e.target.value);
  238. document.querySelector("#opacity-slider").value = opacity;
  239. document.querySelector("#overlay").style.setProperty("--opacity", opacity / 100);
  240. } catch {
  241. console.warn("That wasn't a valid opacity: " + e.target.value);
  242. }
  243. });
  244. document.querySelector("#opacity-input").addEventListener("change", e => {
  245. try {
  246. opacity = parseInt(e.target.value);
  247. document.querySelector("#opacity-slider").value = opacity;
  248. document.querySelector("#overlay").style.setProperty("--opacity", opacity / 100);
  249. } catch {
  250. console.warn("That wasn't a valid opacity: " + e.target.value);
  251. }
  252. setURL();
  253. });
  254. // see if we have params already; if so, use them!
  255. const overlayImg = document.querySelector("#overlay-img");
  256. const baseImg = document.querySelector("#base-img");
  257. const baseInput = document.querySelector("#base-url");
  258. const overlayInput = document.querySelector("#overlay-url");
  259. const artistInput = document.querySelector("#artist-url");
  260. const artistLink = document.querySelector("#artist");
  261. if (url.searchParams.has("base") && url.searchParams.has("overlay")) {
  262. let baseURL = url.searchParams.get("base");
  263. let overlayURL = url.searchParams.get("overlay");
  264. let artistURL = null;
  265. if (url.searchParams.has("artist")) {
  266. artistURL = url.searchParams.get("artist");
  267. }
  268. baseImg.src = baseURL;
  269. overlayImg.src = overlayURL;
  270. baseInput.value = baseURL;
  271. overlayInput.value = overlayURL;
  272. if (artistURL) {
  273. artistLink.href = artistURL;
  274. artistInput.value = artistURL;
  275. artistLink.style.removeProperty("display");
  276. } else {
  277. artistLink.style.display = "none";
  278. }
  279. firstTime = false;
  280. if (url.searchParams.has("radius")) {
  281. try {
  282. radius = parseInt(url.searchParams.get("radius"));
  283. document.querySelector("#radius-slider").value = radius;
  284. document.querySelector("#radius-input").value = radius;
  285. } catch {
  286. console.warn("That was a bogus radius...");
  287. }
  288. }
  289. if (url.searchParams.has("softness")) {
  290. try {
  291. softness = parseInt(url.searchParams.get("softness"));
  292. document.querySelector("#softness-slider").value = softness;
  293. document.querySelector("#softness-input").value = softness;
  294. } catch {
  295. console.warn("That was a bogus softness...");
  296. }
  297. }
  298. if (url.searchParams.has("darkness")) {
  299. try {
  300. darkness = parseInt(url.searchParams.get("darkness"));
  301. document.querySelector("#darkness-slider").value = darkness;
  302. document.querySelector("#darkness-input").value = darkness;
  303. document.querySelector("#shadow").style.setProperty("--opacity", darkness / 100);
  304. } catch {
  305. console.warn("That was a bogus darkness...");
  306. }
  307. }
  308. if (url.searchParams.has("opacity")) {
  309. try {
  310. opacity = parseInt(url.searchParams.get("opacity"));
  311. document.querySelector("#opacity-slider").value = opacity;
  312. document.querySelector("#opacity-input").value = opacity;
  313. document.querySelector("#overlay").style.setProperty("--opacity", opacity / 100);
  314. } catch {
  315. console.warn("That was a bogus opacity...");
  316. }
  317. }
  318. if (url.searchParams.has("border")) {
  319. try {
  320. border = 1 == parseInt(url.searchParams.get("border"));
  321. } catch {
  322. }
  323. } else {
  324. border = false;
  325. }
  326. document.querySelector("#menu").classList.add("hidden")
  327. document.querySelector("#show-toolbar").classList.remove("hidden")
  328. load();
  329. } else {
  330. try {
  331. baseInput.value = localStorage.getItem("base");
  332. overlayInput.value = localStorage.getItem("overlay");
  333. } catch {
  334. console.error("Couldn't get something from local storage :(")
  335. }
  336. }
  337. document.querySelector("#show-border").checked = border;
  338. window.addEventListener("resize", e => {
  339. if (running) {
  340. setup();
  341. }
  342. })
  343. document.querySelector("#fullscreen-button").addEventListener("click", function toggleFullScreen() {
  344. var doc = window.document;
  345. var docEl = doc.documentElement;
  346. var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
  347. var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
  348. if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
  349. requestFullScreen.call(docEl);
  350. }
  351. else {
  352. cancelFullScreen.call(doc);
  353. }
  354. });
  355. document.querySelector("#show-border").addEventListener("change", e => {
  356. border = e.target.checked;
  357. setURL();
  358. });
  359. document.querySelector("#paint-mode").addEventListener("change", e => {
  360. paintMode = e.target.checked;
  361. });
  362. document.querySelector("#offset-mode").addEventListener("change", e => {
  363. offsetMode = e.target.checked;
  364. });
  365. document.querySelector("#fit-screen").addEventListener("change", e => {
  366. fitScreen = e.target.checked;
  367. setup();
  368. });
  369. });
  370. function load() {
  371. document.querySelector("#menu").classList.remove("start");
  372. const overlayImg = document.querySelector("#overlay-img");
  373. const baseImg = document.querySelector("#base-img");
  374. overlayImg.addEventListener("load", function overlayLoad() {
  375. console.log("The overlay is loaded");
  376. overlayLoaded = true;
  377. if (overlayLoaded && baseLoaded) {
  378. setup();
  379. }
  380. overlayImg.removeEventListener("load", overlayLoad);
  381. })
  382. baseImg.addEventListener("load", function baseLoad() {
  383. console.log("The base is loaded");
  384. baseLoaded = true;
  385. if (overlayLoaded && baseLoaded) {
  386. setup();
  387. }
  388. baseImg.removeEventListener("load", baseLoad);
  389. })
  390. }
  391. function reset() {
  392. running = false;
  393. baseLoaded = false;
  394. overlayLoaded = false;
  395. const overlay = document.querySelector("#overlay");
  396. const base = document.querySelector("#base");
  397. const overlayResized = document.querySelector("#overlay-resized");
  398. const baseResized = document.querySelector("#base-resized");
  399. document.querySelector("#menu").classList.add("start");
  400. overlay.classList.add("hidden");
  401. base.classList.add("hidden");
  402. }
  403. function setup() {
  404. running = true;
  405. const overlay = document.querySelector("#overlay");
  406. const base = document.querySelector("#base");
  407. const overlayResized = document.querySelector("#overlay-resized");
  408. const baseResized = document.querySelector("#base-resized");
  409. const shadow = document.querySelector("#shadow");
  410. overlay.classList.remove("hidden");
  411. shadow.classList.remove("hidden");
  412. base.classList.remove("hidden");
  413. const overlayImg = document.querySelector("#overlay-img");
  414. const baseImg = document.querySelector("#base-img");
  415. /** @type {CanvasRenderingContext2D} */
  416. const overlayCtx = overlay.getContext("2d");
  417. /** @type {CanvasRenderingContext2D} */
  418. const baseCtx = base.getContext("2d");
  419. /** @type {CanvasRenderingContext2D} */
  420. const shadowCtx = shadow.getContext("2d");
  421. /** @type {CanvasRenderingContext2D} */
  422. const overlayCtxResized = overlayResized.getContext("2d");
  423. /** @type {CanvasRenderingContext2D} */
  424. const baseCtxResized = baseResized.getContext("2d");
  425. const availableWidth = document.querySelector("#fill-div").getBoundingClientRect().width;
  426. const availableHeight = document.querySelector("#fill-div").getBoundingClientRect().height;
  427. const scaleW = availableWidth / baseImg.width;
  428. const scaleH = availableHeight / baseImg.height;
  429. scale = fitScreen ? Math.min(scaleW, scaleH) : 1;
  430. width = fitScreen ? Math.floor(availableWidth * scale / scaleW) : baseImg.width;
  431. height = fitScreen ? Math.floor(availableHeight * scale / scaleH) : baseImg.height;
  432. const pixelScale = window.devicePixelRatio;
  433. [baseCtx, baseCtxResized, overlayCtx, overlayCtxResized, shadowCtx].forEach(ctx => {
  434. ctx.canvas.style.width = width + "px";
  435. ctx.canvas.style.height = height + "px";
  436. ctx.canvas.width = width;
  437. ctx.canvas.height = height;
  438. ctx.canvas.style.left = (availableWidth - width) / 2 + "px";
  439. ctx.canvas.style.top = fitScreen ? (availableHeight - height) / 2 + "px" : 0;
  440. ctx.canvas.width = Math.floor(width * pixelScale);
  441. ctx.canvas.height = Math.floor(height * pixelScale);
  442. ctx.scale(pixelScale, pixelScale);
  443. });
  444. baseCtxResized.drawImage(baseImg, 0, 0, width, height);
  445. baseCtx.drawImage(baseResized, 0, 0, width, height);
  446. overlayCtxResized.drawImage(overlayImg, 0, 0, width, height);
  447. shadowCtx.fillStyle = "black";
  448. shadowCtx.fillRect(0, 0, width, height);
  449. // if we're starting fresh, set the radius value to be a fraction of the image size
  450. if (firstTime) {
  451. radius = Math.floor((baseImg.width + baseImg.height) / 10);
  452. document.querySelector("#radius-input").value = radius;
  453. document.querySelector("#radius-slider").value = radius;
  454. firstTime = false;
  455. }
  456. // also set up the input ranges
  457. document.querySelector("#radius-input").max = Math.max(baseImg.width, baseImg.height);
  458. document.querySelector("#radius-slider").max = Math.max(baseImg.width, baseImg.height);
  459. setURL();
  460. console.log("Done");
  461. }
  462. function ease(t, k) {
  463. return 1 - Math.pow(2, -k * (1 - t));
  464. }
  465. function updateOverlay(points, clicked) {
  466. if (!running) {
  467. return;
  468. }
  469. const overlay = document.querySelector("#overlay");
  470. const overlayResized = document.querySelector("#overlay-resized");
  471. /** @type {CanvasRenderingContext2D} */
  472. const overlayCtx = overlay.getContext("2d");
  473. const w = overlayCtx.canvas.width;
  474. const h = overlayCtx.canvas.height;
  475. overlayCtx.save();
  476. overlayCtx.globalCompositeOperation = "source-over";
  477. if (!paintMode)
  478. overlayCtx.clearRect(0, 0, w / window.devicePixelRatio, h / window.devicePixelRatio);
  479. if (!paintMode || clicked) {
  480. points.forEach(point => {
  481. let [x,y] = point;
  482. if (offsetMode) {
  483. y -= radius * scale * 1.2;
  484. }
  485. overlayCtx.beginPath();
  486. overlayCtx.ellipse(x, y, radius * scale, radius * scale, 0, 0, 2 * Math.PI);
  487. const gradient = overlayCtx.createRadialGradient(x, y, 0, x, y, Math.floor(radius * scale));
  488. const maxOpacity = ease(0, 1 / (0.00001 + softness / 100));
  489. const steps = 20;
  490. for (let t=0 ; t <= steps; t+= 1) {
  491. let eased = ease(t/steps, 1 / (0.00001 + softness / 100)) / maxOpacity;
  492. gradient.addColorStop(t/steps, `rgba(0, 0, 0, ${eased}`);
  493. }
  494. let eased = ease(0.999, 1 / (0.00001 + softness / 100)) / maxOpacity;
  495. gradient.addColorStop(0.999, `rgba(0, 0, 0, ${eased}`);
  496. overlayCtx.fillStyle = gradient;
  497. overlayCtx.fill();
  498. })
  499. }
  500. overlayCtx.globalCompositeOperation = "source-in";
  501. // the resized canvas was already scaled up, so we have to compensate here
  502. overlayCtx.drawImage(overlayResized, 0, 0, w/window.devicePixelRatio, h/window.devicePixelRatio);
  503. overlayCtx.globalCompositeOperation = "source-over";
  504. if (!paintMode && border) {
  505. points.forEach(point => {
  506. let [x, y] = point;
  507. if (offsetMode) {
  508. y -= radius * scale * 1.2;
  509. }
  510. overlayCtx.strokeStyle = "#000";
  511. overlayCtx.lineWidth = 3;
  512. overlayCtx.beginPath();
  513. overlayCtx.ellipse(x, y, radius * scale, radius * scale, 0, 0, 2 * Math.PI);
  514. overlayCtx.stroke();
  515. });
  516. }
  517. overlayCtx.restore();
  518. }
  519. function setURL() {
  520. let shareURL = new URL(window.location);
  521. // for some reason, the parser gets confused by urlencoded urls...
  522. // so, to get rid of all parameters, we do this
  523. let keys = Array.from(shareURL.searchParams.keys());
  524. do {
  525. keys = Array.from(shareURL.searchParams.keys());
  526. keys.forEach(key => {
  527. shareURL.searchParams.delete(key);
  528. });
  529. } while (keys.length > 0)
  530. const artistLink = document.querySelector("#artist");
  531. const overlayImg = document.querySelector("#overlay-img");
  532. const baseImg = document.querySelector("#base-img");
  533. shareURL.searchParams.append("base", baseImg.src);
  534. shareURL.searchParams.append("overlay", overlayImg.src);
  535. if (artistLink.href) {
  536. shareURL.searchParams.append("artist", artistLink.href);
  537. }
  538. shareURL.searchParams.append("radius", radius);
  539. shareURL.searchParams.append("softness", softness);
  540. shareURL.searchParams.append("darkness", darkness);
  541. shareURL.searchParams.append("opacity", opacity);
  542. if (border) {
  543. shareURL.searchParams.append("border", 1);
  544. }
  545. window.history.replaceState(null, "X-Ray Viewer", shareURL);
  546. }