less
Version:
Leaner CSS
286 lines (252 loc) • 8.55 kB
JavaScript
// @ts-check
import Node from './node.js';
import colors from '../data/colors.js';
/** @import { EvalContext, CSSOutput } from './node.js' */
//
// RGB Colors - #ff0014, #eee
//
class Color extends Node {
get type() { return 'Color'; }
/**
* @param {number[] | string} rgb
* @param {number} [a]
* @param {string} [originalForm]
*/
constructor(rgb, a, originalForm) {
super();
const self = this;
//
// The end goal here, is to parse the arguments
// into an integer triplet, such as `128, 255, 0`
//
// This facilitates operations and conversions.
//
if (Array.isArray(rgb)) {
/** @type {number[]} */
this.rgb = rgb;
} else if (rgb.length >= 6) {
/** @type {number[]} */
this.rgb = [];
/** @type {RegExpMatchArray} */ (rgb.match(/.{2}/g)).map(function (c, i) {
if (i < 3) {
self.rgb.push(parseInt(c, 16));
} else {
self.alpha = (parseInt(c, 16)) / 255;
}
});
} else {
/** @type {number[]} */
this.rgb = [];
rgb.split('').map(function (c, i) {
if (i < 3) {
self.rgb.push(parseInt(c + c, 16));
} else {
self.alpha = (parseInt(c + c, 16)) / 255;
}
});
}
/** @type {number} */
if (typeof this.alpha === 'undefined') {
this.alpha = (typeof a === 'number') ? a : 1;
}
if (typeof originalForm !== 'undefined') {
this.value = originalForm;
}
}
luma() {
let r = this.rgb[0] / 255, g = this.rgb[1] / 255, b = this.rgb[2] / 255;
r = (r <= 0.03928) ? r / 12.92 : Math.pow(((r + 0.055) / 1.055), 2.4);
g = (g <= 0.03928) ? g / 12.92 : Math.pow(((g + 0.055) / 1.055), 2.4);
b = (b <= 0.03928) ? b / 12.92 : Math.pow(((b + 0.055) / 1.055), 2.4);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add(this.toCSS(context));
}
/**
* @param {EvalContext} context
* @param {boolean} [doNotCompress]
* @returns {string}
*/
toCSS(context, doNotCompress) {
const compress = context && context.compress && !doNotCompress;
let color;
let alpha;
/** @type {string | undefined} */
let colorFunction;
/** @type {(string | number)[]} */
let args = [];
// `value` is set if this color was originally
// converted from a named color string so we need
// to respect this and try to output named color too.
alpha = this.fround(context, this.alpha);
if (this.value) {
if (/** @type {string} */ (this.value).indexOf('rgb') === 0) {
if (alpha < 1) {
colorFunction = 'rgba';
}
} else if (/** @type {string} */ (this.value).indexOf('hsl') === 0) {
if (alpha < 1) {
colorFunction = 'hsla';
} else {
colorFunction = 'hsl';
}
} else {
return /** @type {string} */ (this.value);
}
} else {
if (alpha < 1) {
colorFunction = 'rgba';
}
}
switch (colorFunction) {
case 'rgba':
args = this.rgb.map(function (c) {
return clamp(Math.round(c), 255);
}).concat(clamp(alpha, 1));
break;
case 'hsla':
args.push(clamp(alpha, 1));
// eslint-disable-next-line no-fallthrough
case 'hsl':
color = this.toHSL();
args = [
this.fround(context, color.h),
`${this.fround(context, color.s * 100)}%`,
`${this.fround(context, color.l * 100)}%`
].concat(args);
}
if (colorFunction) {
// Values are capped between `0` and `255`, rounded and zero-padded.
return `${colorFunction}(${args.join(`,${compress ? '' : ' '}`)})`;
}
color = this.toRGB();
if (compress) {
const splitcolor = color.split('');
// Convert color to short format
if (splitcolor[1] === splitcolor[2] && splitcolor[3] === splitcolor[4] && splitcolor[5] === splitcolor[6]) {
color = `#${splitcolor[1]}${splitcolor[3]}${splitcolor[5]}`;
}
}
return color;
}
//
// Operations have to be done per-channel, if not,
// channels will spill onto each other. Once we have
// our result, in the form of an integer triplet,
// we create a new Color node to hold the result.
//
/**
* @param {EvalContext} context
* @param {string} op
* @param {Color} other
*/
operate(context, op, other) {
const rgb = new Array(3);
const alpha = this.alpha * (1 - other.alpha) + other.alpha;
for (let c = 0; c < 3; c++) {
rgb[c] = this._operate(context, op, this.rgb[c], other.rgb[c]);
}
return new Color(rgb, alpha);
}
toRGB() {
return toHex(this.rgb);
}
toHSL() {
const r = this.rgb[0] / 255, g = this.rgb[1] / 255, b = this.rgb[2] / 255, a = this.alpha;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
/** @type {number} */
let h;
let s;
const l = (max + min) / 2;
const d = max - min;
if (max === min) {
h = s = 0;
} else {
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
/** @type {number} */ (h) /= 6;
}
return { h: /** @type {number} */ (h) * 360, s, l, a };
}
// Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
toHSV() {
const r = this.rgb[0] / 255, g = this.rgb[1] / 255, b = this.rgb[2] / 255, a = this.alpha;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
/** @type {number} */
let h;
let s;
const v = max;
const d = max - min;
if (max === 0) {
s = 0;
} else {
s = d / max;
}
if (max === min) {
h = 0;
} else {
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
/** @type {number} */ (h) /= 6;
}
return { h: /** @type {number} */ (h) * 360, s, v, a };
}
toARGB() {
return toHex([this.alpha * 255].concat(this.rgb));
}
/**
* @param {Node & { rgb?: number[], alpha?: number }} x
* @returns {0 | undefined}
*/
compare(x) {
return (x.rgb &&
x.rgb[0] === this.rgb[0] &&
x.rgb[1] === this.rgb[1] &&
x.rgb[2] === this.rgb[2] &&
x.alpha === this.alpha) ? 0 : undefined;
}
/** @param {string} keyword */
static fromKeyword(keyword) {
/** @type {Color | undefined} */
let c;
const key = keyword.toLowerCase();
// eslint-disable-next-line no-prototype-builtins
if (colors.hasOwnProperty(key)) {
c = new Color(/** @type {string} */ (colors[/** @type {keyof typeof colors} */ (key)]).slice(1));
}
else if (key === 'transparent') {
c = new Color([0, 0, 0], 0);
}
if (c) {
c.value = keyword;
return c;
}
}
}
/**
* @param {number} v
* @param {number} max
*/
function clamp(v, max) {
return Math.min(Math.max(v, 0), max);
}
/** @param {number[]} v */
function toHex(v) {
return `#${v.map(function (c) {
c = clamp(Math.round(c), 255);
return (c < 16 ? '0' : '') + c.toString(16);
}).join('')}`;
}
export default Color;