Style Transfer

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

目前為 2020-07-09 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Style Transfer
// @match       https://sketchful.io/
// @grant       none
// @version     0.1.1
// @author      Bell
// @namespace   https://greasyfork.org/users/281093
// jshint esversion: 6
// @description Maps the drawing's colors to the current color palette.
// ==/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 = ((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 = 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) ? ((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)];
}