Style Transfer

Maps the drawing's colors to the current color palette.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Style Transfer
// @namespace   https://greasyfork.org/users/281093
// @match       https://sketchful.io/
// @grant       none
// @version     2.1
// @author      Bell
// @license     MIT
// @copyright   2020, Bell
// @description Maps the drawing's colors to the current color palette.
// ==/UserScript==
/* jshint esversion: 6 */

const workerCode = `
	let colorCache = [];
	let palette = [];
	let paletteLab = [];
	let delta00;
	const canvas = {
		width: 800,
		height: 600
	}
	self.onmessage = process;

	function process(e) {
		colorCache = [];
		paletteLab = [];
		let imgData = e.data.imgData;
		let fast = e.data.options.fast;
		let toDither = e.data.options.dither;
		let useWhite = e.data.options.white;
		palette = e.data.palette;
		delta00 = e.data.options.deltaE00;

		let data = imgData.data;

		palette.forEach(rgb => {
			paletteLab.push(rgb2lab(rgb));
		});

		let closestColor;
		for (let y = 0; y < canvas.height; y++) {
			for (let x = 0; x < canvas.width; x++) {
				let i = getIndex(x, y);
				let rgb = data.slice(i, i + 3);
				if (useWhite) {
					if (rgb[0] === 255 && rgb[1] === 255 && rgb[2] === 255) continue;
				}
				closestColor = isCached(rgb) || findClosest(fast, rgb);
				setPixel(data, closestColor, i);
				if (toDither)
					dither(data, rgb, closestColor, x, y);
			}
		}

		postMessage(imgData);
	}

	function dither(data, rgb, closestColor, x, y) {
		let quantError = getQuantError(rgb, closestColor);

		i = getIndex(x + 1, y);
		rgb = data.slice(i, i + 3);
		setPixel(data, getQuantColor(rgb, quantError, 7/16), i);

		i = getIndex(x - 1, y + 1);
		rgb = data.slice(i, i + 3);
		setPixel(data, getQuantColor(rgb, quantError, 3/16), i);

		i = getIndex(x, y + 1);
		rgb = data.slice(i, i + 3);
		setPixel(data, getQuantColor(rgb, quantError, 5/16), i);

		i = getIndex(x + 1, y + 1);
		rgb = data.slice(i, i + 3);
		setPixel(data, getQuantColor(rgb, quantError, 1/16), i);
	}

	function getQuantError(oldColor, newColor) {
		return [
			oldColor[0] - newColor[0],
			oldColor[1] - newColor[1],
			oldColor[2] - newColor[2]
		];
	}

	function getQuantColor(color, error, scale) {
		return [
			color[0] + error[0] * scale,
			color[1] + error[1] * scale,
			color[2] + error[2] * scale
		];
	}

	function setPixel(data, newPixel, index) {
		data[index] = newPixel[0];
		data[index + 1] = newPixel[1];
		data[index + 2] = newPixel[2];
	}

	function rgbValue(data, x, y, c) {
		return data[((y * (4 * canvas.width)) + (4 * x)) + c];
	}

	function getIndex(x, y) {
		return ((y * (4 * canvas.width)) + (4 * x));
	}

	function findClosest(fast, rgb) {
		let closestIndex = fast ? findClosestFast(rgb) : findClosestSlow(rgb);
		cacheColor(rgb, closestIndex);
		return palette[closestIndex];
	}

	function findClosestFast(rgb) {
		let closest = {};
		palette.forEach((color, index) => {
			let distance = ((color[0] - rgb[0]) * 0.30) ** 2 + 
						   ((color[1] - rgb[1]) * 0.59) ** 2+ 
						   ((color[2] - rgb[2]) * 0.11) ** 2;
			if (index === 0 || distance < closest.dist) {
				closest = {
					dist: distance,
					idx: index
				};
			}
		});
		return closest.idx;
	}

	function findClosestSlow(rgb) {
		let closest = {};
		let labColor = rgb2lab(rgb);
		paletteLab.forEach((color, index) => {
			let distance = delta00 ? deltaE00(labColor, color) : 
									 deltaE(labColor, color);
			if (index === 0 || distance < closest.dist) {
				closest = {
					dist: distance,
					idx: index
				};
			}
		});
		return closest.idx;
	}

	function cacheColor(rgb, index) {
		if (colorCache.length > 127) return;
		colorCache.push({
			idx: index,
			color: rgb
		});
	}

	function isCached(rgb) {
		for (let cached of colorCache) {
			if (cached.color[0] === rgb[0] && cached.color[1] === rgb[1] &&
				cached.color[2] === rgb[2]) {
				return palette[cached.idx];
			}
		}
		return false;
	}

	function deltaE(labA, labB) {
		let deltaL = labA[0] - labB[0];
		let deltaA = labA[1] - labB[1];
		let deltaB = labA[2] - labB[2];

		let c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]);
		let c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);

		let deltaC = c1 - c2;
		let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
		deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);

		let sc = 1.0 + 0.045 * c1;
		let sh = 1.0 + 0.015 * c1;

		let deltaLKlsl = deltaL / (1.0);
		let deltaCkcsc = deltaC / (sc);
		let deltaHkhsh = deltaH / (sh);

		let i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
		return i < 0 ? 0 : Math.sqrt(i);
	}

	function rgb2lab(rgb) {
		let r = rgb[0] / 255,
			g = rgb[1] / 255,
			b = rgb[2] / 255,
			x, y, z;

		r = (r > 0.04045) ? ((r + 0.055) / 1.055) ** 2.4 : r / 12.92;
		g = (g > 0.04045) ? ((g + 0.055) / 1.055) ** 2.4 : g / 12.92;
		b = (b > 0.04045) ? ((b + 0.055) / 1.055) ** 2.4 : b / 12.92;

		x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
		y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
		z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;

		x = (x > 0.008856) ? x ** (1 / 3) : (7.787 * x) + 16 / 116;
		y = (y > 0.008856) ? y ** (1 / 3) : (7.787 * y) + 16 / 116;
		z = (z > 0.008856) ? z ** (1 / 3) : (7.787 * z) + 16 / 116;

		return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)];
	}

	function deltaE00(labA, labB) {
		const [l1, a1, b1] = labA;
		const [l2, a2, b2] = labB;

		Math.rad2deg = function(rad) {
			return 360 * rad / (2 * Math.PI);
		};
		Math.deg2rad = function(deg) {
			return (2 * Math.PI * deg) / 360;
		};

		const avgL = (l1 + l2) / 2;
		const C1 = Math.sqrt(Math.pow(a1, 2) + Math.pow(b1, 2));
		const C2 = Math.sqrt(Math.pow(a2, 2) + Math.pow(b2, 2));
		const avgC = (C1 + C2) / 2;
		const G = (1 - Math.sqrt(Math.pow(avgC, 7) / (Math.pow(avgC, 7) + Math.pow(25, 7)))) / 2;

		const A1p = a1 * (1 + G);
		const A2p = a2 * (1 + G);

		const C1p = Math.sqrt(Math.pow(A1p, 2) + Math.pow(b1, 2));
		const C2p = Math.sqrt(Math.pow(A2p, 2) + Math.pow(b2, 2));

		const avgCp = (C1p + C2p) / 2;

		let h1p = Math.rad2deg(Math.atan2(b1, A1p));
		if (h1p < 0) {
			h1p = h1p + 360;
		}

		let h2p = Math.rad2deg(Math.atan2(b2, A2p));
		if (h2p < 0) {
			h2p = h2p + 360;
		}

		const avghp = Math.abs(h1p - h2p) > 180 ? (h1p + h2p + 360) / 2 : (h1p + h1p) / 2;

		const T = 1 - 0.17 * Math.cos(Math.deg2rad(avghp - 30)) + 0.24 * Math.cos(Math.deg2rad(2 * avghp)) + 0.32 * Math.cos(Math.deg2rad(3 * avghp + 6)) - 0.2 * Math.cos(Math.deg2rad(4 * avghp - 63));

		let deltahp = h2p - h1p;
		if (Math.abs(deltahp) > 180) {
			if (h2p <= h1p) {
				deltahp += 360;
			} else {
				deltahp -= 360;
			}
		}

		const delta_lp = l2 - l1;
		const delta_cp = C2p - C1p;

		deltahp = 2 * Math.sqrt(C1p * C2p) * Math.sin(Math.deg2rad(deltahp) / 2);

		const Sl = 1 + ((0.015 * Math.pow(avgL - 50, 2)) / Math.sqrt(20 + Math.pow(avgL - 50, 2)));
		const Sc = 1 + 0.045 * avgCp;
		const Sh = 1 + 0.015 * avgCp * T;

		const deltaro = 30 * Math.exp(-(Math.pow((avghp - 275) / 25, 2)));
		const Rc = 2 * Math.sqrt(Math.pow(avgCp, 7) / (Math.pow(avgCp, 7) + Math.pow(25, 7)));
		const Rt = -Rc * Math.sin(2 * Math.deg2rad(deltaro));

		const kl = 1;
		const kc = 1;
		const kh = 1;

		const deltaE = Math.sqrt(Math.pow(delta_lp / (kl * Sl), 2) + Math.pow(delta_cp / (kc * Sc), 2) + Math.pow(deltahp / (kh * Sh), 2) + Rt * (delta_cp / (kc * Sc)) * (deltahp / (kh * Sh)));

		return deltaE;
	}
`;

const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
const colorButtons = document.querySelectorAll('.gameToolsColor');
const interfaceBar = document.querySelector('#gameInterface');

const container = document.createElement('div');
const fastButton = document.createElement('button');
const slowButton = document.createElement('button');
const ditherText = document.createElement('label');
const ditherCheckbox = document.createElement('input');
const whiteText = document.createElement('label');
const whiteCheckbox = document.createElement('input');
const deltaE00Text = document.createElement('label');
const deltaE00Checkbox = document.createElement('input');
const spinner = document.createElement('img');

let palette = [];
const dataWorker = createWorker(workerCode);

(function init() {
	canvas.save = () => {
		canvas.dispatchEvent(new MouseEvent('pointerup', {
			bubbles: true,
			clientX: 0,
			clientY: 0,
			button: 0
		}));
	};

	initInterface();
	initListeners();
})();

function createWorker(content) {
	const workerBlob = new Blob([content], {
		'type': 'text/javascript'
	});

	const blobURL = window.URL.createObjectURL(workerBlob);

	return new Worker(blobURL);
}

function initListeners() {
	dataWorker.onmessage = (e) => {
		spinner.remove();
		canvas.style.filter = '';
		ctx.putImageData(e.data, 0, 0);
		canvas.save();
	};

	fastButton.onpointerdown = () => {
		transformColor(true);
	};
	slowButton.onpointerdown = () => {
		transformColor(false);
	};
}

function transformColor(fast) {
	getPalette();
	const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
	const options = {
		fast: fast,
		dither: ditherCheckbox.checked,
		white: whiteCheckbox.checked,
		deltaE00: deltaE00Checkbox.checked
	};
	canvas.style.filter = 'brightness(0.5)';
	canvas.parentElement.insertBefore(spinner, canvas);
	dataWorker.postMessage({ imgData: imgData, palette: palette, options: options });
}

function getPalette() {
	palette = whiteCheckbox.checked ? [[255, 255, 255]] : [];
	paletteLab = [];
	colorCache = [];
	colorButtons.forEach(color => {
		if (color.style.background === 'rgb(255, 255, 255)') return;
		palette.push(color.style.background.substring(4, color.style.background.length - 1)
			.replace(/ /g, '').split(',').map(x => parseInt(x)));
	});
}

function canvasVisibility() {
	if (isFreeDraw()) container.style.display = '';
	else container.style.display = 'none';
}

const canvasObserver = new MutationObserver(canvasVisibility);

canvasObserver.observe(document.querySelector('body > div.game'), {
	attributes: true
});
canvasObserver.observe(canvas, {
	attributes: true
});

function isFreeDraw() {
	return canvas.style.display !== 'none' &&
        document.querySelector('#gameClock').style.display === 'none' &&
        document.querySelector('#gameSettings').style.display === 'none';
}

function initInterface() {
	container.style.margin = 'auto';
	container.style.color = '#737373';
	container.style.userSelect = 'none';
	container.style.padding = '9px';

	fastButton.setAttribute('class', 'btn btn-sm btn-primary');
	fastButton.style.marginRight = '5px';

	slowButton.setAttribute('class', 'btn btn-sm btn-primary');
	slowButton.style.marginRight = '5px';

	ditherCheckbox.type = 'checkbox';
	ditherCheckbox.style.marginRight = '5px';
	ditherCheckbox.id = 'dither';
	ditherCheckbox.name = 'dither';
	ditherText.textContent = 'spatter';
	ditherText.style.marginRight = '5px';
	ditherText.setAttribute('for', 'dither');

	whiteCheckbox.type = 'checkbox';
	whiteCheckbox.style.marginRight = '5px';
	whiteCheckbox.id = 'white';
	whiteCheckbox.name = 'white';
	whiteText.textContent = 'white';
	whiteText.style.marginRight = '5px';
	whiteText.setAttribute('for', 'white');

	deltaE00Checkbox.type = 'checkbox';
	deltaE00Checkbox.style.marginRight = '5px';
	deltaE00Checkbox.id = 'deltaE00';
	deltaE00Checkbox.name = 'deltaE00';
	deltaE00Text.textContent = 'deltaE00';
	deltaE00Text.style.marginRight = '5px';
	deltaE00Text.setAttribute('for', 'deltaE00');

	fastButton.textContent = 'FAST';
	slowButton.textContent = 'ACCURATE';

	spinner.style.position = 'absolute';
	spinner.style.width = '100px';
	spinner.style.zIndex = '1';
	spinner.src = '/res/svg/spinner.svg';

	container.append(fastButton, slowButton, ditherCheckbox, ditherText,
		whiteCheckbox, whiteText);
	interfaceBar.appendChild(container);
}