// ==UserScript==
// @name Gradient Tool
// @namespace https://greasyfork.org/users/281093
// @match https://sketchful.io/
// @grant none
// @version 1.0
// @author Bell
// @license MIT
// @copyright 2020, Faux (https://greasyfork.org/users/281093)
// @description Ctrl + Click and drag to draw a gradient, click a color to set it as the first and right click a color to set it as the second.
// ==/UserScript==
/* jshint esversion: 8 */
const canvas = document.querySelector('#canvas');
const colorsDiv = document.querySelector('#gameToolsColors');
const lineCanvas = document.createElement('canvas');
const lineCtx = lineCanvas.getContext('2d');
const sizeInput = document.querySelector("#gameToolsColorPreview");
lineCanvas.style.position = 'absolute';
lineCanvas.style.cursor = 'crosshair';
lineCanvas.style.width = '100%';
lineCanvas.style.display = 'none';
lineCanvas.style.userSelect = 'none';
lineCanvas.style.zIndex = '2';
lineCanvas.style.filter = 'opacity(0.7)';
lineCanvas.oncontextmenu = () => { return false; };
[lineCanvas.width, lineCanvas.height] = [canvas.width, canvas.height];
canvas.parentElement.insertBefore(lineCanvas, canvas);
lineCanvas.clear = () => {
lineCtx.clearRect(0, 0, lineCanvas.width, lineCanvas.height);
};
canvas.save = () => {
canvas.dispatchEvent(createMouseEvent('pointerup', { x: 0, y: 0 }, true));
};
let origin = {};
let realOrigin = {};
let previewPos = {};
let realPos = {};
let canvasHidden = true;
let drawingLine = false;
let activeColor = [0, 0, 0];
let color1 = [0, 200, 0];
let color2 = [250, 0, 0];
let drawingGradient = false;
let queueKeyUp = false;
document.addEventListener('keydown', (e) => {
if (!isDrawing()) return;
if (e.code === 'ControlLeft' && canvasHidden) {
lineCanvas.style.display = '';
disableScroll();
canvasHidden = false;
}
});
document.addEventListener('keyup', (e) => {
if (e.code === 'Digit1' && e.shiftKey) color1 = activeColor;
else if (e.code === 'Digit2' && e.shiftKey) color2 = activeColor;
if (e.code === 'ControlLeft' && !canvasHidden) {
if (drawingGradient) {
queueKeyUp = true;
return;
}
onKeyUp();
}
});
function onKeyUp() {
lineCanvas.style.display = 'none';
canvasHidden = true;
enableScroll();
resetLineCanvas();
document.removeEventListener('pointermove', savePos);
document.removeEventListener('pointerup', pointerUpDraw);
}
function savePos(e) {
previewPos = getPos(e);
realPos = getRealPos(e);
if (canvasHidden || !drawingLine) return;
lineCanvas.clear();
drawPreviewLine(previewPos);
e.preventDefault();
}
colorsDiv.addEventListener('pointerdown', (e) => {
if (!e.target.classList.contains('gameToolsColor') || e.button === 2) return;
const colorStr = e.target.style.background;
const match = colorStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)/);
activeColor = match.slice(1, 4).map(str => parseInt(str));
color1 = activeColor;
});
colorsDiv.addEventListener('contextmenu', (e) => {
if (!e.target.classList.contains('gameToolsColor')) return;
e.preventDefault();
const colorStr = e.target.style.background;
const match = colorStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)/);
color2 = match.slice(1, 4).map(str => parseInt(str));
});
lineCanvas.addEventListener('pointerdown', (e) => {
if (drawingGradient) return;
origin = getPos(e);
realOrigin = getRealPos(e);
drawingLine = true;
document.addEventListener('pointerup', pointerUpDraw);
document.addEventListener('pointermove', savePos);
});
function pointerUpDraw(e) {
document.removeEventListener('pointermove', savePos);
document.removeEventListener('pointerup', pointerUpDraw);
drawGradient(realOrigin.x, realOrigin.y, realPos.x, realPos.y);
previewPos = getPos(e);
realPos = getRealPos(e);
resetLineCanvas();
}
async function drawGradient(x1, y1, x2, y2) {
drawingGradient = true;
const lab1 = rgb2lab(color1);
const lab2 = rgb2lab(color2);
const scale = canvas.getBoundingClientRect().width / lineCanvas.width;
const offset = (parseInt(sizeInput.value) / 2) * scale;
const max = Math.round((y2 - y1) / offset) * 1.6;
for (let s = 0; s <= max; s += 1) {
const t = mapRange(s, 0, max, 0, 1);
const y = lerp(y1 + offset, y2 - offset, t);
const color = lerpColors(lab1, lab2, t);
changeColor(lab2rgb(color));
drawLine(x1 + offset, y, x2 - offset, y);
await delay(10);
}
canvas.save();
drawingGradient = false;
if (queueKeyUp) {
onKeyUp();
queueKeyUp = false;
}
}
function mapRange(value, istart, istop, ostart, ostop) {
return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
}
function delay(time) {
return new Promise((resolve) => {
setTimeout(() => resolve(time), time);
});
}
function changeColor(rgbArray) {
const colorElement = document.querySelector('.gameToolsColor');
const prevColor = colorElement.style.background;
colorElement.style.background = `rgb(${rgbArray[0]}, ${rgbArray[1]}, ${rgbArray[2]})`;
colorElement.dispatchEvent(new Event('pointerdown'));
colorElement.style.background = prevColor;
}
function resetLineCanvas() {
drawingLine = false;
lineCanvas.clear();
}
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 getRealPos(event) {
return {
x: event.clientX,
y: event.clientY
};
}
function drawPreviewLine(pos) {
const offset = parseInt(sizeInput.value) / 2;
roundRect(
lineCtx,
origin.x,
origin.y,
pos.x - origin.x,
pos.y - origin.y,
offset,
true,
false,
[
color1,
color2
]
);
}
function roundRect(ctx, x, y, width, height, radius, fill, stroke, gradientColors) {
if (typeof stroke === 'undefined') {
stroke = true;
}
if (typeof radius === 'undefined') {
radius = 5;
}
if (typeof radius === 'number') {
radius = {tl: radius, tr: radius, br: radius, bl: radius};
} else {
var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
for (var side in defaultRadius) {
radius[side] = radius[side] || defaultRadius[side];
}
}
ctx.beginPath();
ctx.moveTo(x + radius.tl, y);
ctx.lineTo(x + width - radius.tr, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
ctx.lineTo(x + width, y + height - radius.br);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
ctx.lineTo(x + radius.bl, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
ctx.lineTo(x, y + radius.tl);
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
ctx.closePath();
if (gradientColors) {
const gradient = ctx.createLinearGradient(0, y, 0, height + y);
const _color1 = `rgb(${gradientColors[0][0]}, ${gradientColors[0][1]}, ${gradientColors[0][2]}`;
const _color2 = `rgb(${gradientColors[1][0]}, ${gradientColors[1][1]}, ${gradientColors[1][2]}`;
gradient.addColorStop('0', _color1);
gradient.addColorStop('1', _color2);
ctx.strokeStyle = gradient;
ctx.fillStyle = gradient;
}
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.lineWidth = 5;
ctx.stroke();
}
}
function drawLine(x1, y1, x2, y2) {
const coords = { x: x1, y: y1 };
const newCoords = { x: x2, y: y2 };
canvas.dispatchEvent(createMouseEvent('pointerdown', coords));
canvas.dispatchEvent(createMouseEvent('pointermove', newCoords));
}
function createMouseEvent(name, pos, bubbles = false) {
return new MouseEvent(name, {
bubbles: bubbles,
clientX: pos.x,
clientY: pos.y,
button: 0
});
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function lerpColors(lab1, lab2, t) {
return [
lab1[0] + (lab2[0] - lab1[0]) * t,
lab1[1] + (lab2[1] - lab1[1]) * t,
lab1[2] + (lab2[2] - lab1[2]) * t,
];
}
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 lab2rgb(lab){
let y = (lab[0] + 16) / 116,
x = lab[1] / 500 + y,
z = y - lab[2] / 200,
r, g, b;
x = 0.95047 * ((x * x * x > 0.008856) ? x * x * x : (x - 16/116) / 7.787);
y = 1.00000 * ((y * y * y > 0.008856) ? y * y * y : (y - 16/116) / 7.787);
z = 1.08883 * ((z * z * z > 0.008856) ? z * z * z : (z - 16/116) / 7.787);
r = x * 3.2406 + y * -1.5372 + z * -0.4986;
g = x * -0.9689 + y * 1.8758 + z * 0.0415;
b = x * 0.0557 + y * -0.2040 + z * 1.0570;
r = (r > 0.0031308) ? (1.055 * Math.pow(r, 1/2.4) - 0.055) : 12.92 * r;
g = (g > 0.0031308) ? (1.055 * Math.pow(g, 1/2.4) - 0.055) : 12.92 * g;
b = (b > 0.0031308) ? (1.055 * Math.pow(b, 1/2.4) - 0.055) : 12.92 * b;
return [
Math.max(0, Math.min(1, r)) * 255,
Math.max(0, Math.min(1, g)) * 255,
Math.max(0, Math.min(1, b)) * 255
];
}
const keys = { 32: 1, 37: 1, 38: 1, 39: 1, 40: 1 };
function preventDefault(e) {
e.preventDefault();
}
function preventDefaultForScrollKeys(e) {
if (keys[e.keyCode]) {
preventDefault(e);
return false;
}
}
function isDrawing() {
return document.querySelector('#gameTools').style.display !== 'none' &&
document.querySelector('body > div.game').style.display !== 'none' &&
document.activeElement.tagName !== 'INPUT';
}
let supportsPassive = false;
try {
window.addEventListener('test', null, Object.defineProperty({}, 'passive', {
get: function() {
supportsPassive = true;
return true;
}
}));
}
catch(e) {
console.log(e);
}
const wheelOpt = supportsPassive ? { passive: false } : false;
const wheelEvent = 'onwheel' in document.createElement('div') ? 'wheel' : 'mousewheel';
function disableScroll() {
window.addEventListener('DOMMouseScroll', preventDefault, false);
window.addEventListener(wheelEvent, preventDefault, wheelOpt);
window.addEventListener('touchmove', preventDefault, wheelOpt);
window.addEventListener('keydown', preventDefaultForScrollKeys, false);
}
function enableScroll() {
window.removeEventListener('DOMMouseScroll', preventDefault, false);
window.removeEventListener(wheelEvent, preventDefault, wheelOpt);
window.removeEventListener('touchmove', preventDefault, wheelOpt);
window.removeEventListener('keydown', preventDefaultForScrollKeys, false);
}