Mercurial > gemma
view pkg/models/colors.go @ 5675:b8da63027b48 sr-v2
Started to separate mesh serialisation in different files.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Sat, 10 Feb 2024 22:22:03 +0100 |
parents | 18d5461bec5d |
children |
line wrap: on
line source
// This is Free Software under GNU Affero General Public License v >= 3.0 // without warranty, see README.md and license for details. // // SPDX-License-Identifier: AGPL-3.0-or-later // License-Filename: LICENSES/AGPL-3.0.txt // // Copyright (C) 2019 by via donau // – Österreichische Wasserstraßen-Gesellschaft mbH // Software engineering by Intevation GmbH // // Author(s): // * Sascha L. Teichmann <sascha.teichmann@intevation.de> // * Markus Kottländer <markus.kottlaender@intevation.de> package models import ( "fmt" "image/color" "math" "sort" "strconv" "strings" ) type ( // ColorValue models a tuple of a floating point value and a color. ColorValue struct { Value float64 Color color.RGBA } // ColorValues are a list of color values. // Colors between the given ones are linear interpolated. ColorValues []ColorValue // ClassBreak is a potential open interval of values associated // with a color. ClassBreak struct { High float64 HasHigh bool Low float64 HasLow bool Col color.RGBA } ) // Color gives a HTML compatible string representation of the color // of the class break. func (cb *ClassBreak) Color() string { return fmt.Sprintf("#%02x%02x%02x", cb.Col.R, cb.Col.G, cb.Col.B, ) } // ClassBreaks converts a list of colors to a list of class breaks. // The first an the last class break are open on the respective end. func (cc ColorValues) ClassBreaks() []ClassBreak { cbs := make([]ClassBreak, len(cc), len(cc)+1) for i := range cc { if i > 0 { cbs[i].Low = cc[i-1].Value cbs[i].HasLow = true } cbs[i].High = cc[i].Value cbs[i].HasHigh = true cbs[i].Col = cc[i].Color } if len(cc) > 0 { cbs = append(cbs, ClassBreak{ Col: cc[len(cc)-1].Color, Low: cc[len(cc)-1].Value, HasLow: true, }) } return cbs } // Heights extracts the values friom the color value pairs. func (cc ColorValues) Heights() []float64 { heights := make([]float64, len(cc)) for i := range cc { heights[i] = cc[i].Value } return heights } // Clip does the same as Interpolate // but if the value is out of bounds the value of the nearest border // is returned. func (cc ColorValues) Clip(v float64) color.RGBA { if len(cc) == 0 { return color.RGBA{} } if v < cc[0].Value { return cc[0].Color } if v > cc[len(cc)-1].Value { return cc[len(cc)-1].Color } c, _ := cc.Interpolate(v) return c } // Interpolate interpolates the color linearly between the // given values of the color values. // If the value is out of bounds false is returned. func (cc ColorValues) Interpolate(v float64) (color.RGBA, bool) { if len(cc) == 0 || v < cc[0].Value || v > cc[len(cc)-1].Value { return color.RGBA{}, false } if len(cc) == 1 { return cc[0].Color, cc[0].Value == v } for i := 0; i < len(cc)-1; i++ { v1 := cc[i].Value v2 := cc[i+1].Value if v1 <= v && v <= v2 { // f(v1) = 0 // f(v2) = 1 // 0 = m*v1 + c <=> c = -m*v1 // 1 = m*v2 + c // (1 - 0) = m*(v2 - v1) // m = 1/(v2 - v1) for v2 != v1 if v1 == v2 { return color.RGBA{ R: uint8((uint16(cc[i].Color.R) + uint16(cc[i+1].Color.R)) / 2), G: uint8((uint16(cc[i].Color.G) + uint16(cc[i+1].Color.G)) / 2), B: uint8((uint16(cc[i].Color.B) + uint16(cc[i+1].Color.B)) / 2), A: 0xff, }, true } m := 1 / (v2 - v1) c := -m * v1 s := v*m + c interpolate := func(a, b uint8) uint8 { v := math.Round(float64(a) + (float64(b)-float64(a))*s) if v < 0 { return 0 } if v > 255 { return 255 } return uint8(v) } return color.RGBA{ R: interpolate(cc[i].Color.R, cc[i+1].Color.R), G: interpolate(cc[i].Color.G, cc[i+1].Color.G), B: interpolate(cc[i].Color.B, cc[i+1].Color.B), A: 0xff, }, true } } return color.RGBA{}, false } // ParseColorValues parses a string to a list of color values. // The value/color pairs are separated by ','. // A color/value pair is separated with ':'. // f ':' is missing it is assumed that only the value is given. // The missing color is interpolated from fore and aftermost // value/colors pairs. func ParseColorValues(s string) (ColorValues, error) { var err error parseFloat := func(s string) float64 { var v float64 if err == nil { v, err = strconv.ParseFloat(s, 64) } return v } parseColor := func(s string) color.RGBA { if err != nil { return color.RGBA{} } s = strings.Map(func(r rune) rune { if ('0' <= r && r <= '9') || ('a' <= r && r <= 'f') || ('A' <= r && r <= 'F') { return r } return -1 }, s) var v int64 v, err = strconv.ParseInt(s, 16, 64) return color.RGBA{ R: uint8(v >> 16), G: uint8(v >> 8), B: uint8(v >> 0), A: 0xff, } } lines := strings.Split(s, ",") // first pass: find defined colors var defined ColorValues for _, line := range lines { // ignore the lines w/o a color. if !strings.ContainsRune(line, ':') { continue } parts := strings.SplitN(line, ":", 2) if len(parts) < 2 { continue } value := parseFloat(parts[0]) color := parseColor(parts[1]) defined = append(defined, ColorValue{ Value: value, Color: color, }) } if err != nil { return nil, err } sort.Slice(defined, func(i, j int) bool { return defined[i].Value < defined[j].Value }) // second pass: interpolate the rest var final ColorValues for _, line := range lines { if idx := strings.IndexRune(line, ':'); idx >= 0 { line = line[:idx] } value := parseFloat(line) if color, ok := defined.Interpolate(value); ok { final = append(final, ColorValue{ Value: value, Color: color, }) } } return final, err }