1package art3d23import (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}284285type fireProfile struct {286 sat, hit float64287 pinpoints int288 shadow string289 bodyMix float64290 neutral string291}292293func fireProfileFor(stone, colorName string) fireProfile {294 if stone == "Diamond" {295 switch colorName {296 case "Colorless":297 return fireProfile{1.0, 0.85, 5, "#0d1222", 0.12, "#f4f7fc"}298 case "Black":299 return fireProfile{0.15, 0.35, 1, "#000000", 0, ""}300 }301 return fireProfile{0.95, 0.7, 4, "#0b0d18", 0, ""}302 }303 if stone == "Quartz" {304 if colorName == "Rock Crystal" {305 return fireProfile{0.4, 0.6, 3, "#141a2c", 0.25, "#e8edf4"}306 }307 return fireProfile{0.35, 0.5, 3, "#000000", 0, ""}308 }309 if colorName == "Colorless" {310 return fireProfile{1.0, 0.7, 4, "#141a2c", 0.55, "#dfe4ee"}311 }312 if colorName == "White" {313 return fireProfile{0.85, 0.6, 3, "#7d86a0", 0, ""}314 }315 if colorName == "Crystal" {316 return fireProfile{1.0, 0.78, 4, "#141a2c", 0.3, "#e4e9f2"}317 }318 if stone == "Topaz" {319 return fireProfile{0.5, 0.5, 3, "#000000", 0, ""}320 }321 return fireProfile{0.7, 0.5, 3, "#000000", 0, ""}322}323324const fireZoneDeg = 7.0325326type rgbI struct{ r, g, b int }327328func toRGBI(hex string) rgbI { r, g, b := hexToRGB(hex); return rgbI{r, g, b} }329330func (c rgbI) hex() string { return rgbToHex(c.r, c.g, c.b) }331332func mixI(a, b rgbI, amt float64) rgbI {333 return rgbI{334 clampByte(float64(a.r) + float64(b.r-a.r)*amt),335 clampByte(float64(a.g) + float64(b.g-a.g)*amt),336 clampByte(float64(a.b) + float64(b.b-a.b)*amt),337 }338}339340func hueRotateI(x rgbI, deg float64) rgbI {341 c, sn := math.Cos(deg*math.Pi/180), math.Sin(deg*math.Pi/180)342 fr, fg, fb := float64(x.r), float64(x.g), float64(x.b)343 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)*fb344 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)*fb345 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)*fb346 return rgbI{clampByte(nr), clampByte(ng), clampByte(nb)}347}348349func hex3I(x rgbI) string {350 d := "0123456789abcdef"351 q := func(v int) byte { return d[(v+8)*15/255%16] }352 return "#" + string([]byte{q(x.r), q(x.g), q(x.b)})353}354355var whiteI = rgbI{255, 255, 255}356357func darken(hex, shadow string, amt float64) string {358 if amt > 0.95 {359 amt = 0.95360 }361 return mixHex(hex, shadow, amt)362}363364const prismFlash = true365366func flashRamp(ext, sat float64) gramp {367 r := gramp{x1: subjX - ext, y1: subjY - ext, x2: subjX + ext, y2: subjY + ext}368 if !prismFlash {369 r.stops = []gstop{{0, "#ffffff", 1}, {100, "#ffffff", 0.25}}370 return r371 }372 r.stops = []gstop{373 {0, "#ffffff", 1}, {14, "#ff4040", 0.95}, {28, "#ff9a1f", 0.95}, {42, "#ffe81f", 0.9},374 {56, "#35ff7a", 0.85}, {70, "#22ccff", 0.8}, {84, "#5566ff", 0.75}, {100, "#c04dff", 0.7},375 }376 for i := range r.stops {377 r.stops[i].color = mixHex(r.stops[i].color, "#ffffff", 1-sat)378 }379 return r380}381382type v3 struct{ x, y, z float64 }383384func vsub(a, b v3) v3 { return v3{a.x - b.x, a.y - b.y, a.z - b.z} }385func vdot(a, b v3) float64 { return a.x*b.x + a.y*b.y + a.z*b.z }386func 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} }387func vscale(a v3, k float64) v3 { return v3{a.x * k, a.y * k, a.z * k} }388func vnorm(a v3) v3 {389 l := math.Sqrt(vdot(a, a))390 if l == 0 {391 return a392 }393 return v3{a.x / l, a.y / l, a.z / l}394}395396func sgnPow(v, e float64) float64 {397 if v < 0 {398 return -math.Pow(-v, e)399 }400 return math.Pow(v, e)401}402403type outlineFn func(t float64) (float64, float64)404405func circleOutline(t float64) (float64, float64) {406 a := 2 * math.Pi * t407 return math.Cos(a), math.Sin(a)408}409410func superOutline(p float64) outlineFn {411 return func(t float64) (float64, float64) {412 a := 2 * math.Pi * t413 return sgnPow(math.Cos(a), 2/p), sgnPow(math.Sin(a), 2/p)414 }415}416417func polyOutline(vs []pt) outlineFn {418 return func(t float64) (float64, float64) {419 n := len(vs)420 f := t * float64(n)421 k := int(math.Floor(f))422 fr := f - float64(k)423 a := vs[((k%n)+n)%n]424 b := vs[(((k+1)%n)+n)%n]425 return a.x + (b.x-a.x)*fr, a.y + (b.y-a.y)*fr426 }427}428429func octagon(hw, hh, c float64) []pt {430 return []pt{431 {hw - c, hh}, {-hw + c, hh}, {-hw, hh - c}, {-hw, -hh + c},432 {-hw + c, -hh}, {hw - c, -hh}, {hw, -hh + c}, {hw, hh - c},433 }434}435436func pearOutline(t float64) (float64, float64) {437 a := 2 * math.Pi * t438 c := math.Cos(a)439 return c*0.92 + 0.08, 0.7 * math.Sin(a) * (1 - 0.46*c)440}441442func marquiseOutline(t float64) (float64, float64) {443 a := 2 * math.Pi * t444 return math.Cos(a), 0.52 * sgnPow(math.Sin(a), 1.35)445}446447func heartOutline(t float64) (float64, float64) {448 a := 2 * math.Pi * t449 sn := math.Sin(a)450 y := (13*math.Cos(a) - 4*math.Cos(2*a) - 1.4*math.Cos(3*a) - 0.5*math.Cos(4*a)) / 17451 return 0.88*sn*sn*sn + 0.12*sn, (y + 0.1576) / 0.7894452}453454func trillionOutline(t float64) (float64, float64) {455 vs := []pt{{0, 1}, {-0.866, -0.5}, {0.866, -0.5}}456 f := t * 3457 k := int(math.Floor(f))458 fr := f - float64(k)459 a, b := vs[((k%3)+3)%3], vs[(((k+1)%3)+3)%3]460 bulge := 1 + 0.16*math.Sin(math.Pi*fr)461 return (a.x + (b.x-a.x)*fr) * bulge, (a.y + (b.y-a.y)*fr) * bulge462}463464type cutSpec struct {465 n int466 brilliant bool467 outline outlineFn468 rx, ry float64469}470471var ShapeNames = []string{472 "Round Brilliant", "Oval", "Cushion", "Emerald Cut", "Asscher",473 "Radiant", "Princess Cut", "Pear", "Marquise", "Trillion",474 "Heart", "Rose Cut", "Raw Crystal",475}476477const (478 shapeHeart = 10479 shapeRose = 11480 shapeRaw = 12481)482483const stoneScale = 0.78484485func cutFor(shapeIdx int) cutSpec {486 c := cutForUnscaled(shapeIdx)487 c.rx, c.ry = c.rx*stoneScale, c.ry*stoneScale488 return c489}490491func cutForUnscaled(shapeIdx int) cutSpec {492 switch shapeIdx {493 case 1:494 return cutSpec{8, true, circleOutline, 30, 21.5}495 case 2:496 return cutSpec{8, true, superOutline(3.4), 27, 24}497 case 3:498 return cutSpec{8, false, polyOutline(octagon(1, 1, 0.3)), 30, 21}499 case 4:500 return cutSpec{8, false, polyOutline(octagon(1, 1, 0.36)), 25, 25}501 case 5:502 return cutSpec{8, true, polyOutline(octagon(1, 1, 0.3)), 29, 22.5}503 case 6:504 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}505 case 7:506 return cutSpec{8, true, pearOutline, 31, 27}507 case 8:508 return cutSpec{8, true, marquiseOutline, 33, 30}509 case 9:510 return cutSpec{9, true, trillionOutline, 27, 27}511 case shapeHeart:512 return cutSpec{10, true, heartOutline, 29, 28}513 case shapeRose:514 return cutSpec{6, true, circleOutline, 27, 27}515 case shapeRaw:516 return cutSpec{0, false, circleOutline, 27, 27} // sized per stone in buildRaw517 }518 return cutSpec{8, true, circleOutline, 27, 27}519}520521func CutsForStone(stone string) []int {522 switch stone {523 case "Diamond":524 return []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, shapeHeart, shapeRose, shapeRaw}525 case "Sapphire":526 return []int{1, 2, 0, 3, 7, shapeHeart, 8, 9, 6, shapeRose, shapeRaw}527 case "Topaz":528 return []int{3, 1, 7, 2, 8, 0, 9, shapeHeart, 6, shapeRaw}529 }530 return []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, shapeHeart, shapeRose, shapeRaw}531}532533const (534 kCrown = iota535 kTable536 kPav537)538539type face struct {540 p []v3541 kind int542}543544func ringPts(c cutSpec, n int, scale, z, off float64) []v3 {545 out := make([]v3, n)546 for k := 0; k < n; k++ {547 x, y := c.outline((float64(k) + off) / float64(n))548 out[k] = v3{x * c.rx * scale, y * c.ry * scale, z}549 }550 return out551}552553func buildCut(c cutSpec) (faces []face, girdle []v3) {554 base := c.rx555 if c.ry < base {556 base = c.ry557 }558 hc, hp := 0.34*base, 0.9*base559 n := c.n560 if c.brilliant {561 T := ringPts(c, n, 0.56, hc, 0)562 B := ringPts(c, n, 0.8, hc*0.46, 0.5)563 G := ringPts(c, 2*n, 1, 0, 0)564 culet := v3{0, 0, -hp}565 faces = append(faces, face{T, kTable})566 for k := 0; k < n; k++ {567 k1 := (k + 1) % n568 faces = append(faces, face{[]v3{T[k], B[k], T[k1]}, kCrown})569 faces = append(faces, face{[]v3{T[k1], B[k], G[(2*k+2)%(2*n)], B[k1]}, kCrown})570 faces = append(faces, face{[]v3{B[k], G[2*k], G[2*k+1]}, kCrown})571 faces = append(faces, face{[]v3{B[k], G[2*k+1], G[(2*k+2)%(2*n)]}, kCrown})572 faces = append(faces, face{[]v3{G[2*k], G[2*k+1], G[(2*k+2)%(2*n)], culet}, kPav})573 }574 return faces, G575 }576 T := ringPts(c, n, 0.54, hc, 0)577 S := ringPts(c, n, 0.78, hc*0.5, 0)578 G := ringPts(c, n, 1, 0, 0)579 P1 := ringPts(c, n, 0.7, -hp*0.42, 0)580 P2 := ringPts(c, n, 0.38, -hp*0.78, 0)581 K := ringPts(c, n, 0.1, -hp, 0)582 faces = append(faces, face{T, kTable})583 rows := [][2][]v3{{T, S}, {S, G}}584 for _, r := range rows {585 for k := 0; k < n; k++ {586 k1 := (k + 1) % n587 faces = append(faces, face{[]v3{r[0][k], r[0][k1], r[1][k1], r[1][k]}, kCrown})588 }589 }590 prow := [][2][]v3{{G, P1}, {P1, P2}, {P2, K}}591 for _, r := range prow {592 for k := 0; k < n; k++ {593 k1 := (k + 1) % n594 faces = append(faces, face{[]v3{r[0][k], r[0][k1], r[1][k1], r[1][k]}, kPav})595 }596 }597 return faces, G598}599600func buildRose(c cutSpec) (faces []face, girdle []v3) {601 n := c.n602 base := math.Min(c.rx, c.ry)603 h := 0.62 * base604 apex := v3{0, 0, h}605 R := ringPts(c, n, 0.55, h*0.62, 0.5)606 G := ringPts(c, 2*n, 1, 0, 0)607 for k := 0; k < n; k++ {608 k1 := (k + 1) % n609 faces = append(faces, face{[]v3{apex, R[k], R[k1]}, kCrown})610 faces = append(faces, face{[]v3{G[2*k], G[2*k+1], R[k]}, kCrown})611 faces = append(faces, face{[]v3{G[2*k+1], G[(2*k+2)%(2*n)], R[k]}, kCrown})612 faces = append(faces, face{[]v3{R[k], G[(2*k+2)%(2*n)], R[k1]}, kCrown})613 }614 back := make([]v3, len(G))615 for i := range G {616 back[len(G)-1-i] = G[i]617 }618 faces = append(faces, face{back, kPav})619 return faces, G620}621622func ringX(n int, x, ry, rz, phase float64) []v3 {623 out := make([]v3, n)624 for k := 0; k < n; k++ {625 a := 2*math.Pi*(float64(k)+phase)/float64(n) + math.Pi/2626 out[k] = v3{x, ry * math.Cos(a), rz * math.Sin(a)}627 }628 return out629}630631func bandX(a, b []v3) []face {632 var fs []face633 for k := range a {634 k1 := (k + 1) % len(a)635 fs = append(fs, face{[]v3{a[k], a[k1], b[k1], b[k]}, kCrown})636 }637 return fs638}639640func capX(r []v3) face { return face{r, kCrown} }641642func turnFaces(fs []face, ax, az float64) []face {643 cx, sx, cz, sz := math.Cos(ax), math.Sin(ax), math.Cos(az), math.Sin(az)644 for i := range fs {645 fs[i].kind = kPav646 for j, p := range fs[i].p {647 y, z := p.y*cx-p.z*sx, p.y*sx+p.z*cx648 fs[i].p[j] = v3{p.x*cz - y*sz, p.x*sz + y*cz, z}649 }650 }651 return fs652}653654func buildRaw(stone string) (faces []face, girdle []v3) {655 deg := math.Pi / 180656 switch stone {657 case "Diamond":658 a, b, h := 25.0*stoneScale, 22.0*stoneScale, 23.0*stoneScale659 v := []v3{{a, 0, 0}, {0, b, 0}, {-a, 0, 0}, {0, -b, 0}}660 top, bot := v3{0, 0, h}, v3{0, 0, -h}661 for k := 0; k < 4; k++ {662 k1 := (k + 1) % 4663 faces = append(faces, face{[]v3{top, v[k], v[k1]}, kCrown})664 faces = append(faces, face{[]v3{bot, v[k1], v[k]}, kCrown})665 }666 return turnFaces(faces, -38*deg, 28*deg), nil667 case "Sapphire":668 L, R := 26.0*stoneScale, 16.0*stoneScale669 r0 := ringX(6, -L, R*0.42, R*0.42, 0)670 r1 := ringX(6, -L*0.3, R, R*0.94, 0)671 r2 := ringX(6, L*0.3, R, R*0.94, 0)672 r3 := ringX(6, L, R*0.42, R*0.42, 0)673 faces = append(faces, bandX(r0, r1)...)674 faces = append(faces, bandX(r1, r2)...)675 faces = append(faces, bandX(r2, r3)...)676 faces = append(faces, capX(r3))677 rev := make([]v3, 6)678 for i := range r0 {679 rev[5-i] = r0[i]680 }681 faces = append(faces, capX(rev))682 return turnFaces(faces, -30*deg, -14*deg), nil683 }684 if stone == "Quartz" {685 L, R := 27.0*stoneScale, 12.5*stoneScale686 r0 := ringX(6, -L, R*1.04, R, 0)687 r1 := ringX(6, L*0.4, R, R*0.96, 0)688 point := v3{L, 0, 0}689 faces = append(faces, bandX(r0, r1)...)690 for k := 0; k < 6; k++ {691 faces = append(faces, face{[]v3{r1[k], r1[(k+1)%6], point}, kCrown})692 }693 rev := make([]v3, 6)694 for i := range r0 {695 rev[5-i] = r0[i]696 }697 faces = append(faces, capX(rev))698 return turnFaces(faces, -32*deg, 16*deg), nil699 }700 L, ry, rz := 26.0*stoneScale, 16.0*stoneScale, 13.0*stoneScale701 r0 := ringX(8, -L, ry, rz, 0.5)702 r1 := ringX(8, L*0.5, ry, rz, 0.5)703 tip := ringX(8, L*0.82, ry*0.45, rz*0.45, 0.5)704 point := v3{L, 0, 0}705 faces = append(faces, bandX(r0, r1)...)706 faces = append(faces, bandX(r1, tip)...)707 for k := 0; k < 8; k++ {708 faces = append(faces, face{[]v3{tip[k], tip[(k+1)%8], point}, kCrown})709 }710 rev := make([]v3, 8)711 for i := range r0 {712 rev[7-i] = r0[i]713 }714 faces = append(faces, capX(rev))715 return turnFaces(faces, -30*deg, 12*deg), nil716}717718const viewTilt = 61.0719720const (721 pieceCY = subjY + 1.5722)723724func n1(v float64) string {725 neg := v < 0726 if neg {727 v = -v728 }729 t := int64(v*10 + 0.5)730 w, f := t/10, t%10731 s := ""732 if f == 0 {733 s = itoa(w)734 } else if w == 0 {735 s = "." + string(byte('0'+f))736 } else {737 s = itoa(w) + "." + string(byte('0'+f))738 }739 if neg && t != 0 {740 return "-" + s741 }742 return s743}744745func relPath(ps []pt) string {746 s := "M" + n1(ps[0].x) + " " + n1(ps[0].y) + "l"747 px, py := math.Round(ps[0].x*10)/10, math.Round(ps[0].y*10)/10748 for i := 1; i < len(ps); i++ {749 qx, qy := math.Round(ps[i].x*10)/10, math.Round(ps[i].y*10)/10750 dx, dy := n1(qx-px), n1(qy-py)751 if i > 1 && dx[0] != '-' {752 s += " "753 }754 s += dx755 if dy[0] != '-' {756 s += " "757 }758 s += dy759 px, py = qx, qy760 }761 return s + "z"762}763764func hex3(h string) string {765 r, g, b := hexToRGB(h)766 d := "0123456789abcdef"767 q := func(v int) byte { return d[(v+8)*15/255%16] }768 return "#" + string([]byte{q(r), q(g), q(b)})769}770771func convexHull(ps []pt) []pt {772 n := len(ps)773 idx := make([]int, n)774 for i := range idx {775 idx[i] = i776 }777 less := func(a, b int) bool {778 return ps[a].x < ps[b].x || (ps[a].x == ps[b].x && ps[a].y < ps[b].y)779 }780 tmp := make([]int, n)781 for w := 1; w < n; w *= 2 {782 for lo := 0; lo < n; lo += 2 * w {783 mid, hi := lo+w, lo+2*w784 if mid > n {785 mid = n786 }787 if hi > n {788 hi = n789 }790 i, j, k := lo, mid, lo791 for i < mid && j < hi {792 if less(idx[j], idx[i]) {793 tmp[k] = idx[j]794 j++795 } else {796 tmp[k] = idx[i]797 i++798 }799 k++800 }801 for i < mid {802 tmp[k] = idx[i]803 i++804 k++805 }806 for j < hi {807 tmp[k] = idx[j]808 j++809 k++810 }811 }812 idx, tmp = tmp, idx813 }814 cr := func(o, a, b pt) float64 { return (a.x-o.x)*(b.y-o.y) - (a.y-o.y)*(b.x-o.x) }815 var h []pt816 for pass := 0; pass < 2; pass++ {817 start := len(h)818 for i := 0; i < n; i++ {819 p := ps[idx[i]]820 if pass == 1 {821 p = ps[idx[n-1-i]]822 }823 for len(h) >= start+2 && cr(h[len(h)-2], h[len(h)-1], p) <= 0 {824 h = h[:len(h)-1]825 }826 h = append(h, p)827 }828 h = h[:len(h)-1]829 }830 return h831}832833func polyArea(ps []pt) float64 {834 a := 0.0835 for i := range ps {836 j := (i + 1) % len(ps)837 a += ps[i].x*ps[j].y - ps[j].x*ps[i].y838 }839 if a < 0 {840 return -a / 2841 }842 return a / 2843}844845func clipHalf(ps []pt, ox, oy, nx, ny float64) []pt {846 if len(ps) == 0 {847 return ps848 }849 d := func(q pt) float64 { return (q.x-ox)*nx + (q.y-oy)*ny }850 var out []pt851 for i := range ps {852 a, b := ps[i], ps[(i+1)%len(ps)]853 da, db := d(a), d(b)854 if da <= 0 {855 out = append(out, a)856 }857 if (da < 0 && db > 0) || (da > 0 && db < 0) {858 t := da / (da - db)859 out = append(out, pt{a.x + (b.x-a.x)*t, a.y + (b.y-a.y)*t})860 }861 }862 return out863}864865func clipToConvex(ps, win []pt) []pt {866 for i := range win {867 a, b := win[i], win[(i+1)%len(win)]868 nx, ny := b.y-a.y, a.x-b.x869 mid := pt{(a.x + b.x) / 2, (a.y + b.y) / 2}870 cx, cy := centroid(win)871 if (mid.x-cx)*nx+(mid.y-cy)*ny < 0 {872 nx, ny = -nx, -ny873 }874 ps = clipHalf(ps, a.x, a.y, nx, ny)875 if len(ps) < 3 {876 return nil877 }878 }879 return ps880}881882func splitPoly(ps []pt, ang float64) ([]pt, []pt) {883 cx, cy := centroid(ps)884 nx, ny := math.Cos(ang), math.Sin(ang)885 return clipHalf(ps, cx, cy, nx, ny), clipHalf(ps, cx, cy, -nx, -ny)886}887888func renderSolid3D(shapeIdx int, base, salt string, fs *facetStyle) (under, facets, over string) {889 c := cutFor(shapeIdx)890 var faces []face891 var girdle []v3892 switch shapeIdx {893 case shapeRose:894 faces, girdle = buildRose(c)895 case shapeRaw:896 faces, girdle = buildRaw(fs.stone)897 default:898 faces, girdle = buildCut(c)899 }900 raw := shapeIdx == shapeRaw901902 sh := fs.prof.shadow903 if fs.prof.neutral != "" {904 base = fs.prof.neutral905 }906 base = mixHex(base, sh, fs.prof.bodyMix)907 deep := darken(base, sh, 0.9)908 bright := lighten(base, 0.5)909 fs.flash = flashRamp(28, fs.prof.sat)910 deepI, baseI, brightI := toRGBI(deep), toRGBI(base), toRGBI(bright)911 toneI := func(lum float64) rgbI {912 if lum < 0 {913 lum = 0914 }915 if lum > 1 {916 lum = 1917 }918 if lum < 0.5 {919 return mixI(deepI, baseI, lum*2)920 }921 return mixI(baseI, brightI, (lum-0.5)*2)922 }923 tone := func(lum float64) string { return toneI(lum).hex() }924925 yaw := fs.zone.floatIn(-13, 13) * math.Pi / 180926 th := viewTilt * math.Pi / 180927 cy, sy := math.Cos(yaw), math.Sin(yaw)928 ct, stt := math.Cos(th), math.Sin(th)929 view := func(p v3) v3 {930 x1, y1 := p.x*cy-p.y*sy, p.x*sy+p.y*cy931 return v3{x1, y1*ct + p.z*stt, p.z*ct - y1*stt}932 }933 ex0, ey0, ex1, ey1 := 1e9, 1e9, -1e9, -1e9934 for _, f := range faces {935 for _, p := range f.p {936 q := view(p)937 ex0, ey0 = math.Min(ex0, q.x), math.Min(ey0, q.y)938 ex1, ey1 = math.Max(ex1, q.x), math.Max(ey1, q.y)939 }940 }941 ox, oy := -(ex0+ex1)/2, -(ey0+ey1)/2942 scr := func(p v3) pt { return pt{subjX + p.x + ox, pieceCY - (p.y + oy)} }943 light := vnorm(v3{-0.5, 0.62, 0.6})944 inside := v3{0, 0, -0.25 * c.ry}945 if raw {946 inside = v3{}947 } else if shapeIdx == shapeRose {948 inside = v3{0, 0, 0.15 * c.ry}949 }950951 type drawn struct {952 pts []pt953 depth float64954 fill string955 kind int956 lum float64957 nv v3958 jit, drift, thick float64959 ext bool960 }961 shadeFront := func(kind int, nv, L v3, jit, drift, thick float64, ext bool) (string, float64) {962 H := vnorm(v3{L.x, L.y, L.z + 1})963 lam := math.Max(0, vdot(nv, L))964 lum := 0.03 + 0.7*math.Pow(lam, 2.1) + jit965 if ext {966 lum *= 0.3967 }968 if kind == kTable {969 lum = 0.36 + 0.26*lam970 }971 if kind == kPav {972 lum = 0.22 + 0.5*math.Max(0, vdot(vscale(nv, -1), L)) + jit973 }974 if raw {975 lum = 0.3 + 0.42*math.Pow(lam, 1.4) + 0.22*math.Max(0, vdot(vscale(nv, -1), L)) + jit*0.6976 }977 col := mixI(toneI(lum), deepI, 0.34*thick*thick)978 if sp := math.Pow(math.Max(0, vdot(nv, H)), 28); sp > 0.3 {979 col = mixI(col, whiteI, sp*0.85)980 }981 if lum > 0.5 && kind != kTable {982 amt := math.Min(1, (lum-0.5)/0.4) * dispersionMix * fs.prof.sat983 t := math.Atan2(nv.y, nv.x)/math.Pi + drift/40984 col = mixI(col, spectrumAtI(t), amt)985 }986 return hex3I(hueRotateI(col, drift)), lum987 }988 var back, front, pav []drawn989 var all []pt990 for _, f := range faces {991 vp := make([]v3, len(f.p))992 ctr := v3{}993 for i, p := range f.p {994 vp[i] = view(p)995 ctr = v3{ctr.x + p.x, ctr.y + p.y, ctr.z + p.z}996 }997 ctr = vscale(ctr, 1/float64(len(f.p)))998 nm := v3{}999 for i := range f.p {1000 a, b := f.p[i], f.p[(i+1)%len(f.p)]1001 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)}1002 }1003 nm = vnorm(nm)1004 if vdot(nm, vsub(ctr, inside)) < 0 {1005 nm = vscale(nm, -1)1006 }1007 nv := vnorm(view(nm))1008 d := drawn{kind: f.kind}1009 for _, p := range vp {1010 d.pts = append(d.pts, scr(p))1011 d.depth += p.z1012 all = append(all, scr(p))1013 }1014 d.depth /= float64(len(vp))1015 rN := math.Sqrt((ctr.x/c.rx)*(ctr.x/c.rx) + (ctr.y/c.ry)*(ctr.y/c.ry))1016 if rN > 1 {1017 rN = 11018 }1019 thick := 1 - rN1020 jit := fs.zone.floatIn(-0.13, 0.13)1021 if nv.z > 0.02 {1022 ext := f.kind == kCrown && fs.zone.floatIn(0, 1) < 0.281023 drift := fs.zone.floatIn(-fireZoneDeg, fireZoneDeg)1024 d.nv, d.jit, d.drift, d.thick, d.ext = nv, jit, drift, thick, ext1025 d.fill, d.lum = shadeFront(f.kind, nv, light, jit, drift, thick, ext)1026 front = append(front, d)1027 if f.kind == kPav {1028 pav = append(pav, d)1029 }1030 } else if f.kind == kPav {1031 inner := vscale(nv, -1)1032 lum := 0.2 + 0.6*math.Max(0, vdot(inner, light)) + jit1033 d.fill = hex3(mixHex(tone(lum), deep, 0.34*thick*thick))1034 d.lum = lum1035 back = append(back, d)1036 pav = append(pav, d)1037 }1038 }1039 sortDrawn := func(ds []drawn) {1040 for i := 1; i < len(ds); i++ {1041 for j := i; j > 0 && ds[j].depth < ds[j-1].depth; j-- {1042 ds[j], ds[j-1] = ds[j-1], ds[j]1043 }1044 }1045 }1046 sortDrawn(back)1047 sortDrawn(front)10481049 hull := convexHull(all)1050 if shapeIdx == shapeHeart {1051 hull = nil1052 for _, g := range girdle {1053 hull = append(hull, scr(view(g)))1054 }1055 }1056 var underB, facetsB strings.Builder1057 underB.WriteString(`<defs><radialGradient id="bd` + salt + `" cx=".4" cy=".35" r=".75">` +1058 `<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>` +1059 `<path d="` + relPath(hull) + `" fill="url(#bd` + salt + `)" opacity=".7"/><g fill-opacity=".6">`)1060 for _, d := range back {1061 underB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `"/>`)1062 }1063 underB.WriteString(`</g>`)10641065 nCrown := 01066 for _, d := range front {1067 if d.kind == kCrown {1068 nCrown++1069 }1070 }1071 candP := 1.01072 if nCrown > fireMaxFacets {1073 candP = float64(fireMaxFacets) / float64(nCrown) * 1.251074 }1075 nCand := 01076 nSplit := 01077 if raw || fs.thumb {1078 candP = 01079 }1080 for _, d := range front {1081 if d.kind == kTable && shapeIdx == shapeHeart {1082 facetsB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `" fill-opacity=".46"/>`)1083 continue1084 }1085 if d.kind == kTable {1086 facetsB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `" fill-opacity=".46"/>`)1087 win := convexHull(d.pts)1088 tx, ty := centroid(win)1089 if len(pav) > 0 && !fs.thumb {1090 bx0, by0, bx1, by1 := 1e9, 1e9, -1e9, -1e91091 for _, b := range pav {1092 for _, q := range b.pts {1093 bx0, by0 = math.Min(bx0, q.x), math.Min(by0, q.y)1094 bx1, by1 = math.Max(bx1, q.x), math.Max(by1, q.y)1095 }1096 }1097 wx0, wy0, wx1, wy1 := 1e9, 1e9, -1e9, -1e91098 for _, q := range win {1099 wx0, wy0 = math.Min(wx0, q.x), math.Min(wy0, q.y)1100 wx1, wy1 = math.Max(wx1, q.x), math.Max(wy1, q.y)1101 }1102 k := 0.92 * math.Min((wx1-wx0)/math.Max(1, bx1-bx0), (wy1-wy0)/math.Max(1, by1-by0))1103 bx, by := (bx0+bx1)/2, (by0+by1)/21104 var mosB strings.Builder1105 nRefl := 01106 for _, b := range pav {1107 if nRefl >= 8 {1108 break1109 }1110 mp := make([]pt, len(b.pts))1111 for i, q := range b.pts {1112 mp[i] = pt{tx + (q.x-bx)*k, ty - (q.y-by)*k}1113 }1114 cl := clipToConvex(mp, win)1115 if len(cl) < 3 || polyArea(cl) < 4 {1116 continue1117 }1118 nRefl++1119 mosB.WriteString(`<path d="` + relPath(cl) + `" fill="` + hex3(tone(b.lum*0.95)) + `"/>`)1120 }1121 if mosB.Len() > 0 {1122 facetsB.WriteString(`<g fill-opacity=".55">` + mosB.String() + `</g>`)1123 }1124 }1125 continue1126 }1127 if d.kind == kCrown && nCand < fireMaxFacets && fs.zone.floatIn(0, 1) < candP {1128 nCand++1129 cx, cyy := centroid(d.pts)1130 fs.ringCent = append(fs.ringCent, pt{cx, cyy})1131 facetsB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `" class="f"/>`)1132 continue1133 }1134 if !raw && nSplit < 6 && polyArea(d.pts) > 26 && fs.zone.floatIn(0, 1) < 0.5 {1135 a, b := splitPoly(d.pts, fs.zone.floatIn(0, math.Pi))1136 if len(a) >= 3 && len(b) >= 3 {1137 nSplit++1138 lo := hex3(mixHex(tone(math.Max(0, d.lum*0.6-0.04)), deep, 0.18))1139 hi := hex3(tone(math.Min(1, d.lum*1.35+0.06)))1140 facetsB.WriteString(`<path d="` + relPath(a) + `" fill="` + hi + `"/><path d="` + relPath(b) + `" fill="` + lo + `"/>`)1141 continue1142 }1143 }1144 facetsB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `"/>`)1145 }11461147 _, _, _, _, rotDeg := motionPhases(salt)1148 type cand struct {1149 i int1150 fill string1151 delta float641152 }1153 for si, sgn := range []float64{1, -1} {1154 if fs.thumb {1155 break1156 }1157 a := -sgn * rotDeg * lightSwing * math.Pi / 1801158 ca, sa := math.Cos(a), math.Sin(a)1159 L := vnorm(v3{light.x*ca - light.y*sa, light.x*sa + light.y*ca, light.z})1160 var cs []cand1161 for i, d := range front {1162 if d.kind == kTable {1163 continue1164 }1165 f2, _ := shadeFront(d.kind, d.nv, L, d.jit, d.drift, d.thick, d.ext)1166 if dl := lumaDelta(f2, d.fill); dl > 0.06 && polyArea(d.pts) > 3 {1167 cs = append(cs, cand{i, f2, dl})1168 }1169 }1170 for i := 1; i < len(cs); i++ {1171 for j := i; j > 0 && cs[j].delta > cs[j-1].delta; j-- {1172 cs[j], cs[j-1] = cs[j-1], cs[j]1173 }1174 }1175 if len(cs) > lightMaxFacets {1176 cs = cs[:lightMaxFacets]1177 }1178 if len(cs) == 0 {1179 continue1180 }1181 cls := "gt-la"1182 if si == 1 {1183 cls = "gt-lb"1184 }1185 facetsB.WriteString(`<g class="` + cls + `" opacity="0">`)1186 for _, c := range cs {1187 facetsB.WriteString(`<path d="` + relPath(front[c.i].pts) + `" fill="` + c.fill + `"/>`)1188 }1189 facetsB.WriteString(`</g>`)1190 }11911192 gd, lastG := "", ""1193 for i := range girdle {1194 a, b := view(girdle[i]), view(girdle[(i+1)%len(girdle)])1195 mid := vscale(v3{girdle[i].x + girdle[(i+1)%len(girdle)].x, girdle[i].y + girdle[(i+1)%len(girdle)].y, 0}, 0.5)1196 if view(vnorm(mid)).z-0 > -0.05 && view(mid).z >= view(v3{}).z-0.01 {1197 pa, pb := scr(a), scr(b)1198 sa := n1(pa.x) + " " + n1(pa.y)1199 if sa != lastG {1200 gd += "M" + sa1201 }1202 lastG = n1(pb.x) + " " + n1(pb.y)1203 gd += " " + lastG1204 }1205 }1206 over = `<path d="` + gd + `" fill="none" stroke="` + hex3(lighten(base, 0.18)) + `" stroke-width=".7" stroke-linecap="round" opacity=".4"/>`1207 return underB.String(), facetsB.String(), over1208}12091210const dispersionMix = 0.3212111212var spectrum = []string{"#ff4040", "#ff9a1f", "#ffe81f", "#35ff7a", "#22ccff", "#5566ff", "#c04dff"}12131214var spectrumI = func() []rgbI {1215 out := make([]rgbI, len(spectrum))1216 for i, h := range spectrum {1217 out[i] = toRGBI(h)1218 }1219 return out1220}()12211222func spectrumAtI(t float64) rgbI {1223 t = t - math.Floor(t)1224 f := t * float64(len(spectrumI))1225 i := int(f)1226 return mixI(spectrumI[i%len(spectrumI)], spectrumI[(i+1)%len(spectrumI)], f-float64(i))1227}12281229func spectrumAt(t float64) string {1230 t = t - math.Floor(t)1231 f := t * float64(len(spectrum))1232 i := int(f)1233 return mixHex(spectrum[i%len(spectrum)], spectrum[(i+1)%len(spectrum)], f-float64(i))1234}12351236const (1237 lightSwing = 2.21238 lightMaxFacets = 121239)12401241func lumaDelta(a, b string) float64 {1242 wide := func(h string) string {1243 if len(h) == 4 {1244 return "#" + h[1:2] + h[1:2] + h[2:3] + h[2:3] + h[3:4] + h[3:4]1245 }1246 return h1247 }1248 ar, ag, ab := hexToRGB(wide(a))1249 br, bg, bb := hexToRGB(wide(b))1250 d := (0.2126*float64(ar-br) + 0.7152*float64(ag-bg) + 0.0722*float64(ab-bb)) / 2551251 if d < 0 {1252 return -d1253 }1254 return d1255}12561257const fireMaxFacets = 1212581259const starfield = true12601261func renderStars(salt string) string {1262 if !starfield {1263 return ""1264 }1265 rnd := newPRNG("stars" + salt)1266 type star struct{ x, y float64 }1267 var byLevel [4][]star1268 for i := 0; i < 36; i++ {1269 x, y := rnd.floatIn(3, canvasW-3), rnd.floatIn(3, 127)1270 k := rnd.intn(20)1271 dx, dy := x-subjX, y-subjY1272 if r := math.Sqrt(dx*dx + dy*dy); r > 43 && r < 58 {1273 continue1274 }1275 lvl := 0 // most stars are faint; a few are bright1276 switch {1277 case k >= 18:1278 lvl = 31279 case k >= 14:1280 lvl = 21281 case k >= 8:1282 lvl = 11283 }1284 byLevel[lvl] = append(byLevel[lvl], star{x, y})1285 }1286 widths := [4]string{".45", ".6", ".85", "1.15"}1287 ops := [4]string{".22", ".38", ".58", ".85"}1288 var b strings.Builder1289 b.WriteString(`<g stroke="#fff" stroke-linecap="round" fill="none">`)1290 var tw []star1291 for l := 0; l < 4; l++ {1292 if len(byLevel[l]) == 0 {1293 continue1294 }1295 d := ""1296 for i, st := range byLevel[l] {1297 if l >= 1 && l <= 2 && len(tw) < 3 && i%3 == 0 {1298 tw = append(tw, st)1299 continue1300 }1301 d += "M" + n1(st.x) + " " + n1(st.y) + "h.01"1302 }1303 if d != "" {1304 b.WriteString(`<path d="` + d + `" stroke-width="` + widths[l] + `" opacity="` + ops[l] + `"/>`)1305 }1306 }1307 for _, st := range tw {1308 dur := rnd.floatIn(5, 12)1309 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"/>`)1310 }1311 b.WriteString(`</g>`)1312 return b.String()1313}13141315func pearlLusterGradient(gid, base, metal string) string {1316 r, g, b := hexToRGB(base)1317 lum := (0.2126*float64(r) + 0.7152*float64(g) + 0.0722*float64(b)) / 2551318 shadow := "#4a4038"1319 if lum < 0.4 {1320 shadow = "#000000"1321 }1322 hi := base1323 sc, ss, sw := ".6", ".32", ".16"1324 mo := ".3"1325 if lum < 0.4 {1326 mo = ".22"1327 hi = mixHex(base, "#c9d2d8", 0.28)1328 sc, ss, sw = ".34", ".16", ".07"1329 }1330 return `<radialGradient id="` + gid + `" cx=".4" cy=".36" r=".7" fx=".34" fy=".28">` +1331 `<stop offset="0" stop-color="` + hi + `"/>` +1332 `<stop offset=".4" stop-color="` + mixHex(base, shadow, 0.13) + `"/>` +1333 `<stop offset=".76" stop-color="` + mixHex(base, shadow, 0.36) + `"/>` +1334 `<stop offset=".93" stop-color="` + mixHex(base, shadow, 0.5) + `"/>` +1335 `<stop offset="1" stop-color="` + mixHex(base, shadow, 0.3) + `"/></radialGradient>` +1336 `<radialGradient id="` + gid + `o" cx=".45" cy=".44" r=".62">` +1337 `<stop offset=".3" stop-color="#fff" stop-opacity="0"/>` +1338 `<stop offset=".62" stop-color="#ffb3cf" stop-opacity=".2"/>` +1339 `<stop offset=".88" stop-color="#b3ffd6" stop-opacity=".2"/>` +1340 `<stop offset="1" stop-color="#c3d3ff" stop-opacity=".12"/></radialGradient>` +1341 `<radialGradient id="` + gid + `s" cx=".36" cy=".3" r=".4">` +1342 `<stop offset="0" stop-color="#fff" stop-opacity="` + sc + `"/>` +1343 `<stop offset=".35" stop-color="#fff" stop-opacity="` + ss + `"/>` +1344 `<stop offset=".7" stop-color="#fff" stop-opacity=".06"/>` +1345 `<stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient>` +1346 `<radialGradient id="` + gid + `m" cx=".42" cy=".36" r=".68">` +1347 `<stop offset=".78" stop-color="` + metal + `" stop-opacity="0"/>` +1348 `<stop offset=".93" stop-color="` + metal + `" stop-opacity="` + mo + `"/>` +1349 `<stop offset="1" stop-color="` + metal + `" stop-opacity="0"/></radialGradient>` +1350 `<radialGradient id="` + gid + `w" cx=".66" cy=".73" r=".2">` +1351 `<stop offset="0" stop-color="#fff" stop-opacity="` + sw + `"/>` +1352 `<stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient>`1353}13541355func pearlLayers(shape, gid, between string) string {1356 i := strings.Index(shape, ` fill="`)1357 if i < 0 {1358 return shape1359 }1360 cp := func(suffix string) string { return shape[:i] + ` fill="url(#` + gid + suffix + `)"/>` }1361 return shape + cp("o") + cp("m") + between + cp("w") + cp("s")1362}13631364func pearlSweepColorIntensity(base string) float64 {1365 r, g, b := hexToRGB(base)1366 lum := 0.2126*float64(r)/255 + 0.7152*float64(g)/255 + 0.0722*float64(b)/2551367 if lum < 0.4 {1368 return 0.551369 }1370 return 2.01371}13721373func pearlSweepGradient(gid, base string) string {1374 mult := pearlSweepColorIntensity(base)1375 o1, o2, o3 := fnum(0.3*mult), fnum(0.35*mult), fnum(0.3*mult)1376 return `<linearGradient id="` + gid + `" x1="0%" y1="0%" x2="100%" y2="0%">` +1377 `<stop offset="0%" stop-color="#ffffff" stop-opacity="0"/>` +1378 `<stop offset="18%" stop-color="#ffd2e6" stop-opacity="` + o1 + `"/>` +1379 `<stop offset="38%" stop-color="#cfe8ff" stop-opacity="` + o2 + `"/>` +1380 `<stop offset="58%" stop-color="#d4ffe4" stop-opacity="` + o3 + `"/>` +1381 `<stop offset="80%" stop-color="#fff0c2" stop-opacity="` + o3 + `"/>` +1382 `<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>` +1383 `</linearGradient>`1384}13851386func pearlSweepOverlay(salt, base, shapeGeom string, w, h float64) string {1387 rnd := newPRNG(salt + "-sweep")1388 const passes = 31389 total := 15.0 + rnd.floatIn(0, 9)13901391 durs := make([]float64, passes)1392 sumDur := 0.01393 for i := 0; i < passes; i++ {1394 durs[i] = 2.4 + rnd.floatIn(0, 2.2)1395 sumDur += durs[i]1396 }1397 gapTotal := total - sumDur1398 if gapTotal < float64(passes)*0.5 {1399 gapTotal = float64(passes) * 0.51400 }1401 weights := make([]float64, passes)1402 sumW := 0.01403 for i := 0; i < passes; i++ {1404 weights[i] = 0.4 + rnd.floatIn(0, 1)1405 sumW += weights[i]1406 }1407 gaps := make([]float64, passes)1408 for i := 0; i < passes; i++ {1409 gaps[i] = weights[i] / sumW * gapTotal1410 }1411 starts := make([]float64, passes)1412 t := gaps[0]1413 for i := 0; i < passes; i++ {1414 starts[i] = t1415 t += durs[i]1416 if i < passes-1 {1417 t += gaps[i+1]1418 }1419 }14201421 travel := w * 1.051422 clipID := "clp" + salt1423 gradID := "swp" + salt1424 off, on1, on2, out := fnum(-travel), fnum(-travel*0.5), fnum(travel*0.5), fnum(travel)14251426 var defs strings.Builder1427 defs.WriteString(`<defs><clipPath id="` + clipID + `">` + shapeGeom + `</clipPath>`)1428 defs.WriteString(pearlSweepGradient(gradID, base))14291430 var marks strings.Builder1431 marks.WriteString(`<g clip-path="url(#` + clipID + `)">`)1432 for i := 0; i < passes; i++ {1433 angle := fnum(rnd.floatIn(0, 360))1434 startPct := starts[i] / total * 1001435 durPct := durs[i] / total * 1001436 rampPct := durPct * 0.321437 if rampPct > 6 {1438 rampPct = 61439 }1440 p0, p1, p2, p3 := startPct, startPct+rampPct, startPct+durPct-rampPct, startPct+durPct1441 kf := "swp" + salt + "_" + ufmt.Sprintf("%d", i)14421443 closing := ""1444 if p3 < 99.999 {1445 closing = `100%{transform:translate(` + off + `px,0);opacity:0}`1446 }1447 defs.WriteString(`<style>@keyframes ` + kf + `{` +1448 `0%{transform:translate(` + off + `px,0);opacity:0}` +1449 fnum(p0) + `%{transform:translate(` + off + `px,0);opacity:0}` +1450 fnum(p1) + `%{transform:translate(` + on1 + `px,0);opacity:1}` +1451 fnum(p2) + `%{transform:translate(` + on2 + `px,0);opacity:1}` +1452 fnum(p3) + `%{transform:translate(` + out + `px,0);opacity:0}` +1453 closing +1454 `}</style>`)14551456 marks.WriteString(`<g transform="rotate(` + angle + ` ` + fnum(subjX) + ` ` + fnum(subjY) + `)">` +1457 `<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"/>` +1458 `</g>`)1459 }1460 marks.WriteString(`</g>`)1461 defs.WriteString(`</defs>`)1462 return defs.String() + marks.String()1463}14641465func pearlRound(base, metal, salt string) string {1466 gid := "prl" + salt1467 r := 24.0 * stoneScale1468 body := `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(r) + `" fill="url(#` + gid + `)"/>`1469 clipGeom := `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(r) + `"/>`1470 sweep := pearlSweepOverlay(salt, base, clipGeom, r*2, r*2)1471 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, "") + sweep1472}14731474func pearlOval(base, metal, salt string) string {1475 gid := "prl" + salt1476 rx, ry := 18.0*stoneScale, 25.0*stoneScale1477 body := `<ellipse cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" rx="` + fnum(rx) + `" ry="` + fnum(ry) + `" fill="url(#` + gid + `)"/>`1478 clipGeom := `<ellipse cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" rx="` + fnum(rx) + `" ry="` + fnum(ry) + `"/>`1479 sweep := pearlSweepOverlay(salt, base, clipGeom, rx*2, ry*2)1480 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, "") + sweep1481}14821483func pearlButton(base, metal, salt string) string {1484 gid := "prl" + salt1485 rx, ry := 25.0*stoneScale, 18.0*stoneScale1486 body := `<ellipse cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" rx="` + fnum(rx) + `" ry="` + fnum(ry) + `" fill="url(#` + gid + `)"/>`1487 clipGeom := `<ellipse cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" rx="` + fnum(rx) + `" ry="` + fnum(ry) + `"/>`1488 sweep := pearlSweepOverlay(salt, base, clipGeom, rx*2, ry*2)1489 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, "") + sweep1490}14911492func pearlDrop(base, metal, salt string) string {1493 gid := "prl" + salt1494 w, h := 32.0*stoneScale, 42.0*stoneScale1495 path := `M ` + fnum(subjX) + ` ` + fnum(subjY-h*0.55) +1496 ` 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) +1497 ` 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`1498 body := `<path d="` + path + `" fill="url(#` + gid + `)"/>`1499 clipGeom := `<path d="` + path + `"/>`1500 sweep := pearlSweepOverlay(salt, base, clipGeom, w*1.6, h*0.9)1501 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, "") + sweep1502}15031504func pearlBaroqueOutline(seed string) []pt {1505 rnd := newPRNG(seed)1506 n := 101507 pts := make([]pt, n)1508 for k := 0; k < n; k++ {1509 a := 2 * math.Pi * float64(k) / float64(n)1510 rr := 21.0 * stoneScale * (0.75 + 0.4*rnd.floatIn(0, 1))1511 pts[k] = pt{subjX + rr*math.Cos(a)*1.1, subjY + rr*math.Sin(a)*0.9}1512 }1513 return pts1514}15151516func pearlBaroqueSmoothPath(pts []pt) string {1517 n := len(pts)1518 mid := func(a, b pt) pt { return pt{(a.x + b.x) / 2, (a.y + b.y) / 2} }1519 start := mid(pts[n-1], pts[0])1520 path := "M " + fnum(start.x) + " " + fnum(start.y)1521 for k := 0; k < n; k++ {1522 next := pts[(k+1)%n]1523 m := mid(pts[k], next)1524 path += " Q " + fnum(pts[k].x) + " " + fnum(pts[k].y) + " " + fnum(m.x) + " " + fnum(m.y)1525 }1526 return path + " Z"1527}15281529func pearlBaroque(base, metal, salt string) string {1530 gid := "prl" + salt1531 pathD := pearlBaroqueSmoothPath(pearlBaroqueOutline("baroque" + salt))1532 body := `<path d="` + pathD + `" fill="url(#` + gid + `)"/>`1533 clipGeom := `<path d="` + pathD + `"/>`1534 sweep := pearlSweepOverlay(salt, base, clipGeom, 40*stoneScale, 30*stoneScale)1535 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, "") + sweep1536}15371538func pearlCircled(base, metal, salt string) string {1539 gid := "prl" + salt1540 r := 24.0 * stoneScale1541 body := `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(r) + `" fill="url(#` + gid + `)"/>`1542 ringTone := mixHex(base, "#000000", 0.12)1543 rings := ""1544 for _, f := range []float64{0.42, 0.58, 0.74, 0.9} {1545 rings += `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(r*f) + `" fill="none" stroke="` + ringTone + `" stroke-width="0.9" opacity="0.35"/>`1546 }1547 clipGeom := `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(r) + `"/>`1548 sweep := pearlSweepOverlay(salt, base, clipGeom, r*2, r*2)1549 return `<defs>` + pearlLusterGradient(gid, base, metal) + `</defs>` + pearlLayers(body, gid, rings) + sweep1550}15511552var PearlShapeNames = []string{"Round", "Oval", "Button", "Drop", "Baroque", "Circled"}15531554func renderPearlShape(shapeIdx int, base, metal, salt string) string {1555 switch shapeIdx {1556 case 0:1557 return pearlRound(base, metal, salt)1558 case 1:1559 return pearlOval(base, metal, salt)1560 case 2:1561 return pearlButton(base, metal, salt)1562 case 3:1563 return pearlDrop(base, metal, salt)1564 case 4:1565 return pearlBaroque(base, metal, salt)1566 default:1567 return pearlCircled(base, metal, salt)1568 }1569}15701571func renderRing(metalHi, metalLo, salt string) string {1572 gid := "ring" + salt1573 bright := lighten(metalHi, 0.45)1574 deep := mixHex(metalLo, "#000000", 0.45)1575 st := func(off, c string) string { return `<stop offset="` + off + `" stop-color="` + c + `"/>` }1576 defs := `<linearGradient id="` + gid + `" x2="1" y2="1">` +1577 st("0", bright) + st(".12", metalHi) + st(".26", deep) + st(".4", metalLo) + st(".52", bright) +1578 st(".64", metalHi) + st(".78", deep) + st(".9", metalLo) + st("1", bright) + `</linearGradient>`15791580 outer := `<g fill="none" transform="translate(` + cnum(subjX) + ` ` + cnum(subjY) + `)">` +1581 `<circle r="52.6" stroke="#000" stroke-width="1.6" opacity=".45"/>` +1582 `<circle r="50" stroke="url(#` + gid + `)" stroke-width="4.4"/>` +1583 `<circle r="52.3" stroke="` + deep + `" stroke-width=".7" opacity=".8"/>` +1584 `<circle r="47.7" stroke="` + deep + `" stroke-width=".6" opacity=".7"/>` +1585 `<circle r="48.5" stroke="` + lighten(metalHi, 0.35) + `" stroke-width=".5" opacity=".45"/></g>`15861587 const rOut = 46.01588 const coreHalf = 0.61589 const outwardBloom = 0.91590 glowR := rOut + outwardBloom + 31591 pct := func(v float64) float64 {1592 p := v / glowR * 1001593 if p < 0 {1594 p = 01595 }1596 if p > 100 {1597 p = 1001598 }1599 return p1600 }1601 coreColor := lighten(metalHi, 0.25)1602 donutStops := func(inwardBloom, peakOp float64, gid string) string {1603 bloomOp := peakOp * 0.351604 midOp := peakOp * 0.71605 return `<radialGradient id="` + gid + `" cx="50%" cy="50%" r="50%">` +1606 `<stop offset="0%" stop-color="` + metalHi + `" stop-opacity="0"/>` +1607 `<stop offset="` + pctnum(pct(rOut-inwardBloom-1.5)) + `%" stop-color="` + metalHi + `" stop-opacity="0"/>` +1608 `<stop offset="` + pctnum(pct(rOut-inwardBloom)) + `%" stop-color="` + metalHi + `" stop-opacity="` + fnum(bloomOp) + `"/>` +1609 `<stop offset="` + pctnum(pct(rOut-coreHalf)) + `%" stop-color="` + coreColor + `" stop-opacity="` + fnum(midOp) + `"/>` +1610 `<stop offset="` + pctnum(pct(rOut)) + `%" stop-color="` + coreColor + `" stop-opacity="` + fnum(peakOp) + `"/>` +1611 `<stop offset="` + pctnum(pct(rOut+coreHalf)) + `%" stop-color="` + coreColor + `" stop-opacity="` + fnum(midOp) + `"/>` +1612 `<stop offset="` + pctnum(pct(rOut+outwardBloom)) + `%" stop-color="` + metalHi + `" stop-opacity="` + fnum(bloomOp) + `"/>` +1613 `<stop offset="` + pctnum(pct(rOut+outwardBloom+1)) + `%" stop-color="` + metalHi + `" stop-opacity="0"/>` +1614 `<stop offset="100%" stop-color="` + metalHi + `" stop-opacity="0"/></radialGradient>`1615 }16161617 rnd := newPRNG("energy" + salt)1618 gidA, gidB := "glowA"+salt, "glowB"+salt1619 dur := rnd.floatIn(2.13, 3.2)1620 phase := rnd.floatIn(0, dur)16211622 stopsA := donutStops(2.5*2.0/3.0*0.5, 0.4, gidA)1623 stopsB := donutStops(8.0*2.0/3.0*0.5, 0.6, gidB)16241625 energy := `<defs>` + stopsA + stopsB + `</defs>` +1626 `<g>` +1627 `<circle cx="` + fnum(subjX) + `" cy="` + fnum(subjY) + `" r="` + fnum(glowR) + `" fill="url(#` + gidA + `)"/>` +1628 `<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"/>` +1629 `</g>`16301631 return `<defs>` + defs + `</defs>` + outer + energy1632}16331634const (1635 motionVDur, motionHDur, motionRDur, motionSDur = 3.4, 4.66, 5.81, 6.561636 motionRotMin, motionRotMax = 4.0, 8.01637 motionScale = 1.091638)16391640func motionPhases(salt string) (vphase, hphase, rphase, sphase, rotDeg float64) {1641 rnd := newPRNG("motion" + salt)1642 vphase = rnd.floatIn(0, motionVDur)1643 hphase = rnd.floatIn(0, motionHDur)1644 rphase = rnd.floatIn(0, motionRDur)1645 sphase = rnd.floatIn(0, motionSDur)1646 rotDeg = rnd.floatIn(motionRotMin, motionRotMax)1647 return1648}16491650func applyMotion(content, salt string) string {1651 vphase, hphase, rphase, sphase, rotDeg := motionPhases(salt)16521653 return `<g transform="translate(` + fnum(subjX) + ` ` + fnum(subjY) + `)">` +1654 `<g class="gt-v" style="--v-dur:` + fnum(motionVDur) + `s;--v-delay:-` + fnum(vphase) + `s">` +1655 `<g class="gt-h" style="--h-dur:` + fnum(motionHDur) + `s;--h-delay:-` + fnum(hphase) + `s">` +1656 `<g class="gt-r" style="--r-dur:` + fnum(motionRDur) + `s;--r-delay:-` + fnum(rphase) + `s;--rot-deg:` + fnum(rotDeg) + `deg">` +1657 `<g class="gt-s" style="--s-dur:` + fnum(motionSDur) + `s;--s-delay:-` + fnum(sphase) + `s">` +1658 `<g transform="translate(-` + fnum(subjX) + ` -` + fnum(subjY) + `)">` +1659 content + `</g></g></g></g></g></g>`1660}16611662const dotX, dotY = subjX + 50, subjY + 4216631664func renderStatusBadge(active bool) string {1665 if active {1666 return `<circle cx="` + fnum(dotX) + `" cy="` + fnum(dotY) + `" r="4" fill="#7fd88f"/>` +1667 `<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>`1668 }1669 return `<circle cx="` + fnum(dotX) + `" cy="` + fnum(dotY) + `" r="4" fill="#5a5f6b"/>`1670}16711672type facetSpan struct {1673 start, end int1674 ringMember bool1675}16761677func scanFacetSpans(markup string) []facetSpan {1678 var spans []facetSpan1679 i := 01680 for {1681 rel := strings.Index(markup[i:], "<")1682 if rel < 0 {1683 break1684 }1685 start := i + rel1686 relEnd := strings.Index(markup[start:], "/>")1687 if relEnd < 0 {1688 break1689 }1690 end := start + relEnd + 21691 tag := markup[start:end]1692 i = end1693 isFacetTag := strings.HasPrefix(tag, "<path") || strings.HasPrefix(tag, "<circle") || strings.HasPrefix(tag, "<polygon")1694 if !isFacetTag {1695 continue1696 }1697 spans = append(spans, facetSpan{start: start, end: end, ringMember: strings.Contains(tag, `class="f"`)})1698 }1699 return spans1700}17011702const (1703 fireSweepAxis = -35.0 * math.Pi / 1801704 fireSweepSpan = 1.6 // seconds a sweep takes to cross the stone1705)17061707func applyCascade(markup, tidStr string, fs *facetStyle) (overlay string, css string) {1708 if fs == nil || len(fs.ringCent) == 0 {1709 return "", ""1710 }1711 spans := scanFacetSpans(markup)1712 rnd := newPRNG("fire" + tidStr)1713 _, _, rphase, _, _ := motionPhases(tidStr)1714 period := 2 * motionRDur1715 axis := fireSweepAxis + rnd.floatIn(-0.3, 0.3)1716 dx, dy := math.Cos(axis), math.Sin(axis)17171718 ka, kb := "fa"+tidStr, "fb"+tidStr1719 shape := func(at float64) string {1720 return fnum1(at) + `%{opacity:0}` + fnum1(at+2) + `%{opacity:var(--o);fill:#fff}` +1721 fnum1(at+4.5) + `%{opacity:.4;fill:var(--c)}` + fnum1(at+8) + `%{opacity:0}`1722 }1723 per := fnum(period)1724 css = `.` + ka + `{animation:` + ka + ` ` + per + `s linear infinite}.` + kb + `{animation:` + kb + ` ` + per + `s linear infinite}` +1725 `@keyframes ` + ka + `{` + shape(12.5) + `100%{opacity:0}}` +1726 `@keyframes ` + kb + `{` + shape(62.5) + `100%{opacity:0}}`17271728 var out strings.Builder1729 ringIdx := -11730 for _, span := range spans {1731 if !span.ringMember {1732 continue1733 }1734 ringIdx++1735 if ringIdx >= len(fs.ringCent) {1736 break1737 }1738 c := fs.ringCent[ringIdx]1739 color, colorOp := fs.flash.sample(c.x, c.y)1740 pos := ((c.x-subjX)*dx + (c.y-pieceCY)*dy) / (30 * stoneScale)1741 name := ka1742 if rnd.floatIn(0, 1) < 0.5 {1743 name, pos = kb, -pos1744 }1745 delay := rphase - 0.25*motionRDur + period - pos*fireSweepSpan/21746 tag := markup[span.start:span.end]1747 geomEnd := strings.Index(tag, `" fill=`)1748 if geomEnd < 0 {1749 continue1750 }1751 out.WriteString(tag[:geomEnd] + `" opacity="0" class="` + name + `" style="--o:` + fnum(math.Round(rnd.floatIn(0.55, 0.9)*colorOp*10)/10) + `;--c:` + hex3(color) +1752 `;animation-delay:-` + fnum1(delay) + `s"/>`)1753 }1754 return out.String(), css1755}17561757func renderMetalLabelRect(metalHi, metalLo string, x, y, w, h, rx float64, salt string) string {1758 gid := "lg" + salt1759 bright := lighten(metalHi, 0.4)1760 deep := mixHex(metalLo, "#000000", 0.5)1761 st := func(off, c string) string { return `<stop offset="` + off + `" stop-color="` + c + `"/>` }1762 defs := `<linearGradient id="` + gid + `">` +1763 st("0", metalLo) + st(".22", metalHi) + st(".42", bright) + st(".62", metalHi) + st("1", metalLo) + `</linearGradient>`1764 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"/>`1765 const in_ = 0.91766 arc := ` a` + cnum(rx-in_) + ` ` + cnum(rx-in_) + ` 0 0 `1767 d := cnum(rx - in_)1768 edges := `<g fill="none" stroke-linecap="round">` +1769 `<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"/>` +1770 `<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>`1771 sid := "sh" + salt1772 defs += `<g id="` + sid + `"><circle cx=".15" cy=".2" r="1.6" fill="` + deep + `" opacity=".55"/>` +1773 `<circle r="1.3" fill="` + mixHex(metalLo, metalHi, 0.4) + `" stroke="` + deep + `" stroke-width=".25"/>` +1774 `<circle cx="-.4" cy="-.4" r=".75" fill="` + lighten(metalHi, 0.3) + `" opacity=".35"/></g>`1775 seed := 01776 for k := 0; k < len(salt); k++ {1777 seed += int(salt[k])1778 }1779 heads, slots := "", ""1780 n := 01781 for _, px := range []float64{x + 4.2, x + w - 4.2} {1782 for _, py := range []float64{y + 3.7, y + h - 3.7} {1783 heads += `<use href="#` + sid + `" x="` + cnum(px) + `" y="` + cnum(py) + `"/>`1784 ang := float64((seed*37+n*71)%180) * math.Pi / 1801785 dx, dy := 0.9*math.Cos(ang), 0.9*math.Sin(ang)1786 slots += `M` + cnum(px-dx) + ` ` + cnum(py-dy) + `l` + cnum(2*dx) + ` ` + cnum(2*dy)1787 n++1788 }1789 }1790 rivets := heads + `<path d="` + slots + `" stroke="` + deep + `" stroke-width=".45" stroke-linecap="round"/>`1791 return `<defs>` + defs + `</defs>` + rect + edges + rivets1792}17931794const (1795 plateFont = `'DIN Alternate',Bahnschrift,'Roboto Condensed',sans-serif`1796 plateSize = 8.61797 plateSpacing = 1.11798 plateMaxW = 100.01799)18001801func renderPlateText(label, fill, metalHi string) string {1802 g := `<g font-size="` + cnum(plateSize) + `" letter-spacing="` + cnum(plateSpacing) + `" text-anchor="middle" font-family="` + plateFont + `" font-weight="700"`1803 if est := float64(len(label))*(plateSize*0.52+plateSpacing); est > plateMaxW {1804 g += ` textLength="` + cnum(plateMaxW) + `" lengthAdjust="spacingAndGlyphs"`1805 }1806 x := cnum(subjX)1807 return g + `><text x="` + x + `" y="144.9" fill="` + lighten(metalHi, 0.5) + `" opacity=".45">` + label + `</text>` +1808 `<text x="` + x + `" y="144.4" fill="` + fill + `">` + label + `</text></g>`1809}18101811func renderSerialText(tidStr, color string) string {1812 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>`1813}18141815type namedColor struct{ hex, name string }18161817var stoneByChain = map[string]string{1818 "TOPAZ-1": "Topaz",1819 "SAPPHIRE-1": "Sapphire",1820 "PEARL-1": "Pearl",1821 "GNOLAND-1": "Diamond",1822}18231824var stonePalettes = map[string][]namedColor{1825 "Sapphire": {1826 {"#2a5fe0", "Blue"}, {"#ff6ea4", "Pink"}, {"#ff8501", "Orange"}, {"#ffd500", "Yellow"},1827 {"#1aa88a", "Green"}, {"#a658ff", "Violet"}, {"#e6e9f8", "White"}, {"#ff8f72", "Padparadscha"},1828 },1829 "Topaz": {1830 {"#f5edd1", "Colorless"}, {"#ffa300", "Imperial"}, {"#ffd100", "Golden"},1831 {"#c96a28", "Sherry Brown"}, {"#86cdf2", "Sky Blue"}, {"#ff96ba", "Pink"},1832 },1833 "Quartz": {1834 {"#eef2f7", "Rock Crystal"}, {"#9b59d6", "Amethyst"}, {"#f2b01e", "Citrine"},1835 {"#f5b9ca", "Rose Quartz"}, {"#7d6250", "Smoky Quartz"}, {"#a9d9a2", "Prasiolite"},1836 },1837 "Diamond": {1838 {"#f3f6fc", "Colorless"}, {"#ffc400", "Fancy Yellow"}, {"#ff4f9a", "Fancy Pink"},1839 {"#1f8fff", "Fancy Blue"}, {"#14c46e", "Fancy Green"}, {"#d9a560", "Champagne"},1840 {"#b4561a", "Cognac"}, {"#2b2d34", "Black"},1841 {"#e0243a", "Red"}, {"#ff7a1a", "Orange"}, {"#9b4fd6", "Purple"},1842 },1843 "Pearl": {1844 {"#f8f6f0", "White"}, {"#f0e4c8", "Cream"}, {"#3a3a3c", "Black"}, {"#c8cdd4", "Silver"},1845 {"#d4af6a", "Gold"}, {"#f0d4dc", "Pink"}, {"#f2c9a8", "Peach"}, {"#d8cbe0", "Lavender"},1846 },1847}18481849var colorWeights = map[string][]int{1850 "Diamond": {30, 18, 5, 4, 3, 14, 12, 8, 1, 3, 2},1851 "Quartz": {25, 25, 18, 9, 18, 5},1852}18531854const defaultColorWeight = 518551856func colorWeight(stone string, idx int) int {1857 w, ok := colorWeights[stone]1858 if !ok {1859 return 11860 }1861 if idx < len(w) {1862 return w[idx]1863 }1864 return defaultColorWeight1865}18661867func PickColorIndex(stone string, roll uint64) int {1868 n := ColorPaletteSize(stone)1869 total := 01870 for i := 0; i < n; i++ {1871 total += colorWeight(stone, i)1872 }1873 r := int(roll % uint64(total))1874 for i := 0; i < n; i++ {1875 r -= colorWeight(stone, i)1876 if r < 0 {1877 return i1878 }1879 }1880 return n - 11881}18821883func ColorProbability(stone string, idx int) float64 {1884 n := ColorPaletteSize(stone)1885 total := 01886 for i := 0; i < n; i++ {1887 total += colorWeight(stone, i)1888 }1889 return float64(colorWeight(stone, idx)) / float64(total)1890}18911892func ColorRarityCSV(stone string) string {1893 n := ColorPaletteSize(stone)1894 total := 01895 for i := 0; i < n; i++ {1896 total += colorWeight(stone, i)1897 }1898 out := make([]string, n)1899 for i := 0; i < n; i++ {1900 pm := colorWeight(stone, i) * 1000 / total1901 out[i] = colorForStone(stone, i).name + ":" + ufmt.Sprintf("%d.%d", pm/10, pm%10)1902 }1903 return strings.Join(out, ";")1904}19051906func ChainAlwaysActive(chain string) bool { return chain == "GNOLAND-1" }19071908var stoneNames = []string{"Sapphire", "Topaz", "Pearl", "Diamond", "Quartz"}19091910func registerStoneName(stone string) {1911 for _, s := range stoneNames {1912 if s == stone {1913 return1914 }1915 }1916 stoneNames = append(stoneNames, stone)1917}19181919func SetStoneForChain(chainName, stone string) {1920 stoneByChain[chainName] = stone1921 registerStoneName(stone)1922}19231924func AddStoneColor(stone, hex, name string) {1925 stonePalettes[stone] = append(stonePalettes[stone], namedColor{hex: hex, name: name})1926 registerStoneName(stone)1927}19281929func StoneNamesCSV() string {1930 return strings.Join(stoneNames, ";")1931}19321933func ColorPaletteCSV(stone string) string {1934 palette := stonePalettes[stone]1935 pairs := make([]string, len(palette))1936 for i, c := range palette {1937 pairs[i] = c.hex + ":" + c.name1938 }1939 return strings.Join(pairs, ";")1940}19411942func MetalToneHex(metalName string) string {1943 t, ok := metals[metalName]1944 if !ok {1945 return ""1946 }1947 return t.hi + ";" + t.lo1948}19491950func StoneForChain(chain string) string {1951 if s, ok := stoneByChain[chain]; ok {1952 return s1953 }1954 return "Quartz"1955}19561957func colorForStone(stone string, colorIdx int) namedColor {1958 palette := stonePalettes[stone]1959 if len(palette) == 0 {1960 palette = stonePalettes["Quartz"]1961 }1962 if colorIdx < 0 {1963 colorIdx = 01964 }1965 if colorIdx >= len(palette) {1966 colorIdx = len(palette) - 11967 }1968 return palette[colorIdx]1969}19701971func ColorPaletteSize(stone string) int {1972 if n := len(stonePalettes[stone]); n > 0 {1973 return n1974 }1975 return len(stonePalettes["Quartz"])1976}19771978func FindShapeIndex(stone, name string) (idx int, ok bool) {1979 names := ShapeNames1980 if stone == "Pearl" {1981 names = PearlShapeNames1982 }1983 for i, s := range names {1984 if s == name {1985 return i, true1986 }1987 }1988 return -1, false1989}19901991func FindColorIndex(chain, colorName string) (idx int, ok bool) {1992 stone := StoneForChain(chain)1993 palette := stonePalettes[stone]1994 if len(palette) == 0 {1995 palette = stonePalettes["Quartz"]1996 }1997 for i, c := range palette {1998 if c.name == colorName {1999 return i, true2000 }2001 }2002 return -1, false2003}20042005type metalTone struct{ hi, lo string }20062007var metals = map[string]metalTone{2008 "Gold": {"#a48b3f", "#6a5117"},2009 "Silver": {"#95989c", "#5e646c"},2010 "Bronze": {"#865b34", "#52391d"},2011 "Platinum": {"#c3c6c8", "#87909a"},2012 "Obsidian": {"#3a3d47", "#0d0e12"},2013}20142015var MetalNames = []string{"Gold", "Silver", "Bronze", "Platinum", "Obsidian"}20162017type metalLabelTone struct{ bg, text string }20182019var metalLabel = map[string]metalLabelTone{2020 "Gold": {"#f4d060", "#04121a"},2021 "Silver": {"#dfe4ea", "#04121a"},2022 "Bronze": {"#c9884f", "#04121a"},2023 "Platinum": {"#eef2f5", "#04121a"},2024 "Obsidian": {"#5a5f6b", "#e8e9f0"},2025}20262027func RenderTokenThumbSVG(chain, metal string, shapeIdx, colorIdx int, active bool, tidStr string) string {2028 svg, _, _, _ := renderToken(chain, metal, shapeIdx, colorIdx, active, tidStr, true)2029 return svg2030}20312032func RenderTokenSVG(chain, metal string, shapeIdx, colorIdx int, active bool, tidStr string) (svg string, stone string, colorHex string, colorName string) {2033 return renderToken(chain, metal, shapeIdx, colorIdx, active, tidStr, false)2034}20352036func renderToken(chain, metal string, shapeIdx, colorIdx int, active bool, tidStr string, thumb bool) (svg string, stone string, colorHex string, colorName string) {2037 var cascadeCSS string2038 stone = StoneForChain(chain)2039 if ChainAlwaysActive(chain) {2040 active = true2041 }2042 nc := colorForStone(stone, colorIdx)2043 colorHex, colorName = nc.hex, nc.name2044 mt := metals[metal]20452046 ringEls := renderRing(mt.hi, mt.lo, tidStr)2047 var pieceMarkup string2048 if stone == "Pearl" {2049 pearl := renderPearlShape(shapeIdx, colorHex, mt.hi, tidStr)2050 overlay, css := applyCascade(pearl, tidStr, nil)2051 pieceMarkup, cascadeCSS = pearl+overlay, css2052 } else {2053 fs := &facetStyle{prof: fireProfileFor(stone, colorName), zone: newPRNG("zone" + tidStr), stone: stone, thumb: thumb}2054 under, facets, over := renderSolid3D(shapeIdx, colorHex, tidStr, fs)2055 overlay, css := applyCascade(facets, tidStr, fs)2056 cascadeCSS = css2057 pieceMarkup = under + `<g fill-opacity=".9">` + facets + `</g>` + overlay + over2058 }2059 floatingPiece := applyMotion(pieceMarkup, tidStr)2060 status := renderStatusBadge(active)2061 serial := renderSerialText(tidStr, mt.hi)2062 labelText := metalLabel[metal].text2063 labelRect := renderMetalLabelRect(mt.hi, mt.lo, 24, 134, 122, 15, 3, tidStr)20642065 inner := `<rect width="` + fnum(canvasW) + `" height="` + fnum(canvasH) + `" fill="#0a0b10"/>` +2066 renderStars(tidStr) + ringEls + floatingPiece + status + serial + labelRect +2067 renderPlateText(chain+" CHAIN", labelText, mt.hi)20682069 svg = `<svg xmlns="http://www.w3.org/2000/svg" width="` + fnum(canvasW) + `" height="` + fnum(canvasH) + `" viewBox="0 0 ` + fnum(canvasW) + ` ` + fnum(canvasH) + `">` + styleBlock() +2070 `<style>` + cascadeCSS + `</style>` + inner + `</svg>`2071 return svg, stone, colorHex, colorName2072}20732074func styleBlock() string {2075 const easeOut = "cubic-bezier(0,0,.3,1)"2076 return `<style>.gt-v,.gt-h,.gt-r,.gt-s{will-change:transform}` +20772078 `.gt-v{animation:gt-v var(--v-dur) ease-in-out var(--v-delay) infinite}` +2079 `.gt-h{animation:gt-h var(--h-dur) ease-in-out var(--h-delay) infinite}` +2080 `.gt-r{animation:gt-r var(--r-dur) ease-in-out var(--r-delay) infinite}` +2081 `.gt-s{animation:gt-s var(--s-dur) ease-in-out var(--s-delay) infinite}` +2082 `.gt-glow{animation:gt-glow var(--glow-dur) ease-in-out var(--glow-delay) infinite}` +2083 `.gt-la{animation:gt-la var(--r-dur) ease-in-out var(--r-delay) infinite}` +2084 `.gt-lb{animation:gt-lb var(--r-dur) ease-in-out var(--r-delay) infinite}` +2085 `@keyframes gt-la{0%,50%,100%{opacity:0}25%{opacity:1}}` +2086 `@keyframes gt-lb{0%,50%,100%{opacity:0}75%{opacity:1}}` +2087 `.gt-badge-pulse{animation:gt-bp 1.8s ` + easeOut + ` infinite}` +2088 `@keyframes gt-v{0%,100%{transform:translateY(2.5px)}50%{transform:translateY(-2.5px)}}` +2089 `@keyframes gt-h{50%{transform:translateX(1.8px)}}` +2090 `@keyframes gt-r{25%{transform:rotate(calc(-1*var(--rot-deg)))}75%{transform:rotate(var(--rot-deg))}}` +2091 `@keyframes gt-s{50%{transform:scale(` + fnum(motionScale) + `)}}` +2092 `.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}}` +2093 `@keyframes gt-glow{50%{opacity:1}}` +2094 `@keyframes gt-bp{from{transform:scale(1);opacity:.8}to{transform:scale(2.25);opacity:0}}` +2095 `</style>`2096}2097Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.