Doop
Doop is an open-source multiplayer design canvas and design MCP for Claude Code, Codex, and any AI agent. Design side by side with agents — free in beta.
Procedural generator of hand-drawn ink faces. Deterministic, dependency-free, reusable in a game.
Translation pending
DesignJavaScript · HTMLGitHub @flocautankOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Procedural hand-drawn ink portraits. Deterministic, dependency-free, reusable in a game.
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗Générateur procédural de visages au stylo, dans l'esprit du post de @mannay — encre tremblée sur papier vieilli, une planche de têtes toutes différentes.
En ligne : https://flocautank.github.io/doodle-faces/
Une graine → toujours le même visage. Sur n'importe quelle machine, pour toujours. Tu stockes la graine, pas l'image.
Les modules ES exigent un serveur HTTP (file:// est bloqué par le navigateur) :
npx http-server . -p 5180 -c-1
Puis http://localhost:5180.
Déjà fait : le dépôt est publié par GitHub Pages depuis main, à la racine.
Un git push suffit à redéployer — la reconstruction prend une petite minute.
git push origin main
gh api repos/flocautank/doodle-faces/pages/builds/latest --jq .status # built ?
GitHub Pages met les fichiers en cache une dizaine de minutes. Pour qu'un
visiteur déjà venu ne se retrouve pas avec d'anciens fichiers,
tools/stamp-assets.mjs colle une empreinte du contenu sur toutes les URL
que la page charge : la CSS, app.js, et chaque import relatif du graphe de
modules. Chaque déploiement produit donc des URL neuves, et il n'y a plus rien
de périmé à servir. C'est lancé par npm run build, à faire avant de pousser :
npm run build && git commit -am "…" && git push origin main
Tamponner le graphe de modules n'est pas cosmétique. app.js portait une
empreinte et pas les modules qu'il importe : un visiteur venu dix minutes avant
un déploiement pouvait donc exécuter un app.js neuf contre d'anciens modules.
Ce n'est pas un style périmé, c'est un programme incohérent — et c'est
invisible, puisque tout se charge. Ça m'a coûté une fausse piste pendant la
vérification : le site en ligne rendait un visage différent du local pour un cas
que je venais de corriger, alors que l'origine servait démonstrablement le bon
fichier ; c'est l'onglet qui gardait d'anciens modules derrière un point
d'entrée neuf.
Une seule empreinte pour tout l'arbre, plutôt qu'une par fichier : l'arbre part ensemble de toute façon, et des empreintes par fichier obligeraient à réécrire le graphe de bas en haut à chaque build sans rien gagner.
Le site est entièrement statique — aucune étape de build, rien à compiler. Pousse le dossier tel quel :
Le seul point d'attention : servir les .js avec le type MIME
text/javascript, ce que fait tout serveur correct par défaut.
Les modules ES sont agréables à travailler mais exigent un serveur. Pour un hébergeur qui ne prend qu'un fichier — ou pour ouvrir la page directement depuis le disque :
node tools/build-single.mjs
Produit dist/doodle-single.html (page complète, ~220 kB, s'ouvre en
file://) et dist/artifact.html (contenu du body seul, pour les hôtes qui
fournissent l'enveloppe). esbuild aplatit le graphe de modules — c'est aussi lui
qui renomme les quelques clamp privés qui entreraient en collision une fois
concaténés. C'est un outil de développement : le site produit, lui, n'a toujours
aucune dépendance.
Deux modes sur le même générateur, barre latérale à gauche :
← → pour passer au suivant, f pour basculer de mode, r pour une
nouvelle graine.Le panneau Traits épingle un trait pour tout le monde ; le dé à côté de chaque menu ne relance que ce trait sur le portrait affiché, en laissant les autres et la pose intacts. Le panneau Proportions expose les nombres du génome en curseurs, et Genome le JSON complet, éditable.
Les libellés de l'interface sont en anglais, comme le code. Cette documentation reste en français — dis-le si tu la veux en anglais aussi.
localStorage
(k en mode focus, ou l'étoile). Tout accès au stockage est protégé : une
fenêtre privée ou un navigateur qui bloque les données de site ne casse rien,
la sélection vit juste le temps de la session.Panneau From a photo : on dépose un portrait, le générateur le mesure et dessine le visage le plus proche qu'il sache faire. C'est une ressemblance, pas un portrait — le trait reste un doodle.
La photo ne sort pas de l'onglet. Le site est statique, il n'y a aucun serveur vers qui l'envoyer. Le détecteur de visage est téléchargé vers ton navigateur (~6 Mo, une seule fois, au premier usage) et toute la mesure se fait sur place. Rien n'est téléversé, rien n'est écrit sur disque, rien n'est conservé d'une visite à l'autre, et Forget ou un rechargement libère tout. Le lien de partage transporte les nombres obtenus, jamais l'image.
La première version se passait de tout modèle : segmentation du visage par la couleur de peau, puis projections de lignes sombres pour trouver les yeux et la bouche. L'argument était qu'un doodle de quinze traits tient à une poignée de rapports grossiers, et que la précision sous-pixel d'un maillage serait jetée au moment de quantifier en « crâne carré ou en poire ».
L'argument était juste et il répondait à la mauvaise question. Ce n'est jamais la précision qui échouait, c'est la détection. Un corpus de dix-neuf vraies photos — un café, un bar, une forêt enneigée, une main posée sur la mâchoire, des gens en arrière-plan — a donné ceci :
| avant | après | |
|---|---|---|
| lunettes détectées (aucune sur les photos) | 68 % | 5 % |
| yeux lus comme des soucoupes | 84 % | 0 % |
| écart des yeux hors plage plausible | 53 % | 0 % |
| largeur de bouche effondrée | 53 % | 0 % |
| barbe ratée | 68 % | 11 % |
| lacet saturé à la butée | 63 % | 16 % |
| confiance médiane | 0,57 | 0,70 |
Sur plusieurs photos, la « largeur du visage » valait 240 à 291 px dans une image de 320 : le masque de peau avalait le cou, les bras, le bois du bar, le mur beige. Les tests synthétiques validaient la plomberie, pas la perception — un dessin plat sur fond uni ne ressemble à aucune photographie.
D'où le détecteur. Il ne remplace pas mes mesures : il leur donne un point d'ancrage, et rend géométrique ce qui était deviné.
measure.js (repères) et photo.js (pixels) produisent exactement le même
objet, si bien que fit.js ignore lequel il a reçu. Le second reste le repli
quand le modèle ne peut pas être téléchargé : une moins bonne réponse vaut mieux
que pas de réponse.
Chaque mesureur porte ses propres normes. Un ovale tracé sur des repères n'est pas la même forme qu'une tache de couleur : « largeur du visage » n'y désigne pas la même chose, et une base commune biaiserait silencieusement tous les visages de l'écart entre les deux définitions.
Ces normes sont lues sur le corpus — une personne, plus un second visage incident. Assez pour séparer un décalage de définition d'un trait personnel lorsque les deux sujets concordent, pas assez pour faire une population. Quand ils divergeaient, j'ai gardé la valeur anthropométrique publiée : le sujet principal mesure 0,50 d'écart oculaire contre 0,45 pour l'autre visage, donc 0,45 reste la norme et il est dessiné, à juste titre, un peu écarté des yeux.
src/faces/photo.js travaille sur un tampon RGBA — ni canvas, ni DOM,
donc testable hors navigateur. Un masque de peau en chrominance Cb/Cr (une boîte
étroite et remarquablement indépendante de la carnation) donne la silhouette ;
son profil de largeur ligne par ligne est le même objet que la courbe de
largeur de la supellipse du générateur.
L'échelle du crâne vient de la distance yeux → bouche, jamais du blob : le sommet du crâne est sous les cheveux et la ligne des cheveux ne tombe pas au même endroit chez tout le monde. Le menton est ensuite le coin où la mâchoire cesse de se resserrer, borné par deux prédictions indépendantes.
Quelques leçons payées comptant :
src/faces/fit.js décide, avec deux règles.
L'écart, pas l'absolu. Une tête dessinée ici est presque aussi large que haute ; un vrai crâne fait deux tiers. Donner le rapport brut aplatirait tout le monde. Chaque mesure est donc divisée par ce que donne un portrait ordinaire, puis le facteur obtenu est appliqué à la valeur ordinaire du générateur.
Les nombres sont assignés, les noms sont cherchés. Largeur, mâchoire, écart des yeux, largeur de bouche : il y a une mesure derrière chacun, on les pose. En revanche rien dans une photo ne dit « en poire » : ces traits reçoivent une table de poids, puis quelques centaines de génomes candidats sont notés contre la mesure et le plus proche gagne. C'est ce partage qui fait que le résultat ressemble à la personne et reste dessiné : la recherche ne touche pas aux asymétries faites main.
Deux garde-fous appris à l'usage. Les traits absents sont épinglés absents — laisser un chapeau à « improbable » en pose un sur un portrait sur trente, et un chapeau qui couvre le crâne efface en plus la chevelure qui, elle, avait été mesurée. Et les styles « gag » (larme, spirale, langue pendante) sont exclus de l'ajustement : charmants sur un inconnu de la planche, malvenus sur le visage de quelqu'un qui a confié sa photo. Ils restent accessibles en les épinglant.
Le bouton Another take rejoue l'ajustement avec une autre graine : la même personne, un autre dessin — barbe pleine ou barbe longue, lunettes rondes ou carrées — et non quelqu'un d'autre.
src/faces/measure.js garde toute la démarche et remplace les devinettes :
Cheveux, barbe, lunettes et carnation restent lus sur les pixels — aucun repère ne sait de quelle couleur sont les cheveux de quelqu'un — mais échantillonnés là où il faut plutôt que là où une projection le supposait.
Le détecteur de lunettes méritait une refonte à lui seul. Il mesurait jusqu'où l'obscurité s'étend en travers de la bande des yeux, ce qu'une paire de sourcils satisfait sans effort. Une monture se distingue par où elle va que le visage n'a rien : en travers de l'arête du nez, et en arrière sur la tempe vers l'oreille. Les deux tests doivent passer, et c'est le plus faible qui décide.
On ne teste pas « ça me ressemble », mais on peut tester la chaîne qui le
prétend : tools/synth-face.mjs fabrique un portrait dont les proportions sont
connues par construction, et npm test vérifie qu'elles ressortent.
npm test
Cinquante-cinq vérifications sur le chemin de repli : géométrie retrouvée, proportions qui bougent dans le bon sens, expression lue juste, barbe et lunettes détectées, plus les régressions ci-dessus explicitement verrouillées. Un visage synthétique est beaucoup plus facile qu'un vrai : réussir ici est un plancher, pas un plafond.
L'aveu utile est ailleurs, dans l'interface : la mesure est dessinée sur la photo. Un pourcentage de confiance que personne ne peut vérifier vaut bien moins que des lignes dont on voit qu'elles manquent les yeux. Une photo de face, nette, sur fond uni, bien éclairée donne le meilleur résultat ; de profil, dans la pénombre ou devant un fond chargé, la confiance chute et le dit.
Un seul curseur, de −1 (aigre) à +1 (ravi). Ce n'est pas un trait : c'est
une lentille que le renderer applique par-dessus les sourcils, les yeux et la
bouche déjà tirés au sort, pour qu'un personnage puisse changer d'humeur sans
devenir quelqu'un d'autre. mood fait la même chose pour toute une population.
Au-delà d'un seuil le style de bouche est substitué — on ne sourit pas avec une moue — mais tout le reste est continu : les sourcils montent et pivotent, les paupières se plissent, une paupière lourde tombe sur les yeux d'un visage qui en a assez.
Un détail sans lequel ça ne se lisait pas : la plupart des bouches n'ont aucune courbe à fléchir, et beaucoup de visages n'ont pas de sourcils du tout. D'où un signal universel — deux petits traits aux coins de la bouche, qui la retroussent ou l'abaissent quel que soit son style.
Trois choses rendaient les réglages poussifs, dans cet ordre d'importance :
lod (0..1) allonge les segments, réduit le nombre de
traits et écarte les hachures là où ça ne se voit pas. C'est aussi ce qui
rend le glisser fluide : détail grossier pendant le déplacement, redessin net
au relâchement.devicePixelRatio jusqu'à 3, soit 9× les pixels
pour un gain nul. Plafonné à 1,5 en dessous de 200 px.S'ajoute un rendu par tranches : la planche se dessine sur plusieurs frames avec un budget de 10 ms chacune, et s'annule si les réglages rebougent. Mesuré sur une planche de 160 visages pendant un balayage de curseur : aucune tâche longue (>50 ms). En mode focus, un portrait de 560 px se redessine en ~3 ms, soit largement de quoi tourner la tête au doigt.
src/faces/
rng.js PRNG déterministe (mulberry32) + bruit lissé
genome.js traits d'un visage → objet JSON. Aucune notion de dessin.
kin.js les espèces : proportions de crâne, échelle des traits
looks.js les archétypes : biais de style, accessoires, palette
presets.js les populations : carnet, humains, foule, enfants, joueurs, taverne, carnaval
recipes.js les portraits épinglés : un personnage précis, pas une population
breed.js croisement de deux génomes : des frères et sœurs, une famille
landmarks.js trouver le visage (le seul module à dépendance, chargé en différé)
measure.js mesurer à partir des repères
photo.js mesurer sans modèle — le repli, aucun DOM, aucun réseau
fit.js transformer l'une ou l'autre mesure en génome
tools/
build-single.mjs tout aplatir en un fichier HTML autonome
synth-face.mjs un portrait synthétique aux proportions connues
test-photo.mjs vérifier que la mesure les retrouve
anatomy.js le génome → géométrie concrète dans une boîte de 100 unités
ink.js les primitives "stylo à main levée" + la texture papier
color.js la couche couleur : lavis mats, hors repérage
features.js oreilles, yeux, sourcils, nez, bouche, taches
hair.js cheveux et pilosité faciale
accessories.js lunettes et chapeaux
face.js l'API publique
atlas.js export atlas + manifeste pour un moteur de jeu
godot/
doodle_faces.gd chargeur d'atlas pour Godot 4
index.html app.js style.css le studio (barre latérale + planche + focus)
Deux couches nettement séparées, et c'est tout l'intérêt :
{ head: { shape: 'pear', … }, … }) ;Tu peux donc générer, sauvegarder, éditer et rejouer des visages sans jamais toucher au dessin — et inversement changer le style de trait sans changer un seul visage.
C'est là que tout se joue, et ça tient en quatre idées :
Le fond papier est procédural : teinte chaude, taches diffuses, grain fin (une tuile de bruit mise en cache), quelques fibres, vignettage léger.
Environ un visage sur cinq reçoit une touche de couleur, rarement deux. Le principe qui fait tout marcher : le lavis ne se superpose jamais exactement à l'encre. Il est décalé de quelques unités, légèrement redimensionné et pivoté, avec un bord irrégulier — comme une impression dont la plaque de couleur a glissé. Posé pile sur la forme, ça se lirait comme un remplissage ; décalé, ça se lit comme une couleur passée à la main.
La palette est volontairement mate et désaturée (terracotta, rose poudré, sauge, sarcelle, moutarde, ardoise, prune, brique, olive) — aucune couleur saturée. Les cibles possibles : la masse de cheveux, un chapeau, une barbe, les joues, un verre de lunettes, une pastille pleine à côté de la tête, ou un bloc rectangulaire posé de travers sur une partie du visage.
Deux garde-fous : la couche couleur est peinte sous l'encre, pour que le
trait reste lisible ; et les éléments dessinés en aplat noir (cheveux, chapeau ou
barbe de teinte dark) ne sont jamais choisis comme cible — un lavis sous du
noir plein est un lavis qu'on ne voit pas.
Le curseur Couleur du site, et l'option color de l'API, pilotent la
fréquence : 0 pour du pur noir et blanc, 1 par défaut, 2–3 pour insister.
C'est la question qui revient, alors autant l'écrire noir sur blanc. Quatre couches se superposent, de la plus générale à la plus précise, et chacune n'écrase que ce dont elle parle :
DEFAULT_WEIGHTS les proportions de base d'une foule quelconque
↓
population qui se présente : quelles espèces, quels archétypes,
(preset) et éventuellement des goûts communs (une foule discrète
porte peu de chapeaux)
↓
espèce (kin) ce qu'est la créature : crâne, échelle des traits,
et ce qu'elle porte volontiers
↓
archétype (look) ce que le personnage joue : coiffure, accessoires, palette
↓
force ce que tu imposes toi-même — gagne sur tout
Autrement dit : la population est une distribution, l'espèce et l'archétype
sont des propriétés d'un individu. Sur le site, la population est un menu à
part ; les autres menus imposent un trait à tout le monde et l'emportent sur
elle. Choisir population taverne + espèce orc donne 100 % d'orcs, pas la
répartition de la taverne.
L'ordre a été inversé pendant un temps — la population était appliquée en dernier et écrasait l'archétype. Résultat : sur la population « foule », choisir « punk à chien » ne changeait strictement rien, parce que la table de coiffures de la foule repassait par-dessus celle du punk. C'est corrigé ; si tu ajoutes une couche, garde ce sens-là.
kin.js décrit ce qu'est la créature, en quatre blocs de données :
metrics — les proportions du crâne, appliquées par-dessus la forme de tête ;scale — la taille relative de chaque trait (le nez d'un gnome fait 1,7× celui d'un humain) ;lines — la hauteur des lignes yeux / nez / bouche sur le visage (un enfant les a très basses) ;traits — les interrupteurs durs : défenses, oreilles pointues, arcade sourcilière, barbe obligatoire ;weights — ce que l'espèce porte volontiers (un orc a rarement un carré bien coiffé).Douze espèces : humain, enfant, ancien, elfe, nain, gnome, orc, troll, gobelin, mort-vivant, démon, homme-bête. Rien n'est codé en dur dans le renderer : ajouter une espèce, c'est ajouter une entrée dans la table.
Les cornes (traits.horns, styles curved / straight / ram) viennent de
l'espèce, mais 1,5 % des autres visages en poussent aussi. Elles s'enracinent au
bord du crâne près du sommet et sont dessinées en dernier — plantées plus
à l'intérieur et plus courtes, elles finissaient noyées dans les cheveux et se
lisaient comme des traits de stylo égarés.
Un piège rencontré en route : les multiplicateurs de l'espèce et ceux de la
forme de crâne se multiplient. Un orc (large) sur un crâne « wide » donnait
un paillasson. headSize() borne donc la taille absolue puis le rapport
largeur/hauteur, ce qui garde toutes les combinaisons utilisables.
looks.js décide du style plutôt que de l'anatomie : ce que font les cheveux,
les piercings, le maquillage, quelles couleurs sortent. Dix-sept archétypes —
finance, punk, hippie, bobo, goth, metal, sailor, scholar,
raver, farmer, artist, wizard, noble, soldier, clown, detective,
plus plain qui ne biaise rien et reste majoritaire.
Un archétype est un biais, pas un costume : un punk garde un nez tiré au
hasard. LOOK_BLOCKLIST interdit les combinaisons absurdes (pas d'enfant punk
à chien, pas de troll universitaire).
Le signal le plus lisible d'un archétype, c'est la coiffure — d'où une règle
qui compte : un couvre-chef ne fait plus disparaître les cheveux
systématiquement. Seuls ceux qui couvrent le crâne (bonnet, casquette, capuche,
haut-de-forme, béret, casquette plate) masquent ce qu'il y a dessous, et encore :
les coupes assez longues pour dépasser (long, dreads, ponytail, bob,
curly, pigtails, mop) restent visibles. Un bandana ou un serre-tête ne
cachent rien.
Les accessoires disponibles : boucle d'oreille (anneau ou clou), piercing au nez, eye-liner, rouge à lèvres, tatouage, cicatrice, cigarette avec sa volute, cache-œil avec sa sangle, monosourcil.
Le visage est traité comme s'il était peint sur un cylindre : un point à la
position horizontale u = x/w se trouve à l'angle asin(u), et tourner la tête
de yaw l'amène à sin(asin(u) + yaw). On divise par cos(yaw) pour
renormaliser, sinon toute la tête rétrécirait — exact pour un cylindre, faux
pour un dessin.
Résultat : la ligne médiane glisse du côté regardé, les traits se tassent de ce côté et s'écartent de l'autre, et la silhouette ne bouge pas. Tout ce qui sort du crâne (volume des cheveux, chapeaux, oreilles) passe sans transformation, donc rien n'est cisaillé.
Le cylindre seul ne suffit pas : sans autre chose, on obtient un visage de face
dont les traits ont glissé. profileX() ajoute les deux indices qui manquent —
le nez et les lèvres qui débordent du bord avant, et le crâne qui ressort à
l'arrière.
Et deux détails qui se trompent facilement : sur une tête tournée vers la droite, l'oreille visible est celle de gauche de l'image (l'autre passe derrière le crâne), et l'œil raccourci est celui du côté regardé, pas l'autre.
Huit comportements de trait, tirés au sort : fine, ballpoint, pencil,
quill, marker, charcoal, brush, wax. Chacun donne une épaisseur, un nombre de passes,
une noirceur, un taux de décrochage et un tremblé. Les appelants passent des
opacités explicites partout, donc plutôt que de les écraser, la plume les
multiplie : un crayon éclaircit tout dans le même rapport, un feutre
assombrit tout.
Deux têtes qui partagent une coiffure ne doivent pas se ressembler. Chaque
entrée de la table de styles n'est donc qu'un point de départ, retravaillé par
hairPlan() avec les paramètres propres au visage :
puff — le volume général, de 0,6 à 1,6× ;hairTShift / sideShift — la ligne de front plus haute ou plus basse, la
masse plus ou moins descendante sur les côtés ;asym — une tempe plus fournie que l'autre, avec sa ligne de front décalée :
la coque est construite côté droit et côté gauche séparément ;hairline — un tiers des têtes emprunte une autre ligne de front que celle
que suggère leur style ;strokeLen, spread, curl, swirl, turns — la longueur, l'évasement, la
courbure et l'enroulement des traits de stylo, plus l'inclinaison générale du
« peignage ».Les barbes suivent la même logique (extent, gap, strokeLen) : plus ou moins
haut sur les joues, plus ou moins de place laissée autour de la bouche.
import { renderFace, drawFace, makeGenome, makeGenomes, renderSheet } from './src/faces/face.js';
document.body.append(renderFace('pnj-42', { size: 256 }));
const ctx = myCanvas.getContext('2d');
drawFace(ctx, 'pnj-42', { size: 256, x: 0, y: 0 });
drawFace(ctx, seedOuGénome, opts) retourne le génome utilisé.
| option | défaut | effet |
|---|---|---|
size | côté du canvas | côté du carré à dessiner, en px |
x, y | 0 | coin haut-gauche de ce carré |
paper | true | false = fond transparent ; ou { tone, grain, vignette } |
blend | 'multiply' | 'source-over' sur fond transparent, pour une encre opaque |
ink | celle du génome | couleur d'encre, ex. 'rgb(236,229,214)' sur fond sombre |
weight | 1 | multiplicateur d'épaisseur |
shake | 1 | multiplicateur de tremblé |
weights | défauts | preset de population |
force | {} | traits imposés, ex. { hat: 'beanie', beard: 'none' } |
color | 1 | fréquence des touches de couleur ; 0 = encre seule |
turn | 1 | fréquence des têtes tournées ; 0 = toutes de face |
mood | 0 | −1..1, humeur moyenne de la population |
kin | défauts | pondération des espèces, ex. { orc: 50, human: 50 } |
look | défauts | pondération des archétypes |
accents | true | false pour ne pas peindre la couche couleur d'un génome |
lod | selon la taille | niveau de détail 0..1 ; bas pour un glisser, 1 pour un export |
dpr | auto | plafonné à 1,5 sous 200 px et 2 au-delà |
const g = makeGenome('pnj-42');
g.hat.style = 'beanie'; // on retouche
g.pen.shake = 2; // main plus tremblante
g.hair.asym = 0.4; // une tempe bien plus fournie que l'autre
g.accents = []; // ou on retire la couleur
drawFace(ctx, g, { size: 256 });
// du noir et blanc strict
makeGenome('pnj-42', { color: 0 });
const foule = makeGenomes('village', 40); // 40 génomes stables
Traits disponibles : HEAD_SHAPES (14), EYE_STYLES (22), BROW_STYLES (10),
NOSE_STYLES (14), MOUTH_STYLES (18), HAIR_STYLES (27), BEARD_STYLES (15),
GLASSES_STYLES (10), HAT_STYLES (15), EAR_STYLES (7), KIN_NAMES (12),
LOOK_NAMES (17), PEN_NAMES (8) — tous exportés par face.js.
force accepte n'importe laquelle de ces clés, y compris kin, look et
pen :
makeGenome('boss', { force: { kin: 'orc', look: 'metal', pen: 'charcoal' } });
import { PRESETS } from './src/faces/presets.js';
makeGenome('pnj-42', PRESETS.tavern);
Un preset est un objet d'options : il porte trois cadrans — kin (quelles
créatures), look (quels archétypes), weights (les traits individuels) — et
ne change jamais la façon dont quoi que ce soit est dessiné. Ajoute les tiens
dans presets.js plutôt que de toucher aux valeurs par défaut.
Fournis : notebook (le carnet de référence), humains, crowd (lisible en
vignette), kids, gamblers (pour HoH2), tavern (tout le bestiaire),
carnival (tout à fond, pour inspecter les styles).
const sheet = renderSheet(makeGenomes('village', 48), { cell: 200, cols: 8 });
document.body.append(sheet); // ou sheet.toDataURL('image/png')
Les presets font venir un genre de foule. Quand tu veux ce visage-là — vu sur une planche, dans une référence, dans une planche générée qui t'a plu — la méthode est en quatre temps.
1. Épingler ce qui saute aux yeux. Les menus déroulants du site imposent
n'importe quel trait. Pour un bonnet tricoté + lunettes noires + moustache :
espèce humain, archétype quelconque, crâne round, chapeau beanie,
lunettes sunglasses, barbe mustache, nez long, bouche line, plume
fusain. Mets Orientation et Couleur à 0.
2. Relancer jusqu'à approcher. Il reste des dizaines de tirages libres — proportions, teintes, taches. Quelques « Nouvelle planche » suffisent à tomber sur un candidat.
3. Régler à la main. Clique sur le visage : le panneau de droite affiche son génome, éditable. Change des valeurs, clique Appliquer, le portrait se redessine. C'est là que se joue le plus gros de la ressemblance, parce que les menus ne touchent qu'aux traits énumérés — pas aux nombres :
| ce qui manquait | le champ |
|---|---|
| bonnet en aplat noir au lieu de laine hachurée | hat.tone: "mid" (dark remplit) |
| moustache réduite à un filet | beard.tone: "dark", beard.gap: 1.5 |
| verres trop petits | glasses.size: 1.3 |
| crâne trop rond, mâchoire trop fine | head.jaw: 1.2, head.brow: 0.86, head.exp: 2.3 |
| joues nues | marks.freckles: 26 |
Rien n'empêche de sortir des plages du générateur : mouth.w: 1.75 déborde de
l'intervalle tiré au sort, et c'est très bien — un génome n'est que de la
donnée.
4. Figer en recette. Une fois les valeurs trouvées, Copier le génome puis
range-les dans recipes.js :
import { makeRecipe, renderFace } from './src/faces/face.js';
// le même personnage, autant de fois que tu veux
document.body.append(renderFace(makeRecipe('beanieShades', 'pnj-7'), { size: 256 }));
Une recette a deux moitiés, parce que force ne sait épingler que les traits
énumérés : opts (ce qu'on impose avant la génération) et tune (les nombres
qu'on écrase après). La graine continue de faire varier tout ce que la recette
n'a pas fixé — le tremblé du trait, les taches de rousseur exactes, les plis du
bonnet. makeRecipe('beanieShades', i) donne donc des frères et sœurs, pas
des clones.
Le vrai bénéfice du génome-donnée. Les traits énumérés s'héritent en entier d'un parent ou de l'autre (la moitié d'une coiffure n'est pas une coiffure), les nombres se mélangent avec un peu de dérive — un enfant n'est donc jamais exactement la moyenne de ses parents.
import { breed, brood, makeGenome } from './src/faces/face.js';
const mum = makeGenome('mum');
const dad = makeGenome('dad');
breed(mum, dad, 'kid-1'); // un enfant
brood(mum, dad, 'kid', 6); // six frères et sœurs
Sur le site : panneau Lineage en mode focus. Pratique pour une ressemblance familiale dans un jeu — mêmes parents, graines d'enfants différentes.
Sur le site : choisis une population, une graine, puis Atlas + manifeste. Tu récupères un PNG transparent et un JSON qui décrit les cases.
import { buildAtlas, atlasSeeds } from './src/faces/atlas.js';
const { canvas, manifest } = buildAtlas(atlasSeeds('hoh2', 256), {
cell: 192,
ink: 'rgb(236,229,214)', // encre claire, fond sombre
});
Côté Godot 4, godot/doodle_faces.gd fait le reste :
# autoload nommé DoodleFaces
var tex := DoodleFaces.texture_for("dealer_3")
$Portrait.texture = tex
texture_for() hache l'identifiant avec exactement le même FNV-1a que le
générateur JS, donc "dealer_3" tombe sur la même tête dans l'éditeur, dans le
jeu, et dans la prévisualisation web. Vérifié : hashSeed("dealer_3") vaut
3056619830 des deux côtés.
Avec 256 visages dans un atlas de 192 px, tu es à ~3 Mo et le coût à l'exécution est nul. Pour un roguelite, 256 têtes distinctes, c'est déjà l'infini.
Il faut alors porter le renderer dans le moteur. C'est faisable — le code est
écrit exprès en style portable, et godot/doodle_faces.gd contient déjà le
hash_seed et l'imul32 32 bits qui garantissent des graines identiques — mais
ça veut dire maintenir deux renderers en parallèle : chaque nouvelle coiffure
s'écrit deux fois. À ne faire que si tu as vraiment besoin d'un nombre non borné
de visages, ou de laisser le joueur personnaliser le sien.
Note pour un portage : Godot n'a pas de découpe de tracé sur CanvasItem, mais
Geometry2D.intersect_polyline_with_polygon() remplace exactement le clip
utilisé par ink.hatch() et ink.flow().
Les choix structurels (forme de crâne, style d'œil, coiffure, chapeau…) passent
uniquement par des tirages entiers sur le PRNG : ils sont identiques au bit près
partout. Les paramètres continus tirés par rng.gauss() utilisent log et cos
et peuvent différer sur les derniers chiffres significatifs d'un moteur à
l'autre — invisible à l'œil, mais à savoir si tu compares des rendus au pixel.
Ajouter un trait au milieu de makeGenome() décale la suite des tirages et
change donc tous les visages existants. Si tu dois garder un pool stable dans
un jeu déjà publié : dérive le nouveau trait d'un flux séparé (rng.fork('tag'))
ou fige l'atlas.