Animation

Animation tools for Sketchful.io

目前为 2020-08-04 提交的版本。查看 最新版本

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