# 实现 DiceBear Core
本指南说明如何在任何编程语言中实现 DiceBear Core。
正确的实现会为相同的种子和样式定义生成与
[JavaScript](https://github.com/dicebear/dicebear/tree/10.x/src/js/core)、
[PHP](https://github.com/dicebear/dicebear/tree/10.x/src/php/core)、
[Python](https://github.com/dicebear/dicebear/tree/10.x/src/python/core)、
[Rust](https://github.com/dicebear/dicebear/tree/10.x/src/rust/core)、
[Go](https://github.com/dicebear/dicebear/tree/10.x/src/go/core) 和
[Dart](https://github.com/dicebear/dicebear/tree/10.x/src/dart/core) 参考
实现**字节完全一致**的 SVG。
## 架构概览
```
Avatar(definition, options)
│
├── Style 解析并验证定义 JSON
├── Options 使用 PRNG 解析选项
└── Renderer 从解析后的样式 + 选项生成 SVG
│
├── Prng 确定性的随机数生成器
│ ├── Fnv1a FNV-1a 32 位哈希
│ └── Mulberry32 有状态的 PRNG
│
└── SVG 输出
```
核心设计刻意保持极简。它接收一个
[样式定义](https://dicebear.zhcndoc.com/specification/definition-schema/) 和用户选项,通过确定性的 PRNG 解析
可随机化的值,并渲染出一个 SVG 字符串。
## PRNG 契约
PRNG 是互操作性的接口。如果你的 PRNG 对相同输入产生与参考实现相同的输出,你的实现就会生成完全一致的 SVG。请先把这一部分做好。
### FNV-1a 32 位哈希
[FNV-1a](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function)
将字符串转换为 32 位无符号整数。DiceBear 遍历的是 **UTF-16 代码单元**(不是字节,也不是码点)。
```
offset_basis = 0x811c9dc5
prime = 0x01000193
function fnv1a_hash(input: string) -> uint32:
hash = offset_basis
for each UTF-16 code unit c in input:
hash = hash XOR c
hash = hash * prime (32-bit multiply, discard overflow)
return hash as unsigned 32-bit
```
在 JavaScript 中,`input.charCodeAt(i)` 会直接返回 UTF-16 代码单元。
没有原生 UTF-16 字符串的语言必须先进行转换。在基本多文种平面之外(例如 emoji),代码单元和码点会分离,而使用码点会导致这些输入的哈希错误。PHP 参考实现明确进行了转换:
```php
unpack('v*', mb_convert_encoding($input, 'UTF-16LE', 'UTF-8'))
```
Java 和 C# 可以直接遍历 `string.charAt(i)` / `char`。对于其他语言,请转换为 UTF-16 并读取 16 位单元。
**参考(JS):**
```js
static hash(input) {
let hash = 0x811c9dc5;
for (let i = 0; i < input.length; i++) {
hash ^= input.charCodeAt(i);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}
```
### Mulberry32
[Mulberry32](https://gist.github.com/tommyettinger/46a874533244883189143505d203312c)
是一种有状态的 PRNG,它将 32 位种子转换为伪随机数序列。实现必须与 Tommy Ettinger 的 C 参考实现完全一致。
```
function mulberry32_next(state) -> (uint32, new_state):
state = (state + 0x6D2B79F5) as signed 32-bit
z = state
z = (z XOR (z >>> 15)) * (z OR 1) (32-bit multiply)
z = z XOR (z + ((z XOR (z >>> 7)) * (z OR 61))) (32-bit multiply)
return (z XOR (z >>> 14)) as unsigned 32-bit
function mulberry32_next_float(state) -> (float, new_state):
(value, new_state) = mulberry32_next(state)
return value / 2^32
```
**参考(JS):**
```js
next() {
const z = (this.#state = (this.#state + 0x6d2b79f5) | 0);
let t = Math.imul(z ^ (z >>> 15), z | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0);
}
nextFloat() {
return this.next() / 4294967296; // 2^32
}
```
关键细节:
- `| 0` 强制为 32 位有符号整数(处理溢出)
- `>>> 0` 转回无符号 32 位
- `Math.imul` 执行 32 位整数乘法
- `nextFloat()` 通过除以 `2^32` 返回 `[0, 1)` 区间内的值
- 状态是 **有状态的**:每次调用 `next()` 都会推进状态
- 具有 64 位整数但没有原生 `uint32_t` 的语言(例如 PHP、Lua)必须
手动实现 32 位乘法:天真的 `uint32 * uint32` 会超过
`2^63 - 1` 并静默溢出。PHP 参考实现将一个操作数拆分为
16 位半字;参见
[`Prng/Mulberry32.php::mul`](https://github.com/dicebear/dicebear/blob/10.x/src/php/core/src/Prng/Mulberry32.php)。
### 基于 key 的值生成
DiceBear 不会顺序调用 PRNG。相反,每个随机决策都使用一个 **key** 来派生独立的值。这样输出就与调用顺序无关。
```
function getValue(seed: string, key: string) -> float:
hash = fnv1a_hash(seed + ":" + key)
prng = new Mulberry32(hash)
return prng.nextFloat()
```
例如,`getValue("alice", "eyesVariant")` 总会返回相同的浮点数,
不管 `getValue("alice", "mouthVariant")` 是在之前还是之后被调用。
### 选择方法
对于输入中有多个条目的情况,每种选择方法都会先进行标准化:
1. **按项目的字符串表示去重**,保留第一次出现的项
(仅 `pick` 和 `shuffle`;`weightedPick` 操作的是 map,并且
按构造天然具有唯一键)。
2. **按项目的字符串表示排序**,使用 UTF-16 代码单元
比较(JavaScript 默认的 `.sort()` 顺序)。
空输入返回 `undefined`(`shuffle` 则返回空数组);单项输入会原样返回,
不会进行去重或排序。这两个标准化步骤使多项输出与调用方的顺序和重复项无关。
实际上,唯一会被排序的值是组件变体名称和十六进制颜色字符串(两者都保证为 ASCII),
因此实现可以用 `strcmp` 比较并保持一致,尽管 JavaScript 参考实现比较的是完整的 UTF-16 代码单元。
PHP 参考实现就是这么做的。
#### `pick(key, items) -> item | undefined`
从数组中选择一个项目。
```
function pick(seed, key, items):
if items is empty: return undefined
if items has 1 item: return items[0]
unique = deduplicate items by string representation
if unique has 1 item: return unique[0]
sorted = sort unique by string representation
index = floor(getValue(seed, key) * length(sorted))
return sorted[index]
```
#### `weightedPick(key, weights) -> key | undefined`
接受一个 `string → weight` 的 map,并返回其中一个键,按权重偏向选择。当所有权重都为 `0` 时,会退回到对这些键进行无权重的 `pick`。
```
function weightedPick(seed, key, weights):
keys = keys of weights
if keys is empty: return undefined
if keys has 1 item: return keys[0]
sorted = sort keys by string representation
totalWeight = sum of weights[k] for k in sorted
if totalWeight == 0: return pick(seed, key, sorted)
threshold = getValue(seed, key) * totalWeight
cumulative = 0
for each k in sorted:
cumulative += weights[k]
if threshold < cumulative: return k
return last(sorted)
```
#### `bool(key, likelihood) -> boolean`
以 `likelihood / 100` 的概率返回 `true`。`likelihood` 默认值为
`50`。
```
function bool(seed, key, likelihood = 50):
return getValue(seed, key) * 100 < likelihood
```
#### `float(key, range) -> number`
返回闭区间内的浮点数,四舍五入到小数点后四位。`range` 是 schema 的
`{ min, max, step? }` 对象。如果 `min > max`,内部会交换它们。若 `step > 0`,
则在
`{ min + i × step | 0 ≤ i ≤ ⌊(max − min) / step⌋ }` 中均匀采样,因此当 `(max − min)`
不是 `step` 的整数倍时,最后一个桶会 `≤ max`,只有在除法恰好整除时 `max`
本身才会被命中。不提供 `step` 时,范围是连续的。
```
function float(seed, key, range):
min = min(range.min, range.max)
max = max(range.min, range.max)
step = range.step if range.step > 0 else 0
if step > 0:
buckets = floor((max - min) / step) + 1
i = floor(getValue(seed, key) * buckets)
raw = min + i * step
else:
raw = min + getValue(seed, key) * (max - min)
return round(raw * 10000) / 10000 # 四舍五入,.5 进位到 +Infinity
```
这里的 `round` 与 [number formatting](#number-formatting) 中相同:
.5 会朝 **+Infinity** 方向舍入(JavaScript 的 `Math.round`),而不是你所用语言的原生舍入。PHP 的 `round()` 和许多其他实现会把 .5
**远离零** 舍入,这在负值正好落在 `.5` 边界时会产生差异(例如 `round(-0.40625 × 10000) / 10000` 是 `-0.4062`,不是 `-0.4063`)。
#### `integer(key, range) -> number`
返回闭区间内的整数,两端都包含。接受与 `float` 相同的
`{ min, max, step? }` 对象;`step` 出于对称性被接受,但会被忽略,因为整数本身就是按 1 递增。
```
function integer(seed, key, range):
min = min(range.min, range.max)
max = max(range.min, range.max)
return floor(getValue(seed, key) * (max - min + 1)) + min
```
#### `shuffle(key, items) -> items[]`
使用一个 **有状态** 的 Mulberry32 实例(不是基于 key 的)进行 Fisher-Yates 洗牌。对于长度 ≤ 1 的输入,项目会在不去重的情况下以副本形式返回。
```
function shuffle(seed, key, items):
if length(items) <= 1: return copy of items
unique = deduplicate items by string representation
sorted = sort unique by string representation
result = copy of sorted
prng = new Mulberry32(fnv1a_hash(seed + ":" + key))
for i from length(result) - 1 down to 1:
j = floor(prng.nextFloat() * (i + 1))
swap result[i] and result[j]
return result
```
注意:`shuffle` 是唯一一个直接使用有状态 PRNG 实例的方法
(多次调用 `nextFloat()`)。所有其他方法都调用 `getValue()`
,而 `getValue()` 会为每个 key 创建一个新的 PRNG。
## 选项解析
`Options` 类会将原始用户选项解析为渲染器使用的具体值。每次解析都会使用带有特定 key 的 PRNG。
### 核心选项
| 选项 | PRNG key | 解析结果 |
| ----------------- | -------------- | -------------------------------------------------------------------------- |
| `seed` | — | 字面字符串;如果未提供,默认为 `''`。不缓存。 |
| `size` | — | 字面数字;默认未设置(渲染器省略 `width`/`height`)。 |
| `idRandomization` | — | 布尔值;默认 `false`。使用宿主 RNG,而不是 DiceBear PRNG。 |
| `title` | — | 字面字符串;默认未设置(省略 `
`,使用 `aria-hidden`)。 |
| `flip` | `flip` | 从 `['none', 'horizontal', 'vertical', 'both']` 中 `pick`,默认 `'none'` |
| `rotate` | `rotate` | 范围内的 `float`,默认 `0` |
| `scale` | `scale` | 范围内的 `float`,默认 `1` |
| `borderRadius` | `borderRadius` | 范围内的 `float`,默认 `0` |
| `translateX` | `translateX` | 范围内的 `float`,默认 `0` |
| `translateY` | `translateY` | 范围内的 `float`,默认 `0` |
| `fontFamily` | `fontFamily` | 从数组中 `pick`,默认 `'system-ui'` |
| `fontWeight` | `fontWeight` | 从数组中 `pick`,默认 `400` |
没有 PRNG key 的选项会直接从用户输入中读取。其余选项会在给定 key 下从用户提供的范围/列表中采样,
并在未提供时回退到列出的默认值。
范围选项(`rotate`、`scale`、`borderRadius`、`translateX`、
`translateY`,以及每种颜色的 `${name}ColorAngle` / `${name}ColorFillStops`)
接受数字或数组,并在 `float`/`integer` 采样前规范化为 `{ min, max }` 范围:
- 裸数字 `n` → `{ min: n, max: n }`(固定值);
- 单元素数组 `[n]` → `{ min: n, max: n }`(同裸数字);
- 双元素数组 → 取两者中较小/较大的值作为 `{ min, max }`
(顺序无关,采样时也会交换);
- 空数组 `[]`,或该选项未设置 → 回退到列出的默认值。
注意边界情况:`[n]` 是固定值(**不是**默认值),而 `[]`
会回退到默认值(**不是**缺少边界的范围)。`min === max` 的固定范围
总会采样到那个精确值。
### 组件选项
对于每个组件(例如 `eyes`),用户可以提供恰好两个选项:
| 选项 | PRNG key | 解析结果 |
| ------------------ | ----------------- | -------------------------------------------------------------------------------------------------------------- |
| `eyesProbability` | `eyesProbability` | 根据用户选项中的概率执行 `bool`,未提供时回退到组件的 `probability`(如果未设置则为 `100`) |
| `eyesVariant` | `eyesVariant` | 在加权映射上执行 `weightedPick`(见下文) |
如果概率检查失败,则该组件不会被渲染,且 `variant`
返回 `undefined`。
`eyesVariant` 接受用户提供的三种形状:单个变体名、名称数组,或 `Record` 权重映射。前两种会被规范化为一个映射,其中每个命名变体的权重为 `1`。然后删除所有未在组件 `variants` 块中声明的键,并将剩余映射传给 `weightedPick`。当用户未提供该选项时,则根据各变体自身的 `weight` 值构建映射(默认为 `1`)。
对于 **组件别名**(通过定义中的 `extends` 声明),用户侧是共享的,只有 PRNG 侧是独立的。别名不会暴露自己的 `${aliasName}Probability` 或 `${aliasName}Variant` 用户选项。这两者都从源组件的 `${sourceName}Probability` 和
`${sourceName}Variant` 中读取。然而,PRNG 使用别名自身的名称作为 key
(`${aliasName}Probability`、`${aliasName}Variant`),因此每个别名都会独立决定其可见性和变体,同时仍受相同的用户设置权重约束。
### 每个组件的变换(渲染时)
每个组件引用在渲染时还会应用旋转、两个平移和一个缩放。这些都**不是用户选项**:它们会在每次渲染时根据组件定义的
`rotate`/`translate`/`scale` 范围采样。它们会出现在内省用的 `resolvedOptions` 快照中的 `${name}Rotate` /
`${name}TranslateX` / `${name}TranslateY` / `${name}Scale` 下,但它们不是面向用户的 `StyleOptions` 类型的一部分,也不支持将它们回传给新的 `Avatar`。
| 值 | PRNG key | 采样 |
| ---------- | ---------------- | --------------------------------------------- |
| rotate | `eyesRotate` | 来自 `component.rotate` 的 `float`,默认 `0` |
| translateX | `eyesTranslateX` | 来自 `component.translate.x` 的 `float`,默认 `0` |
| translateY | `eyesTranslateY` | 来自 `component.translate.y` 的 `float`,默认 `0` |
| scale | `eyesScale` | 来自 `component.scale` 的 `float`,默认 `1` |
平移值是 **组件自身** `width` 和 `height` 的百分比(不是 avatar 画布);将其乘以组件尺寸即可得到偏移量。像所有输出数字一样,它随后会经过
[`formatNumber`](#number-formatting) 处理(将其限制到小数点后 5 位)。旋转和缩放的变换中心 `(cx, cy)` 是组件自身的中心:
`(width / 2, height / 2)`。
在生成的 SVG 中,非恒等值会以空格分隔后连接成一个单独的 `transform` 属性,写在 `