// ==UserScript==
// @name Animation
// @description Animation tools for Sketchful.io
// @namespace https://greasyfork.org/users/281093
// @match https://sketchful.io/
// @grant none
// @version 0.7.3
// @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
// ==/UserScript==
/* jshint esversion: 6 */
const containerStyle =
`white-space: nowrap;
overflow: auto;
justify-content: center;
margin-top: 10px;
max-width: 70%;
height: 124px;
background: rgb(0 0 0 / 30%);
padding: 12px;
overflow-y: hidden;
border-radius: 10px;
margin-bottom: 5px;
width: 100%;
user-select: none;`;
const canvasLayerStyle =
`width: 100%;
position: absolute;
pointer-events: none;
image-rendering: pixelated;`;
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: 260px; min-width: 260px; }',
'#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 }',
'#buttonContainer input { width: 50px; border: none; height: 30px; text-align: center; border-radius: 5px}',
'#buttonContainer input::-webkit-input-placeholder { text-align: center; }'
];
const sheet = window.document.styleSheets[window.document.styleSheets.length - 1];
const outerContainer = document.createElement('div');
const onionContainer = document.createElement('div');
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
const layerContainer = addLayerContainer();
const onionLayers = createOnionLayers();
(() => {
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', copyPaste);
})();
let copied = null;
function copyPaste(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 });
copy.scrollIntoView();
}
}
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);
onionLayers.previous.putImageData(imgData, 0, 0);
makeTransparent(onionLayers.previous, 30, 0);
}
function createOnionLayers() {
return {
previous: createLayer().getContext('2d'),
next: createLayer().getContext('2d'),
hide: () => {
onionContainer.classList.add('hidden');
},
show: () => {
onionContainer.classList.remove('hidden');
}
};
}
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;
}
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 = '';
const dt = e.dataTransfer;
const files = dt.files;
if (files.length && files !== null) {
handleFiles(files);
}
}
function handleFiles(files) {
files = [...files];
files.forEach(previewFile);
}
function previewFile(file) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = function() {
const 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);
}
img.scrollIntoView();
}
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';
if (!e.shiftKey) {
ctx.drawImage(img, 0, 0);
canvas.save();
}
const previousImg = img.previousSibling;
const nextImg = img.nextSibling;
if (previousImg) {
onionLayers.previous.drawImage(previousImg, 0, 0);
makeTransparent(onionLayers.previous, 30, 0);
}
else {
onionLayers.previous.clearRect(0, 0, canvas.width, canvas.height);
}
if (nextImg) {
onionLayers.next.drawImage(nextImg, 0, 0);
makeTransparent(onionLayers.next, 0, 30);
}
else {
onionLayers.next.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;
onionContainer.appendChild(canvasLayer);
return canvasLayer;
}
function downloadGif(obj) {
const name = 'sketchful-gif-' + Date.now();
const 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 fps = parseInt(input.value);
if (isNaN(fps)) fps = 10;
fps = clamp(fps, 1, 50);
input.value = fps;
return 1000 / fps;
}
function removeLayer() {
const activeLayer = document.querySelector('#activeLayer');
if (!activeLayer) return;
activeLayer.remove();
}
function overwriteLayer() {
const activeLayer = document.querySelector('#activeLayer');
if (!activeLayer) return;
activeLayer.src = canvas.toDataURL();
}
let ahead = false;
function toggleAhead() {
ahead = !ahead;
onionLayers.next.canvas.style.display = ahead ? 'none' : '';
this.classList.toggle('btn-danger');
this.classList.toggle('btn-info');
}
function addButtons() {
const buttonContainer = document.createElement('div');
buttonContainer.id = 'buttonContainer';
outerContainer.append(buttonContainer);
addButton('Play', playAnimation, buttonContainer, 'success');
const downloadBtn = addButton('Download', saveGif, buttonContainer, 'primary');
addButton('Save Layer', addLayer, buttonContainer, 'info');
addButton('Delete', removeLayer, buttonContainer, 'danger');
addButton('Overwrite', overwriteLayer, buttonContainer, 'warning');
addButton('Onion', toggleOnion, buttonContainer, 'success');
addButton('Ahead', toggleAhead, buttonContainer, 'info');
const textDiv = document.createElement('div');
const textInput = document.createElement('input');
textDiv.classList.add('btn');
textDiv.style.padding = '0px';
textInput.placeholder = 'FPS';
textInput.id = 'gifIntervalInput';
setInputFilter(textInput, (v) => {return /^\d*\.?\d*$/.test(v);});
textDiv.append(textInput);
buttonContainer.insertBefore(textDiv, downloadBtn);
}
function addLayerContainer() {
const game = document.querySelector('body > div.game');
const container = document.createElement('div');
outerContainer.style.display = 'flex';
outerContainer.style.flexDirection = 'row';
outerContainer.style.justifyContent = 'center';
container.addEventListener('wheel', (e) => {
if (e.deltaY > 0) container.scrollLeft += 100;
else container.scrollLeft -= 100;
e.preventDefault();
});
container.addEventListener('pointerdown', (e) => {
if (e.button !== 0) {
resetActiveLayer();
return;
}
setActiveLayer(e);
}, true);
container.addEventListener('contextmenu', (e) => {
e.preventDefault();
}, true);
container.id = 'layerContainer';
new Sortable(container, {
animation: 150
});
outerContainer.append(container);
game.append(outerContainer);
return container;
}
let onion = true;
function toggleOnion() {
onion = !onion;
this.textContent = onion ? 'Onion' : 'Onioff';
if (onion) {
onionLayers.show();
}
else {
onionLayers.hide();
}
this.classList.toggle('btn-success');
this.classList.toggle('btn-danger');
}
let animating = false;
function playAnimation() {
let preview = document.querySelector('#gifPreview');
if (animating) {
this.classList.toggle('btn-success');
this.classList.toggle('btn-danger');
this.textContent = 'Play';
while (preview) {
preview.remove();
preview = document.querySelector('#gifPreview');
}
animating = false;
return;
}
const canvasCover = document.querySelector('#canvasCover');
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, red, green) {
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 >= 200 && g >= 200 && b >= 200) {
data[i + 3] = 0;
}
else {
data[i] += (data[i] + red) <= 255 ? red : 0;
data[i + 1] += (data[i + 1] + green) <= 255 ? green : 0;
data[i + 3] = 130;
}
}
context.putImageData(imgData, 0, 0);
}
canvas.save = () => {
canvas.dispatchEvent(new MouseEvent('pointerup', {
bubbles: true,
clientX: 0,
clientY: 0,
button: 0
}));
};