This commit is contained in:
Kevin McIntyre
2025-06-18 01:00:00 -04:00
commit f84b511895
228 changed files with 42509 additions and 0 deletions

172
internal/renderer/svg.go Normal file
View File

@@ -0,0 +1,172 @@
package renderer
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/kevin/go-jdenticon/internal/engine"
)
// SVGPath represents an SVG path element
type SVGPath struct {
data strings.Builder
}
// AddPolygon adds a polygon to the SVG path
func (p *SVGPath) AddPolygon(points []engine.Point) {
if len(points) == 0 {
return
}
// Move to first point
p.data.WriteString(fmt.Sprintf("M%s %s", svgValue(points[0].X), svgValue(points[0].Y)))
// Line to subsequent points
for i := 1; i < len(points); i++ {
p.data.WriteString(fmt.Sprintf("L%s %s", svgValue(points[i].X), svgValue(points[i].Y)))
}
// Close path
p.data.WriteString("Z")
}
// AddCircle adds a circle to the SVG path
func (p *SVGPath) AddCircle(topLeft engine.Point, size float64, counterClockwise bool) {
sweepFlag := "1"
if counterClockwise {
sweepFlag = "0"
}
radius := size / 2
centerX := topLeft.X + radius
centerY := topLeft.Y + radius
svgRadius := svgValue(radius)
svgDiameter := svgValue(size)
svgArc := fmt.Sprintf("a%s,%s 0 1,%s ", svgRadius, svgRadius, sweepFlag)
// Move to start point (left side of circle)
startX := centerX - radius
startY := centerY
p.data.WriteString(fmt.Sprintf("M%s %s", svgValue(startX), svgValue(startY)))
p.data.WriteString(svgArc + svgDiameter + ",0")
p.data.WriteString(svgArc + "-" + svgDiameter + ",0")
}
// DataString returns the SVG path data string
func (p *SVGPath) DataString() string {
return p.data.String()
}
// SVGRenderer implements the Renderer interface for SVG output
type SVGRenderer struct {
*BaseRenderer
pathsByColor map[string]*SVGPath
colorOrder []string
}
// NewSVGRenderer creates a new SVG renderer
func NewSVGRenderer(iconSize int) *SVGRenderer {
return &SVGRenderer{
BaseRenderer: NewBaseRenderer(iconSize),
pathsByColor: make(map[string]*SVGPath),
colorOrder: make([]string, 0),
}
}
// SetBackground sets the background color and opacity
func (r *SVGRenderer) SetBackground(fillColor string, opacity float64) {
r.BaseRenderer.SetBackground(fillColor, opacity)
}
// BeginShape marks the beginning of a new shape with the specified color
func (r *SVGRenderer) BeginShape(color string) {
r.BaseRenderer.BeginShape(color)
if _, exists := r.pathsByColor[color]; !exists {
r.pathsByColor[color] = &SVGPath{}
r.colorOrder = append(r.colorOrder, color)
}
}
// EndShape marks the end of the currently drawn shape
func (r *SVGRenderer) EndShape() {
// No action needed for SVG
}
// getCurrentPath returns the current path for the active color
func (r *SVGRenderer) getCurrentPath() *SVGPath {
currentColor := r.GetCurrentColor()
if currentColor == "" {
return nil
}
return r.pathsByColor[currentColor]
}
// AddPolygon adds a polygon with the current fill color to the SVG
func (r *SVGRenderer) AddPolygon(points []engine.Point) {
if path := r.getCurrentPath(); path != nil {
path.AddPolygon(points)
}
}
// AddCircle adds a circle with the current fill color to the SVG
func (r *SVGRenderer) AddCircle(topLeft engine.Point, size float64, invert bool) {
if path := r.getCurrentPath(); path != nil {
path.AddCircle(topLeft, size, invert)
}
}
// ToSVG generates the final SVG XML string
func (r *SVGRenderer) ToSVG() string {
var svg strings.Builder
iconSize := r.GetSize()
background, backgroundOp := r.GetBackground()
// SVG opening tag with namespace and dimensions
svg.WriteString(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">`,
iconSize, iconSize, iconSize, iconSize))
// Add background rectangle if specified
if background != "" && backgroundOp > 0 {
if backgroundOp >= 1.0 {
svg.WriteString(fmt.Sprintf(`<rect width="100%%" height="100%%" fill="%s"/>`, background))
} else {
svg.WriteString(fmt.Sprintf(`<rect width="100%%" height="100%%" fill="%s" opacity="%.2f"/>`,
background, backgroundOp))
}
}
// Add paths for each color (in insertion order to preserve z-order)
for _, color := range r.colorOrder {
path := r.pathsByColor[color]
dataString := path.DataString()
if dataString != "" {
svg.WriteString(fmt.Sprintf(`<path fill="%s" d="%s"/>`, color, dataString))
}
}
// SVG closing tag
svg.WriteString("</svg>")
return svg.String()
}
// svgValue rounds a float64 to one decimal place, mimicking the Jdenticon JS implementation's
// "round half up" behavior. It also formats the number to a minimal string representation.
func svgValue(value float64) string {
// Use math.Floor to replicate the "round half up" logic from the JS implementation.
// JavaScript: ((value * 10 + 0.5) | 0) / 10
rounded := math.Floor(value*10 + 0.5) / 10
// Format to an integer string if there's no fractional part.
if rounded == math.Trunc(rounded) {
return strconv.Itoa(int(rounded))
}
// Otherwise, format to one decimal place.
return strconv.FormatFloat(rounded, 'f', 1, 64)
}