Bell / Animation

// ==UserScript==
// @name        Animation
// @namespace   https://greasyfork.org/users/281093
// @match       https://sketchful.io/
// @grant       none
// @version     0.2
// @author      Bell
// @license     MIT
// @copyright   2020, Bell (https://openuserjs.org/users/Bell)
// @require     https://gitcdn.link/repo/antimatter15/jsgif/master/LZWEncoder.js
// @require     https://gitcdn.link/repo/antimatter15/jsgif/master/NeuQuant.js
// @require     https://gitcdn.link/repo/antimatter15/jsgif/master/GIFEncoder.js
// @description 8/4/2020, 12:03:46 PM
// ==/UserScript==
/* jshint esversion: 6 */
/* eslint-disable no-undef */

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 }',
];

const sheet = window.document.styleSheets[window.document.styleSheets.length - 1];

const outerContainer = document.createElement('div');
const canvasContainer = document.querySelector('#gameCanvas');
const canvasInner = document.querySelector("#gameCanvasInner");
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: 10px;
						overflow-y: hidden;
						border-radius: 10px;
						margin-bottom: 5px;
						margin-left: 5vw;
						width: 100%`;

const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
const encoder = new GIFEncoder();

const layers = [];
const contextLayers = [];

(() => {
	addLayerContainer();
	styleRules.forEach((rule) => sheet.insertRule(rule));
	document.addEventListener('keydown', handleInput);
	const gameModeObserver = new MutationObserver(checkRoomType);
	
	gameModeObserver.observe(document.querySelector('.game'), 
				 			{ attributes: true });
	gameModeObserver.observe(canvas, { attributes: true });
})();

function checkRoomType() {
	outerContainer.style.display = isFreeDraw() ? 'flex' : 'none';
}

function handleInput(e) {
	switch (e.code) {
	case 'KeyL':
		addLayer();
		break;
	case 'KeyR':
		if (!e.shiftKey) return;
		const name = "sketchful-gif-" + Date.now();
		render(name, 1000);
		break;
	}
}

function addLayer() {
	resetActiveLayer();
	const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
	contextLayers.push(copyCtx(imgData));
	saveLayer();
	const canvasLayer = createLayer();
	const layerCtx = canvasLayer.getContext('2d');
	layerCtx.putImageData(imgData, 0, 0);
	makeTransparent(layerCtx);
	
	// document.querySelector('#gameToolsClear').click();
	const previousLayer = canvasInner.querySelector("#canvasLayer");
	if (previousLayer) previousLayer.remove();
	canvas.parentElement.insertBefore(canvasLayer, canvas);
}

function render(name, delay) {
	if (!contextLayers.length) return;
	encoder.setRepeat(0);
	encoder.setDelay(delay);
	encoder.start();
	contextLayers.forEach(layer => {
		encoder.addFrame(layer);
	});
	encoder.finish();
	encoder.download(name + ".gif");
}

function copyCtx(imgData) {
	const tempCanvas = document.createElement('canvas');
	tempCanvas.width = canvas.width;
	tempCanvas.height = canvas.height;
	const tempCtx = tempCanvas.getContext('2d');
	tempCtx.putImageData(imgData, 0, 0);
	return tempCtx;
}
	
function saveLayer() {
	const container = document.querySelector("#layerContainer");
	// const div = document.createElement("div");
	const img = document.createElement("img");
	img.style.width = "133px";
	img.style.cursor = "pointer";
	img.style.marginRight = "5px";
	img.src = canvas.toDataURL();
	layers.push(img.src);
	container.append(img);
}

function setActiveLayer(e) {
	const img = e.target;
	if (img.tagName !== "IMG") return;
	resetActiveLayer();
	const canvasLayerCtx = document.querySelector("#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);
	}
	img.id = "activeLayer";
	img.style.border = "3px solid red";
	ctx.drawImage(img, 0, 0);
}

function resetActiveLayer() {
	const layer = document.querySelector("#activeLayer");
	if (!layer) return;
	layer.id = "";
	layer.style.border = "";
}

function createLayer() {
	const canvasLayer = document.createElement('canvas');
	canvasLayer.style.width = '100%';
	canvasLayer.style.position = 'absolute';
	canvasLayer.style.pointerEvents = 'none';
	canvasLayer.style.imageRendering = 'pixelated';
	canvasLayer.style.filter = 'opacity(0.5)';
	canvasLayer.width = canvas.width;
	canvasLayer.height = canvas.height;
	canvasLayer.id = "canvasLayer";
	return canvasLayer;
}

function downloadGif(data, name) {
    let a = document.createElement("a");
    a.download = name + ".gif";
    a.href = data;
    a.click();
}

function addButton(text, clickFunction, element, type) {
	const button = document.createElement("div");
	button.setAttribute("class", `btn btn-${type}`);
	button.setAttribute("style", "height: fit-content; margin-top: 10px; margin-left: 10px;");
	button.textContent = text;
	button.onclick = clickFunction;
	element.append(button);
}

function getInterval() {
	let interval = parseInt(document.querySelector("#gifInterval").value);
	if (isNaN(interval) || interval < 0 || interval > 5000) interval = 100;
	return interval;
}

function renderGif() {
	const interval = getInterval();
	const name = "sketchful-gif-" + Date.now();
	render(name, interval);
	console.log("rendered " + name);
}

function removeLayer() {
	const activeLayer = document.querySelector('#activeLayer');
	const layerContainer = document.querySelector('#layerContainer');
	if (!activeLayer) return;
	const index = Array.prototype.indexOf.call(layerContainer.children, activeLayer);
	contextLayers.splice(index, 1);
	activeLayer.remove();
}

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";
	container.setAttribute("style", containerStyle);
	container.setAttribute("ondragstart", "return false");
	outerContainer.append(container);
	
	const buttonContainer = document.createElement("div");
	buttonContainer.style.width = "15%";
	outerContainer.append(buttonContainer);
	addButton("Save Gif", renderGif, buttonContainer, "warning");
	addButton("Save Layer", addLayer, buttonContainer, "info");
	addButton("Delete Layer", removeLayer, buttonContainer, "danger");
	addButton("Play", playAnimation, buttonContainer, "success");
	
	const textInput = document.createElement("input");
	textInput.style.width = "100px";
	textInput.placeholder = "Interval (ms)";
	textInput.style.marginTop = "10px";
	textInput.style.marginLeft = "10px";
	textInput.id = "gifInterval";
	setInputFilter(textInput, function(value) {
		return /^\d*\.?\d*$/.test(value);
	});
	buttonContainer.append(textInput);
	
	game.append(outerContainer);
}

let animating = null;
function playAnimation(e) {
	const playButton = e.target;
	const canvasLayer = document.querySelector("#canvasLayer");
	
	if (playButton.textContent === "Stop") {
		playButton.classList.toggle("btn-success");
		playButton.classList.toggle("btn-danger");
		playButton.textContent = "Play";
		if (animating) clearInterval(animating);
		const preview = document.querySelector("#gifPreview");
		if (preview) preview.remove();
		if (canvasLayer) canvasLayer.style.display = "";
		return;
	}
	
	if (canvasLayer) canvasLayer.style.display = "none";
	const canvasCover = document.querySelector("#canvasCover");
	const layerContainer = document.querySelector("#layerContainer");
	const img = document.createElement('img');
	img.style.position = "absolute";
	img.style.width = "100%";
	img.style.imageRendering = "pixelated";
	img.id = "gifPreview";
	
	canvasCover.parentElement.insertBefore(img, canvasCover);
	
	let frame = layerContainer.firstChild;
	if (!frame) return;
	const interval = getInterval();
	
	playButton.classList.toggle("btn-success");
	playButton.classList.toggle("btn-danger");
	playButton.textContent = "Stop";
	
	animating = setInterval(() => {
		img.src = frame.src;
		frame = frame.nextSibling || layerContainer.firstChild;
	}, 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 == 255 && g == 255 && b == 255) {
			data[i + 3] = 0;
		}
	}
	
	context.putImageData(imgData, 0, 0);
}