mochikao 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.fr.md CHANGED
@@ -16,7 +16,7 @@ Un visage pour chaque nom. `mochikao` transforme n'importe quelle chaîne (pseud
16
16
  - 5 yeux, 6 bouches, 11 accessoires (joues, taches, antenne, pousse, reflet, lunettes, lunettes noires, nœud, couronne, chapeau de fête).
17
17
  - **10 axes continus** réglables (taille et proportion du corps, carrure, ondulation, taille/écart/inclinaison des yeux, regard x/y, taille de la bouche) et 6 préréglages de couleur.
18
18
  - **8 expressions** : `idle`, `happy`, `sad`, `mad`, `love`, `shy`, `sick`, `thinking`.
19
- - **Contraste visage/corps ≥ 4.5:1** pour toute teinte et tout ton (vérifié par les tests).
19
+ - **Contraste visage/corps ≥ 4.5:1** pour toute teinte et tout ton (vérifié par les tests). L'avatar contre *ta* page n'est pas garanti : voir [Contraste](docs/editor.fr.md#8-contraste--ce-qui-est-garanti-et-ce-qui-ne-lest-pas).
20
20
  - **Animations CSS** (respiration, clignement, coups d'œil), **transition animée entre expressions** et **regard qui suit le pointeur**.
21
21
  - Fonction, composants **React / Vue / Svelte / Solid**, web component, API HTTP et CLI.
22
22
 
@@ -132,8 +132,93 @@ mochikao({ name: 'alex', family: produce, shape: 'lemon' });
132
132
 
133
133
  Choisir une famille ne change que la forme : yeux, bouche, accessoire et réglages restent ceux du nom. Là où les options arrivent en texte (attributs HTML, URL, CLI), on déclare les familles disponibles une fois : `defineMochikao({ families: [animals] })` puis `<mochikao-avatar family="animals">`, ou `parseParams(nom, params, [animals, produce])`. L'API et la CLI fournies acceptent les deux.
134
134
 
135
+ **Mélanger des familles.** `mix()` combine plusieurs familles : le nom choisit alors parmi toutes leurs formes, chacune avec la même chance.
136
+
137
+ ```ts
138
+ import { mochikao, blobs, mix } from 'mochikao';
139
+
140
+ mochikao({ name: 'alex', family: mix(blobs, animals) }); // blobs + animaux
141
+ mochikao({ name: 'alex', family: mix(animals, produce) }); // animaux + fruits & légumes
142
+ mochikao({ name: 'alex', family: mix(blobs, animals, produce) }); // tout
143
+ ```
144
+
145
+ | Mode | `family=` | Formes |
146
+ | --- | --- | --- |
147
+ | Blobs (défaut) | `blob` | 10 |
148
+ | Animaux | `animals` | 6 |
149
+ | Fruits & légumes | `produce` | 6 |
150
+ | Blobs + animaux | `animals+blob` | 16 |
151
+ | Blobs + fruits & légumes | `blob+produce` | 16 |
152
+ | Animaux + fruits & légumes | `animals+produce` | 12 |
153
+ | Tout | `all` (ou `animals+blob+produce`) | 22 |
154
+
155
+ Dans les URL, attributs HTML et la CLI, on joint les noms avec `+` (`?family=blob+animals`) ; `all` prend toutes les familles déclarées plus les blobs. L'ordre n'a pas d'importance, et un mélange des mêmes familles donne toujours le même avatar pour un nom.
156
+
135
157
  Une famille, c'est un objet `ShapeFamily` (`name`, `shapes`, `silhouette`, et en option `decorate` et `hue`) : `src/families/` montre comment en écrire une.
136
158
 
159
+ ### Laisser les utilisateurs personnaliser leur avatar
160
+
161
+ mochikao n'embarque pas de sélecteur : un avatar, c'est `nom + options épinglées`. Ton appli enregistre ce que l'utilisateur a touché et le repasse. Ne stocke que les clés épinglées ; supprimer une clé remet ce trait en « auto » (tiré du nom).
162
+
163
+ ```tsx
164
+ import { useState } from 'react';
165
+ import { Mochikao } from 'mochikao/react';
166
+ import { EYES, MOUTHS, EXTRAS, resolve, mix, blobs, type MochikaoOptions } from 'mochikao';
167
+ import { animals } from 'mochikao/animals';
168
+
169
+ const family = mix(blobs, animals); // les formes que tu proposes
170
+ type Prefs = Omit<MochikaoOptions, 'name' | 'family'>;
171
+
172
+ const CHOICES = { shape: family.shapes, eyes: EYES, mouth: MOUTHS, extra: EXTRAS } as const;
173
+
174
+ export function AvatarEditor({ userId, saved, onSave }: {
175
+ userId: string; // un id stable, pas un pseudo modifiable
176
+ saved: Prefs; // lu en base
177
+ onSave: (prefs: Prefs) => void;
178
+ }) {
179
+ const [prefs, setPrefs] = useState<Prefs>(saved);
180
+ const auto = resolve({ name: userId, family }); // ce que donne le nom quand rien n'est épinglé
181
+
182
+ // value === undefined remet le trait en « auto »
183
+ const pin = (key: keyof typeof CHOICES, value?: string) =>
184
+ setPrefs(({ [key]: _, ...rest }) => (value ? { ...rest, [key]: value } : rest));
185
+
186
+ return (
187
+ <div>
188
+ <Mochikao name={userId} family={family} size={128} {...prefs} />
189
+
190
+ {(Object.keys(CHOICES) as (keyof typeof CHOICES)[]).map((key) => (
191
+ <label key={key}>
192
+ {key}
193
+ <select value={prefs[key] ?? ''} onChange={(e) => pin(key, e.target.value || undefined)}>
194
+ <option value="">auto ({auto[key]})</option>
195
+ {CHOICES[key].map((v) => <option key={v}>{v}</option>)}
196
+ </select>
197
+ </label>
198
+ ))}
199
+
200
+ <label>
201
+ teinte
202
+ <input type="range" min={0} max={360} value={prefs.hue ?? auto.hue}
203
+ onChange={(e) => setPrefs({ ...prefs, hue: +e.target.value })} />
204
+ </label>
205
+
206
+ <button onClick={() => onSave(prefs)}>Enregistrer</button>
207
+ <button onClick={() => setPrefs({})}>Réinitialiser</button>
208
+ </div>
209
+ );
210
+ }
211
+ ```
212
+
213
+ Partout ailleurs dans l'appli, on l'affiche pareil : `<Mochikao name={user.id} family={family} {...user.avatarPrefs} />`.
214
+
215
+ - **Stocker du texte** : `toParams(prefs).toString()` donne une chaîne compacte (`shape=frog&eyes=sleepy&hue=140`) et `parseParams(userId, new URLSearchParams(str), [animals])` la relit. Les valeurs invalides sont ignorées : c'est la bonne façon de valider ce qui vient du client.
216
+ - **Même avatar ailleurs** : construis `/api/<id>.svg?family=animals+blob&<prefs>` pour les e-mails, les images OG ou les clients sans JS.
217
+ - **Aperçus** : une tuile, c'est `mochikao({ name, family, ...prefs, eyes: 'wide' })`.
218
+ - **Rester sobre** : plus on laisse épingler de traits, moins deux utilisateurs se distinguent.
219
+
220
+ Le guide complet (modèle, pièges, référence sans framework, aperçus, validation côté serveur) est dans [docs/editor.fr.md](docs/editor.fr.md), avec aussi une version Vue 3. Un exemple Vue complet, proche de l'éditeur du site, est dans [examples/vue/AvatarEditor.vue](examples/vue/AvatarEditor.vue).
221
+
137
222
  ### Transition entre expressions
138
223
 
139
224
  En SVG en ligne (`animate`, `inline` ou `gaze`), changer `expression` déclenche une petite transition : les yeux clignent, la bouche se referme, le visage change pendant qu'ils sont fermés, puis tout se rouvre avec un rebond (~450 ms). Les composants et le web component le font tout seuls. Un `<img>` ne peut pas s'animer : en mode image, le changement est immédiat.
@@ -230,6 +315,44 @@ La CI (`.github/workflows/ci.yml`) vérifie types, tests et builds sur Node 22 e
230
315
 
231
316
  Une fois publié, le rendu des avatars est un contrat : voir Stabilité.
232
317
 
318
+ ## Crédit
319
+
320
+ mochikao est sous licence MIT : tu l'utilises librement, la mention n'est pas obligatoire. Si tu l'embarques, une citation aide beaucoup, et j'aimerais voir où il atterrit :
321
+
322
+ - **Dans ton appli** : une ligne en pied de page ou sur la page « à propos », par exemple `Avatars par <a href="https://mochikao.alexvolkihar.ovh">mochikao</a>`.
323
+ - **Dans ton dépôt** : un lien vers <https://github.com/Alexvolkihar/mochikao> dans ton README (section crédits ou remerciements).
324
+ - **Dis-le-moi** : [ouvre une issue](https://github.com/Alexvolkihar/mochikao/issues/new) avec un lien vers ton projet et j'envisagerai de le lister.
325
+
326
+ ### Badge
327
+
328
+ Un petit badge SVG pour un README ou une page « à propos », en version **sombre** et **claire** (texte au-dessus de 4.5:1 dans les deux, et une bordure qui tient sur une page blanche ou sombre) :
329
+
330
+ | Sombre | Clair |
331
+ | --- | --- |
332
+ | [![avatars by mochikao](https://mochikao.alexvolkihar.ovh/badge-dark.svg)](https://mochikao.alexvolkihar.ovh) | [![avatars by mochikao](https://mochikao.alexvolkihar.ovh/badge-light.svg)](https://mochikao.alexvolkihar.ovh) |
333
+ | `badge-dark.svg` | `badge-light.svg` |
334
+
335
+ ```md
336
+ [![avatars by mochikao](https://mochikao.alexvolkihar.ovh/badge-dark.svg)](https://mochikao.alexvolkihar.ovh)
337
+ ```
338
+
339
+ ```html
340
+ <a href="https://mochikao.alexvolkihar.ovh"><img src="https://mochikao.alexvolkihar.ovh/badge-dark.svg" alt="avatars by mochikao" height="24"></a>
341
+ ```
342
+
343
+ Pour suivre automatiquement le thème du lecteur (README GitHub et toute page qui gère `<picture>`) :
344
+
345
+ ```html
346
+ <a href="https://mochikao.alexvolkihar.ovh">
347
+ <picture>
348
+ <source media="(prefers-color-scheme: dark)" srcset="https://mochikao.alexvolkihar.ovh/badge-light.svg">
349
+ <img src="https://mochikao.alexvolkihar.ovh/badge-dark.svg" alt="avatars by mochikao" height="24">
350
+ </picture>
351
+ </a>
352
+ ```
353
+
354
+ `badge.svg` reste un alias de la version sombre. Les fichiers sont dans [`site/public/`](site/public/) si tu préfères en héberger une copie.
355
+
233
356
  ## Licence
234
357
 
235
358
  MIT
package/README.md CHANGED
@@ -16,7 +16,7 @@ A face for every name. `mochikao` turns any string (username, email, ID) into a
16
16
  - 5 eyes, 6 mouths, 11 accessories (blush, freckles, antenna, sprout, shine, glasses, sunglasses, bow, crown, party hat).
17
17
  - **10 continuous axes** you can tune (body size and proportion, squareness, wobble, eye size/spacing/lean, gaze x/y, mouth size) and 6 color presets.
18
18
  - **8 expressions**: `idle`, `happy`, `sad`, `mad`, `love`, `shy`, `sick`, `thinking`.
19
- - **Face-to-body contrast ≥ 4.5:1** for every hue and tone (checked by the tests).
19
+ - **Face-to-body contrast ≥ 4.5:1** for every hue and tone (checked by the tests). The avatar against *your* page is not guaranteed: see [Contrast](docs/editor.md#8-contrast-what-is-guaranteed-and-what-is-not).
20
20
  - **CSS animations** (breathing, blinking, glances), **animated transitions between expressions** and **eyes that follow the pointer**.
21
21
  - A function, **React / Vue / Svelte / Solid** components, a web component, an HTTP API and a CLI.
22
22
 
@@ -132,8 +132,93 @@ mochikao({ name: 'alex', family: produce, shape: 'lemon' });
132
132
 
133
133
  Picking a family only changes the shape: eyes, mouth, accessory and settings still come from the name. Where options arrive as text (HTML attributes, URLs, CLI), declare the available families once: `defineMochikao({ families: [animals] })` then `<mochikao-avatar family="animals">`, or `parseParams(name, params, [animals, produce])`. The bundled API and CLI accept both.
134
134
 
135
+ **Mixing families.** `mix()` combines several families: the name then picks among all their shapes, each shape equally likely.
136
+
137
+ ```ts
138
+ import { mochikao, blobs, mix } from 'mochikao';
139
+
140
+ mochikao({ name: 'alex', family: mix(blobs, animals) }); // blobs + animals
141
+ mochikao({ name: 'alex', family: mix(animals, produce) }); // animals + fruits & veggies
142
+ mochikao({ name: 'alex', family: mix(blobs, animals, produce) }); // everything
143
+ ```
144
+
145
+ | Mode | `family=` | Shapes |
146
+ | --- | --- | --- |
147
+ | Blobs (default) | `blob` | 10 |
148
+ | Animals | `animals` | 6 |
149
+ | Fruits & veggies | `produce` | 6 |
150
+ | Blobs + animals | `animals+blob` | 16 |
151
+ | Blobs + fruits & veggies | `blob+produce` | 16 |
152
+ | Animals + fruits & veggies | `animals+produce` | 12 |
153
+ | Everything | `all` (or `animals+blob+produce`) | 22 |
154
+
155
+ In URLs, HTML attributes and the CLI, join names with `+` (`?family=blob+animals`); `all` takes every family you declared plus the blobs. The order doesn't matter, and a mix with the same families always gives the same avatar for a given name.
156
+
135
157
  A family is a `ShapeFamily` object (`name`, `shapes`, `silhouette`, and optionally `decorate` and `hue`): `src/families/` shows how to write one.
136
158
 
159
+ ### Letting users customize their avatar
160
+
161
+ mochikao has no built-in picker: an avatar is `name + pinned options`, so your app stores what the user touched and passes it back. Store only the pinned keys; removing a key puts that trait back on "auto" (drawn from the name).
162
+
163
+ ```tsx
164
+ import { useState } from 'react';
165
+ import { Mochikao } from 'mochikao/react';
166
+ import { EYES, MOUTHS, EXTRAS, resolve, mix, blobs, type MochikaoOptions } from 'mochikao';
167
+ import { animals } from 'mochikao/animals';
168
+
169
+ const family = mix(blobs, animals); // the shapes you offer
170
+ type Prefs = Omit<MochikaoOptions, 'name' | 'family'>;
171
+
172
+ const CHOICES = { shape: family.shapes, eyes: EYES, mouth: MOUTHS, extra: EXTRAS } as const;
173
+
174
+ export function AvatarEditor({ userId, saved, onSave }: {
175
+ userId: string; // a stable ID, not a renamable pseudonym
176
+ saved: Prefs; // loaded from your database
177
+ onSave: (prefs: Prefs) => void;
178
+ }) {
179
+ const [prefs, setPrefs] = useState<Prefs>(saved);
180
+ const auto = resolve({ name: userId, family }); // what the name gives when nothing is pinned
181
+
182
+ // value === undefined puts the trait back to "auto"
183
+ const pin = (key: keyof typeof CHOICES, value?: string) =>
184
+ setPrefs(({ [key]: _, ...rest }) => (value ? { ...rest, [key]: value } : rest));
185
+
186
+ return (
187
+ <div>
188
+ <Mochikao name={userId} family={family} size={128} {...prefs} />
189
+
190
+ {(Object.keys(CHOICES) as (keyof typeof CHOICES)[]).map((key) => (
191
+ <label key={key}>
192
+ {key}
193
+ <select value={prefs[key] ?? ''} onChange={(e) => pin(key, e.target.value || undefined)}>
194
+ <option value="">auto ({auto[key]})</option>
195
+ {CHOICES[key].map((v) => <option key={v}>{v}</option>)}
196
+ </select>
197
+ </label>
198
+ ))}
199
+
200
+ <label>
201
+ hue
202
+ <input type="range" min={0} max={360} value={prefs.hue ?? auto.hue}
203
+ onChange={(e) => setPrefs({ ...prefs, hue: +e.target.value })} />
204
+ </label>
205
+
206
+ <button onClick={() => onSave(prefs)}>Save</button>
207
+ <button onClick={() => setPrefs({})}>Reset</button>
208
+ </div>
209
+ );
210
+ }
211
+ ```
212
+
213
+ Anywhere else in your app, display it the same way: `<Mochikao name={user.id} family={family} {...user.avatarPrefs} />`.
214
+
215
+ - **Store text**: `toParams(prefs).toString()` gives a compact string (`shape=frog&eyes=sleepy&hue=140`) and `parseParams(userId, new URLSearchParams(str), [animals])` reads it back. Invalid values are ignored, which makes it the right way to validate what comes from the client.
216
+ - **Same avatar elsewhere**: build `/api/<id>.svg?family=animals+blob&<prefs>` for emails, OG images or non-JS clients.
217
+ - **Previews**: a tile is just `mochikao({ name, family, ...prefs, eyes: 'wide' })`.
218
+ - **Keep it small**: the more traits you let people pin, the less two users differ.
219
+
220
+ The full guide (model, pitfalls, a framework-free reference, previews, server-side validation) is in [docs/editor.md](docs/editor.md), with a Vue 3 version as well. A complete Vue example, close to the site's editor, is in [examples/vue/AvatarEditor.vue](examples/vue/AvatarEditor.vue).
221
+
137
222
  ### Transitions between expressions
138
223
 
139
224
  With inline SVG (`animate`, `inline` or `gaze`), changing `expression` plays a short transition: the eyes blink, the mouth closes, the face changes while they are shut, then everything reopens with a little bounce (~450 ms). The components and the web component do it on their own. An `<img>` cannot animate: in image mode the change is immediate.
@@ -230,6 +315,44 @@ CI (`.github/workflows/ci.yml`) checks types, tests and builds on Node 22 and 24
230
315
 
231
316
  Once published, the avatars' rendering is a contract: see Stability.
232
317
 
318
+ ## Credit
319
+
320
+ mochikao is MIT: you can use it freely, credit is not required. If you ship it, though, a mention helps a lot, and I'd love to see where it ends up:
321
+
322
+ - **In your app**: a line in the footer or the "about" page, such as `Avatars by <a href="https://mochikao.alexvolkihar.ovh">mochikao</a>`.
323
+ - **In your repo**: a link to <https://github.com/Alexvolkihar/mochikao> in your README (credits or acknowledgements section).
324
+ - **Tell me**: [open an issue](https://github.com/Alexvolkihar/mochikao/issues/new) with a link to your project and I'll consider listing it.
325
+
326
+ ### Badge
327
+
328
+ A small SVG badge for a README or an "about" page, in a **dark** and a **light** version (text above 4.5:1 in both, and a border that holds on a white or a dark page):
329
+
330
+ | Dark | Light |
331
+ | --- | --- |
332
+ | [![avatars by mochikao](https://mochikao.alexvolkihar.ovh/badge-dark.svg)](https://mochikao.alexvolkihar.ovh) | [![avatars by mochikao](https://mochikao.alexvolkihar.ovh/badge-light.svg)](https://mochikao.alexvolkihar.ovh) |
333
+ | `badge-dark.svg` | `badge-light.svg` |
334
+
335
+ ```md
336
+ [![avatars by mochikao](https://mochikao.alexvolkihar.ovh/badge-dark.svg)](https://mochikao.alexvolkihar.ovh)
337
+ ```
338
+
339
+ ```html
340
+ <a href="https://mochikao.alexvolkihar.ovh"><img src="https://mochikao.alexvolkihar.ovh/badge-dark.svg" alt="avatars by mochikao" height="24"></a>
341
+ ```
342
+
343
+ To follow the reader's theme automatically (GitHub READMEs and any page that supports `<picture>`):
344
+
345
+ ```html
346
+ <a href="https://mochikao.alexvolkihar.ovh">
347
+ <picture>
348
+ <source media="(prefers-color-scheme: dark)" srcset="https://mochikao.alexvolkihar.ovh/badge-light.svg">
349
+ <img src="https://mochikao.alexvolkihar.ovh/badge-dark.svg" alt="avatars by mochikao" height="24">
350
+ </picture>
351
+ </a>
352
+ ```
353
+
354
+ `badge.svg` is kept as an alias of the dark one. The files live in [`site/public/`](site/public/) if you prefer to host a copy.
355
+
233
356
  ## License
234
357
 
235
358
  MIT
@@ -14,7 +14,7 @@ const USAGE = `usage : mochikao <nom> [options]
14
14
  --tone <0-1> luminosité
15
15
  --expression <e> idle | happy | sad | mad | love | shy | sick | thinking
16
16
  --animate <a> none | hover | always
17
- --family <f> blob | animals | produce
17
+ --family <f> blob | animals | produce | all, ou un mélange : blob+animals, animals+produce...
18
18
  --color <c> pastel | pale | mid | deep | bright | ink
19
19
  --shape --eyes --mouth --extra épingle un trait
20
20
  --info affiche les traits au lieu du SVG`;
@@ -49,3 +49,16 @@ export interface ShapeFamily<S extends string = string> {
49
49
  hue?(shape: S): number | undefined;
50
50
  }
51
51
  export declare function isFamily(value: unknown): value is ShapeFamily;
52
+ /**
53
+ * Combine plusieurs familles en une : le nom choisit parmi toutes leurs formes.
54
+ *
55
+ * mix(blobs, animals) // blobs et animaux
56
+ * mix(blobs, animals, produce) // tout
57
+ *
58
+ * Chaque forme a la même chance d'être tirée, donc une famille pèse selon son
59
+ * nombre de formes. L'ordre des arguments ne compte pas : les familles sont
60
+ * triées par nom (le résultat s'appelle `animals+blob`), ce qui fixe la liste
61
+ * des formes et donc quel avatar sort pour quel nom. Si deux familles ont une
62
+ * forme du même nom, la première (dans l'ordre trié) l'emporte.
63
+ */
64
+ export declare function mix(...families: ShapeFamily[]): ShapeFamily;
@@ -3,3 +3,36 @@ export function isFamily(value) {
3
3
  return typeof f === 'object' && f !== null && typeof f.name === 'string' &&
4
4
  Array.isArray(f.shapes) && f.shapes.length > 0 && typeof f.silhouette === 'function';
5
5
  }
6
+ /**
7
+ * Combine plusieurs familles en une : le nom choisit parmi toutes leurs formes.
8
+ *
9
+ * mix(blobs, animals) // blobs et animaux
10
+ * mix(blobs, animals, produce) // tout
11
+ *
12
+ * Chaque forme a la même chance d'être tirée, donc une famille pèse selon son
13
+ * nombre de formes. L'ordre des arguments ne compte pas : les familles sont
14
+ * triées par nom (le résultat s'appelle `animals+blob`), ce qui fixe la liste
15
+ * des formes et donc quel avatar sort pour quel nom. Si deux familles ont une
16
+ * forme du même nom, la première (dans l'ordre trié) l'emporte.
17
+ */
18
+ export function mix(...families) {
19
+ const unique = [...new Map(families.filter(isFamily).map((f) => [f.name, f])).values()]
20
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
21
+ if (unique.length === 0)
22
+ throw new TypeError('mix() attend au moins une famille');
23
+ if (unique.length === 1)
24
+ return unique[0];
25
+ const owner = new Map();
26
+ for (const f of unique)
27
+ for (const s of f.shapes)
28
+ if (!owner.has(s))
29
+ owner.set(s, f);
30
+ const of = (shape) => owner.get(shape) ?? unique[0];
31
+ return {
32
+ name: unique.map((f) => f.name).join('+'),
33
+ shapes: [...owner.keys()],
34
+ silhouette: (shape, traits) => of(shape).silhouette(shape, traits),
35
+ decorate: (shape, body, colors, eyeY) => of(shape).decorate?.(shape, body, colors, eyeY) ?? {},
36
+ hue: (shape) => of(shape).hue?.(shape),
37
+ };
38
+ }
@@ -3,7 +3,7 @@ import { type ShapeFamily } from './family.ts';
3
3
  import { type Animation, type Background, type ColorPreset, type Expression, type ExtraName, type EyesName, type MouthName, type Traits } from './traits.ts';
4
4
  export * from './traits.ts';
5
5
  export { blobs } from './shape.ts';
6
- export { isFamily } from './family.ts';
6
+ export { isFamily, mix } from './family.ts';
7
7
  export type { Decoration, ShapeDef, ShapeFamily } from './family.ts';
8
8
  export { palette, contrast, hslToRgb } from './color.ts';
9
9
  export type { Palette } from './color.ts';
package/dist/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { blobs, buildBody, superellipsePath } from "./shape.js";
6
6
  import { ANIMATIONS, BACKGROUNDS, COLORS, EXPRESSIONS, EXTRAS, EYES, MOUTHS, deriveTraits, pick, positionOf, } from "./traits.js";
7
7
  export * from "./traits.js";
8
8
  export { blobs } from "./shape.js";
9
- export { isFamily } from "./family.js";
9
+ export { isFamily, mix } from "./family.js";
10
10
  export { palette, contrast, hslToRgb } from "./color.js";
11
11
  /** Ce que le nom donne, sans dessiner. */
12
12
  export function resolve(options) {
@@ -1,5 +1,5 @@
1
1
  import type { MochikaoOptions } from './index.ts';
2
- import type { ShapeFamily } from './family.ts';
2
+ import { type ShapeFamily } from './family.ts';
3
3
  type Source = {
4
4
  get(key: string): string | null | undefined;
5
5
  };
@@ -8,6 +8,8 @@ type Source = {
8
8
  * Les valeurs invalides sont ignorées plutôt que de faire échouer le rendu.
9
9
  * `families` liste les familles qu'on accepte de désigner par leur nom
10
10
  * (`?family=animals`) ; sans elles, seuls les blobs sont disponibles.
11
+ * Plusieurs familles se combinent avec « + » (`?family=blob+animals`), et
12
+ * `family=all` prend toutes celles qu'on fournit.
11
13
  */
12
14
  export declare function parseParams(name: string, src: Source, families?: readonly ShapeFamily[]): MochikaoOptions;
13
15
  /**
@@ -1,3 +1,4 @@
1
+ import { mix } from "./family.js";
1
2
  import { blobs } from "./shape.js";
2
3
  import { ANIMATIONS, BACKGROUNDS, COLORS, CONTINUOUS_AXES, EXPRESSIONS, EXTRAS, EYES, MOUTHS } from "./traits.js";
3
4
  const oneOf = (list, v) => v != null && list.includes(v) ? v : undefined;
@@ -7,11 +8,27 @@ const num = (v, min, max) => {
7
8
  const n = Number(v);
8
9
  return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : undefined;
9
10
  };
11
+ /**
12
+ * `animals`, `blob`, ou un mode combiné : `blob+animals`, `animals+produce`...
13
+ * `all` prend les blobs et toutes les familles fournies. Les noms inconnus
14
+ * sont ignorés ; sans aucun nom valide, on n'a pas de famille.
15
+ */
16
+ function findFamily(value, families) {
17
+ if (!value)
18
+ return undefined;
19
+ const known = [blobs, ...families];
20
+ if (value === 'all')
21
+ return mix(...known);
22
+ const found = value.split(/[+ ]/).map((n) => known.find((f) => f.name === n)).filter((f) => f !== undefined);
23
+ return found.length ? mix(...found) : undefined;
24
+ }
10
25
  /**
11
26
  * Options depuis des paramètres texte (query string, attributs HTML, flags CLI).
12
27
  * Les valeurs invalides sont ignorées plutôt que de faire échouer le rendu.
13
28
  * `families` liste les familles qu'on accepte de désigner par leur nom
14
29
  * (`?family=animals`) ; sans elles, seuls les blobs sont disponibles.
30
+ * Plusieurs familles se combinent avec « + » (`?family=blob+animals`), et
31
+ * `family=all` prend toutes celles qu'on fournit.
15
32
  */
16
33
  export function parseParams(name, src, families = []) {
17
34
  const options = { name };
@@ -26,7 +43,7 @@ export function parseParams(name, src, families = []) {
26
43
  set('expression', oneOf(EXPRESSIONS, src.get('expression')));
27
44
  set('animate', oneOf(ANIMATIONS, src.get('animate')));
28
45
  // `family` désigne une famille par son nom, parmi celles qu'on nous a confiées.
29
- const family = families.find((fam) => fam.name === src.get('family'));
46
+ const family = findFamily(src.get('family'), families);
30
47
  set('family', family);
31
48
  set('shape', oneOf((family ?? blobs).shapes, src.get('shape')));
32
49
  set('eyes', oneOf(EYES, src.get('eyes')));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mochikao",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Deterministic avatars: any string becomes a cute, unique SVG character. Zero dependencies, React / Vue / Svelte / Solid / web component.",
5
5
  "keywords": [
6
6
  "avatar",