From 974ac9e18292ebd90c7660f5dc77a5e21e64a578 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 13 Jan 2026 07:36:07 +0000 Subject: [PATCH 1/4] feat: Add multi-layered foreground avatars Implements multi-layered foregrounds for avatars. - Updates `options` struct to support multiple foreground colors and layers. - Adds `WithLayers` and `WithLayerColor` options. - Updates `Make` function to render multiple layers with different hashes. - Updates example code to demonstrate new features. - Updates tests to verify multi-layered rendering logic. --- example/main.go | 18 +++++++- goavatar.go | 113 +++++++++++++++++++++++++++++++++-------------- goavatar_test.go | 41 ++++++++++++++--- 3 files changed, 131 insertions(+), 41 deletions(-) diff --git a/example/main.go b/example/main.go index a47cc3f..e13c347 100644 --- a/example/main.go +++ b/example/main.go @@ -39,7 +39,6 @@ func main() { goavatar.WithSize(50), // Set custom image widthxheight if size is less then 64 this will go to default (default is 64) goavatar.WithBgColor(170, 120, 10, 255), // Change background color (default is light gray) goavatar.WithFgColor(255, 255, 255, 255), // Change foreground color (default is extracted from hash) - ) // Generate an avatar using default settings @@ -54,8 +53,23 @@ func main() { opts = append(opts, goavatar.WithGridSize(13)) image6 := goavatar.Make("nice__user__name", opts...) + // Generate multi-layered avatar (2 layers) + image7 := goavatar.Make("MultiLayer2", + goavatar.WithSize(512), + goavatar.WithLayers(2), + ) + + // Generate multi-layered avatar (3 layers) with custom colors for each layer + image8 := goavatar.Make("MultiLayer3Custom", + goavatar.WithSize(512), + goavatar.WithLayers(3), + goavatar.WithLayerColor(0, 255, 0, 0, 255), // Red + goavatar.WithLayerColor(1, 0, 255, 0, 255), // Green + goavatar.WithLayerColor(2, 0, 0, 255, 255), // Blue + ) + // append all the images into the list - imgSlice = append(imgSlice, image1, image2, image3, image4, image5, image6) + imgSlice = append(imgSlice, image1, image2, image3, image4, image5, image6, image7, image8) // loop through the image slice and save the images for i, img := range imgSlice { diff --git a/goavatar.go b/goavatar.go index 8abf7ea..1618027 100644 --- a/goavatar.go +++ b/goavatar.go @@ -5,18 +5,20 @@ import ( "encoding/hex" "image" "image/color" + "image/draw" "math" ) -// option contains the configuration for the avatar generator. +// options contains the configuration for the avatar generator. type options struct { size int gridSize int bgColor color.RGBA - fgColor color.RGBA + fgColors []color.RGBA + layers int } -// optFunc is a function that applies an option to the options struct. +// OptFunc is a function that applies an option to the options struct. type OptFunc func(*options) // WithSize sets the width and height of the avatar minimum 64x64. @@ -47,19 +49,41 @@ func WithBgColor(r, g, b, a uint8) OptFunc { } // WithFgColor sets the foreground color of the avatar. +// It sets the first layer's color. func WithFgColor(r, g, b, a uint8) OptFunc { return func(o *options) { - o.fgColor = color.RGBA{r, g, b, a} + o.fgColors = []color.RGBA{{r, g, b, a}} + } +} + +// WithLayers sets the number of foreground layers (1-3). +func WithLayers(n int) OptFunc { + return func(o *options) { + if n >= 1 && n <= 3 { + o.layers = n + } + } +} + +// WithLayerColor sets the color for a specific layer index (0-based). +func WithLayerColor(layerIndex int, r, g, b, a uint8) OptFunc { + return func(o *options) { + // Expand slice if needed + for len(o.fgColors) <= layerIndex { + o.fgColors = append(o.fgColors, color.RGBA{}) + } + o.fgColors[layerIndex] = color.RGBA{r, g, b, a} } } // defaultOptions provides the default value to generate the avatar. func defaultOptions(hash string) options { return options{ - size: 64, // default size should be 64 to make sure images are perfect square - gridSize: 8, // minimum size for the grid for make shape complexity - bgColor: color.RGBA{240, 240, 240, 255}, // light gray color - fgColor: color.RGBA{hash[0], hash[1], hash[2], 255}, // use the first three hash bytes as the foreground color + size: 64, // default size should be 64 to make sure images are perfect square + gridSize: 8, // minimum size for the grid for make shape complexity + bgColor: color.RGBA{240, 240, 240, 255}, // light gray color + fgColors: []color.RGBA{{hash[0], hash[1], hash[2], 255}}, // use the first three hash bytes as the foreground color + layers: 1, } } @@ -106,37 +130,58 @@ func Make(input string, opts ...OptFunc) image.Image { // create a blank image img := image.NewRGBA(image.Rect(0, 0, o.size, o.size)) - // generate colors - avatarColor := o.fgColor - bgColor := o.bgColor + // Fill background + draw.Draw(img, img.Bounds(), &image.Uniform{o.bgColor}, image.Point{}, draw.Src) + + currentHash := hash isOdd := o.gridSize%2 != 0 - // generate the pixel pattern - // loop over each pixel in the grid - for y := 0; y < o.gridSize; y++ { - for x := 0; x < o.gridSize/2; x++ { - // use bitwise operation to determine if a pixel should be colored - pixelOn := (hash[y]>>(x%8))&1 == 1 - - // image should - if pixelOn { - drawPixel(img, x, y, avatarColor, o.gridSize, o.size) - drawPixel(img, o.gridSize-1-x, y, avatarColor, o.gridSize, o.size) // mirror the pixel - } else { - drawPixel(img, x, y, bgColor, o.gridSize, o.size) - drawPixel(img, o.gridSize-1-x, y, bgColor, o.gridSize, o.size) // mirror the bg pixel - } + for l := 0; l < o.layers; l++ { + // derive hash for this layer + if l > 0 { + currentHash = generateHash(currentHash) + } + // determine color + var avatarColor color.RGBA + if l < len(o.fgColors) { + avatarColor = o.fgColors[l] + // Check if color is empty/zero? defaultOptions sets index 0. + // WithLayerColor might extend with zeros. + // If alpha is 0, should we generate? + // User might purposefully set transparent? Unlikely for avatar foreground. + // Assuming if user sets it, they set it. + // But if we expanded with empty RGBA (0,0,0,0), it's invisible. + // If it is strictly 0,0,0,0, maybe fallback to hash? + // Let's assume user provides valid colors if they use WithLayerColor. + // But for "unspecified" layers where user requested 3 layers but provided 1 color: + if avatarColor == (color.RGBA{}) { + avatarColor = color.RGBA{currentHash[0], currentHash[1], currentHash[2], 255} + } + } else { + avatarColor = color.RGBA{currentHash[0], currentHash[1], currentHash[2], 255} } - // Draw the center column if gridSize is odd - if isOdd { - mid := o.gridSize / 2 - pixelOn := (hash[y]>>(mid%8))&1 == 1 - color := bgColor - if pixelOn { - color = avatarColor + + // generate the pixel pattern + // loop over each pixel in the grid + for y := 0; y < o.gridSize; y++ { + for x := 0; x < o.gridSize/2; x++ { + // use bitwise operation to determine if a pixel should be colored + pixelOn := (currentHash[y]>>(x%8))&1 == 1 + + if pixelOn { + drawPixel(img, x, y, avatarColor, o.gridSize, o.size) + drawPixel(img, o.gridSize-1-x, y, avatarColor, o.gridSize, o.size) // mirror the pixel + } + } + // Draw the center column if gridSize is odd + if isOdd { + mid := o.gridSize / 2 + pixelOn := (currentHash[y]>>(mid%8))&1 == 1 + if pixelOn { + drawPixel(img, mid, y, avatarColor, o.gridSize, o.size) + } } - drawPixel(img, mid, y, color, o.gridSize, o.size) } } diff --git a/goavatar_test.go b/goavatar_test.go index 2c73207..8e6f4e1 100644 --- a/goavatar_test.go +++ b/goavatar_test.go @@ -18,12 +18,43 @@ func expectedTopLeftPixel(input string, opts []OptFunc) (col color.Color) { for _, opt := range opts { opt(&conf) } - // For the top‐left cell (x=0,y=0), the decision is based on the least‐significant bit of the raw hash character. - // Using the raw ASCII value of hash[0] as in the current implementation. - if (hash[0] & 1) == 1 { - return conf.fgColor + + // Determine the final color at (0,0) + // It's cumulative. Background first. + // Then layer 0. If pixelOn, draw layer 0 color. + // Then layer 1. If pixelOn, draw layer 1 color. + // ... + // Since we overwrite, the LAST active layer wins. + + finalColor := conf.bgColor + + currentHash := hash + + for l := 0; l < conf.layers; l++ { + if l > 0 { + currentHash = generateHash(currentHash) + } + + // For the top‐left cell (x=0,y=0), the decision is based on the least‐significant bit of the raw hash character. + // Using the raw ASCII value of hash[0] as in the current implementation. + pixelOn := (currentHash[0] & 1) == 1 + + if pixelOn { + // determine color for this layer + var layerColor color.RGBA + if l < len(conf.fgColors) { + layerColor = conf.fgColors[l] + if layerColor == (color.RGBA{}) { + layerColor = color.RGBA{currentHash[0], currentHash[1], currentHash[2], 255} + } + } else { + layerColor = color.RGBA{currentHash[0], currentHash[1], currentHash[2], 255} + } + finalColor = layerColor + } } - return conf.bgColor + + return finalColor } func TestMake(t *testing.T) { From 0c363ddfc6aeeead3d2b7ed3fbe020b8d459da8a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 13 Jan 2026 07:41:32 +0000 Subject: [PATCH 2/4] chore: Add generated multi-layer avatar examples --- arts/.gitignore | 2 ++ arts/avatar_7.png | Bin 0 -> 2090 bytes arts/avatar_8.png | Bin 0 -> 2058 bytes 3 files changed, 2 insertions(+) create mode 100644 arts/avatar_7.png create mode 100644 arts/avatar_8.png diff --git a/arts/.gitignore b/arts/.gitignore index afb5035..7f78405 100644 --- a/arts/.gitignore +++ b/arts/.gitignore @@ -4,4 +4,6 @@ !avatar_4.png !avatar_5.png !avatar_6.png +!avatar_7.png +!avatar_8.png !goavatar-banner.png diff --git a/arts/avatar_7.png b/arts/avatar_7.png new file mode 100644 index 0000000000000000000000000000000000000000..01e4129faf2bf1e0a108fda546734d674aaab16b GIT binary patch literal 2090 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&t&wwUqN(1_t(zo-U3d6?5KP-`JZKF5;G$ ze36%P+mizi6$E|c%h-KHjsFTLIC868IsE$J+=SOzwehU=1@9wo{oeoY@BesZE~ z0|vmJ_~S1*Gj|^@t~xYn?f?Dq^W%YnI~W)iu`noj4NK}9$g|AS-*(jb`+Hb}0^!oO ze|5WtrC*_u`0X`@?WA14FCT}+%dVCv_~)w*al$mboFyt=akR{01W`MU;qFB literal 0 HcmV?d00001 diff --git a/arts/avatar_8.png b/arts/avatar_8.png new file mode 100644 index 0000000000000000000000000000000000000000..f8e9dabb8e5eb54af6714bc882b1d515e230b21a GIT binary patch literal 2058 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&t&wwUqN(1_t(vo-U3d6?5KPKbYHMAi)+W z^H%oPdLb9a&sK}pC}#KnvF`i+V@tWO{*BH1{{DUcd*-|eWwFn{wAh||zheJO zMusy23=JBL3>`wlj*2>NZJT)c*Ejis`{%!~NECy8{eD*5hNIjJ3SJBhi&%yul_mTB zOUsV0Z|e`#+FusFwC?qsc^jU`Ugo~EXSn(p6qR+ytCpKHgua^}7{|a6$iyI^IxMOE zzw4Hn=j>cpTy)ZGB zz)an;<+asv^*4oe?v;!TE}RSwQy7LL_4i(~Z~l!>Hs8Or=wAQAAW>5P=g&O(VIJFc zmzja}lqs+Tx3* Date: Sun, 8 Feb 2026 22:22:52 +0500 Subject: [PATCH 3/4] fix: switch color representation from RGBA to NRGBA to fix incorrect alpha colors - Updated the `options` struct to use `color.NRGBA` for background and foreground colors. - Adjusted related functions and tests to accommodate the new color type. - Ensured consistent color handling across avatar generation logic. --- goavatar.go | 33 +++++++++++++++------------------ goavatar_test.go | 8 ++++---- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/goavatar.go b/goavatar.go index 1618027..c06edab 100644 --- a/goavatar.go +++ b/goavatar.go @@ -13,9 +13,9 @@ import ( type options struct { size int gridSize int - bgColor color.RGBA - fgColors []color.RGBA - layers int + bgColor color.NRGBA + fgColors []color.NRGBA + layers int } // OptFunc is a function that applies an option to the options struct. @@ -44,7 +44,7 @@ func WithGridSize(g int) OptFunc { // WithBgColor sets the background color of the avatar. func WithBgColor(r, g, b, a uint8) OptFunc { return func(o *options) { - o.bgColor = color.RGBA{r, g, b, a} + o.bgColor = color.NRGBA{r, g, b, a} } } @@ -52,7 +52,7 @@ func WithBgColor(r, g, b, a uint8) OptFunc { // It sets the first layer's color. func WithFgColor(r, g, b, a uint8) OptFunc { return func(o *options) { - o.fgColors = []color.RGBA{{r, g, b, a}} + o.fgColors = []color.NRGBA{{r, g, b, a}} } } @@ -70,9 +70,9 @@ func WithLayerColor(layerIndex int, r, g, b, a uint8) OptFunc { return func(o *options) { // Expand slice if needed for len(o.fgColors) <= layerIndex { - o.fgColors = append(o.fgColors, color.RGBA{}) + o.fgColors = append(o.fgColors, color.NRGBA{}) } - o.fgColors[layerIndex] = color.RGBA{r, g, b, a} + o.fgColors[layerIndex] = color.NRGBA{r, g, b, a} } } @@ -81,8 +81,8 @@ func defaultOptions(hash string) options { return options{ size: 64, // default size should be 64 to make sure images are perfect square gridSize: 8, // minimum size for the grid for make shape complexity - bgColor: color.RGBA{240, 240, 240, 255}, // light gray color - fgColors: []color.RGBA{{hash[0], hash[1], hash[2], 255}}, // use the first three hash bytes as the foreground color + bgColor: color.NRGBA{240, 240, 240, 255}, // light gray color + fgColors: []color.NRGBA{{hash[0], hash[1], hash[2], 255}}, // use the first three hash bytes as the foreground color layers: 1, } } @@ -110,11 +110,8 @@ func drawPixel(img *image.RGBA, gridX, gridY int, c color.Color, gridSize, image } // Fill the block - for y := startY; y < endY; y++ { - for x := startX; x < endX; x++ { - img.Set(x, y, c) - } - } + rect := image.Rect(startX, startY, endX, endY) + draw.Draw(img, rect, &image.Uniform{c}, image.Point{}, draw.Over) } // Make generates an avatar image based on the input string and options. @@ -143,7 +140,7 @@ func Make(input string, opts ...OptFunc) image.Image { } // determine color - var avatarColor color.RGBA + var avatarColor color.NRGBA if l < len(o.fgColors) { avatarColor = o.fgColors[l] // Check if color is empty/zero? defaultOptions sets index 0. @@ -155,11 +152,11 @@ func Make(input string, opts ...OptFunc) image.Image { // If it is strictly 0,0,0,0, maybe fallback to hash? // Let's assume user provides valid colors if they use WithLayerColor. // But for "unspecified" layers where user requested 3 layers but provided 1 color: - if avatarColor == (color.RGBA{}) { - avatarColor = color.RGBA{currentHash[0], currentHash[1], currentHash[2], 255} + if avatarColor == (color.NRGBA{}) { + avatarColor = color.NRGBA{currentHash[0], currentHash[1], currentHash[2], 255} } } else { - avatarColor = color.RGBA{currentHash[0], currentHash[1], currentHash[2], 255} + avatarColor = color.NRGBA{currentHash[0], currentHash[1], currentHash[2], 255} } // generate the pixel pattern diff --git a/goavatar_test.go b/goavatar_test.go index 8e6f4e1..7ccd6c6 100644 --- a/goavatar_test.go +++ b/goavatar_test.go @@ -41,14 +41,14 @@ func expectedTopLeftPixel(input string, opts []OptFunc) (col color.Color) { if pixelOn { // determine color for this layer - var layerColor color.RGBA + var layerColor color.NRGBA if l < len(conf.fgColors) { layerColor = conf.fgColors[l] - if layerColor == (color.RGBA{}) { - layerColor = color.RGBA{currentHash[0], currentHash[1], currentHash[2], 255} + if layerColor == (color.NRGBA{}) { + layerColor = color.NRGBA{currentHash[0], currentHash[1], currentHash[2], 255} } } else { - layerColor = color.RGBA{currentHash[0], currentHash[1], currentHash[2], 255} + layerColor = color.NRGBA{currentHash[0], currentHash[1], currentHash[2], 255} } finalColor = layerColor } From 1d7a22a1b739b8df18bddc721b47c9f17e872d2a Mon Sep 17 00:00:00 2001 From: Sheikh Abdullah Date: Sun, 8 Feb 2026 22:41:39 +0500 Subject: [PATCH 4/4] docs: update README with new multi-layer avatar examples and images --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1f93e2b..a25ea34 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,19 @@ This package provides a simple way to generate unique, symmetric identicons base      - Avatar 5
+ Avatar 6
nice__user__name
+      + + Avatar 7
+ MultiLayer2 +
+      + + Avatar 8
+ MultiLayer3Custom +

## Installation @@ -107,16 +117,40 @@ func main() { // Saves the generated avatar as avatar_5.png image5 := goavatar.Make("EmberNexus23") - // Collect options dynamically - var opts []goavatar.OptFunc - - // add size - opts = append(opts, goavatar.WithSize(100)) - opts = append(opts, goavatar.WithGridSize(10)) - image6 := goavatar.Make("nice__user__name", opts...) - - // append all the images into the list - imgSlice = append(imgSlice, image1, image2, image3, image4, image5, image6) + // Collect options dynamically + var opts []goavatar.OptFunc + + // add size + opts = append(opts, goavatar.WithSize(100)) + opts = append(opts, goavatar.WithGridSize(10)) + image6 := goavatar.Make("nice__user__name", opts...) + + // Generate multi-layered avatar (2 layers) + image7 := goavatar.Make("MultiLayer2", + goavatar.WithSize(512), + goavatar.WithLayers(2), + ) + + // Generate multi-layered avatar (3 layers) with custom colors for each layer + image8 := goavatar.Make("MultiLayer3Custom", + goavatar.WithSize(512), + goavatar.WithLayers(3), + goavatar.WithLayerColor(0, 255, 0, 0, 255), // Red + goavatar.WithLayerColor(1, 0, 255, 0, 255), // Green + goavatar.WithLayerColor(2, 0, 0, 255, 255), // Blue + ) + + // Generate multi-layered avatar with transparency + image9 := goavatar.Make("MultiLayer3Transparent", + goavatar.WithSize(512), + goavatar.WithLayers(3), + goavatar.WithLayerColor(0, 255, 0, 0, 80), // Red (Transparent) + goavatar.WithLayerColor(1, 0, 255, 0, 160), // Green (Transparent) + goavatar.WithLayerColor(2, 0, 0, 255, 255), // Blue (Opaque) + ) + + // append all the images into the list + imgSlice = append(imgSlice, image1, image2, image3, image4, image5, image6, image7, image8, image9) // loop through the image slice and save the images for i, img := range imgSlice {