Skip to content

Commit b8fd4db

Browse files
authored
Merge pull request #59 from codedex-io/blog/avatar-customization
blog(create): Customize your avatars in worlds blog
2 parents 72ece2d + 19b8b14 commit b8fd4db

File tree

1 file changed

+257
-0
lines changed

1 file changed

+257
-0
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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+
![Sprite](https://firebasestorage.googleapis.com/v0/b/codedex-io.appspot.com/o/blogs%2Fcustomize-your-avatar-in-worlds%2Fcharacter-customizer.gif?alt=media&token=406feea2-5de2-446f-be7e-b2cf1e0a1ad6)
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+
![spritesheet](https://firebasestorage.googleapis.com/v0/b/codedex-io.appspot.com/o/blogs%2Fcustomize-your-avatar-in-worlds%2Fbody-idle.png?alt=media&token=735a8e3c-fb19-40fa-ad2d-6897b748e097)
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+
![color mappings](https://firebasestorage.googleapis.com/v0/b/codedex-io.appspot.com/o/blogs%2Fcustomize-your-avatar-in-worlds%2Fcolors.png?alt=media&token=1c262911-ee9a-41c2-af2f-53bf18762642)
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+
![Sprite](https://firebasestorage.googleapis.com/v0/b/codedex-io.appspot.com/o/blogs%2Fcustomize-your-avatar-in-worlds%2Fcharacter-customizer.gif?alt=media&token=406feea2-5de2-446f-be7e-b2cf1e0a1ad6)
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

Comments
 (0)