- // ==UserScript==
- // @name Animation
- // @description Animation tools for Sketchful.io
- // @namespace https://greasyfork.org/users/281093
- // @match https://sketchful.io/
- // @grant none
- // @version 0.8
- // @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/libgif@0.0.3/libgif.min.js
- // ==/UserScript==
- /* jshint esversion: 6 */
-
- const css = `
- #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 {
- 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;
- scrollbar-width: thin;
- scrollbar-color: #F5BC09 transparent;
- }
-
- .layer {
- width: 100%;
- position: absolute;
- pointer-events: none;
- image-rendering: pixelated;
- }
-
- #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 outerContainer = document.createElement('div');
- const onionContainer = document.createElement('div');
- const gameDiv = document.querySelector('.game');
- const canvas = document.querySelector('#canvas');
- const ctx = canvas.getContext('2d');
- const layerContainer = addLayerContainer();
- const onionLayers = createOnionLayers();
-
- (function init() {
- addButtons();
- addCSS(css);
- addListeners();
- addObservers();
- })();
-
- function addListeners() {
- layerContainer.addEventListener('dragenter', highlight, false);
- layerContainer.addEventListener('dragleave', unhighlight, false);
- layerContainer.addEventListener('drop', handleDrop, false);
- layerContainer.addEventListener('dragover', preventDefault, false);
-
- document.addEventListener('keydown', documentKeydown);
- }
-
- function addObservers() {
- const gameModeObserver = new MutationObserver(checkRoomType);
- const config = { attributes: true };
- gameModeObserver.observe(gameDiv, config);
- gameModeObserver.observe(canvas, config);
- }
-
- function addCSS(style) {
- const stylesheet = document.createElement('style');
- stylesheet.type = 'text/css';
- stylesheet.innerText = style;
- document.head.appendChild(stylesheet);
- }
-
- let copied = null;
- function documentKeydown(e) {
- if (document.activeElement.tagName === 'INPUT') return;
-
- if (e.code === 'KeyC' && e.ctrlKey) {
- const selectedLayer = document.querySelector('#activeLayer');
- if (!selectedLayer) return;
- copied = selectedLayer.cloneNode();
- e.stopImmediatePropagation();
- }
- else if (e.code === 'KeyV' && copied && e.ctrlKey) {
- pasteLayer();
- }
- }
-
- function pasteLayer() {
- const selectedLayer = document.querySelector('#activeLayer');
- 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() {
- canvas.parentElement.insertBefore(onionContainer, canvas);
- 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 => /^\d*\.?\d*$/.test(v));
- textDiv.append(textInput);
-
- buttonContainer.insertBefore(textDiv, downloadBtn);
- }
-
- function containerScroll(e) {
- e.preventDefault();
- const container = document.querySelector('#layerContainer');
- if (e.deltaY > 0) container.scrollLeft += 100;
- else container.scrollLeft -= 100;
- }
-
- function containerClick(e) {
- if (e.button !== 0) {
- resetActiveLayer();
- return;
- }
- setActiveLayer(e);
- }
-
- function preventDefault(e) {
- e.preventDefault();
- }
-
- function addLayerContainer() {
- const game = document.querySelector('div.gameParent');
- const container = document.createElement('div');
-
- outerContainer.style.display = 'flex';
- outerContainer.style.flexDirection = 'row';
- outerContainer.style.justifyContent = 'center';
-
- container.addEventListener('wheel', containerScroll);
- container.addEventListener('pointerdown', containerClick, true);
- container.addEventListener('contextmenu', 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 stopAnimation() {
- let preview = document.querySelector('#gifPreview');
- this.classList.toggle('btn-success');
- this.classList.toggle('btn-danger');
- this.textContent = 'Play';
- while (preview) {
- preview.remove();
- preview = document.querySelector('#gifPreview');
- }
- animating = false;
- }
-
- function playAnimation() {
- if (animating) {
- stopAnimation.call(this);
- 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
- }));
- };