Animation

Animation tools for Sketchful.io

当前为 2020-08-21 提交的版本,查看 最新版本

// ==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
	}));
};