296 lines
13 KiB
Go
296 lines
13 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]) + "…"
|
|
}
|
|
|
|
type rarityStyle struct {
|
|
Name string
|
|
AccentA string
|
|
AccentB string
|
|
MetalBase string
|
|
MetalEdge string
|
|
PillFill string
|
|
PillText string
|
|
EditionFill string
|
|
EditionStroke string
|
|
EditionText string
|
|
HeaderLine string
|
|
OuterGlow string
|
|
OuterGlowOpacity string
|
|
HoloOpacityA string
|
|
HoloOpacityB string
|
|
BorderOpacityA string
|
|
BorderOpacityB string
|
|
SparkleBoost int
|
|
FoilPattern string
|
|
BadgeText string
|
|
BadgeSubtext string
|
|
}
|
|
|
|
func cardRarityStyle(traits collectionTraits) rarityStyle {
|
|
base := rarityStyle{
|
|
Name: traits.Rarity,
|
|
AccentA: traits.AccentA,
|
|
AccentB: traits.AccentB,
|
|
MetalBase: "#f4f7ff",
|
|
MetalEdge: "#e9f2ff",
|
|
PillFill: "#060a12",
|
|
PillText: "#ffffff",
|
|
EditionFill: "#ffffff",
|
|
EditionStroke: "#ffffff",
|
|
EditionText: "#ffffff",
|
|
HeaderLine: "#ffc400",
|
|
OuterGlow: traits.AccentA,
|
|
OuterGlowOpacity: ".00",
|
|
HoloOpacityA: ".10",
|
|
HoloOpacityB: ".12",
|
|
BorderOpacityA: ".28",
|
|
BorderOpacityB: ".72",
|
|
SparkleBoost: 0,
|
|
BadgeText: traits.Rarity,
|
|
BadgeSubtext: "RIFT EDITION",
|
|
}
|
|
switch strings.ToUpper(strings.TrimSpace(traits.Rarity)) {
|
|
case "COMMON":
|
|
base.AccentA = "#7e8ca5"
|
|
base.AccentB = "#5f6c83"
|
|
base.MetalBase = "#d8dee9"
|
|
base.MetalEdge = "#aeb8c8"
|
|
base.HeaderLine = "#b9c2d4"
|
|
base.OuterGlowOpacity = ".00"
|
|
base.HoloOpacityA = ".05"
|
|
base.HoloOpacityB = ".06"
|
|
base.BorderOpacityA = ".20"
|
|
base.BorderOpacityB = ".45"
|
|
base.BadgeSubtext = "COMMON"
|
|
case "UNCOMMON":
|
|
base.AccentA = "#60eaa9"
|
|
base.AccentB = "#2dbf9a"
|
|
base.MetalBase = "#e8fff5"
|
|
base.MetalEdge = "#b4f1db"
|
|
base.HeaderLine = "#7bffc8"
|
|
base.HoloOpacityA = ".10"
|
|
base.HoloOpacityB = ".14"
|
|
base.BorderOpacityB = ".68"
|
|
base.SparkleBoost = 8
|
|
base.BadgeSubtext = "UNCOMMON"
|
|
case "RARE":
|
|
base.AccentA = "#6ae4ff"
|
|
base.AccentB = "#6f7bff"
|
|
base.MetalBase = "#f0fbff"
|
|
base.MetalEdge = "#c8dcff"
|
|
base.HeaderLine = "#7ed8ff"
|
|
base.HoloOpacityA = ".16"
|
|
base.HoloOpacityB = ".18"
|
|
base.BorderOpacityA = ".30"
|
|
base.BorderOpacityB = ".82"
|
|
base.SparkleBoost = 18
|
|
base.FoilPattern = `<path d="M38 220 C250 120 420 30 710 20" stroke="url(#foilA)" stroke-width="24" stroke-linecap="round" opacity=".34" fill="none"/><path d="M120 1160 C420 1120 710 1020 986 820" stroke="url(#foilA)" stroke-width="18" stroke-linecap="round" opacity=".24" fill="none"/>`
|
|
base.BadgeSubtext = "RARE"
|
|
case "ULTRA RARE":
|
|
base.AccentA = "#ffcf58"
|
|
base.AccentB = "#ff69c9"
|
|
base.MetalBase = "#fff6d8"
|
|
base.MetalEdge = "#ffe7a6"
|
|
base.HeaderLine = "#ffd86c"
|
|
base.OuterGlow = "#ffd670"
|
|
base.OuterGlowOpacity = ".30"
|
|
base.HoloOpacityA = ".24"
|
|
base.HoloOpacityB = ".28"
|
|
base.BorderOpacityA = ".36"
|
|
base.BorderOpacityB = ".92"
|
|
base.SparkleBoost = 34
|
|
base.FoilPattern = `<path d="M-80 220 L400 -40 L790 120 L210 470 Z" fill="url(#foilA)" opacity=".20"/><path d="M160 1540 L1130 1030 L1130 1210 L300 1540 Z" fill="url(#foilB)" opacity=".22"/><ellipse cx="840" cy="340" rx="220" ry="120" fill="url(#foilA)" opacity=".18"/>`
|
|
base.BadgeSubtext = "ULTRA"
|
|
case "SPECIAL ILLUSTRATION RARE":
|
|
base.AccentA = "#ffd46a"
|
|
base.AccentB = "#8b5fff"
|
|
base.MetalBase = "#fff7df"
|
|
base.MetalEdge = "#fff0bf"
|
|
base.PillFill = "#fff6d0"
|
|
base.PillText = "#241535"
|
|
base.EditionFill = "#fff8da"
|
|
base.EditionStroke = "#ffe596"
|
|
base.EditionText = "#241535"
|
|
base.HeaderLine = "#ffe17c"
|
|
base.OuterGlow = "#ffd76d"
|
|
base.OuterGlowOpacity = ".42"
|
|
base.HoloOpacityA = ".32"
|
|
base.HoloOpacityB = ".36"
|
|
base.BorderOpacityA = ".46"
|
|
base.BorderOpacityB = "1.00"
|
|
base.SparkleBoost = 56
|
|
base.FoilPattern = `<path d="M-80 180 L350 -30 L860 70 L1040 250 L540 470 Z" fill="url(#foilA)" opacity=".28"/><path d="M130 1480 C260 1180 520 990 1020 880 L1020 1160 L430 1540 Z" fill="url(#foilB)" opacity=".24"/><circle cx="846" cy="264" r="170" fill="url(#foilA)" opacity=".22"/><path d="M84 226 C170 176 250 136 332 102" stroke="#fff6ca" stroke-width="4" stroke-opacity=".56" stroke-linecap="round" fill="none"/><path d="M700 1240 C820 1170 910 1090 984 990" stroke="#fff1b4" stroke-width="4" stroke-opacity=".42" stroke-linecap="round" fill="none"/>`
|
|
base.BadgeText = "SIR"
|
|
base.BadgeSubtext = "SPECIAL"
|
|
}
|
|
return base
|
|
}
|
|
|
|
func raritySparkleCount(style rarityStyle) int {
|
|
count := 42 + style.SparkleBoost
|
|
if count < 16 {
|
|
count = 16
|
|
}
|
|
return count
|
|
}
|
|
|
|
func renderSparkles(h [32]byte, style rarityStyle) string {
|
|
count := raritySparkleCount(style)
|
|
var sparkles strings.Builder
|
|
for i := 0; i < count; 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 := 10 + (b1 % 30)
|
|
if style.SparkleBoost > 0 && i%9 == 0 {
|
|
r += 2
|
|
op += 18
|
|
}
|
|
if style.SparkleBoost > 24 && i%13 == 0 {
|
|
fmt.Fprintf(&sparkles, `<path d="M%d %d l7 0 l0 -7 l3 0 l0 7 l7 0 l0 3 l-7 0 l0 7 l-3 0 l0 -7 l-7 0 z" fill="#ffffff" opacity="0.%02d"/>`, xv, yv, minInt(98, op))
|
|
continue
|
|
}
|
|
fmt.Fprintf(&sparkles, `<circle cx="%d" cy="%d" r="%d" fill="white" opacity="0.%02d"/>`, xv, yv, r, minInt(98, op))
|
|
}
|
|
return sparkles.String()
|
|
}
|
|
|
|
func minInt(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// 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)
|
|
editionLabel := "WINNING EDITION"
|
|
bitLabel := fmt.Sprintf("%d-BIT HUNT", x.RangeBits)
|
|
if strings.EqualFold(strings.TrimSpace(x.Origin), "admin_drop") {
|
|
editionLabel = "ADMIN DROP"
|
|
bitLabel = "COLLECTIBLE"
|
|
}
|
|
style := cardRarityStyle(traits)
|
|
h := sha256.Sum256([]byte(x.ID + "|card|" + x.Winner + "|" + x.Seed))
|
|
sparkles := renderSparkles(h, style)
|
|
|
|
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="%s"/><stop offset=".18" stop-color="%s"/><stop offset=".48" stop-color="#101725"/><stop offset=".73" stop-color="%s"/><stop offset="1" stop-color="%s"/></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=".78"/><stop offset=".42" stop-color="#ffffff" stop-opacity=".22"/><stop offset=".67" stop-color="%s" stop-opacity=".70"/><stop offset="1" stop-color="#ffffff" stop-opacity=".08"/></linearGradient>
|
|
<linearGradient id="foilA" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#ffffff" stop-opacity=".00"/><stop offset=".2" stop-color="%s" stop-opacity=".35"/><stop offset=".5" stop-color="#ffffff" stop-opacity=".12"/><stop offset=".76" stop-color="%s" stop-opacity=".30"/><stop offset="1" stop-color="#ffffff" stop-opacity=".00"/></linearGradient>
|
|
<linearGradient id="foilB" x1="0" y1="1" x2="1" y2="0"><stop stop-color="#ffffff" stop-opacity=".00"/><stop offset=".18" stop-color="%s" stop-opacity=".22"/><stop offset=".55" stop-color="#ffffff" stop-opacity=".10"/><stop offset=".86" stop-color="%s" stop-opacity=".26"/><stop offset="1" stop-color="#ffffff" stop-opacity=".00"/></linearGradient>
|
|
<radialGradient id="outerGlow" cx="50%%" cy="50%%" r="60%%"><stop offset="0%%" stop-color="%s" stop-opacity="%s"/><stop offset="78%%" stop-color="%s" stop-opacity="0"/></radialGradient>
|
|
<filter id="shadow"><feDropShadow dx="0" dy="18" stdDeviation="22" flood-color="#000000" flood-opacity=".55"/></filter>
|
|
<filter id="glow"><feDropShadow dx="0" dy="0" stdDeviation="16" flood-color="%s" flood-opacity="%s"/></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"/>
|
|
<rect x="18" y="18" width="988" height="1500" rx="66" fill="url(#outerGlow)" opacity=".55"/>
|
|
</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="%s"/>
|
|
<path d="M120 1530 L1130 1030 L1130 1190 L330 1530 Z" fill="url(#holo)" opacity="%s"/>
|
|
%s
|
|
<g>%s</g>
|
|
</g>
|
|
<rect x="42" y="42" width="940" height="1452" rx="48" fill="none" stroke="#ffffff" stroke-opacity="%s" stroke-width="2"/>
|
|
<rect x="49" y="49" width="926" height="1438" rx="42" fill="none" stroke="url(#holo)" stroke-opacity="%s" stroke-width="3" filter="url(#glow)"/>
|
|
|
|
<!-- 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="%s" 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(738 72)">
|
|
<rect width="198" height="62" rx="31" fill="%s" fill-opacity=".88" stroke="url(#holo)" stroke-width="2.4"/>
|
|
<text x="99" y="30" text-anchor="middle" font-size="18" font-weight="900" letter-spacing="2" fill="%s">%s</text>
|
|
<text x="99" y="47" text-anchor="middle" font-size="9.5" font-weight="800" letter-spacing="2.4" fill="%s" opacity=".82">%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">%s</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="216" height="50" rx="25" fill="%s" fill-opacity=".16" stroke="%s" stroke-opacity=".38"/>
|
|
<text x="108" y="32" text-anchor="middle" font-size="14" font-weight="900" letter-spacing="1.8" fill="%s">%s</text>
|
|
</g>
|
|
|
|
<text x="76" y="1414" font-size="16" font-weight="900" letter-spacing="1.8" fill="%s">%s</text>
|
|
<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>`,
|
|
style.MetalBase, style.AccentA, style.AccentB, style.MetalEdge,
|
|
style.AccentA, style.AccentB,
|
|
style.AccentA, style.AccentB,
|
|
style.AccentB, style.AccentA,
|
|
style.OuterGlow, style.OuterGlowOpacity, style.OuterGlow,
|
|
style.OuterGlow, style.OuterGlowOpacity,
|
|
mime, encoded, style.HoloOpacityA, style.HoloOpacityB, style.FoilPattern, sparkles,
|
|
style.BorderOpacityA, style.BorderOpacityB,
|
|
style.HeaderLine,
|
|
series,
|
|
style.PillFill, style.PillText, html.EscapeString(style.BadgeText), style.PillText, html.EscapeString(style.BadgeSubtext),
|
|
editionLabel, theme,
|
|
style.EditionFill, style.EditionStroke, style.EditionText, bitLabel,
|
|
style.AccentA, rarity,
|
|
completed, style.AccentA, style.AccentB,
|
|
)
|
|
return []byte(svg)
|
|
}
|