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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

art.gno

63.23 Kb · 2140 lines
   1package art3d2
   2
   3import (
   4	"crypto/sha256"
   5	"math"
   6	"strings"
   7
   8	"gno.land/p/nt/ufmt/v0"
   9)
  10
  11const (
  12	canvasW, canvasH = 170.0, 170.0
  13	subjX, subjY     = 85.0, 78.0
  14)
  15
  16func itoa(v int64) string {
  17	if v == 0 {
  18		return "0"
  19	}
  20	neg := v < 0
  21	if neg {
  22		v = -v
  23	}
  24	var b [20]byte
  25	i := len(b)
  26	for v > 0 {
  27		i--
  28		b[i] = byte('0' + v%10)
  29		v /= 10
  30	}
  31	if neg {
  32		i--
  33		b[i] = '-'
  34	}
  35	return string(b[i:])
  36}
  37
  38func fnum(v float64) string { return cnum(v) }
  39
  40func fnumPad(v float64) string {
  41	neg := v < 0
  42	if neg {
  43		v = -v
  44	}
  45	scaled := int64(v*100 + 0.5)
  46	whole := scaled / 100
  47	frac := scaled % 100
  48	fracStr := itoa(frac)
  49	if frac < 10 {
  50		fracStr = "0" + fracStr
  51	}
  52	s := itoa(whole) + "." + fracStr
  53	if neg {
  54		return "-" + s
  55	}
  56	return s
  57}
  58
  59func 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 s
  70}
  71
  72func fnum1(v float64) string {
  73	neg := v < 0
  74	if neg {
  75		v = -v
  76	}
  77	scaled := int64(v*10 + 0.5)
  78	s := itoa(scaled/10) + "." + itoa(scaled%10)
  79	if neg {
  80		return "-" + s
  81	}
  82	return s
  83}
  84
  85func pctnum(v float64) string { return fnum(v) }
  86
  87func hexToRGB(hex string) (int, int, int) {
  88	h := hex
  89	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, b
  96}
  97
  98func hexByte(s string) int {
  99	v := 0
 100	for i := 0; i < len(s); i++ {
 101		c := s[i]
 102		var d int
 103		switch {
 104		case c >= '0' && c <= '9':
 105			d = int(c - '0')
 106		case c >= 'a' && c <= 'f':
 107			d = int(c-'a') + 10
 108		case c >= 'A' && c <= 'F':
 109			d = int(c-'A') + 10
 110		}
 111		v = v*16 + d
 112	}
 113	return v
 114}
 115
 116func rgbToHex(r, g, b int) string {
 117	return "#" + hexDigit2(r) + hexDigit2(g) + hexDigit2(b)
 118}
 119
 120func hexDigit2(v int) string {
 121	if v < 0 {
 122		v = 0
 123	}
 124	if v > 255 {
 125		v = 255
 126	}
 127	const digits = "0123456789abcdef"
 128	return string(digits[v/16]) + string(digits[v%16])
 129}
 130
 131func clampByte(v float64) int {
 132	iv := int(v)
 133	if iv < 0 {
 134		return 0
 135	}
 136	if iv > 255 {
 137		return 255
 138	}
 139	return iv
 140}
 141
 142func 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}
 149
 150func 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}
 158
 159func 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)*fb
 164	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)*fb
 165	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)*fb
 166	return rgbToHex(clampByte(nr), clampByte(ng), clampByte(nb))
 167}
 168
 169func 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 := lo
 174		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 out
 180}
 181
 182type prng struct {
 183	seed string
 184	n    int
 185}
 186
 187func newPRNG(seed string) *prng { return &prng{seed: seed} }
 188
 189func (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 uint64
 194	for i := 0; i < 8; i++ {
 195		v = v<<8 | uint64(sum[i])
 196	}
 197	return v
 198}
 199
 200func (p *prng) floatIn(lo, hi float64) float64 {
 201	frac := float64(p.next()%1000000) / 1000000.0
 202	return lo + frac*(hi-lo)
 203}
 204
 205func (p *prng) intn(n int) int {
 206	if n <= 1 {
 207		return 0
 208	}
 209	return int(p.next() % uint64(n))
 210}
 211
 212type pt struct{ x, y float64 }
 213
 214func 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}
 224
 225func 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 s
 234}
 235
 236type gstop struct {
 237	pct   float64
 238	color string
 239	op    float64
 240}
 241
 242type gramp struct {
 243	x1, y1, x2, y2 float64
 244	stops          []gstop
 245}
 246
 247func (g gramp) sample(px, py float64) (string, float64) {
 248	dx, dy := g.x2-g.x1, g.y2-g.y1
 249	t := ((px-g.x1)*dx + (py-g.y1)*dy) / (dx*dx + dy*dy) * 100
 250	if t < 0 {
 251		t = 0
 252	}
 253	if t > 100 {
 254		t = 100
 255	}
 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)*f
 261		}
 262	}
 263	last := g.stops[len(g.stops)-1]
 264	return last.color, last.op
 265}
 266
 267func centroid(poly []pt) (float64, float64) {
 268	cx, cy := 0.0, 0.0
 269	for _, p := range poly {
 270		cx += p.x
 271		cy += p.y
 272	}
 273	return cx / float64(len(poly)), cy / float64(len(poly))
 274}
 275
 276type facetStyle struct {
 277	flash    gramp
 278	ringCent []pt
 279	prof     fireProfile
 280	zone     *prng // per-facet hue drift and shade jitter, seeded per token
 281	stone    string // which crystal habit a Raw Crystal takes
 282	thumb bool
 283	glints string
 284}
 285
 286type fireProfile struct {
 287	sat, hit  float64
 288	pinpoints int
 289	shadow    string
 290	bodyMix   float64
 291	neutral   string
 292}
 293
 294func 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}
 324
 325const fireZoneDeg = 7.0
 326
 327type rgbI struct{ r, g, b int }
 328
 329func toRGBI(hex string) rgbI { r, g, b := hexToRGB(hex); return rgbI{r, g, b} }
 330
 331func (c rgbI) hex() string { return rgbToHex(c.r, c.g, c.b) }
 332
 333func 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}
 340
 341func 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)*fb
 345	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)*fb
 346	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)*fb
 347	return rgbI{clampByte(nr), clampByte(ng), clampByte(nb)}
 348}
 349
 350func 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}
 355
 356var whiteI = rgbI{255, 255, 255}
 357
 358func darken(hex, shadow string, amt float64) string {
 359	if amt > 0.95 {
 360		amt = 0.95
 361	}
 362	return mixHex(hex, shadow, amt)
 363}
 364
 365const prismFlash = true
 366
 367func 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 r
 372	}
 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 r
 381}
 382
 383type v3 struct{ x, y, z float64 }
 384
 385func 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 a
 393	}
 394	return v3{a.x / l, a.y / l, a.z / l}
 395}
 396
 397func sgnPow(v, e float64) float64 {
 398	if v < 0 {
 399		return -math.Pow(-v, e)
 400	}
 401	return math.Pow(v, e)
 402}
 403
 404type outlineFn func(t float64) (float64, float64)
 405
 406func circleOutline(t float64) (float64, float64) {
 407	a := 2 * math.Pi * t
 408	return math.Cos(a), math.Sin(a)
 409}
 410
 411func superOutline(p float64) outlineFn {
 412	return func(t float64) (float64, float64) {
 413		a := 2 * math.Pi * t
 414		return sgnPow(math.Cos(a), 2/p), sgnPow(math.Sin(a), 2/p)
 415	}
 416}
 417
 418func 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)*fr
 427	}
 428}
 429
 430func 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}
 436
 437func pearOutline(t float64) (float64, float64) {
 438	a := 2 * math.Pi * t
 439	c := math.Cos(a)
 440	return c*0.92 + 0.08, 0.7 * math.Sin(a) * (1 - 0.46*c)
 441}
 442
 443func marquiseOutline(t float64) (float64, float64) {
 444	a := 2 * math.Pi * t
 445	return math.Cos(a), 0.52 * sgnPow(math.Sin(a), 1.35)
 446}
 447
 448func heartOutline(t float64) (float64, float64) {
 449	a := 2 * math.Pi * t
 450	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)) / 17
 452	return 0.88*sn*sn*sn + 0.12*sn, (y + 0.1576) / 0.7894
 453}
 454
 455func trillionOutline(t float64) (float64, float64) {
 456	vs := []pt{{0, 1}, {-0.866, -0.5}, {0.866, -0.5}}
 457	f := t * 3
 458	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) * bulge
 463}
 464
 465type cutSpec struct {
 466	n         int
 467	brilliant bool
 468	outline   outlineFn
 469	rx, ry    float64
 470}
 471
 472var ShapeNames = []string{
 473	"Round Brilliant", "Oval", "Cushion", "Emerald Cut", "Asscher",
 474	"Radiant", "Princess Cut", "Pear", "Marquise", "Trillion",
 475	"Heart", "Rose Cut", "Raw Crystal",
 476}
 477
 478const (
 479	shapeHeart = 10
 480	shapeRose  = 11
 481	shapeRaw   = 12
 482)
 483
 484const stoneScale = 0.78
 485
 486func cutFor(shapeIdx int) cutSpec {
 487	c := cutForUnscaled(shapeIdx)
 488	c.rx, c.ry = c.rx*stoneScale, c.ry*stoneScale
 489	return c
 490}
 491
 492func 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 buildRaw
 518	}
 519	return cutSpec{8, true, circleOutline, 27, 27}
 520}
 521
 522func 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}
 533
 534const (
 535	kCrown = iota
 536	kTable
 537	kPav
 538)
 539
 540type face struct {
 541	p    []v3
 542	kind int
 543}
 544
 545func 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 out
 552}
 553
 554func buildCut(c cutSpec) (faces []face, girdle []v3) {
 555	base := c.rx
 556	if c.ry < base {
 557		base = c.ry
 558	}
 559	hc, hp := 0.34*base, 0.9*base
 560	n := c.n
 561	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) % n
 569			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, G
 576	}
 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) % n
 588			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) % n
 595			faces = append(faces, face{[]v3{r[0][k], r[0][k1], r[1][k1], r[1][k]}, kPav})
 596		}
 597	}
 598	return faces, G
 599}
 600
 601func buildRose(c cutSpec) (faces []face, girdle []v3) {
 602	n := c.n
 603	base := math.Min(c.rx, c.ry)
 604	h := 0.62 * base
 605	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) % n
 610		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, G
 621}
 622
 623func 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/2
 627		out[k] = v3{x, ry * math.Cos(a), rz * math.Sin(a)}
 628	}
 629	return out
 630}
 631
 632func bandX(a, b []v3) []face {
 633	var fs []face
 634	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 fs
 639}
 640
 641func capX(r []v3) face { return face{r, kCrown} }
 642
 643func 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 = kPav
 647		for j, p := range fs[i].p {
 648			y, z := p.y*cx-p.z*sx, p.y*sx+p.z*cx
 649			fs[i].p[j] = v3{p.x*cz - y*sz, p.x*sz + y*cz, z}
 650		}
 651	}
 652	return fs
 653}
 654
 655func buildRaw(stone string) (faces []face, girdle []v3) {
 656	deg := math.Pi / 180
 657	switch stone {
 658	case "Diamond":
 659		a, b, h := 25.0*stoneScale, 22.0*stoneScale, 23.0*stoneScale
 660		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) % 4
 664			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), nil
 668	case "Sapphire":
 669		L, R := 26.0*stoneScale, 16.0*stoneScale
 670		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), nil
 684	}
 685	if stone == "Quartz" {
 686		L, R := 27.0*stoneScale, 12.5*stoneScale
 687		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), nil
 700	}
 701	L, ry, rz := 26.0*stoneScale, 16.0*stoneScale, 13.0*stoneScale
 702	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), nil
 717}
 718
 719const viewTilt = 61.0
 720
 721const (
 722	pieceCY   = subjY + 1.5
 723)
 724
 725func n1(v float64) string {
 726	neg := v < 0
 727	if neg {
 728		v = -v
 729	}
 730	t := int64(v*10 + 0.5)
 731	w, f := t/10, t%10
 732	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 "-" + s
 742	}
 743	return s
 744}
 745
 746func 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)/10
 749	for i := 1; i < len(ps); i++ {
 750		qx, qy := math.Round(ps[i].x*10)/10, math.Round(ps[i].y*10)/10
 751		dx, dy := n1(qx-px), n1(qy-py)
 752		if i > 1 && dx[0] != '-' {
 753			s += " "
 754		}
 755		s += dx
 756		if dy[0] != '-' {
 757			s += " "
 758		}
 759		s += dy
 760		px, py = qx, qy
 761	}
 762	return s + "z"
 763}
 764
 765func 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}
 771
 772func convexHull(ps []pt) []pt {
 773	n := len(ps)
 774	idx := make([]int, n)
 775	for i := range idx {
 776		idx[i] = i
 777	}
 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*w
 785			if mid > n {
 786				mid = n
 787			}
 788			if hi > n {
 789				hi = n
 790			}
 791			i, j, k := lo, mid, lo
 792			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, idx
 814	}
 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 []pt
 817	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 h
 832}
 833
 834func polyArea(ps []pt) float64 {
 835	a := 0.0
 836	for i := range ps {
 837		j := (i + 1) % len(ps)
 838		a += ps[i].x*ps[j].y - ps[j].x*ps[i].y
 839	}
 840	if a < 0 {
 841		return -a / 2
 842	}
 843	return a / 2
 844}
 845
 846func clipHalf(ps []pt, ox, oy, nx, ny float64) []pt {
 847	if len(ps) == 0 {
 848		return ps
 849	}
 850	d := func(q pt) float64 { return (q.x-ox)*nx + (q.y-oy)*ny }
 851	var out []pt
 852	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 out
 864}
 865
 866func 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.x
 870		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, -ny
 874		}
 875		ps = clipHalf(ps, a.x, a.y, nx, ny)
 876		if len(ps) < 3 {
 877			return nil
 878		}
 879	}
 880	return ps
 881}
 882
 883func 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}
 888
 889func renderSolid3D(shapeIdx int, base, salt string, fs *facetStyle) (under, facets, over string) {
 890	c := cutFor(shapeIdx)
 891	var faces []face
 892	var girdle []v3
 893	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 == shapeRaw
 902
 903	sh := fs.prof.shadow
 904	if fs.prof.neutral != "" {
 905		base = fs.prof.neutral
 906	}
 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 = 0
 915		}
 916		if lum > 1 {
 917			lum = 1
 918		}
 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() }
 925
 926	yaw := fs.zone.floatIn(-13, 13) * math.Pi / 180
 927	th := viewTilt * math.Pi / 180
 928	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*cy
 932		return v3{x1, y1*ct + p.z*stt, p.z*ct - y1*stt}
 933	}
 934	ex0, ey0, ex1, ey1 := 1e9, 1e9, -1e9, -1e9
 935	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)/2
 943	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	}
 951
 952	type drawn struct {
 953		pts   []pt
 954		depth float64
 955		fill  string
 956		kind  int
 957		lum   float64
 958		nv                v3
 959		jit, drift, thick float64
 960		ext               bool
 961	}
 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) + jit
 966		if ext {
 967			lum *= 0.3
 968		}
 969		if kind == kTable {
 970			lum = 0.36 + 0.26*lam
 971		}
 972		if kind == kPav {
 973			lum = 0.22 + 0.5*math.Max(0, vdot(vscale(nv, -1), L)) + jit
 974		}
 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.6
 977		}
 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.sat
 984			t := math.Atan2(nv.y, nv.x)/math.Pi + drift/40
 985			col = mixI(col, spectrumAtI(t), amt)
 986		}
 987		return hex3I(hueRotateI(col, drift)), lum
 988	}
 989	var back, front, pav []drawn
 990	var all []pt
 991	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.z
1013			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 = 1
1019		}
1020		thick := 1 - rN
1021		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.28
1024			drift := fs.zone.floatIn(-fireZoneDeg, fireZoneDeg)
1025			d.nv, d.jit, d.drift, d.thick, d.ext = nv, jit, drift, thick, ext
1026			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)) + jit
1034			d.fill = hex3(mixHex(tone(lum), deep, 0.34*thick*thick))
1035			d.lum = lum
1036			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)
1049
1050	hull := convexHull(all)
1051	if shapeIdx == shapeHeart {
1052		hull = nil
1053		for _, g := range girdle {
1054			hull = append(hull, scr(view(g)))
1055		}
1056	}
1057	var underB, facetsB strings.Builder
1058	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>`)
1065
1066	nCrown := 0
1067	for _, d := range front {
1068		if d.kind == kCrown {
1069			nCrown++
1070		}
1071	}
1072	candP := 1.0
1073	if nCrown > fireMaxFacets {
1074		candP = float64(fireMaxFacets) / float64(nCrown) * 1.25
1075	}
1076	nCand := 0
1077	nSplit := 0
1078	if raw || fs.thumb {
1079		candP = 0
1080	}
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			continue
1085		}
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, -1e9
1092				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, -1e9
1099				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)/2
1105				var mosB strings.Builder
1106				nRefl := 0
1107				for _, b := range pav {
1108					if nRefl >= 8 {
1109						break
1110					}
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						continue
1118					}
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			continue
1127		}
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			continue
1134		}
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				continue
1143			}
1144		}
1145		facetsB.WriteString(`<path d="` + relPath(d.pts) + `" fill="` + d.fill + `"/>`)
1146	}
1147
1148	_, _, _, _, rotDeg := motionPhases(salt)
1149	type cand struct {
1150		i     int
1151		fill  string
1152		delta float64
1153	}
1154	for si, sgn := range []float64{1, -1} {
1155		if fs.thumb {
1156			break
1157		}
1158		a := -sgn * rotDeg * lightSwing * math.Pi / 180
1159		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 []cand
1162		for i, d := range front {
1163			if d.kind == kTable {
1164				continue
1165			}
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			continue
1181		}
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	}
1192
1193	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" + sa
1202			}
1203			lastG = n1(pb.x) + " " + n1(pb.y)
1204			gd += " " + lastG
1205		}
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(), over
1209}
1210
1211const dispersionMix = 0.32
1212
1213var spectrum = []string{"#ff4040", "#ff9a1f", "#ffe81f", "#35ff7a", "#22ccff", "#5566ff", "#c04dff"}
1214
1215var spectrumI = func() []rgbI {
1216	out := make([]rgbI, len(spectrum))
1217	for i, h := range spectrum {
1218		out[i] = toRGBI(h)
1219	}
1220	return out
1221}()
1222
1223func 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}
1229
1230func 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}
1236
1237const (
1238	lightSwing     = 2.2
1239	lightMaxFacets = 12
1240)
1241
1242func 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 h
1248	}
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)) / 255
1252	if d < 0 {
1253		return -d
1254	}
1255	return d
1256}
1257
1258const fireMaxFacets = 12
1259
1260const starfield = true
1261
1262func 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][]star
1269	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-subjY
1273		if r := math.Sqrt(dx*dx + dy*dy); r > 43 && r < 58 {
1274			continue
1275		}
1276		lvl := 0 // most stars are faint; a few are bright
1277		switch {
1278		case k >= 18:
1279			lvl = 3
1280		case k >= 14:
1281			lvl = 2
1282		case k >= 8:
1283			lvl = 1
1284		}
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.Builder
1290	b.WriteString(`<g stroke="#fff" stroke-linecap="round" fill="none">`)
1291	var tw []star
1292	for l := 0; l < 4; l++ {
1293		if len(byLevel[l]) == 0 {
1294			continue
1295		}
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				continue
1301			}
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}
1315
1316func 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)) / 255
1319	shadow := "#4a4038"
1320	if lum < 0.4 {
1321		shadow = "#000000"
1322	}
1323	hi := base
1324	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}
1355
1356func pearlLayers(shape, gid, between string) string {
1357	i := strings.Index(shape, ` fill="`)
1358	if i < 0 {
1359		return shape
1360	}
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}
1364
1365func 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)/255
1368	if lum < 0.4 {
1369		return 0.55
1370	}
1371	return 2.0
1372}
1373
1374func 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}
1386
1387func pearlSweepOverlay(salt, base, shapeGeom string, w, h float64) string {
1388	rnd := newPRNG(salt + "-sweep")
1389	const passes = 3
1390	total := 15.0 + rnd.floatIn(0, 9)
1391
1392	durs := make([]float64, passes)
1393	sumDur := 0.0
1394	for i := 0; i < passes; i++ {
1395		durs[i] = 2.4 + rnd.floatIn(0, 2.2)
1396		sumDur += durs[i]
1397	}
1398	gapTotal := total - sumDur
1399	if gapTotal < float64(passes)*0.5 {
1400		gapTotal = float64(passes) * 0.5
1401	}
1402	weights := make([]float64, passes)
1403	sumW := 0.0
1404	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 * gapTotal
1411	}
1412	starts := make([]float64, passes)
1413	t := gaps[0]
1414	for i := 0; i < passes; i++ {
1415		starts[i] = t
1416		t += durs[i]
1417		if i < passes-1 {
1418			t += gaps[i+1]
1419		}
1420	}
1421
1422	travel := w * 1.05
1423	clipID := "clp" + salt
1424	gradID := "swp" + salt
1425	off, on1, on2, out := fnum(-travel), fnum(-travel*0.5), fnum(travel*0.5), fnum(travel)
1426
1427	var defs strings.Builder
1428	defs.WriteString(`<defs><clipPath id="` + clipID + `">` + shapeGeom + `</clipPath>`)
1429	defs.WriteString(pearlSweepGradient(gradID, base))
1430
1431	var marks strings.Builder
1432	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 * 100
1436		durPct := durs[i] / total * 100
1437		rampPct := durPct * 0.32
1438		if rampPct > 6 {
1439			rampPct = 6
1440		}
1441		p0, p1, p2, p3 := startPct, startPct+rampPct, startPct+durPct-rampPct, startPct+durPct
1442		kf := "swp" + salt + "_" + ufmt.Sprintf("%d", i)
1443
1444		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>`)
1456
1457		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}
1465
1466func pearlRound(base, metal, salt string) string {
1467	gid := "prl" + salt
1468	r := 24.0 * stoneScale
1469	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, "") + sweep
1473}
1474
1475func pearlOval(base, metal, salt string) string {
1476	gid := "prl" + salt
1477	rx, ry := 18.0*stoneScale, 25.0*stoneScale
1478	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, "") + sweep
1482}
1483
1484func pearlButton(base, metal, salt string) string {
1485	gid := "prl" + salt
1486	rx, ry := 25.0*stoneScale, 18.0*stoneScale
1487	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, "") + sweep
1491}
1492
1493func pearlDrop(base, metal, salt string) string {
1494	gid := "prl" + salt
1495	w, h := 32.0*stoneScale, 42.0*stoneScale
1496	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, "") + sweep
1503}
1504
1505func pearlBaroqueOutline(seed string) []pt {
1506	rnd := newPRNG(seed)
1507	n := 10
1508	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 pts
1515}
1516
1517func 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}
1529
1530func pearlBaroque(base, metal, salt string) string {
1531	gid := "prl" + salt
1532	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, "") + sweep
1537}
1538
1539func pearlCircled(base, metal, salt string) string {
1540	gid := "prl" + salt
1541	r := 24.0 * stoneScale
1542	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) + sweep
1551}
1552
1553var PearlShapeNames = []string{"Round", "Oval", "Button", "Drop", "Baroque", "Circled"}
1554
1555func 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}
1571
1572func renderRing(metalHi, metalLo, salt string) string {
1573	gid := "ring" + salt
1574	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>`
1580
1581	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>`
1587
1588	const rOut = 46.0
1589	const coreHalf = 0.6
1590	const outwardBloom = 0.9
1591	glowR := rOut + outwardBloom + 3
1592	pct := func(v float64) float64 {
1593		p := v / glowR * 100
1594		if p < 0 {
1595			p = 0
1596		}
1597		if p > 100 {
1598			p = 100
1599		}
1600		return p
1601	}
1602	coreColor := lighten(metalHi, 0.25)
1603	donutStops := func(inwardBloom, peakOp float64, gid string) string {
1604		bloomOp := peakOp * 0.35
1605		midOp := peakOp * 0.7
1606		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	}
1617
1618	rnd := newPRNG("energy" + salt)
1619	gidA, gidB := "glowA"+salt, "glowB"+salt
1620	dur := rnd.floatIn(2.13, 3.2)
1621	phase := rnd.floatIn(0, dur)
1622
1623	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)
1625
1626	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>`
1631
1632	return `<defs>` + defs + `</defs>` + outer + energy
1633}
1634
1635const (
1636	motionVDur, motionHDur, motionRDur, motionSDur = 3.4, 4.66, 5.81, 6.56
1637	motionRotMin, motionRotMax                     = 4.0, 8.0
1638	motionScale                                    = 1.09
1639)
1640
1641func 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	return
1649}
1650
1651func applyMotion(content, salt string) string {
1652	vphase, hphase, rphase, sphase, rotDeg := motionPhases(salt)
1653
1654	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}
1662
1663const dotX, dotY = subjX + 50, subjY + 42
1664
1665func 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}
1672
1673type facetSpan struct {
1674	start, end int
1675	ringMember bool
1676}
1677
1678func scanFacetSpans(markup string) []facetSpan {
1679	var spans []facetSpan
1680	i := 0
1681	for {
1682		rel := strings.Index(markup[i:], "<")
1683		if rel < 0 {
1684			break
1685		}
1686		start := i + rel
1687		relEnd := strings.Index(markup[start:], "/>")
1688		if relEnd < 0 {
1689			break
1690		}
1691		end := start + relEnd + 2
1692		tag := markup[start:end]
1693		i = end
1694		isFacetTag := strings.HasPrefix(tag, "<path") || strings.HasPrefix(tag, "<circle") || strings.HasPrefix(tag, "<polygon")
1695		if !isFacetTag {
1696			continue
1697		}
1698		spans = append(spans, facetSpan{start: start, end: end, ringMember: strings.Contains(tag, `class="f"`)})
1699	}
1700	return spans
1701}
1702
1703const (
1704	fireSweepAxis = -35.0 * math.Pi / 180
1705	fireSweepSpan = 1.6 // seconds a sweep takes to cross the stone
1706)
1707
1708func 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 * motionRDur
1716	axis := fireSweepAxis + rnd.floatIn(-0.3, 0.3)
1717	dx, dy := math.Cos(axis), math.Sin(axis)
1718
1719	ka, kb := "fa"+tidStr, "fb"+tidStr
1720	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}}`
1728
1729	var out strings.Builder
1730	type glintAt struct {
1731		x, y, delay float64
1732		rule        string
1733	}
1734	var cands []glintAt
1735	ringIdx := -1
1736	for _, span := range spans {
1737		if !span.ringMember {
1738			continue
1739		}
1740		ringIdx++
1741		if ringIdx >= len(fs.ringCent) {
1742			break
1743		}
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 := ka
1748		if rnd.floatIn(0, 1) < 0.5 {
1749			name, pos = kb, -pos
1750		}
1751		delay := rphase - 0.25*motionRDur + period - pos*fireSweepSpan/2
1752		tag := markup[span.start:span.end]
1753		geomEnd := strings.Index(tag, `" fill=`)
1754		if geomEnd < 0 {
1755			continue
1756		}
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 := 3
1763		if fs.prof.sat < 0.6 {
1764			n = 2 // quartz, topaz: less fire, fewer stars
1765		}
1766		ga, gb := "ga"+tidStr, "gb"+tidStr
1767		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 * stoneScale
1777		w := 0.9 * stoneScale
1778		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.Builder
1781		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] = true
1788			c := cands[i]
1789			cls := "ga" + tidStr
1790			if c.rule == kb {
1791				cls = "gb" + tidStr
1792			}
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(), css
1799}
1800
1801func renderMetalLabelRect(metalHi, metalLo string, x, y, w, h, rx float64, salt string) string {
1802	gid := "lg" + salt
1803	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.9
1810	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" + salt
1816	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 := 0
1820	for k := 0; k < len(salt); k++ {
1821		seed += int(salt[k])
1822	}
1823	heads, slots := "", ""
1824	n := 0
1825	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 / 180
1829			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 + rivets
1836}
1837
1838const (
1839	plateFont    = `'DIN Alternate',Bahnschrift,'Roboto Condensed',sans-serif`
1840	plateSize    = 8.6
1841	plateSpacing = 1.1
1842	plateMaxW    = 100.0
1843)
1844
1845func 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}
1854
1855func 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}
1858
1859type namedColor struct{ hex, name string }
1860
1861var stoneByChain = map[string]string{
1862	"TOPAZ-1":    "Topaz",
1863	"SAPPHIRE-1": "Sapphire",
1864	"PEARL-1":    "Pearl",
1865	"GNOLAND-1": "Diamond",
1866}
1867
1868var 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}
1892
1893var 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}
1897
1898const defaultColorWeight = 5
1899
1900func colorWeight(stone string, idx int) int {
1901	w, ok := colorWeights[stone]
1902	if !ok {
1903		return 1
1904	}
1905	if idx < len(w) {
1906		return w[idx]
1907	}
1908	return defaultColorWeight
1909}
1910
1911func PickColorIndex(stone string, roll uint64) int {
1912	n := ColorPaletteSize(stone)
1913	total := 0
1914	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 i
1922		}
1923	}
1924	return n - 1
1925}
1926
1927func ColorProbability(stone string, idx int) float64 {
1928	n := ColorPaletteSize(stone)
1929	total := 0
1930	for i := 0; i < n; i++ {
1931		total += colorWeight(stone, i)
1932	}
1933	return float64(colorWeight(stone, idx)) / float64(total)
1934}
1935
1936func ColorRarityCSV(stone string) string {
1937	n := ColorPaletteSize(stone)
1938	total := 0
1939	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 / total
1945		out[i] = colorForStone(stone, i).name + ":" + ufmt.Sprintf("%d.%d", pm/10, pm%10)
1946	}
1947	return strings.Join(out, ";")
1948}
1949
1950func ChainAlwaysActive(chain string) bool { return chain == "GNOLAND-1" }
1951
1952var stoneNames = []string{"Sapphire", "Topaz", "Pearl", "Diamond", "Quartz"}
1953
1954func registerStoneName(stone string) {
1955	for _, s := range stoneNames {
1956		if s == stone {
1957			return
1958		}
1959	}
1960	stoneNames = append(stoneNames, stone)
1961}
1962
1963func SetStoneForChain(chainName, stone string) {
1964	stoneByChain[chainName] = stone
1965	registerStoneName(stone)
1966}
1967
1968func AddStoneColor(stone, hex, name string) {
1969	stonePalettes[stone] = append(stonePalettes[stone], namedColor{hex: hex, name: name})
1970	registerStoneName(stone)
1971}
1972
1973func StoneNamesCSV() string {
1974	return strings.Join(stoneNames, ";")
1975}
1976
1977func 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.name
1982	}
1983	return strings.Join(pairs, ";")
1984}
1985
1986func MetalToneHex(metalName string) string {
1987	t, ok := metals[metalName]
1988	if !ok {
1989		return ""
1990	}
1991	return t.hi + ";" + t.lo
1992}
1993
1994func StoneForChain(chain string) string {
1995	if s, ok := stoneByChain[chain]; ok {
1996		return s
1997	}
1998	return "Quartz"
1999}
2000
2001func 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 = 0
2008	}
2009	if colorIdx >= len(palette) {
2010		colorIdx = len(palette) - 1
2011	}
2012	return palette[colorIdx]
2013}
2014
2015func ColorPaletteSize(stone string) int {
2016	if n := len(stonePalettes[stone]); n > 0 {
2017		return n
2018	}
2019	return len(stonePalettes["Quartz"])
2020}
2021
2022func FindShapeIndex(stone, name string) (idx int, ok bool) {
2023	names := ShapeNames
2024	if stone == "Pearl" {
2025		names = PearlShapeNames
2026	}
2027	for i, s := range names {
2028		if s == name {
2029			return i, true
2030		}
2031	}
2032	return -1, false
2033}
2034
2035func 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, true
2044		}
2045	}
2046	return -1, false
2047}
2048
2049type metalTone struct{ hi, lo string }
2050
2051var 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}
2058
2059var MetalNames = []string{"Gold", "Silver", "Bronze", "Platinum", "Obsidian"}
2060
2061type metalLabelTone struct{ bg, text string }
2062
2063var 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}
2070
2071func RenderTokenThumbSVG(chain, metal string, shapeIdx, colorIdx int, active bool, tidStr string) string {
2072	svg, _, _, _ := renderToken(chain, metal, shapeIdx, colorIdx, active, tidStr, true)
2073	return svg
2074}
2075
2076func 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}
2079
2080func renderToken(chain, metal string, shapeIdx, colorIdx int, active bool, tidStr string, thumb bool) (svg string, stone string, colorHex string, colorName string) {
2081	var cascadeCSS string
2082	stone = StoneForChain(chain)
2083	if ChainAlwaysActive(chain) {
2084		active = true
2085	}
2086	nc := colorForStone(stone, colorIdx)
2087	colorHex, colorName = nc.hex, nc.name
2088	mt := metals[metal]
2089
2090	ringEls := renderRing(mt.hi, mt.lo, tidStr)
2091	var pieceMarkup string
2092	if stone == "Pearl" {
2093		pearl := renderPearlShape(shapeIdx, colorHex, mt.hi, tidStr)
2094		overlay, css := applyCascade(pearl, tidStr, nil)
2095		pieceMarkup, cascadeCSS = pearl+overlay, css
2096	} 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 = css
2101		pieceMarkup = under + `<g fill-opacity=".9">` + facets + `</g>` + overlay + over + fs.glints
2102	}
2103	floatingPiece := applyMotion(pieceMarkup, tidStr)
2104	status := renderStatusBadge(active)
2105	serial := renderSerialText(tidStr, mt.hi)
2106	labelText := metalLabel[metal].text
2107	labelRect := renderMetalLabelRect(mt.hi, mt.lo, 24, 134, 122, 15, 3, tidStr)
2108
2109	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)
2112
2113	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, colorName
2116}
2117
2118func 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}` +
2121
2122		`.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}