init
This commit is contained in:
172
internal/renderer/svg.go
Normal file
172
internal/renderer/svg.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user