Animation

Animation tools for Sketchful.io

目前為 2020-08-06 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Animation 
// @namespace   https://greasyfork.org/users/281093
// @match       https://sketchful.io/
// @grant       none
// @version     0.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
// @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 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');

(() => {
	canvas.parentElement.insertBefore(onionContainer, canvas);
	addLayerContainer();
	addButtons();
	styleRules.forEach((rule) => sheet.insertRule(rule));
	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';
	onionContainer.style.display = isFreeDraw() ? "" : "none";
}

function addLayer() {
	const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
	saveLayer(imgData);
	
	const 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 saveLayer(data) {
	const activeLayer = document.querySelector("#activeLayer");
	const container = document.querySelector("#layerContainer");
	const img = document.createElement("img");
	img.src = canvas.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();
	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";
	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.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("NOnion", toggleOnion, buttonContainer, "warning");
	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);
}

let onion = true;
function toggleOnion(e) {
	onion = !onion;
	this.textContent = onion ? "NOnion" : "Onion";
	onionContainer.classList.toggle("hidden");
}

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 >= 230 && g >= 230 && b >= 230) {
			data[i + 3] = 0;
		}
	}
	
	context.putImageData(imgData, 0, 0);
}