PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/art3d2

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
art3d2
Namespace
g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4
Files
2 (gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/art3d2
gno
0.9

Files (2)

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

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.