Color Picker

Color picker for Sketchful

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

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