Gradient Tool

Ctrl + Click and drag to draw a gradient, click a color to set it as the first and right click a color to set it as the second.

  1. // ==UserScript==
  2. // @name Gradient Tool
  3. // @namespace https://greasyfork.org/users/281093
  4. // @match https://sketchful.io/
  5. // @grant none
  6. // @version 1.0
  7. // @author Bell
  8. // @license MIT
  9. // @copyright 2020, Faux (https://greasyfork.org/users/281093)
  10. // @description Ctrl + Click and drag to draw a gradient, click a color to set it as the first and right click a color to set it as the second.
  11. // ==/UserScript==
  12. /* jshint esversion: 8 */
  13.  
  14. const canvas = document.querySelector('#canvas');
  15. const colorsDiv = document.querySelector('#gameToolsColors');
  16. const lineCanvas = document.createElement('canvas');
  17. const lineCtx = lineCanvas.getContext('2d');
  18. const sizeInput = document.querySelector("#gameToolsColorPreview");
  19. lineCanvas.style.position = 'absolute';
  20. lineCanvas.style.cursor = 'crosshair';
  21. lineCanvas.style.width = '100%';
  22. lineCanvas.style.display = 'none';
  23. lineCanvas.style.userSelect = 'none';
  24. lineCanvas.style.zIndex = '2';
  25. lineCanvas.style.filter = 'opacity(0.7)';
  26. lineCanvas.oncontextmenu = () => { return false; };
  27. [lineCanvas.width, lineCanvas.height] = [canvas.width, canvas.height];
  28. canvas.parentElement.insertBefore(lineCanvas, canvas);
  29.  
  30. lineCanvas.clear = () => {
  31. lineCtx.clearRect(0, 0, lineCanvas.width, lineCanvas.height);
  32. };
  33.  
  34. canvas.save = () => {
  35. canvas.dispatchEvent(createMouseEvent('pointerup', { x: 0, y: 0 }, true));
  36. };
  37.  
  38. let origin = {};
  39. let realOrigin = {};
  40. let previewPos = {};
  41. let realPos = {};
  42. let canvasHidden = true;
  43. let drawingLine = false;
  44. let activeColor = [0, 0, 0];
  45. let color1 = [0, 200, 0];
  46. let color2 = [250, 0, 0];
  47. let drawingGradient = false;
  48. let queueKeyUp = false;
  49.  
  50. document.addEventListener('keydown', (e) => {
  51. if (!isDrawing()) return;
  52. if (e.code === 'ControlLeft' && canvasHidden) {
  53. lineCanvas.style.display = '';
  54. disableScroll();
  55. canvasHidden = false;
  56. }
  57. });
  58.  
  59. document.addEventListener('keyup', (e) => {
  60. if (e.code === 'Digit1' && e.shiftKey) color1 = activeColor;
  61. else if (e.code === 'Digit2' && e.shiftKey) color2 = activeColor;
  62. if (e.code === 'ControlLeft' && !canvasHidden) {
  63. if (drawingGradient) {
  64. queueKeyUp = true;
  65. return;
  66. }
  67. onKeyUp();
  68. }
  69. });
  70.  
  71. function onKeyUp() {
  72. lineCanvas.style.display = 'none';
  73. canvasHidden = true;
  74. enableScroll();
  75. resetLineCanvas();
  76. document.removeEventListener('pointermove', savePos);
  77. document.removeEventListener('pointerup', pointerUpDraw);
  78. }
  79.  
  80. function savePos(e) {
  81. previewPos = getPos(e);
  82. realPos = getRealPos(e);
  83.  
  84. if (canvasHidden || !drawingLine) return;
  85. lineCanvas.clear();
  86. drawPreviewLine(previewPos);
  87. e.preventDefault();
  88. }
  89.  
  90. colorsDiv.addEventListener('pointerdown', (e) => {
  91. if (!e.target.classList.contains('gameToolsColor') || e.button === 2) return;
  92.  
  93. const colorStr = e.target.style.background;
  94. const match = colorStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)/);
  95. activeColor = match.slice(1, 4).map(str => parseInt(str));
  96. color1 = activeColor;
  97. });
  98.  
  99. colorsDiv.addEventListener('contextmenu', (e) => {
  100. if (!e.target.classList.contains('gameToolsColor')) return;
  101. e.preventDefault();
  102. const colorStr = e.target.style.background;
  103. const match = colorStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)/);
  104. color2 = match.slice(1, 4).map(str => parseInt(str));
  105. });
  106.  
  107. lineCanvas.addEventListener('pointerdown', (e) => {
  108. if (drawingGradient) return;
  109. origin = getPos(e);
  110. realOrigin = getRealPos(e);
  111. drawingLine = true;
  112. document.addEventListener('pointerup', pointerUpDraw);
  113. document.addEventListener('pointermove', savePos);
  114. });
  115.  
  116. function pointerUpDraw(e) {
  117. document.removeEventListener('pointermove', savePos);
  118. document.removeEventListener('pointerup', pointerUpDraw);
  119. drawGradient(realOrigin.x, realOrigin.y, realPos.x, realPos.y);
  120. previewPos = getPos(e);
  121. realPos = getRealPos(e);
  122. resetLineCanvas();
  123. }
  124.  
  125. async function drawGradient(x1, y1, x2, y2) {
  126. drawingGradient = true;
  127. const lab1 = rgb2lab(color1);
  128. const lab2 = rgb2lab(color2);
  129. const scale = canvas.getBoundingClientRect().width / lineCanvas.width;
  130. const offset = (parseInt(sizeInput.value) / 2) * scale;
  131. const max = Math.round((y2 - y1) / offset) * 1.6;
  132. for (let s = 0; s <= max; s += 1) {
  133. const t = mapRange(s, 0, max, 0, 1);
  134. const y = lerp(y1 + offset, y2 - offset, t);
  135. const color = lerpColors(lab1, lab2, t);
  136. changeColor(lab2rgb(color));
  137. drawLine(x1 + offset, y, x2 - offset, y);
  138. await delay(10);
  139. }
  140. canvas.save();
  141. drawingGradient = false;
  142. if (queueKeyUp) {
  143. onKeyUp();
  144. queueKeyUp = false;
  145. }
  146. }
  147.  
  148. function mapRange(value, istart, istop, ostart, ostop) {
  149. return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
  150. }
  151.  
  152. function delay(time) {
  153. return new Promise((resolve) => {
  154. setTimeout(() => resolve(time), time);
  155. });
  156. }
  157.  
  158. function changeColor(rgbArray) {
  159. const colorElement = document.querySelector('.gameToolsColor');
  160. const prevColor = colorElement.style.background;
  161. colorElement.style.background = `rgb(${rgbArray[0]}, ${rgbArray[1]}, ${rgbArray[2]})`;
  162. colorElement.dispatchEvent(new Event('pointerdown'));
  163. colorElement.style.background = prevColor;
  164. }
  165.  
  166. function resetLineCanvas() {
  167. drawingLine = false;
  168. lineCanvas.clear();
  169. }
  170.  
  171. function getPos(event) {
  172. const canvasRect = canvas.getBoundingClientRect();
  173. const canvasScale = canvas.width / canvasRect.width;
  174. return {
  175. x: (event.clientX - canvasRect.left) * canvasScale,
  176. y: (event.clientY - canvasRect.top) * canvasScale
  177. };
  178. }
  179.  
  180. function getRealPos(event) {
  181. return {
  182. x: event.clientX,
  183. y: event.clientY
  184. };
  185. }
  186.  
  187. function drawPreviewLine(pos) {
  188. const offset = parseInt(sizeInput.value) / 2;
  189. roundRect(
  190. lineCtx,
  191. origin.x,
  192. origin.y,
  193. pos.x - origin.x,
  194. pos.y - origin.y,
  195. offset,
  196. true,
  197. false,
  198. [
  199. color1,
  200. color2
  201. ]
  202. );
  203. }
  204.  
  205. function roundRect(ctx, x, y, width, height, radius, fill, stroke, gradientColors) {
  206. if (typeof stroke === 'undefined') {
  207. stroke = true;
  208. }
  209. if (typeof radius === 'undefined') {
  210. radius = 5;
  211. }
  212. if (typeof radius === 'number') {
  213. radius = {tl: radius, tr: radius, br: radius, bl: radius};
  214. } else {
  215. var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
  216. for (var side in defaultRadius) {
  217. radius[side] = radius[side] || defaultRadius[side];
  218. }
  219. }
  220. ctx.beginPath();
  221. ctx.moveTo(x + radius.tl, y);
  222. ctx.lineTo(x + width - radius.tr, y);
  223. ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
  224. ctx.lineTo(x + width, y + height - radius.br);
  225. ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
  226. ctx.lineTo(x + radius.bl, y + height);
  227. ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
  228. ctx.lineTo(x, y + radius.tl);
  229. ctx.quadraticCurveTo(x, y, x + radius.tl, y);
  230. ctx.closePath();
  231.  
  232. if (gradientColors) {
  233. const gradient = ctx.createLinearGradient(0, y, 0, height + y);
  234. const _color1 = `rgb(${gradientColors[0][0]}, ${gradientColors[0][1]}, ${gradientColors[0][2]}`;
  235. const _color2 = `rgb(${gradientColors[1][0]}, ${gradientColors[1][1]}, ${gradientColors[1][2]}`;
  236. gradient.addColorStop('0', _color1);
  237. gradient.addColorStop('1', _color2);
  238. ctx.strokeStyle = gradient;
  239. ctx.fillStyle = gradient;
  240. }
  241.  
  242. if (fill) {
  243. ctx.fill();
  244. }
  245. if (stroke) {
  246. ctx.lineWidth = 5;
  247. ctx.stroke();
  248. }
  249. }
  250.  
  251. function drawLine(x1, y1, x2, y2) {
  252. const coords = { x: x1, y: y1 };
  253. const newCoords = { x: x2, y: y2 };
  254.  
  255. canvas.dispatchEvent(createMouseEvent('pointerdown', coords));
  256. canvas.dispatchEvent(createMouseEvent('pointermove', newCoords));
  257. }
  258.  
  259. function createMouseEvent(name, pos, bubbles = false) {
  260. return new MouseEvent(name, {
  261. bubbles: bubbles,
  262. clientX: pos.x,
  263. clientY: pos.y,
  264. button: 0
  265. });
  266. }
  267.  
  268. function lerp(a, b, t) {
  269. return a + (b - a) * t;
  270. }
  271.  
  272. function lerpColors(lab1, lab2, t) {
  273. return [
  274. lab1[0] + (lab2[0] - lab1[0]) * t,
  275. lab1[1] + (lab2[1] - lab1[1]) * t,
  276. lab1[2] + (lab2[2] - lab1[2]) * t,
  277. ];
  278. }
  279.  
  280. function rgb2lab(rgb) {
  281. let r = rgb[0] / 255,
  282. g = rgb[1] / 255,
  283. b = rgb[2] / 255,
  284. x, y, z;
  285.  
  286. r = (r > 0.04045) ? ((r + 0.055) / 1.055) ** 2.4 : r / 12.92;
  287. g = (g > 0.04045) ? ((g + 0.055) / 1.055) ** 2.4 : g / 12.92;
  288. b = (b > 0.04045) ? ((b + 0.055) / 1.055) ** 2.4 : b / 12.92;
  289.  
  290. x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
  291. y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
  292. z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
  293.  
  294. x = (x > 0.008856) ? x ** (1 / 3) : (7.787 * x) + 16 / 116;
  295. y = (y > 0.008856) ? y ** (1 / 3) : (7.787 * y) + 16 / 116;
  296. z = (z > 0.008856) ? z ** (1 / 3) : (7.787 * z) + 16 / 116;
  297.  
  298. return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)];
  299. }
  300.  
  301. function lab2rgb(lab){
  302. let y = (lab[0] + 16) / 116,
  303. x = lab[1] / 500 + y,
  304. z = y - lab[2] / 200,
  305. r, g, b;
  306.  
  307. x = 0.95047 * ((x * x * x > 0.008856) ? x * x * x : (x - 16/116) / 7.787);
  308. y = 1.00000 * ((y * y * y > 0.008856) ? y * y * y : (y - 16/116) / 7.787);
  309. z = 1.08883 * ((z * z * z > 0.008856) ? z * z * z : (z - 16/116) / 7.787);
  310.  
  311. r = x * 3.2406 + y * -1.5372 + z * -0.4986;
  312. g = x * -0.9689 + y * 1.8758 + z * 0.0415;
  313. b = x * 0.0557 + y * -0.2040 + z * 1.0570;
  314.  
  315. r = (r > 0.0031308) ? (1.055 * Math.pow(r, 1/2.4) - 0.055) : 12.92 * r;
  316. g = (g > 0.0031308) ? (1.055 * Math.pow(g, 1/2.4) - 0.055) : 12.92 * g;
  317. b = (b > 0.0031308) ? (1.055 * Math.pow(b, 1/2.4) - 0.055) : 12.92 * b;
  318.  
  319. return [
  320. Math.max(0, Math.min(1, r)) * 255,
  321. Math.max(0, Math.min(1, g)) * 255,
  322. Math.max(0, Math.min(1, b)) * 255
  323. ];
  324. }
  325.  
  326. const keys = { 32: 1, 37: 1, 38: 1, 39: 1, 40: 1 };
  327.  
  328. function preventDefault(e) {
  329. e.preventDefault();
  330. }
  331.  
  332. function preventDefaultForScrollKeys(e) {
  333. if (keys[e.keyCode]) {
  334. preventDefault(e);
  335. return false;
  336. }
  337. }
  338.  
  339. function isDrawing() {
  340. return document.querySelector('#gameTools').style.display !== 'none' &&
  341. document.querySelector('body > div.game').style.display !== 'none' &&
  342. document.activeElement.tagName !== 'INPUT';
  343. }
  344.  
  345. let supportsPassive = false;
  346. try {
  347. window.addEventListener('test', null, Object.defineProperty({}, 'passive', {
  348. get: function() {
  349. supportsPassive = true;
  350. return true;
  351. }
  352. }));
  353. }
  354. catch(e) {
  355. console.log(e);
  356. }
  357.  
  358. const wheelOpt = supportsPassive ? { passive: false } : false;
  359. const wheelEvent = 'onwheel' in document.createElement('div') ? 'wheel' : 'mousewheel';
  360.  
  361. function disableScroll() {
  362. window.addEventListener('DOMMouseScroll', preventDefault, false);
  363. window.addEventListener(wheelEvent, preventDefault, wheelOpt);
  364. window.addEventListener('touchmove', preventDefault, wheelOpt);
  365. window.addEventListener('keydown', preventDefaultForScrollKeys, false);
  366. }
  367.  
  368. function enableScroll() {
  369. window.removeEventListener('DOMMouseScroll', preventDefault, false);
  370. window.removeEventListener(wheelEvent, preventDefault, wheelOpt);
  371. window.removeEventListener('touchmove', preventDefault, wheelOpt);
  372. window.removeEventListener('keydown', preventDefaultForScrollKeys, false);
  373. }