|
| 1 | +--- |
| 2 | +title: Customize Your Avatar in Worlds |
| 3 | +description: Enjoying your customized avatars? This blog goes into detail about the new avatar customizer and the engineering behind it! |
| 4 | +author: Dharma Jethva |
| 5 | +seoImageLink: TBD |
| 6 | +dateCreated: 2025-11-25 |
| 7 | +published: false |
| 8 | +tags: |
| 9 | + - Product |
| 10 | +--- |
| 11 | + |
| 12 | +Hey, Dharma here! Back with another update from the Codédex Engineering team. |
| 13 | + |
| 14 | +This time around, let's talk avatars. |
| 15 | + |
| 16 | +## Express Yourself in Pixels |
| 17 | + |
| 18 | +Since we launched [Codédex Worlds](https://www.codedex.io/worlds), you've been able to pick from a handful of preset characters. |
| 19 | + |
| 20 | +That changes with this update. |
| 21 | + |
| 22 | +We're excited to introduce **avatar customization** – a new way to create a character that actually represents you in Codédex Worlds. |
| 23 | + |
| 24 | +## What You Can Customize |
| 25 | + |
| 26 | +Meet the new – our first fully customizable character. Here's what you can personalize: |
| 27 | + |
| 28 | + |
| 29 | + |
| 30 | +| Category | Options | |
| 31 | +| ------------ | ----------------------------------------------------------------------------------- | |
| 32 | +| Skin Tone | 7 tones to match your look | |
| 33 | +| Hair Style | 3 styles to choose from | |
| 34 | +| Hair Color | 10 colors including blonde, brown, black, white, red, pink, purple, green, and blue | |
| 35 | +| Outfit Style | 4 different outfit designs | |
| 36 | +| Outfit Color | 5 colors per outfit style | |
| 37 | +| Background | Custom preview background | |
| 38 | + |
| 39 | +Mix and match to create hundreds of unique combinations. Want pink hair with a green outfit? Go for it. Prefer something more classic with the default colors? That works too. |
| 40 | + |
| 41 | +Of course, your avatar automatically gets saved to your account, so you’ll look the same every time you enter Worlds and in the dashboard and other places. |
| 42 | + |
| 43 | +## The Challenge |
| 44 | + |
| 45 | +Building a customization system sounds simple: add the outfit/hair style, pick a color, apply it to the sprite and boom! Done, right? |
| 46 | + |
| 47 | +Well, not quite. Here’s what I had to consider: |
| 48 | + |
| 49 | +**Static Assets Combination** - 7 skin tones, 3 hair styles with 10 hair colors, 4 outfits with 5 different colors for each outfit and 8 background colors. The number of combinations is 7 \* 3 \* 10 \* 4 \* 5 \* 8 = 33600 combinations! Creating a separate sprite file for each? That’s over 33600 asset files for \*one\* character, across walking and standing animations in 4 directions. NO THANKS! |
| 50 | + |
| 51 | +**Preserving pixel-art depth** – You can't just find-and-replace colors. A sprite has shadows, mid-tones, and highlights. Swap "brown" for "pink" and you lose all that shading. The character looks flat and lifeless. |
| 52 | + |
| 53 | +**Real-time preview** – The Worlds runs in Phaser, but the customizer lives in React outside of the Worlds. I needed instant visual feedback without booting up a game engine every time you tweak a style for your character. |
| 54 | + |
| 55 | +**Layer synchronization** – Body, hair, and outfit are separate sprite layers. They all need to animate together, frame-perfectly, in all four directions. Imagine a game world where your character walks and the hair comes floating behind it. Yeah, I wouldn’t play that game either! |
| 56 | + |
| 57 | +So what did I do to address these issues, you ask? Let’s take a look! |
| 58 | + |
| 59 | +## The Solution |
| 60 | + |
| 61 | +It starts with understanding the spritesheet. |
| 62 | + |
| 63 | +### Anatomy of a Spritesheet |
| 64 | + |
| 65 | +Each character is a 24x24 pixel sprite arranged in a grid. Similiary each asset such as hair type, outfit type, etc. is a similar spritesheet. |
| 66 | + |
| 67 | +There are two variant spritesheets of each asset. One for idle standing animation and one for walking animation. Take a look at this base body idle standing spritesheet: |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | +It has 10 frames in each row. A total of 40 frames. |
| 72 | + |
| 73 | +```js |
| 74 | +// Standing/idle: 10 frames × 4 directions = 40 frames |
| 75 | +const standingSpritesheet = { |
| 76 | + frameWidth: 24, |
| 77 | + frameHeight: 24, |
| 78 | + columns: 10, |
| 79 | + rows: 4, |
| 80 | +}; |
| 81 | + |
| 82 | +// Walking animation: 6 frames × 4 directions = 24 total frames |
| 83 | +const walkingSpritesheet = { |
| 84 | + frameWidth: 24, |
| 85 | + frameHeight: 24, |
| 86 | + columns: 6, // Animation frames |
| 87 | + rows: 4, // Down, Right, Left, Up |
| 88 | +}; |
| 89 | + |
| 90 | +// That's 64 frames per layer, per character. |
| 91 | +// Now multiply by hair, body, outfit layers... |
| 92 | +``` |
| 93 | + |
| 94 | +### The Multi-Shade Palette System |
| 95 | + |
| 96 | +Here's the key insight: a "color" isn't one color – it's a coordinated palette of shades. |
| 97 | + |
| 98 | + |
| 99 | + |
| 100 | +Each shade maps to specific pixels in the sprite. The dark base creates depth around edges, mid-tones fill the bulk, and highlights add that pixel-art pop. |
| 101 | + |
| 102 | +Shoutout to our brilliant in-house Product Designer Jackie Liu for handcrafting these assets and color palettes! |
| 103 | + |
| 104 | +### Pixel-by-Pixel Color Swapping |
| 105 | + |
| 106 | +Here's where the magic happens. We literally scan every pixel: |
| 107 | + |
| 108 | +```js |
| 109 | +function recolorImageData(imageData: ImageData, colorMap: Map<string, RGBA>) { |
| 110 | + const buffer = imageData.data; // Raw RGBA pixel array |
| 111 | + const tolerance = 15; // Forgiveness for compression tolerance |
| 112 | + |
| 113 | + // Loop through every pixel (4 values each: R, G, B, A) |
| 114 | + for (let i = 0; i < buffer.length; i += 4) { |
| 115 | + const r = buffer[i]; |
| 116 | + const g = buffer[i + 1]; |
| 117 | + const b = buffer[i + 2]; |
| 118 | + const a = buffer[i + 3]; |
| 119 | + |
| 120 | + // Skip transparent pixels (nothing to recolor) |
| 121 | + if (a === 0) continue; |
| 122 | + |
| 123 | + // Find if this pixel matches any source color |
| 124 | + for (const [sourceColor, targetColor] of colorMap) { |
| 125 | + const distance = |
| 126 | + Math.abs(r - sourceColor[0]) + |
| 127 | + Math.abs(g - sourceColor[1]) + |
| 128 | + Math.abs(b - sourceColor[2]); |
| 129 | + |
| 130 | + if (distance <= tolerance) { |
| 131 | + // Swap the pixel to the new color! |
| 132 | + buffer[i] = targetColor[0]; // R |
| 133 | + buffer[i + 1] = targetColor[1]; // G |
| 134 | + buffer[i + 2] = targetColor[2]; // B |
| 135 | + break; |
| 136 | + } |
| 137 | + } |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +``` |
| 142 | + |
| 143 | +For a 144×96 walking spritesheet, that's 13,824 pixels to check. Multiply this number with 5 which is the number of color shades and that results in \~69,000 comparisons. And, it happens instantly! |
| 144 | + |
| 145 | +**Note**: "Compression tolerance" refers to small color shifts that happen when images are saved or compressed. A pixel that should be exactly #341300 might become #351401. The tolerance = 15 means: if a pixel is close enough to the target color (within 15 units), we still consider it a match and swap it. |
| 146 | + |
| 147 | +This would often happen in production environment when it doesn't happen on local development environments because of all the optimizations that production is streamlined with. |
| 148 | + |
| 149 | +Of course, that’s not all. We also handle shading relationships using color luminance with the “Human eye perception” formula. So, when the original sprite has a dark brown shadow pixel, your pink version gets an equivalent dark pink shadow. The depth is preserved. |
| 150 | + |
| 151 | +So when the original sprite has a dark brown shadow pixel, your pink version gets an equivalently dark pink shadow. The depth is preserved. |
| 152 | + |
| 153 | +### Layer Compositing |
| 154 | + |
| 155 | +Finally, we stack the layers like transparent overlays: |
| 156 | + |
| 157 | +```js |
| 158 | +// Sort layers by z-index and draw them in order |
| 159 | +const layerCanvases = [ |
| 160 | + bodyCanvas, // zIndex: 0 – base layer |
| 161 | + hairCanvas, // zIndex: 1 – on top |
| 162 | + outfitCanvas, // zIndex: 2 – topmost details |
| 163 | +]; |
| 164 | + |
| 165 | +layerCanvases.forEach((layer) => { |
| 166 | + ctx.drawImage(layer, 0, 0); // Each layer composites onto the previous |
| 167 | +}); |
| 168 | + |
| 169 | +// Register the final composite with Phaser's texture manager |
| 170 | +scene.textures.addSpriteSheet(uniqueKey, compositeCanvas, { |
| 171 | + frameWidth: 24, |
| 172 | + frameHeight: 24, |
| 173 | +}); |
| 174 | +``` |
| 175 | + |
| 176 | +The result gets a unique key based on your exact configuration, so the same pink-hair-green-outfit combo is shared across all players who chose it. Gotta be efficient after all, right? |
| 177 | + |
| 178 | +This also makes sure that the layers are never out of sync when rendered. So, no “hair floating behind the character” or “outfit floating above the character” moments! |
| 179 | + |
| 180 | +### Real-Time Preview (Without the Game Engine) |
| 181 | + |
| 182 | +As stated, the customizer runs in React, but the avatars render in Phaser(our game engine). We can’t boot up a whole game just to show a preview. |
| 183 | + |
| 184 | +Solution? A parallel rendering system: |
| 185 | + |
| 186 | +```js |
| 187 | +// Load the spritesheet as a regular image |
| 188 | +async function loadImage(src: string): Promise<HTMLImageElement> { |
| 189 | + return new Promise((resolve, reject) => { |
| 190 | + const img = new Image(); |
| 191 | + img.crossOrigin = "anonymous"; |
| 192 | + img.onload = () => resolve(img); |
| 193 | + img.onerror = reject; |
| 194 | + img.src = src; |
| 195 | + }); |
| 196 | +} |
| 197 | + |
| 198 | +// Extract just one frame from the spritesheet for preview |
| 199 | +ctx.drawImage( |
| 200 | + image, |
| 201 | + frameWidth, // Start at 2nd frame (facing forward) |
| 202 | + 0, |
| 203 | + frameWidth, |
| 204 | + frameHeight, |
| 205 | + 0, |
| 206 | + 0, |
| 207 | + frameWidth, |
| 208 | + frameHeight, |
| 209 | +); |
| 210 | +``` |
| 211 | + |
| 212 | +We grab the standing-facing-forward frame, apply the same palette swapping logic, composite the layers, and export it: |
| 213 | + |
| 214 | +```js |
| 215 | +export async function generateAvatarPreview(config: AvatarConfig) { |
| 216 | + // ... load and composite all layers ... |
| 217 | + |
| 218 | + // Export as a data URL for instant display |
| 219 | + return finalCanvas.toDataURL("image/png"); |
| 220 | + |
| 221 | + // Result: "data:image/png;base64,iVBORw0KGgo..." |
| 222 | + // Displays instantly in an <img> tag! |
| 223 | +} |
| 224 | +``` |
| 225 | + |
| 226 | + |
| 227 | + |
| 228 | +Same color math, same layer stacking – just without Phaser. When you tweak your hair color, you see the result in milliseconds. |
| 229 | + |
| 230 | +**The payoff?** Hundreds of unique combinations from ~20 base sprite files. No 10,000-asset nightmare. Just math, pixels, and a lot of for-loops running twice – once for your preview, once in the game. |
| 231 | + |
| 232 | +## Try It Out |
| 233 | + |
| 234 | +Ready to create your avatar? Here's how: |
| 235 | + |
| 236 | +1. Head to [codedex.io/worlds](https://www.codedex.io/worlds) |
| 237 | +2. Before entering, you'll see the character selection screen |
| 238 | +3. Choose **Custom** tab - the customizable option in the avatar picker |
| 239 | +4. Pick your skin tone, hair style & color, and outfit style & color |
| 240 | +5. Hit **Save** and enter Worlds! |
| 241 | + |
| 242 | +Your customizations save automatically to your account. Next time you visit, your avatar will be waiting exactly as you left it. |
| 243 | + |
| 244 | +## Why This Matters |
| 245 | + |
| 246 | +Worlds is where our community comes together – for events, hackathons (raffles and winner announcements), and just hanging out. But when everyone looks the same, it's hard to feel like **you**. |
| 247 | + |
| 248 | +Now you can stand out. Express yourself. And when you spot that bright pink hair across the room, you'll know exactly who it is. |
| 249 | + |
| 250 | +## What's Next |
| 251 | + |
| 252 | +This is just the beginning. More customization options are in the works. Stay tuned. |
| 253 | + |
| 254 | +--- |
| 255 | + |
| 256 | +Happy coding, |
| 257 | +Dharma |
0 commit comments