[JS] color brightness 조절하기

쌍제이(JJVoiture)·2024년 9월 10일
0
post-thumbnail

next.js에서 _app의 static chunk가 602kB로 다소 용량이 커서 사용하지 않거나 한 곳에서만 사용하는 라이브러리를 지우는 작업을 진행 중이다. 도중 chroma-js란 라이브러리의 darken이라는 메소드를 사용하는 코드를 발견하여, 해당 라이브러리와 이 메소드의 동작 원리를 파악하고 이를 대체하는 코드를 만드는 과정을 기록했다.

chroma-js

색상을 변환하는 도구를 제공하는 라이브러리.

https://github.com/gka/chroma.js/issues/217

위 이슈에 따르면 chroma js는 국제 조명 위원회(CIE)에서 정의한 CIEL*a*b*라는 색 공간을 기반으로 메소드를 구현했다고 한다.

CIEL*a*b*

  • 인간이 인지하는 색상에 가깝게 표현
  • L(lightness): 밝기
  • a : Red와 Green의 정도
  • b : Yellow와 Blue의 정도

chroma.darken

chroma-js의 darken method는 CIELab의 L 값을 낮추는 메소드이다.

chroma('hotpink').darken(); // default value = 1
chroma('hotpink').darken(2);
chroma('hotpink').darken(2.6);

// result
// #c93384
// #930058
// #74003f
// chroma.js 내부의 darken 구현 코드
Color$k.prototype.darken = function(amount) {
  if ( amount === void 0 ) amount=1;

  var me = this;
  var lab = me.lab();
  lab[0] -= LAB_CONSTANTS$1.Kn * amount;
  return new Color$k(lab, 'lab').alpha(me.alpha(), true);
};

// LAB_CONSTANTS$1.Kn = 18

구현

아래와 같이 Chroma라는 클래스를 구현했다.

// Utility functions for color conversion
// (e.g. "03F") to full form (e.g. "0033FF")
const hexShorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
const hexFullRegex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;

function expandHexShorthand(hex: string): string {
  return hex.replace(hexShorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
}

function hex2Rgb(hex: string) {
  hex = expandHexShorthand(hex);
  const result = hexFullRegex.exec(hex);
  return result
    ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16),
      }
    : null;
}

function rgb2Hex(rgb: { r: number; g: number; b: number }) {
  return (
    "#" +
    [rgb.r, rgb.g, rgb.b]
      .map((value) => value.toString(16).padStart(2, "0"))
      .join("")
  );
}

// Conversion between RGB and Chroma (L*a*b*) color spaces
function rgb2Chroma(rgb: { r: number; g: number; b: number }) {
  const normalize = (v: number) =>
    v > 0.04045 ? Math.pow((v + 0.055) / 1.055, 2.4) : v / 12.92;
  const r = normalize(rgb.r / 255);
  const g = normalize(rgb.g / 255);
  const b = normalize(rgb.b / 255);

  let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
  let y = r * 0.2126 + g * 0.7152 + b * 0.0722;
  let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;

  x = x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116;
  y = y > 0.008856 ? Math.pow(y, 1 / 3) : 7.787 * y + 16 / 116;
  z = z > 0.008856 ? Math.pow(z, 1 / 3) : 7.787 * z + 16 / 116;

  return {
    L: 116 * y - 16,
    a: 500 * (x - y),
    b: 200 * (y - z),
  };
}

function chroma2Rgb(chroma: { L: number; a: number; b: number }) {
  const y = (chroma.L + 16) / 116;
  const x = chroma.a / 500 + y;
  const z = y - chroma.b / 200;

  const toLinearRgb = (v: number) =>
    v > 0.008856 ? Math.pow(v, 3) : (v - 16 / 116) / 7.787;

  let xr = 0.95047 * toLinearRgb(x);
  let yr = toLinearRgb(y);
  let zr = 1.08883 * toLinearRgb(z);

  const toRgb = (value: number) =>
    value > 0.0031308
      ? 1.055 * Math.pow(value, 1 / 2.4) - 0.055
      : 12.92 * value;

  return {
    r: Math.round(
      Math.max(
        0,
        Math.min(1, toRgb(xr * 3.2406 + yr * -1.5372 + zr * -0.4986)),
      ) * 255,
    ),
    g: Math.round(
      Math.max(
        0,
        Math.min(1, toRgb(xr * -0.9689 + yr * 1.8758 + zr * 0.0415)),
      ) * 255,
    ),
    b: Math.round(
      Math.max(0, Math.min(1, toRgb(xr * 0.0557 + yr * -0.204 + zr * 1.057))) *
        255,
    ),
  };
}

// Chroma class for managing color transformations
export default class Chroma {
  private L: number;
  private a: number;
  private b: number;

  constructor(hex: string) {
    const chroma = hex2Chroma(hex);
    this.L = chroma?.L ?? 0;
    this.a = chroma?.a ?? 0;
    this.b = chroma?.b ?? 0;
  }

  public darken(value: number = 1) {
    this.L = Math.max(0, this.L - 18 * value);
    return this;
  }

  public toHex() {
    return rgb2Hex(
      chroma2Rgb({
        L: this.L,
        a: this.a,
        b: this.b,
      }),
    );
  }
}

// Convert hex to Chroma
function hex2Chroma(hex: string) {
  const rgb = hex2Rgb(hex);
  return rgb ? rgb2Chroma(rgb) : null;
}

결과

chroma-js 라이브러리를 걷어내어 _app chunk 파일의 크기가 16kB 줄어들었다.

전)

후)


관련 문서

CIELAB_색_공간_wikipedia

profile
안녕하세요. 중구난방 개발자 쌍제이입니다.

0개의 댓글