Skip to content

Instantly share code, notes, and snippets.

@marcelbeumer
Last active July 30, 2020 15:41
Show Gist options
  • Save marcelbeumer/d6cfc65644ed4c6c235592c3d960a425 to your computer and use it in GitHub Desktop.
Save marcelbeumer/d6cfc65644ed4c6c235592c3d960a425 to your computer and use it in GitHub Desktop.
Super minimal color util lib
// 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) };
}
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