1.2.0 • Published 1 year ago

linear-color v1.2.0

Weekly downloads
-
License
MIT
Repository
-
Last release
1 year ago

linear-color

Use linear colorspace blending to achieve a more natural color transition. Watch this video to learn more. This implementation was extracted from framer-motion, which means it's battle-tested.

Only HEX, RGB, and HSL colors are supported.

Usage

import { mixColor, parseColor } from 'linear-color'

// Mix two colors together. The colors can be strings or Color objects.
const mixedColor = mixColor('#ff0000', '#0000ff', 0.5)
mixedColor // => 'rgba(180, 0, 180, 1)'

// This function is useful for animation libraries, because it
// lets you pre-parse values to reuse every animation frame.
const rgbaColor = parseColor('hsla(0, 100%, 50%, 0.5)')
rgbaColor.red //   => 255
rgbaColor.green // => 0
rgbaColor.blue //  => 0
rgbaColor.alpha // => 0.5

Also exported are the Color, RGBA, and HSLA object types.

Code

When compiled, this library contains ~120 eLOC.

// src/utils/mix.ts
var mix = (from, to, progress) => -progress * from + progress * to + from

// src/utils/hsla-to-rgba.ts
function hueToRgb(p, q, t) {
  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
}
function hslaToRgba({ hue, saturation, lightness, alpha: alpha2 }) {
  hue /= 360
  saturation /= 100
  lightness /= 100
  let red = 0
  let green = 0
  let blue = 0
  if (!saturation) {
    red = green = blue = lightness
  } else {
    const q =
      lightness < 0.5
        ? lightness * (1 + saturation)
        : lightness + saturation - lightness * saturation
    const p = 2 * lightness - q
    red = hueToRgb(p, q, hue + 1 / 3)
    green = hueToRgb(p, q, hue)
    blue = hueToRgb(p, q, hue - 1 / 3)
  }
  return {
    red: Math.round(red * 255),
    green: Math.round(green * 255),
    blue: Math.round(blue * 255),
    alpha: alpha2,
  }
}

// src/utils/clamp.ts
var clamp = (min, max, v) => Math.min(Math.max(v, min), max)

// src/types/numbers/index.ts
var number = {
  test: v => typeof v === 'number',
  parse: parseFloat,
  transform: v => v,
}
var alpha = {
  ...number,
  transform: v => clamp(0, 1, v),
}

// src/types/utils.ts
var sanitize = v => Math.round(v * 1e5) / 1e5
var floatRegex = /(-)?([\d]*\.?[\d])+/g
var singleColorRegex =
  /^(#[0-9a-f]{3,8}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))$/i
function isString(v) {
  return typeof v === 'string'
}

// src/types/color/utils.ts
var isColorString = (type, testProp) => v => {
  return Boolean(
    (isString(v) && singleColorRegex.test(v) && v.startsWith(type)) ||
      (testProp && Object.prototype.hasOwnProperty.call(v, testProp))
  )
}
var splitColor = (aName, bName, cName) => v => {
  if (!isString(v)) return v
  const [a, b, c, alpha2] = v.match(floatRegex)
  return {
    [aName]: parseFloat(a),
    [bName]: parseFloat(b),
    [cName]: parseFloat(c),
    alpha: alpha2 !== void 0 ? parseFloat(alpha2) : 1,
  }
}

// src/types/color/rgba.ts
var clampRgbUnit = v => clamp(0, 255, v)
var rgbUnit = {
  ...number,
  transform: v => Math.round(clampRgbUnit(v)),
}
var rgba = {
  test: isColorString('rgb', 'red'),
  parse: splitColor('red', 'green', 'blue'),
  transform: ({ red, green, blue, alpha: alpha2 = 1 }) =>
    'rgba(' +
    rgbUnit.transform(red) +
    ', ' +
    rgbUnit.transform(green) +
    ', ' +
    rgbUnit.transform(blue) +
    ', ' +
    sanitize(alpha.transform(alpha2)) +
    ')',
}

// src/types/color/hex.ts
function parseHex(v) {
  let r = ''
  let g = ''
  let b = ''
  let a = ''
  if (v.length > 5) {
    r = v.substring(1, 3)
    g = v.substring(3, 5)
    b = v.substring(5, 7)
    a = v.substring(7, 9)
  } else {
    r = v.substring(1, 2)
    g = v.substring(2, 3)
    b = v.substring(3, 4)
    a = v.substring(4, 5)
    r += r
    g += g
    b += b
    a += a
  }
  return {
    red: parseInt(r, 16),
    green: parseInt(g, 16),
    blue: parseInt(b, 16),
    alpha: a ? parseInt(a, 16) / 255 : 1,
  }
}
var hex = {
  test: isColorString('#'),
  parse: parseHex,
  transform: rgba.transform,
}

// src/types/numbers/units.ts
var createUnitType = unit => ({
  test: v => isString(v) && v.endsWith(unit) && v.split(' ').length === 1,
  parse: parseFloat,
  transform: v => `${v}${unit}`,
})
var percent = createUnitType('%')

// src/types/color/hsla.ts
var hsla = {
  test: isColorString('hsl', 'hue'),
  parse: splitColor('hue', 'saturation', 'lightness'),
  transform: ({ hue, saturation, lightness, alpha: alpha2 = 1 }) => {
    return (
      'hsla(' +
      Math.round(hue) +
      ', ' +
      percent.transform(sanitize(saturation)) +
      ', ' +
      percent.transform(sanitize(lightness)) +
      ', ' +
      sanitize(alpha.transform(alpha2)) +
      ')'
    )
  },
}

// src/mix-color.ts
var mixLinearColor = (from, to, v) => {
  const fromExpo = from * from
  return Math.sqrt(Math.max(0, v * (to * to - fromExpo) + fromExpo))
}
var colorTypes = [hex, rgba, hsla]
var getColorType = v => colorTypes.find(type => type.test(v))
function parseColor(color) {
  const type = getColorType(color)
  if (!type) {
    throw Error(
      `'${color}' is not an animatable color. Use the equivalent color code instead.`
    )
  }
  let model = isString(color) ? type.parse(color) : color
  if (type === hsla) {
    model = hslaToRgba(model)
  }
  return model
}
var mixColor = (from, to, v) => {
  from = parseColor(from)
  to = parseColor(to)
  return rgba.transform({
    red: mixLinearColor(from.red, to.red, v),
    green: mixLinearColor(from.green, to.green, v),
    blue: mixLinearColor(from.blue, to.blue, v),
    alpha: mix(from.alpha, to.alpha, v),
  })
}
export { mixColor, mixLinearColor, parseColor }
1.2.0

1 year ago

1.1.1

1 year ago

1.1.0

1 year ago

1.0.0

1 year ago