Displays a base image and an "x-ray" view of a second image where the mouse is pointing
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

646 satır
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. load();
  269. } else {
  270. try {
  271. baseInput.value = localStorage.getItem("base");
  272. overlayInput.value = localStorage.getItem("overlay");
  273. } catch {
  274. console.error("Couldn't get something from local storage :(")
  275. }
  276. }
  277. if (url.searchParams.has("radius")) {
  278. try {
  279. radius = parseInt(url.searchParams.get("radius"));
  280. document.querySelector("#radius-slider").value = radius;
  281. document.querySelector("#radius-input").value = radius;
  282. } catch {
  283. console.warn("That was a bogus radius...");
  284. }
  285. }
  286. if (url.searchParams.has("softness")) {
  287. try {
  288. softness = parseInt(url.searchParams.get("softness"));
  289. document.querySelector("#softness-slider").value = softness;
  290. document.querySelector("#softness-input").value = softness;
  291. } catch {
  292. console.warn("That was a bogus softness...");
  293. }
  294. }
  295. if (url.searchParams.has("darkness")) {
  296. try {
  297. darkness = parseInt(url.searchParams.get("darkness"));
  298. document.querySelector("#darkness-slider").value = darkness;
  299. document.querySelector("#darkness-input").value = darkness;
  300. document.querySelector("#shadow").style.setProperty("--opacity", darkness / 100);
  301. } catch {
  302. console.warn("That was a bogus darkness...");
  303. }
  304. }
  305. if (url.searchParams.has("opacity")) {
  306. try {
  307. opacity = parseInt(url.searchParams.get("opacity"));
  308. document.querySelector("#opacity-slider").value = opacity;
  309. document.querySelector("#opacity-input").value = opacity;
  310. document.querySelector("#overlay").style.setProperty("--opacity", opacity / 100);
  311. } catch {
  312. console.warn("That was a bogus opacity...");
  313. }
  314. }
  315. if (url.searchParams.has("border")) {
  316. try {
  317. border = 1 == parseInt(url.searchParams.get("border"));
  318. console.log(border);
  319. } catch {
  320. }
  321. } else {
  322. border = false;
  323. }
  324. document.querySelector("#show-border").checked = border;
  325. window.addEventListener("resize", e => {
  326. if (running) {
  327. setup();
  328. }
  329. })
  330. document.querySelector("#fullscreen-button").addEventListener("click", function toggleFullScreen() {
  331. var doc = window.document;
  332. var docEl = doc.documentElement;
  333. var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
  334. var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
  335. if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
  336. requestFullScreen.call(docEl);
  337. }
  338. else {
  339. cancelFullScreen.call(doc);
  340. }
  341. });
  342. document.querySelector("#show-border").addEventListener("change", e => {
  343. border = e.target.checked;
  344. setURL();
  345. });
  346. document.querySelector("#paint-mode").addEventListener("change", e => {
  347. paintMode = e.target.checked;
  348. });
  349. document.querySelector("#fit-screen").addEventListener("change", e => {
  350. fitScreen = e.target.checked;
  351. setup();
  352. });
  353. });
  354. function load() {
  355. document.querySelector("#menu").classList.remove("start");
  356. const overlayImg = document.querySelector("#overlay-img");
  357. const baseImg = document.querySelector("#base-img");
  358. overlayImg.addEventListener("load", function overlayLoad() {
  359. console.log("The overlay is loaded");
  360. overlayLoaded = true;
  361. if (overlayLoaded && baseLoaded) {
  362. setup();
  363. }
  364. overlayImg.removeEventListener("load", overlayLoad);
  365. })
  366. baseImg.addEventListener("load", function baseLoad() {
  367. console.log("The base is loaded");
  368. baseLoaded = true;
  369. if (overlayLoaded && baseLoaded) {
  370. setup();
  371. }
  372. baseImg.removeEventListener("load", baseLoad);
  373. })
  374. }
  375. function reset() {
  376. running = false;
  377. baseLoaded = false;
  378. overlayLoaded = false;
  379. const overlay = document.querySelector("#overlay");
  380. const base = document.querySelector("#base");
  381. const overlayResized = document.querySelector("#overlay-resized");
  382. const baseResized = document.querySelector("#base-resized");
  383. document.querySelector("#menu").classList.add("start");
  384. overlay.classList.add("hidden");
  385. base.classList.add("hidden");
  386. }
  387. function setup() {
  388. running = true;
  389. const overlay = document.querySelector("#overlay");
  390. const base = document.querySelector("#base");
  391. const overlayResized = document.querySelector("#overlay-resized");
  392. const baseResized = document.querySelector("#base-resized");
  393. const shadow = document.querySelector("#shadow");
  394. overlay.classList.remove("hidden");
  395. shadow.classList.remove("hidden");
  396. base.classList.remove("hidden");
  397. const overlayImg = document.querySelector("#overlay-img");
  398. const baseImg = document.querySelector("#base-img");
  399. /** @type {CanvasRenderingContext2D} */
  400. const overlayCtx = overlay.getContext("2d");
  401. /** @type {CanvasRenderingContext2D} */
  402. const baseCtx = base.getContext("2d");
  403. /** @type {CanvasRenderingContext2D} */
  404. const shadowCtx = shadow.getContext("2d");
  405. /** @type {CanvasRenderingContext2D} */
  406. const overlayCtxResized = overlayResized.getContext("2d");
  407. /** @type {CanvasRenderingContext2D} */
  408. const baseCtxResized = baseResized.getContext("2d");
  409. const availableWidth = document.querySelector("#fill-div").getBoundingClientRect().width;
  410. const availableHeight = document.querySelector("#fill-div").getBoundingClientRect().height;
  411. const scaleW = availableWidth / baseImg.width;
  412. const scaleH = availableHeight / baseImg.height;
  413. scale = fitScreen ? Math.min(scaleW, scaleH) : 1;
  414. width = fitScreen ? Math.floor(availableWidth * scale / scaleW) : baseImg.width;
  415. height = fitScreen ? Math.floor(availableHeight * scale / scaleH) : baseImg.height;
  416. [baseCtx, baseCtxResized, overlayCtx, overlayCtxResized, shadowCtx].forEach(ctx => {
  417. ctx.canvas.width = width;
  418. ctx.canvas.height = height;
  419. ctx.canvas.style.left = fitScreen ? (availableWidth - width) / 2 + "px" : 0;
  420. ctx.canvas.style.top = fitScreen ? (availableHeight - height) / 2 + "px" : 0;
  421. });
  422. baseCtxResized.drawImage(baseImg, 0, 0, width, height);
  423. baseCtx.drawImage(baseResized, 0, 0, width, height);
  424. overlayCtxResized.drawImage(overlayImg, 0, 0, width, height);
  425. shadowCtx.fillStyle = "black";
  426. shadowCtx.fillRect(0, 0, width, height);
  427. // if we're starting fresh, set the radius value to be a fraction of the image size
  428. if (firstTime) {
  429. radius = Math.floor((baseImg.width + baseImg.height) / 10);
  430. document.querySelector("#radius-input").value = radius;
  431. document.querySelector("#radius-slider").value = radius;
  432. firstTime = false;
  433. }
  434. // also set up the input ranges
  435. document.querySelector("#radius-input").max = Math.max(baseImg.width, baseImg.height);
  436. document.querySelector("#radius-slider").max = Math.max(baseImg.width, baseImg.height);
  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. const overlay = document.querySelector("#overlay");
  444. const overlayResized = document.querySelector("#overlay-resized");
  445. /** @type {CanvasRenderingContext2D} */
  446. const overlayCtx = overlay.getContext("2d");
  447. /** @type {CanvasRenderingContext2D} */
  448. const overlayCtxResized = overlay.getContext("2d");
  449. const w = overlayCtx.canvas.width;
  450. const h = overlayCtx.canvas.height;
  451. overlayCtx.save();
  452. overlayCtx.globalCompositeOperation = "source-over";
  453. if (!paintMode)
  454. overlayCtx.clearRect(0, 0, w, h);
  455. if (!paintMode || clicked) {
  456. points.forEach(point => {
  457. const [x,y] = point;
  458. overlayCtx.beginPath();
  459. overlayCtx.ellipse(x, y, radius * scale, radius * scale, 0, 0, 2 * Math.PI);
  460. const gradient = overlayCtx.createRadialGradient(x, y, 0, x, y, Math.floor(radius * scale));
  461. const maxOpacity = ease(0, 1 / (0.00001 + softness / 100));
  462. const steps = 20;
  463. for (let t=0 ; t <= steps; t+= 1) {
  464. let eased = ease(t/steps, 1 / (0.00001 + softness / 100)) / maxOpacity;
  465. gradient.addColorStop(t/steps, `rgba(0, 0, 0, ${eased}`);
  466. }
  467. let eased = ease(0.999, 1 / (0.00001 + softness / 100)) / maxOpacity;
  468. gradient.addColorStop(0.999, `rgba(0, 0, 0, ${eased}`);
  469. overlayCtx.fillStyle = gradient;
  470. overlayCtx.fill();
  471. })
  472. }
  473. overlayCtx.globalCompositeOperation = "source-in";
  474. overlayCtx.drawImage(overlayResized, 0, 0);
  475. overlayCtx.globalCompositeOperation = "source-over";
  476. if (!paintMode && border) {
  477. points.forEach(point => {
  478. const [x, y] = point;
  479. overlayCtx.strokeStyle = "#000";
  480. overlayCtx.lineWidth = 3;
  481. overlayCtx.beginPath();
  482. overlayCtx.ellipse(x, y, radius * scale, radius * scale, 0, 0, 2 * Math.PI);
  483. overlayCtx.stroke();
  484. });
  485. }
  486. overlayCtx.restore();
  487. }
  488. function setURL() {
  489. let shareURL = new URL(window.location);
  490. // for some reason, the parser gets confused by urlencoded urls...
  491. // so, to get rid of all parameters, we do this
  492. let keys = Array.from(shareURL.searchParams.keys());
  493. do {
  494. keys = Array.from(shareURL.searchParams.keys());
  495. keys.forEach(key => {
  496. shareURL.searchParams.delete(key);
  497. });
  498. } while (keys.length > 0)
  499. const artistLink = document.querySelector("#artist");
  500. const overlayImg = document.querySelector("#overlay-img");
  501. const baseImg = document.querySelector("#base-img");
  502. shareURL.searchParams.append("base", baseImg.src);
  503. shareURL.searchParams.append("overlay", overlayImg.src);
  504. if (artistLink.href) {
  505. shareURL.searchParams.append("artist", artistLink.href);
  506. }
  507. shareURL.searchParams.append("radius", radius);
  508. shareURL.searchParams.append("softness", softness);
  509. shareURL.searchParams.append("darkness", darkness);
  510. shareURL.searchParams.append("opacity", opacity);
  511. if (border) {
  512. shareURL.searchParams.append("border", 1);
  513. }
  514. window.history.replaceState(null, "X-Ray Viewer", shareURL);
  515. }