202 lines
6.6 KiB
Go
202 lines
6.6 KiB
Go
// Based on: https://bottosson.github.io/posts/oklab/
|
|
//
|
|
// OkLab first defines a transform from xyz, and multiplies by this matrix:
|
|
// [+0.8189330101, +0.3618667424, -0.1288597137]
|
|
// [+0.0329845436, +0.9293118715, +0.0361456387]
|
|
// [+0.0482003018, +0.2643662691, +0.633851707 ]
|
|
//
|
|
// Wikipedia lists this matrix for converting from linear sRGB to D65 CIE XYZ, so
|
|
// I'm considering it canonical:
|
|
// [+0.4124, +0.3576, +0.1805]
|
|
// [+0.2126, +0.7152, +0.0722]
|
|
// [+0.0193, +0.1192, +0.9505]
|
|
//
|
|
// Combined, we get:
|
|
// [+0.41217385032507, +0.5362974607032, +0.05146302925248]
|
|
// [+0.21187214048845, +0.6807476834212, +0.10740645682645]
|
|
// [+0.08831541121808, +0.2818663070584, +0.63026344660742]
|
|
//
|
|
// And its inverse:
|
|
// [+4.0767584135565013494237930518854, -3.3072279873944731418619352916485, +0.230721459944885632473018834049 ]
|
|
// [-1.2681810851624033989047813181437, +2.6092932102856398573991970933594, -0.3411121165477535569679616041822]
|
|
// [-0.0040984077180314400491332639337372, -0.70350366010241732765095902557887, +1.7068604529788013559365593912662]
|
|
//
|
|
// After a non-linear transformation (cube root), OkLab applies a second matrix:
|
|
// [+0.2104542553, +0.793617785, -0.0040720468]
|
|
// [+1.9779984951, -2.428592205, +0.4505937099]
|
|
// [+0.0259040371, +0.7827717662, -0.808675766 ]
|
|
//
|
|
// And its inverse:
|
|
// [+0.99999999845051981426207542502031, +0.39633779217376785682345989261573, +0.21580375806075880342314146183004 ]
|
|
// [+1.0000000088817607767160752456705, -0.10556134232365634941095687705472, -0.063854174771705903405254198817796]
|
|
// [+1.0000000546724109177012928651534, -0.089484182094965759689052745863391, -1.2914855378640917399489287529148 ]
|
|
//
|
|
// It should be noted that the numbers in the first column were supposed to be 1, and other implementations skip
|
|
// multiplying by that column altogether. I however am insane, and shall continue barging forward.
|
|
|
|
package oklab
|
|
|
|
import (
|
|
"image/color"
|
|
"math"
|
|
)
|
|
|
|
type Color struct {
|
|
Lightness, ChromaA, ChromaB, A float64
|
|
}
|
|
|
|
func linearize(c uint32) float64 {
|
|
l := float64(c) / 0xffff
|
|
|
|
if l <= 0.039285714285714285714285714285714 {
|
|
return l / 12.923210180787861094641554898407
|
|
}
|
|
|
|
return math.Pow((l+0.055)/1.055, 2.4)
|
|
}
|
|
|
|
func delinearize(l float64) uint32 {
|
|
switch {
|
|
case l <= 0:
|
|
return 0
|
|
case l <= 0.0030399346397784299969770436366690:
|
|
return uint32(l*846922.57919793247683733430026710 + 0.5)
|
|
case l >= 1:
|
|
return 0xffff
|
|
default:
|
|
return uint32(69139.425*math.Pow(l, 1/2.4) - 3603.925)
|
|
}
|
|
}
|
|
|
|
// FromNRGBA create an OkLab color from non-pre-multiplied RGBA.
|
|
func FromNRGBA(r, g, b, a uint32) Color {
|
|
rLin, gLin, bLin := linearize(r), linearize(g), linearize(b)
|
|
|
|
l := math.Cbrt(0.41217385032507*rLin + 0.5362974607032*gLin + 0.05146302925248*bLin)
|
|
m := math.Cbrt(0.21187214048845*rLin + 0.6807476834212*gLin + 0.10740645682645*bLin)
|
|
s := math.Cbrt(0.08831541121808*rLin + 0.2818663070584*gLin + 0.63026344660742*bLin)
|
|
|
|
return Color{
|
|
0.2104542553*l + 0.793617785*m - 0.0040720468*s,
|
|
1.9779984951*l - 2.428592205*m + 0.4505937099*s,
|
|
0.0259040371*l + 0.7827717662*m - 0.808675766*s,
|
|
float64(a) / float64(0xffff),
|
|
}
|
|
}
|
|
|
|
// FromRGBA create an OkLab color from pre-multiplied RGBA.
|
|
func FromRGBA(r, g, b, a uint32) Color {
|
|
switch a {
|
|
case 0xffff:
|
|
// do nothing.
|
|
case 0:
|
|
// completely transparent, color information was lost.
|
|
// pretend it was gray.
|
|
r, g, b = 0x7fff, 0x7fff, 0x7fff
|
|
default:
|
|
// un-premultiply rgb.
|
|
//
|
|
// Note that I'm rounding up here, which is the opposite of what the NRGBA/NRGBA64 colors do,
|
|
// which may be a bug as RGBA64->NRGBA64->RGBA64 is lossy.
|
|
r = (r*0xffff + a - 1) / a
|
|
g = (g*0xffff + a - 1) / a
|
|
b = (b*0xffff + a - 1) / a
|
|
}
|
|
|
|
return FromNRGBA(r, g, b, a)
|
|
}
|
|
|
|
// NRGBAColor represent a color that can give us non pre-multiplied RGBA components.
|
|
//
|
|
// This isn't a standard interface, but we implement it and check for it regardless.
|
|
type NRGBAColor interface {
|
|
color.Color
|
|
NRGBA() (r, g, b, a uint32)
|
|
}
|
|
|
|
// FromColor converts an arbitrary color type to an OKLab [Color], with special handling
|
|
// for [color.NRGBA], [color.NRGBA64], and anything implementing the [NRGBAColor] interface
|
|
// to preserve transparent colors.
|
|
func FromColor(c color.Color) Color {
|
|
switch c := c.(type) {
|
|
case Color:
|
|
return c
|
|
|
|
// Special handling for [color.NRGBA] and [color.NRGBA64]
|
|
case color.NRGBA:
|
|
return FromNRGBA(uint32(c.R)*0x101, uint32(c.G)*0x101, uint32(c.B)*0x101, uint32(c.A)*0x101)
|
|
|
|
case color.NRGBA64:
|
|
return FromNRGBA(uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A))
|
|
|
|
case NRGBAColor:
|
|
return FromNRGBA(c.NRGBA())
|
|
|
|
default:
|
|
return FromRGBA(c.RGBA())
|
|
}
|
|
}
|
|
|
|
func cube(v float64) float64 {
|
|
return v * v * v
|
|
}
|
|
|
|
// NRGBA converts to non-premultiplied RGBA.
|
|
func (c Color) NRGBA() (r, g, b, a uint32) {
|
|
l := cube(0.99999999845051981426207542502031*c.Lightness + 0.39633779217376785682345989261573*c.ChromaA + 0.21580375806075880342314146183004*c.ChromaB)
|
|
m := cube(1.0000000088817607767160752456705*c.Lightness - 0.10556134232365634941095687705472*c.ChromaA - 0.063854174771705903405254198817796*c.ChromaB)
|
|
s := cube(1.0000000546724109177012928651534*c.Lightness - 0.089484182094965759689052745863391*c.ChromaA - 1.2914855378640917399489287529148*c.ChromaB)
|
|
|
|
r = delinearize(4.0767584135565013494237930518854*l - 3.3072279873944731418619352916485*m + 0.230721459944885632473018834049*s)
|
|
g = delinearize(-1.2681810851624033989047813181437*l + 2.6092932102856398573991970933594*m - 0.3411121165477535569679616041822*s)
|
|
b = delinearize(-0.0040984077180314400491332639337372*l - 0.70350366010241732765095902557887*m + 1.7068604529788013559365593912662*s)
|
|
a = uint32(c.A*0xffff + 0.5)
|
|
|
|
return
|
|
}
|
|
|
|
// RGBA converts to premultiplied RGBA.
|
|
func (c Color) RGBA() (r, g, b, a uint32) {
|
|
r, g, b, a = c.NRGBA()
|
|
|
|
r = r * a / 0xffff
|
|
g = g * a / 0xffff
|
|
b = b * a / 0xffff
|
|
|
|
return
|
|
}
|
|
|
|
func sqr(a float64) float64 {
|
|
return a * a
|
|
}
|
|
|
|
// Distance returns the perceptual distance between two colors.
|
|
//
|
|
// This controls for possible transparency. Two different but otherwise fully transparent colors would have a distance of zero.
|
|
func Distance(a, b Color) float64 {
|
|
L1, a1, b1 := a.Lightness*a.A, a.ChromaA*a.A, a.ChromaB*a.A
|
|
L2, a2, b2 := b.Lightness*b.A, b.ChromaA*b.A, b.ChromaB*b.A
|
|
dL := L1 - L2
|
|
da := a1 - a2
|
|
db := b1 - b2
|
|
dA := a.A - b.A
|
|
return math.Sqrt(max(sqr(dL), sqr(dL+dA)) + max(sqr(da), sqr(da+dA)) + max(sqr(db), sqr(db+dA)))
|
|
}
|
|
|
|
func okLabModel(c color.Color) color.Color {
|
|
return FromColor(c)
|
|
}
|
|
|
|
var (
|
|
// A color model for converting arbitrary colors to OKLab.
|
|
//
|
|
// Wraps the [FromColor] function, returning a [color.Color] interface rather than the [Color] type.
|
|
Model = color.ModelFunc(okLabModel)
|
|
|
|
// Type assertions.
|
|
_ interface {
|
|
color.Color
|
|
NRGBAColor
|
|
} = Color{}
|
|
)
|