From 9ce4bd39e48854f903bb8848785a9a7c94508379 Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Thu, 20 Mar 2025 22:49:00 -0400 Subject: [PATCH 01/13] Untested initial implementation of generating a relative of a face --- package.json | 3 ++ pnpm-lock.yaml | 20 ++++++++ src/generate.ts | 110 +++++++++++++++++++++++++++----------------- src/makeRelative.ts | 82 +++++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 42 deletions(-) create mode 100644 src/makeRelative.ts diff --git a/package.json b/package.json index a31c8000..8f29e72d 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@phosphor-icons/react": "^2.1.6", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", + "@types/dlv": "^1.1.5", "@types/node": "^20.14.6", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", @@ -76,6 +77,8 @@ "zustand": "^4.5.2" }, "dependencies": { + "dlv": "^1.1.3", + "dset": "^3.1.4", "svg-path-bbox": "^2.0.0" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 717831b2..c9a48736 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + dlv: + specifier: ^1.1.3 + version: 1.1.3 + dset: + specifier: ^3.1.4 + version: 3.1.4 svg-path-bbox: specifier: ^2.0.0 version: 2.0.0 @@ -42,6 +48,9 @@ importers: '@tailwindcss/typography': specifier: ^0.5.13 version: 0.5.13(tailwindcss@3.4.4) + '@types/dlv': + specifier: ^1.1.5 + version: 1.1.5 '@types/node': specifier: ^20.14.6 version: 20.14.6 @@ -2091,6 +2100,9 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@types/dlv@1.1.5': + resolution: {integrity: sha512-JHOWNfiWepAhfwlSw17kiWrWrk6od2dEQgHltJw9AS0JPFoLZJBge5+Dnil2NfdjAvJ/+vGSX60/BRW20PpUXw==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -2556,6 +2568,10 @@ packages: domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -6596,6 +6612,8 @@ snapshots: '@trysound/sax@0.2.0': {} + '@types/dlv@1.1.5': {} + '@types/estree@1.0.5': {} '@types/lodash.debounce@4.0.9': @@ -7089,6 +7107,8 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dset@3.1.4: {} + eastasianwidth@0.2.0: {} electron-to-chromium@1.4.807: {} diff --git a/src/generate.ts b/src/generate.ts index a589d5a0..295ade71 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -2,16 +2,17 @@ import override from "./override.js"; import { svgsGenders, svgsIndex } from "./svgs-index.js"; import { Feature, Gender, Overrides, Race, TeamColors } from "./types.js"; -function randomInt( - minInclusive: number, - max: number, - inclusiveMax: boolean = false, -) { - if (inclusiveMax) { - max += 1; - } - return Math.floor(Math.random() * (max - minInclusive)) + minInclusive; -} +const randInt = (a: number, b: number): number => { + return Math.floor(Math.random() * (1 + b - a)) + a; +}; + +const randUniform = (a: number, b: number): number => { + return Math.random() * (b - a) + a; +}; + +const randChoice = (array: T[]): T => { + return array[Math.floor(Math.random() * array.length)]; +}; const getID = (type: Feature, gender: Gender): string => { const validIDs = svgsIndex[type].filter((_id, index) => { @@ -20,7 +21,7 @@ const getID = (type: Feature, gender: Gender): string => { ); }); - return validIDs[randomInt(0, validIDs.length)]; + return randChoice(validIDs); }; export const colors = { @@ -53,6 +54,49 @@ const defaultTeamColors: TeamColors = ["#89bfd3", "#7a1319", "#07364f"]; const roundTwoDecimals = (x: number) => Math.round(x * 100) / 100; +export const numberRanges = { + "body.size": { + female: [0.8, 0.9], + male: [0.95, 1.05], + }, + fatness: { + female: [0, 0.4], + male: [0, 1], + }, + "ear.size": { + female: [0.5, 1], + male: [0.5, 1.5], + }, + "eye.angle": { + female: [-10, 15], + male: [-10, 15], + }, + "eyebrow.angle": { + female: [-15, 20], + male: [-15, 20], + }, + "nose.size": { + female: [0.5, 1], + male: [0.5, 1.25], + }, + "smileLine.size": { + female: [0.25, 2.25], + male: [0.25, 2.25], + }, +} as const; + +const getRandInt = (key: keyof typeof numberRanges, gender: Gender) => { + return randInt(numberRanges[key][gender][0], numberRanges[key][gender][1]); +}; + +const getRandUniform = (key: keyof typeof numberRanges, gender: Gender) => { + return roundTwoDecimals( + randUniform(numberRanges[key][gender][0], numberRanges[key][gender][1]), + ); +}; + +export const races: Race[] = ["white", "black", "brown", "asian"]; + export const generate = ( overrides?: Overrides, options?: { gender?: Gender; race?: Race }, @@ -61,22 +105,11 @@ export const generate = ( if (options && options.race) { return options.race; } - switch (randomInt(0, 4)) { - case 0: - return "white"; - case 1: - return "asian"; - case 2: - return "brown"; - default: - return "black"; - } + return randChoice(races); })(); const gender = options && options.gender ? options.gender : "male"; - const eyeAngle = randomInt(-10, 15, true); - const palette = (() => { switch (playerRace) { case "white": @@ -89,12 +122,11 @@ export const generate = ( return colors.black; } })(); - const skinColor = palette.skin[randomInt(0, palette.skin.length)]; - const hairColor = palette.hair[randomInt(0, palette.hair.length)]; - const isFlipped = Math.random() < 0.5; + const skinColor = randChoice(palette.skin); + const hairColor = randChoice(palette.hair); const face = { - fatness: roundTwoDecimals((gender === "female" ? 0.4 : 1) * Math.random()), + fatness: getRandUniform("fatness", gender), teamColors: defaultTeamColors, hairBg: { id: @@ -105,18 +137,14 @@ export const generate = ( body: { id: getID("body", gender), color: skinColor, - size: roundTwoDecimals( - Math.random() * 0.1 + (gender === "female" ? 0.8 : 0.95), - ), + size: getRandUniform("body.size", gender), }, jersey: { id: getID("jersey", gender), }, ear: { id: getID("ear", gender), - size: roundTwoDecimals( - 0.5 + (gender === "female" ? 0.5 : 1) * Math.random(), - ), + size: getRandUniform("ear.size", gender), }, head: { id: getID("head", gender), @@ -134,7 +162,7 @@ export const generate = ( Math.random() < (gender === "male" ? 0.75 : 0.1) ? getID("smileLine", gender) : "none", - size: roundTwoDecimals(0.25 + 2 * Math.random()), + size: getRandUniform("smileLine.size", gender), }, miscLine: { id: Math.random() < 0.5 ? getID("miscLine", gender) : "none", @@ -142,26 +170,24 @@ export const generate = ( facialHair: { id: Math.random() < 0.5 ? getID("facialHair", gender) : "none", }, - eye: { id: getID("eye", gender), angle: eyeAngle }, + eye: { id: getID("eye", gender), angle: getRandInt("eye.angle", gender) }, eyebrow: { id: getID("eyebrow", gender), - angle: randomInt(-15, 20, true), + angle: getRandInt("eyebrow.angle", gender), }, hair: { id: getID("hair", gender), color: hairColor, - flip: isFlipped, + flip: Math.random() < 0.5, }, mouth: { id: getID("mouth", gender), - flip: isFlipped, + flip: Math.random() < 0.5, }, nose: { id: getID("nose", gender), - flip: isFlipped, - size: roundTwoDecimals( - 0.5 + Math.random() * (gender === "female" ? 0.5 : 0.75), - ), + flip: Math.random() < 0.5, + size: getRandUniform("nose.size", gender), }, glasses: { id: Math.random() < 0.1 ? getID("glasses", gender) : "none", diff --git a/src/makeRelative.ts b/src/makeRelative.ts new file mode 100644 index 00000000..a011fc6c --- /dev/null +++ b/src/makeRelative.ts @@ -0,0 +1,82 @@ +import delve from "dlv"; +import { dset } from "dset"; +import { colors, generate, numberRanges, races } from "./generate"; +import type { FaceConfig, Gender } from "./types"; +import { deepCopy } from "./utils"; + +// Currently, race just affects skin color and hair color. Let's ignore hair color (since you could imagine it being dyed anyway) and figure out someone's race just based on skin color. If no race is found, return undefined. +const imputeRace = (face: FaceConfig) => { + return races.find((race) => colors[race].skin.includes(face.body.color)); +}; + +export const makeRelative = ({ + gender, + relative, +}: { + gender: Gender; + relative: FaceConfig; +}) => { + const face = deepCopy(relative); + + const race = imputeRace(face); + + const randomFace = generate(undefined, { + gender, + race, + }); + + // Always regenerate some properties + face.accessories = randomFace.accessories; + face.body.size = randomFace.body.size; + face.facialHair = randomFace.facialHair; + face.fatness = randomFace.fatness; + face.glasses = randomFace.glasses; + face.hair.id = randomFace.hair.id; + face.hair.flip = randomFace.hair.flip; + face.hairBg = randomFace.hairBg; + face.head.shave = randomFace.head.shave; + + // Regenerate some properties with some probability + const probRegenerate = 0.5; + const regenerateProperties = [ + "eyeLine", + "miscLine", + "mouth", + "nose", + "smileLine", + "eye.angle", + "eyebrow.angle", + "body.id", + "ear.id", + "eye.id", + "eyebrow.id", + "head.id", + "ear.size", + ] as const; + for (const path of regenerateProperties) { + if (Math.random() < probRegenerate) { + dset(face, path, delve(randomFace, path)); + } + } + + // Maybe apply race-appropriate new colors, if we have been able to identify the original race + if (race !== undefined) { + if (Math.random() < probRegenerate) { + face.body.color = randomFace.body.color; + } + if (Math.random() < probRegenerate) { + face.hair.color = randomFace.hair.color; + } + } + + // Override any properties that are not valid for the specified gender + for (const [path, ranges] of Object.entries(numberRanges)) { + const current = delve(face, path); + const range = ranges[gender]; + if (current < range[0] || current > range[1]) { + dset(face, path, delve(randomFace, path)); + } + } + + return face; +}; From b4fc8c44128d9610f6dd7387d9662d3245125b6b Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Thu, 20 Mar 2025 23:26:27 -0400 Subject: [PATCH 02/13] Checkbox option for relative --- public/editor/TopBar.tsx | 14 ++++++++++++++ public/editor/stateStore.ts | 18 +++++++++++++----- public/editor/types.ts | 4 ++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/public/editor/TopBar.tsx b/public/editor/TopBar.tsx index 963bcb40..20f4c19e 100644 --- a/public/editor/TopBar.tsx +++ b/public/editor/TopBar.tsx @@ -23,6 +23,7 @@ import { } from "@nextui-org/react"; import { capitalizeFirstLetter } from "./utils"; import { shuffleEntireFace } from "./shuffleFace"; +import { OtherSetting } from "./types"; export const TopBar = () => { const stateStore = useStateStore(); @@ -33,8 +34,10 @@ export const TopBar = () => { gallerySectionConfigList, shuffleGenderSettingObject, shuffleRaceSettingObject, + shuffleOtherSettingObject, setShuffleGenderSettingObject, setShuffleRaceSettingObject, + setShuffleOtherSettingObject, } = stateStore; const genders: Gender[] = ["male", "female"]; const races: Race[] = ["white", "black", "brown", "asian"]; @@ -115,6 +118,17 @@ export const TopBar = () => { ); })} + { + setShuffleOtherSettingObject( + otherList as OtherSetting[], + ); + }} + > + Relative + )} diff --git a/public/editor/stateStore.ts b/public/editor/stateStore.ts index d9849b65..7a39aa6f 100644 --- a/public/editor/stateStore.ts +++ b/public/editor/stateStore.ts @@ -12,7 +12,7 @@ import { distinctSkinColors, jerseyColorOptions, } from "./defaultColors"; -import { FaceConfig, Gender, Race } from "../../src/types"; +import { FaceConfig } from "../../src/types"; const gallerySectionInfos: (Pick< GallerySectionConfig, @@ -401,21 +401,29 @@ const createGallerySlice: StateCreator = ( shuffleGenderSettingObject: ["male"], shuffleRaceSettingObject: ["white", "brown", "black", "asian"], + shuffleOtherSettingObject: [], - setShuffleGenderSettingObject: (options: Gender[]) => - set((state: CombinedState) => { + setShuffleGenderSettingObject: (options) => + set((state) => { return { ...state, shuffleGenderSettingObject: options, }; }), - setShuffleRaceSettingObject: (options: Race[]) => - set((state: CombinedState) => { + setShuffleRaceSettingObject: (options) => + set((state) => { return { ...state, shuffleRaceSettingObject: options, }; }), + setShuffleOtherSettingObject: (options) => + set((state) => { + return { + ...state, + shuffleOtherSettingObject: options, + }; + }), }); export const useStateStore = create()( diff --git a/public/editor/types.ts b/public/editor/types.ts index 9bb25be5..42c54bb4 100644 --- a/public/editor/types.ts +++ b/public/editor/types.ts @@ -8,6 +8,8 @@ export type FaceState = { setFaceStore: (newFace: FaceConfig) => void; }; +export type OtherSetting = "relative"; + export type GalleryState = { gallerySize: GallerySize; gallerySectionConfigList: GallerySectionConfig[]; @@ -23,8 +25,10 @@ export type GalleryState = { shuffleGenderSettingObject: Gender[]; shuffleRaceSettingObject: Race[]; + shuffleOtherSettingObject: OtherSetting[]; setShuffleGenderSettingObject: (options: Gender[]) => void; setShuffleRaceSettingObject: (options: Race[]) => void; + setShuffleOtherSettingObject: (options: OtherSetting[]) => void; }; type GallerySectionConfigBase = { From e08b6116584814ad20c9f5f6e781ab25dcc2da8d Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Thu, 20 Mar 2025 23:37:10 -0400 Subject: [PATCH 03/13] UI to make relatives kind of works --- public/editor/shuffleFace.ts | 25 ++++++++++++++++++++++--- src/generate.ts | 19 ++++++------------- src/utils.ts | 12 ++++++++++++ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/public/editor/shuffleFace.ts b/public/editor/shuffleFace.ts index 7ee4e215..20e1af97 100644 --- a/public/editor/shuffleFace.ts +++ b/public/editor/shuffleFace.ts @@ -3,7 +3,8 @@ import { generate } from "../../src/generate"; import { CombinedState, GallerySectionConfig } from "./types"; import { deleteFromDict, pickRandom } from "./utils"; import { jerseyColorOptions } from "./defaultColors"; -import { deepCopy } from "../../src/utils"; +import { deepCopy, randChoice } from "../../src/utils"; +import { makeRelative } from "../../src/makeRelative"; type GenerateOptions = Parameters[1]; @@ -12,8 +13,26 @@ export const shuffleEntireFace = ( gallerySectionConfigList: GallerySectionConfig[], stateStore: CombinedState, ) => { - const { setFaceStore, shuffleGenderSettingObject, shuffleRaceSettingObject } = - stateStore; + const { + setFaceStore, + shuffleGenderSettingObject, + shuffleRaceSettingObject, + shuffleOtherSettingObject, + } = stateStore; + + if (shuffleOtherSettingObject.includes("relative")) { + // Special case for relative - ignore randomizeEnabled and race + const gender = + shuffleGenderSettingObject.length === 1 + ? shuffleGenderSettingObject[0] + : randChoice(["male", "female"] as const); + + const newFace = makeRelative({ gender, relative: faceConfig }); + setFaceStore(newFace); + + return; + } + const faceConfigCopy: Overrides = deepCopy(faceConfig); const options: GenerateOptions = {}; diff --git a/src/generate.ts b/src/generate.ts index 295ade71..b86accd1 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,18 +1,7 @@ import override from "./override.js"; import { svgsGenders, svgsIndex } from "./svgs-index.js"; import { Feature, Gender, Overrides, Race, TeamColors } from "./types.js"; - -const randInt = (a: number, b: number): number => { - return Math.floor(Math.random() * (1 + b - a)) + a; -}; - -const randUniform = (a: number, b: number): number => { - return Math.random() * (b - a) + a; -}; - -const randChoice = (array: T[]): T => { - return array[Math.floor(Math.random() * array.length)]; -}; +import { randChoice, randInt, randUniform } from "./utils.js"; const getID = (type: Feature, gender: Gender): string => { const validIDs = svgsIndex[type].filter((_id, index) => { @@ -75,6 +64,10 @@ export const numberRanges = { female: [-15, 20], male: [-15, 20], }, + "head.shave": { + female: [0, 0], + male: [0, 0.2], + }, "nose.size": { female: [0.5, 1], male: [0.5, 1.25], @@ -150,7 +143,7 @@ export const generate = ( id: getID("head", gender), shave: `rgba(0,0,0,${ gender === "male" && Math.random() < 0.25 - ? roundTwoDecimals(Math.random() / 5) + ? getRandUniform("head.shave", gender) : 0 })`, }, diff --git a/src/utils.ts b/src/utils.ts index 5eaf9c66..11bbb821 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,15 @@ export const deepCopy = (object: T): T => { return JSON.parse(JSON.stringify(object)); }; + +export const randInt = (a: number, b: number): number => { + return Math.floor(Math.random() * (1 + b - a)) + a; +}; + +export const randUniform = (a: number, b: number): number => { + return Math.random() * (b - a) + a; +}; + +export const randChoice = (array: T[]): T => { + return array[Math.floor(Math.random() * array.length)]; +}; From 219367b4e7451345dd6f9997cc027b03d981bc87 Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Fri, 21 Mar 2025 09:29:40 -0400 Subject: [PATCH 04/13] Handle resetting male/female-specific features when creating an opposite-gender relative --- src/generate.ts | 11 ++++++++--- src/makeRelative.ts | 20 +++++++++++++++++--- src/types.ts | 39 +++++++++++++++++++++------------------ src/utils.ts | 2 +- tools/lib/process-svgs.js | 2 +- 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/generate.ts b/src/generate.ts index b86accd1..c81d6465 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,6 +1,13 @@ import override from "./override.js"; import { svgsGenders, svgsIndex } from "./svgs-index.js"; -import { Feature, Gender, Overrides, Race, TeamColors } from "./types.js"; +import { + type Feature, + type Gender, + type Overrides, + type Race, + races, + type TeamColors, +} from "./types.js"; import { randChoice, randInt, randUniform } from "./utils.js"; const getID = (type: Feature, gender: Gender): string => { @@ -88,8 +95,6 @@ const getRandUniform = (key: keyof typeof numberRanges, gender: Gender) => { ); }; -export const races: Race[] = ["white", "black", "brown", "asian"]; - export const generate = ( overrides?: Overrides, options?: { gender?: Gender; race?: Race }, diff --git a/src/makeRelative.ts b/src/makeRelative.ts index a011fc6c..382f94b7 100644 --- a/src/makeRelative.ts +++ b/src/makeRelative.ts @@ -1,8 +1,9 @@ import delve from "dlv"; import { dset } from "dset"; -import { colors, generate, numberRanges, races } from "./generate"; -import type { FaceConfig, Gender } from "./types"; +import { colors, generate, numberRanges } from "./generate"; +import { features, races, type FaceConfig, type Gender } from "./types"; import { deepCopy } from "./utils"; +import { svgsGenders, svgsIndex } from "./svgs-index"; // Currently, race just affects skin color and hair color. Let's ignore hair color (since you could imagine it being dyed anyway) and figure out someone's race just based on skin color. If no race is found, return undefined. const imputeRace = (face: FaceConfig) => { @@ -69,7 +70,20 @@ export const makeRelative = ({ } } - // Override any properties that are not valid for the specified gender + // Override any ID properties that are not valid for the specified gender + for (const key of features) { + const svgIndex = svgsIndex[key].findIndex((id) => id === face[key].id); + const svgGender = svgsGenders[key][svgIndex]; + if ( + svgIndex < 0 || + (svgGender === "male" && gender === "female") || + (svgGender === "female" && gender === "male") + ) { + face[key].id = randomFace[key].id; + } + } + + // Override any numeric properties that are not valid for the specified gender for (const [path, ranges] of Object.entries(numberRanges)) { const current = delve(face, path); const range = ranges[gender]; diff --git a/src/types.ts b/src/types.ts index 7efca0ae..3a0eda30 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,25 +6,28 @@ export type Overrides = { export type Gender = "male" | "female"; -export type Feature = - | "accessories" - | "body" - | "ear" - | "eye" - | "eyebrow" - | "eyeLine" - | "facialHair" - | "glasses" - | "hair" - | "hairBg" - | "head" - | "jersey" - | "miscLine" - | "mouth" - | "nose" - | "smileLine"; +export const features = [ + "accessories", + "body", + "ear", + "eye", + "eyebrow", + "eyeLine", + "facialHair", + "glasses", + "hair", + "hairBg", + "head", + "jersey", + "miscLine", + "mouth", + "nose", + "smileLine", +] as const; +export type Feature = (typeof features)[number]; -export type Race = "asian" | "black" | "brown" | "white"; +export const races = ["white", "black", "brown", "asian"] as const; +export type Race = (typeof races)[number]; export type TeamColors = [string, string, string]; diff --git a/src/utils.ts b/src/utils.ts index 11bbb821..b2b72d1e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,6 +10,6 @@ export const randUniform = (a: number, b: number): number => { return Math.random() * (b - a) + a; }; -export const randChoice = (array: T[]): T => { +export const randChoice = (array: T[] | readonly T[]): T => { return array[Math.floor(Math.random() * array.length)]; }; diff --git a/tools/lib/process-svgs.js b/tools/lib/process-svgs.js index 6651107e..69d5800f 100644 --- a/tools/lib/process-svgs.js +++ b/tools/lib/process-svgs.js @@ -74,7 +74,7 @@ const processSVGs = async () => { path.join(import.meta.dirname, "..", "..", "src", "svgs-index.ts"), `${warning}\n\nexport const svgsIndex = ${JSON.stringify( svgsIndex, - )};\n\nexport const svgsGenders = ${JSON.stringify(svgsGenders)};`, + )} as const;\n\nexport const svgsGenders = ${JSON.stringify(svgsGenders)} as const;`, ); console.log( From a3bc956348dfd19f5b64f108f5cd5eef42846bb7 Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Fri, 21 Mar 2025 09:30:49 -0400 Subject: [PATCH 05/13] Rename types.ts to common.ts, since it is no longer just types --- public/editor/EditJsonModal.tsx | 2 +- public/editor/FeatureGallery.tsx | 2 +- public/editor/TopBar.tsx | 2 +- public/editor/defaultColors.ts | 2 +- public/editor/downloadFace.ts | 2 +- public/editor/overrideList.ts | 2 +- public/editor/shuffleFace.ts | 2 +- public/editor/stateStore.ts | 2 +- src/Face.tsx | 2 +- src/cli.ts | 2 +- src/{types.ts => common.ts} | 0 src/display.ts | 2 +- src/faceToSvgString.ts | 2 +- src/generate.ts | 2 +- src/index.ts | 2 +- src/makeRelative.ts | 2 +- src/override.ts | 2 +- 17 files changed, 16 insertions(+), 16 deletions(-) rename src/{types.ts => common.ts} (100%) diff --git a/public/editor/EditJsonModal.tsx b/public/editor/EditJsonModal.tsx index 8755df0c..a77251ef 100644 --- a/public/editor/EditJsonModal.tsx +++ b/public/editor/EditJsonModal.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import override from "../../src/override"; -import { Overrides } from "../../src/types"; +import { Overrides } from "../../src/common"; import { useStateStore } from "./stateStore"; import { Textarea, diff --git a/public/editor/FeatureGallery.tsx b/public/editor/FeatureGallery.tsx index c34187e5..ae808757 100644 --- a/public/editor/FeatureGallery.tsx +++ b/public/editor/FeatureGallery.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import override from "../../src/override"; -import { FaceConfig } from "../../src/types"; +import { FaceConfig } from "../../src/common"; import { useStateStore } from "./stateStore"; import { Shuffle, LockSimpleOpen, LockSimple } from "@phosphor-icons/react"; import { diff --git a/public/editor/TopBar.tsx b/public/editor/TopBar.tsx index 20f4c19e..5d1de2b3 100644 --- a/public/editor/TopBar.tsx +++ b/public/editor/TopBar.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Gender, Race } from "../../src/types"; +import { Gender, Race } from "../../src/common"; import { useStateStore } from "./stateStore"; import { House, diff --git a/public/editor/defaultColors.ts b/public/editor/defaultColors.ts index 61af59ae..18d0ddd2 100644 --- a/public/editor/defaultColors.ts +++ b/public/editor/defaultColors.ts @@ -1,5 +1,5 @@ import { colors } from "../../src/generate"; -import { TeamColors } from "../../src/types"; +import { TeamColors } from "../../src/common"; import { distinct } from "./utils"; export const jerseyColorOptions: TeamColors[] = [ diff --git a/public/editor/downloadFace.ts b/public/editor/downloadFace.ts index 4f2521ce..a3ab21f3 100644 --- a/public/editor/downloadFace.ts +++ b/public/editor/downloadFace.ts @@ -1,4 +1,4 @@ -import { FaceConfig } from "../../src/types"; +import { FaceConfig } from "../../src/common"; import { getCurrentTimestampAsString } from "./utils"; // https://blog.logrocket.com/programmatically-downloading-files-browser/ diff --git a/public/editor/overrideList.ts b/public/editor/overrideList.ts index ba3dc85c..ee6460a2 100644 --- a/public/editor/overrideList.ts +++ b/public/editor/overrideList.ts @@ -1,6 +1,6 @@ import override from "../../src/override"; import { svgsIndex } from "../../src/svgs-index"; -import { FaceConfig as FaceType, Overrides } from "../../src/types"; +import { FaceConfig as FaceType, Overrides } from "../../src/common"; import { deepCopy } from "../../src/utils"; import { GallerySectionConfig, OverrideListItem } from "./types"; import { doesStrLookLikeColor, luma, setToDict } from "./utils"; diff --git a/public/editor/shuffleFace.ts b/public/editor/shuffleFace.ts index 20e1af97..5cd208f8 100644 --- a/public/editor/shuffleFace.ts +++ b/public/editor/shuffleFace.ts @@ -1,4 +1,4 @@ -import { FaceConfig, Overrides } from "../../src/types"; +import { FaceConfig, Overrides } from "../../src/common"; import { generate } from "../../src/generate"; import { CombinedState, GallerySectionConfig } from "./types"; import { deleteFromDict, pickRandom } from "./utils"; diff --git a/public/editor/stateStore.ts b/public/editor/stateStore.ts index 7a39aa6f..bf61e75f 100644 --- a/public/editor/stateStore.ts +++ b/public/editor/stateStore.ts @@ -12,7 +12,7 @@ import { distinctSkinColors, jerseyColorOptions, } from "./defaultColors"; -import { FaceConfig } from "../../src/types"; +import { FaceConfig } from "../../src/common"; const gallerySectionInfos: (Pick< GallerySectionConfig, diff --git a/src/Face.tsx b/src/Face.tsx index e305c478..c6d1fd1c 100644 --- a/src/Face.tsx +++ b/src/Face.tsx @@ -6,7 +6,7 @@ import { useState, type CSSProperties, } from "react"; -import { FaceConfig, Overrides } from "./types"; +import { FaceConfig, Overrides } from "./common"; import { display } from "./display"; import { deepCopy } from "./utils"; diff --git a/src/cli.ts b/src/cli.ts index eec5d82f..c33786c8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,7 @@ import fs from "node:fs"; import { parseArgs } from "node:util"; import { faceToSvgString } from "./faceToSvgString.js"; import { generate } from "./generate.js"; -import { Gender, Overrides, Race } from "./types.js"; +import { Gender, Overrides, Race } from "./common.js"; const { values: options } = parseArgs({ options: { diff --git a/src/types.ts b/src/common.ts similarity index 100% rename from src/types.ts rename to src/common.ts diff --git a/src/display.ts b/src/display.ts index e4db2610..6dd95640 100644 --- a/src/display.ts +++ b/src/display.ts @@ -1,6 +1,6 @@ import override from "./override.js"; import svgs from "./svgs.js"; -import { FaceConfig, Overrides } from "./types.js"; +import { FaceConfig, Overrides } from "./common.js"; const addWrapper = (svgString: string) => `${svgString}`; diff --git a/src/faceToSvgString.ts b/src/faceToSvgString.ts index 3c7858eb..3650d52d 100644 --- a/src/faceToSvgString.ts +++ b/src/faceToSvgString.ts @@ -1,6 +1,6 @@ import { svgPathBbox } from "svg-path-bbox"; import { display } from "./display.js"; -import { FaceConfig, Overrides } from "./types.js"; +import { FaceConfig, Overrides } from "./common.js"; /** * An instance of this object can pretend to be the global "document" diff --git a/src/generate.ts b/src/generate.ts index c81d6465..f37205c8 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -7,7 +7,7 @@ import { type Race, races, type TeamColors, -} from "./types.js"; +} from "./common.js"; import { randChoice, randInt, randUniform } from "./utils.js"; const getID = (type: Feature, gender: Gender): string => { diff --git a/src/index.ts b/src/index.ts index 8f4053d9..a8131609 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,4 +6,4 @@ export { faceToSvgString } from "./faceToSvgString.js"; export { default as svgs } from "./svgs.js"; export { svgsIndex } from "./svgs-index.js"; -export type * from "./types.js"; +export type * from "./common.js"; diff --git a/src/makeRelative.ts b/src/makeRelative.ts index 382f94b7..13f0c3a0 100644 --- a/src/makeRelative.ts +++ b/src/makeRelative.ts @@ -1,7 +1,7 @@ import delve from "dlv"; import { dset } from "dset"; import { colors, generate, numberRanges } from "./generate"; -import { features, races, type FaceConfig, type Gender } from "./types"; +import { features, races, type FaceConfig, type Gender } from "./common"; import { deepCopy } from "./utils"; import { svgsGenders, svgsIndex } from "./svgs-index"; diff --git a/src/override.ts b/src/override.ts index 09bc7f0d..b3fd53a0 100644 --- a/src/override.ts +++ b/src/override.ts @@ -1,4 +1,4 @@ -import { Overrides } from "./types"; +import { Overrides } from "./common"; const override = (obj: Overrides, overrides?: Overrides) => { if (!overrides || !obj) { From aacffd0e85b13e798e83886d49c8051fada5632c Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Fri, 21 Mar 2025 09:32:07 -0400 Subject: [PATCH 06/13] Rename makeRelative to generateRelative --- public/editor/shuffleFace.ts | 4 ++-- src/{makeRelative.ts => generateRelative.ts} | 4 ++-- src/index.ts | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) rename src/{makeRelative.ts => generateRelative.ts} (97%) diff --git a/public/editor/shuffleFace.ts b/public/editor/shuffleFace.ts index 5cd208f8..748fa019 100644 --- a/public/editor/shuffleFace.ts +++ b/public/editor/shuffleFace.ts @@ -4,7 +4,7 @@ import { CombinedState, GallerySectionConfig } from "./types"; import { deleteFromDict, pickRandom } from "./utils"; import { jerseyColorOptions } from "./defaultColors"; import { deepCopy, randChoice } from "../../src/utils"; -import { makeRelative } from "../../src/makeRelative"; +import { generateRelative } from "../../src/generateRelative"; type GenerateOptions = Parameters[1]; @@ -27,7 +27,7 @@ export const shuffleEntireFace = ( ? shuffleGenderSettingObject[0] : randChoice(["male", "female"] as const); - const newFace = makeRelative({ gender, relative: faceConfig }); + const newFace = generateRelative({ gender, relative: faceConfig }); setFaceStore(newFace); return; diff --git a/src/makeRelative.ts b/src/generateRelative.ts similarity index 97% rename from src/makeRelative.ts rename to src/generateRelative.ts index 13f0c3a0..2963a910 100644 --- a/src/makeRelative.ts +++ b/src/generateRelative.ts @@ -10,7 +10,7 @@ const imputeRace = (face: FaceConfig) => { return races.find((race) => colors[race].skin.includes(face.body.color)); }; -export const makeRelative = ({ +export const generateRelative = ({ gender, relative, }: { @@ -38,7 +38,7 @@ export const makeRelative = ({ face.head.shave = randomFace.head.shave; // Regenerate some properties with some probability - const probRegenerate = 0.5; + const probRegenerate = 0.25; const regenerateProperties = [ "eyeLine", "miscLine", diff --git a/src/index.ts b/src/index.ts index a8131609..e8eb68ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ export { display } from "./display.js"; export { generate } from "./generate.js"; +export { generateRelative } from "./generateRelative.js"; export { faceToSvgString } from "./faceToSvgString.js"; // Usually not needed, but just in case... From cd5a6df8925fb05b9351007d3cbf1bc785feb2ab Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Fri, 21 Mar 2025 09:38:54 -0400 Subject: [PATCH 07/13] Lint --- public/editor/overrideList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/editor/overrideList.ts b/public/editor/overrideList.ts index ee6460a2..7f0ec7c0 100644 --- a/public/editor/overrideList.ts +++ b/public/editor/overrideList.ts @@ -15,7 +15,7 @@ export const getOverrideListForItem = ( const featureName = gallerySectionConfig.key.split(".")[0]; const svgNames = [ - ...(svgsIndex as Record)[featureName], + ...(svgsIndex as Record)[featureName], ]; svgNames.sort((a, b) => { if (a === "none" || a === "bald") return -1; From b2c3b2dcd8365f758602537e4e9fa4b8b8be1e45 Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Fri, 21 Mar 2025 09:52:49 -0400 Subject: [PATCH 08/13] Add generateRelative to README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 3e203627..55dfeeef 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,18 @@ Or both together: const face = generate(undefined, { gender: "female", race: "asian" }); ``` +### Relatives + +There is a separate `generateRelative` function to make a relative of an existing face object. Call it like: + +```javascript +const { generate, generateRelative } = require("facesjs"); +const face1 = generate(); +const face2 = generateRelative({ gender: "male", relative: face1 }); +``` + +This works by randomizing only some features of the existing face, so the new face is fairly similar to the existing one, like an immediate family member. + ### React integration You can use the `display` function within any frontend JS framework, but for ease of use with the most popular one, this package includes a `Face` component for React. From e0bde26895e09bb29cfecad82323e4adcbfc3753 Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Fri, 21 Mar 2025 22:00:43 -0400 Subject: [PATCH 09/13] Better type --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index b2b72d1e..45de147a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,6 +10,6 @@ export const randUniform = (a: number, b: number): number => { return Math.random() * (b - a) + a; }; -export const randChoice = (array: T[] | readonly T[]): T => { +export const randChoice = (array: readonly T[]): T => { return array[Math.floor(Math.random() * array.length)]; }; From a82d588ef225cc737d4bd84eff2c5cb76d1ad2b9 Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Sun, 23 Mar 2025 08:43:09 -0400 Subject: [PATCH 10/13] Define features to replace all in one place, even if replacing with different logic --- src/generateRelative.ts | 74 +++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/src/generateRelative.ts b/src/generateRelative.ts index 2963a910..a8e927b0 100644 --- a/src/generateRelative.ts +++ b/src/generateRelative.ts @@ -26,47 +26,43 @@ export const generateRelative = ({ race, }); - // Always regenerate some properties - face.accessories = randomFace.accessories; - face.body.size = randomFace.body.size; - face.facialHair = randomFace.facialHair; - face.fatness = randomFace.fatness; - face.glasses = randomFace.glasses; - face.hair.id = randomFace.hair.id; - face.hair.flip = randomFace.hair.flip; - face.hairBg = randomFace.hairBg; - face.head.shave = randomFace.head.shave; - - // Regenerate some properties with some probability + // Regenerate some properties always, and others with some probabilityF const probRegenerate = 0.25; - const regenerateProperties = [ - "eyeLine", - "miscLine", - "mouth", - "nose", - "smileLine", - "eye.angle", - "eyebrow.angle", - "body.id", - "ear.id", - "eye.id", - "eyebrow.id", - "head.id", - "ear.size", - ] as const; - for (const path of regenerateProperties) { - if (Math.random() < probRegenerate) { - dset(face, path, delve(randomFace, path)); - } - } + const regenerateProperties = { + accessories: "always", + "body.id": "sometimes", + "body.size": "always", + "ear.id": "sometimes", + "ear.size": "sometimes", + "eye.angle": "sometimes", + "eye.id": "sometimes", + "eyebrow.angle": "sometimes", + "eyebrow.id": "sometimes", + eyeLine: "sometimes", + "face.body.color": "sometimesIfRaceIsKnown", + "face.hair.color": "sometimesIfRaceIsKnown", + facialHair: "always", + fatness: "always", + glasses: "always", + "hair.flip": "always", + "hair.id": "always", + hairBg: "always", + "head.id": "sometimes", + "head.shave": "always", + miscLine: "sometimes", + mouth: "sometimes", + nose: "sometimes", + smileLine: "sometimes", + } as const; - // Maybe apply race-appropriate new colors, if we have been able to identify the original race - if (race !== undefined) { - if (Math.random() < probRegenerate) { - face.body.color = randomFace.body.color; - } - if (Math.random() < probRegenerate) { - face.hair.color = randomFace.hair.color; + for (const [path, regenerateType] of Object.entries(regenerateProperties)) { + if ( + regenerateType === "always" || + ((regenerateType === "sometimes" || + (regenerateType === "sometimesIfRaceIsKnown" && race !== undefined)) && + Math.random() < probRegenerate) + ) { + dset(face, path, delve(randomFace, path)); } } From 0934b74c02c83259efd8e6e13c843773341bfd70 Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Sun, 23 Mar 2025 08:43:58 -0400 Subject: [PATCH 11/13] DRY --- public/editor/TopBar.tsx | 4 +--- src/common.ts | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/public/editor/TopBar.tsx b/public/editor/TopBar.tsx index 5d1de2b3..4f43cc58 100644 --- a/public/editor/TopBar.tsx +++ b/public/editor/TopBar.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Gender, Race } from "../../src/common"; +import { Gender, genders, Race, races } from "../../src/common"; import { useStateStore } from "./stateStore"; import { House, @@ -39,8 +39,6 @@ export const TopBar = () => { setShuffleRaceSettingObject, setShuffleOtherSettingObject, } = stateStore; - const genders: Gender[] = ["male", "female"]; - const races: Race[] = ["white", "black", "brown", "asian"]; const [genderInvalidOptions, setGenderInvalidOptions] = useState(false); const [raceInvalidOptions, setRaceInvalidOptions] = useState(false); diff --git a/src/common.ts b/src/common.ts index 3a0eda30..05e70eab 100644 --- a/src/common.ts +++ b/src/common.ts @@ -4,7 +4,8 @@ export type Overrides = { [key: string]: boolean | string | number | any[] | Overrides; }; -export type Gender = "male" | "female"; +export const genders = ["male", "female"] as const; +export type Gender = (typeof genders)[number]; export const features = [ "accessories", From d33b1a019a6e7f75c2dc0366337b535a8b81929a Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Sun, 23 Mar 2025 10:04:56 -0400 Subject: [PATCH 12/13] Exhaustively specify how to handle every part of a FaceConfig object in generateRelative, and have TypeScript enforce that it's exhaustive! --- src/generateRelative.ts | 91 +++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/src/generateRelative.ts b/src/generateRelative.ts index a8e927b0..58841862 100644 --- a/src/generateRelative.ts +++ b/src/generateRelative.ts @@ -26,45 +26,82 @@ export const generateRelative = ({ race, }); - // Regenerate some properties always, and others with some probabilityF - const probRegenerate = 0.25; - const regenerateProperties = { + // Regenerate some properties always, and others with some probability + type RegenerateType = + | "always" + | "never" + | "sometimes" + | "sometimesIfRaceIsKnown"; + type ToRegenerateProperties = T extends object + ? T extends any[] + ? U + : { + [K in keyof T]: T[K] extends object + ? ToRegenerateProperties | U + : U; + } + : U; + type RegenerateProperties = ToRegenerateProperties< + FaceConfig, + RegenerateType + >; + + const regenerateProperties: RegenerateProperties = { accessories: "always", - "body.id": "sometimes", - "body.size": "always", - "ear.id": "sometimes", - "ear.size": "sometimes", - "eye.angle": "sometimes", - "eye.id": "sometimes", - "eyebrow.angle": "sometimes", - "eyebrow.id": "sometimes", + body: { + color: "sometimesIfRaceIsKnown", + id: "sometimes", + size: "always", + }, + ear: "sometimes", + eye: "sometimes", + eyebrow: "sometimes", eyeLine: "sometimes", - "face.body.color": "sometimesIfRaceIsKnown", - "face.hair.color": "sometimesIfRaceIsKnown", facialHair: "always", fatness: "always", glasses: "always", - "hair.flip": "always", - "hair.id": "always", + hair: { + color: "sometimesIfRaceIsKnown", + flip: "always", + id: "always", + }, hairBg: "always", - "head.id": "sometimes", - "head.shave": "always", + head: { + id: "sometimes", + shave: "always", + }, + jersey: "never", miscLine: "sometimes", mouth: "sometimes", nose: "sometimes", smileLine: "sometimes", - } as const; + teamColors: "never", + }; - for (const [path, regenerateType] of Object.entries(regenerateProperties)) { - if ( - regenerateType === "always" || - ((regenerateType === "sometimes" || - (regenerateType === "sometimesIfRaceIsKnown" && race !== undefined)) && - Math.random() < probRegenerate) - ) { - dset(face, path, delve(randomFace, path)); + const probRegenerate = 0.25; + const processRegenerateProperties = ( + objOutput: any, + objRandom: any, + regeneratePropertiesLocal: + | RegenerateProperties + | Record, + ) => { + for (const [key, value] of Object.entries(regeneratePropertiesLocal)) { + if (typeof value === "string") { + if ( + value === "always" || + ((value === "sometimes" || + (value === "sometimesIfRaceIsKnown" && race !== undefined)) && + Math.random() < probRegenerate) + ) { + objOutput[key] = objRandom[key]; + } + } else { + processRegenerateProperties(objOutput[key], objRandom[key], value); + } } - } + }; + processRegenerateProperties(face, randomFace, regenerateProperties); // Override any ID properties that are not valid for the specified gender for (const key of features) { From 4ebe8fdb16cf4e4ac084202db173b67fee25c159 Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Sun, 23 Mar 2025 12:13:21 -0400 Subject: [PATCH 13/13] Docs, changelog --- CHANGELOG.md | 4 + public/index.html | 185 +++++++++++++++++++++++++++++----------- src/generateRelative.ts | 10 +-- 3 files changed, 140 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65893a18..d88b5dd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Development + +- #58 - New `generateRelative` function to generate a face that is similar to an exisiting face, like a sibling/parent/child. + # 4.2.2 (2024-08-04) - The `Face` React component should now be imported as: diff --git a/public/index.html b/public/index.html index 5688b863..d89064b2 100644 --- a/public/index.html +++ b/public/index.html @@ -60,51 +60,87 @@

Installation

npm install --save facesjs

Use

Import it with ES modules:

- +
import { display, generate } from "facesjs";

or CommonJS:

- +
const { display, generate } = require("facesjs");

Then, generate a random face:

- +
const face = generate();

And display it:

- -

If you’d like a non-random face, look inside the face variable and you’ll see all the available options for a manually constructed face.

+
// Display in a div with id "my-div-id"
+display("my-div-id", face);
+
+// Display in a div you already have a reference to
+const element = document.getElementById("my-div-id");
+display(element, face);
+

If you’d like a non-random face, look inside the face +variable and you’ll see all the available options for a manually +constructed face.

Overrides

-

Both display and generate accept an optional argument, specifying values to override either the randomly generated face (for generate) or the supplied face (for display). For instance:

- +

Both display and generate accept an +optional argument, specifying values to override either the randomly +generated face (for generate) or the supplied face (for +display). For instance:

+
// Generate a random face that always has blue skin
+const face = generate({ body: { color: "blue" } });
+
+// Display a face, but impose that it has blue skin
+display("my-div-id", face, { body: { color: "blue" } });

Options

-

The generate function takes a second optional argument, which takes in extra parameters for player creation, in the form of an object.

+

The generate function takes a second optional argument, +which takes in extra parameters for player creation, in the form of an +object.

Generate a female/male face (default is male):

- -

Assign a race attribute that can be white, black, asian, or brown (default is random):

- +
const face = generate(undefined, { gender: "female" });
+

Assign a race attribute that can be white, black, asian, or brown +(default is random):

+
const face = generate(undefined, { race: "white" });

Or both together:

- +
const face = generate(undefined, { gender: "female", race: "asian" });
+

Relatives

+

There is a separate generateRelative function to make a +relative of an existing face object. Call it like:

+
const { generate, generateRelative } = require("facesjs");
+const face1 = generate();
+const face2 = generateRelative({ gender: "male", relative: face1 });
+

This works by randomizing only some features of the existing face, so +the new face is fairly similar to the existing one, like an immediate +family member.

React integration

-

You can use the display function within any frontend JS framework, but for ease of use with the most popular one, this package includes a Face component for React.

- +

You can use the display function within any frontend JS +framework, but for ease of use with the most popular one, this package +includes a Face component for React.

+
import { generate } from "facesjs";
+import { Face } from "facesjs/react";
+import { useEffect } from "react";
+
+export const MyFace = ({ face }) => {
+    return <Face
+        face={face}
+        lazy
+        style={{
+            width: 100,
+        }}
+    >;
+};

Props of the Face component:

+++++++ @@ -134,14 +170,19 @@

React integration

- + - + @@ -161,16 +202,25 @@

React integration

Prop boolean falseIf true, then any errors when internally running display will be suppressed. This is useful if you accept user-defined faces and you don’t want errors from them to clog up your error logs.If true, then any errors when internally running +display will be suppressed. This is useful if you accept +user-defined faces and you don’t want errors from them to clog up your +error logs.
lazy boolean falseIf true, then application of overrides and rendering of the face will be delayed until this component is actually visible (as determined by an intersection observer).If true, then application of overrides and rendering of +the face will be delayed until this component is actually visible (as +determined by an intersection observer).
className

Exporting SVGs

API

-

You can use faceToSvgString to convert a face object to an SVG string.

- +

You can use faceToSvgString to convert a face object to +an SVG string.

+
import { faceToSvgString, generate } from "facesjs";
+
+const face = generate();
+const svg = faceToSvgString(face);

You can also specify overrides, similar to display:

- -

faceToSvgString is intended to be used in Node.js If you are doing client-side JS, it would be more efficient to render a face to the DOM using display and then convert it to a blob like this.

+
const svg = faceToSvgString(face, { body: { color: "blue" } });
+

faceToSvgString is intended to be used in Node.js If you +are doing client-side JS, it would be more efficient to render a face to +the DOM using display and then convert +it to a blob like this.

CLI

-

You can also use facesjs as a CLI program. All of the functionality from generate and display are available on the CLI too.

+

You can also use facesjs as a CLI program. All of the +functionality from generate and display are +available on the CLI too.

Examples

Output a random face to stdout:

$ npx facesjs
@@ -185,7 +235,9 @@

Options

-j, --input-json String faces.js JSON object to convert to SVG -r, --race Race - white/black/asian/brown, default is random -g, --gender Gender - male/female, default is male -

--input-file and --input-json can specify either an entire face object or a partial face object. If it’s a partial face object, the other features will be random.

+

--input-file and --input-json can specify +either an entire face object or a partial face object. If it’s a partial +face object, the other features will be random.

Development

Running pnpm run dev will do a few things:

    @@ -196,13 +248,42 @@

    Development

This lets you immediately see your changes as you work.

Adding new facial features

-

Each face is assembled from multiple SVGs. You can see them within the “svg” folder. If you want to add another feature, just create an SVG (using a vector graphics editor like Inkscape) and put it in the appropriate folder. It should automatically work. If not, it’s a bug, please let me know!

-

When creating SVGs, assume the size of the canvas is 400x600. For most features, it doesn’t matter where you draw on the canvas because it will automatically identify your object and position it in the appropriate place. But for head and hair SVGs, position does matter. For those you do need to make sure they are in the correct place on a 400x600 canvas, same as the existing head and hair SVGs. Otherwise it won’t know where to place the other facial features relative to the head and hair.

-

If you find it not quite placing a facial feature exactly where you want, it’s because by default it finds the center of the eye/eyebrow/mouth/nose SVG and places that in a specific location. If that’s not good for a certain facial feature, that behavior can be overridden in code. For instance, see how it’s done in display.js for the “pinocchio” nose which uses the left side of the SVG rather than the center to place it.

-

If you want a brand new “class” of facial features (like facial hair, or earrings, or hats) you’ll have to create a new subfolder within the “svg” folder and edit the code to recognize your new feature.

-

If you find any of this confusing, feel free to reach out to me for help! I would love for someone to help me make better looking faces :)

+

Each face is assembled from multiple SVGs. You can see them within +the “svg” folder. If you want to add another feature, just create an SVG +(using a vector graphics editor like Inkscape) and put it in the appropriate +folder. It should automatically work. If not, it’s a bug, please let me +know!

+

When creating SVGs, assume the size of the canvas is 400x600. For +most features, it doesn’t matter where you draw on the canvas because it +will automatically identify your object and position it in the +appropriate place. But for head and hair SVGs, position does matter. For +those you do need to make sure they are in the correct place on a +400x600 canvas, same as the existing head and hair SVGs. Otherwise it +won’t know where to place the other facial features relative to the head +and hair.

+

If you find it not quite placing a facial feature exactly where you +want, it’s because by default it finds the center of the +eye/eyebrow/mouth/nose SVG and places that in a specific location. If +that’s not good for a certain facial feature, that behavior can be +overridden in code. For instance, see how it’s done in display.js for +the “pinocchio” nose which uses the left side of the SVG rather than the +center to place it.

+

If you want a brand new “class” of facial features (like facial hair, +or earrings, or hats) you’ll have to create a new subfolder within the +“svg” folder and edit the code to recognize your new feature.

+

If you find any of this confusing, feel free to reach out to me for +help! I would love for someone to help me make better looking faces +:)

Credits

-

dumbmatter wrote most of the code, TravisJB89 made most of the graphics, Lia Cui made most of the female graphics, gurushida wrote the code to export faces as SVG strings, and tomkennedy22 wrote most of the editor UI code.

+

dumbmatter wrote most of +the code, TravisJB89 made +most of the graphics, Lia Cui +made most of the female graphics, gurushida wrote the code to +export faces as SVG strings, and tomkennedy22 wrote most of +the editor UI code.

diff --git a/src/generateRelative.ts b/src/generateRelative.ts index 58841862..5bbef6ee 100644 --- a/src/generateRelative.ts +++ b/src/generateRelative.ts @@ -27,11 +27,7 @@ export const generateRelative = ({ }); // Regenerate some properties always, and others with some probability - type RegenerateType = - | "always" - | "never" - | "sometimes" - | "sometimesIfRaceIsKnown"; + type RegenerateType = "always" | "sometimes" | "sometimesIfRaceIsKnown"; type ToRegenerateProperties = T extends object ? T extends any[] ? U @@ -70,12 +66,12 @@ export const generateRelative = ({ id: "sometimes", shave: "always", }, - jersey: "never", + jersey: "always", miscLine: "sometimes", mouth: "sometimes", nose: "sometimes", smileLine: "sometimes", - teamColors: "never", + teamColors: "always", }; const probRegenerate = 0.25;