evilmartians / oklch-picker

Color Picker for LCH
https://oklch.com
Other
849 stars 65 forks source link

Соображения по оптимизации преобразований цветов #79

Closed dom1n1k closed 1 year ago

dom1n1k commented 1 year ago

Пока изучал код, появились соображения. Сразу конкретный пример:

https://github.com/evilmartians/oklch-picker/blob/89638b45390a1be7f391c9a0964c931f1edadb20/lib/colors.ts#L151-L161

Что тут бросается в глаза? В каждую из трех проверочных функций передается исходный цвет, который внутри будет сконвертирован в целевое пространство. Нюанс в том, что в Culori нет прямого перехода из OKLch в P3 или Rec2020. Он в любом случае идет через sRGB. В реальности под капотом такие цепочки:

OKLch -> OKLab -> Linear sRGB -> sRGB
OKLch -> OKLab -> Linear sRGB -> sRGB -> Linear sRGB -> XYZ-65 -> Linear P3 -> P3
OKLch -> OKLab -> Linear sRGB -> sRGB -> Linear sRGB -> XYZ-65 -> Linear Rec2020 -> Rec2020

Да, двойной проход через Linear sRGB. Он избыточен, но такой граф у Culori. Технически можно короче, но это пришлось бы разруливать вручную.

Из LCH будет вот так:

LCH -> Lab -> XYZ-50 -> XYZ-65 -> Linear sRGB -> sRGB
LCH -> Lab -> XYZ-50 -> XYZ-65 -> Linear P3 -> P3
LCH -> Lab -> XYZ-50 -> XYZ-65 -> Linear Rec2020 -> Rec2020

Очевидно, что проходить все три цепочки от начала до конца неэффективно – половина шагов повторяется. Можно закешировать результат последнего общего шага. Примерно так:

export function getSpace(color: Color): Space {
  let proxyColor: Color

  if (color.mode === 'oklch') {
    proxyColor = rgb(color)
  } else if (color.mode === 'lch') {
    proxyColor = xyz65(color)
  } else {
    proxyColor = color
  }

  if (inRGB(proxyColor)) {
    return Space.sRGB
  } else if (inP3(proxyColor)) {
    return Space.P3
  } else if (inRec2020(proxyColor)) {
    return Space.Rec2020
  } else {
    return Space.Out
  }
}

Стоимость кеширования практически нулевая, поскольку мы не делаем лишних вычислений. Только сохраняем то, что и так было бы.

Это в качестве примера. Вообще, если честно, я сомневаюсь в необходимости функций generateGetSpace и generateGetPixel. Я понимаю, зачем они были сделаны. Но кажется, что это экономия на спичках, когда в комнате есть слон пожирнее. Цветовые преобразования намного дороже, чем пара if-ов.

ai commented 1 year ago

Очень нравится хак. Пришлёшь PR с результатами бенчмарка.

Только вместо if (color.mode === 'oklch') лучше сделать COLOR_FN === 'oklch'. Это константа и JIT-компилятор там вообще выкинет проверку.

ai commented 1 year ago

proxyColor = rgb(color)

А у culori нет linaer RGB? (странно, он часто нужен)

dom1n1k commented 1 year ago

А у culori нет linaer RGB? (странно, он часто нужен)

Linear sRGB - есть. Linear P3/Rec2020 - нет. Ну в смысле нет в виде отдельных сущностей. Неявным образом внутри конечно есть.

dom1n1k commented 1 year ago

Только вместо if (color.mode === 'oklch') лучше сделать COLOR_FN === 'oklch'

Нет, это не прокатит, потому что mode в данном случае не режим всего приложения, а тип конкретного цвета, который передали параметром. И там ещё может быть rgb-цвет.

ai commented 1 year ago

Да, точно :(

ai commented 1 year ago

Если будет ускорение, я потом подумаю как ещё ускорить. Жду PR.