117 lines
5.8 KiB
Go
117 lines
5.8 KiB
Go
package artifact
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"html"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
func imageMIME(ext string) string {
|
|
switch strings.ToLower(strings.TrimPrefix(ext, ".")) {
|
|
case "jpg", "jpeg":
|
|
return "image/jpeg"
|
|
case "webp":
|
|
return "image/webp"
|
|
case "svg":
|
|
return "image/svg+xml"
|
|
default:
|
|
return "image/png"
|
|
}
|
|
}
|
|
|
|
func trimLabel(s string, maxRunes int) string {
|
|
s = strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
|
|
if s == "" {
|
|
return "NEURAL HUNT"
|
|
}
|
|
if utf8.RuneCountInString(s) <= maxRunes {
|
|
return s
|
|
}
|
|
r := []rune(s)
|
|
return string(r[:maxRunes-1]) + "…"
|
|
}
|
|
|
|
// renderCardSVG converts raw generated artwork into the deterministic final
|
|
// collectible card. Keeping the typography/layout outside the image model makes
|
|
// all cards line up exactly and avoids model-generated fake text/logos.
|
|
func renderCardSVG(art []byte, artExt string, x win, traits collectionTraits) []byte {
|
|
encoded := base64.StdEncoding.EncodeToString(art)
|
|
series := html.EscapeString(strings.ToUpper(trimLabel(x.DisplayName, 28)))
|
|
theme := html.EscapeString(strings.ToUpper(trimLabel(traits.ThemeName, 24)))
|
|
rarity := html.EscapeString(traits.Rarity)
|
|
completed := x.Completed.UTC().Format("2006.01.02")
|
|
mime := imageMIME(artExt)
|
|
|
|
var sparkles strings.Builder
|
|
h := sha256.Sum256([]byte(x.ID + "|card|" + x.Winner + "|" + x.Seed))
|
|
for i := 0; i < 42; i++ {
|
|
b0 := int(h[i%32])
|
|
b1 := int(h[(i*7+5)%32])
|
|
xv := 62 + (b0*37+i*97)%900
|
|
yv := 54 + (b1*29+i*83)%1420
|
|
r := 1 + (b0+i)%4
|
|
op := 12 + (b1 % 34)
|
|
fmt.Fprintf(&sparkles, `<circle cx="%d" cy="%d" r="%d" fill="white" opacity="0.%02d"/>`, xv, yv, r, op)
|
|
}
|
|
|
|
svg := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1536" viewBox="0 0 1024 1536" role="img" aria-label="Neural Hunt collectible card">
|
|
<defs>
|
|
<clipPath id="cardClip"><rect x="26" y="26" width="972" height="1484" rx="58"/></clipPath>
|
|
<clipPath id="artClip"><rect x="42" y="42" width="940" height="1452" rx="48"/></clipPath>
|
|
<linearGradient id="metal" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#f4f7ff"/><stop offset=".18" stop-color="%s"/><stop offset=".48" stop-color="#101725"/><stop offset=".73" stop-color="%s"/><stop offset="1" stop-color="#e9f2ff"/></linearGradient>
|
|
<linearGradient id="topShade" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#050811" stop-opacity=".82"/><stop offset="1" stop-color="#050811" stop-opacity="0"/></linearGradient>
|
|
<linearGradient id="bottomShade" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#07101d" stop-opacity="0"/><stop offset=".34" stop-color="#07101d" stop-opacity=".58"/><stop offset="1" stop-color="#030711" stop-opacity=".96"/></linearGradient>
|
|
<linearGradient id="holo" x1="0" y1="0" x2="1" y2="1"><stop stop-color="%s" stop-opacity=".72"/><stop offset=".42" stop-color="#ffffff" stop-opacity=".22"/><stop offset=".67" stop-color="%s" stop-opacity=".64"/><stop offset="1" stop-color="#ffffff" stop-opacity=".06"/></linearGradient>
|
|
<filter id="shadow"><feDropShadow dx="0" dy="18" stdDeviation="22" flood-color="#000000" flood-opacity=".55"/></filter>
|
|
</defs>
|
|
<g filter="url(#shadow)">
|
|
<rect x="18" y="18" width="988" height="1500" rx="66" fill="url(#metal)"/>
|
|
<rect x="28" y="28" width="968" height="1480" rx="56" fill="#0b101b"/>
|
|
</g>
|
|
<g clip-path="url(#artClip)">
|
|
<image x="42" y="42" width="940" height="1452" preserveAspectRatio="xMidYMid slice" href="data:%s;base64,%s"/>
|
|
<rect x="42" y="42" width="940" height="360" fill="url(#topShade)"/>
|
|
<rect x="42" y="940" width="940" height="554" fill="url(#bottomShade)"/>
|
|
<path d="M-80 270 L760 -80 L1100 80 L180 480 Z" fill="url(#holo)" opacity=".10"/>
|
|
<path d="M120 1530 L1130 1030 L1130 1190 L330 1530 Z" fill="url(#holo)" opacity=".12"/>
|
|
<g>%s</g>
|
|
</g>
|
|
<rect x="42" y="42" width="940" height="1452" rx="48" fill="none" stroke="#ffffff" stroke-opacity=".28" stroke-width="2"/>
|
|
<rect x="49" y="49" width="926" height="1438" rx="42" fill="none" stroke="url(#holo)" stroke-opacity=".72" stroke-width="3"/>
|
|
|
|
<!-- Header -->
|
|
<g font-family="Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Arial,sans-serif" fill="#ffffff">
|
|
<text x="76" y="104" font-size="22" font-weight="800" letter-spacing="5" opacity=".76">NEURAL HUNT</text>
|
|
<line x1="76" y1="118" x2="286" y2="118" stroke="#ffc400" stroke-width="5" stroke-linecap="round" opacity=".92"/>
|
|
<text x="76" y="154" font-size="17" font-weight="700" letter-spacing="4" opacity=".76">%s</text>
|
|
<g transform="translate(770 77)">
|
|
<rect width="166" height="54" rx="27" fill="#060a12" fill-opacity=".72" stroke="url(#holo)" stroke-width="2"/>
|
|
<text x="83" y="35" text-anchor="middle" font-size="17" font-weight="900" letter-spacing="2">%s</text>
|
|
</g>
|
|
</g>
|
|
|
|
<!-- Bottom collectible information panel -->
|
|
<g font-family="Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Arial,sans-serif" fill="#ffffff">
|
|
<text x="76" y="1234" font-size="15" font-weight="800" letter-spacing="5" opacity=".62">WINNING EDITION</text>
|
|
<text x="76" y="1290" font-size="42" font-weight="900" letter-spacing=".8">%s</text>
|
|
<line x1="76" y1="1320" x2="948" y2="1320" stroke="url(#holo)" stroke-width="2" opacity=".72"/>
|
|
|
|
<g transform="translate(76 1345)">
|
|
<rect width="176" height="50" rx="25" fill="#ffffff" fill-opacity=".10" stroke="#ffffff" stroke-opacity=".18"/>
|
|
<text x="88" y="32" text-anchor="middle" font-size="15" font-weight="800" letter-spacing="2">%d-BIT HUNT</text>
|
|
</g>
|
|
|
|
<text x="76" y="1442" font-size="14" font-weight="700" letter-spacing="2" opacity=".56">%s · ORIGINAL FULL-ART SERIES</text>
|
|
<circle cx="930" cy="1437" r="11" fill="%s"/><circle cx="902" cy="1437" r="7" fill="%s"/>
|
|
</g>
|
|
</svg>`,
|
|
traits.AccentA, traits.AccentB, traits.AccentA, traits.AccentB,
|
|
mime, encoded, sparkles.String(),
|
|
series, rarity, theme, x.RangeBits, completed, traits.AccentA, traits.AccentB,
|
|
)
|
|
return []byte(svg)
|
|
}
|