colorpicker: add hwb colors

This commit is contained in:
tophf 2022-04-01 16:38:52 +03:00
parent f54d145bf5
commit 537372dffa
4 changed files with 225 additions and 255 deletions

View File

@ -2,29 +2,82 @@
const colorConverter = (() => { const colorConverter = (() => {
const RXS_NUM = /\s*([+-]?(?:\d+\.?\d*|\d*\.\d+))(?:e[+-]?\d+)?/.source;
const RXS_NUM_ANGLE = `${RXS_NUM}(deg|g?rad|turn)?`;
const RX_COLOR = {
hex: /#([a-f\d]{3}(?:[a-f\d](?:[a-f\d]{2}){0,2})?)\b/iy,
hsl: new RegExp([
// num_or_angle, pct, pct [ , num_or_pct]?
`^(${RXS_NUM_ANGLE})\\s*,(${RXS_NUM}%\\s*(,|$)){2}(${RXS_NUM}%?)?\\s*$`,
// num_or_angle pct pct [ / num_or_pct]?
`^(${RXS_NUM_ANGLE})\\s+(${RXS_NUM}%\\s*(\\s|$)){2}(/${RXS_NUM}%?)?\\s*$`,
].join('|'), 'iy'),
hwb: new RegExp(
// num|angle|none pct|none pct|none [ / num|pct|none ]?
`^(${RXS_NUM_ANGLE}|none)(\\s+(${RXS_NUM}%|none)){2}(\\s+|$)(/${RXS_NUM}%?|none)?\\s*$`,
'iy'),
rgb: new RegExp([
// num, num, num [ , num_or_pct]?
// pct, pct, pct [ , num_or_pct]?
`^((${RXS_NUM}\\s*(,|$)){3}|(${RXS_NUM}%\\s*(,|$)){3})(${RXS_NUM}%?)?\\s*$`,
// num num num [ / num_or_pct]?
// pct pct pct [ / num_or_pct]?
`^((${RXS_NUM}\\s*(\\s|$)){3}|(${RXS_NUM}%\\s*(\\s|$)){3})(/${RXS_NUM}%?)?\\s*$`,
].join('|'), 'iy'),
};
const ANGLE_TO_DEG = {
grad: 360 / 400,
rad: 180 / Math.PI,
turn: 360,
};
const TO_HSV = {
hex: RGBtoHSV,
hsl: HSLtoHSV,
hwb: HWBtoHSV,
rgb: RGBtoHSV,
};
const FROM_HSV = {
hex: HSVtoRGB,
hsl: HSVtoHSL,
hwb: HSVtoHWB,
rgb: HSVtoRGB,
};
const guessType = c =>
'r' in c ? 'rgb' :
'w' in c ? 'hwb' :
'v' in c ? 'hsv' :
'l' in c ? 'hsl' :
undefined;
return { return {
parse, parse,
format, format,
formatAlpha, formatAlpha,
RGBtoHSV, fromHSV: (color, type) => FROM_HSV[type](color),
HSVtoRGB, toHSV: color => TO_HSV[color.type || 'rgb'](color),
HSLtoHSV, constrain,
HSVtoHSL,
constrainHue, constrainHue,
guessType,
snapToInt, snapToInt,
testAt,
ALPHA_DIGITS: 3, ALPHA_DIGITS: 3,
RX_COLOR,
// NAMED_COLORS is added below // NAMED_COLORS is added below
}; };
function format(color = '', type = color.type, hexUppercase, usoMode) { function format(color = '', type = color.type, {hexUppercase, usoMode, round} = {}) {
if (!color || !type) return typeof color === 'string' ? color : ''; if (!color || !type) return typeof color === 'string' ? color : '';
const {a} = color; const {a, type: src = guessType(color)} = color;
let aStr = formatAlpha(a); const aFmt = formatAlpha(a);
if (aStr) aStr = ', ' + aStr; const aStr = aFmt ? ', ' + aFmt : '';
if (type !== 'hsl' && color.type === 'hsl') { const srcConv = src === 'hex' ? 'rgb' : src;
color = HSVtoRGB(HSLtoHSV(color)); const dstConv = type === 'hex' ? 'rgb' : type;
} color = srcConv === dstConv ? color : FROM_HSV[dstConv](TO_HSV[srcConv](color));
const {r, g, b, h, s, l} = color; round = round ? Math.round : v => v;
const {r, g, b, h, s, l, w} = color;
switch (type) { switch (type) {
case 'hex': { case 'hex': {
let res = '#' + hex2(r) + hex2(g) + hex2(b) + (aStr ? hex2(Math.round(a * 255)) : ''); let res = '#' + hex2(r) + hex2(g) + hex2(b) + (aStr ? hex2(Math.round(a * 255)) : '');
@ -36,51 +89,15 @@ const colorConverter = (() => {
return usoMode ? rgb : `rgb${aStr ? 'a' : ''}(${rgb}${aStr})`; return usoMode ? rgb : `rgb${aStr ? 'a' : ''}(${rgb}${aStr})`;
} }
case 'hsl': case 'hsl':
return `hsl${aStr ? 'a' : ''}(${h}, ${s}%, ${l}%${aStr})`; return `hsl${aStr ? 'a' : ''}(${round(h)}, ${round(s)}%, ${round(l)}%${aStr})`;
case 'hwb':
return `hwb(${round(h)} ${round(w)}% ${round(b)}%${aFmt ? ' / ' + aFmt : ''})`;
} }
} }
// Copied from _hexcolor() in parserlib.js
function validateHex(color) {
return /^#[a-f\d]+$/i.test(color) && [4, 5, 7, 9].some(n => color.length === n);
}
function validateRGB(nums) {
const isPercentage = nums[0].endsWith('%');
const valid = isPercentage ? validatePercentage : validateNum;
return nums.slice(0, 3).every(valid);
}
function validatePercentage(s) {
if (!s.endsWith('%')) return false;
const n = Number(s.slice(0, -1));
return n >= 0 && n <= 100;
}
function validateNum(s) {
const n = Number(s);
return n >= 0 && n <= 255;
}
function validateHSL(nums) {
return validateAngle(nums[0]) && nums.slice(1, 3).every(validatePercentage);
}
function validateAngle(s) {
return /^-?(\d+|\d*\.\d+)(deg|grad|rad|turn)?$/i.test(s);
}
function validateAlpha(alpha) {
if (alpha.endsWith('%')) {
return validatePercentage(alpha);
}
const n = Number(alpha);
return n >= 0 && n <= 1;
}
function parse(str) { function parse(str) {
if (typeof str !== 'string') return; if (typeof str !== 'string') return;
str = str.trim(); str = str.trim().toLowerCase();
if (!str) return; if (!str) return;
if (str[0] !== '#' && !str.includes('(')) { if (str[0] !== '#' && !str.includes('(')) {
@ -90,47 +107,45 @@ const colorConverter = (() => {
} }
if (str[0] === '#') { if (str[0] === '#') {
if (!validateHex(str)) { if (!testAt(RX_COLOR.hex, 0, str)) {
return null; return;
} }
str = str.slice(1); str = str.slice(1);
const [r, g, b, a = 255] = str.length <= 4 ? const [r, g, b, a = 255] = str.length <= 4 ?
str.match(/(.)/g).map(c => parseInt(c + c, 16)) : str.match(/(.)/g).map(c => parseInt(c + c, 16)) :
str.match(/(..)/g).map(c => parseInt(c, 16)); str.match(/(..)/g).map(c => parseInt(c, 16));
return {type: 'hex', r, g, b, a: a === 255 ? undefined : a / 255}; return {
type: 'hex',
r,
g,
b,
a: a === 255 ? undefined : a / 255,
};
} }
const [, type, value] = str.match(/^(rgb|hsl)a?\((.*?)\)|$/i); const [, func, type = func, value] = str.match(/^((rgb|hsl)a?|hwb)\(\s*(.*?)\s*\)|$/);
if (!type) return; if (!func || !testAt(RX_COLOR[type], 0, value)) {
return;
const comma = value.includes(',') && !value.includes('/');
const num = value.trim().split(comma ? /\s*,\s*/ : /\s+(?!\/)|\s*\/\s*/);
if (num.length < 3 || num.length > 4) return;
if (num[3] && !validateAlpha(num[3])) return null;
let a = !num[3] ? 1 : parseFloat(num[3]) / (num[3].endsWith('%') ? 100 : 1);
if (isNaN(a)) a = 1;
const first = num[0];
if (/rgb/i.test(type)) {
if (!validateRGB(num)) {
return null;
} }
const k = first.endsWith('%') ? 2.55 : 1; const [s1, s2, s3, sA] = value.split(/\s*[,/]\s*|\s+/);
const [r, g, b] = num.map(s => Math.round(parseFloat(s) * k)); const a = isNaN(sA) ? 1 : constrain(0, 1, sA / (sA.endsWith('%') ? 100 : 1));
return {type: 'rgb', r, g, b, a};
} else { if (type === 'rgb') {
if (!validateHSL(num)) { const k = s1.endsWith('%') ? 2.55 : 1;
return null; return {
} type,
let h = parseFloat(first); r: constrain(0, 255, Math.round(s1 * k)),
if (first.endsWith('grad')) h *= 360 / 400; g: constrain(0, 255, Math.round(s2 * k)),
else if (first.endsWith('rad')) h *= 180 / Math.PI; b: constrain(0, 255, Math.round(s3 * k)),
else if (first.endsWith('turn')) h *= 360; a,
const s = parseFloat(num[1]); };
const l = parseFloat(num[2]);
return {type: 'hsl', h, s, l, a};
} }
const h = constrainHue(parseFloat(s1) * (ANGLE_TO_DEG[s1.match(/\D*$/)[0]] || 1));
const n2 = constrain(0, 100, parseFloat(s2) || 0);
const n3 = constrain(0, 100, parseFloat(s3) || 0);
return type === 'hwb'
? {type, h, w: n2, b: n3, a}
: {type, h, s: n2, l: n3, a};
} }
function formatAlpha(a) { function formatAlpha(a) {
@ -164,8 +179,8 @@ const colorConverter = (() => {
}; };
} }
function HSVtoRGB({h, s, v}) { function HSVtoRGB({h, s, v, a}) {
h = constrainHue(h) % 360; h = constrainHue(h);
const C = s * v; const C = s * v;
const X = C * (1 - Math.abs((h / 60) % 2 - 1)); const X = C * (1 - Math.abs((h / 60) % 2 - 1));
const m = v - C; const m = v - C;
@ -180,9 +195,11 @@ const colorConverter = (() => {
r: snapToInt(Math.round((r + m) * 255)), r: snapToInt(Math.round((r + m) * 255)),
g: snapToInt(Math.round((g + m) * 255)), g: snapToInt(Math.round((g + m) * 255)),
b: snapToInt(Math.round((b + m) * 255)), b: snapToInt(Math.round((b + m) * 255)),
a,
}; };
} }
function HSLtoHSV({h, s, l, a}) { function HSLtoHSV({h, s, l, a}) {
const t = s * (l < 50 ? l : 100 - l) / 100; const t = s * (l < 50 ? l : 100 - l) / 100;
return { return {
@ -193,16 +210,41 @@ const colorConverter = (() => {
}; };
} }
function HSVtoHSL({h, s, v}) { function HSVtoHSL({h, s, v, a}) {
const l = (2 - s) * v / 2; const l = (2 - s) * v / 2;
const t = l < .5 ? l * 2 : 2 - l * 2; const t = l < .5 ? l * 2 : 2 - l * 2;
return { return {
h: Math.round(constrainHue(h)), h: constrainHue(h),
s: Math.round(t ? s * v / t * 100 : 0), s: t ? s * v / t * 100 : 0,
l: Math.round(l * 100), l: l * 100,
a,
}; };
} }
function HWBtoHSV({h, w, b, a}) {
w = constrain(0, 100, w) / 100;
b = constrain(0, 100, b) / 100;
return {
h: constrainHue(h),
s: b === 1 ? 0 : 1 - w / (1 - b),
v: 1 - b,
a,
};
}
function HSVtoHWB({h, s, v, a}) {
return {
h: constrainHue(h),
w: (1 - s) * v * 100,
b: (1 - v) * 100,
a,
};
}
function constrain(min, max, value) {
return value < min ? min : value > max ? max : value;
}
function constrainHue(h) { function constrainHue(h) {
return h < 0 ? h % 360 + 360 : return h < 0 ? h % 360 + 360 :
h > 360 ? h % 360 : h > 360 ? h % 360 :
@ -215,7 +257,13 @@ const colorConverter = (() => {
} }
function hex2(val) { function hex2(val) {
return (val < 16 ? '0' : '') + (val >> 0).toString(16); return (val < 16 ? '0' : '') + Math.round(val).toString(16);
}
function testAt(rx, index, text) {
if (!rx) return false;
rx.lastIndex = index;
return rx.test(text);
} }
})(); })();

View File

@ -3,6 +3,7 @@
'use strict'; 'use strict';
(window.CodeMirror ? window.CodeMirror.prototype : window).colorpicker = function () { (window.CodeMirror ? window.CodeMirror.prototype : window).colorpicker = function () {
const {constrain} = colorConverter;
const cm = window.CodeMirror && this; const cm = window.CodeMirror && this;
const CSS_PREFIX = 'colorpicker-'; const CSS_PREFIX = 'colorpicker-';
const HUE_COLORS = [ const HUE_COLORS = [
@ -40,8 +41,6 @@
let /** @type {HTMLElement} */ $palette; let /** @type {HTMLElement} */ $palette;
const $inputGroups = {}; const $inputGroups = {};
const $inputs = {}; const $inputs = {};
const $rgb = {};
const $hsl = {};
const $hexLettercase = {}; const $hexLettercase = {};
const allowInputFocus = !('ontouchstart' in document) || window.innerHeight > 800; const allowInputFocus = !('ontouchstart' in document) || window.innerHeight > 800;
@ -85,6 +84,21 @@
return Object.assign(el, props); return Object.assign(el, props);
} }
const alphaPattern = /^\s*(0+\.?|0*\.\d+|0*1\.?|0*1\.0*)?\s*$/.source; const alphaPattern = /^\s*(0+\.?|0*\.\d+|0*1\.?|0*1\.0*)?\s*$/.source;
const nestedObj = (obj, key) => (obj[key] || (obj[key] = {}));
const makeNum = (type, channel, props, min, max) =>
$(['input-field', `${type}-${channel}`], [
(nestedObj($inputs, type)[channel] =
$('input', props || {tag: 'input', type: 'number', min, max, step: 1})),
$('title', channel.toUpperCase()),
]);
const ColorGroup = (type, channels) => (
$inputGroups[type] = $(['input-group', type], [
...Object.entries(channels).map(([k, v]) =>
makeNum(type, k, null, v[0], v[1])),
makeNum(type, 'a',
{tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}),
])
);
$root = $('popup', { $root = $('popup', {
oninput: setFromInputs, oninput: setFromInputs,
onkeydown: setFromKeyboard, onkeydown: setFromKeyboard,
@ -128,42 +142,9 @@
]), ]),
]), ]),
]), ]),
$inputGroups.rgb = $(['input-group', 'rgb'], [ ColorGroup('rgb', {r: [0, 255], g: [0, 255], b: [0, 255]}),
$(['input-field', 'rgb-r'], [ ColorGroup('hsl', {h: [], s: [0, 100], l: [0, 100]}),
$rgb.r = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}), ColorGroup('hwb', {h: [], w: [0, 100], b: [0, 100]}),
$('title', 'R'),
]),
$(['input-field', 'rgb-g'], [
$rgb.g = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}),
$('title', 'G'),
]),
$(['input-field', 'rgb-b'], [
$rgb.b = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}),
$('title', 'B'),
]),
$(['input-field', 'rgb-a'], [
$rgb.a = $('input', {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}),
$('title', 'A'),
]),
]),
$inputGroups.hsl = $(['input-group', 'hsl'], [
$(['input-field', 'hsl-h'], [
$hsl.h = $('input', {tag: 'input', type: 'number', step: 1}),
$('title', 'H'),
]),
$(['input-field', 'hsl-s'], [
$hsl.s = $('input', {tag: 'input', type: 'number', min: 0, max: 100, step: 1}),
$('title', 'S'),
]),
$(['input-field', 'hsl-l'], [
$hsl.l = $('input', {tag: 'input', type: 'number', min: 0, max: 100, step: 1}),
$('title', 'L'),
]),
$(['input-field', 'hsl-a'], [
$hsl.a = $('input', {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}),
$('title', 'A'),
]),
]),
$('format-change', [ $('format-change', [
$formatChangeButton = $('format-change-button', {onclick: setFromFormatElement}, '↔'), $formatChangeButton = $('format-change-button', {onclick: setFromFormatElement}, '↔'),
]), ]),
@ -180,19 +161,26 @@
}), }),
]); ]);
$inputs.hex = [$hexCode]; const inputsToObj = type => {
$inputs.rgb = [$rgb.r, $rgb.g, $rgb.b, $rgb.a]; const res = {type};
$inputs.hsl = [$hsl.h, $hsl.s, $hsl.l, $hsl.a]; for (const [k, el] of Object.entries($inputs[type])) {
const inputsToArray = inputs => inputs.map(el => parseFloat(el.value)); res[k] = parseFloat(el.value);
const inputsToHexString = () => $hexCode.value.trim(); }
const inputsToRGB = ([r, g, b, a] = inputsToArray($inputs.rgb)) => ({r, g, b, a, type: 'rgb'}); return res;
const inputsToHSL = ([h, s, l, a] = inputsToArray($inputs.hsl)) => ({h, s, l, a, type: 'hsl'}); };
Object.defineProperty($inputs.hex, 'color', {get: inputsToHexString}); for (const [key, val] of Object.entries($inputs)) {
Object.defineProperty($inputs.rgb, 'color', {get: inputsToRGB}); Object.defineProperty(val, 'color', {
Object.defineProperty($inputs.hsl, 'color', {get: inputsToHSL}); get: inputsToObj.bind(null, key),
Object.defineProperty($inputs, 'color', {get: () => $inputs[currentFormat].color}); });
}
Object.defineProperty($inputs.hex = [$hexCode], 'color', {
get: () => $hexCode.value.trim(),
});
Object.defineProperty($inputs, 'color', {
get: () => $inputs[currentFormat].color,
});
Object.defineProperty($inputs, 'colorString', { Object.defineProperty($inputs, 'colorString', {
get: () => currentFormat && colorConverter.format($inputs[currentFormat].color), get: () => currentFormat && colorConverter.format($inputs[currentFormat].color, undefined, {round: true}),
}); });
HUE_COLORS.forEach(color => Object.assign(color, colorConverter.parse(color.hex))); HUE_COLORS.forEach(color => Object.assign(color, colorConverter.parse(color.hex)));
@ -210,6 +198,7 @@
HSV = {}; HSV = {};
currentFormat = ''; currentFormat = '';
options = PUBLIC_API.options = opt; options = PUBLIC_API.options = opt;
if (opt.round !== false) opt.round = true;
prevFocusedElement = document.activeElement; prevFocusedElement = document.activeElement;
userActivity = 0; userActivity = 0;
lastOutputColor = opt.color || ''; lastOutputColor = opt.color || '';
@ -246,33 +235,19 @@
} }
function setColor(color) { function setColor(color) {
switch (typeof color) { if (typeof color === 'string') {
case 'string':
color = colorConverter.parse(color); color = colorConverter.parse(color);
break; } else if (typeof color === 'object' && color && !color.type) {
case 'object': { color = Object.assign({}, color, {type: colorConverter.guessType(color)});
const {r, g, b, a} = color;
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
color = {r, g, b, a, type: 'rgb'};
break;
} }
const {h, s, l} = color; if (!color || !color.type) {
if (!isNaN(h) && !isNaN(s) && !isNaN(l)) {
color = {h, s, l, a, type: 'hsl'};
break;
}
}
// fallthrough
default:
return false; return false;
} }
if (color) {
if (!initialized) { if (!initialized) {
init(); init();
} }
setFromColor(color); setFromColor(color);
} return true;
return Boolean(color);
} }
function getColor(type) { function getColor(type) {
@ -280,9 +255,7 @@
return; return;
} }
readCurrentColorFromRamps(); readCurrentColorFromRamps();
const color = type === 'hsl' ? const color = colorConverter.fromHSV(HSV, type);
colorConverter.HSVtoHSL(HSV) :
colorConverter.HSVtoRGB(HSV);
return type ? colorToString(color, type) : color; return type ? colorToString(color, type) : color;
} }
@ -341,7 +314,7 @@
function setFromFormatElement({shiftKey}) { function setFromFormatElement({shiftKey}) {
userActivity = performance.now(); userActivity = performance.now();
HSV.a = isNaN(HSV.a) ? 1 : HSV.a; HSV.a = isNaN(HSV.a) ? 1 : HSV.a;
const formats = ['hex', 'rgb', 'hsl']; const formats = Object.keys($inputGroups);
const dir = shiftKey ? -1 : 1; const dir = shiftKey ? -1 : 1;
const total = formats.length; const total = formats.length;
if ($inputs.colorString === $inputs.prevColorString) { if ($inputs.colorString === $inputs.prevColorString) {
@ -362,7 +335,7 @@
function setFromInputs(event) { function setFromInputs(event) {
userActivity = event ? performance.now() : userActivity; userActivity = event ? performance.now() : userActivity;
if ($inputs[currentFormat].every(validateInput)) { if (Object.values($inputs[currentFormat]).every(validateInput)) {
setFromColor($inputs.color); setFromColor($inputs.color);
} }
} }
@ -375,7 +348,7 @@
case 'PageDown': case 'PageDown':
if (!ctrl && !alt && !meta) { if (!ctrl && !alt && !meta) {
const el = document.activeElement; const el = document.activeElement;
const inputs = $inputs[currentFormat]; const inputs = Object.values($inputs[currentFormat]);
const lastInput = inputs[inputs.length - 1]; const lastInput = inputs[inputs.length - 1];
if (key === 'Tab' && shift && el === inputs[0]) { if (key === 'Tab' && shift && el === inputs[0]) {
maybeFocus(lastInput); maybeFocus(lastInput);
@ -424,8 +397,8 @@
newValue = options.hexUppercase ? newValue.toUpperCase() : newValue.toLowerCase(); newValue = options.hexUppercase ? newValue.toUpperCase() : newValue.toLowerCase();
} else if (!alt) { } else if (!alt) {
value = parseFloat(el.value); value = parseFloat(el.value);
const isHue = el === $inputs.hsl[0]; const isHue = el.title === 'H';
const isAlpha = el === $inputs[currentFormat][3]; const isAlpha = el === $inputs[currentFormat].a;
const isRGB = currentFormat === 'rgb'; const isRGB = currentFormat === 'rgb';
const min = isHue ? -360 : 0; const min = isHue ? -360 : 0;
const max = isHue ? 360 : isAlpha ? 1 : isRGB ? 255 : 100; const max = isHue ? 360 : isAlpha ? 1 : isRGB ? 255 : 100;
@ -446,7 +419,7 @@
} }
function validateInput(el) { function validateInput(el) {
const isAlpha = el === $inputs[currentFormat][3]; const isAlpha = el === $inputs[currentFormat].a;
let isValid = (isAlpha || el.value.trim()) && el.checkValidity(); let isValid = (isAlpha || el.value.trim()) && el.checkValidity();
if (!isAlpha && !isValid && currentFormat === 'rgb') { if (!isAlpha && !isValid && currentFormat === 'rgb') {
isValid = parseAs(el, parseInt); isValid = parseAs(el, parseInt);
@ -464,9 +437,7 @@
function setFromColor(color) { function setFromColor(color) {
color = typeof color === 'string' ? colorConverter.parse(color) : color; color = typeof color === 'string' ? colorConverter.parse(color) : color;
color = color || colorConverter.parse('#f00'); color = color || colorConverter.parse('#f00');
const newHSV = color.type === 'hsl' ? const newHSV = colorConverter.toHSV(color);
colorConverter.HSLtoHSV(color) :
colorConverter.RGBtoHSV(color);
if (Object.entries(newHSV).every(([k, v]) => v === HSV[k] || Math.abs(v - HSV[k]) < 1e-3)) { if (Object.entries(newHSV).every(([k, v]) => v === HSV[k] || Math.abs(v - HSV[k]) < 1e-3)) {
return; return;
} }
@ -488,7 +459,7 @@
} }
} }
$inputGroups[format].dataset.active = ''; $inputGroups[format].dataset.active = '';
maybeFocus($inputs[format][0]); maybeFocus(Object.values($inputs[format])[0]);
currentFormat = format; currentFormat = format;
} }
@ -510,25 +481,13 @@
} }
function renderInputs() { function renderInputs() {
const rgb = colorConverter.HSVtoRGB(HSV); const rgb = colorConverter.fromHSV(HSV, 'rgb');
switch (currentFormat) { if (currentFormat === 'hex') {
case 'hex':
rgb.a = HSV.a;
$hexCode.value = colorToString(rgb, 'hex'); $hexCode.value = colorToString(rgb, 'hex');
break; } else {
case 'rgb': { for (const [k, v] of Object.entries(colorConverter.fromHSV(HSV, currentFormat))) {
$rgb.r.value = rgb.r; const el = $inputs[currentFormat][k];
$rgb.g.value = rgb.g; if (el) el.value = k === 'a' ? alphaToString() || 1 : Math.round(v);
$rgb.b.value = rgb.b;
$rgb.a.value = alphaToString() || 1;
break;
}
case 'hsl': {
const {h, s, l} = colorConverter.HSVtoHSL(HSV);
$hsl.h.value = h;
$hsl.s.value = s;
$hsl.l.value = l;
$hsl.a.value = alphaToString() || 1;
} }
} }
$swatch.style.backgroundColor = colorToString(rgb, 'rgb'); $swatch.style.backgroundColor = colorToString(rgb, 'rgb');
@ -704,7 +663,7 @@
} }
if ( if (
userActivity && userActivity &&
$inputs[currentFormat].every(el => el.checkValidity()) Object.values($inputs[currentFormat]).every(el => el.checkValidity())
) { ) {
lastOutputColor = colorString.replace(/\b0\./g, '.'); lastOutputColor = colorString.replace(/\b0\./g, '.');
if (isCallable) { if (isCallable) {
@ -770,7 +729,7 @@
//region Color conversion utilities //region Color conversion utilities
function colorToString(color, type = currentFormat) { function colorToString(color, type = currentFormat) {
return colorConverter.format(color, type, options.hexUppercase); return colorConverter.format(color, type, options);
} }
function alphaToString(a = HSV.a) { function alphaToString(a = HSV.a) {
@ -778,9 +737,7 @@
} }
function currentColorToString(format = currentFormat, alpha = HSV.a) { function currentColorToString(format = currentFormat, alpha = HSV.a) {
const converted = format === 'hsl' ? const converted = colorConverter.fromHSV(HSV, format);
colorConverter.HSVtoHSL(HSV) :
colorConverter.HSVtoRGB(HSV);
converted.a = isNaN(alpha) || alpha === 1 ? undefined : alpha; converted.a = isNaN(alpha) || alpha === 1 ? undefined : alpha;
return colorToString(converted, format); return colorToString(converted, format);
} }
@ -879,10 +836,6 @@
return bgLuma < .5 ? 'dark' : 'light'; return bgLuma < .5 ? 'dark' : 'light';
} }
function constrain(min, max, value) {
return value < min ? min : value > max ? max : value;
}
function parseAs(el, parser) { function parseAs(el, parser) {
const num = parser(el.value); const num = parser(el.value);
if (!isNaN(num) && if (!isNaN(num) &&

View File

@ -8,50 +8,27 @@
const COLORVIEW_CLASS = 'colorview'; const COLORVIEW_CLASS = 'colorview';
const COLORVIEW_SWATCH_CLASS = COLORVIEW_CLASS + '-swatch'; const COLORVIEW_SWATCH_CLASS = COLORVIEW_CLASS + '-swatch';
const COLORVIEW_SWATCH_CSS = `--${COLORVIEW_SWATCH_CLASS}:`; const COLORVIEW_SWATCH_CSS = `--${COLORVIEW_SWATCH_CLASS}:`;
const CLOSE_POPUP_EVENT = 'close-colorpicker-popup'; const CLOSE_POPUP_EVENT = 'close-colorpicker-popup';
const RXS_NUM = /\s*([+-]?(?:\d+\.?\d*|\d*\.\d+))(?:e[+-]?\d+)?/.source; const {RX_COLOR, testAt} = colorConverter;
const RX_COLOR = { const RX_UNSUPPORTED = (s => s && new RegExp(s))([
hex: /#(?:[a-f\d]{3}(?:[a-f\d](?:[a-f\d]{2}){0,2})?)\b/iy,
rgb: new RegExp([
// num, num, num [ , num_or_pct]?
// pct, pct, pct [ , num_or_pct]?
`^((${RXS_NUM}\\s*(,|$)){3}|(${RXS_NUM}%\\s*(,|$)){3})(${RXS_NUM}%?)?\\s*$`,
// num num num [ / num_or_pct]?
// pct pct pct [ / num_or_pct]?
`^((${RXS_NUM}\\s*(\\s|$)){3}|(${RXS_NUM}%\\s*(\\s|$)){3})(/${RXS_NUM}%?)?\\s*$`,
].join('|'), 'iy'),
hsl: new RegExp([
// num_or_angle, pct, pct [ , num_or_pct]?
`^(${RXS_NUM}(|deg|g?rad|turn)\\s*),(${RXS_NUM}%\\s*(,|$)){2}(${RXS_NUM}%?)?\\s*$`,
// num_or_angle pct pct [ / num_or_pct]?
`^(${RXS_NUM}(|deg|g?rad|turn)\\s*)\\s(${RXS_NUM}%\\s*(\\s|$)){2}(/${RXS_NUM}%?)?\\s*$`,
].join('|'), 'iy'),
unsupported: new RegExp([
!CSS.supports('color', '#abcd') && /#(.{4}){1,2}$/, !CSS.supports('color', '#abcd') && /#(.{4}){1,2}$/,
!CSS.supports('color', 'hwb(1 0% 0%)') && /^hwb\(/,
!CSS.supports('color', 'rgb(1e2,0,0)') && /\de/, !CSS.supports('color', 'rgb(1e2,0,0)') && /\de/,
!CSS.supports('color', 'rgb(1.5,0,0)') && /^rgba?\((([^,]+,){0,2}[^,]*\.|(\s*\S+\s+){0,2}\S*\.)/, !CSS.supports('color', 'rgb(1.5,0,0)') &&
/^rgba?\((([^,]+,){0,2}[^,]*\.|(\s*\S+\s+){0,2}\S*\.)/,
!CSS.supports('color', 'rgb(1,2,3,.5)') && /[^a]\(([^,]+,){3}/, !CSS.supports('color', 'rgb(1,2,3,.5)') && /[^a]\(([^,]+,){3}/,
!CSS.supports('color', 'rgb(1,2,3,50%)') && /\((([^,]+,){3}|(\s*\S+[\s/]+){3}).*?%/, !CSS.supports('color', 'rgb(1,2,3,50%)') && /\((([^,]+,){3}|(\s*\S+[\s/]+){3}).*?%/,
!CSS.supports('color', 'rgb(1 2 3 / 1)') && /^[^,]+$/, !CSS.supports('color', 'rgb(1 2 3 / 1)') && /^[^,]+$/,
!CSS.supports('color', 'hsl(1turn, 2%, 3%)') && /deg|g?rad|turn/, !CSS.supports('color', 'hsl(1turn, 2%, 3%)') && /deg|g?rad|turn/,
].filter(Boolean).map(rx => rx.source).join('|') || '^$', 'i'), ].filter(Boolean).map(rx => rx.source).join('|'));
};
if (RX_COLOR.unsupported.source === '^$') {
RX_COLOR.unsupported = null;
}
const RX_DETECT = new RegExp('(^|[\\s(){}[\\]:,/"=])' + const RX_DETECT = new RegExp('(^|[\\s(){}[\\]:,/"=])' +
'(' + '(' +
RX_COLOR.hex.source + '|' + RX_COLOR.hex.source + '|' +
'(?:rgb|hsl)a?(?=\\()|(?:' + [...colorConverter.NAMED_COLORS.keys()].join('|') + ')' + '(?:(?:rgb|hsl)a?|hwb)(?=\\()|(?:' + [...colorConverter.NAMED_COLORS.keys()].join('|') + ')' +
'(?=[\\s;(){}[\\]/"!]|$)' + '(?=[\\s;(){}[\\]/"!]|$)' +
')', 'gi'); ')', 'gi');
const RX_DETECT_FUNC = /(rgb|hsl)a?\(/iy; const RX_DETECT_FUNC = /((rgb|hsl)a?|hwb)\(/iy;
const RX_COMMENT = /\/\*([^*]|\*(?!\/))*(\*\/|$)/g; const RX_COMMENT = /\/\*([^*]|\*(?!\/))*(\*\/|$)/g;
const SPACE1K = ' '.repeat(1000); const SPACE1K = ' '.repeat(1000);
@ -439,7 +416,7 @@
function getSafeColorValue() { function getSafeColorValue() {
if (isHex && color.length !== 5 && color.length !== 9) return color; if (isHex && color.length !== 5 && color.length !== 9) return color;
if (!RX_COLOR.unsupported || !RX_COLOR.unsupported.test(color)) return color; if (!RX_UNSUPPORTED || !RX_UNSUPPORTED.test(color)) return color;
const value = colorConverter.parse(color); const value = colorConverter.parse(color);
return colorConverter.format(value, 'rgb'); return colorConverter.format(value, 'rgb');
} }
@ -710,14 +687,6 @@
setTimeout(() => el.remove(), DURATION_SEC * 1000); setTimeout(() => el.remove(), DURATION_SEC * 1000);
} }
function testAt(rx, index, text) {
if (!rx) return false;
rx.lastIndex = index;
return rx.test(text);
}
function getStyleAtPos({ function getStyleAtPos({
line, line,
styles = this.getLineHandle(line).styles, styles = this.getLineHandle(line).styles,

View File

@ -83,7 +83,7 @@ const BUILDERS = Object.assign(Object.create(null), {
if (alpha) delete value.a; if (alpha) delete value.a;
const isRgb = isUsoRgb || value.type === 'rgb' || value.a != null && value.a !== 1; const isRgb = isUsoRgb || value.type === 'rgb' || value.a != null && value.a !== 1;
const usoMode = isUsoRgb || !isRgb; const usoMode = isUsoRgb || !isRgb;
value = colorConverter.format(value, isRgb ? 'rgb' : 'hex', undefined, usoMode); value = colorConverter.format(value, isRgb ? 'rgb' : 'hex', {usoMode});
} }
return value; return value;
case 'dropdown': case 'dropdown':