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(``, iconSize, iconSize, iconSize, iconSize)) // Add background rectangle if specified if background != "" && backgroundOp > 0 { if backgroundOp >= 1.0 { svg.WriteString(fmt.Sprintf(``, background)) } else { svg.WriteString(fmt.Sprintf(``, 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(``, color, dataString)) } } // SVG closing tag svg.WriteString("") 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) }