UNPKG

material-chalk

Version:

Generate beautiful colors from namespaces based on color theory

153 lines (152 loc) 6.33 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Format = exports.TONE_RANGE = void 0; exports.colorFromSeed = colorFromSeed; exports.registerBrand = registerBrand; exports.createMaterial = createMaterial; exports.colorToNamespace = colorToNamespace; const material_color_utilities_1 = require("@material/material-color-utilities"); const fnv1a_js_1 = __importDefault(require("./fnv1a.js")); const scheme_override_js_1 = require("./scheme-override.js"); /** * Given a seed of randomly generated bites, allow consuming bits as needed * Note: this only works up to the 32 bits */ function consumableRandomness(seed) { let remainingRandomness = seed >>> 0; // Convert to unsigned 32-bit integer return { consume: (val, precision) => { const bits = Math.ceil(Math.log2(val)); // take `bits` for the whole number part, plus `precision` extra bits for the floating point parts const mask = (1 << (bits + precision)) - 1; // take a number within the bit range const result = remainingRandomness % mask; // note: `result` is a number from [0, 2^bits] // so we need to rescale it to [0, val] // do this by dividing by mask (becomes a [0,1] range) // then multiply by val // DANGER: multiply first before dividing to avoid precision loss! // we have up to Number.MAX_SAFE_INTEGER, so multiplication won't lead to precision loss // but division can, so we leave it to last const final = (result * val) / mask; remainingRandomness = remainingRandomness >>> (bits + precision); // Use unsigned right shift return final; }, }; } exports.TONE_RANGE = { Min: 68, Max: 70, }; /** * Generates a color deterministically based on the seed provided */ function colorFromSeed(seed) { // note: sum of `bits` in this function adds up to exactly 32 const randomness = consumableRandomness(seed); const hue = randomness.consume(360, // 9 bits 5); // pick a tone that guarantees chroma >= 48 exists (see justification.md to learn more) const tone = exports.TONE_RANGE.Min + randomness.consume(exports.TONE_RANGE.Max - exports.TONE_RANGE.Min, // 2 bits 5); // pick a chroma >= 48 const minChroma = 48; const maxChroma = material_color_utilities_1.Hct.from(hue, 200, tone).chroma; // pick a chroma that is way too high and see what it gets clamped to const range = maxChroma - minChroma; const chroma = minChroma + randomness.consume(range, // at most 6 bits on the tone interval we care about (could be up to 8 bits otherwise) 5); return material_color_utilities_1.Hct.from(hue, chroma, tone); } /** * Static type representing all the different formatters supported by material-chalk */ exports.Format = { /** * Format as a HCT <Hue, Chroma, Tone> tuple used by Material Design */ Hct: (namespace) => namespace, /** * Format as a color hex code (ex. #ff0000 for red). Output is always lowercase */ Hex: (namespace) => (0, material_color_utilities_1.hexFromArgb)(namespace.toInt()), /** * Decorates a given `chalk` object with the color of this material */ Chalk: (chalk) => (namespace) => chalk.hex((0, material_color_utilities_1.hexFromArgb)(namespace.toInt())), /** * Use the material as the source color for a Material Design scheme */ Scheme: (scheme) => (...args) => (namespace) => (0, scheme_override_js_1.buildScheme)(scheme, namespace)(...args), /** * Custom formatter if none of the existing ones satisfy a use-case */ Custom: (fn) => (namespace) => fn(namespace), }; const hctCache = {}; function getHct(namespace, cache) { // always look at the cache content // so that `registerBrand` always resolves properly if (namespace in hctCache) { return hctCache[namespace]; } const color = colorFromSeed(Number((0, fnv1a_js_1.default)(namespace, { size: 32 }))); if (cache) { hctCache[namespace] = color; } return color; } /** * Force a specific color to be used for a namespace. * This is useful if you need to force a brand color for a namespace * * This will cause the specified color to be used even deep inside `createMaterial` calls * * Careful: this is change is global, so if you use this in a library, * only use it for namespaces that are unlikely to be used by downstream users * @param namespace - the namespace to override * @param color - the color to use (see `matchColor` on how to generate this color easily) */ function registerBrand(namespace, color) { hctCache[namespace] = color; } /** * Creates a material for the given namespaces * * @param namespace a single namespace, or a hierarchy of namespaces (ex: `["parent", "child"]`) * @param options options used to construct the material * @returns a new material */ function createMaterial(namespace, options = {}) { const cache = options.cache ?? true; const color = (() => { if (typeof namespace === "string") { return getHct(namespace, cache); } const allColors = namespace.map((color) => { if (typeof color === "string") return getHct(color, cache).toInt(); return color.toInt(); }); let finalColor = allColors[allColors.length - 1]; for (let i = allColors.length - 2; i >= 0; i--) { // shift the color towards the parent finalColor = material_color_utilities_1.Blend.harmonize(finalColor, allColors[i]); } return material_color_utilities_1.Hct.fromInt(finalColor); })(); return colorToNamespace(color, options); } /** * Wrap a color with some utility functions to make it easier to work with. See `NamespaceResult` */ function colorToNamespace(color, options = {}) { return { formatAs: (format) => format(color), subMaterial: (subNamespace) => createMaterial([color, subNamespace], options), }; }