Animation

Animation tools for Sketchful.io

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

  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.2
  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 }',
  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 canvasContainer = document.querySelector('#gameCanvas');
  59. const canvasInner = document.querySelector("#gameCanvasInner");
  60. const canvas = document.querySelector('#canvas');
  61. const ctx = canvas.getContext('2d');
  62. const layerContainer = addLayerContainer();
  63. const onionLayers = createOnionLayers();
  64.  
  65. (() => {
  66. canvas.parentElement.insertBefore(onionContainer, canvas);
  67. addButtons();
  68. styleRules.forEach((rule) => sheet.insertRule(rule));
  69. const gameModeObserver = new MutationObserver(checkRoomType);
  70. gameModeObserver.observe(document.querySelector('.game'),
  71. { attributes: true });
  72. gameModeObserver.observe(canvas, { attributes: true });
  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. document.addEventListener('keydown', copyPaste);
  80. })();
  81.  
  82. let copied = null;
  83. function copyPaste(e) {
  84. if (!e.ctrlKey || document.activeElement.tagName === "INPUT") return;
  85.  
  86. const selectedLayer = document.querySelector("#activeLayer");
  87.  
  88. if (e.code === "KeyC") {
  89. if (!selectedLayer) return;
  90. copied = selectedLayer.cloneNode();
  91. e.stopImmediatePropagation();
  92. }
  93. else if (e.code === "KeyV" && copied) {
  94. const copy = copied.cloneNode();
  95.  
  96. if (selectedLayer)
  97. insertAfter(copy, selectedLayer);
  98. else
  99. layerContainer.append(copy);
  100.  
  101. resetActiveLayer();
  102. setActiveLayer({ target: copy });
  103. copy.scrollIntoView();
  104. }
  105. }
  106.  
  107. function checkRoomType() {
  108. outerContainer.style.display = isFreeDraw() ? 'flex' : 'none';
  109. onionContainer.style.display = isFreeDraw() ? "" : "none";
  110. }
  111.  
  112. function addLayer() {
  113. const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  114. saveLayer(canvas);
  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. const interval = getInterval();
  137. gifshot.createGIF({
  138. gifWidth: canvas.width,
  139. gifHeight: canvas.height,
  140. interval: interval / 1000,
  141. images: layers
  142. }, downloadGif);
  143. }
  144.  
  145. function extractFrames(img) {
  146. const gifLoaderTemp = document.createElement('div');
  147.  
  148. gifLoaderTemp.style.display = "none";
  149. gifLoaderTemp.append(img);
  150. document.body.append(gifLoaderTemp);
  151. img.setAttribute ("rel:auto_play", 0);
  152. const gif = new SuperGif({ gif: img });
  153. gif.load(()=> {
  154. const gifCanvas = gif.get_canvas();
  155. if (gifCanvas.width !== canvas.width || gifCanvas.height !== canvas.height) {
  156. alert("Not a sketchful gif");
  157. return;
  158. }
  159. const numFrames = gif.get_length();
  160. for (let i = 0; i < numFrames; i++) {
  161. gif.move_to(i);
  162. saveLayer(gifCanvas);
  163. }
  164. });
  165. }
  166.  
  167. function handleDrop(e) {
  168. e.preventDefault();
  169. layerContainer.style.filter = "";
  170. let dt = e.dataTransfer;
  171. let files = dt.files;
  172.  
  173. if (files.length && files !== null) {
  174. handleFiles(files);
  175. }
  176. }
  177.  
  178. function handleFiles(files) {
  179. files = [...files];
  180. files.forEach(previewFile);
  181. }
  182.  
  183. function previewFile(file) {
  184. let reader = new FileReader();
  185. reader.readAsDataURL(file);
  186. reader.onloadend = function () {
  187. let gif = document.createElement('img');
  188. gif.src = reader.result;
  189. extractFrames(gif);
  190. };
  191. }
  192.  
  193. function highlight(e) {
  194. e.preventDefault();
  195. layerContainer.style.filter = "drop-shadow(0px 0px 6px green)";
  196. }
  197.  
  198. function unhighlight(e) {
  199. e.preventDefault();
  200. layerContainer.style.filter = "";
  201. }
  202.  
  203. function saveLayer(canv) {
  204. const activeLayer = document.querySelector("#activeLayer");
  205. const container = document.querySelector("#layerContainer");
  206. const img = document.createElement("img");
  207. img.src = canv.toDataURL();
  208. if (activeLayer) {
  209. insertAfter(img, activeLayer);
  210. setActiveLayer({ target: img });
  211. } else {
  212. container.append(img);
  213. }
  214. img.scrollIntoView();
  215. }
  216.  
  217. function insertAfter(newNode, referenceNode) {
  218. referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  219. }
  220.  
  221. function setActiveLayer(e) {
  222. const img = e.target;
  223. if (img.tagName !== "IMG") {
  224. resetActiveLayer();
  225. return;
  226. }
  227. resetActiveLayer();
  228. img.id = "activeLayer";
  229. if (!e.shiftKey) {
  230. ctx.drawImage(img, 0, 0);
  231. canvas.save();
  232. }
  233. const previousImg = img.previousSibling;
  234. const nextImg = img.nextSibling;
  235. if (previousImg) {
  236. onionLayers.previous.drawImage(previousImg, 0, 0);
  237. makeTransparent(onionLayers.previous, 30, 0);
  238. } else {
  239. onionLayers.previous.clearRect(0, 0, canvas.width, canvas.height);
  240. }
  241. if (nextImg) {
  242. onionLayers.next.drawImage(nextImg, 0, 0);
  243. makeTransparent(onionLayers.next, 0, 30);
  244. } else {
  245. onionLayers.next.clearRect(0, 0, canvas.width, canvas.height);
  246. }
  247. }
  248.  
  249. function resetActiveLayer() {
  250. const layer = document.querySelector("#activeLayer");
  251. if (!layer) return;
  252. layer.id = "";
  253. layer.style.border = "";
  254. }
  255.  
  256. function createLayer() {
  257. const canvasLayer = document.createElement('canvas');
  258. canvasLayer.classList.add("layer");
  259. canvasLayer.width = canvas.width;
  260. canvasLayer.height = canvas.height;
  261. onionContainer.appendChild(canvasLayer);
  262. return canvasLayer;
  263. }
  264.  
  265. function downloadGif(obj) {
  266. const name = "sketchful-gif-" + Date.now();
  267. let a = document.createElement("a");
  268. a.download = name + ".gif";
  269. a.href = obj.image;
  270. a.click();
  271. }
  272.  
  273. function addButton(text, clickFunction, element, type) {
  274. const button = document.createElement("div");
  275. button.setAttribute("class", `btn btn-sm btn-${type}`);
  276. button.textContent = text;
  277. button.onpointerup = clickFunction;
  278. element.append(button);
  279. return button;
  280. }
  281.  
  282. function clamp(num, min, max) {
  283. return num <= min ? min : num >= max ? max : num;
  284. }
  285.  
  286. function getInterval() {
  287. const input = document.querySelector("#gifIntervalInput");
  288. let fps = parseInt(input.value);
  289. if (isNaN(fps)) fps = 10;
  290. fps = clamp(fps, 1, 50);
  291. input.value = fps;
  292. return 1000 / fps;
  293. }
  294.  
  295. function removeLayer() {
  296. const activeLayer = document.querySelector('#activeLayer');
  297. const layerContainer = document.querySelector('#layerContainer');
  298. if (!activeLayer) return;
  299. const index = nodeIndex(activeLayer);
  300. activeLayer.remove();
  301. }
  302.  
  303. function nodeIndex(node) {
  304. return Array.prototype.indexOf.call(node.parentNode.children, node);
  305. }
  306.  
  307. function overwriteLayer() {
  308. const activeLayer = document.querySelector("#activeLayer");
  309. if (!activeLayer) return;
  310. activeLayer.src = canvas.toDataURL();
  311. }
  312.  
  313. let ahead = false;
  314. function toggleAhead() {
  315. ahead = !ahead;
  316. onionLayers.next.canvas.style.display = ahead ? "none" : "";
  317. this.classList.toggle("btn-danger");
  318. this.classList.toggle("btn-info");
  319. }
  320.  
  321. function addButtons() {
  322. const buttonContainer = document.createElement("div");
  323. buttonContainer.id = "buttonContainer";
  324. outerContainer.append(buttonContainer);
  325. addButton("Play", playAnimation, buttonContainer, "success");
  326. const downloadBtn = addButton("Download", saveGif, buttonContainer, "warning");
  327. addButton("Save Layer", addLayer, buttonContainer, "info");
  328. addButton("Delete", removeLayer, buttonContainer, "danger");
  329. addButton("Overwrite", overwriteLayer, buttonContainer, "warning");
  330. addButton("Onion", toggleOnion, buttonContainer, "success");
  331. addButton("Ahead", toggleAhead, buttonContainer, "info");
  332. const textDiv = document.createElement('div');
  333. const textInput = document.createElement("input");
  334. textDiv.classList.add('btn');
  335. textDiv.style.padding = '0px';
  336. textInput.placeholder = "FPS";
  337. textInput.id = "gifIntervalInput";
  338. setInputFilter(textInput, (v) => {return /^\d*\.?\d*$/.test(v);});
  339. textDiv.append(textInput);
  340.  
  341. buttonContainer.insertBefore(textDiv, downloadBtn);
  342. }
  343.  
  344. function addLayerContainer() {
  345. const game = document.querySelector("body > div.game");
  346. const container = document.createElement("div");
  347. outerContainer.style.display = "flex";
  348. outerContainer.style.flexDirection = "row";
  349. outerContainer.style.justifyContent = "center";
  350. container.addEventListener('wheel', (e) => {
  351. if (e.deltaY > 0) container.scrollLeft += 100;
  352. else container.scrollLeft -= 100;
  353. e.preventDefault();
  354. });
  355. container.addEventListener('pointerdown', (e) => {
  356. if (e.button !== 0) {
  357. resetActiveLayer();
  358. return;
  359. }
  360. setActiveLayer(e);
  361. }, true);
  362. container.addEventListener('contextmenu', (e) => {
  363. e.preventDefault();
  364. }, true);
  365. container.id = "layerContainer";
  366. new Sortable(container, {
  367. animation: 150,
  368. });
  369. outerContainer.append(container);
  370. game.append(outerContainer);
  371. return container;
  372. }
  373.  
  374. let onion = true;
  375. function toggleOnion(e) {
  376. onion = !onion;
  377. this.textContent = onion ? "Onion" : "Onioff";
  378. if (onion) {
  379. onionLayers.show();
  380. } else {
  381. onionLayers.hide();
  382. }
  383. this.classList.toggle("btn-success");
  384. this.classList.toggle("btn-danger");
  385. }
  386.  
  387. let animating = false;
  388. function playAnimation(e) {
  389. let preview = document.querySelector("#gifPreview");
  390. if (animating) {
  391. this.classList.toggle("btn-success");
  392. this.classList.toggle("btn-danger");
  393. this.textContent = "Play";
  394. while (preview) {
  395. preview.remove();
  396. preview = document.querySelector("#gifPreview");
  397. }
  398. animating = false;
  399. return;
  400. }
  401. const canvasCover = document.querySelector("#canvasCover");
  402. const layerContainer = document.querySelector("#layerContainer");
  403. const img = document.createElement('img');
  404. img.id = "gifPreview";
  405. img.draggable = false;
  406. canvasCover.parentElement.insertBefore(img, canvasCover);
  407. let frame = layerContainer.firstChild;
  408. if (!frame) return;
  409. const interval = getInterval();
  410. this.classList.toggle("btn-success");
  411. this.classList.toggle("btn-danger");
  412. this.textContent = "Stop";
  413. animating = true;
  414. (function playFrame() {
  415. if (!animating) return;
  416. img.src = frame.src;
  417. frame = frame.nextSibling || layerContainer.firstChild;
  418. setTimeout(playFrame, interval);
  419. })();
  420. }
  421.  
  422. function isFreeDraw() {
  423. return (
  424. document.querySelector("#canvas").style.display !== 'none' &&
  425. document.querySelector('#gameClock').style.display === 'none' &&
  426. document.querySelector('#gameSettings').style.display === 'none'
  427. );
  428. }
  429.  
  430. function setInputFilter(textbox, inputFilter) {
  431. ["input", "keydown", "keyup", "mousedown",
  432. "mouseup", "select", "contextmenu", "drop"].forEach(function(event) {
  433. textbox.addEventListener(event, function() {
  434. if (inputFilter(this.value)) {
  435. this.oldValue = this.value;
  436. this.oldSelectionStart = this.selectionStart;
  437. this.oldSelectionEnd = this.selectionEnd;
  438. } else if (this.hasOwnProperty("oldValue")) {
  439. this.value = this.oldValue;
  440. this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
  441. } else {
  442. this.value = "";
  443. }
  444. });
  445. });
  446. }
  447.  
  448. function makeTransparent(context, red, green) {
  449. const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
  450. const data = imgData.data;
  451.  
  452. for(let i = 0; i < data.length; i += 4) {
  453. const [r, g, b] = data.slice(i, i + 3);
  454. if (r >= 200 && g >= 200 && b >= 200) {
  455. data[i + 3] = 0;
  456. } else {
  457. data[i] += (data[i] + red) <= 255 ? red : 0;
  458. data[i + 1] += (data[i + 1] + green) <= 255 ? green : 0;
  459. data[i + 3] = 130;
  460. }
  461. }
  462. context.putImageData(imgData, 0, 0);
  463. }
  464.  
  465. canvas.save = () => {
  466. canvas.dispatchEvent(new MouseEvent('pointerup', {
  467. bubbles: true,
  468. clientX: 0,
  469. clientY: 0,
  470. button: 0
  471. }));
  472. }