⏱️ Lectura: 12 min
An emoji only gives you five skin tones to choose from; a makeup brand offers up to 50; and a character editor throws all 16,777,216 combinations of the full RGB space at you and leaves you with just the picker. None of these three options resembles the real variety of human skin tones, and that’s the gap an open source project published by developer Toney Alexander tries to close.
📑 En este artículo
The result is a mathematical color space, not a fixed palette: it defines an ellipsoid-shaped region inside the RGB cube where plausible skin tones fall. With a handful of equations and a single adjustable parameter, any developer can generate believable skin colors for character creators, digital art, or accessibility tools.
TL;DR
- The project defines an ellipsoidal color space inside the RGB cube to represent human skin tones more inclusively than a fixed palette.
- It uses a 3D vector (t, u, v) sampled inside a sphere of adjustable radius (parameter R²) and transforms it to RGB with a fixed linear matrix.
- The reference code is published in Python and JavaScript alongside an interactive color picker on the project page.
- It compares the proposal against the current extremes: emojis with 5 tones (based on the Fitzpatrick scale), makeup brands with up to 50 tones, and the full RGB cube with 16,777,216 combinations.
- The author warns that the model is “good enough,” not clinically accurate: it doesn’t capture vitiligo, hyperpigmentation, scars, or conditions like argyria or high bilirubin.
- The main use case is character design in video games and digital art, where informal references like the “Flesh Cloud” image or very limited preset palettes are used today.
What happened: a color space designed for real skin tones
Toney Alexander published an interactive page presenting a custom color space for skin tones, accompanied by the complete source code and a visual picker where you can move a point inside a sphere and see the resulting color in real time. The stated goal is to identify, within the RGB space, the widest possible region of colors corresponding to plausible, though simplified, skin tones.
The key word is “good enough”: the author himself avoids calling it an authoritative model. It’s a simple formula designed to solve a practical, recurring problem in software development: what colors to show by default to someone customizing an avatar, a video game character, or an illustration, without manually curating hundreds of swatches.
Context and history: between 5 emojis and 16.7 million colors
This isn’t a new problem. People emojis use skin tone modifiers based on the Fitzpatrick scale, published as part of Unicode Technical Report #51: just five tones to represent all of humanity. A makeup brand, at the opposite extreme, can offer up to 50 different shades, much more granular but still a closed list.
In between sits the generic color picker: 16,777,216 possible combinations in 24-bit RGB, technically sufficient but practically useless as a starting point, because it tells the user nothing about which region of that cube corresponds to a real skin tone.
The digital art community has been solving this informally for years, with reference images like the so-called “Flesh Cloud,” a cloud of color points shared by Tumblr user shiroxix to help other artists locate plausible skin tones within the color space. Something similar happens in video games: the character creator in the early access game Paralives offers many presets, but ends up relying on a general color picker when those presets fall short.
Technical details: how the color space is built
The construction has two steps. First, a point is randomly chosen inside a sphere in an auxiliary three-coordinate space (t, u, v), with a radius controlled by the parameter R² (radius squared). Second, that point is transformed with a fixed linear formula that converts it into R, G, and B values.
The source code offers two ways to solve the first step. The analytical approach uses spherical coordinates and corrects the radius with a cube root so that point density stays uniform throughout the sphere’s volume, not just near the center:
def select_point(r_square=2.0):
"""Uniform analytical sampling inside the sphere"""
radius = r_square ** (1. / 2)
phi = uniform(0, 2 * math.pi)
costheta = uniform(-1, 1)
n = uniform(0, 1)
theta = math.acos(costheta)
r = radius * (n ** (1.0 / 3))
t = r * math.sin(theta) * math.cos(phi)
u = r * math.sin(theta) * math.sin(phi)
v = r * math.cos(theta)
return (t, u, v)
The rejection-based alternative is easier to read: it generates points inside a cube and discards the ones that fall outside the sphere’s radius, repeating until it succeeds. It’s less efficient in the worst case, but simpler to port to another language without carrying over sign errors in trigonometry.
The second step, the transformation to RGB, is a fixed linear combination of three coefficients per channel, calculated by the author so the result falls within the range of plausible skin tones:
def to_rgb(t, u, v):
x = (t - 0.15) / 0.45
y = (v - 1.2 * t ** 2 + 0.2 * t + 0.655) / 1.84
z = u / 3.6
r = 28.77438370854 * x + 36.78307445559 * y - 19.69766918644 * z + 187.1436241611
g = 35.38327306318 * x - 2.009931981182 * y + 47.93462563172 * z + 137.1073825503
b = 36.14733717939 * x - 43.54346996173 * y - 28.50821294135 * z + 108.2241610738
return int(r), int(g), int(b)
The R² parameter is, in practice, the system’s inclusivity control: larger values expand the region of skin tones the generator can produce, while smaller values concentrate results near the center of the space.
💡 Tip: if you integrate this into a character editor, expose R² as a slider to the user instead of hardcoding it. That’s the difference between a rigid palette and an adjustable range of skin tones.
| Sampling method | When to use it | Advantage | Limitation |
|---|---|---|---|
| Analytical (spherical coordinates) | When you need to generate many tones per second, for example in an editor with a live preview | Constant time: always computes the point in a single step | The formula with cube root and trigonometry is less readable at a glance |
| Rejection-based | Quick prototypes or one-off generation of a few tones | Trivial code to read, debug, and port to another language | May iterate several times before finding a valid point inside the sphere |
The project doesn’t publish its own performance benchmarks, so there’s no reference figure to cite here. If you care about measuring it in your own environment, the most direct way is to wrap the call to select_point() with time.perf_counter() in Python or console.time() in JavaScript and compare both methods with the same number of samples.
flowchart TD
A["Sampling (t, u, v) inside sphere R2"] --> B["Projection to x, y, z"]
B --> C["Linear transformation to_rgb()"]
C --> D[("Final skin tone in RGB")]
How to start testing it
You don’t need to install any package: the reference script only uses Python’s standard library (math and random). Save it as color_space.py and run it with the interpreter you already have installed:
- Windows:
python color_space.py - macOS:
python3 color_space.py - Linux:
python3 color_space.py
A first minimal script, to confirm the transformation works, converts a single point near the center of the space:
import math
from random import uniform
def to_rgb(t, u, v):
x = (t - 0.15) / 0.45
y = (v - 1.2 * t ** 2 + 0.2 * t + 0.655) / 1.84
z = u / 3.6
r = 28.77438370854 * x + 36.78307445559 * y - 19.69766918644 * z + 187.1436241611
g = 35.38327306318 * x - 2.009931981182 * y + 47.93462563172 * z + 137.1073825503
b = 36.14733717939 * x - 43.54346996173 * y - 28.50821294135 * z + 108.2241610738
return int(r), int(g), int(b)
print(to_rgb(0.0, 0.0, 0.0))
That command prints a (r, g, b) tuple corresponding to a tone near the center of the color space. With that confirmed, the next step is to generate a complete palette using rejection sampling, useful as a default set of swatches in a character creator:
def muestrear_punto(r_cuadrado=2.0):
radio = r_cuadrado ** 0.5
R = radio + 1
while R > radio:
t = uniform(-radio, radio)
u = uniform(-radio, radio)
v = uniform(-radio, radio)
R = (t**2 + u**2 + v**2) ** 0.5
return t, u, v
def generar_paleta(cantidad=8, r_cuadrado=2.0):
paleta = []
for _ in range(cantidad):
t, u, v = muestrear_punto(r_cuadrado)
r, g, b = to_rgb(t, u, v)
r, g, b = max(0, min(255, r)), max(0, min(255, g)), max(0, min(255, b))
paleta.append('#{:02x}{:02x}{:02x}'.format(r, g, b))
return paleta
print(generar_paleta(cantidad=8))
The result is a list of 8 hexadecimal codes ready to use directly as swatches in any web color picker or game engine: you just need to port the same four arithmetic operations (addition, subtraction, multiplication, division) to whatever language you’re using, with no external dependencies.
Impact and analysis: how far the model goes
The author himself dedicates an entire section of the project to explaining why this color space shouldn’t be treated as definitive. Real skin varies enormously between different areas of the body and depends on complex biological processes: blood flow, melanin concentration, and the scattering of light through multiple layers of tissue, in addition to vitiligo, freckles, hyperpigmentation, and scars.
There are also medical conditions that push skin tone outside any “plausible” range defined in advance. Argyria can leave skin a bluish-gray color; high bilirubin can give it a yellowish or greenish tone. No color space designed for general use cases, like this one, attempts to cover those extremes.
The author is also honest about his own bias: he clarifies that he’s not a researcher, that he doesn’t have a known color blindness condition, but that many of the color space’s design decisions are subjective. And there’s an additional problem no code can solve: the same RGB value looks different depending on the viewer’s screen, brightness, and ambient light.
⚠️ Heads up: the model doesn’t replace dermatological classifications and isn’t meant for medical or forensic contexts. It’s a design tool, meant to be “good enough,” not clinically precise.
That said, the approach has a concrete practical merit: instead of requiring every development team to build its own palette by hand or train a machine learning model for something this narrow, it offers a short, deterministic, easily auditable formula. That makes it suitable both for an indie character editor and for an accessibility tool that needs to generate skin tone variants on demand.
💭 Key point: the difference compared to a dataset of thousands of labeled photos is that here inclusivity is encoded in four linear coefficients, not gigabytes of training data.
What’s next
The author himself leaves the door open to extending the work toward more rigorous directions, such as models that incorporate genetic determinants of skin tone, an area he describes as interesting but outside the scope of this project. The original motivation was to solve simple use cases, not to build a biologically complete model.
For a development team that wants to go beyond the published code, a reasonable extension is to convert the output to a perceptually uniform space like OKLab or CIELAB before showing it to the user, so that tone shifts in the picker feel even to the human eye. The original code works directly in RGB, which doesn’t have that property.
Try it yourself: copy the to_rgb() block from this article, save it as color_space.py, and run python3 color_space.py (or python color_space.py on Windows) to generate your first skin tone palette in seconds.
📖 Summary on Telegram: View summary
Frequently Asked Questions
Does this color space replace the Fitzpatrick scale?
No. The Fitzpatrick scale classifies skin into six categories based on its reaction to ultraviolet radiation, for dermatological purposes. Toney Alexander’s project aims at something different: generating plausible skin tones for digital art and video games, not classifying skin types medically.
Do I need to install any library to use the code?
No. The reference script in Python only uses math and random, both part of the standard library. The JavaScript equivalent also has no external dependencies and runs directly in the browser.
Is it suitable for medical or forensic representation?
No. The author himself clarifies that the model is “good enough” for simple use cases like visual customization, not for contexts where skin tone accuracy matters clinically.
What exactly does the R² parameter control?
It’s the squared radius of the sampling sphere in the auxiliary space (t, u, v). A larger R² expands the range of skin tones the algorithm can generate; a smaller one concentrates results near the center.
Is the color space perceptually uniform?
No. It works directly in RGB, which isn’t a perceptually uniform space. Converting the output to OKLab or CIELAB before displaying it is a possible extension, but it’s not included in the original code.
Can I port this to a game engine like Unity or Godot?
Yes. The logic is language-independent: you just need to port the same arithmetic operations to C# or GDScript, which is straightforward if you use the rejection-based variant, since it doesn’t depend on trigonometric functions.
References
- Toney Alexander: “What Colors Are We? Constructing A Color Space For Skin Tones”: the original project page, with the interactive picker and the complete source code in Python and JavaScript.
- Unicode Technical Report #51: Unicode Emoji: specification of the skin tone modifiers for emojis, based on the Fitzpatrick scale.
- Wikipedia: Fitzpatrick scale: context on the dermatological classification of skin types used as a historical reference.
- Wikipedia: Color space: fundamentals on what a color space is and how it’s mathematically defined.
- Paralives: the early access video game whose character editor is mentioned as an example of a general color picker.
📱 Do you like this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
Imagen destacada: Foto de Susan Wilkinson en Unsplash
0 Comments