Displays a base image and an "x-ray" view of a second image where the mouse is pointing
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

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