Animation

Animation tools for Sketchful.io

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

  1. // ==UserScript==
  2. // @name Animation
  3. // @description Animation tools for Sketchful.io
  4. // @namespace https://greasyfork.org/users/281093
  5. // @match https://sketchful.io/
  6. // @grant none
  7. // @version 0.7.3
  8. // @author Bell
  9. // @license MIT
  10. // @copyright 2020, Bell (https://openuserjs.org/users/Bell)
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/gifshot/0.3.2/gifshot.min.js
  12. // @require https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js
  13. // @require https://cdn.jsdelivr.net/npm/libgif@0.0.3/libgif.min.js
  14. // ==/UserScript==
  15. /* jshint esversion: 6 */
  16.  
  17. const containerStyle =
  18. `white-space: nowrap;
  19. overflow: auto;
  20. justify-content: center;
  21. margin-top: 10px;
  22. max-width: 70%;
  23. height: 124px;
  24. background: rgb(0 0 0 / 30%);
  25. padding: 12px;
  26. overflow-y: hidden;
  27. border-radius: 10px;
  28. margin-bottom: 5px;
  29. width: 100%;
  30. user-select: none;`;
  31.  
  32. const canvasLayerStyle =
  33. `width: 100%;
  34. position: absolute;
  35. pointer-events: none;
  36. image-rendering: pixelated;`;
  37.  
  38. const styleRules = [
  39. '#layerContainer::-webkit-scrollbar { width: 5px; height: 5px; overflow: hidden}',
  40. '#layerContainer::-webkit-scrollbar-track { background: none }',
  41. '#layerContainer::-webkit-scrollbar-thumb { background: #F5BC09; border-radius: 5px }',
  42. `#layerContainer { ${containerStyle} }`,
  43. `.layer { ${canvasLayerStyle} }`,
  44. '#layerContainer img { width: 133px; cursor: pointer; margin-right: 5px }',
  45. '#buttonContainer { max-width: 260px; min-width: 260px; }',
  46. '#buttonContainer div { height: fit-content; margin-top: 10px; margin-left: 10px; }',
  47. '#buttonContainer { width: 15%; padding-top: 5px }',
  48. '#gifPreview { position: absolute; z-index: 1; width: 100%; image-rendering: pixelated; }',
  49. '.hidden { display: none }',
  50. '#activeLayer { margin-top: -1px; border: 3px solid red }',
  51. '#buttonContainer input { width: 50px; border: none; height: 30px; text-align: center; border-radius: 5px}',
  52. '#buttonContainer input::-webkit-input-placeholder { text-align: center; }'
  53. ];
  54.  
  55. const sheet = window.document.styleSheets[window.document.styleSheets.length - 1];
  56. const outerContainer = document.createElement('div');
  57. const onionContainer = document.createElement('div');
  58. const canvas = document.querySelector('#canvas');
  59. const ctx = canvas.getContext('2d');
  60. const layerContainer = addLayerContainer();
  61. const onionLayers = createOnionLayers();
  62.  
  63. (() => {
  64. canvas.parentElement.insertBefore(onionContainer, canvas);
  65. addButtons();
  66. styleRules.forEach((rule) => sheet.insertRule(rule));
  67. const gameModeObserver = new MutationObserver(checkRoomType);
  68.  
  69. gameModeObserver.observe(document.querySelector('.game'),
  70. { attributes: true });
  71. gameModeObserver.observe(canvas, { attributes: true });
  72.  
  73. layerContainer.addEventListener('dragenter', highlight, false);
  74. layerContainer.addEventListener('dragleave', unhighlight, false);
  75. layerContainer.addEventListener('drop', handleDrop, false);
  76. layerContainer.addEventListener('dragover', function(event) {
  77. event.preventDefault();
  78. }, false);
  79.  
  80. document.addEventListener('keydown', copyPaste);
  81. })();
  82.  
  83. let copied = null;
  84. function copyPaste(e) {
  85. if (!e.ctrlKey || document.activeElement.tagName === 'INPUT') return;
  86.  
  87. const selectedLayer = document.querySelector('#activeLayer');
  88.  
  89. if (e.code === 'KeyC') {
  90. if (!selectedLayer) return;
  91. copied = selectedLayer.cloneNode();
  92. e.stopImmediatePropagation();
  93. }
  94. else if (e.code === 'KeyV' && copied) {
  95. const copy = copied.cloneNode();
  96.  
  97. if (selectedLayer) {insertAfter(copy, selectedLayer);}
  98. else {layerContainer.append(copy);}
  99.  
  100. resetActiveLayer();
  101. setActiveLayer({ target: copy });
  102. copy.scrollIntoView();
  103. }
  104. }
  105.  
  106. function checkRoomType() {
  107. outerContainer.style.display = isFreeDraw() ? 'flex' : 'none';
  108. onionContainer.style.display = isFreeDraw() ? '' : 'none';
  109. }
  110.  
  111. function addLayer() {
  112. const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  113. saveLayer(canvas);
  114.  
  115. onionLayers.previous.putImageData(imgData, 0, 0);
  116. makeTransparent(onionLayers.previous, 30, 0);
  117. }
  118.  
  119. function createOnionLayers() {
  120. return {
  121. previous: createLayer().getContext('2d'),
  122. next: createLayer().getContext('2d'),
  123. hide: () => {
  124. onionContainer.classList.add('hidden');
  125. },
  126. show: () => {
  127. onionContainer.classList.remove('hidden');
  128. }
  129. };
  130. }
  131.  
  132. function saveGif() {
  133. const container = document.querySelector('#layerContainer');
  134. if (!container.childElementCount) return;
  135. const layers = Array.from(container.children).map(image => image.src);
  136.  
  137. const interval = getInterval();
  138. gifshot.createGIF({
  139. gifWidth: canvas.width,
  140. gifHeight: canvas.height,
  141. interval: interval / 1000,
  142. images: layers
  143. }, downloadGif);
  144. }
  145.  
  146. function extractFrames(img) {
  147. const gifLoaderTemp = document.createElement('div');
  148.  
  149. gifLoaderTemp.style.display = 'none';
  150. gifLoaderTemp.append(img);
  151.  
  152. document.body.append(gifLoaderTemp);
  153. img.setAttribute ('rel:auto_play', 0);
  154. const gif = new SuperGif({ gif: img });
  155.  
  156. gif.load(()=> {
  157. const gifCanvas = gif.get_canvas();
  158.  
  159. if (gifCanvas.width !== canvas.width || gifCanvas.height !== canvas.height) {
  160. alert('Not a sketchful gif');
  161. return;
  162. }
  163.  
  164. const numFrames = gif.get_length();
  165. for (let i = 0; i < numFrames; i++) {
  166. gif.move_to(i);
  167. saveLayer(gifCanvas);
  168. }
  169. });
  170. }
  171.  
  172. function handleDrop(e) {
  173. e.preventDefault();
  174. layerContainer.style.filter = '';
  175. const dt = e.dataTransfer;
  176. const files = dt.files;
  177.  
  178. if (files.length && files !== null) {
  179. handleFiles(files);
  180. }
  181. }
  182.  
  183. function handleFiles(files) {
  184. files = [...files];
  185. files.forEach(previewFile);
  186. }
  187.  
  188. function previewFile(file) {
  189. const reader = new FileReader();
  190. reader.readAsDataURL(file);
  191. reader.onloadend = function() {
  192. const gif = document.createElement('img');
  193. gif.src = reader.result;
  194. extractFrames(gif);
  195. };
  196. }
  197.  
  198. function highlight(e) {
  199. e.preventDefault();
  200. layerContainer.style.filter = 'drop-shadow(0px 0px 6px green)';
  201. }
  202.  
  203. function unhighlight(e) {
  204. e.preventDefault();
  205. layerContainer.style.filter = '';
  206. }
  207.  
  208. function saveLayer(canv) {
  209. const activeLayer = document.querySelector('#activeLayer');
  210. const container = document.querySelector('#layerContainer');
  211. const img = document.createElement('img');
  212. img.src = canv.toDataURL();
  213.  
  214. if (activeLayer) {
  215. insertAfter(img, activeLayer);
  216. setActiveLayer({ target: img });
  217. }
  218. else {
  219. container.append(img);
  220. }
  221. img.scrollIntoView();
  222. }
  223.  
  224. function insertAfter(newNode, referenceNode) {
  225. referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  226. }
  227.  
  228. function setActiveLayer(e) {
  229. const img = e.target;
  230. if (img.tagName !== 'IMG') {
  231. resetActiveLayer();
  232. return;
  233. }
  234. resetActiveLayer();
  235.  
  236. img.id = 'activeLayer';
  237. if (!e.shiftKey) {
  238. ctx.drawImage(img, 0, 0);
  239. canvas.save();
  240. }
  241.  
  242. const previousImg = img.previousSibling;
  243. const nextImg = img.nextSibling;
  244.  
  245. if (previousImg) {
  246. onionLayers.previous.drawImage(previousImg, 0, 0);
  247. makeTransparent(onionLayers.previous, 30, 0);
  248. }
  249. else {
  250. onionLayers.previous.clearRect(0, 0, canvas.width, canvas.height);
  251. }
  252.  
  253. if (nextImg) {
  254. onionLayers.next.drawImage(nextImg, 0, 0);
  255. makeTransparent(onionLayers.next, 0, 30);
  256. }
  257. else {
  258. onionLayers.next.clearRect(0, 0, canvas.width, canvas.height);
  259. }
  260. }
  261.  
  262. function resetActiveLayer() {
  263. const layer = document.querySelector('#activeLayer');
  264. if (!layer) return;
  265. layer.id = '';
  266. layer.style.border = '';
  267. }
  268.  
  269. function createLayer() {
  270. const canvasLayer = document.createElement('canvas');
  271. canvasLayer.classList.add('layer');
  272. canvasLayer.width = canvas.width;
  273. canvasLayer.height = canvas.height;
  274. onionContainer.appendChild(canvasLayer);
  275. return canvasLayer;
  276. }
  277.  
  278. function downloadGif(obj) {
  279. const name = 'sketchful-gif-' + Date.now();
  280. const a = document.createElement('a');
  281. a.download = name + '.gif';
  282. a.href = obj.image;
  283. a.click();
  284. }
  285.  
  286. function addButton(text, clickFunction, element, type) {
  287. const button = document.createElement('div');
  288. button.setAttribute('class', `btn btn-sm btn-${type}`);
  289. button.textContent = text;
  290. button.onpointerup = clickFunction;
  291. element.append(button);
  292. return button;
  293. }
  294.  
  295. function clamp(num, min, max) {
  296. return num <= min ? min : num >= max ? max : num;
  297. }
  298.  
  299. function getInterval() {
  300. const input = document.querySelector('#gifIntervalInput');
  301. let fps = parseInt(input.value);
  302. if (isNaN(fps)) fps = 10;
  303. fps = clamp(fps, 1, 50);
  304. input.value = fps;
  305. return 1000 / fps;
  306. }
  307.  
  308. function removeLayer() {
  309. const activeLayer = document.querySelector('#activeLayer');
  310. if (!activeLayer) return;
  311. activeLayer.remove();
  312. }
  313.  
  314. function overwriteLayer() {
  315. const activeLayer = document.querySelector('#activeLayer');
  316. if (!activeLayer) return;
  317. activeLayer.src = canvas.toDataURL();
  318. }
  319.  
  320. let ahead = false;
  321. function toggleAhead() {
  322. ahead = !ahead;
  323. onionLayers.next.canvas.style.display = ahead ? 'none' : '';
  324. this.classList.toggle('btn-danger');
  325. this.classList.toggle('btn-info');
  326. }
  327.  
  328. function addButtons() {
  329. const buttonContainer = document.createElement('div');
  330. buttonContainer.id = 'buttonContainer';
  331. outerContainer.append(buttonContainer);
  332. addButton('Play', playAnimation, buttonContainer, 'success');
  333. const downloadBtn = addButton('Download', saveGif, buttonContainer, 'primary');
  334. addButton('Save Layer', addLayer, buttonContainer, 'info');
  335. addButton('Delete', removeLayer, buttonContainer, 'danger');
  336. addButton('Overwrite', overwriteLayer, buttonContainer, 'warning');
  337. addButton('Onion', toggleOnion, buttonContainer, 'success');
  338. addButton('Ahead', toggleAhead, buttonContainer, 'info');
  339.  
  340. const textDiv = document.createElement('div');
  341. const textInput = document.createElement('input');
  342. textDiv.classList.add('btn');
  343. textDiv.style.padding = '0px';
  344. textInput.placeholder = 'FPS';
  345. textInput.id = 'gifIntervalInput';
  346. setInputFilter(textInput, (v) => {return /^\d*\.?\d*$/.test(v);});
  347. textDiv.append(textInput);
  348.  
  349. buttonContainer.insertBefore(textDiv, downloadBtn);
  350. }
  351.  
  352. function addLayerContainer() {
  353. const game = document.querySelector('body > div.game');
  354. const container = document.createElement('div');
  355. outerContainer.style.display = 'flex';
  356. outerContainer.style.flexDirection = 'row';
  357. outerContainer.style.justifyContent = 'center';
  358.  
  359. container.addEventListener('wheel', (e) => {
  360. if (e.deltaY > 0) container.scrollLeft += 100;
  361. else container.scrollLeft -= 100;
  362. e.preventDefault();
  363. });
  364.  
  365. container.addEventListener('pointerdown', (e) => {
  366. if (e.button !== 0) {
  367. resetActiveLayer();
  368. return;
  369. }
  370. setActiveLayer(e);
  371. }, true);
  372.  
  373. container.addEventListener('contextmenu', (e) => {
  374. e.preventDefault();
  375. }, true);
  376.  
  377. container.id = 'layerContainer';
  378.  
  379. new Sortable(container, {
  380. animation: 150
  381. });
  382. outerContainer.append(container);
  383.  
  384. game.append(outerContainer);
  385. return container;
  386. }
  387.  
  388. let onion = true;
  389. function toggleOnion() {
  390. onion = !onion;
  391. this.textContent = onion ? 'Onion' : 'Onioff';
  392. if (onion) {
  393. onionLayers.show();
  394. }
  395. else {
  396. onionLayers.hide();
  397. }
  398. this.classList.toggle('btn-success');
  399. this.classList.toggle('btn-danger');
  400. }
  401.  
  402. let animating = false;
  403. function playAnimation() {
  404. let preview = document.querySelector('#gifPreview');
  405.  
  406. if (animating) {
  407. this.classList.toggle('btn-success');
  408. this.classList.toggle('btn-danger');
  409. this.textContent = 'Play';
  410. while (preview) {
  411. preview.remove();
  412. preview = document.querySelector('#gifPreview');
  413. }
  414. animating = false;
  415. return;
  416. }
  417.  
  418. const canvasCover = document.querySelector('#canvasCover');
  419. const img = document.createElement('img');
  420. img.id = 'gifPreview';
  421. img.draggable = false;
  422. canvasCover.parentElement.insertBefore(img, canvasCover);
  423.  
  424. let frame = layerContainer.firstChild;
  425. if (!frame) return;
  426. const interval = getInterval();
  427.  
  428. this.classList.toggle('btn-success');
  429. this.classList.toggle('btn-danger');
  430. this.textContent = 'Stop';
  431. animating = true;
  432.  
  433. (function playFrame() {
  434. if (!animating) return;
  435. img.src = frame.src;
  436. frame = frame.nextSibling || layerContainer.firstChild;
  437. setTimeout(playFrame, interval);
  438. })();
  439. }
  440.  
  441. function isFreeDraw() {
  442. return (
  443. document.querySelector('#canvas').style.display !== 'none' &&
  444. document.querySelector('#gameClock').style.display === 'none' &&
  445. document.querySelector('#gameSettings').style.display === 'none'
  446. );
  447. }
  448.  
  449. function setInputFilter(textbox, inputFilter) {
  450. ['input', 'keydown', 'keyup', 'mousedown',
  451. 'mouseup', 'select', 'contextmenu', 'drop'].forEach(function(event) {
  452. textbox.addEventListener(event, function() {
  453. if (inputFilter(this.value)) {
  454. this.oldValue = this.value;
  455. this.oldSelectionStart = this.selectionStart;
  456. this.oldSelectionEnd = this.selectionEnd;
  457. }
  458. else if (this.hasOwnProperty('oldValue')) {
  459. this.value = this.oldValue;
  460. this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
  461. }
  462. else {
  463. this.value = '';
  464. }
  465. });
  466. });
  467. }
  468.  
  469. function makeTransparent(context, red, green) {
  470. const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
  471. const data = imgData.data;
  472.  
  473. for(let i = 0; i < data.length; i += 4) {
  474. const [r, g, b] = data.slice(i, i + 3);
  475. if (r >= 200 && g >= 200 && b >= 200) {
  476. data[i + 3] = 0;
  477. }
  478. else {
  479. data[i] += (data[i] + red) <= 255 ? red : 0;
  480. data[i + 1] += (data[i + 1] + green) <= 255 ? green : 0;
  481. data[i + 3] = 130;
  482. }
  483. }
  484.  
  485. context.putImageData(imgData, 0, 0);
  486. }
  487.  
  488. canvas.save = () => {
  489. canvas.dispatchEvent(new MouseEvent('pointerup', {
  490. bubbles: true,
  491. clientX: 0,
  492. clientY: 0,
  493. button: 0
  494. }));
  495. };