Animation

Animation tools for Sketchful.io

目前为 2020-08-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Animation
  3. // @namespace https://greasyfork.org/users/281093
  4. // @match https://sketchful.io/
  5. // @grant none
  6. // @version 0.6
  7. // @author Bell
  8. // @license MIT
  9. // @copyright 2020, Bell (https://openuserjs.org/users/Bell)
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/gifshot/0.3.2/gifshot.min.js
  11. // @require https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js
  12. // @description Animation tools for Sketchful.io
  13. // ==/UserScript==
  14. /* jshint esversion: 6 */
  15.  
  16. const containerStyle =
  17. `white-space: nowrap;
  18. overflow: auto;
  19. justify-content:center;
  20. margin-top: 10px;
  21. max-width: 76%;
  22. height: 124px;
  23. background: rgb(0 0 0 / 30%);
  24. padding: 12px;
  25. overflow-y: hidden;
  26. border-radius: 10px;
  27. margin-bottom: 5px;
  28. margin-left: 5vw;
  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. filter: opacity(0.5);`;
  38.  
  39. const styleRules = [
  40. '#layerContainer::-webkit-scrollbar { width: 5px; height: 5px; overflow: hidden}',
  41. '#layerContainer::-webkit-scrollbar-track { background: none }',
  42. '#layerContainer::-webkit-scrollbar-thumb { background: #F5BC09; border-radius: 5px }',
  43. `#layerContainer { ${containerStyle} }`,
  44. `.layer { ${canvasLayerStyle} }`,
  45. '#layerContainer img { width: 133px; cursor: pointer; margin-right: 5px }',
  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. ];
  52.  
  53. const sheet = window.document.styleSheets[window.document.styleSheets.length - 1];
  54. const outerContainer = document.createElement('div');
  55. const onionContainer = document.createElement('div');
  56. const canvasContainer = document.querySelector('#gameCanvas');
  57. const canvasInner = document.querySelector("#gameCanvasInner");
  58. const canvas = document.querySelector('#canvas');
  59. const ctx = canvas.getContext('2d');
  60.  
  61. (() => {
  62. canvas.parentElement.insertBefore(onionContainer, canvas);
  63. addLayerContainer();
  64. addButtons();
  65. styleRules.forEach((rule) => sheet.insertRule(rule));
  66. const gameModeObserver = new MutationObserver(checkRoomType);
  67. gameModeObserver.observe(document.querySelector('.game'),
  68. { attributes: true });
  69. gameModeObserver.observe(canvas, { attributes: true });
  70. })();
  71.  
  72. function checkRoomType() {
  73. outerContainer.style.display = isFreeDraw() ? 'flex' : 'none';
  74. onionContainer.style.display = isFreeDraw() ? "" : "none";
  75. }
  76.  
  77. function addLayer() {
  78. const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  79. saveLayer(imgData);
  80. const canvasLayer = createLayer();
  81. const layerCtx = canvasLayer.getContext('2d');
  82. layerCtx.putImageData(imgData, 0, 0);
  83. makeTransparent(layerCtx);
  84. const previousLayer = canvasInner.querySelector("#canvasLayer");
  85. if (previousLayer) previousLayer.remove();
  86. onionContainer.appendChild(canvasLayer);
  87. }
  88.  
  89. function saveGif() {
  90. const container = document.querySelector("#layerContainer");
  91. if (!container.childElementCount) return;
  92. const layers = Array.from(container.children).map(image => image.src);
  93. const interval = getInterval();
  94. gifshot.createGIF({
  95. gifWidth: canvas.width,
  96. gifHeight: canvas.height,
  97. interval: interval / 1000,
  98. images: layers
  99. }, downloadGif);
  100. }
  101.  
  102. function saveLayer(data) {
  103. const activeLayer = document.querySelector("#activeLayer");
  104. const container = document.querySelector("#layerContainer");
  105. const img = document.createElement("img");
  106. img.src = canvas.toDataURL();
  107. if (activeLayer) {
  108. insertAfter(img, activeLayer);
  109. setActiveLayer({ target: img });
  110. } else {
  111. container.append(img);
  112. }
  113. }
  114.  
  115. function insertAfter(newNode, referenceNode) {
  116. referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  117. }
  118.  
  119. function setActiveLayer(e) {
  120. const img = e.target;
  121. if (img.tagName !== "IMG") {
  122. resetActiveLayer();
  123. return;
  124. };
  125. resetActiveLayer();
  126. const canvasLayerCtx = document.querySelector("#canvasLayer").getContext("2d");
  127. const previousImg = img.previousSibling;
  128. if (previousImg) {
  129. canvasLayerCtx.drawImage(previousImg, 0, 0);
  130. makeTransparent(canvasLayerCtx);
  131. } else {
  132. canvasLayerCtx.clearRect(0, 0, canvas.width, canvas.height);
  133. }
  134. img.id = "activeLayer";
  135. ctx.drawImage(img, 0, 0);
  136. }
  137.  
  138. function resetActiveLayer() {
  139. const layer = document.querySelector("#activeLayer");
  140. if (!layer) return;
  141. layer.id = "";
  142. layer.style.border = "";
  143. }
  144.  
  145. function createLayer() {
  146. const canvasLayer = document.createElement('canvas');
  147. canvasLayer.classList.add("layer");
  148. canvasLayer.width = canvas.width;
  149. canvasLayer.height = canvas.height;
  150. canvasLayer.id = "canvasLayer";
  151. return canvasLayer;
  152. }
  153.  
  154. function downloadGif(obj) {
  155. const name = "sketchful-gif-" + Date.now();
  156. let a = document.createElement("a");
  157. a.download = name + ".gif";
  158. a.href = obj.image;
  159. a.click();
  160. }
  161.  
  162. function addButton(text, clickFunction, element, type) {
  163. const button = document.createElement("div");
  164. button.setAttribute("class", `btn btn-sm btn-${type}`);
  165. button.textContent = text;
  166. button.onpointerup = clickFunction;
  167. element.append(button);
  168. return button;
  169. }
  170.  
  171. function clamp(num, min, max) {
  172. return num <= min ? min : num >= max ? max : num;
  173. }
  174.  
  175. function getInterval() {
  176. const input = document.querySelector("#gifIntervalInput");
  177. let interval = parseInt(input.value);
  178. if (isNaN(interval)) interval = 100;
  179. interval = clamp(interval, 20, 10000);
  180. input.value = interval;
  181. return interval;
  182. }
  183.  
  184. function removeLayer() {
  185. const activeLayer = document.querySelector('#activeLayer');
  186. const layerContainer = document.querySelector('#layerContainer');
  187. if (!activeLayer) return;
  188. const index = nodeIndex(activeLayer);
  189. activeLayer.remove();
  190. }
  191.  
  192. function nodeIndex(node) {
  193. return Array.prototype.indexOf.call(node.parentNode.children, node);
  194. }
  195.  
  196. function addButtons() {
  197. const buttonContainer = document.createElement("div");
  198. buttonContainer.id = "buttonContainer";
  199. outerContainer.append(buttonContainer);
  200. addButton("Save Gif", saveGif, buttonContainer, "warning");
  201. addButton("NOnion", toggleOnion, buttonContainer, "warning");
  202. addButton("Save Layer", addLayer, buttonContainer, "info");
  203. addButton("Delete Layer", removeLayer, buttonContainer, "danger");
  204. addButton("Play", playAnimation, buttonContainer, "success");
  205. const textDiv = document.createElement('div');
  206. const textInput = document.createElement("input");
  207. textDiv.classList.add('btn');
  208. textDiv.style.padding = '0px';
  209. textInput.placeholder = "Interval (ms)";
  210. textInput.style.width = "100px";
  211. textInput.id = "gifIntervalInput";
  212. setInputFilter(textInput, (v) => {return /^\d*\.?\d*$/.test(v);});
  213. textDiv.append(textInput);
  214. buttonContainer.append(textDiv);
  215. }
  216.  
  217. function addLayerContainer() {
  218. const game = document.querySelector("body > div.game");
  219. const container = document.createElement("div");
  220. outerContainer.style.display = "flex";
  221. outerContainer.style.flexDirection = "row";
  222. container.addEventListener('wheel', (e) => {
  223. if (e.deltaY > 0) container.scrollLeft += 100;
  224. else container.scrollLeft -= 100;
  225. e.preventDefault();
  226. });
  227. container.addEventListener('pointerdown', setActiveLayer, true);
  228. container.id = "layerContainer";
  229. new Sortable(container, {
  230. animation: 150,
  231. });
  232. outerContainer.append(container);
  233. game.append(outerContainer);
  234. }
  235.  
  236. let onion = true;
  237. function toggleOnion(e) {
  238. onion = !onion;
  239. this.textContent = onion ? "NOnion" : "Onion";
  240. onionContainer.classList.toggle("hidden");
  241. }
  242.  
  243. let animating = false;
  244. function playAnimation(e) {
  245. const preview = document.querySelector("#gifPreview");
  246. if (animating) {
  247. this.classList.toggle("btn-success");
  248. this.classList.toggle("btn-danger");
  249. this.textContent = "Play";
  250. if (preview) preview.remove();
  251. animating = false;
  252. return;
  253. }
  254. const canvasCover = document.querySelector("#canvasCover");
  255. const layerContainer = document.querySelector("#layerContainer");
  256. const img = document.createElement('img');
  257. img.id = "gifPreview";
  258. img.draggable = false;
  259. canvasCover.parentElement.insertBefore(img, canvasCover);
  260. let frame = layerContainer.firstChild;
  261. if (!frame) return;
  262. const interval = getInterval();
  263. this.classList.toggle("btn-success");
  264. this.classList.toggle("btn-danger");
  265. this.textContent = "Stop";
  266. animating = true;
  267. (function playFrame() {
  268. if (!animating) return;
  269. img.src = frame.src;
  270. frame = frame.nextSibling || layerContainer.firstChild;
  271. setTimeout(playFrame, interval);
  272. })();
  273. }
  274.  
  275. function isFreeDraw() {
  276. return (
  277. document.querySelector("#canvas").style.display !== 'none' &&
  278. document.querySelector('#gameClock').style.display === 'none' &&
  279. document.querySelector('#gameSettings').style.display === 'none'
  280. );
  281. }
  282.  
  283. function setInputFilter(textbox, inputFilter) {
  284. ["input", "keydown", "keyup", "mousedown",
  285. "mouseup", "select", "contextmenu", "drop"].forEach(function(event) {
  286. textbox.addEventListener(event, function() {
  287. if (inputFilter(this.value)) {
  288. this.oldValue = this.value;
  289. this.oldSelectionStart = this.selectionStart;
  290. this.oldSelectionEnd = this.selectionEnd;
  291. } else if (this.hasOwnProperty("oldValue")) {
  292. this.value = this.oldValue;
  293. this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
  294. } else {
  295. this.value = "";
  296. }
  297. });
  298. });
  299. }
  300.  
  301. function makeTransparent(context) {
  302. const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
  303. const data = imgData.data;
  304.  
  305. for(let i = 0; i < data.length; i += 4) {
  306. const [r, g, b] = data.slice(i, i + 3);
  307. if (r >= 230 && g >= 230 && b >= 230) {
  308. data[i + 3] = 0;
  309. }
  310. }
  311. context.putImageData(imgData, 0, 0);
  312. }