Sleazy Fork is available in English.

Danbooru Strip

Strip Danbooru images with your mouse

  1. // ==UserScript==
  2. // @name Danbooru Strip
  3. // @description Strip Danbooru images with your mouse
  4. // @version 0.1.4
  5. // @namespace https://github.com/andre-atgit/danbooru-strip/
  6. // @match *://danbooru.donmai.us/posts/*
  7. // @icon https://danbooru.donmai.us/favicon.svg
  8. // @license MIT
  9. // @run-at document-idle
  10. // ==/UserScript==
  11.  
  12. /* jshint esversion: 8 */
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const strip = { isLoaded: false };
  18. appendStripOnPageLoad();
  19.  
  20. function appendStripOnPageLoad() {
  21. const parentNotice = document.getElementsByClassName('post-notice-parent');
  22. const childNotice = document.getElementsByClassName('post-notice-child');
  23. if (!parentNotice.length && !childNotice.length) return;
  24.  
  25. const currentPost = document.getElementsByClassName('current-post');
  26. const intervalId = setInterval(() => {
  27. if (currentPost.length) {
  28. appendCss();
  29. appendStripTags();
  30. clearInterval(intervalId);
  31. }
  32. }, 100);
  33. }
  34.  
  35. function appendCss() {
  36. const style = document.createElement('style');
  37. document.head.appendChild(style);
  38. style.innerHTML = `
  39. .strip-preview-tag {
  40. border-radius: 0px 0px 5px 5px;
  41. color: white;
  42. text-align: center;
  43. }
  44.  
  45. .post-status-has-children .strip-preview-tag {
  46. background-color: var(--preview-has-children-color);
  47. }
  48.  
  49. .post-status-has-parent .strip-preview-tag {
  50. background-color: var(--preview-has-parent-color);
  51. }
  52.  
  53. #strip-canvas-container {
  54. position: relative;
  55. width: fit-content;
  56. }
  57.  
  58. #strip-full-view-container {
  59. display: none;
  60. position: fixed;
  61. z-index: 1;
  62. left: 0;
  63. top: 0;
  64. width: 100%;
  65. height: 100%;
  66. overflow: auto;
  67. background-color: #0E0E0E;
  68. }
  69.  
  70. #strip-canvas-container .fit {
  71. max-width: 100%;
  72. height: auto !important
  73. }
  74.  
  75. #strip-full-view-container .fit {
  76. max-height: 100%;
  77. max-width: 100%;
  78. position: fixed !important;
  79. top: 50%;
  80. left: 50%;
  81. transform: translate(-50%, -50%);
  82. }
  83.  
  84. #strip-drawing-layer {
  85. position: absolute;
  86. z-index: 1;
  87. }
  88.  
  89. #strip-cursor-layer {
  90. position: relative;
  91. z-index: 2;
  92. }`;
  93. }
  94.  
  95. function appendStripTags() {
  96. const previewElements = document.getElementsByClassName('post-preview-container');
  97. if (!previewElements || previewElements.length < 2) return;
  98.  
  99. for (const previewElement of previewElements) {
  100. const previewLinkElem = previewElement.getElementsByClassName('post-preview-link')[0];
  101. const apiLink = previewLinkElem.href.split('?')[0] + '.json';
  102.  
  103. const p = document.createElement('p');
  104. p.classList.add('strip-preview-tag', 'cursor-pointer');
  105. previewElement.after(p);
  106.  
  107. if (previewElement.parentElement.classList.contains('current-post')) {
  108. p.innerHTML = 'Current';
  109. strip.topImageApiLink = apiLink;
  110. } else {
  111. p.innerHTML = '<a>Strip!</a>';
  112. p.onclick = (evt) => {
  113. const currentlySelected = document.getElementById('strip-selected');
  114. if (currentlySelected) currentlySelected.parentElement.innerHTML = '<a>Strip!</a>';
  115. evt.currentTarget.innerHTML = '<span id="strip-selected">Selected</span>';
  116.  
  117. strip.bottomImageLink = apiLink;
  118. initCanvas();
  119. };
  120. }
  121. }
  122. }
  123.  
  124. async function initCanvas() {
  125. if (strip.isLoaded) {
  126. await fetchData();
  127. loadImgs();
  128. return;
  129. }
  130.  
  131. strip.isLoaded = true;
  132. strip.lineWidth = 100;
  133. strip.isDrawing = false;
  134.  
  135. strip.undoHistory = [];
  136. strip.redoHistory = [];
  137.  
  138. strip.prevX = null;
  139. strip.prevY = null;
  140. strip.currentX = null;
  141. strip.currentY = null;
  142.  
  143. appendCanvas();
  144. appendOptions();
  145. await fetchData();
  146. loadImgs();
  147. addEvents();
  148. addHotkeys();
  149. }
  150.  
  151. function appendCanvas() {
  152. const content = document.createElement('div');
  153. content.innerHTML = `
  154. <div id="strip-canvas-container">
  155. <canvas id="strip-drawing-layer" class="fit"></canvas>
  156. <canvas id="strip-cursor-layer" oncontextmenu="return false" onselectstart="return false" class="fit"> </canvas>
  157. </div>
  158. <div id="strip-full-view-container"></div>`;
  159.  
  160. const containers = content.children;
  161. strip.canvasContainer = containers[0];
  162. strip.fullViewContainer = containers[1];
  163. strip.fullViewContainer.onmousedown = (evt) => (evt.target === strip.fullViewContainer) && toggleFullView();
  164.  
  165. const canvases = content.getElementsByTagName('canvas');
  166. strip.drawingLayer = canvases[0];
  167. strip.cursorLayer = canvases[1];
  168.  
  169. strip.drawingCtx = strip.drawingLayer.getContext('2d');
  170. strip.cursorCtx = strip.cursorLayer.getContext('2d');
  171.  
  172. const resizeNotice = document.getElementById('image-resize-notice');
  173. if (resizeNotice) resizeNotice.style.display = 'none';
  174.  
  175. const image = document.getElementById('image');
  176. const imageSection = image.closest('section');
  177. for (const child of imageSection.children) {
  178. child.style.display = 'none';
  179. }
  180.  
  181. imageSection.appendChild(strip.canvasContainer);
  182. imageSection.appendChild(strip.fullViewContainer);
  183. }
  184.  
  185. function appendOptions() {
  186. const stripOptions = document.createElement('section');
  187. stripOptions.innerHTML = `
  188. <h2>Strip</h2>
  189. <ul>
  190. <li><a class="cursor-pointer" title="Shortcut is esc">Toggle full view</a></li>
  191. <li><a class="cursor-pointer" title="Ctrl + z">Undo</a></li>
  192. <li><a class="cursor-pointer" title="Ctrl + y">Redo</a></li>
  193. <li><a class="cursor-pointer">Download strip</a></li>
  194. <li><a class="cursor-pointer" title="Shortcut is + and -">Brush width</a></li>
  195. <li><input type="range" min="1" max="400" value="100"></li>
  196. </ul>`;
  197.  
  198. const options = stripOptions.getElementsByTagName('a');
  199. options[0].onclick = () => toggleFullView();
  200. options[1].onclick = () => undo();
  201. options[2].onclick = () => redo();
  202. options[3].onclick = () => download();
  203.  
  204. strip.lineWidthInput = stripOptions.getElementsByTagName('input')[0];
  205. strip.lineWidthInput.onchange = (evt) => setLineWidth(Number(evt.target.value));
  206.  
  207. const sidebar = document.getElementById('sidebar');
  208. sidebar.appendChild(stripOptions);
  209. }
  210.  
  211. async function fetchData() {
  212. const topImageRequest = fetch(strip.topImageApiLink).then((res) => res.json());
  213. const bottomImageRequest = fetch(strip.bottomImageLink).then((res) => res.json());
  214.  
  215. const [topImageData, bottomImageData] = await Promise.all([topImageRequest, bottomImageRequest]);
  216. const image = document.getElementById('image');
  217.  
  218. const topVariant = topImageData.media_asset.variants.find((variant) => variant.width === image.naturalWidth);
  219. const bottomVariant = bottomImageData.media_asset.variants.find((variant) => variant.width === image.naturalWidth);
  220.  
  221. strip.topImageUrl = topVariant ? topVariant.url : topImageData.file_url;
  222. strip.bottomImageUrl = bottomVariant ? bottomVariant.url : bottomImageData.file_url;
  223. }
  224.  
  225. function loadImgs() {
  226. const topImg = new Image();
  227. const bottomImg = new Image();
  228.  
  229. topImg.crossOrigin = 'anonymous';
  230. topImg.src = strip.topImageUrl;
  231.  
  232. bottomImg.crossOrigin = 'anonymous';
  233. bottomImg.src = strip.bottomImageUrl;
  234.  
  235. topImg.onload = () => {
  236. strip.topImage = topImg;
  237. if (strip.bottomImage) drawImgs();
  238. };
  239.  
  240. bottomImg.onload = () => {
  241. strip.bottomImage = bottomImg;
  242. if (strip.topImage) drawImgs();
  243. };
  244. }
  245.  
  246. function drawImgs() {
  247. strip.cursorLayer.width = strip.drawingLayer.width = strip.topImage.width;
  248. strip.cursorLayer.height = strip.drawingLayer.height = strip.topImage.height;
  249. strip.drawingCtx.drawImage(strip.undoHistory.at(-1) || strip.topImage, 0, 0);
  250. }
  251.  
  252. function addEvents() {
  253. strip.cursorLayer.addEventListener('pointerenter', (evt) => {
  254. strip.prevX = strip.currentX = evt.offsetX;
  255. strip.prevY = strip.currentY = evt.offsetY;
  256.  
  257. if (evt.pressure) {
  258. strip.isDrawing = true;
  259. }
  260.  
  261. if (!evt.pressure && strip.isDrawing) {
  262. strip.isDrawing = false;
  263. addStrokeToHistory();
  264. }
  265. });
  266.  
  267. strip.cursorLayer.addEventListener('pointermove', (evt) => {
  268. strip.prevX = strip.currentX;
  269. strip.prevY = strip.currentY;
  270. strip.currentX = evt.offsetX;
  271. strip.currentY = evt.offsetY;
  272. drawCursor(strip.currentX, strip.currentY);
  273.  
  274. if (evt.buttons & 1) {
  275. strip.isDrawing = true;
  276. drawLine(strip.prevX, strip.prevY, strip.currentX, strip.currentY, strip.bottomImage);
  277. }
  278. else if (evt.buttons & 2) {
  279. strip.isDrawing = true;
  280. drawLine(strip.prevX, strip.prevY, strip.currentX, strip.currentY, strip.topImage);
  281. }
  282. });
  283.  
  284. strip.cursorLayer.addEventListener('pointerleave', (evt) => {
  285. strip.currentX = null;
  286. strip.currentY = null;
  287. clearCursor();
  288. });
  289.  
  290. strip.cursorLayer.addEventListener('pointerdown', (evt) => {
  291. drawCursor(strip.currentX, strip.currentY);
  292.  
  293. if (evt.buttons & 1) {
  294. strip.isDrawing = true;
  295. drawArc(evt.offsetX, evt.offsetY, strip.bottomImage);
  296. }
  297. else if (evt.buttons & 2) {
  298. strip.isDrawing = true;
  299. drawArc(evt.offsetX, evt.offsetY, strip.topImage);
  300. }
  301. });
  302.  
  303. strip.cursorLayer.addEventListener('pointerup', (evt) => {
  304. strip.isDrawing = false;
  305. addStrokeToHistory();
  306. });
  307.  
  308. strip.cursorLayer.addEventListener('touchmove', (evt) => {
  309. if (evt.changedTouches.length === 1) evt.preventDefault();
  310. });
  311. }
  312.  
  313. function addHotkeys() {
  314. document.addEventListener('keydown', (evt) => {
  315. if (document.activeElement.value !== undefined) return;
  316. switch (evt.key) {
  317. case '+':
  318. setLineWidth(strip.lineWidth + 1);
  319. break;
  320. case '-':
  321. setLineWidth(Math.max(strip.lineWidth - 1, 1));
  322. break;
  323. case 'Escape':
  324. evt.preventDefault();
  325. toggleFullView();
  326. break;
  327. case 'z':
  328. if (evt.ctrlKey && undo()) {
  329. evt.preventDefault();
  330. }
  331. break;
  332. case 'y':
  333. if (evt.ctrlKey && redo()) {
  334. evt.preventDefault();
  335. }
  336. break;
  337. }
  338. });
  339. }
  340.  
  341. function drawArc(x, y, overlay) {
  342. const scale = getScale();
  343. strip.drawingCtx.globalCompositeOperation = 'destination-out';
  344. strip.drawingCtx.beginPath();
  345. strip.drawingCtx.arc(x / scale, y / scale, strip.lineWidth / 2, 0, Math.PI * 2);
  346. strip.drawingCtx.fill();
  347.  
  348. strip.drawingCtx.globalCompositeOperation = 'destination-over';
  349. strip.drawingCtx.drawImage(overlay, 0, 0);
  350. }
  351.  
  352. function drawLine(x1, y1, x2, y2, overlay) {
  353. const scale = getScale();
  354. strip.drawingCtx.globalCompositeOperation = 'destination-out';
  355. strip.drawingCtx.beginPath();
  356. strip.drawingCtx.lineWidth = strip.lineWidth;
  357. strip.drawingCtx.lineJoin = 'round';
  358. strip.drawingCtx.moveTo(x1 / scale, y1 / scale);
  359. strip.drawingCtx.lineTo(x2 / scale, y2 / scale);
  360. strip.drawingCtx.closePath();
  361. strip.drawingCtx.stroke();
  362.  
  363. strip.drawingCtx.globalCompositeOperation = 'destination-over';
  364. strip.drawingCtx.drawImage(overlay, 0, 0);
  365. }
  366.  
  367. function drawCursor(x, y) {
  368. const scale = getScale();
  369. strip.cursorCtx.clearRect(0, 0, strip.cursorLayer.width, strip.cursorLayer.height);
  370. strip.cursorCtx.beginPath();
  371. strip.cursorCtx.arc(x / scale, y / scale, strip.lineWidth / 2, 0, Math.PI * 2);
  372. strip.cursorCtx.lineWidth = 1;
  373. strip.cursorCtx.strokeStyle = 'black';
  374. strip.cursorCtx.fillStyle = 'transparent';
  375. strip.cursorCtx.stroke();
  376. }
  377.  
  378. function clearCursor() {
  379. strip.cursorCtx.clearRect(0, 0, strip.cursorLayer.width, strip.cursorLayer.height);
  380. }
  381.  
  382. function getScale() {
  383. return strip.cursorLayer.getBoundingClientRect().width / strip.cursorLayer.width;
  384. }
  385.  
  386. function setLineWidth(width) {
  387. strip.lineWidth = width;
  388. strip.lineWidthInput.value = strip.lineWidth;
  389. if (strip.currentX !== null) drawCursor(strip.currentX, strip.currentY);
  390. }
  391.  
  392. function addStrokeToHistory() {
  393. const historyEntry = document.createElement('canvas');
  394. historyEntry.width = strip.drawingLayer.width;
  395. historyEntry.height = strip.drawingLayer.height;
  396.  
  397. const context = historyEntry.getContext('2d');
  398. context.drawImage(strip.drawingLayer, 0, 0);
  399.  
  400. strip.redoHistory = [];
  401. strip.undoHistory.push(historyEntry);
  402. }
  403.  
  404. function undo() {
  405. if (strip.isDrawing) {
  406. strip.isDrawing = false;
  407. addStrokeToHistory();
  408. }
  409.  
  410. if (!strip.undoHistory.length) return false;
  411. strip.redoHistory.push(strip.undoHistory.pop());
  412.  
  413. strip.drawingCtx.globalCompositeOperation = 'source-over';
  414. strip.drawingCtx.drawImage(strip.undoHistory.at(-1) || strip.topImage, 0, 0);
  415. return true;
  416. }
  417.  
  418. function redo() {
  419. if (strip.isDrawing) {
  420. strip.isDrawing = false;
  421. addStrokeToHistory();
  422. }
  423.  
  424. if (!strip.redoHistory.length) return false;
  425. strip.undoHistory.push(strip.redoHistory.pop());
  426.  
  427. strip.drawingCtx.globalCompositeOperation = 'source-over';
  428. strip.drawingCtx.drawImage(strip.undoHistory.at(-1), 0, 0);
  429. return true;
  430. }
  431.  
  432. function download() {
  433. const link = document.createElement('a');
  434. link.href = strip.drawingLayer.toDataURL('image/png');
  435. link.download = 'strip.png';
  436. link.click();
  437. }
  438.  
  439. function toggleFullView() {
  440. if (strip.fullViewContainer.style.display !== 'block') {
  441. strip.fullViewContainer.style.display = 'block';
  442. while (strip.canvasContainer.firstChild) {
  443. strip.fullViewContainer.appendChild(strip.canvasContainer.firstChild);
  444. }
  445. } else {
  446. strip.fullViewContainer.style.display = 'none';
  447. while (strip.fullViewContainer.firstChild) {
  448. strip.canvasContainer.appendChild(strip.fullViewContainer.firstChild);
  449. }
  450. }
  451. }
  452. })();