oklab/oklab.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{}
)