mirror of
https://github.com/alecthomas/chroma.git
synced 2025-03-19 21:10:15 +02:00
Add Colour.ClampBrightness and StyleBuilder.Transform.
These functions can make it easier to to update a style's contrast when viewed against light and dark backgrounds. See #353. Also avoid a segfault when Get is called on a StyleBuilder that was created using NewStyleBuilder (as opposed to Style.Builder).
This commit is contained in:
parent
e1a35d4eea
commit
dbb09a52a8
32
colour.go
32
colour.go
@ -92,7 +92,7 @@ func (c Colour) Brighten(factor float64) Colour {
|
||||
return NewColour(uint8(r), uint8(g), uint8(b))
|
||||
}
|
||||
|
||||
// BrightenOrDarken brightens a colour if it is < 0.5 brighteness or darkens if > 0.5 brightness.
|
||||
// BrightenOrDarken brightens a colour if it is < 0.5 brightness or darkens if > 0.5 brightness.
|
||||
func (c Colour) BrightenOrDarken(factor float64) Colour {
|
||||
if c.Brightness() < 0.5 {
|
||||
return c.Brighten(factor)
|
||||
@ -100,7 +100,35 @@ func (c Colour) BrightenOrDarken(factor float64) Colour {
|
||||
return c.Brighten(-factor)
|
||||
}
|
||||
|
||||
// Brightness of the colour (roughly) in the range 0.0 to 1.0
|
||||
// ClampBrightness returns a copy of this colour with its brightness adjusted such that
|
||||
// it falls within the range [min, max] (or very close to it due to rounding errors).
|
||||
// The supplied values use the same [0.0, 1.0] range as Brightness.
|
||||
func (c Colour) ClampBrightness(min, max float64) Colour {
|
||||
if !c.IsSet() {
|
||||
return c
|
||||
}
|
||||
|
||||
min = math.Max(min, 0)
|
||||
max = math.Min(max, 1)
|
||||
current := c.Brightness()
|
||||
target := math.Min(math.Max(current, min), max)
|
||||
if current == target {
|
||||
return c
|
||||
}
|
||||
|
||||
r := float64(c.Red())
|
||||
g := float64(c.Green())
|
||||
b := float64(c.Blue())
|
||||
rgb := r + g + b
|
||||
if target > current {
|
||||
// Solve for x: target == ((255-r)*x + r + (255-g)*x + g + (255-b)*x + b) / 255 / 3
|
||||
return c.Brighten((target*255*3 - rgb) / (255*3 - rgb))
|
||||
}
|
||||
// Solve for x: target == (r*(x+1) + g*(x+1) + b*(x+1)) / 255 / 3
|
||||
return c.Brighten((target*255*3)/rgb - 1)
|
||||
}
|
||||
|
||||
// Brightness of the colour (roughly) in the range 0.0 to 1.0.
|
||||
func (c Colour) Brightness() float64 {
|
||||
return (float64(c.Red()) + float64(c.Green()) + float64(c.Blue())) / 255.0 / 3.0
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package chroma
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -40,3 +41,48 @@ func TestColourBrightess(t *testing.T) {
|
||||
actual := NewColour(128, 128, 128).Brightness()
|
||||
assert.True(t, distance(128, uint8(actual*255.0)) <= 2)
|
||||
}
|
||||
|
||||
// hue returns c's hue. See https://stackoverflow.com/a/23094494.
|
||||
func hue(c Colour) float64 {
|
||||
r := float64(c.Red()) / 255
|
||||
g := float64(c.Green()) / 255
|
||||
b := float64(c.Blue()) / 255
|
||||
|
||||
min := math.Min(math.Min(r, g), b)
|
||||
max := math.Max(math.Max(r, g), b)
|
||||
|
||||
switch {
|
||||
case r == min:
|
||||
return (g - b) / (max - min)
|
||||
case g == min:
|
||||
return 2 + (b-r)/(max-min)
|
||||
default:
|
||||
return 4 + (r-g)/(max-min)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColourClampBrightness(t *testing.T) {
|
||||
const delta = 0.01 // used for brightness and hue comparisons
|
||||
|
||||
// Start with a colour with a brightness close to 0.5.
|
||||
initial := NewColour(0, 128, 255)
|
||||
br := initial.Brightness()
|
||||
assert.InDelta(t, 0.5, br, delta)
|
||||
|
||||
// Passing a range that includes the colour's brightness should be a no-op.
|
||||
assert.Equal(t, initial.String(), initial.ClampBrightness(br-0.01, br+0.01).String())
|
||||
|
||||
// Clamping to [0, 0] or [1, 1] should produce black or white, respectively.
|
||||
assert.Equal(t, "#000000", initial.ClampBrightness(0, 0).String())
|
||||
assert.Equal(t, "#ffffff", initial.ClampBrightness(1, 1).String())
|
||||
|
||||
// Clamping to a brighter or darker range should produce the requested
|
||||
// brightness while preserving the colour's hue.
|
||||
brighter := initial.ClampBrightness(0.75, 1)
|
||||
assert.InDelta(t, 0.75, brighter.Brightness(), delta)
|
||||
assert.InDelta(t, hue(initial), hue(brighter), delta)
|
||||
|
||||
darker := initial.ClampBrightness(0, 0.25)
|
||||
assert.InDelta(t, 0.25, darker.Brightness(), delta)
|
||||
assert.InDelta(t, hue(initial), hue(darker), delta)
|
||||
}
|
||||
|
26
style.go
26
style.go
@ -157,9 +157,12 @@ func (s *StyleBuilder) AddAll(entries StyleEntries) *StyleBuilder {
|
||||
}
|
||||
|
||||
func (s *StyleBuilder) Get(ttype TokenType) StyleEntry {
|
||||
// This is less than ideal, but it's the price for having to check errors on each Add().
|
||||
// This is less than ideal, but it's the price for not having to check errors on each Add().
|
||||
entry, _ := ParseStyleEntry(s.entries[ttype])
|
||||
return entry.Inherit(s.parent.Get(ttype))
|
||||
if s.parent != nil {
|
||||
entry = entry.Inherit(s.parent.Get(ttype))
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
// Add an entry to the Style map.
|
||||
@ -175,6 +178,25 @@ func (s *StyleBuilder) AddEntry(ttype TokenType, entry StyleEntry) *StyleBuilder
|
||||
return s
|
||||
}
|
||||
|
||||
// Transform passes each style entry currently defined in the builder to the supplied
|
||||
// function and saves the returned value. This can be used to adjust a style's colours;
|
||||
// see Colour's ClampBrightness function, for example.
|
||||
func (s *StyleBuilder) Transform(transform func(StyleEntry) StyleEntry) *StyleBuilder {
|
||||
types := make(map[TokenType]struct{})
|
||||
for tt := range s.entries {
|
||||
types[tt] = struct{}{}
|
||||
}
|
||||
if s.parent != nil {
|
||||
for _, tt := range s.parent.Types() {
|
||||
types[tt] = struct{}{}
|
||||
}
|
||||
}
|
||||
for tt := range types {
|
||||
s.AddEntry(tt, transform(s.Get(tt)))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StyleBuilder) Build() (*Style, error) {
|
||||
style := &Style{
|
||||
Name: s.name,
|
||||
|
@ -63,3 +63,41 @@ func TestSynthesisedStyleClone(t *testing.T) {
|
||||
assert.Equal(t, "bg:#ffffff", style.Get(LineHighlight).String())
|
||||
assert.Equal(t, "bg:#fffff1", style.Get(LineNumbers).String())
|
||||
}
|
||||
|
||||
func TestStyleBuilderTransform(t *testing.T) {
|
||||
orig, err := NewStyle("test", StyleEntries{
|
||||
Name: "#000",
|
||||
NameVariable: "bold #f00",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Derive a style that inherits entries from orig.
|
||||
builder := orig.Builder()
|
||||
builder.Add(NameVariableGlobal, "#f30")
|
||||
deriv, err := builder.Build()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Use Transform to brighten or darken all of the colours in the derived style.
|
||||
light, err := deriv.Builder().Transform(func(se StyleEntry) StyleEntry {
|
||||
se.Colour = se.Colour.ClampBrightness(0.9, 1)
|
||||
return se
|
||||
}).Build()
|
||||
assert.Nilf(t, err, "Transform failed: %v", err)
|
||||
assert.GreaterOrEqual(t, light.Get(Name).Colour.Brightness(), 0.89)
|
||||
assert.GreaterOrEqual(t, light.Get(NameVariable).Colour.Brightness(), 0.89)
|
||||
assert.GreaterOrEqual(t, light.Get(NameVariableGlobal).Colour.Brightness(), 0.89)
|
||||
|
||||
dark, err := deriv.Builder().Transform(func(se StyleEntry) StyleEntry {
|
||||
se.Colour = se.Colour.ClampBrightness(0, 0.1)
|
||||
return se
|
||||
}).Build()
|
||||
assert.Nilf(t, err, "Transform failed: %v", err)
|
||||
assert.LessOrEqual(t, dark.Get(Name).Colour.Brightness(), 0.11)
|
||||
assert.LessOrEqual(t, dark.Get(NameVariable).Colour.Brightness(), 0.11)
|
||||
assert.LessOrEqual(t, dark.Get(NameVariableGlobal).Colour.Brightness(), 0.11)
|
||||
|
||||
// The original styles should be unchanged.
|
||||
assert.Equal(t, "#000000", orig.Get(Name).Colour.String())
|
||||
assert.Equal(t, "#ff0000", orig.Get(NameVariable).Colour.String())
|
||||
assert.Equal(t, "#ff3300", deriv.Get(NameVariableGlobal).Colour.String())
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user