Color Picker

Color picker for Sketchful

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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)';
}