1package art3d223import (4 "crypto/sha256"5 "math"6 "strings"78 "gno.land/p/nt/ufmt/v0"9)1011const (12 canvasW, canvasH = 170.0, 170.013 subjX, subjY = 85.0, 78.014)1516func itoa(v int64) string {17 if v == 0 {18 return "0"19 }20 neg := v < 021 if neg {22 v = -v23 }24 var b [20]byte25 i := len(b)26 for v > 0 {27 i--28 b[i] = byte('0' + v%10)29 v /= 1030 }31 if neg {32 i--33 b[i] = '-'34 }35 return string(b[i:])36}3738func fnum(v float64) string { return cnum(v) }3940func fnumPad(v float64) string {41 neg := v < 042 if neg {43 v = -v44 }45 scaled := int64(v*100 + 0.5)46 whole := scaled / 10047 frac := scaled % 10048 fracStr := itoa(frac)49 if frac < 10 {50 fracStr = "0" + fracStr51 }52 s := itoa(whole) + "." + fracStr53 if neg {54 return "-" + s55 }56 return s57}5859func cnum(v float64) string {60 s := fnumPad(v)61 if strings.Contains(s, ".") {62 s = strings.TrimRight(strings.TrimRight(s, "0"), ".")63 }64 if strings.HasPrefix(s, "0.") {65 s = s[1:]66 } else if strings.HasPrefix(s, "-0.") {67 s = "-" + s[2:]68 }69 return s70}7172func fnum1(v float64) string {73 neg := v < 074 if neg {75 v = -v76 }77 scaled := int64(v*10 + 0.5)78 s := itoa(scaled/10) + "." + itoa(scaled%10)79 if neg {80 return "-" + s81 }82 return s83}8485func pctnum(v float64) string { return fnum(v) }8687func hexToRGB(hex string) (int, int, int) {88 h := hex89 if len(h) > 0 && h[0] == '#' {90 h = h[1:]91 }92 r := hexByte(h[0:2])93 g := hexByte(h[2:4])94 b := hexByte(h[4:6])95 return r, g, b96}9798func hexByte(s string) int {99 v := 0100 for i := 0; i < len(s); i++ {101 c := s[i]102 var d int103 switch {104 case c >= '0' && c <= '9':105 d = int(c - '0')106 case c >= 'a' && c <= 'f':107 d = int(c-'a') + 10108 case c >= 'A' && c <= 'F':109 d = int(c-'A') + 10110 }111 v = v*16 + d112 }113 return v114}115116func rgbToHex(r, g, b int) string {117 return "#" + hexDigit2(r) + hexDigit2(g) + hexDigit2(b)118}119120func hexDigit2(v int) string {121 if v < 0 {122 v = 0123 }124 if v > 255 {125 v = 255126 }127 const digits = "0123456789abcdef"128 return string(digits[v/16]) + string(digits[v%16])129}130131func clampByte(v float64) int {132 iv := int(v)133 if iv < 0 {134 return 0135 }136 if iv > 255 {137 return 255138 }139 return iv140}141142func lighten(hex string, amt float64) string {143 r, g, b := hexToRGB(hex)144 nr := clampByte(float64(r) + (255-float64(r))*amt)145 ng := clampByte(float64(g) + (255-float64(g))*amt)146 nb := clampByte(float64(b) + (255-float64(b))*amt)147 return rgbToHex(nr, ng, nb)148}149150func mixHex(hex, target string, amt float64) string {151 r1, g1, b1 := hexToRGB(hex)152 r2, g2, b2 := hexToRGB(target)153 nr := clampByte(float64(r1) + float64(r2-r1)*amt)154 ng := clampByte(float64(g1) + float64(g2-g1)*amt)155 nb := clampByte(float64(b1) + float64(b2-b1)*amt)156 return rgbToHex(nr, ng, nb)157}158159func hueRotate(hex string, deg float64) string {160 c, sn := math.Cos(deg*math.Pi/180), math.Sin(deg*math.Pi/180)161 r, g, b := hexToRGB(hex)162 fr, fg, fb := float64(r), float64(g), float64(b)163 nr := (0.213+c*0.787-sn*0.213)*fr + (0.715-c*0.715-sn*0.715)*fg + (0.072-c*0.072+sn*0.928)*fb164 ng := (0.213-c*0.213+sn*0.143)*fr + (0.715+c*0.285+sn*0.140)*fg + (0.072-c*0.072-sn*0.283)*fb165 nb := (0.213-c*0.213-sn*0.787)*fr + (0.715-c*0.715+sn*0.715)*fg + (0.072+c*0.928+sn*0.072)*fb166 return rgbToHex(clampByte(nr), clampByte(ng), clampByte(nb))167}168169func shadeSeries(hex string, n int, lo, hi float64) []string {170 r, g, b := hexToRGB(hex)171 out := make([]string, n)172 for i := 0; i < n; i++ {173 t := lo174 if n > 1 {175 t = lo + (hi-lo)*float64(i)/float64(n-1)176 }177 out[i] = rgbToHex(clampByte(float64(r)*t), clampByte(float64(g)*t), clampByte(float64(b)*t))178 }179 return out180}181182type prng struct {183 seed string184 n int185}186187func newPRNG(seed string) *prng { return &prng{seed: seed} }188189func (p *prng) next() uint64 {190 p.n++191 material := p.seed + ":" + itoa(int64(p.n)) // same bytes as "%s:%d"192 sum := sha256.Sum256([]byte(material))193 var v uint64194 for i := 0; i < 8; i++ {195 v = v<<8 | uint64(sum[i])196 }197 return v198}199200func (p *prng) floatIn(lo, hi float64) float64 {201 frac := float64(p.next()%1000000) / 1000000.0202 return lo + frac*(hi-lo)203}204205func (p *prng) intn(n int) int {206 if n <= 1 {207 return 0208 }209 return int(p.next() % uint64(n))210}211212type pt struct{ x, y float64 }213214func polyPath(pts []pt) string {215 s := "M"216 for i, p := range pts {217 if i > 0 {218 s += " L"219 }220 s += fnum(p.x) + "," + fnum(p.y)221 }222 return s + " Z"223}224225func polygonPoints(pts []pt) string {226 s := ""227 for i, p := range pts {228 if i > 0 {229 s += " "230 }231 s += fnum(p.x) + "," + fnum(p.y)232 }233 return s234}235236type gstop struct {237 pct float64238 color string239 op float64240}241242type gramp struct {243 x1, y1, x2, y2 float64244 stops []gstop245}246247func (g gramp) sample(px, py float64) (string, float64) {248 dx, dy := g.x2-g.x1, g.y2-g.y1249 t := ((px-g.x1)*dx + (py-g.y1)*dy) / (dx*dx + dy*dy) * 100250 if t < 0 {251 t = 0252 }253 if t > 100 {254 t = 100255 }256 for i := 1; i < len(g.stops); i++ {257 if t <= g.stops[i].pct {258 a, b := g.stops[i-1], g.stops[i]259 f := (t - a.pct) / (b.pct - a.pct)260 return mixHex(a.color, b.color, f), a.op + (b.op-a.op)*f261 }262 }263 last := g.stops[len(g.stops)-1]264 return last.color, last.op265}266267func centroid(poly []pt) (float64, float64) {268 cx, cy := 0.0, 0.0269 for _, p := range poly {270 cx += p.x271 cy += p.y272 }273 return cx / float64(len(poly)), cy / float64(len(poly))274}275276type facetStyle struct {277 flash gramp278 ringCent []pt279 prof fireProfile280 zone *prng // per-facet hue drift and shade jitter, seeded per token281 stone string // which crystal habit a Raw Crystal takes282 thumb bool283 glints string284}285286type fireProfile struct {287 sat, hit float64288 pinpoints int289 shadow string290 bodyMix float64291 neutral string292}293294func fireProfileFor(stone, colorName string) fireProfile {295 if stone == "Diamond" {296 switch colorName {297 case "Colorless":298 return fireProfile{1.0, 0.85, 5, "#0d1222", 0.12, "#f4f7fc"}299 case "Black":300 return fireProfile{0.15, 0.35, 1, "#000000", 0, ""}301 }302 return fireProfile{0.95, 0.7, 4, "#0b0d18", 0, ""}303 }304 if stone == "Quartz" {305 if colorName == "Rock Crystal" {306 return fireProfile{0.4, 0.6, 3, "#141a2c", 0.25, "#e8edf4"}307 }308 return fireProfile{0.35, 0.5, 3, "#000000", 0, ""}309 }310 if colorName == "Colorless" {311 return fireProfile{1.0, 0.7, 4, "#141a2c", 0.55, "#dfe4ee"}312 }313 if colorName == "White" {314 return fireProfile{0.85, 0.6, 3, "#7d86a0", 0, ""}315 }316 if colorName == "Crystal" {317 return fireProfile{1.0, 0.78, 4, "#141a2c", 0.3, "#e4e9f2"}318 }319 if stone == "Topaz" {320 return fireProfile{0.5, 0.5, 3, "#000000", 0, ""}321 }322 return fireProfile{0.7, 0.5, 3, "#000000", 0, ""}323}324325const fireZoneDeg = 7.0326327type rgbI struct{ r, g, b int }328329func toRGBI(hex string) rgbI { r, g, b := hexToRGB(hex); return rgbI{r, g, b} }330331func (c rgbI) hex() string { return rgbToHex(c.r, c.g, c.b) }332333func mixI(a, b rgbI, amt float64) rgbI {334 return rgbI{335 clampByte(float64(a.r) + float64(b.r-a.r)*amt),336 clampByte(float64(a.g) + float64(b.g-a.g)*amt),337 clampByte(float64(a.b) + float64(b.b-a.b)*amt),338 }339}340341func hueRotateI(x rgbI, deg float64) rgbI {342 c, sn := math.Cos(deg*math.Pi/180), math.Sin(deg*math.Pi/180)343 fr, fg, fb := float64(x.r), float64(x.g), float64(x.b)344 nr := (0.213+c*0.787-sn*0.213)*fr + (0.715-c*0.715-sn*0.715)*fg + (0.072-c*0.072+sn*0.928)*fb345 ng := (0.213-c*0.213+sn*0.143)*fr + (0.715+c*0.285+sn*0.140)*fg + (0.072-c*0.072-sn*0.283)*fb346 nb := (0.213-c*0.213-sn*0.787)*fr + (0.715-c*0.715+sn*0.715)*fg + (0.072+c*0.928+sn*0.072)*fb347 return rgbI{clampByte(nr), clampByte(ng), clampByte(nb)}348}349350func hex3I(x rgbI) string {351 d := "0123456789abcdef"352 q := func(v int) byte { return d[(v+8)*15/255%16] }353 return "#" + string([]byte{q(x.r), q(x.g), q(x.b)})354}355356var whiteI = rgbI{255, 255, 255}357358func darken(hex, shadow string, amt float64) string {359 if amt > 0.95 {360 amt = 0.95361 }362 return mixHex(hex, shadow, amt)363}364365const prismFlash = true366367func flashRamp(ext, sat float64) gramp {368 r := gramp{x1: subjX - ext, y1: subjY - ext, x2: subjX + ext, y2: subjY + ext}369 if !prismFlash {370 r.stops = []gstop{{0, "#ffffff", 1}, {100, "#ffffff", 0.25}}371 return r372 }373 r.stops = []gstop{374 {0, "#ffffff", 1}, {14, "#ff4040", 0.95}, {28, "#ff9a1f", 0.95}, {42, "#ffe81f", 0.9},375 {56, "#35ff7a", 0.85}, {70, "#22ccff", 0.8}, {84, "#5566ff", 0.75}, {100, "#c04dff", 0.7},376 }377 for i := range r.stops {378 r.stops[i].color = mixHex(r.stops[i].color, "#ffffff", 1-sat)379 }380 return r381}382383type v3 struct{ x, y, z float64 }384385func vsub(a, b v3) v3 { return v3{a.x - b.x, a.y - b.y, a.z - b.z} }386func vdot(a, b v3) float64 { return a.x*b.x + a.y*b.y + a.z*b.z }387func vcross(a, b v3) v3 { return v3{a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x} }388func vscale(a v3, k float64) v3 { return v3{a.x * k, a.y * k, a.z * k} }389func vnorm(a v3) v3 {390 l := math.Sqrt(vdot(a, a))391 if l == 0 {392 return a393 }394 return v3{a.x / l, a.y / l, a.z / l}395}396397func sgnPow(v, e float64) float64 {398 if v < 0 {399 return -math.Pow(-v, e)400 }401 return math.Pow(v, e)402}403404type outlineFn func(t float64) (float64, float64)405406func circleOutline(t float64) (float64, float64) {407 a := 2 * math.Pi * t408 return math.Cos(a), math.Sin(a)409}410411func superOutline(p float64) outlineFn {412 return func(t float64) (float64, float64) {413 a := 2 * math.Pi * t414 return sgnPow(math.Cos(a), 2/p), sgnPow(math.Sin(a), 2/p)415 }416}417418func polyOutline(vs []pt) outlineFn {419 return func(t float64) (float64, float64) {420 n := len(vs)421 f := t * float64(n)422 k := int(math.Floor(f))423 fr := f - float64(k)424 a := vs[((k%n)+n)%n]425 b := vs[(((k+1)%n)+n)%n]426 return a.x + (b.x-a.x)*fr, a.y + (b.y-a.y)*fr427 }428}429430func octagon(hw, hh, c float64) []pt {431 return []pt{432 {hw - c, hh}, {-hw + c, hh}, {-hw, hh - c}, {-hw, -hh + c},433 {-hw + c, -hh}, {hw - c, -hh}, {hw, -hh + c}, {hw, hh - c},434 }435}436437func pearOutline(t float64) (float64, float64) {438 a := 2 * math.Pi * t439 c := math.Cos(a)440 return c*0.92 + 0.08, 0.7 * math.Sin(a) * (1 - 0.46*c)441}442443func marquiseOutline(t float64) (float64, float64) {444 a := 2 * math.Pi * t445 return math.Cos(a), 0.52 * sgnPow(math.Sin(a), 1.35)446}447448func heartOutline(t float64) (float64, float64) {449 a := 2 * math.Pi * t450 sn := math.Sin(a)451 y := (13*math.Cos(a) - 4*math.Cos(2*a) - 1.4*math.Cos(3*a) - 0.5*math.Cos(4*a)) / 17452 return 0.88*sn*sn*sn + 0.12*sn, (y + 0.1576) / 0.7894453}454455func trillionOutline(t float64) (float64, float64) {456 vs := []pt{{0, 1}, {-0.866, -0.5}, {0.866, -0.5}}457 f := t * 3458 k := int(math.Floor(f))459 fr := f - float64(k)460 a, b := vs[((k%3)+3)%3], vs[(((k+1)%3)+3)%3]461 bulge := 1 + 0.16*math.Sin(math.Pi*fr)462 return (a.x + (b.x-a.x)*fr) * bulge, (a.y + (b.y-a.y)*fr) * bulge463}464465type cutSpec struct {466 n int467 brilliant bool468 outline outlineFn469 rx, ry float64470}471472var ShapeNames = []string{473 "Round Brilliant", "Oval", "Cushion", "Emerald Cut", "Asscher",474 "Radiant", "Princess Cut", "Pear", "Marquise", "Trillion",475 "Heart", "Rose Cut", "Raw Crystal",476}477478const (479 shapeHeart = 10480 shapeRose = 11481 shapeRaw = 12482)483484const stoneScale = 0.78485486func cutFor(shapeIdx int) cutSpec {487 c := cutForUnscaled(shapeIdx)488 c.rx, c.ry = c.rx*stoneScale, c.ry*stoneScale489 return c490}491492func cutForUnscaled(shapeIdx int) cutSpec {493 switch shapeIdx {494 case 1:495 return cutSpec{8, true, circleOutline, 30, 21.5}496 case 2:497 return cutSpec{8, true, superOutline(3.4), 27, 24}498 case 3:499 return cutSpec{8, false, polyOutline(octagon(1, 1, 0.3)), 30, 21}500 case 4:501 return cutSpec{8, false, polyOutline(octagon(1, 1, 0.36)), 25, 25}502 case 5:503 return cutSpec{8, true, polyOutline(octagon(1, 1, 0.3)), 29, 22.5}504 case 6:505 return cutSpec{8, true, polyOutline([]pt{{1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}, {1, 0}}), 23.5, 23.5}506 case 7:507 return cutSpec{8, true, pearOutline, 31, 27}508 case 8:509 return cutSpec{8, true, marquiseOutline, 33, 30}510 case 9:511 return cutSpec{9, true, trillionOutline, 27, 27}512 case shapeHeart:513 return cutSpec{10, true, heartOutline, 29, 28}514 case shapeRose:515 return cutSpec{6, true, circleOutline, 27, 27}516 case shapeRaw:517 return cutSpec{0, false, circleOutline, 27, 27} // sized per stone in buildRaw518 }519 return cutSpec{8, true, circleOutline, 27, 27}520}521522func CutsForStone(stone string) []int {523 switch stone {524 case "Diamond":525 return []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, shapeHeart, shapeRose, shapeRaw}526 case "Sapphire":527 return []int{1, 2, 0, 3, 7, shapeHeart, 8, 9, 6, shapeRose, shapeRaw}528 case "Topaz":529 return []int{3, 1, 7, 2, 8, 0, 9, shapeHeart, 6, shapeRaw}530 }531 return []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, shapeHeart, shapeRose, shapeRaw}532}533534const (535 kCrown = iota536 kTable537 kPav538)539540type face struct {541 p []v3542 kind int543}544545func ringPts(c cutSpec, n int, scale, z, off float64) []v3 {546 out := make([]v3, n)547 for k := 0; k < n; k++ {548 x, y := c.outline((float64(k) + off) / float64(n))549 out[k] = v3{x * c.rx * scale, y * c.ry * scale, z}550 }551 return out552}553554func buildCut(c cutSpec) (faces []face, girdle []v3) {555 base := c.rx556 if c.ry < base {557 base = c.ry558 }559 hc, hp := 0.34*base, 0.9*base560 n := c.n561 if c.brilliant {562 T := ringPts(c, n, 0.56, hc, 0)563 B := ringPts(c, n, 0.8, hc*0.46, 0.5)564 G := ringPts(c, 2*n, 1, 0, 0)565 culet := v3{0, 0, -hp}566 faces = append(faces, face{T, kTable})567 for k := 0; k < n; k++ {568 k1 := (k + 1) % n569 faces = append(faces, face{[]v3{T[k], B[k], T[k1]}, kCrown})570 faces = append(faces, face{[]v3{T[k1], B[k], G[(2*k+2)%(2*n)], B[k1]}, kCrown})571 faces = append(faces, face{[]v3{B[k], G[2*k], G[2*k+1]}, kCrown})572 faces = append(faces, face{[]v3{B[k], G[2*k+1], G[(2*k+2)%(2*n)]}, kCrown})573 faces = append(faces, face{[]v3{G[2*k], G[2*k+1], G[(2*k+2)%(2*n)], culet}, kPav})574 }575 return faces, G576 }577 T := ringPts(c, n, 0.54, hc, 0)578 S := ringPts(c, n, 0.78, hc*0.5, 0)579 G := ringPts(c, n, 1, 0, 0)580 P1 := ringPts(c, n, 0.7, -hp*0.42, 0)581 P2 := ringPts(c, n, 0.38, -hp*0.78, 0)582 K := ringPts(c, n, 0.1, -hp, 0)583 faces = append(faces, face{T, kTable})584 rows := [][2][]v3{{T, S}, {S, G}}585 for _, r := range rows {586 for k := 0; k < n; k++ {587 k1 := (k + 1) % n588 faces = append(faces, face{[]v3{r[0][k], r[0][k1], r[1][k1], r[1][k]}, kCrown})589 }590 }591 prow := [][2][]v3{{G, P1}, {P1, P2}, {P2, K}}592 for _, r := range prow {593 for k := 0; k < n; k++ {594 k1 := (k + 1) % n595 faces = append(faces, face{[]v3{r[0][k], r[0][k1], r[1][k1], r[1][k]}, kPav})596 }597 }598 return faces, G599}600601func buildRose(c cutSpec) (faces []face, girdle []v3) {602 n := c.n603 base := math.Min(c.rx, c.ry)604 h := 0.62 * base605 apex := v3{0, 0, h}606 R := ringPts(c, n, 0.55, h*0.62, 0.5)607 G := ringPts(c, 2*n, 1, 0, 0)608 for k := 0; k < n; k++ {609 k1 := (k + 1) % n610 faces = append(faces, face{[]v3{apex, R[k], R[k1]}, kCrown})611 faces = append(faces, face{[]v3{G[2*k], G[2*k+1], R[k]}, kCrown})612 faces = append(faces, face{[]v3{G[2*k+1], G[(2*k+2)%(2*n)], R[k]}, kCrown})613 faces = append(faces, face{[]v3{R[k], G[(2*k+2)%(2*n)], R[k1]}, kCrown})614 }615 back := make([]v3, len(G))616 for i := range G {617 back[len(G)-1-i] = G[i]618 }619 faces = append(faces, face{back, kPav})620 return faces, G621}622623func ringX(n int, x, ry, rz, phase float64) []v3 {624 out := make([]v3, n)625 for k := 0; k < n; k++ {626 a := 2*math.Pi*(float64(k)+phase)/float64(n) + math.Pi/2627 out[k] = v3{x, ry * math.Cos(a), rz * math.Sin(a)}628 }629 return out630}631632func bandX(a, b []v3) []face {633 var fs []face634 for k := range a {635 k1 := (k + 1) % len(a)636 fs = append(fs, face{[]v3{a[k], a[k1], b[k1], b[k]}, kCrown})637 }638 return fs639}640641func capX(r []v3) face { return face{r, kCrown} }642643func turnFaces(fs []face, ax, az float64) []face {644 cx, sx, cz, sz := math.Cos(ax), math.Sin(ax), math.Cos(az), math.Sin(az)645 for i := range fs {646 fs[i].kind = kPav647 for j, p := range fs[i].p {648 y, z := p.y*cx-p.z*sx, p.y*sx+p.z*cx649 fs[i].p[j] = v3{p.x*cz - y*sz, p.x*sz + y*cz, z}650 }651 }652 return fs653}654655func buildRaw(stone string) (faces []face, girdle []v3) {656 deg := math.Pi / 180657 switch stone {658 case "Diamond":659 a, b, h := 25.0*stoneScale, 22.0*stoneScale, 23.0*stoneScale660 v := []v3{{a, 0, 0}, {0, b, 0}, {-a, 0, 0}, {0, -b, 0}}661 top, bot := v3{0, 0, h}, v3{0, 0, -h}662 for k := 0; k < 4; k++ {663 k1 := (k + 1) % 4664 faces = append(faces, face{[]v3{top, v[k], v[k1]}, kCrown})665 faces = append(faces, face{[]v3{bot, v[k1], v[k]}, kCrown})666 }667 return turnFaces(faces, -38*deg, 28*deg), nil668 case "Sapphire":669 L, R := 26.0*stoneScale, 16.0*stoneScale670 r0 := ringX(6, -L, R*0.42, R*0.42, 0)671 r1 := ringX(6, -L*0.3, R, R*0.94, 0)672 r2 := ringX(6, L*0.3, R, R*0.94, 0)673 r3 := ringX(6, L, R*0.42, R*0.42, 0)674 faces = append(faces, bandX(r0, r1)...)675 faces = append(faces, bandX(r1, r2)...)676 faces = append(faces, bandX(r2, r3)...)677 faces = append(faces, capX(r3))678 rev := make([]v3, 6)679 for i := range r0 {680 rev[5-i] = r0[i]681 }682 faces = append(faces, capX(rev))683 return turnFaces(faces, -30*deg, -14*deg), nil684 }685 if stone == "Quartz" {686 L, R := 27.0*stoneScale, 12.5*stoneScale687 r0 := ringX(6, -L, R*1.04, R, 0)688 r1 := ringX(6, L*0.4, R, R*0.96, 0)689 point := v3{L, 0, 0}690 faces = append(faces, bandX(r0, r1)...)691 for k := 0; k < 6; k++ {692 faces = append(faces, face{[]v3{r1[k], r1[(k+1)%6], point}, kCrown})693 }694 rev := make([]v3, 6)695 for i := range r0 {696 rev[5-i] = r0[i]697 }698 faces = append(faces, capX(rev))699 return turnFaces(faces, -32*deg, 16*deg), nil700 }701 L, ry, rz := 26.0*stoneScale, 16.0*stoneScale, 13.0*stoneScale702 r0 := ringX(8, -L, ry, rz, 0.5)703 r1 := ringX(8, L*0.5, ry, rz, 0.5)704 tip := ringX(8, L*0.82, ry*0.45, rz*0.45, 0.5)705 point := v3{L, 0, 0}706 faces = append(faces, bandX(r0, r1)...)707 faces = append(faces, bandX(r1, tip)...)708 for k := 0; k < 8; k++ {709 faces = append(faces, face{[]v3{tip[k], tip[(k+1)%8], point}, kCrown})710 }711 rev := make([]v3, 8)712 for i := range r0 {713 rev[7-i] = r0[i]714 }715 faces = append(faces, capX(rev))716 return turnFaces(faces, -30*deg, 12*deg), nil717}718719const viewTilt = 61.0720721const (722 pieceCY = subjY + 1.5723)724725func n1(v float64) string {726 neg := v < 0727 if neg {728 v = -v729 }730 t := int64(v*10 + 0.5)731 w, f := t/10, t%10732 s := ""733 if f == 0 {734 s = itoa(w)735 } else if w == 0 {736 s = "." + string(byte('0'+f))737 } else {738 s = itoa(w) + "." + string(byte('0'+f))739 }740 if neg && t != 0 {741 return "-" + s742 }743 return s744}745746func relPath(ps []pt) string {747 s := "M" + n1(ps[0].x) + " " + n1(ps[0].y) + "l"748 px, py := math.Round(ps[0].x*10)/10, math.Round(ps[0].y*10)/10749 for i := 1; i < len(ps); i++ {750 qx, qy := math.Round(ps[i].x*10)/10, math.Round(ps[i].y*10)/10751 dx, dy := n1(qx-px), n1(qy-py)752 if i > 1 && dx[0] != '-' {753 s += " "754 }755 s += dx756 if dy[0] != '-' {757 s += " "758 }759 s += dy760 px, py = qx, qy761 }762 return s + "z"763}764765func hex3(h string) string {766 r, g, b := hexToRGB(h)767 d := "0123456789abcdef"768 q := func(v int) byte { return d[(v+8)*15/255%16] }769 return "#" + string([]byte{q(r), q(g), q(b)})770}771772func convexHull(ps []pt) []pt {773 n := len(ps)774 idx := make([]int, n)775 for i := range idx {776 idx[i] = i777 }778 less := func(a, b int) bool {779 return ps[a].x < ps[b].x || (ps[a].x == ps[b].x && ps[a].y < ps[b].y)780 }781 tmp := make([]int, n)782 for w := 1; w < n; w *= 2 {783 for lo := 0; lo < n; lo += 2 * w {784 mid, hi := lo+w, lo+2*w785 if mid > n {786 mid = n787 }788 if hi > n {789 hi = n790 }791 i, j, k := lo, mid, lo792 for i < mid && j < hi {793 if less(idx[j], idx[i]) {794 tmp[k] = idx[j]795 j++796 } else {797 tmp[k] = idx[i]798 i++799 }800 k++801 }802 for i < mid {803 tmp[k] = idx[i]804 i++805 k++806 }807 for j < hi {808 tmp[k] = idx[j]809 j++810 k++811 }812 }813 idx, tmp = tmp, idx814 }815 cr := func(o, a, b pt) float64 { return (a.x-o.x)*(b.y-o.y) - (a.y-o.y)*(b.x-o.x) }816 var h []pt817 for pass := 0; pass < 2; pass++ {818 start := len(h)819 for i := 0; i < n; i++ {820 p := ps[idx[i]]821 if pass == 1 {822 p = ps[idx[n-1-i]]823 }824 for len(h) >= start+2 && cr(h[len(h)-2], h[len(h)-1], p) <= 0 {825 h = h[:len(h)-1]826 }827 h = append(h, p)828 }829 h = h[:len(h)-1]830 }831 return h832}833834func polyArea(ps []pt) float64 {835 a := 0.0836 for i := range ps {837 j := (i + 1) % len(ps)838 a += ps[i].x*ps[j].y - ps[j].x*ps[i].y839 }840 if a < 0 {841 return -a / 2842 }843 return a / 2844}845846func clipHalf(ps []pt, ox, oy, nx, ny float64) []pt {847 if len(ps) == 0 {848 return ps849 }850 d := func(q pt) float64 { return (q.x-ox)*nx + (q.y-oy)*ny }851 var out []pt852 for i := range ps {853 a, b := ps[i], ps[(i+1)%len(ps)]854 da, db := d(a), d(b)855 if da <= 0 {856 out = append(out, a)857 }858 if (da < 0 && db > 0) || (da > 0 && db < 0) {859 t := da / (da - db)860 out = append(out, pt{a.x + (b.x-a.x)*t, a.y + (b.y-a.y)*t})861 }862 }863 return out864}865866func clipToConvex(ps, win []pt) []pt {867 for i := range win {868 a, b := win[i], win[(i+1)%len(win)]869 nx, ny := b.y-a.y, a.x-b.x870 mid := pt{(a.x + b.x) / 2, (a.y + b.y) / 2}871 cx, cy := centroid(win)872 if (mid.x-cx)*nx+(mid.y-cy)*ny < 0 {873 nx, ny = -nx, -ny874 }875 ps = clipHalf(ps, a.x, a.y, nx, ny)876 if len(ps) < 3 {877 return nil878 }879 }880 return ps881}882883func splitPoly(ps []pt, ang float64) ([]pt, []pt) {884 cx, cy := centroid(ps)885 nx, ny := math.Cos(ang), math.Sin(ang)886 return clipHalf(ps, cx, cy, nx, ny), clipHalf(ps, cx, cy, -nx, -ny)887}888889func renderSolid3D(shapeIdx int, base, salt string, fs *facetStyle) (under, facets, over string) {890 c := cutFor(shapeIdx)891 var faces []face892 var girdle []v3893 switch shapeIdx {894 case shapeRose:895 faces, girdle = buildRose(c)896 case shapeRaw:897 faces, girdle = buildRaw(fs.stone)898 default:899 faces, girdle = buildCut(c)900 }901 raw := shapeIdx == shapeRaw902903 sh := fs.prof.shadow904 if fs.prof.neutral != "" {905 base = fs.prof.neutral906 }907 base = mixHex(base, sh, fs.prof.bodyMix)908 deep := darken(base, sh, 0.9)909 bright := lighten(base, 0.5)910 fs.flash = flashRamp(28, fs.prof.sat)911 deepI, baseI, brightI := toRGBI(deep), toRGBI(base), toRGBI(bright)912 toneI := func(lum float64) rgbI {913 if lum < 0 {914 lum = 0915 }916 if lum > 1 {917 lum = 1918 }919 if lum < 0.5 {920 return mixI(deepI, baseI, lum*2)921 }922 return mixI(baseI, brightI, (lum-0.5)*2)923 }924 tone := func(lum float64) string { return toneI(lum).hex() }925926 yaw := fs.zone.floatIn(-13, 13) * math.Pi / 180927 th := viewTilt * math.Pi / 180928 cy, sy := math.Cos(yaw), math.Sin(yaw)929 ct, stt := math.Cos(th), math.Sin(th)930 view := func(p v3) v3 {931 x1, y1 := p.x*cy-p.y*sy, p.x*sy+p.y*cy932 return v3{x1, y1*ct + p.z*stt, p.z*ct - y1*stt}933 }934 ex0, ey0, ex1, ey1 := 1e9, 1e9, -1e9, -1e9935 for _, f := range faces {936 for _, p := range f.p {937 q := view(p)938 ex0, ey0 = math.Min(ex0, q.x), math.Min(ey0, q.y)939 ex1, ey1 = math.Max(ex1, q.x), math.Max(ey1, q.y)940 }941 }942 ox, oy := -(ex0+ex1)/2, -(ey0+ey1)/2943 scr := func(p v3) pt { return pt{subjX + p.x + ox, pieceCY - (p.y + oy)} }944 light := vnorm(v3{-0.5, 0.62, 0.6})945 inside := v3{0, 0, -0.25 * c.ry}946 if raw {947 inside = v3{}948 } else if shapeIdx == shapeRose {949 inside = v3{0, 0, 0.15 * c.ry}950 }951952 type drawn struct {953 pts []pt954 depth float64955 fill string956 kind int957 lum float64958 nv v3959 jit, drift, thick float64960 ext bool961 }962 shadeFront := func(kind int, nv, L v3, jit, drift, thick float64, ext bool) (string, float64) {963 H := vnorm(v3{L.x, L.y, L.z + 1})964 lam := math.Max(0, vdot(nv, L))965 lum := 0.03 + 0.7*math.Pow(lam, 2.1) + jit966 if ext {967 lum *= 0.3968 }969 if kind == kTable {970 lum = 0.36 + 0.26*lam971 }972 if kind == kPav {973 lum = 0.22 + 0.5*math.Max(0, vdot(vscale(nv, -1), L)) + jit974 }975 if raw {976 lum = 0.3 + 0.42*math.Pow(lam, 1.4) + 0.22*math.Max(0, vdot(vscale(nv, -1), L)) + jit*0.6977 }978 col := mixI(toneI(lum), deepI, 0.34*thick*thick)979 if sp := math.Pow(math.Max(0, vdot(nv, H)), 28); sp > 0.3 {980 col = mixI(col, whiteI, sp*0.85)981 }982 if lum > 0.5 && kind != kTable {983 amt := math.Min(1, (lum-0.5)/0.4) * dispersionMix * fs.prof.sat984 t := math.Atan2(nv.y, nv.x)/math.Pi + drift/40985 col = mixI(col, spectrumAtI(t), amt)986 }987 return hex3I(hueRotateI(col, drift)), lum988 }989 var back, front, pav []drawn990 var all []pt991 for _, f := range faces {992 vp := make([]v3, len(f.p))993 ctr := v3{}994 for i, p := range f.p {995 vp[i] = view(p)996 ctr = v3{ctr.x + p.x, ctr.y + p.y, ctr.z + p.z}997 }998 ctr = vscale(ctr, 1/float64(len(f.p)))999 nm := v3{}1000 for i := range f.p {1001 a, b := f.p[i], f.p[(i+1)%len(f.p)]1002 nm = v3{nm.x + (a.y-b.y)*(a.z+b.z), nm.y + (a.z-b.z)*(a.x+b.x), nm.z + (a.x-b.x)*(a.y+b.y)}1003 }1004 nm = vnorm(nm)1005 if vdot(nm, vsub(ctr, inside)) < 0 {1006 nm = vscale(nm, -1)1007 }1008 nv := vnorm(view(nm))1009 d := drawn{kind: f.kind}1010 for _, p := range vp {1011 d.pts = append(d.pts, scr(p))1012 d.depth += p.z1013 all = append(all, scr(p))1014 }1015 d.depth /= float64(len(vp))1016 rN := math.Sqrt((ctr.x/c.rx)*(ctr.x/c.rx) + (ctr.y/c.ry)*(ctr.y/c.ry))1017 if rN > 1 {1018 rN = 11019 }1020 thick := 1 - rN1021 jit := fs.zone.floatIn(-0.13, 0.13)1022 if nv.z > 0.02 {1023 ext := f.kind == kCrown && fs.zone.floatIn(0, 1) < 0.281024 drift := fs.zone.floatIn(-fireZoneDeg, fireZoneDeg)1025 d.nv, d.jit, d.drift, d.thick, d.ext = nv, jit, drift, thick, ext1026 d.fill, d.lum = shadeFront(f.kind, nv, light, jit, drift, thick, ext)1027 front = append(front, d)1028 if f.kind == kPav {1029 pav = append(pav, d)1030 }1031 } else if f.kind == kPav {1032 inner := vscale(nv, -1)1033 lum := 0.2 + 0.6*math.Max(0, vdot(inner, light)) + jit1034 d.fill = hex3(mixHex(tone(lum), deep, 0.34*thick*thick))1035 d.lum = lum1036 back = append(back, d)1037 pav = append(pav, d)1038 }1039 }1040 sortDrawn := func(ds []drawn) {1041 for i := 1; i < len(ds); i++ {1042 for j := i; j > 0 && ds[j].depth < ds[j-1].depth; j-- {1043 ds[j], ds[j-1] = ds[j-1], ds[j]1044 }1045 }1046 }1047 sortDrawn(back)1048 sortDrawn(front)10491050 hull := convexHull(all)1051 if shapeIdx == shapeHeart {1052 hull = nil1053 for _, g := range girdle {1054 hull = append(hull, scr(view(g)))1055 }1056 }1057 var underB, facetsB strings.Builder1058 underB.WriteString(`<defs><radialGradient id="bd` + salt + `" cx=".4" cy=".35" r=".75">` +1059 `<stop offset="0" stop-color="` + hex3(base) + `"/><stop offset=".5" stop-color="` + hex3(darken(base, sh, 0.25)) + `"/><stop offset="1" stop-color="` + hex3(darken(base, sh, 0.85)) + `"/></radialGradient></defs>` +1060 `<path d="` + relPath(hull) + `" fill="url(#bd` + salt + `)" opacity=".7"/><g fill-opacity=".6">`)1061 for _, d := range back {1062 underB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `"/>`)1063 }1064 underB.WriteString(`</g>`)10651066 nCrown := 01067 for _, d := range front {1068 if d.kind == kCrown {1069 nCrown++1070 }1071 }1072 candP := 1.01073 if nCrown > fireMaxFacets {1074 candP = float64(fireMaxFacets) / float64(nCrown) * 1.251075 }1076 nCand := 01077 nSplit := 01078 if raw || fs.thumb {1079 candP = 01080 }1081 for _, d := range front {1082 if d.kind == kTable && shapeIdx == shapeHeart {1083 facetsB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `" fill-opacity=".46"/>`)1084 continue1085 }1086 if d.kind == kTable {1087 facetsB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `" fill-opacity=".46"/>`)1088 win := convexHull(d.pts)1089 tx, ty := centroid(win)1090 if len(pav) > 0 && !fs.thumb {1091 bx0, by0, bx1, by1 := 1e9, 1e9, -1e9, -1e91092 for _, b := range pav {1093 for _, q := range b.pts {1094 bx0, by0 = math.Min(bx0, q.x), math.Min(by0, q.y)1095 bx1, by1 = math.Max(bx1, q.x), math.Max(by1, q.y)1096 }1097 }1098 wx0, wy0, wx1, wy1 := 1e9, 1e9, -1e9, -1e91099 for _, q := range win {1100 wx0, wy0 = math.Min(wx0, q.x), math.Min(wy0, q.y)1101 wx1, wy1 = math.Max(wx1, q.x), math.Max(wy1, q.y)1102 }1103 k := 0.92 * math.Min((wx1-wx0)/math.Max(1, bx1-bx0), (wy1-wy0)/math.Max(1, by1-by0))1104 bx, by := (bx0+bx1)/2, (by0+by1)/21105 var mosB strings.Builder1106 nRefl := 01107 for _, b := range pav {1108 if nRefl >= 8 {1109 break1110 }1111 mp := make([]pt, len(b.pts))1112 for i, q := range b.pts {1113 mp[i] = pt{tx + (q.x-bx)*k, ty - (q.y-by)*k}1114 }1115 cl := clipToConvex(mp, win)1116 if len(cl) < 3 || polyArea(cl) < 4 {1117 continue1118 }1119 nRefl++1120 mosB.WriteString(`<path d="` + relPath(cl) + `" fill="` + hex3(tone(b.lum*0.95)) + `"/>`)1121 }1122 if mosB.Len() > 0 {1123 facetsB.WriteString(`<g fill-opacity=".55">` + mosB.String() + `</g>`)1124 }1125 }1126 continue1127 }1128 if d.kind == kCrown && nCand < fireMaxFacets && fs.zone.floatIn(0, 1) < candP {1129 nCand++1130 cx, cyy := centroid(d.pts)1131 fs.ringCent = append(fs.ringCent, pt{cx, cyy})1132 facetsB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `" class="f"/>`)1133 continue1134 }1135 if !raw && nSplit < 6 && polyArea(d.pts) > 26 && fs.zone.floatIn(0, 1) < 0.5 {1136 a, b := splitPoly(d.pts, fs.zone.floatIn(0, math.Pi))1137 if len(a) >= 3 && len(b) >= 3 {1138 nSplit++1139 lo := hex3(mixHex(tone(math.Max(0, d.lum*0.6-0.04)), deep, 0.18))1140 hi := hex3(tone(math.Min(1, d.lum*1.35+0.06)))1141 facetsB.WriteString(`<path d="` + relPath(a) + `" fill="` + hi + `"/><path d="` + relPath(b) + `" fill="` + lo + `"/>`)1142 continue1143 }1144 }1145 facetsB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `"/>`)1146 }11471148 _, _, _, _, rotDeg := motionPhases(salt)1149 type cand struct {1150 i int1151 fill string1152 delta float641153 }1154 for si, sgn := range []float64{1, -1} {1155 if fs.thumb {1156 break1157 }1158 a := -sgn * rotDeg * lightSwing * math.Pi / 1801159 ca, sa := math.Cos(a), math.Sin(a)1160 L := vnorm(v3{light.x*ca - light.y*sa, light.x*sa + light.y*ca, light.z})1161 var cs []cand1162 for i, d := range front {1163 if d.kind == kTable {1164 continue1165 }1166 f2, _ := shadeFront(d.kind, d.nv, L, d.jit, d.drift, d.thick, d.ext)1167 if dl := lumaDelta(f2, d.fill); dl > 0.06 && polyArea(d.pts) > 3 {1168 cs = append(cs, cand{i, f2, dl})1169 }1170 }1171 for i := 1; i < len(cs); i++ {1172 for j := i; j > 0 && cs[j].delta > cs[j-1].delta; j-- {1173 cs[j], cs[j-1] = cs[j-1], cs[j]1174 }1175 }1176 if len(cs) > lightMaxFacets {1177 cs = cs[:lightMaxFacets]1178 }1179 if len(cs) == 0 {1180 continue1181 }1182 cls := "gt-la"1183 if si == 1 {1184 cls = "gt-lb"1185 }1186 facetsB.WriteString(`<g class="` + cls + `" opacity="0">`)1187 for _, c := range cs {1188 facetsB.WriteString(`<path d="` + relPath(front[c.i].pts) + `" fill="` + c.fill + `"/>`)1189 }1190 facetsB.WriteString(`</g>`)1191 }11921193 gd, lastG := "", ""1194 for i := range girdle {1195 a, b := view(girdle[i]), view(girdle[(i+1)%len(girdle)])1196 mid := vscale(v3{girdle[i].x + girdle[(i+1)%len(girdle)].x, girdle[i].y + girdle[(i+1)%len(girdle)].y, 0}, 0.5)1197 if view(vnorm(mid)).z-0 > -0.05 && view(mid).z >= view(v3{}).z-0.01 {1198 pa, pb := scr(a), scr(b)1199 sa := n1(pa.x) + " " + n1(pa.y)1200 if sa != lastG {1201 gd += "M" + sa1202 }1203 lastG = n1(pb.x) + " " + n1(pb.y)1204 gd += " " + lastG1205 }1206 }1207 over = `<path d="` + gd + `" fill="none" stroke="` + hex3(lighten(base, 0.18)) + `" stroke-width=".7" stroke-linecap="round" opacity=".4"/>`1208 return underB.String(), facetsB.String(), over1209}12101211const dispersionMix = 0.3212121213var spectrum = []string{"#ff4040", "#ff9a1f", "#ffe81f", "#35ff7a", "#22ccff", "#5566ff", "#c04dff"}12141215var spectrumI = func() []rgbI {1216 out := make([]rgbI, len(spectrum))1217 for i, h := range spectrum {1218 out[i] = toRGBI(h)1219 }1220 return out1221}()12221223func spectrumAtI(t float64) rgbI {1224 t = t - math.Floor(t)1225 f := t * float64(len(spectrumI))1226 i := int(f)1227 return mixI(spectrumI[i%len(spectrumI)], spectrumI[(i+1)%len(spectrumI)], f-float64(i))1228}12291230func spectrumAt(t float64) string {1231 t = t - math.Floor(t)1232 f := t * float64(len(spectrum))1233 i := int(f)1234 return mixHex(spectrum[i%len(spectrum)], spectrum[(i+1)%len(spectrum)], f-float64(i))1235}12361237const (1238 lightSwing = 2.21239 lightMaxFacets = 121240)12411242func lumaDelta(a, b string) float64 {1243 wide := func(h string) string {1244 if len(h) == 4 {1245 return "#" + h[1:2] + h[1:2] + h[2:3] + h[2:3] + h[3:4] + h[3:4]1246 }1247 return h1248 }1249 ar, ag, ab := hexToRGB(wide(a))1250 br, bg, bb := hexToRGB(wide(b))1251 d := (0.2126*float64(ar-br) + 0.7152*float64(ag-bg) + 0.0722*float64(ab-bb)) / 2551252 if d < 0 {1253 return -d1254 }1255 return d1256}12571258const fireMaxFacets = 1212591260const starfield = true12611262func renderStars(salt string) string {1263 if !starfield {1264 return ""1265 }1266 rnd := newPRNG("stars" + salt)1267 type star struct{ x, y float64 }1268 var byLevel [4][]star1269 for i := 0; i < 36; i++ {1270 x, y := rnd.floatIn(3, canvasW-3), rnd.floatIn(3, 127)1271 k := rnd.intn(20)1272 dx, dy := x-subjX, y-subjY1273 if r := math.Sqrt(dx*dx + dy*dy); r > 43 && r < 58 {1274 continue1275 }1276 lvl := 0 // most stars are faint; a few are bright1277 switch {1278 case k >= 18:1279 lvl = 31280 case k >= 14:1281 lvl = 21282 case k >= 8:1283 lvl = 11284 }1285 byLevel[lvl] = append(byLevel[lvl], star{x, y})1286 }1287 widths := [4]string{".45", ".6", ".85", "1.15"}1288 ops := [4]string{".22", ".38", ".58", ".85"}1289 var b strings.Builder1290 b.WriteString(`<g stroke="#fff" stroke-linecap="round" fill="none">`)1291 var tw []star1292 for l := 0; l < 4; l++ {1293 if len(byLevel[l]) == 0 {1294 continue1295 }1296 d := ""1297 for i, st := range byLevel[l] {1298 if l >= 1 && l <= 2 && len(tw) < 3 && i%3 == 0 {1299 tw = append(tw, st)1300 continue1301 }1302 d += "M" + n1(st.x) + " " + n1(st.y) + "h.01"1303 }1304 if d != "" {1305 b.WriteString(`<path d="` + d + `" stroke-width="` + widths[l] + `" opacity="` + ops[l] + `"/>`)1306 }1307 }1308 for _, st := range tw {1309 dur := rnd.floatIn(5, 12)1310 b.WriteString(`<path d="M` + n1(st.x) + " " + n1(st.y) + `h.01" stroke-width=".9" opacity=".4" class="gt-tw" style="--d:` + fnum1(dur) + `s;--dl:-` + fnum1(rnd.floatIn(0, dur)) + `s"/>`)1311 }1312 b.WriteString(`</g>`)1313 return b.String()1314}13151316func pearlLusterGradient(gid, base, metal string) string {1317 r, g, b := hexToRGB(base)1318 lum := (0.2126*float64(r) + 0.7152*float64(g) + 0.0722*float64(b)) / 2551319 shadow := "#4a4038"1320 if lum < 0.4 {1321 shadow = "#000000"1322 }1323 hi := base1324 sc, ss, sw := ".6", ".32", ".16"1325 mo := ".3"1326 if lum < 0.4 {1327 mo = ".22"1328 hi = mixHex(base, "#c9d2d8", 0.28)1329 sc, ss, sw = ".34", ".16", ".07"1330 }1331 return `<radialGradient id="` + gid + `" cx=".4" cy=".36" r=".7" fx=".34" fy=".28">` +1332 `<stop offset="0" stop-color="` + hi + `"/>` +1333 `<stop offset=".4" stop-color="` + mixHex(base, shadow, 0.13) + `"/>` +1334 `<stop offset=".76" stop-color="` + mixHex(base, shadow, 0.36) + `"/>` +1335 `<stop offset=".93" stop-color="` + mixHex(base, shadow, 0.5) + `"/>` +1336 `<stop offset="1" stop-color="` + mixHex(base, shadow, 0.3) + `"/></radialGradient>` +1337 `<radialGradient id="` + gid + `o" cx=".45" cy=".44" r=".62">` +1338 `<stop offset=".3" stop-color="#fff" stop-opacity="0"/>` +1339 `<stop offset=".62" stop-color="#ffb3cf" stop-opacity=".2"/>` +1340 `<stop offset=".88" stop-color="#b3ffd6" stop-opacity=".2"/>` +1341 `<stop offset="1" stop-color="#c3d3ff" stop-opacity=".12"/></radialGradient>` +1342 `<radialGradient id="` + gid + `s" cx=".36" cy=".3" r=".4">` +1343 `<stop offset="0" stop-color="#fff" stop-opacity="` + sc + `"/>` +1344 `<stop offset=".35" stop-color="#fff" stop-opacity="` + ss + `"/>` +1345 `<stop offset=".7" stop-color="#fff" stop-opacity=".06"/>` +1346 `<stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient>` +1347 `<radialGradient id="` + gid + `m" cx=".42" cy=".36" r=".68">` +1348 `<stop offset=".78" stop-color="` + metal + `" stop-opacity="0"/>` +1349 `<stop offset=".93" stop-color="` + metal + `" stop-opacity="` + mo + `"/>` +1350 `<stop offset="1" stop-color="` + metal + `" stop-opacity="0"/></radialGradient>` +1351 `<radialGradient id="` + gid + `w" cx=".66" cy=".73" r=".2">` +1352 `<stop offset="0" stop-color="#fff" stop-opacity="` + sw + `"/>` +1353 `<stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient>`1354}13551356func pearlLayers(shape, gid, between string) string {1357 i := strings.Index(shape, ` fill="`)1358 if i < 0 {1359 return shape1360 }1361 cp := func(suffix string) string { return shape[:i] + ` fill="url(#` + gid + suffix + `)"/>` }1362 return shape + cp("o") + cp("m") + between + cp("w") + cp("s")1363}13641365func pearlSweepColorIntensity(base string) float64 {1366 r, g, b := hexToRGB(base)1367 lum := 0.2126*float64(r)/255 + 0.7152*float64(g)/255 + 0.0722*float64(b)/2551368 if lum < 0.4 {1369 return 0.551370 }1371 return 2.01372}13731374func pearlSweepGradient(gid, base string) string {1375 mult := pearlSweepColorIntensity(base)1376 o1, o2, o3 := fnum(0.3*mult), fnum(0.35*mult), fnum(0.3*mult)1377 return `<linearGradient id="` + gid + `" x1="0%" y1="0%" x2="100%" y2="0%">` +1378 `<stop offset="0%" stop-color="#ffffff" stop-opacity="0"/>` +1379 `<stop offset="18%" stop-color="#ffd2e6" stop-opacity="` + o1 + `"/>` +1380 `<stop offset="38%" stop-color="#cfe8ff" stop-opacity="` + o2 + `"/>` +1381 `<stop offset="58%" stop-color="#d4ffe4" stop-opacity="` + o3 + `"/>` +1382 `<stop offset="80%" stop-color="#fff0c2" stop-opacity="` + o3 + `"/>` +1383 `<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>` +1384 `</linearGradient>`1385}13861387func pearlSweepOverlay(salt, base, shapeGeom string, w, h float64) string {1388 rnd := newPRNG(salt + "-sweep")1389 const passes = 31390 total := 15.0 + rnd.floatIn(0, 9)13911392 durs := make([]float64, passes)1393 sumDur := 0.01394 for i := 0; i < passes; i++ {1395 durs[i] = 2.4 + rnd.floatIn(0, 2.2)1396 sumDur += durs[i]1397 }1398 gapTotal := total - sumDur1399 if gapTotal < float64(passes)*0.5 {1400 gapTotal = float64(passes) * 0.51401 }1402 weights := make([]float64, passes)1403 sumW := 0.01404 for i := 0; i < passes; i++ {1405 weights[i] = 0.4 + rnd.floatIn(0, 1)1406 sumW += weights[i]1407 }1408 gaps := make([]float64, passes)1409 for i := 0; i < passes; i++ {1410 gaps[i] = weights[i] / sumW * gapTotal1411 }1412 starts := make([]float64, passes)1413 t := gaps[0]1414 for i := 0; i < passes; i++ {1415 starts[i] = t1416 t += durs[i]1417 if i < passes-1 {1418 t += gaps[i+1]1419 }1420 }14211422 travel := w * 1.051423 clipID := "clp" + salt1424 gradID := "swp" + salt1425 off, on1, on2, out := fnum(-travel), fnum(-travel*0.5), fnum(travel*0.5), fnum(travel)14261427 var defs strings.Builder1428 defs.WriteString(`<defs><clipPath id="` + clipID + `">` + shapeGeom + `</clipPath>`)1429 defs.WriteString(pearlSweepGradient(gradID, base))14301431 var marks strings.Builder1432 marks.WriteString(`<g clip-path="url(#` + clipID + `)">`)1433 for i := 0; i < passes; i++ {1434 angle := fnum(rnd.floatIn(0, 360))1435 startPct := starts[i] / total * 1001436 durPct := durs[i] / total * 1001437 rampPct := durPct * 0.321438 if rampPct > 6 {1439 rampPct = 61440 }1441 p0, p1, p2, p3 := startPct, startPct+rampPct, startPct+durPct-rampPct, startPct+durPct1442 kf := "swp" + salt + "_" + ufmt.Sprintf("%d", i)14431444 closing := ""1445 if p3 < 99.999 {1446 closing = `100%{transform:translate(` + off + `px,0);opacity:0}`1447 }1448 defs.WriteString(`<style>@keyframes ` + kf + `{` +1449 `0%{transform:translate(` + off + `px,0);opacity:0}` +1450 fnum(p0) + `%{transform:translate(` + off + `px,0);opacity:0}` +1451 fnum(p1) + `%{transform:translate(` + on1 + `px,0);opacity:1}` +1452 fnum(p2) + `%{transform:translate(` + on2 + `px,0);opacity:1}` +1453 fnum(p3) + `%{transform:translate(` + out + `px,0);opacity:0}` +1454 closing +1455 `}</style>`)14561457 marks.WriteString(`<g transform="rotate(` + angle + ` ` + fnum(subjX) + ` ` + fnum(subjY) + `)">` +1458 `<rect x="` + fnum(subjX-w*1.3) + `" y="` + fnum(subjY-h*0.95) + `" width="` + fnum(w*2.6) + `" height="` + fnum(h*1.9) + `" fill="url(#` + gradID + `)" opacity="0" style="mix-blend-mode:screen;animation:` + kf + ` ` + fnum(total) + `s linear infinite"/>` +1459 `</g>`)1460 }1461 marks.WriteString(`</g>`)1462 defs.WriteString(`</defs>`)1463 return defs.String() + marks.String()1464}14651466func pearlRound(base, metal, salt string) string {1467 gid := "prl" + salt1468 r := 24.0 * stoneScale1469 body := `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(r) + `" fill="url(#` + gid + `)"/>`1470 clipGeom := `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(r) + `"/>`1471 sweep := pearlSweepOverlay(salt, base, clipGeom, r*2, r*2)1472 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, "") + sweep1473}14741475func pearlOval(base, metal, salt string) string {1476 gid := "prl" + salt1477 rx, ry := 18.0*stoneScale, 25.0*stoneScale1478 body := `<ellipse cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" rx="` + fnum(rx) + `" ry="` + fnum(ry) + `" fill="url(#` + gid + `)"/>`1479 clipGeom := `<ellipse cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" rx="` + fnum(rx) + `" ry="` + fnum(ry) + `"/>`1480 sweep := pearlSweepOverlay(salt, base, clipGeom, rx*2, ry*2)1481 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, "") + sweep1482}14831484func pearlButton(base, metal, salt string) string {1485 gid := "prl" + salt1486 rx, ry := 25.0*stoneScale, 18.0*stoneScale1487 body := `<ellipse cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" rx="` + fnum(rx) + `" ry="` + fnum(ry) + `" fill="url(#` + gid + `)"/>`1488 clipGeom := `<ellipse cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" rx="` + fnum(rx) + `" ry="` + fnum(ry) + `"/>`1489 sweep := pearlSweepOverlay(salt, base, clipGeom, rx*2, ry*2)1490 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, "") + sweep1491}14921493func pearlDrop(base, metal, salt string) string {1494 gid := "prl" + salt1495 w, h := 32.0*stoneScale, 42.0*stoneScale1496 path := `M ` + fnum(subjX) + ` ` + fnum(subjY-h*0.55) +1497 ` C ` + fnum(subjX+w*0.55) + ` ` + fnum(subjY-h*0.15) + ` ` + fnum(subjX+w*0.42) + ` ` + fnum(subjY+h*0.42) + ` ` + fnum(subjX) + ` ` + fnum(subjY+h*0.48) +1498 ` C ` + fnum(subjX-w*0.42) + ` ` + fnum(subjY+h*0.42) + ` ` + fnum(subjX-w*0.55) + ` ` + fnum(subjY-h*0.15) + ` ` + fnum(subjX) + ` ` + fnum(subjY-h*0.55) + ` Z`1499 body := `<path d="` + path + `" fill="url(#` + gid + `)"/>`1500 clipGeom := `<path d="` + path + `"/>`1501 sweep := pearlSweepOverlay(salt, base, clipGeom, w*1.6, h*0.9)1502 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, "") + sweep1503}15041505func pearlBaroqueOutline(seed string) []pt {1506 rnd := newPRNG(seed)1507 n := 101508 pts := make([]pt, n)1509 for k := 0; k < n; k++ {1510 a := 2 * math.Pi * float64(k) / float64(n)1511 rr := 21.0 * stoneScale * (0.75 + 0.4*rnd.floatIn(0, 1))1512 pts[k] = pt{subjX + rr*math.Cos(a)*1.1, subjY + rr*math.Sin(a)*0.9}1513 }1514 return pts1515}15161517func pearlBaroqueSmoothPath(pts []pt) string {1518 n := len(pts)1519 mid := func(a, b pt) pt { return pt{(a.x + b.x) / 2, (a.y + b.y) / 2} }1520 start := mid(pts[n-1], pts[0])1521 path := "M " + fnum(start.x) + " " + fnum(start.y)1522 for k := 0; k < n; k++ {1523 next := pts[(k+1)%n]1524 m := mid(pts[k], next)1525 path += " Q " + fnum(pts[k].x) + " " + fnum(pts[k].y) + " " + fnum(m.x) + " " + fnum(m.y)1526 }1527 return path + " Z"1528}15291530func pearlBaroque(base, metal, salt string) string {1531 gid := "prl" + salt1532 pathD := pearlBaroqueSmoothPath(pearlBaroqueOutline("baroque" + salt))1533 body := `<path d="` + pathD + `" fill="url(#` + gid + `)"/>`1534 clipGeom := `<path d="` + pathD + `"/>`1535 sweep := pearlSweepOverlay(salt, base, clipGeom, 40*stoneScale, 30*stoneScale)1536 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, "") + sweep1537}15381539func pearlCircled(base, metal, salt string) string {1540 gid := "prl" + salt1541 r := 24.0 * stoneScale1542 body := `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(r) + `" fill="url(#` + gid + `)"/>`1543 ringTone := mixHex(base, "#000000", 0.12)1544 rings := ""1545 for _, f := range []float64{0.42, 0.58, 0.74, 0.9} {1546 rings += `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(r*f) + `" fill="none" stroke="` + ringTone + `" stroke-width="0.9" opacity="0.35"/>`1547 }1548 clipGeom := `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(r) + `"/>`1549 sweep := pearlSweepOverlay(salt, base, clipGeom, r*2, r*2)1550 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, rings) + sweep1551}15521553var PearlShapeNames = []string{"Round", "Oval", "Button", "Drop", "Baroque", "Circled"}15541555func renderPearlShape(shapeIdx int, base, metal, salt string) string {1556 switch shapeIdx {1557 case 0:1558 return pearlRound(base, metal, salt)1559 case 1:1560 return pearlOval(base, metal, salt)1561 case 2:1562 return pearlButton(base, metal, salt)1563 case 3:1564 return pearlDrop(base, metal, salt)1565 case 4:1566 return pearlBaroque(base, metal, salt)1567 default:1568 return pearlCircled(base, metal, salt)1569 }1570}15711572func renderRing(metalHi, metalLo, salt string) string {1573 gid := "ring" + salt1574 bright := lighten(metalHi, 0.45)1575 deep := mixHex(metalLo, "#000000", 0.45)1576 st := func(off, c string) string { return `<stop offset="` + off + `" stop-color="` + c + `"/>` }1577 defs := `<linearGradient id="` + gid + `" x2="1" y2="1">` +1578 st("0", bright) + st(".12", metalHi) + st(".26", deep) + st(".4", metalLo) + st(".52", bright) +1579 st(".64", metalHi) + st(".78", deep) + st(".9", metalLo) + st("1", bright) + `</linearGradient>`15801581 outer := `<g fill="none" transform="translate(` + cnum(subjX) + ` ` + cnum(subjY) + `)">` +1582 `<circle r="52.6" stroke="#000" stroke-width="1.6" opacity=".45"/>` +1583 `<circle r="50" stroke="url(#` + gid + `)" stroke-width="4.4"/>` +1584 `<circle r="52.3" stroke="` + deep + `" stroke-width=".7" opacity=".8"/>` +1585 `<circle r="47.7" stroke="` + deep + `" stroke-width=".6" opacity=".7"/>` +1586 `<circle r="48.5" stroke="` + lighten(metalHi, 0.35) + `" stroke-width=".5" opacity=".45"/></g>`15871588 const rOut = 46.01589 const coreHalf = 0.61590 const outwardBloom = 0.91591 glowR := rOut + outwardBloom + 31592 pct := func(v float64) float64 {1593 p := v / glowR * 1001594 if p < 0 {1595 p = 01596 }1597 if p > 100 {1598 p = 1001599 }1600 return p1601 }1602 coreColor := lighten(metalHi, 0.25)1603 donutStops := func(inwardBloom, peakOp float64, gid string) string {1604 bloomOp := peakOp * 0.351605 midOp := peakOp * 0.71606 return `<radialGradient id="` + gid + `" cx="50%" cy="50%" r="50%">` +1607 `<stop offset="0%" stop-color="` + metalHi + `" stop-opacity="0"/>` +1608 `<stop offset="` + pctnum(pct(rOut-inwardBloom-1.5)) + `%" stop-color="` + metalHi + `" stop-opacity="0"/>` +1609 `<stop offset="` + pctnum(pct(rOut-inwardBloom)) + `%" stop-color="` + metalHi + `" stop-opacity="` + fnum(bloomOp) + `"/>` +1610 `<stop offset="` + pctnum(pct(rOut-coreHalf)) + `%" stop-color="` + coreColor + `" stop-opacity="` + fnum(midOp) + `"/>` +1611 `<stop offset="` + pctnum(pct(rOut)) + `%" stop-color="` + coreColor + `" stop-opacity="` + fnum(peakOp) + `"/>` +1612 `<stop offset="` + pctnum(pct(rOut+coreHalf)) + `%" stop-color="` + coreColor + `" stop-opacity="` + fnum(midOp) + `"/>` +1613 `<stop offset="` + pctnum(pct(rOut+outwardBloom)) + `%" stop-color="` + metalHi + `" stop-opacity="` + fnum(bloomOp) + `"/>` +1614 `<stop offset="` + pctnum(pct(rOut+outwardBloom+1)) + `%" stop-color="` + metalHi + `" stop-opacity="0"/>` +1615 `<stop offset="100%" stop-color="` + metalHi + `" stop-opacity="0"/></radialGradient>`1616 }16171618 rnd := newPRNG("energy" + salt)1619 gidA, gidB := "glowA"+salt, "glowB"+salt1620 dur := rnd.floatIn(2.13, 3.2)1621 phase := rnd.floatIn(0, dur)16221623 stopsA := donutStops(2.5*2.0/3.0*0.5, 0.4, gidA)1624 stopsB := donutStops(8.0*2.0/3.0*0.5, 0.6, gidB)16251626 energy := `<defs>` + stopsA + stopsB + `</defs>` +1627 `<g>` +1628 `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(glowR) + `" fill="url(#` + gidA + `)"/>` +1629 `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(glowR) + `" fill="url(#` + gidB + `)" opacity="0" class="gt-glow" style="--glow-dur:` + fnum(dur) + `s;--glow-delay:-` + fnum(phase) + `s"/>` +1630 `</g>`16311632 return `<defs>` + defs + `</defs>` + outer + energy1633}16341635const (1636 motionVDur, motionHDur, motionRDur, motionSDur = 3.4, 4.66, 5.81, 6.561637 motionRotMin, motionRotMax = 4.0, 8.01638 motionScale = 1.091639)16401641func motionPhases(salt string) (vphase, hphase, rphase, sphase, rotDeg float64) {1642 rnd := newPRNG("motion" + salt)1643 vphase = rnd.floatIn(0, motionVDur)1644 hphase = rnd.floatIn(0, motionHDur)1645 rphase = rnd.floatIn(0, motionRDur)1646 sphase = rnd.floatIn(0, motionSDur)1647 rotDeg = rnd.floatIn(motionRotMin, motionRotMax)1648 return1649}16501651func applyMotion(content, salt string) string {1652 vphase, hphase, rphase, sphase, rotDeg := motionPhases(salt)16531654 return `<g transform="translate(` + fnum(subjX) + ` ` + fnum(subjY) + `)">` +1655 `<g class="gt-v" style="--v-dur:` + fnum(motionVDur) + `s;--v-delay:-` + fnum(vphase) + `s">` +1656 `<g class="gt-h" style="--h-dur:` + fnum(motionHDur) + `s;--h-delay:-` + fnum(hphase) + `s">` +1657 `<g class="gt-r" style="--r-dur:` + fnum(motionRDur) + `s;--r-delay:-` + fnum(rphase) + `s;--rot-deg:` + fnum(rotDeg) + `deg">` +1658 `<g class="gt-s" style="--s-dur:` + fnum(motionSDur) + `s;--s-delay:-` + fnum(sphase) + `s">` +1659 `<g transform="translate(-` + fnum(subjX) + ` -` + fnum(subjY) + `)">` +1660 content + `</g></g></g></g></g></g>`1661}16621663const dotX, dotY = subjX + 50, subjY + 4216641665func renderStatusBadge(active bool) string {1666 if active {1667 return `<circle cx="` + fnum(dotX) + `" cy="` + fnum(dotY) + `" r="4" fill="#7fd88f"/>` +1668 `<g transform="translate(` + fnum(dotX) + ` ` + fnum(dotY) + `)"><circle r="4" fill="none" stroke="#7fd88f" stroke-width="1.2" class="gt-badge-pulse" opacity="0.8"/></g>`1669 }1670 return `<circle cx="` + fnum(dotX) + `" cy="` + fnum(dotY) + `" r="4" fill="#5a5f6b"/>`1671}16721673type facetSpan struct {1674 start, end int1675 ringMember bool1676}16771678func scanFacetSpans(markup string) []facetSpan {1679 var spans []facetSpan1680 i := 01681 for {1682 rel := strings.Index(markup[i:], "<")1683 if rel < 0 {1684 break1685 }1686 start := i + rel1687 relEnd := strings.Index(markup[start:], "/>")1688 if relEnd < 0 {1689 break1690 }1691 end := start + relEnd + 21692 tag := markup[start:end]1693 i = end1694 isFacetTag := strings.HasPrefix(tag, "<path") || strings.HasPrefix(tag, "<circle") || strings.HasPrefix(tag, "<polygon")1695 if !isFacetTag {1696 continue1697 }1698 spans = append(spans, facetSpan{start: start, end: end, ringMember: strings.Contains(tag, `class="f"`)})1699 }1700 return spans1701}17021703const (1704 fireSweepAxis = -35.0 * math.Pi / 1801705 fireSweepSpan = 1.6 // seconds a sweep takes to cross the stone1706)17071708func applyCascade(markup, tidStr string, fs *facetStyle) (overlay string, css string) {1709 if fs == nil || len(fs.ringCent) == 0 {1710 return "", ""1711 }1712 spans := scanFacetSpans(markup)1713 rnd := newPRNG("fire" + tidStr)1714 _, _, rphase, _, _ := motionPhases(tidStr)1715 period := 2 * motionRDur1716 axis := fireSweepAxis + rnd.floatIn(-0.3, 0.3)1717 dx, dy := math.Cos(axis), math.Sin(axis)17181719 ka, kb := "fa"+tidStr, "fb"+tidStr1720 shape := func(at float64) string {1721 return fnum1(at) + `%{opacity:0}` + fnum1(at+2) + `%{opacity:var(--o);fill:#fff}` +1722 fnum1(at+4.5) + `%{opacity:.4;fill:var(--c)}` + fnum1(at+8) + `%{opacity:0}`1723 }1724 per := fnum(period)1725 css = `.` + ka + `{animation:` + ka + ` ` + per + `s linear infinite}.` + kb + `{animation:` + kb + ` ` + per + `s linear infinite}` +1726 `@keyframes ` + ka + `{` + shape(12.5) + `100%{opacity:0}}` +1727 `@keyframes ` + kb + `{` + shape(62.5) + `100%{opacity:0}}`17281729 var out strings.Builder1730 type glintAt struct {1731 x, y, delay float641732 rule string1733 }1734 var cands []glintAt1735 ringIdx := -11736 for _, span := range spans {1737 if !span.ringMember {1738 continue1739 }1740 ringIdx++1741 if ringIdx >= len(fs.ringCent) {1742 break1743 }1744 c := fs.ringCent[ringIdx]1745 color, colorOp := fs.flash.sample(c.x, c.y)1746 pos := ((c.x-subjX)*dx + (c.y-pieceCY)*dy) / (30 * stoneScale)1747 name := ka1748 if rnd.floatIn(0, 1) < 0.5 {1749 name, pos = kb, -pos1750 }1751 delay := rphase - 0.25*motionRDur + period - pos*fireSweepSpan/21752 tag := markup[span.start:span.end]1753 geomEnd := strings.Index(tag, `" fill=`)1754 if geomEnd < 0 {1755 continue1756 }1757 cands = append(cands, glintAt{c.x, c.y, delay, name})1758 out.WriteString(tag[:geomEnd] + `" opacity="0" class="` + name + `" style="--o:` + fnum(math.Round(math.Min(1, rnd.floatIn(0.85, 1.05)*colorOp)*10)/10) + `;--c:` + hex3(color) +1759 `;animation-delay:-` + fnum1(delay) + `s"/>`)1760 }1761 if len(cands) > 0 && fs.prof.sat >= 0.3 {1762 n := 31763 if fs.prof.sat < 0.6 {1764 n = 2 // quartz, topaz: less fire, fewer stars1765 }1766 ga, gb := "ga"+tidStr, "gb"+tidStr1767 star := func(at float64) string {1768 return fnum1(at+0.6) + `%{opacity:0;transform:scale(.2) rotate(0)}` +1769 fnum1(at+2.2) + `%{opacity:1;transform:scale(1) rotate(25deg)}` +1770 fnum1(at+4.4) + `%{opacity:0;transform:scale(.35) rotate(55deg)}`1771 }1772 css += `.` + ga + `,.` + gb + `{opacity:0}` +1773 `.` + ga + `{animation:` + ga + ` ` + per + `s linear infinite}.` + gb + `{animation:` + gb + ` ` + per + `s linear infinite}` +1774 `@keyframes ` + ga + `{0%{opacity:0}` + star(12.5) + `100%{opacity:0}}` +1775 `@keyframes ` + gb + `{0%{opacity:0}` + star(62.5) + `100%{opacity:0}}`1776 r := 7.5 * stoneScale1777 w := 0.9 * stoneScale1778 ray := `M0 -` + fnum1(r) + `L` + fnum1(w) + ` -` + fnum1(w) + `L` + fnum1(r) + ` 0L` + fnum1(w) + ` ` + fnum1(w) +1779 `L0 ` + fnum1(r) + `L-` + fnum1(w) + ` ` + fnum1(w) + `L-` + fnum1(r) + ` 0L-` + fnum1(w) + ` -` + fnum1(w) + `Z`1780 var g strings.Builder1781 used := map[int]bool{}1782 for k := 0; k < n && k < len(cands); k++ {1783 i := rnd.intn(len(cands))1784 for tries := 0; used[i] && tries < len(cands); tries++ {1785 i = (i + 1) % len(cands)1786 }1787 used[i] = true1788 c := cands[i]1789 cls := "ga" + tidStr1790 if c.rule == kb {1791 cls = "gb" + tidStr1792 }1793 g.WriteString(`<g transform="translate(` + fnum1(c.x) + ` ` + fnum1(c.y) + `)"><g class="` + cls + `" opacity="0" style="animation-delay:-` + fnum1(c.delay) + `s">` +1794 `<path d="` + ray + `" fill="#fff"/><circle r="` + fnum1(1.1*stoneScale) + `" fill="#fff"/></g></g>`)1795 }1796 fs.glints = g.String()1797 }1798 return out.String(), css1799}18001801func renderMetalLabelRect(metalHi, metalLo string, x, y, w, h, rx float64, salt string) string {1802 gid := "lg" + salt1803 bright := lighten(metalHi, 0.4)1804 deep := mixHex(metalLo, "#000000", 0.5)1805 st := func(off, c string) string { return `<stop offset="` + off + `" stop-color="` + c + `"/>` }1806 defs := `<linearGradient id="` + gid + `">` +1807 st("0", metalLo) + st(".22", metalHi) + st(".42", bright) + st(".62", metalHi) + st("1", metalLo) + `</linearGradient>`1808 rect := `<rect x="` + cnum(x) + `" y="` + cnum(y) + `" width="` + cnum(w) + `" height="` + cnum(h) + `" rx="` + cnum(rx) + `" fill="url(#` + gid + `)" stroke="` + deep + `" stroke-width=".8"/>`1809 const in_ = 0.91810 arc := ` a` + cnum(rx-in_) + ` ` + cnum(rx-in_) + ` 0 0 `1811 d := cnum(rx - in_)1812 edges := `<g fill="none" stroke-linecap="round">` +1813 `<path d="M` + cnum(x+in_) + ` ` + cnum(y+rx) + arc + `1 ` + d + ` -` + d + `H` + cnum(x+w-rx) + arc + `1 ` + d + ` ` + d + `" stroke="` + lighten(metalHi, 0.45) + `" stroke-width=".45" opacity=".4"/>` +1814 `<path d="M` + cnum(x+in_) + ` ` + cnum(y+h-rx) + arc + `0 ` + d + ` ` + d + `H` + cnum(x+w-rx) + arc + `0 ` + d + ` -` + d + `" stroke="` + deep + `" stroke-width=".5" opacity=".5"/></g>`1815 sid := "sh" + salt1816 defs += `<g id="` + sid + `"><circle cx=".15" cy=".2" r="1.6" fill="` + deep + `" opacity=".55"/>` +1817 `<circle r="1.3" fill="` + mixHex(metalLo, metalHi, 0.4) + `" stroke="` + deep + `" stroke-width=".25"/>` +1818 `<circle cx="-.4" cy="-.4" r=".75" fill="` + lighten(metalHi, 0.3) + `" opacity=".35"/></g>`1819 seed := 01820 for k := 0; k < len(salt); k++ {1821 seed += int(salt[k])1822 }1823 heads, slots := "", ""1824 n := 01825 for _, px := range []float64{x + 4.2, x + w - 4.2} {1826 for _, py := range []float64{y + 3.7, y + h - 3.7} {1827 heads += `<use href="#` + sid + `" x="` + cnum(px) + `" y="` + cnum(py) + `"/>`1828 ang := float64((seed*37+n*71)%180) * math.Pi / 1801829 dx, dy := 0.9*math.Cos(ang), 0.9*math.Sin(ang)1830 slots += `M` + cnum(px-dx) + ` ` + cnum(py-dy) + `l` + cnum(2*dx) + ` ` + cnum(2*dy)1831 n++1832 }1833 }1834 rivets := heads + `<path d="` + slots + `" stroke="` + deep + `" stroke-width=".45" stroke-linecap="round"/>`1835 return `<defs>` + defs + `</defs>` + rect + edges + rivets1836}18371838const (1839 plateFont = `'DIN Alternate',Bahnschrift,'Roboto Condensed',sans-serif`1840 plateSize = 8.61841 plateSpacing = 1.11842 plateMaxW = 100.01843)18441845func renderPlateText(label, fill, metalHi string) string {1846 g := `<g font-size="` + cnum(plateSize) + `" letter-spacing="` + cnum(plateSpacing) + `" text-anchor="middle" font-family="` + plateFont + `" font-weight="700"`1847 if est := float64(len(label))*(plateSize*0.52+plateSpacing); est > plateMaxW {1848 g += ` textLength="` + cnum(plateMaxW) + `" lengthAdjust="spacingAndGlyphs"`1849 }1850 x := cnum(subjX)1851 return g + `><text x="` + x + `" y="144.9" fill="` + lighten(metalHi, 0.5) + `" opacity=".45">` + label + `</text>` +1852 `<text x="` + x + `" y="144.4" fill="` + fill + `">` + label + `</text></g>`1853}18541855func renderSerialText(tidStr, color string) string {1856 return `<text x="` + fnum(subjX) + `" y="159.5" font-size="6.5" text-anchor="middle" fill="` + color + `" font-family="ui-monospace,monospace" opacity="0.85">#` + tidStr + `</text>`1857}18581859type namedColor struct{ hex, name string }18601861var stoneByChain = map[string]string{1862 "TOPAZ-1": "Topaz",1863 "SAPPHIRE-1": "Sapphire",1864 "PEARL-1": "Pearl",1865 "GNOLAND-1": "Diamond",1866}18671868var stonePalettes = map[string][]namedColor{1869 "Sapphire": {1870 {"#2a5fe0", "Blue"}, {"#ff6ea4", "Pink"}, {"#ff8501", "Orange"}, {"#ffd500", "Yellow"},1871 {"#1aa88a", "Green"}, {"#a658ff", "Violet"}, {"#e6e9f8", "White"}, {"#ff8f72", "Padparadscha"},1872 },1873 "Topaz": {1874 {"#f5edd1", "Colorless"}, {"#ffa300", "Imperial"}, {"#ffd100", "Golden"},1875 {"#c96a28", "Sherry Brown"}, {"#86cdf2", "Sky Blue"}, {"#ff96ba", "Pink"},1876 },1877 "Quartz": {1878 {"#eef2f7", "Rock Crystal"}, {"#9b59d6", "Amethyst"}, {"#f2b01e", "Citrine"},1879 {"#f5b9ca", "Rose Quartz"}, {"#7d6250", "Smoky Quartz"}, {"#a9d9a2", "Prasiolite"},1880 },1881 "Diamond": {1882 {"#f3f6fc", "Colorless"}, {"#ffc400", "Fancy Yellow"}, {"#ff4f9a", "Fancy Pink"},1883 {"#1f8fff", "Fancy Blue"}, {"#14c46e", "Fancy Green"}, {"#d9a560", "Champagne"},1884 {"#b4561a", "Cognac"}, {"#2b2d34", "Black"},1885 {"#e0243a", "Red"}, {"#ff7a1a", "Orange"}, {"#9b4fd6", "Purple"},1886 },1887 "Pearl": {1888 {"#f8f6f0", "White"}, {"#f0e4c8", "Cream"}, {"#3a3a3c", "Black"}, {"#c8cdd4", "Silver"},1889 {"#d4af6a", "Gold"}, {"#f0d4dc", "Pink"}, {"#f2c9a8", "Peach"}, {"#d8cbe0", "Lavender"},1890 },1891}18921893var colorWeights = map[string][]int{1894 "Diamond": {30, 18, 5, 4, 3, 14, 12, 8, 1, 3, 2},1895 "Quartz": {25, 25, 18, 9, 18, 5},1896}18971898const defaultColorWeight = 518991900func colorWeight(stone string, idx int) int {1901 w, ok := colorWeights[stone]1902 if !ok {1903 return 11904 }1905 if idx < len(w) {1906 return w[idx]1907 }1908 return defaultColorWeight1909}19101911func PickColorIndex(stone string, roll uint64) int {1912 n := ColorPaletteSize(stone)1913 total := 01914 for i := 0; i < n; i++ {1915 total += colorWeight(stone, i)1916 }1917 r := int(roll % uint64(total))1918 for i := 0; i < n; i++ {1919 r -= colorWeight(stone, i)1920 if r < 0 {1921 return i1922 }1923 }1924 return n - 11925}19261927func ColorProbability(stone string, idx int) float64 {1928 n := ColorPaletteSize(stone)1929 total := 01930 for i := 0; i < n; i++ {1931 total += colorWeight(stone, i)1932 }1933 return float64(colorWeight(stone, idx)) / float64(total)1934}19351936func ColorRarityCSV(stone string) string {1937 n := ColorPaletteSize(stone)1938 total := 01939 for i := 0; i < n; i++ {1940 total += colorWeight(stone, i)1941 }1942 out := make([]string, n)1943 for i := 0; i < n; i++ {1944 pm := colorWeight(stone, i) * 1000 / total1945 out[i] = colorForStone(stone, i).name + ":" + ufmt.Sprintf("%d.%d", pm/10, pm%10)1946 }1947 return strings.Join(out, ";")1948}19491950func ChainAlwaysActive(chain string) bool { return chain == "GNOLAND-1" }19511952var stoneNames = []string{"Sapphire", "Topaz", "Pearl", "Diamond", "Quartz"}19531954func registerStoneName(stone string) {1955 for _, s := range stoneNames {1956 if s == stone {1957 return1958 }1959 }1960 stoneNames = append(stoneNames, stone)1961}19621963func SetStoneForChain(chainName, stone string) {1964 stoneByChain[chainName] = stone1965 registerStoneName(stone)1966}19671968func AddStoneColor(stone, hex, name string) {1969 stonePalettes[stone] = append(stonePalettes[stone], namedColor{hex: hex, name: name})1970 registerStoneName(stone)1971}19721973func StoneNamesCSV() string {1974 return strings.Join(stoneNames, ";")1975}19761977func ColorPaletteCSV(stone string) string {1978 palette := stonePalettes[stone]1979 pairs := make([]string, len(palette))1980 for i, c := range palette {1981 pairs[i] = c.hex + ":" + c.name1982 }1983 return strings.Join(pairs, ";")1984}19851986func MetalToneHex(metalName string) string {1987 t, ok := metals[metalName]1988 if !ok {1989 return ""1990 }1991 return t.hi + ";" + t.lo1992}19931994func StoneForChain(chain string) string {1995 if s, ok := stoneByChain[chain]; ok {1996 return s1997 }1998 return "Quartz"1999}20002001func colorForStone(stone string, colorIdx int) namedColor {2002 palette := stonePalettes[stone]2003 if len(palette) == 0 {2004 palette = stonePalettes["Quartz"]2005 }2006 if colorIdx < 0 {2007 colorIdx = 02008 }2009 if colorIdx >= len(palette) {2010 colorIdx = len(palette) - 12011 }2012 return palette[colorIdx]2013}20142015func ColorPaletteSize(stone string) int {2016 if n := len(stonePalettes[stone]); n > 0 {2017 return n2018 }2019 return len(stonePalettes["Quartz"])2020}20212022func FindShapeIndex(stone, name string) (idx int, ok bool) {2023 names := ShapeNames2024 if stone == "Pearl" {2025 names = PearlShapeNames2026 }2027 for i, s := range names {2028 if s == name {2029 return i, true2030 }2031 }2032 return -1, false2033}20342035func FindColorIndex(chain, colorName string) (idx int, ok bool) {2036 stone := StoneForChain(chain)2037 palette := stonePalettes[stone]2038 if len(palette) == 0 {2039 palette = stonePalettes["Quartz"]2040 }2041 for i, c := range palette {2042 if c.name == colorName {2043 return i, true2044 }2045 }2046 return -1, false2047}20482049type metalTone struct{ hi, lo string }20502051var metals = map[string]metalTone{2052 "Gold": {"#a48b3f", "#6a5117"},2053 "Silver": {"#95989c", "#5e646c"},2054 "Bronze": {"#865b34", "#52391d"},2055 "Platinum": {"#c3c6c8", "#87909a"},2056 "Obsidian": {"#3a3d47", "#0d0e12"},2057}20582059var MetalNames = []string{"Gold", "Silver", "Bronze", "Platinum", "Obsidian"}20602061type metalLabelTone struct{ bg, text string }20622063var metalLabel = map[string]metalLabelTone{2064 "Gold": {"#f4d060", "#04121a"},2065 "Silver": {"#dfe4ea", "#04121a"},2066 "Bronze": {"#c9884f", "#04121a"},2067 "Platinum": {"#eef2f5", "#04121a"},2068 "Obsidian": {"#5a5f6b", "#e8e9f0"},2069}20702071func RenderTokenThumbSVG(chain, metal string, shapeIdx, colorIdx int, active bool, tidStr string) string {2072 svg, _, _, _ := renderToken(chain, metal, shapeIdx, colorIdx, active, tidStr, true)2073 return svg2074}20752076func RenderTokenSVG(chain, metal string, shapeIdx, colorIdx int, active bool, tidStr string) (svg string, stone string, colorHex string, colorName string) {2077 return renderToken(chain, metal, shapeIdx, colorIdx, active, tidStr, false)2078}20792080func renderToken(chain, metal string, shapeIdx, colorIdx int, active bool, tidStr string, thumb bool) (svg string, stone string, colorHex string, colorName string) {2081 var cascadeCSS string2082 stone = StoneForChain(chain)2083 if ChainAlwaysActive(chain) {2084 active = true2085 }2086 nc := colorForStone(stone, colorIdx)2087 colorHex, colorName = nc.hex, nc.name2088 mt := metals[metal]20892090 ringEls := renderRing(mt.hi, mt.lo, tidStr)2091 var pieceMarkup string2092 if stone == "Pearl" {2093 pearl := renderPearlShape(shapeIdx, colorHex, mt.hi, tidStr)2094 overlay, css := applyCascade(pearl, tidStr, nil)2095 pieceMarkup, cascadeCSS = pearl+overlay, css2096 } else {2097 fs := &facetStyle{prof: fireProfileFor(stone, colorName), zone: newPRNG("zone" + tidStr), stone: stone, thumb: thumb}2098 under, facets, over := renderSolid3D(shapeIdx, colorHex, tidStr, fs)2099 overlay, css := applyCascade(facets, tidStr, fs)2100 cascadeCSS = css2101 pieceMarkup = under + `<g fill-opacity=".9">` + facets + `</g>` + overlay + over + fs.glints2102 }2103 floatingPiece := applyMotion(pieceMarkup, tidStr)2104 status := renderStatusBadge(active)2105 serial := renderSerialText(tidStr, mt.hi)2106 labelText := metalLabel[metal].text2107 labelRect := renderMetalLabelRect(mt.hi, mt.lo, 24, 134, 122, 15, 3, tidStr)21082109 inner := `<rect width="` + fnum(canvasW) + `" height="` + fnum(canvasH) + `" fill="#0a0b10"/>` +2110 renderStars(tidStr) + ringEls + floatingPiece + status + serial + labelRect +2111 renderPlateText(chain+" CHAIN", labelText, mt.hi)21122113 svg = `<svg xmlns="http://www.w3.org/2000/svg" width="` + fnum(canvasW) + `" height="` + fnum(canvasH) + `" viewBox="0 0 ` + fnum(canvasW) + ` ` + fnum(canvasH) + `">` + styleBlock() +2114 `<style>` + cascadeCSS + `</style>` + inner + `</svg>`2115 return svg, stone, colorHex, colorName2116}21172118func styleBlock() string {2119 const easeOut = "cubic-bezier(0,0,.3,1)"2120 return `<style>.gt-v,.gt-h,.gt-r,.gt-s{will-change:transform}` +21212122 `.gt-v{animation:gt-v var(--v-dur) ease-in-out var(--v-delay) infinite}` +2123 `.gt-h{animation:gt-h var(--h-dur) ease-in-out var(--h-delay) infinite}` +2124 `.gt-r{animation:gt-r var(--r-dur) ease-in-out var(--r-delay) infinite}` +2125 `.gt-s{animation:gt-s var(--s-dur) ease-in-out var(--s-delay) infinite}` +2126 `.gt-glow{animation:gt-glow var(--glow-dur) ease-in-out var(--glow-delay) infinite}` +2127 `.gt-la{animation:gt-la var(--r-dur) ease-in-out var(--r-delay) infinite}` +2128 `.gt-lb{animation:gt-lb var(--r-dur) ease-in-out var(--r-delay) infinite}` +2129 `@keyframes gt-la{0%,50%,100%{opacity:0}25%{opacity:1}}` +2130 `@keyframes gt-lb{0%,50%,100%{opacity:0}75%{opacity:1}}` +2131 `.gt-badge-pulse{animation:gt-bp 1.8s ` + easeOut + ` infinite}` +2132 `@keyframes gt-v{0%,100%{transform:translateY(2.5px)}50%{transform:translateY(-2.5px)}}` +2133 `@keyframes gt-h{50%{transform:translateX(1.8px)}}` +2134 `@keyframes gt-r{25%{transform:rotate(calc(-1*var(--rot-deg)))}75%{transform:rotate(var(--rot-deg))}}` +2135 `@keyframes gt-s{50%{transform:scale(` + fnum(motionScale) + `)}}` +2136 `.gt-tw{animation:gt-tw var(--d) ease-in-out var(--dl) infinite}@keyframes gt-tw{0%,84%,100%{opacity:.4}90%{opacity:1}94%{opacity:.2}}` +2137 `@keyframes gt-glow{50%{opacity:1}}` +2138 `@keyframes gt-bp{from{transform:scale(1);opacity:.8}to{transform:scale(2.25);opacity:0}}` +2139 `</style>`2140}2141Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.