Style Transfer

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
}