Last active
July 30, 2020 15:41
-
-
Save marcelbeumer/d6cfc65644ed4c6c235592c3d960a425 to your computer and use it in GitHub Desktop.
Super minimal color util lib
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Based on: | |
// https://github.com/styled-components/polished | |
// https://github.com/bgrins/TinyColor | |
export interface Rgba { | |
r: number; | |
g: number; | |
b: number; | |
a: number; | |
} | |
export interface Hsla { | |
h: number; | |
s: number; | |
l: number; | |
a: number; | |
} | |
export type ColorInput = string | Rgba | Hsla; | |
const hexRegex = /^#[a-fA-F0-9]{6}$/; | |
const hexRgbaRegex = /^#[a-fA-F0-9]{8}$/; | |
const rgbRegex = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i; | |
const rgbaRegex = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([-+]?[0-9]*[.]?[0-9]+)\s*\)$/i; | |
const hslRegex = /^hsl\(\s*(\d{0,3}[.]?[0-9]+)\s*,\s*(\d{1,3}[.]?[0-9]?)%\s*,\s*(\d{1,3}[.]?[0-9]?)%\s*\)$/i; | |
const hslaRegex = /^hsla\(\s*(\d{0,3}[.]?[0-9]+)\s*,\s*(\d{1,3}[.]?[0-9]?)%\s*,\s*(\d{1,3}[.]?[0-9]?)%\s*,\s*([-+]?[0-9]*[.]?[0-9]+)\s*\)$/i; | |
const round = Math.round; | |
const minMax = (min: number, max: number, value: number): number => | |
Math.max(min, Math.min(max, value)); | |
function hueToRgb(p: number, q: number, t: number): number { | |
if (t < 0) t += 1; | |
if (t > 1) t -= 1; | |
if (t < 1 / 6) return p + (q - p) * 6 * t; | |
if (t < 1 / 2) return q; | |
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; | |
return p; | |
} | |
export const isHsla = (o: unknown): o is Hsla => | |
typeof (o as Hsla).h === "number" && | |
typeof (o as Hsla).s === "number" && | |
typeof (o as Hsla).l === "number" && | |
typeof (o as Hsla).a === "number"; | |
export const isRgba = (o: unknown): o is Rgba => | |
typeof (o as Rgba).r === "number" && | |
typeof (o as Rgba).g === "number" && | |
typeof (o as Rgba).b === "number" && | |
typeof (o as Rgba).a === "number"; | |
export function cssToRgba(color: string): Rgba { | |
const str = String(color).trim(); | |
const isHexStr = hexRegex.test(str); | |
const isHexRgbaStr = !isHexStr && hexRgbaRegex.test(str); | |
if (isHexStr || isHexRgbaStr) { | |
return { | |
r: parseInt(`${str[1]}${str[2]}`, 16), | |
g: parseInt(`${str[3]}${str[4]}`, 16), | |
b: parseInt(`${str[5]}${str[6]}`, 16), | |
a: isHexRgbaStr | |
? parseFloat((parseInt(`${str[7]}${str[8]}`, 16) / 255).toFixed(2)) | |
: 1, | |
}; | |
} | |
const rgbMatch = str.match(rgbRegex); | |
if (rgbMatch) { | |
return { | |
r: parseInt(rgbMatch[1], 10), | |
g: parseInt(rgbMatch[2], 10), | |
b: parseInt(rgbMatch[3], 10), | |
a: 1, | |
}; | |
} | |
const rgbaMatch = str.match(rgbaRegex); | |
if (rgbaMatch) { | |
return { | |
r: parseInt(rgbaMatch[1], 10), | |
g: parseInt(rgbaMatch[2], 10), | |
b: parseInt(rgbaMatch[3], 10), | |
a: parseFloat(rgbaMatch[4]), | |
}; | |
} | |
const hslMatch = hslRegex.exec(str); | |
if (hslMatch) { | |
const h = parseInt(hslMatch[1], 10); | |
const s = parseInt(hslMatch[2], 10) / 100; | |
const l = parseInt(hslMatch[3], 10) / 100; | |
return hslaToRgba({ h, s, l, a: 1 }); | |
} | |
const hslaMatch = hslaRegex.exec(str); | |
if (hslaMatch) { | |
const h = parseInt(hslaMatch[1], 10); | |
const s = parseInt(hslaMatch[2], 10) / 100; | |
const l = parseInt(hslaMatch[3], 10) / 100; | |
const a = parseFloat(hslaMatch[4]); | |
return hslaToRgba({ h, s, l, a }); | |
} | |
throw new Error(`Unrecognized css color value: ${str}`); | |
} | |
export function rgbaToHsla(rgba: Rgba): Hsla { | |
const r = rgba.r / 255; | |
const g = rgba.g / 255; | |
const b = rgba.b / 255; | |
const a = rgba.a; | |
const max = Math.max(r, g, b); | |
const min = Math.min(r, g, b); | |
const l = (max + min) / 2; | |
// achromatic | |
if (max === min) return { h: 0, s: 0, l, a }; | |
let h: number; | |
const d = max - min; | |
const 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; | |
default: | |
h = (r - g) / d + 4; | |
break; | |
} | |
h *= 60; | |
return { h, s, l, a }; | |
} | |
export function hslaToRgba(hsla: Hsla): Rgba { | |
let r: number, g: number, b: number; | |
const h = hsla.h / 360; | |
const { s, l, a } = hsla; | |
if (s === 0) { | |
r = g = b = l; // achromatic | |
} else { | |
var q = l < 0.5 ? l * (1 + s) : l + s - l * s; | |
var p = 2 * l - q; | |
r = hueToRgb(p, q, h + 1 / 3); | |
g = hueToRgb(p, q, h); | |
b = hueToRgb(p, q, h - 1 / 3); | |
} | |
return { r: r * 255, g: g * 255, b: b * 255, a }; | |
} | |
export function hslaToCss(hsla: Hsla): string { | |
return hsla.a === 1 | |
? `hsl(${round(hsla.h)}, ${round(hsla.s * 100)}%, ${round(hsla.l * 100)}%)` | |
: `hsla(${round(hsla.h)}, ${round(hsla.s * 100)}%, ${round( | |
hsla.l * 100 | |
)}%, ${round(hsla.a)})`; | |
} | |
export function rgbaToCss(rgba: Rgba): string { | |
return rgba.a === 1 | |
? `rgb(${round(rgba.r)}, ${round(rgba.g)}, ${round(rgba.b)})` | |
: `rgba(${round(rgba.r)}, ${round(rgba.g)}, ${round(rgba.b)}, ${ | |
round(rgba.a * 1000) / 1000 | |
})`; | |
} | |
export function toRgba(input: ColorInput): Rgba { | |
if (isRgba(input)) return input; | |
if (isHsla(input)) return hslaToRgba(input); | |
if (typeof input === "string") return cssToRgba(input); | |
throw new Error(`Unsupported input: ${input}`); | |
} | |
export function toHsla(input: ColorInput): Hsla { | |
if (isHsla(input)) return input; | |
if (isRgba(input)) return rgbaToHsla(input); | |
if (typeof input === "string") return rgbaToHsla(cssToRgba(input)); | |
throw new Error(`Unsupported input: ${input}`); | |
} | |
export function toCss(input: ColorInput): string { | |
if (typeof input === "string") return rgbaToCss(cssToRgba(input)); | |
if (isRgba(input)) return rgbaToCss(input); | |
if (isHsla(input)) return rgbaToCss(hslaToRgba(input)); | |
throw new Error(`Unsupported input: ${input}`); | |
} | |
// Following could be application utility code: | |
export function darken(input: ColorInput, perc: number): Hsla { | |
const hsla = toHsla(input); | |
return { ...hsla, l: minMax(0, 1, hsla.l - perc) }; | |
} | |
export function lighten(input: ColorInput, perc: number): Hsla { | |
const hsla = toHsla(input); | |
return { ...hsla, l: minMax(0, 1, hsla.l + perc) }; | |
} | |
export function alpha(input: ColorInput, a: number): Rgba { | |
return { ...toRgba(input), a: minMax(0, 1, a) }; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react"; | |
import { styled } from "../styled"; | |
import { darken, alpha, toCss } from "../color"; | |
type ButtonProps = { | |
color: string; | |
size: string; | |
}; | |
export const Button = styled<ButtonProps>("div")` | |
width: ${(props) => props.size}; | |
height: ${(props) => props.size}; | |
border-radius: 100%; | |
border: 3px solid #fff; | |
background: ${(props) => props.color}; | |
box-shadow: 0 -2px 0 3px ${(props) => toCss(darken(props.color, 0.1))} inset, | |
0 5px 5px ${(props) => toCss(alpha(darken(props.color, 0.4), 0.17))}, | |
0 15px ${toCss(alpha("#ffffff", 0.25))} inset; | |
`; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment