// ==UserScript==
// @name Danbooru Strip
// @description Strip Danbooru images with your mouse
// @version 0.1.4
// @namespace https://github.com/andre-atgit/danbooru-strip/
// @match *://danbooru.donmai.us/posts/*
// @icon https://danbooru.donmai.us/favicon.svg
// @license MIT
// @run-at document-idle
// ==/UserScript==
/* jshint esversion: 8 */
(function() {
'use strict';
const strip = { isLoaded: false };
appendStripOnPageLoad();
function appendStripOnPageLoad() {
const parentNotice = document.getElementsByClassName('post-notice-parent');
const childNotice = document.getElementsByClassName('post-notice-child');
if (!parentNotice.length && !childNotice.length) return;
const currentPost = document.getElementsByClassName('current-post');
const intervalId = setInterval(() => {
if (currentPost.length) {
appendCss();
appendStripTags();
clearInterval(intervalId);
}
}, 100);
}
function appendCss() {
const style = document.createElement('style');
document.head.appendChild(style);
style.innerHTML = `
.strip-preview-tag {
border-radius: 0px 0px 5px 5px;
color: white;
text-align: center;
}
.post-status-has-children .strip-preview-tag {
background-color: var(--preview-has-children-color);
}
.post-status-has-parent .strip-preview-tag {
background-color: var(--preview-has-parent-color);
}
#strip-canvas-container {
position: relative;
width: fit-content;
}
#strip-full-view-container {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: #0E0E0E;
}
#strip-canvas-container .fit {
max-width: 100%;
height: auto !important
}
#strip-full-view-container .fit {
max-height: 100%;
max-width: 100%;
position: fixed !important;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#strip-drawing-layer {
position: absolute;
z-index: 1;
}
#strip-cursor-layer {
position: relative;
z-index: 2;
}`;
}
function appendStripTags() {
const previewElements = document.getElementsByClassName('post-preview-container');
if (!previewElements || previewElements.length < 2) return;
for (const previewElement of previewElements) {
const previewLinkElem = previewElement.getElementsByClassName('post-preview-link')[0];
const apiLink = previewLinkElem.href.split('?')[0] + '.json';
const p = document.createElement('p');
p.classList.add('strip-preview-tag', 'cursor-pointer');
previewElement.after(p);
if (previewElement.parentElement.classList.contains('current-post')) {
p.innerHTML = 'Current';
strip.topImageApiLink = apiLink;
} else {
p.innerHTML = '<a>Strip!</a>';
p.onclick = (evt) => {
const currentlySelected = document.getElementById('strip-selected');
if (currentlySelected) currentlySelected.parentElement.innerHTML = '<a>Strip!</a>';
evt.currentTarget.innerHTML = '<span id="strip-selected">Selected</span>';
strip.bottomImageLink = apiLink;
initCanvas();
};
}
}
}
async function initCanvas() {
if (strip.isLoaded) {
await fetchData();
loadImgs();
return;
}
strip.isLoaded = true;
strip.lineWidth = 100;
strip.isDrawing = false;
strip.undoHistory = [];
strip.redoHistory = [];
strip.prevX = null;
strip.prevY = null;
strip.currentX = null;
strip.currentY = null;
appendCanvas();
appendOptions();
await fetchData();
loadImgs();
addEvents();
addHotkeys();
}
function appendCanvas() {
const content = document.createElement('div');
content.innerHTML = `
<div id="strip-canvas-container">
<canvas id="strip-drawing-layer" class="fit"></canvas>
<canvas id="strip-cursor-layer" oncontextmenu="return false" onselectstart="return false" class="fit"> </canvas>
</div>
<div id="strip-full-view-container"></div>`;
const containers = content.children;
strip.canvasContainer = containers[0];
strip.fullViewContainer = containers[1];
strip.fullViewContainer.onmousedown = (evt) => (evt.target === strip.fullViewContainer) && toggleFullView();
const canvases = content.getElementsByTagName('canvas');
strip.drawingLayer = canvases[0];
strip.cursorLayer = canvases[1];
strip.drawingCtx = strip.drawingLayer.getContext('2d');
strip.cursorCtx = strip.cursorLayer.getContext('2d');
const resizeNotice = document.getElementById('image-resize-notice');
if (resizeNotice) resizeNotice.style.display = 'none';
const image = document.getElementById('image');
const imageSection = image.closest('section');
for (const child of imageSection.children) {
child.style.display = 'none';
}
imageSection.appendChild(strip.canvasContainer);
imageSection.appendChild(strip.fullViewContainer);
}
function appendOptions() {
const stripOptions = document.createElement('section');
stripOptions.innerHTML = `
<h2>Strip</h2>
<ul>
<li><a class="cursor-pointer" title="Shortcut is esc">Toggle full view</a></li>
<li><a class="cursor-pointer" title="Ctrl + z">Undo</a></li>
<li><a class="cursor-pointer" title="Ctrl + y">Redo</a></li>
<li><a class="cursor-pointer">Download strip</a></li>
<li><a class="cursor-pointer" title="Shortcut is + and -">Brush width</a></li>
<li><input type="range" min="1" max="400" value="100"></li>
</ul>`;
const options = stripOptions.getElementsByTagName('a');
options[0].onclick = () => toggleFullView();
options[1].onclick = () => undo();
options[2].onclick = () => redo();
options[3].onclick = () => download();
strip.lineWidthInput = stripOptions.getElementsByTagName('input')[0];
strip.lineWidthInput.onchange = (evt) => setLineWidth(Number(evt.target.value));
const sidebar = document.getElementById('sidebar');
sidebar.appendChild(stripOptions);
}
async function fetchData() {
const topImageRequest = fetch(strip.topImageApiLink).then((res) => res.json());
const bottomImageRequest = fetch(strip.bottomImageLink).then((res) => res.json());
const [topImageData, bottomImageData] = await Promise.all([topImageRequest, bottomImageRequest]);
const image = document.getElementById('image');
const topVariant = topImageData.media_asset.variants.find((variant) => variant.width === image.naturalWidth);
const bottomVariant = bottomImageData.media_asset.variants.find((variant) => variant.width === image.naturalWidth);
strip.topImageUrl = topVariant ? topVariant.url : topImageData.file_url;
strip.bottomImageUrl = bottomVariant ? bottomVariant.url : bottomImageData.file_url;
}
function loadImgs() {
const topImg = new Image();
const bottomImg = new Image();
topImg.crossOrigin = 'anonymous';
topImg.src = strip.topImageUrl;
bottomImg.crossOrigin = 'anonymous';
bottomImg.src = strip.bottomImageUrl;
topImg.onload = () => {
strip.topImage = topImg;
if (strip.bottomImage) drawImgs();
};
bottomImg.onload = () => {
strip.bottomImage = bottomImg;
if (strip.topImage) drawImgs();
};
}
function drawImgs() {
strip.cursorLayer.width = strip.drawingLayer.width = strip.topImage.width;
strip.cursorLayer.height = strip.drawingLayer.height = strip.topImage.height;
strip.drawingCtx.drawImage(strip.undoHistory.at(-1) || strip.topImage, 0, 0);
}
function addEvents() {
strip.cursorLayer.addEventListener('pointerenter', (evt) => {
strip.prevX = strip.currentX = evt.offsetX;
strip.prevY = strip.currentY = evt.offsetY;
if (evt.pressure) {
strip.isDrawing = true;
}
if (!evt.pressure && strip.isDrawing) {
strip.isDrawing = false;
addStrokeToHistory();
}
});
strip.cursorLayer.addEventListener('pointermove', (evt) => {
strip.prevX = strip.currentX;
strip.prevY = strip.currentY;
strip.currentX = evt.offsetX;
strip.currentY = evt.offsetY;
drawCursor(strip.currentX, strip.currentY);
if (evt.buttons & 1) {
strip.isDrawing = true;
drawLine(strip.prevX, strip.prevY, strip.currentX, strip.currentY, strip.bottomImage);
}
else if (evt.buttons & 2) {
strip.isDrawing = true;
drawLine(strip.prevX, strip.prevY, strip.currentX, strip.currentY, strip.topImage);
}
});
strip.cursorLayer.addEventListener('pointerleave', (evt) => {
strip.currentX = null;
strip.currentY = null;
clearCursor();
});
strip.cursorLayer.addEventListener('pointerdown', (evt) => {
drawCursor(strip.currentX, strip.currentY);
if (evt.buttons & 1) {
strip.isDrawing = true;
drawArc(evt.offsetX, evt.offsetY, strip.bottomImage);
}
else if (evt.buttons & 2) {
strip.isDrawing = true;
drawArc(evt.offsetX, evt.offsetY, strip.topImage);
}
});
strip.cursorLayer.addEventListener('pointerup', (evt) => {
strip.isDrawing = false;
addStrokeToHistory();
});
strip.cursorLayer.addEventListener('touchmove', (evt) => {
if (evt.changedTouches.length === 1) evt.preventDefault();
});
}
function addHotkeys() {
document.addEventListener('keydown', (evt) => {
if (document.activeElement.value !== undefined) return;
switch (evt.key) {
case '+':
setLineWidth(strip.lineWidth + 1);
break;
case '-':
setLineWidth(Math.max(strip.lineWidth - 1, 1));
break;
case 'Escape':
evt.preventDefault();
toggleFullView();
break;
case 'z':
if (evt.ctrlKey && undo()) {
evt.preventDefault();
}
break;
case 'y':
if (evt.ctrlKey && redo()) {
evt.preventDefault();
}
break;
}
});
}
function drawArc(x, y, overlay) {
const scale = getScale();
strip.drawingCtx.globalCompositeOperation = 'destination-out';
strip.drawingCtx.beginPath();
strip.drawingCtx.arc(x / scale, y / scale, strip.lineWidth / 2, 0, Math.PI * 2);
strip.drawingCtx.fill();
strip.drawingCtx.globalCompositeOperation = 'destination-over';
strip.drawingCtx.drawImage(overlay, 0, 0);
}
function drawLine(x1, y1, x2, y2, overlay) {
const scale = getScale();
strip.drawingCtx.globalCompositeOperation = 'destination-out';
strip.drawingCtx.beginPath();
strip.drawingCtx.lineWidth = strip.lineWidth;
strip.drawingCtx.lineJoin = 'round';
strip.drawingCtx.moveTo(x1 / scale, y1 / scale);
strip.drawingCtx.lineTo(x2 / scale, y2 / scale);
strip.drawingCtx.closePath();
strip.drawingCtx.stroke();
strip.drawingCtx.globalCompositeOperation = 'destination-over';
strip.drawingCtx.drawImage(overlay, 0, 0);
}
function drawCursor(x, y) {
const scale = getScale();
strip.cursorCtx.clearRect(0, 0, strip.cursorLayer.width, strip.cursorLayer.height);
strip.cursorCtx.beginPath();
strip.cursorCtx.arc(x / scale, y / scale, strip.lineWidth / 2, 0, Math.PI * 2);
strip.cursorCtx.lineWidth = 1;
strip.cursorCtx.strokeStyle = 'black';
strip.cursorCtx.fillStyle = 'transparent';
strip.cursorCtx.stroke();
}
function clearCursor() {
strip.cursorCtx.clearRect(0, 0, strip.cursorLayer.width, strip.cursorLayer.height);
}
function getScale() {
return strip.cursorLayer.getBoundingClientRect().width / strip.cursorLayer.width;
}
function setLineWidth(width) {
strip.lineWidth = width;
strip.lineWidthInput.value = strip.lineWidth;
if (strip.currentX !== null) drawCursor(strip.currentX, strip.currentY);
}
function addStrokeToHistory() {
const historyEntry = document.createElement('canvas');
historyEntry.width = strip.drawingLayer.width;
historyEntry.height = strip.drawingLayer.height;
const context = historyEntry.getContext('2d');
context.drawImage(strip.drawingLayer, 0, 0);
strip.redoHistory = [];
strip.undoHistory.push(historyEntry);
}
function undo() {
if (strip.isDrawing) {
strip.isDrawing = false;
addStrokeToHistory();
}
if (!strip.undoHistory.length) return false;
strip.redoHistory.push(strip.undoHistory.pop());
strip.drawingCtx.globalCompositeOperation = 'source-over';
strip.drawingCtx.drawImage(strip.undoHistory.at(-1) || strip.topImage, 0, 0);
return true;
}
function redo() {
if (strip.isDrawing) {
strip.isDrawing = false;
addStrokeToHistory();
}
if (!strip.redoHistory.length) return false;
strip.undoHistory.push(strip.redoHistory.pop());
strip.drawingCtx.globalCompositeOperation = 'source-over';
strip.drawingCtx.drawImage(strip.undoHistory.at(-1), 0, 0);
return true;
}
function download() {
const link = document.createElement('a');
link.href = strip.drawingLayer.toDataURL('image/png');
link.download = 'strip.png';
link.click();
}
function toggleFullView() {
if (strip.fullViewContainer.style.display !== 'block') {
strip.fullViewContainer.style.display = 'block';
while (strip.canvasContainer.firstChild) {
strip.fullViewContainer.appendChild(strip.canvasContainer.firstChild);
}
} else {
strip.fullViewContainer.style.display = 'none';
while (strip.fullViewContainer.firstChild) {
strip.canvasContainer.appendChild(strip.fullViewContainer.firstChild);
}
}
}
})();