package avatars import ( "fmt" "math" "math/bits" "strconv" "strings" ) // MakeAvatar create svg from seed func MakeAvatar(seedString string) string { seed := generateSeed(seedString) if seed&1 == 0 { return femaleAvatar(seed, "") } return maleAvatar(seed, "") } // MakeFemaleAvatar create female svg from seed func MakeFemaleAvatar(seedString string) string { return femaleAvatar(generateSeed(seedString), "") } // MakeMaleAvatar create male svg from seed func MakeMaleAvatar(seedString string) string { return maleAvatar(generateSeed(seedString), "") } /** * Based on TypeScript DiceBear Avatars, which in turn were inspired by 8biticon avatars: * (MIT License, Copyright (c) 2012 Plastic Jam, Copyright (c) 2019 DiceBear) * cf. https://github.com/DiceBear/avatars/blob/master/packages/avatars-male-sprites/src/index.ts */ type lcg struct { seed uint64 } func generateSeed(seedString string) (seed uint64) { for _, c := range []byte(seedString) { seed = bits.RotateLeft64(seed, 8) seed ^= uint64(c) } return seed } func (g *lcg) random() uint32 { /* Linear Congruent Generator, POSIX/glibc [de]rand48 setting, bits [47..0] are output bits */ g.seed = (25214903917*g.seed + 11) % 281474976710656 return uint32(g.seed) } func (g *lcg) binomial(p float64) bool { /* Sample from Binomial distribution with probability p */ var sample = float64(g.random()) * float64(1.0/4294967295.0) return sample > p } func (g *lcg) pickOne(s []string) string { /* Pick one element from list */ var N = uint32(len(s)) return s[g.random()%N] } func (g *lcg) pickOneFloat(s []float64) float64 { /* Pick one element from list - float version*/ var N = uint32(len(s)) return s[g.random()%N] } func (g *lcg) pickAorB(p float64, a string, b string) string { /* Pick a or b with probability p of picking a */ if g.binomial(p) { return a } return b } func linearCongruentialGenerator(seed uint64) *lcg { g := new(lcg) g.seed = seed return g } type rgb struct { r uint8 g uint8 b uint8 a uint8 } type hsv struct { h float64 s float64 v float64 } func f2rgb(r, g, b float64) *rgb { // make rgb from 3 floats c := new(rgb) c.r = uint8(r * 1.0 / 255) c.g = uint8(g * 1.0 / 255) c.b = uint8(b * 1.0 / 255) c.a = 255 return c } func f2hsv(h, s, v float64) *hsv { // make hsv from 3 floats c := new(hsv) c.h = h c.s = s c.v = v return c } func toRgb(s string) *rgb { c := new(rgb) c.a = 255 fmt.Sscanf(s, "#%02x%02x%02x", &c.r, &c.g, &c.b) return c } func (c *rgb) toHsv() *hsv { var h float64 var s float64 var v float64 r := 255.0 * float64(c.r) g := 255.0 * float64(c.g) b := 255.0 * float64(c.b) min := math.Min(math.Min(r, g), b) v = math.Max(math.Max(r, g), b) C := v - min s = 0.0 if v != 0.0 { s = C / v } h = 0.0 if min != v { if v == r { h = math.Mod((g-b)/C, 6.0) } if v == g { h = (b-r)/C + 2.0 } if v == b { h = (r-g)/C + 4.0 } h *= 60.0 if h < 0.0 { h += 360.0 } } return f2hsv(h, s, v) } func (c *hsv) toRgb() *rgb { h := int((c.h / 60)) f := c.h/60 - float64(h) p := c.v * (1 - c.s) q := c.v * (1 - c.s*f) t := c.v * (1 - c.s*(1-f)) switch h { case 6: case 0: return f2rgb(c.v, t, p) case 1: return f2rgb(q, c.v, p) case 2: return f2rgb(p, c.v, t) case 3: return f2rgb(p, q, c.v) case 4: return f2rgb(t, p, c.v) case 5: return f2rgb(c.v, p, q) } return f2rgb(0, 0, 0) } func (c *rgb) brighterThan(ref *rgb, delta float64) *rgb { primary := c.toHsv() secondary := ref.toHsv() if primary.v >= secondary.v+delta { return c } primary.v = secondary.v + delta if primary.v > 360 { primary.v = 360 } return primary.toRgb() } func (c *rgb) darkerThan(ref *rgb, delta float64) *rgb { primary := c.toHsv() secondary := ref.toHsv() if primary.v <= secondary.v-delta { return c } primary.v = secondary.v - delta if primary.v < 0 { primary.v = 0 } return primary.toRgb() } func (c *rgb) brighterOrDarkerThan(ref *rgb, delta float64) *rgb { primary := c.toHsv() secondary := ref.toHsv() if primary.v <= secondary.v { return c.darkerThan(ref, delta) } return c.brighterThan(ref, delta) } func (c *rgb) withAlpha(alpha float64) *rgb { c.a = uint8(alpha * 255) return c } func (c *rgb) html() string { if c.a == 255 { return fmt.Sprintf("#%02x%02x%02x", c.r, c.g, c.b) } return fmt.Sprintf("#%02x%02x%02x%02x", c.r, c.g, c.b, c.a) } func maleAvatar(seed uint64, mood string) string { var g = linearCongruentialGenerator(seed) var skinColor = toRgb(g.pickOne([]string{"#FFDBAC", "#F5CFA0", "#EAC393", "#E0B687", "#CB9E6E", "#B68655", "#A26D3D", "#8D5524"})) var hairColor = toRgb(g.pickOne([]string{"#090806", "#2c222b", "#71635a", "#b7a69e", "#b89778", "#a56b46", "#b55239", "#8d4a43", "#91553d", "#533d32", "#3b3024", "#554838", "#4e433f", "#504444", "#6a4e42", "#a7856a", "#977961"})).brighterOrDarkerThan(skinColor, 17) var eyesColor = toRgb(g.pickOne([]string{"#76778b", "#697b94", "#647b90", "#5b7c8b", "#588387"})) var eyebrowsColor = hairColor.darkerThan(skinColor, 7).darkerThan(hairColor, 10) var mustacheColor = hairColor.darkerThan(skinColor, 7).withAlpha(g.pickOneFloat([]float64{1, 0.75, 0.5})) var mouthColor = toRgb(g.pickOne([]string{"#eec1ad", "#dbac98", "#d29985"})).brighterOrDarkerThan(skinColor, 10) var glassesColor = toRgb(g.pickOne([]string{"#5f705c", "#43677d", "#5e172d", "#ffb67a", "#a04b5d", "#191919", "#323232", "#4b4b4b"})) var clothesColor = toRgb(g.pickOne([]string{"#5bc0de", "#5cb85c", "#428bca", "#03396c", "#005b96", "#6497b1", "#1b85b8", "#5a5255", "#559e83", "#ae5a41", "#c3cb71", "#666547", "#ffe28a"})) var hatColor = toRgb(g.pickOne([]string{"#18293b", "#2e1e05", "#989789", "#3d6ba7", "#517459", "#a62116"})) if mood == "" { mood = g.pickOne([]string{"sad", "happy", "surprised"}) } var mouth string if mood == "sad" { mouth = "" + "" + "" + "" + "" } else if mood == "happy" { mouth = "" + "" + "" + "" } else if mood == "surprised" { mouth = "" + "" } var s = strings.Join([]string{ "", // Head "", // Eyes g.pickOne([]string{ "", "", "", "", "", "", "", "", "", "", "", "", "", }), // Eyebrows g.pickOne([]string{ "", "", "", "", "", "", "", "", "", "", "", "", "", }), // Mustache (50% chance) g.pickAorB(0.5, g.pickOne([]string{ "", "", "", "", }), ""), // Mouth mouth, // Glasses (25% chance) g.pickAorB(0.25, g.pickOne([]string{ "", "", "", "", "", "", }), ""), // Clothes g.pickOne([]string{ "", "", "", "", "", "", "", "", "", "", "", "", "", }), // Hair (95% chance) g.pickAorB(0.95, g.pickOne([]string{ "", "", "", "", "", "", "", "", "", "", "", "", "", }), ""), // Hat (5% chance) g.pickAorB(0.05, g.pickOne([]string{ "", "", "", "", "", "", "", "", "", "", "", "", }), ""), "", }, "") m := []string{ "${skinColor}", skinColor.html(), "${hairColor}", hairColor.html(), "${eyesColor}", eyesColor.html(), "${eyebrowsColor}", eyebrowsColor.html(), "${mustacheColor}", mustacheColor.html(), "${mustacheColorAlpha}", strconv.Itoa(int(mustacheColor.a)), "${mouthColor}", mouthColor.html(), "${glassesColor}", glassesColor.html(), "${clothesColor}", clothesColor.html(), "${hatColor}", hatColor.html(), } return strings.NewReplacer(m...).Replace(s) } func femaleAvatar(seed uint64, mood string) string { var g = linearCongruentialGenerator(seed) var skinColor = toRgb(g.pickOne([]string{"#FFDBAC", "#F5CFA0", "#EAC393", "#E0B687", "#CB9E6E", "#B68655", "#A26D3D", "#8D5524"})) var hairColor = toRgb(g.pickOne([]string{"#090806", "#2c222b", "#71635a", "#b7a69e", "#d6c4c2", "#cabfb1", "#dcd0ba", "#fff5e1", "#e6cea8", "#e5c8a8", "#debc99", "#b89778", "#a56b46", "#b55239", "#8d4a43", "#91553d", "#533d32", "#3b3024", "#554838", "#4e433f", "#504444", "#6a4e42", "#a7856a", "#977961"})).brighterOrDarkerThan(skinColor, 17) var eyesColor = toRgb(g.pickOne([]string{"#76778b", "#697b94", "#647b90", "#5b7c8b", "#588387"})) var eyebrowsColor = hairColor.darkerThan(skinColor, 7).darkerThan(hairColor, 10) var accessoriesColor = toRgb(g.pickOne([]string{"#daa520", "#ffd700", "#eee8aa", "#fafad2", "#d3d3d3", "#a9a9a9"})) var mouthColor = toRgb(g.pickOne([]string{"#dbac98", "#d29985", "#c98276", "#e35d6a", "#e32153", "#de0f0d"})).brighterOrDarkerThan(skinColor, 10) var glassesColor = toRgb(g.pickOne([]string{"#5f705c", "#43677d", "#5e172d", "#ffb67a", "#a04b5d", "#191919", "#323232", "#4b4b4b"})) var clothesColor = toRgb(g.pickOne([]string{"#d11141", "#00b159", "#00aedb", "#f37735", "#ffc425", "#740001", "#ae0001", "#eeba30", "#96ceb4", "#ffeead", "#ff6f69", "#ffcc5c", "#88d8b0"})) var hatColor = toRgb(g.pickOne([]string{"#cc6192", "#2663a3", "#a62116", "#3d8a6b", "#614f8a"})) if mood == "" { mood = g.pickOne([]string{"sad", "happy", "surprised"}) } var mouth string if mood == "sad" { mouth = "" + "" + "" + "" } else if mood == "happy" { mouth = "" + "" + "" + "" + "" + "" + "" } else if mood == "surprised" { mouth = "" + "" } var s = strings.Join([]string{ "", // Head "", // Eyes g.pickOne([]string{ "", "", "", "", "", "", "", "", "", "", "", "", "", }), // Eyebrows g.pickOne([]string{ "", "", "", "", "", "", "", "", "", "", "", "", "", }), // Accessories (15% chance) g.pickAorB(0.15, g.pickOne([]string{ "", "", "", "", }), ""), // Mouth mouth, // Glasses (25% chance) g.pickAorB(0.25, g.pickOne([]string{ "", "", "", "", "", "", "", }), ""), // Clothes g.pickOne([]string{ "", "", "", "", "", "", "", "", "", "", "", "", "", }), // Hair g.pickOne([]string{ "", "", "", "", "", "", "", "", "", "", "", "", "", }), // Hat (5% chance) g.pickAorB(0.05, g.pickOne([]string{ "", "", "", "", "", "", "", "", "", "", "", "", }), ""), "", }, "") m := []string{ "${skinColor}", skinColor.html(), "${hairColor}", hairColor.html(), "${eyesColor}", eyesColor.html(), "${eyebrowsColor}", eyebrowsColor.html(), "${accessoriesColor}", accessoriesColor.html(), "${mouthColor}", mouthColor.html(), "${glassesColor}", glassesColor.html(), "${clothesColor}", clothesColor.html(), "${hatColor}", hatColor.html(), } return strings.NewReplacer(m...).Replace(s) }