material-chalk
Version:
Generate beautiful colors from namespaces based on color theory
153 lines (152 loc) • 6.33 kB
JavaScript
;
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),
};
}