Color Picker

Color picker for Sketchful

  1. // ==UserScript==
  2. // @name Color Picker
  3. // @namespace https://greasyfork.org/users/281093
  4. // @match https://sketchful.io/*
  5. // @grant none
  6. // @version 0.7.5
  7. // @author Bell
  8. // @license MIT
  9. // @copyright 2020, Bell
  10. // @description Color picker for Sketchful
  11. // @run-at document-end
  12. // ==/UserScript==
  13. // jshint esversion: 6
  14.  
  15. const defaultPalettes = [
  16. [
  17. '#ffffff', '#d3d1d2', '#f70f0f', '#ff7200', '#fce700', '#02cb00', '#01fe94', '#05b0ff', '#221ecd', '#a300bd', '#cc7fad', '#fdad88', '#9e5425',
  18. '#514f54', '#a9a7a8', '#ae0b00', '#c84706', '#ec9e06', '#007612', '#049d6f', '#00579d', '#0f0b96', '#6e0083', '#a65673', '#e38a5e', '#5e320d',
  19. '#000000', '#827c80', '#57060c', '#8b2500', '#9e6600', '#003f00', '#00766a', '#003b75', '#0e0151', '#3c0350', '#73314d', '#d1754e', '#421e06'
  20. ],
  21.  
  22. [
  23. '#3a3a3c', '#8e8e93', '#f8f9fa', '#ffadad', '#ffd6a5', '#fdffb6', '#caffbf', '#9bf6ff', '#a0c4ff', '#bdb2ff', '#ffc6ff', '#fdad88', '#9e5425',
  24. '#2c2c2e', '#636366', '#e0e0e0', '#ff7070', '#f3a220', '#f9e079', '#049d6f', '#92ddea', '#6dafe0', '#ab87ff', '#ff87ab', '#e38a5e', '#5e320d',
  25. '#1c1c1e', '#48484a', '#c2c2c2', '#f54d4d', '#dc8700', '#f0c808', '#00766a', '#219bc3', '#548bbc', '#715aff', '#ff5d8f', '#d1754e', '#421e06'
  26. ],
  27.  
  28. [
  29. '#081c15', '#1b4332', '#2d6a4f', '#40916c', '#52b788', '#74c69d', '#95d5b2', '#b7e4c7', '#d8f3dc', '#000000', '#faf9f9', '#ffd6ba', '#fec89a',
  30. '#774936', '#8a5a44', '#9d6b53', '#b07d62', '#c38e70', '#cd9777', '#d69f7e', '#deab90', '#e6b8a2', '#edc4b3', '#ffb5a7', '#fcd5ce', '#f8edeb',
  31. '#cb997e', '#eddcd2', '#fff1e6', '#f0efeb', '#ddbea9', '#a5a58d', '#b7b7a4', '#6d6875', '#b5838d', '#e5989b', '#ffb4a2', '#ffcdb2', '#f9dcc4'
  32. ],
  33.  
  34. [
  35. '#10002b', '#240046', '#3c096c', '#5a189a', '#7b2cbf', '#9d4edd', '#c77dff', '#e0aaff', '#efcefa', '#d4b2d8', '#a88fac', '#826c7f', '#5d4e60',
  36. '#7c6f93', '#886f93', '#a967ad', '#ad6789', '#db81ad', '#ff6c91', '#ff736c', '#ff9e46', '#faa275', '#ff8c61', '#ce6a85', '#985277', '#5c374c',
  37. '#721b65', '#b80d57', '#f8615a', '#ffd868', '#bb596b', '#f96d80', '#ff9a76', '#ffc4a3', '#00e0ff', '#74f9ff', '#a6fff2', '#e8ffe8', '#ffffff'
  38. ],
  39.  
  40. [
  41. '#007f5f', '#2b9348', '#55a630', '#80b918', '#aacc00', '#bfd200', '#d4d700', '#dddf00', '#eeef20', '#ffff3f', '#03045e', '#0077b6', '#00b4d8',
  42. '#ff4800', '#ff5400', '#ff6000', '#ff6d00', '#ff7900', '#ff8500', '#ff9100', '#ff9e00', '#ffaa00', '#ffb600', '#90e0ef', '#caf0f8', '#000000',
  43. '#143642', '#263c41', '#38413f', '#4a473e', '#5c4d3c', '#6f523b', '#815839', '#935e38', '#a56336', '#b76935', '#000000', '#ffffff', '#ffffff'
  44. ]
  45. ];
  46. const palettes = JSON.parse(localStorage.getItem('palettes')) || defaultPalettes;
  47. let paletteIndex = parseInt(localStorage.getItem('paletteIndex')) || 0;
  48. let lockedPalettes = JSON.parse(localStorage.getItem('lockedPalettes')) || [0];
  49.  
  50. let activeColor = {
  51. node: null,
  52. index: null
  53. };
  54.  
  55. const canvas = document.querySelector('#canvas');
  56. const ctx = canvas.getContext('2d');
  57. const gameTools = document.querySelector('#gameTools');
  58. const chatBox = document.querySelector('#gameChat');
  59. const colorButtons = document.querySelectorAll('.gameToolsColor');
  60. const colorsDiv = document.querySelector('#gameToolsColors');
  61. const colorButton = document.querySelector('#gameToolsColors > div:nth-child(1) > div:nth-child(1)');
  62.  
  63. const colorPickerWrapper = document.createElement('div');
  64. const colorInput = document.createElement('input');
  65. const colorPicker = document.createElement('input');
  66. const inputStyle = 'margin: 5px 0; height: 20px; width: 35%; text-align: center; border: none;font-weight: 800; border-radius: 5px; background-color: #CBCBCB;';
  67. const wrapperStyle = 'position: absolute; margin: 5px 35%; height: 20px; width: 37px; border-radius: 5px;';
  68.  
  69. (function init() {
  70. addPicker();
  71. updatePageStyle();
  72. addObservers();
  73. addListeners();
  74. changePalette();
  75. })();
  76.  
  77. function addPicker() {
  78. colorPicker.type = 'color';
  79. colorPicker.setAttribute('style', 'opacity: 0; width: 37px; cursor: pointer;');
  80. colorPicker.oninput = updatePicker;
  81. colorPicker.setAttribute('data-toggle', 'tooltip');
  82. colorPicker.setAttribute('data-original-title', 'Color Picker');
  83. colorPicker.setAttribute('data-placement', 'bottom');
  84. colorPicker.title = 'Color Picker';
  85.  
  86. colorPickerWrapper.setAttribute('style', wrapperStyle);
  87. colorPickerWrapper.style.backgroundColor = colorPicker.value;
  88. colorPickerWrapper.appendChild(colorPicker);
  89. gameTools.appendChild(colorPickerWrapper);
  90.  
  91. colorInput.oninput = updateInput;
  92. colorInput.onclick = selectInputText;
  93. colorInput.setAttribute('style', inputStyle);
  94. colorInput.setAttribute('spellcheck', 'false');
  95. colorInput.setAttribute('maxlength', '7');
  96. colorInput.value = colorPicker.value;
  97. gameTools.appendChild(colorInput);
  98. addButtons();
  99. }
  100.  
  101. const setColorDebounced = debounce(setColor, 5);
  102.  
  103. function debounce(func, delay) {
  104. let inDebounce;
  105. return function() {
  106. const context = this;
  107. const args = arguments;
  108. clearTimeout(inDebounce);
  109. inDebounce = setTimeout(() => func.apply(context, args), delay);
  110. };
  111. }
  112.  
  113. function addObservers() {
  114. const heightObserver = new MutationObserver(adjustChatSize);
  115. const config = {
  116. attributes: true
  117. };
  118. heightObserver.observe(gameTools, config);
  119. heightObserver.observe(chatBox, config);
  120. }
  121.  
  122. let pickingColor = false;
  123. function pickerIconOn(e) {
  124. if (e.code !== 'AltLeft') return;
  125. canvas.style.cursor = 'crosshair';
  126. pickingColor = true;
  127. e.preventDefault();
  128. }
  129.  
  130. function pickerIconOff(e) {
  131. if (e.code !== 'AltLeft' || !pickingColor) return;
  132. pickingColor = false;
  133. regenerateCursor();
  134. e.preventDefault();
  135. }
  136.  
  137. function regenerateCursor() {
  138. const selectedTool = document.querySelector('.gameToolsSelected');
  139. selectedTool.id === 'gameToolsDraw' ? selectedTool.nextSibling.click() :
  140. selectedTool.previousSibling.click();
  141. selectedTool.click();
  142. }
  143.  
  144. function addListeners() {
  145. canvas.addEventListener('pointerdown', pickCanvasColor, false);
  146. document.addEventListener('keydown', pickerIconOn, true);
  147. document.addEventListener('keyup', pickerIconOff, true);
  148.  
  149. const saveBtn = document.querySelector('#savePalette');
  150. saveBtn.addEventListener('dragenter', highlight, false);
  151. saveBtn.addEventListener('dragleave', unhighlight, false);
  152. saveBtn.addEventListener('drop', handleDrop, false);
  153. saveBtn.addEventListener('dragover', e => {
  154. e.preventDefault();
  155. }, false);
  156.  
  157. document.addEventListener('keydown', e => {
  158. if (e.altKey && e.shiftKey && !isPaletteLocked(paletteIndex)) {
  159. colorsDiv.style.boxShadow = '0 0 0 2px red';
  160. }
  161. }, false);
  162. document.addEventListener('keyup', e => {
  163. if (e.altKey || e.shiftKey) {
  164. colorsDiv.style.boxShadow = '';
  165. }
  166. }, false);
  167.  
  168. document.addEventListener('paste', e => {
  169. if (document.activeElement.tagName === 'INPUT') return;
  170. const paste = (e.clipboardData || window.clipboardData).getData('text');
  171. const coolorRegex = /coolors\.co\/([a-f0-9-]+)/;
  172. const match = coolorRegex.exec(paste);
  173. if (match) {addHexFromString(match[1]);}
  174. }, false);
  175.  
  176. colorsDiv.addEventListener('pointerenter', () => {
  177. colorsDiv.addEventListener('pointerdown', editColor, true);
  178. });
  179.  
  180. colorsDiv.addEventListener('pointerleave', () => {
  181. colorsDiv.removeEventListener('pointerdown', editColor, true);
  182. });
  183.  
  184. document.addEventListener('keydown', e=> {
  185. if (!e.ctrlKey || e.code !== 'KeyZ') return;
  186. e.preventDefault();
  187. });
  188. }
  189.  
  190. function updatePageStyle() {
  191. document.querySelector('#gameToolsSlider').style.top = '77px';
  192. gameTools.style.height = '200px';
  193. }
  194.  
  195. function toggleLock() {
  196. const lockBtn = document.querySelector('#lockButton');
  197. if (lockBtn.getAttribute('state') === 'unlocked') {
  198. lockPalette(lockBtn);
  199. }
  200. else {
  201. unlockPalette(lockBtn);
  202. }
  203. updateLock();
  204. }
  205.  
  206. function lockPalette() {
  207. lockedPalettes.push(paletteIndex);
  208. localStorage.setItem('lockedPalettes', JSON.stringify(lockedPalettes));
  209. }
  210.  
  211. function unlockPalette() {
  212. const index = lockedPalettes.indexOf(paletteIndex);
  213. if (index < 0) return;
  214. lockedPalettes.splice(index, 1);
  215. localStorage.setItem('lockedPalettes', JSON.stringify(lockedPalettes));
  216. }
  217.  
  218. function updateLock() {
  219. const lockBtn = document.querySelector('#lockButton');
  220. if (isPaletteLocked(paletteIndex)) {
  221. lockBtn.classList.remove('fa-unlock-alt');
  222. lockBtn.classList.add('fa-lock');
  223. lockBtn.setAttribute('state', 'locked');
  224. colorsDiv.style.boxShadow = '';
  225. }
  226. else {
  227. lockBtn.classList.add('fa-unlock-alt');
  228. lockBtn.classList.remove('fa-lock');
  229. lockBtn.setAttribute('state', 'unlocked');
  230. }
  231. resetActiveColor();
  232. }
  233.  
  234. function addButtons() {
  235. const prevPaletteBtn = document.createElement('button');
  236. const saveColorBtn = document.createElement('button');
  237. const nextPaletteBtn = document.createElement('button');
  238. const lockBtn = document.createElement('button');
  239.  
  240. const saveTooltip = 'Save Color<br>Hold <strong>shift</strong> to save the current palette.';
  241. const lockTooltip = 'Lock Current Palette';
  242.  
  243. addButton(prevPaletteBtn, 'arrow-left', '5px 5px 5px 45px;');
  244. addButton(saveColorBtn, 'save', '5px 5px 5px 75px;', saveTooltip, 'savePalette');
  245. addButton(nextPaletteBtn, 'arrow-right', '5px 5px 5px 105px;');
  246. addButton(lockBtn, 'unlock-alt', '5px 5px 5px 135px;', lockTooltip, 'lockButton');
  247. lockBtn.setAttribute('state', 'unlocked');
  248.  
  249. prevPaletteBtn.addEventListener('click', prevPalette, false);
  250. saveColorBtn.addEventListener('click', saveColor, false);
  251. nextPaletteBtn.addEventListener('click', nextPalette, false);
  252. lockBtn.addEventListener('click', toggleLock, false);
  253. }
  254.  
  255. function nextPalette() {
  256. paletteIndex = paletteIndex < (palettes.length - 1) ? paletteIndex + 1 : 0;
  257. localStorage.setItem('paletteIndex', paletteIndex);
  258. changePalette();
  259. }
  260.  
  261. function prevPalette() {
  262. paletteIndex = paletteIndex > 0 ? paletteIndex - 1 : palettes.length - 1;
  263. localStorage.setItem('paletteIndex', paletteIndex);
  264. changePalette();
  265. }
  266.  
  267. function saveColor(e) {
  268. if (e.shiftKey) {
  269. downloadPalettes();
  270. return;
  271. }
  272. const currentPalette = palettes[paletteIndex];
  273. if (activeColor.index) {
  274. currentPalette[activeColor.index] = colorPicker.value;
  275. }
  276. else {
  277. addColor(colorPicker.value);
  278. }
  279. changePalette();
  280. savePalettes();
  281. }
  282.  
  283. function addColor(color) {
  284. if (palettes[paletteIndex].length > 38 || isPaletteLocked(paletteIndex)) {
  285. palettes.push([]);
  286. paletteIndex = palettes.length - 1;
  287. }
  288. palettes[paletteIndex].push(color);
  289. }
  290.  
  291. function rgbToHex(rgb) {
  292. const regEx = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/;
  293. const [, r, g, b] = regEx.exec(rgb);
  294.  
  295. function hex(x) {
  296. return ('0' + parseInt(x).toString(16)).slice(-2);
  297. }
  298.  
  299. return `#${hex(r)}${hex(g)}${hex(b)}`;
  300. }
  301.  
  302. function savePalettes() {
  303. localStorage.setItem('palettes', JSON.stringify(palettes));
  304. }
  305.  
  306. function downloadPalettes() {
  307. const formattedPaletteData = JSON.stringify(palettes[paletteIndex]).replace(/\],/g, '],\n\n');
  308. download('palette.txt', formattedPaletteData);
  309. }
  310.  
  311. function download(filename, text) {
  312. const pom = document.createElement('a');
  313. pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
  314. pom.setAttribute('download', filename);
  315.  
  316. if (document.createEvent) {
  317. const event = document.createEvent('MouseEvents');
  318. event.initEvent('click', true, true);
  319. pom.dispatchEvent(event);
  320. }
  321. else {
  322. pom.click();
  323. }
  324. }
  325.  
  326. function isPaletteLocked(index) {
  327. return lockedPalettes.includes(index);
  328. }
  329.  
  330. function updateColorInputs(colorValue) {
  331. colorPickerWrapper.style.backgroundColor = colorValue;
  332. colorPicker.value = colorValue;
  333. colorInput.value = colorValue;
  334. }
  335.  
  336. function editColor(event) {
  337. if (!event.target.classList.contains('gameToolsColor')) return;
  338. if (!event.shiftKey) updateColorInputs(rgbToHex(event.target.style.backgroundColor));
  339.  
  340. if (isPaletteLocked(paletteIndex)) return;
  341. const color = {
  342. node: event.target,
  343. index: Array.prototype.indexOf.call(colorButtons, event.target)
  344. };
  345.  
  346. if (event.altKey && event.shiftKey) {
  347. deletePalette(paletteIndex);
  348. }
  349. else if (event.altKey && color.index >= 0) {
  350. const palette = palettes[paletteIndex];
  351. palette.splice(color.index, 1);
  352. changePalette();
  353. if (isPaletteEmpty(palette)) deletePalette(paletteIndex);
  354. savePalettes();
  355. }
  356. else if (event.shiftKey && color.index >= 0) {
  357. setActiveColor(color);
  358. }
  359. }
  360.  
  361. function deletePalette(index) {
  362. if (palettes.length < 2) return;
  363. palettes.splice(index, 1);
  364. lockedPalettes = lockedPalettes.map(lockedIndex => {
  365. return lockedIndex > index ? lockedIndex - 1 : lockedIndex;
  366. });
  367. localStorage.setItem('lockedPalettes', JSON.stringify(lockedPalettes));
  368. prevPalette();
  369. savePalettes();
  370. updateLock();
  371. }
  372.  
  373. function changePalette() {
  374. if (paletteIndex < 0 || paletteIndex >= palettes.length) {
  375. paletteIndex = 0;
  376. localStorage.setItem('paletteIndex', paletteIndex);
  377. }
  378. colorButtons.forEach((button, idx) => {
  379. button.style.backgroundColor = palettes[paletteIndex][idx] || '#fff';
  380. });
  381. updateLock();
  382. }
  383.  
  384. function isPaletteEmpty(palette) {
  385. if (!palette) return true;
  386. let empty = true;
  387. for (const color of palette) {
  388. if (color) {
  389. empty = false;
  390. break;
  391. }
  392. }
  393. return empty;
  394. }
  395.  
  396. function setActiveColor(color) {
  397. resetActiveColor();
  398. activeColor = color;
  399. activeColor.node.style.border = 'solid 2px red';
  400. }
  401.  
  402. function resetActiveColor() {
  403. if (activeColor.node) {
  404. activeColor.node.style.border = '';
  405. activeColor = {
  406. node: null,
  407. index: null
  408. };
  409. }
  410. }
  411.  
  412. function addButton(button, icon, pos, tooltip = '', id = '') {
  413. const buttonStyle = `margin: ${pos}; position: absolute; height: 20px; border: none; background-color: #CBCBCB; border-radius: 5px;`;
  414. button.setAttribute('style', buttonStyle);
  415. button.setAttribute('class', `fas fa-${icon}`);
  416. tooltip && button.setAttribute('data-toggle', 'tooltip');
  417. tooltip && button.setAttribute('data-original-title', tooltip);
  418. button.setAttribute('data-placement', 'bottom');
  419. button.title = tooltip;
  420. button.id = id;
  421. gameTools.appendChild(button);
  422. }
  423.  
  424. function updatePicker(event) {
  425. const color = event.target.value;
  426. colorPickerWrapper.style.backgroundColor = color;
  427. colorInput.value = color;
  428. setColorDebounced(color);
  429. }
  430.  
  431. function updateInput(event) {
  432. const hexFound = /([0-9A-Fa-f]{3}){1,2}/.exec(event.target.value);
  433. if (!hexFound) return;
  434. const color = '#' + hexFound[0];
  435. colorPickerWrapper.style.backgroundColor = color;
  436. colorPicker.value = color;
  437. setColorDebounced(color);
  438. }
  439.  
  440. const pointerdownEvent = new Event('pointerdown');
  441.  
  442. function setColor(color) {
  443. const prevColor = colorButton.style.backgroundColor;
  444. colorButton.style.backgroundColor = color;
  445. colorButton.dispatchEvent(pointerdownEvent);
  446. colorButton.style.backgroundColor = prevColor;
  447. }
  448.  
  449. function selectInputText() {
  450. colorInput.select();
  451. }
  452.  
  453. function pickCanvasColor(event) {
  454. if (!event.altKey) return;
  455. event.preventDefault();
  456. event.stopImmediatePropagation();
  457. const pos = getPos(event);
  458. const [r, g, b] = ctx.getImageData(pos.x, pos.y, 1, 1).data;
  459. const color = `rgb(${r}, ${g}, ${b})`;
  460. updateColorInputs(rgbToHex(color));
  461. setColorDebounced(color);
  462. }
  463.  
  464. function getPos(event) {
  465. const canvasRect = canvas.getBoundingClientRect();
  466. const canvasScale = canvas.width / canvasRect.width;
  467. return {
  468. x: (event.clientX - canvasRect.left) * canvasScale,
  469. y: (event.clientY - canvasRect.top) * canvasScale
  470. };
  471. }
  472.  
  473. function handleDrop(e) {
  474. e.preventDefault();
  475. colorsDiv.style.filter = '';
  476. handleFiles(e.dataTransfer.files);
  477. }
  478.  
  479. function handleFiles(files) {
  480. if (!files) return;
  481. files = [...files];
  482. files.forEach(file => {
  483. const reader = new FileReader();
  484. reader.readAsText(file);
  485. reader.onload = loadPalette;
  486. });
  487. }
  488.  
  489. function addPalette(palette) {
  490. if (palettes[paletteIndex].length + palette.length < 40 && !isPaletteLocked(paletteIndex)) {
  491. palettes[paletteIndex] = palettes[paletteIndex].concat(palette);
  492. }
  493. else {
  494. palettes.push(palette);
  495. paletteIndex = palettes.length - 1;
  496. localStorage.setItem('paletteIndex', paletteIndex);
  497. }
  498. resetPaletteState();
  499. }
  500.  
  501. function loadPalette(event) {
  502. const loadedString = event.target.result;
  503. const coolorRegex = /CSV \*\/\s*(\S+)/;
  504. const arrayRegex = /\[\[?\s*([^\]]+)/g;
  505. const hexRegex = /#([0-9A-Fa-f]{3}){1,2}/g;
  506. const coolorMatch = loadedString.match(coolorRegex);
  507. const arrayMatch = loadedString.match(arrayRegex);
  508. if (coolorMatch) {
  509. const palette = coolorMatch[1].split(',').map(color => `#${color}`);
  510. addPalette(palette);
  511. return;
  512. }
  513. else if (arrayMatch) {
  514. const paletteMatch = arrayMatch.map(palette => palette.match(hexRegex));
  515. paletteMatch.forEach(palette => addPalette(palette));
  516. }
  517. else {
  518. addHexFromString(loadedString);
  519. }
  520. }
  521.  
  522. function addHexFromString(string) {
  523. const hexRegex = /([0-9A-Fa-f]{3}){1,2}/g;
  524. const hexCodesFound = [...new Set(string.match(hexRegex))];
  525. console.log('Hex codes found: ', hexCodesFound);
  526. const codes = hexCodesFound.map(code => '#' + code);
  527. codes.forEach(code => addColor(code));
  528. changePalette();
  529. savePalettes();
  530. }
  531.  
  532. function resetPaletteState() {
  533. updateLock();
  534. changePalette();
  535. savePalettes();
  536. }
  537.  
  538. function highlight(e) {
  539. e.preventDefault();
  540. colorsDiv.style.filter = 'brightness(0.6)';
  541. }
  542.  
  543. function unhighlight(e) {
  544. e.preventDefault();
  545. colorsDiv.style.filter = '';
  546. }
  547.  
  548. function isDrawing() {
  549. return document.querySelector('#gameTools').style.display !== 'none';
  550. }
  551.  
  552. function adjustChatSize() {
  553. chatBox.style.height = isDrawing() ? 'calc(100% - 200px)' : 'calc(100% - 180px)';
  554. }