// ==UserScript==
// @name Style Transfer
// @match https://sketchful.io/
// @grant none
// @version 0.1
// @author Bell
// @description Maps the drawing's colors to the current color palette.
// @namespace https://greasyfork.org/users/281093
// jshint esversion: 6
// ==/UserScript==
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext('2d');
const gameParent = document.querySelector("body > div.game > div.gameParent");
const colorButtons = document.querySelectorAll(".gameToolsColor");
const interfaceBar = document.querySelector("#gameInterface");
const buttonContainer = document.createElement("div");
const fastButton = document.createElement("button");
const slowButton = document.createElement("button");
const undoButton = document.createElement("button");
let palette = [];
let paletteLab = [];
let storedDrawings = [];
let colorCache = [];
(function init() {
initInterface();
initListeners();
})();
function initInterface() {
buttonContainer.style.marginLeft = "45%";
fastButton.style = `height: 50px;border: none;background-color: cornflowerblue; height: 30px;border-radius: 5px;margin-top: 10px; font-weight: 800`;
slowButton.style = `height: 30px; border: none;background-color: chocolate; border-radius: 5px; margin-top: 10px; margin-left: 10px; font-weight: 800`;
undoButton.style = `height: 30px; border: none;background-color: cadetblue; border-radius: 5px; margin-top: 10px; margin-left: 10px; font-weight: 800`;
fastButton.textContent = "FAST";
slowButton.textContent = "ACCURATE";
undoButton.setAttribute("class", "fas fa-undo-alt");
buttonContainer.appendChild(fastButton);
buttonContainer.appendChild(slowButton);
buttonContainer.appendChild(undoButton);
interfaceBar.appendChild(buttonContainer);
}
function initListeners() {
fastButton.onpointerdown = () => {
transformColor(true);
};
slowButton.onpointerdown = () => {
transformColor(false);
};
undoButton.onpointerdown = () => {
if (!storedDrawings.length) return;
ctx.putImageData(storedDrawings.pop(), 0, 0);
};
}
function transformColor(fast) {
getPalette();
storedDrawings.length < 15 && storedDrawings.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let data = imgData.data;
let closestColor;
for (let i = 0; i < data.length; i += 4) {
let rgb = data.slice(i, i + 3);
closestColor = isCached(rgb) || findClosest(fast, rgb);
data[i] = closestColor[0];
data[i + 1] = closestColor[1];
data[i + 2] = closestColor[2];
}
ctx.putImageData(imgData, 0, 0);
}
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 = Math.pow(((color[0] - rgb[0]) * 0.30), 2) +
Math.pow(((color[1] - rgb[1]) * 0.59), 2) +
Math.pow(((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 = 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 getPalette() {
palette = [];
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)));
});
palette.forEach(rgb => {
paletteLab.push(rgb2lab(rgb));
});
}
function canvasVisibility(mutations, observer) {
for (let mutation of mutations) {
if (isFreeDraw()) buttonContainer.style.display = "";
else buttonContainer.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 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) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
b = (b > 0.04045) ? Math.pow((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) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116;
y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + 16 / 116;
z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + 16 / 116;
return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)];
}