UNPKG

buefy

Version:

Lightweight UI components for Vue.js (v3) based on Bulma

508 lines (440 loc) 15 kB
export const colorChannels = ['red', 'green', 'blue', 'alpha'] as const export type ColorChannel = typeof colorChannels[number] export type Rgba = { [C in ColorChannel]: number } export type Rgb = Omit<Rgba, 'alpha'> export const hslChannels = ['hue', 'saturation', 'lightness'] as const export type HslChannel = typeof hslChannels[number] export type CapitalizedHslChannel = Capitalize<HslChannel> export type Hsla = { [C in HslChannel]: number } export type Hsl = Omit<Hsla, 'alpha'> type ChannelTuple = [number, number, number, number] export const colorsNammed = { transparent: '#00000000', black: '#000000', silver: '#c0c0c0', gray: '#808080', white: '#ffffff', maroon: '#800000', red: '#ff0000', purple: '#800080', fuchsia: '#ff00ff', green: '#008000', lime: '#00ff00', olive: '#808000', yellow: '#ffff00', navy: '#000080', blue: '#0000ff', teal: '#008080', aqua: '#00ffff', orange: '#ffa500', aliceblue: '#f0f8ff', antiquewhite: '#faebd7', aquamarine: '#7fffd4', azure: '#f0ffff', beige: '#f5f5dc', bisque: '#ffe4c4', blanchedalmond: '#ffebcd', blueviolet: '#8a2be2', brown: '#a52a2a', burlywood: '#deb887', cadetblue: '#5f9ea0', chartreuse: '#7fff00', chocolate: '#d2691e', coral: '#ff7f50', cornflowerblue: '#6495ed', cornsilk: '#fff8dc', crimson: '#dc143c', cyan: '#00ffff', darkblue: '#00008b', darkcyan: '#008b8b', darkgoldenrod: '#b8860b', darkgray: '#a9a9a9', darkgreen: '#006400', darkgrey: '#a9a9a9', darkkhaki: '#bdb76b', darkmagenta: '#8b008b', darkolivegreen: '#556b2f', darkorange: '#ff8c00', darkorchid: '#9932cc', darkred: '#8b0000', darksalmon: '#e9967a', darkseagreen: '#8fbc8f', darkslateblue: '#483d8b', darkslategray: '#2f4f4f', darkslategrey: '#2f4f4f', darkturquoise: '#00ced1', darkviolet: '#9400d3', deeppink: '#ff1493', deepskyblue: '#00bfff', dimgray: '#696969', dimgrey: '#696969', dodgerblue: '#1e90ff', firebrick: '#b22222', floralwhite: '#fffaf0', forestgreen: '#228b22', gainsboro: '#dcdcdc', ghostwhite: '#f8f8ff', gold: '#ffd700', goldenrod: '#daa520', greenyellow: '#adff2f', grey: '#808080', honeydew: '#f0fff0', hotpink: '#ff69b4', indianred: '#cd5c5c', indigo: '#4b0082', ivory: '#fffff0', khaki: '#f0e68c', lavender: '#e6e6fa', lavenderblush: '#fff0f5', lawngreen: '#7cfc00', lemonchiffon: '#fffacd', lightblue: '#add8e6', lightcoral: '#f08080', lightcyan: '#e0ffff', lightgoldenrodyellow: '#fafad2', lightgray: '#d3d3d3', lightgreen: '#90ee90', lightgrey: '#d3d3d3', lightpink: '#ffb6c1', lightsalmon: '#ffa07a', lightseagreen: '#20b2aa', lightskyblue: '#87cefa', lightslategray: '#778899', lightslategrey: '#778899', lightsteelblue: '#b0c4de', lightyellow: '#ffffe0', limegreen: '#32cd32', linen: '#faf0e6', magenta: '#ff00ff', mediumaquamarine: '#66cdaa', mediumblue: '#0000cd', mediumorchid: '#ba55d3', mediumpurple: '#9370db', mediumseagreen: '#3cb371', mediumslateblue: '#7b68ee', mediumspringgreen: '#00fa9a', mediumturquoise: '#48d1cc', mediumvioletred: '#c71585', midnightblue: '#191970', mintcream: '#f5fffa', mistyrose: '#ffe4e1', moccasin: '#ffe4b5', navajowhite: '#ffdead', oldlace: '#fdf5e6', olivedrab: '#6b8e23', orangered: '#ff4500', orchid: '#da70d6', palegoldenrod: '#eee8aa', palegreen: '#98fb98', paleturquoise: '#afeeee', palevioletred: '#db7093', papayawhip: '#ffefd5', peachpuff: '#ffdab9', peru: '#cd853f', pink: '#ffc0cb', plum: '#dda0dd', powderblue: '#b0e0e6', rosybrown: '#bc8f8f', royalblue: '#4169e1', saddlebrown: '#8b4513', salmon: '#fa8072', sandybrown: '#f4a460', seagreen: '#2e8b57', seashell: '#fff5ee', sienna: '#a0522d', skyblue: '#87ceeb', slateblue: '#6a5acd', slategray: '#708090', slategrey: '#708090', snow: '#fffafa', springgreen: '#00ff7f', steelblue: '#4682b4', tan: '#d2b48c', thistle: '#d8bfd8', tomato: '#ff6347', turquoise: '#40e0d0', violet: '#ee82ee', wheat: '#f5deb3', whitesmoke: '#f5f5f5', yellowgreen: '#9acd32', rebeccapurple: '#663399' } as const export type ColorName = keyof typeof colorsNammed export class ColorTypeError extends Error { constructor() { super('ColorTypeError: type must be hex(a), rgb(a) or hsl(a)') } } class Color { // @ts-expect-error - TypeScript failed to inter the initialization of this property $channels: Uint8Array // Since getters and setters for the color channels, e.g., "alpha", are // dynamically defined with `Object.defineProperty` in the constructor, we // cannot write property declarations inside the class body. Instead, we // augment the `Color` class with an ambient module declared in `color.ts`. // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(...args: any[]) { if (args.length > 0) { return Color.parse(...args) } this.$channels = new Uint8Array(colorChannels.length) } get red(): number { return this.$channels[0] } set red(byte: number) { if (!Number.isNaN(byte / 1)) { this.$channels[0] = Math.min(255, Math.max(0, byte)) } } get green(): number { return this.$channels[1] } set green(byte: number) { if (!Number.isNaN(byte / 1)) { this.$channels[1] = Math.min(255, Math.max(0, byte)) } } get blue(): number { return this.$channels[2] } set blue(byte: number) { if (!Number.isNaN(byte / 1)) { this.$channels[2] = Math.min(255, Math.max(0, byte)) } } get alpha(): number { return this.$channels[3] } set alpha(byte: number) { if (!Number.isNaN(byte / 1)) { this.$channels[3] = Math.min(255, Math.max(0, byte)) } } get hue(): number { return this.getHue() } set hue(value: number) { if (!Number.isNaN(value / 1)) { this.setHue(value) } } get saturation(): number { return this.getSaturation() } set saturation(value: number) { if (!Number.isNaN(value / 1)) { this.setSaturation(value) } } get lightness(): number { return this.getLightness() } set lightness(value: number) { if (!Number.isNaN(value / 1)) { this.setLightness(value) } } getHue() { const [red, green, blue] = Array.from(this.$channels).map((c) => c / 255) const [min, max] = [Math.min(red, green, blue), Math.max(red, green, blue)] const delta = max - min let hue = 0 if (delta === 0) { return hue } if (red === max) { hue = ((green - blue) / delta) % 6 } else if (green === max) { hue = (blue - red) / delta + 2 } else { hue = (red - green) / delta + 4 } hue *= 60 while (hue !== -Infinity && hue < 0) hue += 360 return Math.round(hue % 360) } setHue(value: number) { const color = Color.fromHSL(value, this.saturation, this.lightness, this.alpha / 255) for (let i = 0; i < this.$channels.length; i++) { this.$channels[i] = Number(color.$channels[i]) } } getSaturation() { const [red, green, blue] = Array.from(this.$channels).map((c) => c / 255) const [min, max] = [Math.min(red, green, blue), Math.max(red, green, blue)] const delta = max - min return delta !== 0 ? Math.round(delta / (1 - Math.abs(2 * this.lightness - 1)) * 100) / 100 : 0 } setSaturation(value: number) { const color = Color.fromHSL(this.hue, value, this.lightness, this.alpha / 255) colorChannels.forEach((_, i) => (this.$channels[i] = color.$channels[i])) } getLightness() { const [red, green, blue] = Array.from(this.$channels).map((c) => c / 255) const [min, max] = [Math.min(red, green, blue), Math.max(red, green, blue)] return Math.round((max + min) / 2 * 100) / 100 } setLightness(value: number) { const color = Color.fromHSL(this.hue, this.lightness, value, this.alpha / 255) colorChannels.forEach((_, i) => (this.$channels[i] = color.$channels[i])) } clone() { const color = new Color() colorChannels.forEach((_, i) => (color.$channels[i] = this.$channels[i])) return color } toString(type = 'hex') { switch (String(type).toLowerCase()) { case 'hex': return '#' + colorChannels.slice(0, 3) .map((channel) => this[channel].toString(16).padStart(2, '0')) .join('') case 'hexa': return '#' + colorChannels .map((channel) => this[channel].toString(16).padStart(2, '0')) .join('') case 'rgb': return `rgb(${this.red}, ${this.green}, ${this.blue})` case 'rgba': return `rgba(${this.red}, ${this.green}, ${this.blue}, ${Math.round(this.alpha / 2.55) / 100})` case 'hsl': return `hsl(${Math.round(this.hue)}deg, ${Math.round(this.saturation * 100)}%, ${Math.round(this.lightness * 100)}%)` case 'hsla': return `hsla(${Math.round(this.hue)}deg, ${Math.round(this.saturation * 100)}%, ${Math.round(this.lightness * 100)}%, ${Math.round(this.alpha / 2.55) / 100})` default: throw new ColorTypeError() } } get [Symbol.toStringTag]() { return this.toString('hex') } // eslint-disable-next-line @typescript-eslint/no-explicit-any static parse(...args: any[]): Color { if (typeof args[0] === 'object') { return Color.parseObject(args[0]) } else if (args.every((arg) => !Number.isNaN(arg / 1))) { const color = new Color() if (args.length > 3) { color.red = args[0] color.green = args[1] color.blue = args[2] if (args[3]) { color.alpha = args[3] } } else if (args.length === 1) { const index = Number(args[0]) return Color.parseIndex(index, index > 2 ** 24 ? 3 : 4) } } else if (typeof args[0] === 'string') { let match = null if (typeof colorsNammed[args[0].toLowerCase() as ColorName] === 'string') { return Color.parseHex(colorsNammed[args[0].toLowerCase() as ColorName]) } else if ((match = args[0].match(/^(#|&h|0x)?(([a-f0-9]{3,4}){1,2})$/i)) !== null) { return Color.parseHex(match[2]) } else if ((match = args[0].match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(\s*,\s*(\d*\.?\d+))?\s*\)$/i)) !== null) { const channels = [ match[1], match[2], match[3], typeof match[5] !== 'undefined' ? match[5] : 1 ] return Color.fromRGB(...(channels.map((value) => Number(value)) as ChannelTuple)) } else if ((args[0].match(/^(h(sl|wb)a?|lab|color|cmyk)\(/i))) { throw new Error('Color expression not implemented yet') } } throw new Error('Invalid color expression') } static parseObject(object: unknown) { const color = new Color() if (object === null || typeof object !== 'object') { return color } if (Color.isColor(object)) { return object.clone() } colorChannels.forEach((channel) => { if (!Number.isNaN((object as Rgba)[channel])) { color[channel] = (object as Rgba)[channel] } }) return color } static parseHex(hex: string) { if (typeof hex !== 'string') { throw new Error('Hex expression must be a string') } hex = hex.trim().replace(/^(0x|&h|#)/i, '') if (hex.length === 3 || hex.length === 4) { hex = hex.split('') .map((c) => c.repeat(2)) .join('') } if (!(hex.length === 6 || hex.length === 8)) { throw new Error('Incorrect Hex expression length') } const chans = hex.split(/(..)/) .filter((value) => value) .map((value) => Number.parseInt(value, 16)) if (typeof chans[3] === 'number') { chans[3] /= 255 } return Color.fromRGB(...(chans as ChannelTuple)) } static parseIndex(value: number, channels: number = 3) { const color = new Color() for (let i = 0; i < 4; i++) { color[colorChannels[i]] = (value >> ((channels - i) * 8)) && 0xff } return color } static fromRGB(red: number, green: number, blue: number, alpha: number = 1) { if ([red, green, blue, alpha].some((arg) => Number.isNaN(arg / 1))) { throw new Error('Invalid arguments') } alpha *= 255 const color = new Color() ;[red, green, blue, alpha].forEach((value, index) => { color[colorChannels[index]] = value }) return color } static fromHSL(hue: number, saturation: number, lightness: number, alpha: number = 1) { if ([hue, saturation, lightness, alpha].some((arg) => Number.isNaN(arg))) { throw new Error('Invalid arguments') } while (hue < 0 && hue !== -Infinity) hue += 360 hue = hue % 360 saturation = Math.max(0, Math.min(1, saturation)) lightness = Math.max(0, Math.min(1, lightness)) alpha = Math.max(0, Math.min(1, alpha)) const c = (1 - Math.abs(2 * lightness - 1)) * saturation const x = c * (1 - Math.abs(hue / 60 % 2 - 1)) const m = lightness - c / 2 const [r, g, b] = hue < 60 ? [c, x, 0] : hue < 120 ? [x, c, 0] : hue < 180 ? [0, c, x] : hue < 240 ? [0, x, c] : hue < 300 ? [x, 0, c] : [c, 0, x] return Color.fromRGB((r + m) * 255, (g + m) * 255, (b + m) * 255, alpha) } static isColor(arg: unknown): arg is Color { return arg instanceof Color } } export default Color