color_oklab.go RawHistoryFolder

1package theme
2
3import (
4	"math"
5
6	renderer "github.com/bnema/vev-vt"
7)
8
9// oklab is the perceptually uniform OKLab representation of an sRGB color.
10type oklab struct {
11	L float64
12	A float64
13	B float64
14}
15
16// rgbToOKLab converts an 8-bit sRGB color to OKLab via linear sRGB and LMS.
17func rgbToOKLab(color renderer.RGB) oklab {
18	r := srgbToLinear(color.R)
19	g := srgbToLinear(color.G)
20	b := srgbToLinear(color.B)
21
22	l := math.Cbrt(0.4122214708*r + 0.5363325363*g + 0.0514459929*b)
23	m := math.Cbrt(0.2119034982*r + 0.6806995451*g + 0.1073969566*b)
24	s := math.Cbrt(0.0883024619*r + 0.2817188376*g + 0.6299787005*b)
25
26	return oklab{
27		L: 0.2104542553*l + 0.7936177850*m - 0.0040720468*s,
28		A: 1.9779984951*l - 2.4285922050*m + 0.4505937099*s,
29		B: 0.0259040371*l + 0.7827717662*m - 0.8086757660*s,
30	}
31}
32
33// okLabToRGB converts OKLab to sRGB. Linear channels are clamped before
34// transfer encoding so out-of-gamut intermediate values are deterministic.
35func okLabToRGB(color oklab) renderer.RGB {
36	l := cube(color.L + 0.3963377774*color.A + 0.2158037573*color.B)
37	m := cube(color.L - 0.1055613458*color.A - 0.0638541728*color.B)
38	s := cube(color.L - 0.0894841775*color.A - 1.2914855480*color.B)
39
40	return renderer.RGB{
41		R: linearToSRGB(4.0767416621*l - 3.3077115913*m + 0.2309699292*s),
42		G: linearToSRGB(-1.2684380046*l + 2.6097574011*m - 0.3413193965*s),
43		B: linearToSRGB(-0.0041960863*l - 0.7034186147*m + 1.7076147010*s),
44	}
45}
46
47func okLabDistance(a, b oklab) float64 {
48	return math.Sqrt(square(a.L-b.L) + square(a.A-b.A) + square(a.B-b.B))
49}
50
51func okLabChroma(color oklab) float64 {
52	return math.Hypot(color.A, color.B)
53}
54
55// oklch is the polar (lightness, chroma, hue) form of oklab. Hue is degrees
56// in [0, 360).
57type oklch struct {
58	L float64
59	C float64
60	H float64
61}
62
63// oklabToOKLCh converts to the polar form used to isolate hue from
64// lightness and chroma.
65func oklabToOKLCh(color oklab) oklch {
66	hue := math.Atan2(color.B, color.A) * 180 / math.Pi
67	if hue < 0 {
68		hue += 360
69	}
70	return oklch{L: color.L, C: okLabChroma(color), H: hue}
71}
72
73// oklchToOKLab converts back from the polar form.
74func oklchToOKLab(color oklch) oklab {
75	rad := color.H * math.Pi / 180
76	return oklab{L: color.L, A: color.C * math.Cos(rad), B: color.C * math.Sin(rad)}
77}
78
79// shiftHue replaces color's OKLCh hue with targetDegrees while preserving
80// its lightness and chroma, so the result stays in the same value/intensity
81// band as the source. A near-neutral source (chroma close to zero) yields a
82// near-neutral result regardless of the target: hue is not meaningful there.
83func shiftHue(color renderer.RGB, targetDegrees float64) renderer.RGB {
84	lch := oklabToOKLCh(rgbToOKLab(color))
85	lch.H = targetDegrees
86	return okLabToRGB(oklchToOKLab(lch))
87}
88
89// okLabLerp interpolates sRGB endpoints in OKLab. Exact endpoints avoid
90// round-trip quantization changing terminal-provided colors.
91func okLabLerp(a, b renderer.RGB, weight float64) renderer.RGB {
92	weight = clampUnit(weight)
93	if weight == 0 {
94		return a
95	}
96	if weight == 1 {
97		return b
98	}
99	start := rgbToOKLab(a)
100	end := rgbToOKLab(b)
101	return okLabToRGB(oklab{
102		L: start.L + (end.L-start.L)*weight,
103		A: start.A + (end.A-start.A)*weight,
104		B: start.B + (end.B-start.B)*weight,
105	})
106}
107
108func srgbToLinear(channel uint8) float64 {
109	value := float64(channel) / 255
110	if value <= 0.04045 {
111		return value / 12.92
112	}
113	return math.Pow((value+0.055)/1.055, 2.4)
114}
115
116func linearToSRGB(value float64) uint8 {
117	value = clampUnit(value)
118	if value <= 0.0031308 {
119		value *= 12.92
120	} else {
121		value = 1.055*math.Pow(value, 1/2.4) - 0.055
122	}
123	return uint8(value*255 + 0.5)
124}
125
126func clampUnit(value float64) float64 {
127	if math.IsNaN(value) || value <= 0 {
128		return 0
129	}
130	if value >= 1 {
131		return 1
132	}
133	return value
134}
135
136func square(value float64) float64 {
137	return value * value
138}
139
140func cube(value float64) float64 {
141	return value * value * value
142}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close