cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / ui / theme / contrast.go
contrast.go 3.0K
1package theme23import (4 "image/color"5 "math"6)78// Contrast correction for color schemes, adapted from puzzletea's theme9// package (same author): WCAG 2 contrast, and a foreground that falls10// short moved just far enough to be readable.1112// Contrast minimums.13const (14 minText = 4.5 // text: WCAG AA15 minTextAAA = 7.0 // text: WCAG AAA, the high-contrast schemes' minimum16 minSecondary = 3.0 // hints, muted text, borders, chart axes17 minDisabled = 2.0 // unavailable items stay visible but recede18 minDistinct = 1.2 // a background role against the screen, so bars and highlights show19)2021func luminance(c color.Color) float64 {22 r, g, b, _ := c.RGBA()23 lin := func(v uint32) float64 {24 f := float64(v) / 0xffff25 if f <= 0.04045 {26 return f / 12.9227 }28 return math.Pow((f+0.055)/1.055, 2.4)29 }30 return 0.2126*lin(r) + 0.7152*lin(g) + 0.0722*lin(b)31}3233// contrast is the WCAG contrast ratio of two colors, from 1 to 21.34func contrast(a, b color.Color) float64 {35 la, lb := luminance(a), luminance(b)36 if la < lb {37 la, lb = lb, la38 }39 return (la + 0.05) / (lb + 0.05)40}4142// readable returns fg, or when it has less than min contrast with bg, fg43// moved just far enough toward toward (the scheme's text color, which44// keeps grays gray and hues close) to reach it, or failing that toward45// black or white.46func readable(fg, bg, toward color.Color, min float64) color.Color {47 if contrast(fg, bg) >= min {48 return fg49 }50 if toward != nil && contrast(toward, bg) >= min {51 return blendUntil(fg, toward, bg, min)52 }53 return nudge(fg, bg, min)54}5556// blendUntil moves fg toward end until it has min contrast with bg; end57// itself must have it.58func blendUntil(fg, end, bg color.Color, min float64) color.Color {59 lo, hi := 0.0, 1.060 for range 24 {61 mid := (lo + hi) / 262 if contrast(blend(fg, end, mid), bg) >= min {63 hi = mid64 } else {65 lo = mid66 }67 }68 return blend(fg, end, hi)69}7071// nudge blends fg toward white or black, whichever moves it away from72// bg, just far enough to reach min contrast.73func nudge(fg, bg color.Color, min float64) color.Color {74 var end color.Color = color.White75 if luminance(fg) <= luminance(bg) {76 end = color.Black77 }78 if contrast(end, bg) < min {79 if end == color.White {80 end = color.Black81 } else {82 end = color.White83 }84 }85 return blendUntil(fg, end, bg, min)86}8788// distinct returns bg, or when it's too close to the screen to show, bg89// moved toward toward until it stands apart.90func distinct(bg, screen, toward color.Color) color.Color {91 if contrast(bg, screen) >= minDistinct {92 return bg93 }94 lo, hi := 0.0, 1.095 for range 24 {96 mid := (lo + hi) / 297 if contrast(blend(bg, toward, mid), screen) >= minDistinct {98 hi = mid99 } else {100 lo = mid101 }102 }103 return blend(bg, toward, hi)104}105106// blend is t of the way from a to b.107func blend(a, b color.Color, t float64) color.Color {108 ar, ag, ab, _ := a.RGBA()109 br, bg, bb, _ := b.RGBA()110 mix := func(x, y uint32) uint8 {111 return uint8(float64(x>>8)*(1-t) + float64(y>>8)*t + 0.5)112 }113 return color.RGBA{R: mix(ar, br), G: mix(ag, bg), B: mix(ab, bb), A: 0xff}114}