Displays a base image and an "x-ray" view of a second image where the mouse is pointing
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

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