mirror of
https://github.com/alecthomas/chroma.git
synced 2025-07-17 01:22:22 +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:
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))
|
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 {
|
func (c Colour) BrightenOrDarken(factor float64) Colour {
|
||||||
if c.Brightness() < 0.5 {
|
if c.Brightness() < 0.5 {
|
||||||
return c.Brighten(factor)
|
return c.Brighten(factor)
|
||||||
@ -100,7 +100,35 @@ func (c Colour) BrightenOrDarken(factor float64) Colour {
|
|||||||
return c.Brighten(-factor)
|
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 {
|
func (c Colour) Brightness() float64 {
|
||||||
return (float64(c.Red()) + float64(c.Green()) + float64(c.Blue())) / 255.0 / 3.0
|
return (float64(c.Red()) + float64(c.Green()) + float64(c.Blue())) / 255.0 / 3.0
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package chroma
|
package chroma
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -40,3 +41,48 @@ func TestColourBrightess(t *testing.T) {
|
|||||||
actual := NewColour(128, 128, 128).Brightness()
|
actual := NewColour(128, 128, 128).Brightness()
|
||||||
assert.True(t, distance(128, uint8(actual*255.0)) <= 2)
|
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 {
|
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])
|
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.
|
// Add an entry to the Style map.
|
||||||
@ -175,6 +178,25 @@ func (s *StyleBuilder) AddEntry(ttype TokenType, entry StyleEntry) *StyleBuilder
|
|||||||
return s
|
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) {
|
func (s *StyleBuilder) Build() (*Style, error) {
|
||||||
style := &Style{
|
style := &Style{
|
||||||
Name: s.name,
|
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:#ffffff", style.Get(LineHighlight).String())
|
||||||
assert.Equal(t, "bg:#fffff1", style.Get(LineNumbers).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())
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user