UNPKG

colorjs.io

Version:

Let’s get serious about color

218 lines (177 loc) 5.69 kB
/** * Functions related to color interpolation */ import Color from "./color.js"; import ColorSpace from "./space.js"; import {type, interpolate} from "./util.js"; import getColor from "./getColor.js"; import clone from "./clone.js"; import to from "./to.js"; import toGamut from "./toGamut.js"; import get from "./get.js"; import set from "./set.js"; import defaults from "./defaults.js"; import * as angles from "./angles.js"; import deltaE from "./deltaE.js"; /** * Return an intermediate color between two colors * Signatures: mix(c1, c2, p, options) * mix(c1, c2, options) * mix(color) * @param {Color | string} c1 The first color * @param {Color | string} [c2] The second color * @param {number} [p=.5] A 0-1 percentage where 0 is c1 and 1 is c2 * @param {Object} [o={}] * @return {Color} */ export function mix (c1, c2, p = .5, o = {}) { [c1, c2] = [getColor(c1), getColor(c2)]; if (type(p) === "object") { [p, o] = [.5, p]; } let {space, outputSpace, premultiplied} = o; let r = range(c1, c2, {space, outputSpace, premultiplied}); return r(p); } /** * * @param {Color | string | Function} c1 The first color or a range * @param {Color | string} [c2] The second color if c1 is not a range * @param {Object} [options={}] * @return {Color[]} */ export function steps (c1, c2, options = {}) { let colorRange; if (isRange(c1)) { // Tweaking existing range [colorRange, options] = [c1, c2]; [c1, c2] = colorRange.rangeArgs.colors; } let { maxDeltaE, deltaEMethod, steps = 2, maxSteps = 1000, ...rangeOptions } = options; if (!colorRange) { [c1, c2] = [getColor(c1), getColor(c2)]; colorRange = range(c1, c2, rangeOptions); } let totalDelta = deltaE(c1, c2); let actualSteps = maxDeltaE > 0? Math.max(steps, Math.ceil(totalDelta / maxDeltaE) + 1) : steps; let ret = []; if (maxSteps !== undefined) { actualSteps = Math.min(actualSteps, maxSteps); } if (actualSteps === 1) { ret = [{p: .5, color: colorRange(.5)}]; } else { let step = 1 / (actualSteps - 1); ret = Array.from({length: actualSteps}, (_, i) => { let p = i * step; return {p, color: colorRange(p)}; }); } if (maxDeltaE > 0) { // Iterate over all stops and find max deltaE let maxDelta = ret.reduce((acc, cur, i) => { if (i === 0) { return 0; } let ΔΕ = deltaE(cur.color, ret[i - 1].color, deltaEMethod); return Math.max(acc, ΔΕ); }, 0); while (maxDelta > maxDeltaE) { // Insert intermediate stops and measure maxDelta again // We need to do this for all pairs, otherwise the midpoint shifts maxDelta = 0; for (let i = 1; (i < ret.length) && (ret.length < maxSteps); i++) { let prev = ret[i - 1]; let cur = ret[i]; let p = (cur.p + prev.p) / 2; let color = colorRange(p); maxDelta = Math.max(maxDelta, deltaE(color, prev.color), deltaE(color, cur.color)); ret.splice(i, 0, {p, color: colorRange(p)}); i++; } } } ret = ret.map(a => a.color); return ret; }; /** * Interpolate to color2 and return a function that takes a 0-1 percentage * @param {Color | string | Function} color1 The first color or an existing range * @param {Color | string} [color2] If color1 is a color, this is the second color * @param {Object} [options={}] * @returns {Function} A function that takes a 0-1 percentage and returns a color */ export function range (color1, color2, options = {}) { if (isRange(color1)) { // Tweaking existing range let [r, options] = [color1, color2]; return range(...r.rangeArgs.colors, {...r.rangeArgs.options, ...options}); } let {space, outputSpace, progression, premultiplied} = options; color1 = getColor(color1); color2 = getColor(color2); // Make sure we're working on copies of these colors color1 = clone(color1); color2 = clone(color2); let rangeArgs = {colors: [color1, color2], options}; if (space) { space = ColorSpace.get(space); } else { space = ColorSpace.registry[defaults.interpolationSpace] || color1.space; } outputSpace = outputSpace? ColorSpace.get(outputSpace) : space; color1 = to(color1, space); color2 = to(color2, space); // Gamut map to avoid areas of flat color color1 = toGamut(color1); color2 = toGamut(color2); // Handle hue interpolation // See https://github.com/w3c/csswg-drafts/issues/4735#issuecomment-635741840 if (space.coords.h && space.coords.h.type === "angle") { let arc = options.hue = options.hue || "shorter"; let hue = [space, "h"]; let1, θ2] = [get(color1, hue), get(color2, hue)]; [θ1, θ2] = angles.adjust(arc, [θ1, θ2]); set(color1, hue, θ1); set(color2, hue, θ2); } if (premultiplied) { // not coping with polar spaces yet color1.coords = color1.coords.map(c => c * color1.alpha); color2.coords = color2.coords.map(c => c * color2.alpha); } return Object.assign(p => { p = progression? progression(p) : p; let coords = color1.coords.map((start, i) => { let end = color2.coords[i]; return interpolate(start, end, p); }); let alpha = interpolate(color1.alpha, color2.alpha, p); let ret = {space, coords, alpha}; if (premultiplied) { // undo premultiplication ret.coords = ret.coords.map(c => c / alpha); } if (outputSpace !== space) { ret = to(ret, outputSpace); } return ret; }, { rangeArgs }); }; export function isRange (val) { return type(val) === "function" && !!val.rangeArgs; }; defaults.interpolationSpace = "lab"; export function register(Color) { Color.defineFunction("mix", mix, {returns: "color"}); Color.defineFunction("range", range, {returns: "function<color>"}); Color.defineFunction("steps", steps, {returns: "array<color>"}); }