// ==UserScript==
// @name Animation
// @namespace https://greasyfork.org/users/281093
// @match https://sketchful.io/
// @grant none
// @version 0.6.6
// @author Bell
// @license MIT
// @copyright 2020, Bell (https://openuserjs.org/users/Bell)
// @require https://cdnjs.cloudflare.com/ajax/libs/gifshot/0.3.2/gifshot.min.js
// @require https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/libgif.min.js
// @description Animation tools for Sketchful.io
// ==/UserScript==
/* jshint esversion: 6 */
const containerStyle =
`white-space: nowrap;
overflow: auto;
justify-content:center;
margin-top: 10px;
max-width: 76%;
height: 124px;
background: rgb(0 0 0 / 30%);
padding: 12px;
overflow-y: hidden;
border-radius: 10px;
margin-bottom: 5px;
margin-left: 5vw;
width: 100%;
user-select: none;`;
const canvasLayerStyle =
`width: 100%;
position: absolute;
pointer-events: none;
image-rendering: pixelated;
filter: opacity(0.5);`;
const styleRules = [
'#layerContainer::-webkit-scrollbar { width: 5px; height: 5px; overflow: hidden}',
'#layerContainer::-webkit-scrollbar-track { background: none }',
'#layerContainer::-webkit-scrollbar-thumb { background: #F5BC09; border-radius: 5px }',
`#layerContainer { ${containerStyle} }`,
`.layer { ${canvasLayerStyle} }`,
'#layerContainer img { width: 133px; cursor: pointer; margin-right: 5px }',
'#buttonContainer { max-width: 230px; min-width: 230px; }',
'#buttonContainer div { height: fit-content; margin-top: 10px; margin-left: 10px; }',
'#buttonContainer { width: 15%; padding-top: 5px }',
'#gifPreview { position: absolute; z-index: 1; width: 100%; image-rendering: pixelated; }',
'.hidden { display: none }',
'#activeLayer { margin-top: -1px; border: 3px solid red }'
];
const sheet = window.document.styleSheets[window.document.styleSheets.length - 1];
const outerContainer = document.createElement('div');
const onionContainer = document.createElement('div');
const canvasContainer = document.querySelector('#gameCanvas');
const canvasInner = document.querySelector("#gameCanvasInner");
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
const layerContainer = addLayerContainer();
let copied = null;
(() => {
canvas.parentElement.insertBefore(onionContainer, canvas);
addButtons();
styleRules.forEach((rule) => sheet.insertRule(rule));
const gameModeObserver = new MutationObserver(checkRoomType);
gameModeObserver.observe(document.querySelector('.game'),
{ attributes: true });
gameModeObserver.observe(canvas, { attributes: true });
layerContainer.addEventListener("dragenter", highlight, false);
layerContainer.addEventListener("dragleave", unhighlight, false);
layerContainer.addEventListener("drop", handleDrop, false);
layerContainer.addEventListener("dragover", function(event) {
event.preventDefault();
}, false);
document.addEventListener('keydown', (e) => {
if (!e.ctrlKey || document.activeElement.tagName === "INPUT") return;
const selectedLayer = document.querySelector("#activeLayer");
if (e.code === "KeyC") {
if (!selectedLayer) return;
copied = selectedLayer.cloneNode();
e.stopImmediatePropagation();
}
else if (e.code === "KeyV" && copied) {
const copy = copied.cloneNode();
if (selectedLayer)
insertAfter(copy, selectedLayer);
else
layerContainer.append(copy);
resetActiveLayer();
setActiveLayer({ target: copy });
}
});
})();
function checkRoomType() {
outerContainer.style.display = isFreeDraw() ? 'flex' : 'none';
onionContainer.style.display = isFreeDraw() ? "" : "none";
}
function addLayer() {
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
saveLayer(canvas);
const canvasLayer = document.querySelector("#canvasLayer") || createLayer();
const layerCtx = canvasLayer.getContext('2d');
layerCtx.putImageData(imgData, 0, 0);
makeTransparent(layerCtx);
const previousLayer = canvasInner.querySelector("#canvasLayer");
if (previousLayer) previousLayer.remove();
onionContainer.appendChild(canvasLayer);
}
function saveGif() {
const container = document.querySelector("#layerContainer");
if (!container.childElementCount) return;
const layers = Array.from(container.children).map(image => image.src);
const interval = getInterval();
gifshot.createGIF({
gifWidth: canvas.width,
gifHeight: canvas.height,
interval: interval / 1000,
images: layers
}, downloadGif);
}
function extractFrames(img) {
const gifLoaderTemp = document.createElement('div');
gifLoaderTemp.style.display = "none";
gifLoaderTemp.append(img);
document.body.append(gifLoaderTemp);
img.setAttribute ("rel:auto_play", 0);
const gif = new SuperGif({ gif: img });
gif.load(()=> {
const gifCanvas = gif.get_canvas();
if (gifCanvas.width !== canvas.width || gifCanvas.height !== canvas.height) {
alert("Not a sketchful gif");
return;
}
if (!document.querySelector("#canvasLayer"))
onionContainer.appendChild(createLayer());
const numFrames = gif.get_length();
for (let i = 0; i < numFrames; i++) {
gif.move_to(i);
saveLayer(gifCanvas);
}
});
}
function handleDrop(e) {
e.preventDefault();
layerContainer.style.filter = "";
let dt = e.dataTransfer;
let files = dt.files;
if (files.length && files !== null) {
handleFiles(files);
}
}
function handleFiles(files) {
files = [...files];
files.forEach(previewFile);
}
function previewFile(file) {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = function () {
let gif = document.createElement('img');
gif.src = reader.result;
extractFrames(gif);
};
}
function highlight(e) {
e.preventDefault();
layerContainer.style.filter = "drop-shadow(0px 0px 6px green)";
}
function unhighlight(e) {
e.preventDefault();
layerContainer.style.filter = "";
}
function saveLayer(canv) {
const activeLayer = document.querySelector("#activeLayer");
const container = document.querySelector("#layerContainer");
const img = document.createElement("img");
img.src = canv.toDataURL();
if (activeLayer) {
insertAfter(img, activeLayer);
setActiveLayer({ target: img });
} else {
container.append(img);
}
}
function insertAfter(newNode, referenceNode) {
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}
function setActiveLayer(e) {
const img = e.target;
if (img.tagName !== "IMG") {
resetActiveLayer();
return;
}
resetActiveLayer();
img.id = "activeLayer";
ctx.drawImage(img, 0, 0);
const canvasLayer = document.querySelector("#canvasLayer");
if (!canvasLayer) return;
const canvasLayerCtx = canvasLayer.getContext("2d");
const previousImg = img.previousSibling;
if (previousImg) {
canvasLayerCtx.drawImage(previousImg, 0, 0);
makeTransparent(canvasLayerCtx);
} else {
canvasLayerCtx.clearRect(0, 0, canvas.width, canvas.height);
}
}
function resetActiveLayer() {
const layer = document.querySelector("#activeLayer");
if (!layer) return;
layer.id = "";
layer.style.border = "";
}
function createLayer() {
const canvasLayer = document.createElement('canvas');
canvasLayer.classList.add("layer");
canvasLayer.width = canvas.width;
canvasLayer.height = canvas.height;
canvasLayer.id = "canvasLayer";
return canvasLayer;
}
function downloadGif(obj) {
const name = "sketchful-gif-" + Date.now();
let a = document.createElement("a");
a.download = name + ".gif";
a.href = obj.image;
a.click();
}
function addButton(text, clickFunction, element, type) {
const button = document.createElement("div");
button.setAttribute("class", `btn btn-sm btn-${type}`);
button.textContent = text;
button.onpointerup = clickFunction;
element.append(button);
return button;
}
function clamp(num, min, max) {
return num <= min ? min : num >= max ? max : num;
}
function getInterval() {
const input = document.querySelector("#gifIntervalInput");
let interval = parseInt(input.value);
if (isNaN(interval)) interval = 100;
interval = clamp(interval, 20, 10000);
input.value = interval;
return interval;
}
function removeLayer() {
const activeLayer = document.querySelector('#activeLayer');
const layerContainer = document.querySelector('#layerContainer');
if (!activeLayer) return;
const index = nodeIndex(activeLayer);
activeLayer.remove();
}
function nodeIndex(node) {
return Array.prototype.indexOf.call(node.parentNode.children, node);
}
function addButtons() {
const buttonContainer = document.createElement("div");
buttonContainer.id = "buttonContainer";
outerContainer.append(buttonContainer);
addButton("Save Gif", saveGif, buttonContainer, "warning");
addButton("Onion", toggleOnion, buttonContainer, "success");
addButton("Save Layer", addLayer, buttonContainer, "info");
addButton("Delete Layer", removeLayer, buttonContainer, "danger");
addButton("Play", playAnimation, buttonContainer, "success");
const textDiv = document.createElement('div');
const textInput = document.createElement("input");
textDiv.classList.add('btn');
textDiv.style.padding = '0px';
textInput.placeholder = "Interval (ms)";
textInput.style.width = "100px";
textInput.id = "gifIntervalInput";
setInputFilter(textInput, (v) => {return /^\d*\.?\d*$/.test(v);});
textDiv.append(textInput);
buttonContainer.append(textDiv);
}
function addLayerContainer() {
const game = document.querySelector("body > div.game");
const container = document.createElement("div");
outerContainer.style.display = "flex";
outerContainer.style.flexDirection = "row";
container.addEventListener('wheel', (e) => {
if (e.deltaY > 0) container.scrollLeft += 100;
else container.scrollLeft -= 100;
e.preventDefault();
});
container.addEventListener('pointerdown', setActiveLayer, true);
container.id = "layerContainer";
new Sortable(container, {
animation: 150,
});
outerContainer.append(container);
game.append(outerContainer);
return container;
}
let onion = true;
function toggleOnion(e) {
onion = !onion;
this.textContent = onion ? "Onion" : "Onioff";
onionContainer.classList.toggle("hidden");
this.classList.toggle("btn-success");
this.classList.toggle("btn-danger");
}
let animating = false;
function playAnimation(e) {
const preview = document.querySelector("#gifPreview");
if (animating) {
this.classList.toggle("btn-success");
this.classList.toggle("btn-danger");
this.textContent = "Play";
if (preview) preview.remove();
animating = false;
return;
}
const canvasCover = document.querySelector("#canvasCover");
const layerContainer = document.querySelector("#layerContainer");
const img = document.createElement('img');
img.id = "gifPreview";
img.draggable = false;
canvasCover.parentElement.insertBefore(img, canvasCover);
let frame = layerContainer.firstChild;
if (!frame) return;
const interval = getInterval();
this.classList.toggle("btn-success");
this.classList.toggle("btn-danger");
this.textContent = "Stop";
animating = true;
(function playFrame() {
if (!animating) return;
img.src = frame.src;
frame = frame.nextSibling || layerContainer.firstChild;
setTimeout(playFrame, interval);
})();
}
function isFreeDraw() {
return (
document.querySelector("#canvas").style.display !== 'none' &&
document.querySelector('#gameClock').style.display === 'none' &&
document.querySelector('#gameSettings').style.display === 'none'
);
}
function setInputFilter(textbox, inputFilter) {
["input", "keydown", "keyup", "mousedown",
"mouseup", "select", "contextmenu", "drop"].forEach(function(event) {
textbox.addEventListener(event, function() {
if (inputFilter(this.value)) {
this.oldValue = this.value;
this.oldSelectionStart = this.selectionStart;
this.oldSelectionEnd = this.selectionEnd;
} else if (this.hasOwnProperty("oldValue")) {
this.value = this.oldValue;
this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
} else {
this.value = "";
}
});
});
}
function makeTransparent(context) {
const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imgData.data;
for(let i = 0; i < data.length; i += 4) {
const [r, g, b] = data.slice(i, i + 3);
if (r >= 210 && g >= 210 && b >= 210) {
data[i + 3] = 0;
}
}
context.putImageData(imgData, 0, 0);
}