Greasy Fork is available in English.

Color Picker

Color picker for Sketchful

// ==UserScript==
// @name        Color Picker
// @namespace   https://greasyfork.org/users/281093
// @match       https://sketchful.io/*
// @grant       none
// @version     0.7.5
// @author      Bell
// @license     MIT
// @copyright   2020, Bell
// @description Color picker for Sketchful
// @run-at      document-end
// ==/UserScript==
// jshint esversion: 6

const defaultPalettes = [
	[
		'#ffffff', '#d3d1d2', '#f70f0f', '#ff7200', '#fce700', '#02cb00', '#01fe94', '#05b0ff', '#221ecd', '#a300bd', '#cc7fad', '#fdad88', '#9e5425',
		'#514f54', '#a9a7a8', '#ae0b00', '#c84706', '#ec9e06', '#007612', '#049d6f', '#00579d', '#0f0b96', '#6e0083', '#a65673', '#e38a5e', '#5e320d',
		'#000000', '#827c80', '#57060c', '#8b2500', '#9e6600', '#003f00', '#00766a', '#003b75', '#0e0151', '#3c0350', '#73314d', '#d1754e', '#421e06'
	],

	[
		'#3a3a3c', '#8e8e93', '#f8f9fa', '#ffadad', '#ffd6a5', '#fdffb6', '#caffbf', '#9bf6ff', '#a0c4ff', '#bdb2ff', '#ffc6ff', '#fdad88', '#9e5425',
		'#2c2c2e', '#636366', '#e0e0e0', '#ff7070', '#f3a220', '#f9e079', '#049d6f', '#92ddea', '#6dafe0', '#ab87ff', '#ff87ab', '#e38a5e', '#5e320d',
		'#1c1c1e', '#48484a', '#c2c2c2', '#f54d4d', '#dc8700', '#f0c808', '#00766a', '#219bc3', '#548bbc', '#715aff', '#ff5d8f', '#d1754e', '#421e06'
	],

	[
		'#081c15', '#1b4332', '#2d6a4f', '#40916c', '#52b788', '#74c69d', '#95d5b2', '#b7e4c7', '#d8f3dc', '#000000', '#faf9f9', '#ffd6ba', '#fec89a',
		'#774936', '#8a5a44', '#9d6b53', '#b07d62', '#c38e70', '#cd9777', '#d69f7e', '#deab90', '#e6b8a2', '#edc4b3', '#ffb5a7', '#fcd5ce', '#f8edeb',
		'#cb997e', '#eddcd2', '#fff1e6', '#f0efeb', '#ddbea9', '#a5a58d', '#b7b7a4', '#6d6875', '#b5838d', '#e5989b', '#ffb4a2', '#ffcdb2', '#f9dcc4'
	],

	[
		'#10002b', '#240046', '#3c096c', '#5a189a', '#7b2cbf', '#9d4edd', '#c77dff', '#e0aaff', '#efcefa', '#d4b2d8', '#a88fac', '#826c7f', '#5d4e60',
		'#7c6f93', '#886f93', '#a967ad', '#ad6789', '#db81ad', '#ff6c91', '#ff736c', '#ff9e46', '#faa275', '#ff8c61', '#ce6a85', '#985277', '#5c374c',
		'#721b65', '#b80d57', '#f8615a', '#ffd868', '#bb596b', '#f96d80', '#ff9a76', '#ffc4a3', '#00e0ff', '#74f9ff', '#a6fff2', '#e8ffe8', '#ffffff'
	],

	[
		'#007f5f', '#2b9348', '#55a630', '#80b918', '#aacc00', '#bfd200', '#d4d700', '#dddf00', '#eeef20', '#ffff3f', '#03045e', '#0077b6', '#00b4d8',
		'#ff4800', '#ff5400', '#ff6000', '#ff6d00', '#ff7900', '#ff8500', '#ff9100', '#ff9e00', '#ffaa00', '#ffb600', '#90e0ef', '#caf0f8', '#000000',
		'#143642', '#263c41', '#38413f', '#4a473e', '#5c4d3c', '#6f523b', '#815839', '#935e38', '#a56336', '#b76935', '#000000', '#ffffff', '#ffffff'
	]
];
const palettes = JSON.parse(localStorage.getItem('palettes')) || defaultPalettes;
let paletteIndex = parseInt(localStorage.getItem('paletteIndex')) || 0;
let lockedPalettes = JSON.parse(localStorage.getItem('lockedPalettes')) || [0];

let activeColor = {
	node: null,
	index: null
};

const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
const gameTools = document.querySelector('#gameTools');
const chatBox = document.querySelector('#gameChat');
const colorButtons = document.querySelectorAll('.gameToolsColor');
const colorsDiv = document.querySelector('#gameToolsColors');
const colorButton = document.querySelector('#gameToolsColors > div:nth-child(1) > div:nth-child(1)');

const colorPickerWrapper = document.createElement('div');
const colorInput = document.createElement('input');
const colorPicker = document.createElement('input');
const inputStyle = 'margin: 5px 0; height: 20px; width: 35%; text-align: center; border: none;font-weight: 800; border-radius: 5px; background-color: #CBCBCB;';
const wrapperStyle = 'position: absolute; margin: 5px 35%; height: 20px; width: 37px; border-radius: 5px;';

(function init() {
	addPicker();
	updatePageStyle();
	addObservers();
	addListeners();
	changePalette();
})();

function addPicker() {
	colorPicker.type = 'color';
	colorPicker.setAttribute('style', 'opacity: 0; width: 37px; cursor: pointer;');
	colorPicker.oninput = updatePicker;
	colorPicker.setAttribute('data-toggle', 'tooltip');
	colorPicker.setAttribute('data-original-title', 'Color Picker');
	colorPicker.setAttribute('data-placement', 'bottom');
	colorPicker.title = 'Color Picker';

	colorPickerWrapper.setAttribute('style', wrapperStyle);
	colorPickerWrapper.style.backgroundColor = colorPicker.value;
	colorPickerWrapper.appendChild(colorPicker);
	gameTools.appendChild(colorPickerWrapper);

	colorInput.oninput = updateInput;
	colorInput.onclick = selectInputText;
	colorInput.setAttribute('style', inputStyle);
	colorInput.setAttribute('spellcheck', 'false');
	colorInput.setAttribute('maxlength', '7');
	colorInput.value = colorPicker.value;
	gameTools.appendChild(colorInput);
	addButtons();
}

const setColorDebounced = debounce(setColor, 5);

function debounce(func, delay) {
	let inDebounce;
	return function() {
		const context = this;
		const args = arguments;
		clearTimeout(inDebounce);
		inDebounce = setTimeout(() => func.apply(context, args), delay);
	};
}

function addObservers() {
	const heightObserver = new MutationObserver(adjustChatSize);
	const config = {
		attributes: true
	};
	heightObserver.observe(gameTools, config);
	heightObserver.observe(chatBox, config);
}

let pickingColor = false;
function pickerIconOn(e) {
	if (e.code !== 'AltLeft') return;
	canvas.style.cursor = 'crosshair';
	pickingColor = true;
	e.preventDefault();
}

function pickerIconOff(e) {
	if (e.code !== 'AltLeft' || !pickingColor) return;
	pickingColor = false;
	regenerateCursor();
	e.preventDefault();
}

function regenerateCursor() {
	const selectedTool = document.querySelector('.gameToolsSelected');
	selectedTool.id === 'gameToolsDraw' ? selectedTool.nextSibling.click() :
		selectedTool.previousSibling.click();
	selectedTool.click();
}

function addListeners() {
	canvas.addEventListener('pointerdown', pickCanvasColor, false);
	document.addEventListener('keydown', pickerIconOn, true);
	document.addEventListener('keyup', pickerIconOff, true);

	const saveBtn = document.querySelector('#savePalette');
	saveBtn.addEventListener('dragenter', highlight, false);
	saveBtn.addEventListener('dragleave', unhighlight, false);
	saveBtn.addEventListener('drop', handleDrop, false);
	saveBtn.addEventListener('dragover', e => {
		e.preventDefault();
	}, false);

	document.addEventListener('keydown', e => {
		if (e.altKey && e.shiftKey && !isPaletteLocked(paletteIndex)) {
			colorsDiv.style.boxShadow = '0 0 0 2px red';
		}
	}, false);
	document.addEventListener('keyup', e => {
		if (e.altKey || e.shiftKey) {
			colorsDiv.style.boxShadow = '';
		}
	}, false);

	document.addEventListener('paste', e => {
		if (document.activeElement.tagName === 'INPUT') return;
		const paste = (e.clipboardData || window.clipboardData).getData('text');
		const coolorRegex = /coolors\.co\/([a-f0-9-]+)/;
		const match = coolorRegex.exec(paste);
		if (match) {addHexFromString(match[1]);}
	}, false);

	colorsDiv.addEventListener('pointerenter', () => {
		colorsDiv.addEventListener('pointerdown', editColor, true);
	});

	colorsDiv.addEventListener('pointerleave', () => {
		colorsDiv.removeEventListener('pointerdown', editColor, true);
	});

	document.addEventListener('keydown', e=> {
		if (!e.ctrlKey || e.code !== 'KeyZ') return;
		e.preventDefault();
	});
}

function updatePageStyle() {
	document.querySelector('#gameToolsSlider').style.top = '77px';
	gameTools.style.height = '200px';
}

function toggleLock() {
	const lockBtn = document.querySelector('#lockButton');
	if (lockBtn.getAttribute('state') === 'unlocked') {
		lockPalette(lockBtn);
	}
	else {
		unlockPalette(lockBtn);
	}
	updateLock();
}

function lockPalette() {
	lockedPalettes.push(paletteIndex);
	localStorage.setItem('lockedPalettes', JSON.stringify(lockedPalettes));
}

function unlockPalette() {
	const index = lockedPalettes.indexOf(paletteIndex);
	if (index < 0) return;
	lockedPalettes.splice(index, 1);
	localStorage.setItem('lockedPalettes', JSON.stringify(lockedPalettes));
}

function updateLock() {
	const lockBtn = document.querySelector('#lockButton');
	if (isPaletteLocked(paletteIndex)) {
		lockBtn.classList.remove('fa-unlock-alt');
		lockBtn.classList.add('fa-lock');
		lockBtn.setAttribute('state', 'locked');
		colorsDiv.style.boxShadow = '';
	}
	else {
		lockBtn.classList.add('fa-unlock-alt');
		lockBtn.classList.remove('fa-lock');
		lockBtn.setAttribute('state', 'unlocked');
	}
	resetActiveColor();
}

function addButtons() {
	const prevPaletteBtn = document.createElement('button');
	const saveColorBtn = document.createElement('button');
	const nextPaletteBtn = document.createElement('button');
	const lockBtn = document.createElement('button');

	const saveTooltip = 'Save Color<br>Hold <strong>shift</strong> to save the current palette.';
	const lockTooltip = 'Lock Current Palette';

	addButton(prevPaletteBtn, 'arrow-left', '5px 5px 5px 45px;');
	addButton(saveColorBtn, 'save', '5px 5px 5px 75px;', saveTooltip, 'savePalette');
	addButton(nextPaletteBtn, 'arrow-right', '5px 5px 5px 105px;');
	addButton(lockBtn, 'unlock-alt', '5px 5px 5px 135px;', lockTooltip, 'lockButton');
	lockBtn.setAttribute('state', 'unlocked');

	prevPaletteBtn.addEventListener('click', prevPalette, false);
	saveColorBtn.addEventListener('click', saveColor, false);
	nextPaletteBtn.addEventListener('click', nextPalette, false);
	lockBtn.addEventListener('click', toggleLock, false);
}

function nextPalette() {
	paletteIndex = paletteIndex < (palettes.length - 1) ? paletteIndex + 1 : 0;
	localStorage.setItem('paletteIndex', paletteIndex);
	changePalette();
}

function prevPalette() {
	paletteIndex = paletteIndex > 0 ? paletteIndex - 1 : palettes.length - 1;
	localStorage.setItem('paletteIndex', paletteIndex);
	changePalette();
}

function saveColor(e) {
	if (e.shiftKey) {
		downloadPalettes();
		return;
	}
	const currentPalette = palettes[paletteIndex];
	if (activeColor.index) {
		currentPalette[activeColor.index] = colorPicker.value;
	}
	else {
		addColor(colorPicker.value);
	}
	changePalette();
	savePalettes();
}

function addColor(color) {
	if (palettes[paletteIndex].length > 38 || isPaletteLocked(paletteIndex)) {
		palettes.push([]);
		paletteIndex = palettes.length - 1;
	}
	palettes[paletteIndex].push(color);
}

function rgbToHex(rgb) {
	const regEx = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/;
	const [, r, g, b] = regEx.exec(rgb);

	function hex(x) {
		return ('0' + parseInt(x).toString(16)).slice(-2);
	}

	return `#${hex(r)}${hex(g)}${hex(b)}`;
}

function savePalettes() {
	localStorage.setItem('palettes', JSON.stringify(palettes));
}

function downloadPalettes() {
	const formattedPaletteData = JSON.stringify(palettes[paletteIndex]).replace(/\],/g, '],\n\n');
	download('palette.txt', formattedPaletteData);
}

function download(filename, text) {
	const pom = document.createElement('a');
	pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
	pom.setAttribute('download', filename);

	if (document.createEvent) {
		const event = document.createEvent('MouseEvents');
		event.initEvent('click', true, true);
		pom.dispatchEvent(event);
	}
	else {
		pom.click();
	}
}

function isPaletteLocked(index) {
	return lockedPalettes.includes(index);
}

function updateColorInputs(colorValue) {
	colorPickerWrapper.style.backgroundColor = colorValue;
	colorPicker.value = colorValue;
	colorInput.value = colorValue;
}

function editColor(event) {
	if (!event.target.classList.contains('gameToolsColor')) return;
	if (!event.shiftKey) updateColorInputs(rgbToHex(event.target.style.backgroundColor));

	if (isPaletteLocked(paletteIndex)) return;
	const color = {
		node: event.target,
		index: Array.prototype.indexOf.call(colorButtons, event.target)
	};

	if (event.altKey && event.shiftKey) {
		deletePalette(paletteIndex);
	}
	else if (event.altKey && color.index >= 0) {
		const palette = palettes[paletteIndex];
		palette.splice(color.index, 1);
		changePalette();
		if (isPaletteEmpty(palette)) deletePalette(paletteIndex);
		savePalettes();
	}
	else if (event.shiftKey && color.index >= 0) {
		setActiveColor(color);
	}
}

function deletePalette(index) {
	if (palettes.length < 2) return;
	palettes.splice(index, 1);
	lockedPalettes = lockedPalettes.map(lockedIndex => {
		return lockedIndex > index ? lockedIndex - 1 : lockedIndex;
	});
	localStorage.setItem('lockedPalettes', JSON.stringify(lockedPalettes));
	prevPalette();
	savePalettes();
	updateLock();
}

function changePalette() {
	if (paletteIndex < 0 || paletteIndex >= palettes.length) {
		paletteIndex = 0;
		localStorage.setItem('paletteIndex', paletteIndex);
	}
	colorButtons.forEach((button, idx) => {
		button.style.backgroundColor = palettes[paletteIndex][idx] || '#fff';
	});
	updateLock();
}

function isPaletteEmpty(palette) {
	if (!palette) return true;
	let empty = true;
	for (const color of palette) {
		if (color) {
			empty = false;
			break;
		}
	}
	return empty;
}

function setActiveColor(color) {
	resetActiveColor();
	activeColor = color;
	activeColor.node.style.border = 'solid 2px red';
}

function resetActiveColor() {
	if (activeColor.node) {
		activeColor.node.style.border = '';
		activeColor = {
			node: null,
			index: null
		};
	}
}

function addButton(button, icon, pos, tooltip = '', id = '') {
	const buttonStyle = `margin: ${pos}; position: absolute; height: 20px; border: none; background-color: #CBCBCB; border-radius: 5px;`;
	button.setAttribute('style', buttonStyle);
	button.setAttribute('class', `fas fa-${icon}`);
	tooltip && button.setAttribute('data-toggle', 'tooltip');
	tooltip && button.setAttribute('data-original-title', tooltip);
	button.setAttribute('data-placement', 'bottom');
	button.title = tooltip;
	button.id = id;
	gameTools.appendChild(button);
}

function updatePicker(event) {
	const color = event.target.value;
	colorPickerWrapper.style.backgroundColor = color;
	colorInput.value = color;
	setColorDebounced(color);
}

function updateInput(event) {
	const hexFound = /([0-9A-Fa-f]{3}){1,2}/.exec(event.target.value);
	if (!hexFound) return;
	const color = '#' + hexFound[0];
	colorPickerWrapper.style.backgroundColor = color;
	colorPicker.value = color;
	setColorDebounced(color);
}

const pointerdownEvent = new Event('pointerdown');

function setColor(color) {
	const prevColor = colorButton.style.backgroundColor;
	colorButton.style.backgroundColor = color;
	colorButton.dispatchEvent(pointerdownEvent);
	colorButton.style.backgroundColor = prevColor;
}

function selectInputText() {
	colorInput.select();
}

function pickCanvasColor(event) {
	if (!event.altKey) return;
	event.preventDefault();
	event.stopImmediatePropagation();
	const pos = getPos(event);
	const [r, g, b] = ctx.getImageData(pos.x, pos.y, 1, 1).data;
	const color = `rgb(${r}, ${g}, ${b})`;
	updateColorInputs(rgbToHex(color));
	setColorDebounced(color);
}

function getPos(event) {
	const canvasRect = canvas.getBoundingClientRect();
	const canvasScale = canvas.width / canvasRect.width;
	return {
		x: (event.clientX - canvasRect.left) * canvasScale,
		y: (event.clientY - canvasRect.top) * canvasScale
	};
}

function handleDrop(e) {
	e.preventDefault();
	colorsDiv.style.filter = '';
	handleFiles(e.dataTransfer.files);
}

function handleFiles(files) {
	if (!files) return;
	files = [...files];
	files.forEach(file => {
		const reader = new FileReader();
		reader.readAsText(file);
		reader.onload = loadPalette;
	});
}

function addPalette(palette) {
	if (palettes[paletteIndex].length + palette.length < 40 && !isPaletteLocked(paletteIndex)) {
		palettes[paletteIndex] = palettes[paletteIndex].concat(palette);
	}
	else {
		palettes.push(palette);
		paletteIndex = palettes.length - 1;
		localStorage.setItem('paletteIndex', paletteIndex);
	}
	resetPaletteState();
}

function loadPalette(event) {
	const loadedString = event.target.result;
	const coolorRegex = /CSV \*\/\s*(\S+)/;
	const arrayRegex = /\[\[?\s*([^\]]+)/g;
	const hexRegex = /#([0-9A-Fa-f]{3}){1,2}/g;
	const coolorMatch = loadedString.match(coolorRegex);
	const arrayMatch = loadedString.match(arrayRegex);
	if (coolorMatch) {
		const palette = coolorMatch[1].split(',').map(color => `#${color}`);
		addPalette(palette);
		return;
	}
	else if (arrayMatch) {
		const paletteMatch = arrayMatch.map(palette => palette.match(hexRegex));
		paletteMatch.forEach(palette => addPalette(palette));
	}
	else {
		addHexFromString(loadedString);
	}
}

function addHexFromString(string) {
	const hexRegex = /([0-9A-Fa-f]{3}){1,2}/g;
	const hexCodesFound = [...new Set(string.match(hexRegex))];
	console.log('Hex codes found: ', hexCodesFound);
	const codes = hexCodesFound.map(code => '#' + code);
	codes.forEach(code => addColor(code));
	changePalette();
	savePalettes();
}

function resetPaletteState() {
	updateLock();
	changePalette();
	savePalettes();
}

function highlight(e) {
	e.preventDefault();
	colorsDiv.style.filter = 'brightness(0.6)';
}

function unhighlight(e) {
	e.preventDefault();
	colorsDiv.style.filter = '';
}

function isDrawing() {
	return document.querySelector('#gameTools').style.display !== 'none';
}

function adjustChatSize() {
	chatBox.style.height = isDrawing() ? 'calc(100% - 200px)' : 'calc(100% - 180px)';
}