Animation

Animation tools for Sketchful.io

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