Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

svg_generator.gno

7.01 Kb · 243 lines
  1package gnft
  2
  3import (
  4	b64 "encoding/base64"
  5	"errors"
  6	"math/rand"
  7	"strconv"
  8	"strings"
  9
 10	"gno.land/p/gnoswap/deps/tokens/grc721"
 11	ufmt "gno.land/p/nt/ufmt/v0"
 12)
 13
 14// MUST BE IMMUTABLE, DO NOT MODIFY.
 15// SVG template structure:
 16// The template is split at variable insertion points for efficient string concatenation.
 17// Full template with placeholders (for reference):
 18//
 19//	<svg width="135" height="135" viewBox="0 0 135 135" fill="none" xmlns="...">
 20//	  <g clip-path="url(#clip0_7698_56846)">
 21//	    <circle cx="67.5" cy="67.5" r="67.5" fill="url(#paint0_linear_7698_56846)"/>
 22//	    ... (path elements) ...
 23//	  </g>
 24//	  <defs>
 25//	    <linearGradient id="paint0_linear_7698_56846"
 26//	      x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}"   <-- variables
 27//	      gradientUnits="userSpaceOnUse">
 28//	      <stop stop-color="{color1}"/>             <-- variable
 29//	      <stop offset="1" stop-color="{color2}"/>  <-- variable
 30//	    </linearGradient>
 31//	    ...
 32//	  </defs>
 33//	</svg>
 34
 35// svgTemplate holds pre-split template parts for efficient concatenation.
 36// Usage: svgTemplate[0] + clipID + svgTemplate[1] + paintID + ... + color2 + svgTemplate[10]
 37var svgTemplate = [11]string{
 38	// [0] SVG header and body (before clip-path id)
 39	`<svg width="135" height="135" viewBox="0 0 135 135" fill="none" xmlns="http://www.w3.org/2000/svg">
 40<g clip-path="url(#`,
 41	// [1] between clip-path id and fill id
 42	`)">
 43<circle cx="67.5" cy="67.5" r="67.5" fill="url(#`,
 44	// [2] between fill id and gradient id
 45	`)"/>
 46<path d="M51.2905 42.9449L66.4895 33L97 52.8061L81.8241 62.7425L51.2905 42.9449Z" fill="white"/>
 47<path d="M51.6055 67.5059L66.8044 57.561L97 77.0657L82.1046 87.1793L51.6055 67.5059Z" fill="white" fill-opacity="0.4"/>
 48<path d="M36.0464 81.7559L51.2905 71.811L81.7336 91.6547L66.4895 101.508L36.0464 81.7559Z" fill="white" fill-opacity="0.6"/>
 49<path d="M36.001 52.8055L51.2884 42.9177L51.2884 71.8145L36.001 81.779L36.001 52.8055Z" fill="white"/>
 50<path d="M82.1051 87.1797L97.0016 77.0662L97.0016 81.7029L81.7896 91.629L82.1051 87.1797Z" fill="white" fill-opacity="0.5"/>
 51</g>
 52<defs>
 53<linearGradient id="`,
 54	// [3] between gradient id and x1
 55	`" x1="`,
 56	// [4] between x1 and y1
 57	`" y1="`,
 58	// [5] between y1 and x2
 59	`" x2="`,
 60	// [6] between x2 and y2
 61	`" y2="`,
 62	// [7] between y2 and color1
 63	`" gradientUnits="userSpaceOnUse">
 64<stop stop-color="`,
 65	// [8] between color1 and color2
 66	`"/>
 67<stop offset="1" stop-color="`,
 68	// [9] between color2 and clipPath id
 69	`"/>
 70</linearGradient>
 71<clipPath id="`,
 72	// [10] SVG footer (after clipPath id)
 73	`">
 74<rect width="135" height="135" fill="white"/>
 75</clipPath>
 76</defs>
 77</svg>
 78`,
 79}
 80
 81// charset contains valid hex digits for color generation.
 82const charset = "0123456789ABCDEF"
 83
 84// Parameter range constants for gradient coordinates.
 85const (
 86	x1Min = 7
 87	x1Max = 13
 88	y1Min = 7
 89	y1Max = 13
 90	x2Min = 121
 91	x2Max = 126
 92	y2Min = 121
 93	y2Max = 126
 94
 95	x1Range = x1Max - x1Min + 1
 96	y1Range = y1Max - y1Min + 1
 97	x2Range = x2Max - x2Min + 1
 98	y2Range = y2Max - y2Min + 1
 99)
100
101// genImageParamsString generates random gradient parameters and returns them as a compact string.
102// Format: "x1,y1,x2,y2,color1,color2" (e.g., "10,12,125,123,#AABBCC,#DDEEFF")
103func genImageParamsString(r *rand.Rand) string {
104	x1 := x1Min + r.Uint64N(x1Range)
105	y1 := y1Min + r.Uint64N(y1Range)
106	x2 := x2Min + r.Uint64N(x2Range)
107	y2 := y2Min + r.Uint64N(y2Range)
108
109	var buf1 [7]byte
110	var buf2 [7]byte
111	buf1[0] = '#'
112	buf2[0] = '#'
113	for i := 1; i < 7; i++ {
114		buf1[i] = charset[r.IntN(16)]
115		buf2[i] = charset[r.IntN(16)]
116	}
117	color1 := string(buf1[:])
118	color2 := string(buf2[:])
119
120	return strconv.Itoa(int(x1)) + "," + strconv.Itoa(int(y1)) + "," +
121		strconv.Itoa(int(x2)) + "," + strconv.Itoa(int(y2)) + "," +
122		color1 + "," + color2
123}
124
125// ImageParams holds parsed and validated image parameters.
126type ImageParams struct {
127	x1, y1, x2, y2 int
128	color1, color2 string
129}
130
131// validateCoordinate checks if a coordinate value is within valid range.
132// Returns error with details about which coordinate failed if out of range.
133func validateCoordinate(value int, min int, max int, name string) error {
134	if value < min || value > max {
135		details := ufmt.Sprintf("%s=%d (expected range: [%d, %d])", name, value, min, max)
136		return makeErrorWithDetails(errInvalidTokenParamsRange, details)
137	}
138	return nil
139}
140
141// parseImageParams parses and validates parameters in one step.
142// Returns parsed ImageParams pointer or error.
143// Returns nil on error to avoid allocating zero-value struct.
144// Expected format: "x1,y1,x2,y2,color1,color2"
145func parseImageParams(s string) (*ImageParams, error) {
146	parts := strings.Split(s, ",")
147	if len(parts) != 6 {
148		return nil, errors.New(errInvalidTokenParams)
149	}
150
151	x1, err := strconv.Atoi(parts[0])
152	if err != nil {
153		return nil, errors.New(errInvalidTokenParams)
154	}
155
156	y1, err := strconv.Atoi(parts[1])
157	if err != nil {
158		return nil, errors.New(errInvalidTokenParams)
159	}
160
161	x2, err := strconv.Atoi(parts[2])
162	if err != nil {
163		return nil, errors.New(errInvalidTokenParams)
164	}
165
166	y2, err := strconv.Atoi(parts[3])
167	if err != nil {
168		return nil, errors.New(errInvalidTokenParams)
169	}
170
171	// Validate coordinate ranges with detailed error messages
172	if err := validateCoordinate(x1, x1Min, x1Max, "x1"); err != nil {
173		return nil, err
174	}
175	if err := validateCoordinate(y1, y1Min, y1Max, "y1"); err != nil {
176		return nil, err
177	}
178	if err := validateCoordinate(x2, x2Min, x2Max, "x2"); err != nil {
179		return nil, err
180	}
181	if err := validateCoordinate(y2, y2Min, y2Max, "y2"); err != nil {
182		return nil, err
183	}
184
185	color1 := parts[4]
186	color2 := parts[5]
187
188	// Validate color format (#XXXXXX)
189	if !isValidHexColor(color1) || !isValidHexColor(color2) {
190		return nil, errors.New(errInvalidColorFormat)
191	}
192
193	return &ImageParams{
194		x1:     x1,
195		y1:     y1,
196		x2:     x2,
197		y2:     y2,
198		color1: color1,
199		color2: color2,
200	}, nil
201}
202
203// generateImageURI converts parsed image parameters to a base64-encoded SVG image URI.
204func (params ImageParams) generateImageURI(tid grc721.TokenID) string {
205	svg := params.generateSVG(tid)
206	sEnc := b64.StdEncoding.EncodeToString([]byte(svg))
207
208	return "data:image/svg+xml;base64," + sEnc
209}
210
211// generateSVG generates SVG image from parsed and validated parameters.
212func (params ImageParams) generateSVG(tid grc721.TokenID) string {
213	suffix := svgIDSuffix(tid)
214	paintID := "paint0_linear_" + suffix
215	clipID := "clip0_" + suffix
216	return svgTemplate[0] + clipID + svgTemplate[1] + paintID + svgTemplate[2] + paintID +
217		svgTemplate[3] + strconv.Itoa(params.x1) + svgTemplate[4] + strconv.Itoa(params.y1) +
218		svgTemplate[5] + strconv.Itoa(params.x2) + svgTemplate[6] + strconv.Itoa(params.y2) +
219		svgTemplate[7] + params.color1 + svgTemplate[8] + params.color2 + svgTemplate[9] +
220		clipID + svgTemplate[10]
221}
222
223func svgIDSuffix(tid grc721.TokenID) string {
224	return "gnft_" + string(tid)
225}
226
227// isValidHexColor checks if a string is a valid hex color in #XXXXXX format.
228func isValidHexColor(color string) bool {
229	if len(color) != 7 || color[0] != '#' {
230		return false
231	}
232
233	for i := 1; i < 7; i++ {
234		c := color[i]
235
236		isHex := (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')
237		if !isHex {
238			return false
239		}
240	}
241
242	return true
243}