d2/lib/color/gradient.go
2025-04-11 01:43:47 +05:30

254 lines
5.8 KiB
Go

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)
}
var URLGradientID = regexp.MustCompile(`^url\('#grad-[a-f0-9]{40}'\)$`)
func IsURLGradientID(color string) bool {
return URLGradientID.MatchString(color)
}