package color
import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
)
type Gradient struct {
Type string
Direction string
ColorStops []ColorStop
ID string
}
type ColorStop struct {
Color string
Position string
}
func ParseGradient (cssGradient string ) (Gradient , error ) {
cssGradient = strings .TrimSpace (cssGradient )
re := regexp .MustCompile (`^(linear-gradient|radial-gradient)\((.*)\)$` )
matches := re .FindStringSubmatch (cssGradient )
if matches == nil {
return Gradient {}, errors .New ("invalid gradient syntax" )
}
gradientType := matches [1 ]
params := matches [2 ]
gradient := Gradient {
Type : strings .TrimSuffix (gradientType , "-gradient" ),
}
paramList := splitParams (params )
if len (paramList ) == 0 {
return Gradient {}, errors .New ("no parameters in gradient" )
}
firstParam := strings .TrimSpace (paramList [0 ])
if gradient .Type == "linear" && (strings .HasSuffix (firstParam , "deg" ) || strings .HasPrefix (firstParam , "to " )) {
gradient .Direction = firstParam
colorStops := paramList [1 :]
if len (colorStops ) == 0 {
return Gradient {}, errors .New ("no color stops in gradient" )
}
gradient .ColorStops = parseColorStops (colorStops )
} else if gradient .Type == "radial" && (firstParam == "circle" || firstParam == "ellipse" ) {
gradient .Direction = firstParam
colorStops := paramList [1 :]
if len (colorStops ) == 0 {
return Gradient {}, errors .New ("no color stops in gradient" )
}
gradient .ColorStops = parseColorStops (colorStops )
} else {
gradient .ColorStops = parseColorStops (paramList )
}
gradient .ID = UniqueGradientID (cssGradient )
return gradient , nil
}
func splitParams(params string ) []string {
var parts []string
var buf strings .Builder
nesting := 0
for _ , r := range params {
switch r {
case ',' :
if nesting == 0 {
parts = append (parts , buf .String ())
buf .Reset ()
continue
}
case '(' :
nesting ++
case ')' :
if nesting > 0 {
nesting --
}
}
buf .WriteRune (r )
}
if buf .Len () > 0 {
parts = append (parts , buf .String ())
}
return parts
}
func parseColorStops(params []string ) []ColorStop {
var colorStops []ColorStop
for _ , p := range params {
p = strings .TrimSpace (p )
parts := strings .Fields (p )
switch len (parts ) {
case 1 :
colorStops = append (colorStops , ColorStop {Color : parts [0 ]})
case 2 :
colorStops = append (colorStops , ColorStop {Color : parts [0 ], Position : parts [1 ]})
default :
continue
}
}
return colorStops
}
func GradientToSVG (gradient Gradient ) string {
switch gradient .Type {
case "linear" :
return LinearGradientToSVG (gradient )
case "radial" :
return RadialGradientToSVG (gradient )
default :
return ""
}
}
func LinearGradientToSVG (gradient Gradient ) string {
x1 , y1 , x2 , y2 := parseLinearGradientDirection (gradient .Direction )
var sb strings .Builder
sb .WriteString (fmt .Sprintf (`<linearGradient id="%s" ` , gradient .ID ))
sb .WriteString (fmt .Sprintf (`x1="%s" y1="%s" x2="%s" y2="%s">` , x1 , y1 , x2 , y2 ))
sb .WriteString ("\n" )
totalStops := len (gradient .ColorStops )
for i , cs := range gradient .ColorStops {
offset := cs .Position
if offset == "" {
offsetValue := float64 (i ) / float64 (totalStops -1 ) * 100
offset = fmt .Sprintf ("%.2f%%" , offsetValue )
}
sb .WriteString (fmt .Sprintf (`<stop offset="%s" stop-color="%s" />` , offset , cs .Color ))
sb .WriteString ("\n" )
}
sb .WriteString (`</linearGradient>` )
return sb .String ()
}
func parseLinearGradientDirection(direction string ) (x1 , y1 , x2 , y2 string ) {
x1 , y1 , x2 , y2 = "0%" , "0%" , "0%" , "100%"
direction = strings .TrimSpace (direction )
if strings .HasPrefix (direction , "to " ) {
dir := strings .TrimPrefix (direction , "to " )
dir = strings .TrimSpace (dir )
parts := strings .Fields (dir )
xStart , yStart := "50%" , "50%"
xEnd , yEnd := "50%" , "50%"
xDirSet , yDirSet := false , false
for _ , part := range parts {
switch part {
case "left" :
xStart = "100%"
xEnd = "0%"
xDirSet = true
case "right" :
xStart = "0%"
xEnd = "100%"
xDirSet = true
case "top" :
yStart = "100%"
yEnd = "0%"
yDirSet = true
case "bottom" :
yStart = "0%"
yEnd = "100%"
yDirSet = true
}
}
if !xDirSet {
xStart = "50%"
xEnd = "50%"
}
if !yDirSet {
yStart = "50%"
yEnd = "50%"
}
x1 , y1 = xStart , yStart
x2 , y2 = xEnd , yEnd
} else if strings .HasSuffix (direction , "deg" ) {
angleStr := strings .TrimSuffix (direction , "deg" )
angle , err := strconv .ParseFloat (strings .TrimSpace (angleStr ), 64 )
if err == nil {
cssAngle := angle
svgAngle := (90 - cssAngle ) * (math .Pi / 180 )
x1f := 50.0
y1f := 50.0
x2f := x1f + 50 *math .Cos (svgAngle )
y2f := y1f + 50 *math .Sin (svgAngle )
x1 = fmt .Sprintf ("%.2f%%" , x1f )
y1 = fmt .Sprintf ("%.2f%%" , y1f )
x2 = fmt .Sprintf ("%.2f%%" , x2f )
y2 = fmt .Sprintf ("%.2f%%" , y2f )
}
}
return x1 , y1 , x2 , y2
}
func RadialGradientToSVG (gradient Gradient ) string {
var sb strings .Builder
sb .WriteString (fmt .Sprintf (`<radialGradient id="%s">` , gradient .ID ))
sb .WriteString ("\n" )
totalStops := len (gradient .ColorStops )
for i , cs := range gradient .ColorStops {
offset := cs .Position
if offset == "" {
offsetValue := float64 (i ) / float64 (totalStops -1 ) * 100
offset = fmt .Sprintf ("%.2f%%" , offsetValue )
}
sb .WriteString (fmt .Sprintf (`<stop offset="%s" stop-color="%s" />` , offset , cs .Color ))
sb .WriteString ("\n" )
}
sb .WriteString (`</radialGradient>` )
return sb .String ()
}
func UniqueGradientID (cssGradient string ) string {
h := sha1 .New ()
h .Write ([]byte (cssGradient ))
hash := hex .EncodeToString (h .Sum (nil ))
return "grad-" + hash
}
var GradientRegex = regexp .MustCompile (`^(linear|radial)-gradient\((.+)\)$` )
func IsGradient (color string ) bool {
return GradientRegex .MatchString (color )
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .