All files / src/utils mix-color.ts

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52                7x 79x 79x 79x     7x 7x 107x   7x 52x   7x 26x 26x   26x 26x 26x         26x 26x 26x     26x   26x 33x 132x 99x       33x   33x      
import { mix } from './mix';
import { hsla, rgba, hex, Color } from 'style-value-types';
import { invariant } from 'hey-listen';
 
// Linear color space blending
// Explained https://www.youtube.com/watch?v=LKnqECcg6Gw
// Demonstrated http://codepen.io/osublake/pen/xGVVaN
 
export const mixLinearColor = (from: number, to: number, v: number) => {
  const fromExpo = from * from;
  const toExpo = to * to;
  return Math.sqrt(Math.max(0, v * (toExpo - fromExpo) + fromExpo));
};
 
const colorTypes = [hex, rgba, hsla];
const getColorType = (v: Color | string) =>
  colorTypes.find(type => type.test(v));
 
const notAnimatable = (color: Color | string) =>
  `'${color}' is not an animatable color. Use the equivalent color code instead.`;
 
export const mixColor = (from: Color | string, to: Color | string) => {
  const fromColorType = getColorType(from);
  const toColorType = getColorType(to);
 
  invariant(!!fromColorType, notAnimatable(from));
  invariant(!!toColorType, notAnimatable(to));
  invariant(
    fromColorType.transform === toColorType.transform,
    'Both colors must be hex/RGBA, OR both must be HSLA.'
  );
 
  const fromColor = fromColorType.parse(from);
  const toColor = toColorType.parse(to);
  const blended = { ...fromColor };
 
  // Only use the linear blending function for rgba and hex
  const mixFunc = fromColorType === hsla ? mix : mixLinearColor;
 
  return (v: number) => {
    for (const key in blended) {
      if (key !== 'alpha') {
        blended[key] = mixFunc(fromColor[key], toColor[key], v);
      }
    }
 
    blended.alpha = mix(fromColor.alpha, toColor.alpha, v);
 
    return fromColorType.transform(blended);
  };
};