diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/facesjs.iml b/.idea/facesjs.iml new file mode 100644 index 0000000..0c8867d --- /dev/null +++ b/.idea/facesjs.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..42e034d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/editor.html b/public/editor.html index 5c6ce95..1a320c8 100644 --- a/public/editor.html +++ b/public/editor.html @@ -204,6 +204,80 @@

+
+
+
+

Aging

+
+ +
+
+
+
+
+ +
+
+
+   +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
@@ -353,7 +427,7 @@

let face; if (location.hash.length <= 1) { - face = faces.generate(); + face = faces.generate({ aging: { enabled: true } }); } else { try { face = JSON.parse(atob(location.hash.slice(1))); } catch (error) { console.error(error); - face = faces.generate(); + face = faces.generate({ aging: { enabled: true } }); } } @@ -904,13 +978,13 @@

Array.from(document.getElementsByClassName("random-attribute")).forEach( (elem) => { if (!elem.checked) { - const attr = elem.id.split("-").slice(1); - if (attr.length === 1) newFace[attr[0]] = oldFace[attr[0]]; - else if (!isNaN(parseInt(attr[1]))) { - const idx = parseInt(attr[1]); - newFace[attr[0]][idx] = oldFace[attr[0]][idx]; - } else if (attr.length === 2) - newFace[attr[0]][attr[1]] = oldFace[attr[0]][attr[1]]; + const parts = elem.id.split("-").slice(1); + if (parts.length === 1) newFace[parts[0]] = oldFace[parts[0]]; + else if (!isNaN(parseInt(parts[1]))) { + const idx = parseInt(parts[1]); + newFace[parts[0]][idx] = oldFace[parts[0]][idx]; + } else if (parts.length === 2) + newFace[parts[0]][parts[1]] = oldFace[parts[0]][parts[1]]; } } ); @@ -929,16 +1003,27 @@

}; const initializeSelectOptions = () => { + let selectElement, optionElement; for (const feature of Object.keys(faces.svgsIndex)) { const options = faces.svgsIndex[feature]; - const selectElement = document.getElementById(`${feature}-id`); + selectElement = document.getElementById(`${feature}-id`); for (const option of options) { - const optionElement = document.createElement("option"); + optionElement = document.createElement("option"); optionElement.value = option; optionElement.text = option; selectElement.add(optionElement, null); } } + selectElement = document.getElementById("aging-enabled"); + optionElement = document.createElement("option"); + optionElement.selected = true; + optionElement.value = "true"; + optionElement.text = "Enabled"; + selectElement.add(optionElement, null); + optionElement = document.createElement("option"); + optionElement.value = "false"; + optionElement.text = "Disabled"; + selectElement.add(optionElement, null); }; const isValue = (obj) => @@ -975,6 +1060,12 @@

if (typeof oldValue === "number") { return parseFloat(event.target.value); } + if ( + typeof oldValue === "boolean" && + typeof event.target.value !== "boolean" + ) { + return event.target.value === "true"; + } if (typeof oldValue === "boolean") { return event.target.checked; } @@ -1008,8 +1099,10 @@

} document.getElementById("randomize").addEventListener("click", () => { - const oldFace = face; - face = randomizeFace(oldFace, faces.generate()); + face = randomizeFace( + face, + faces.generate({ aging: { enabled: face.aging.enabled } }) + ); initializeFormValues(); updateDisplay(); }); diff --git a/src/display.ts b/src/display.ts index f5721c3..eadca99 100644 --- a/src/display.ts +++ b/src/display.ts @@ -97,11 +97,93 @@ type FeatureInfo = { scaleFatness?: true; }; +const hashCode = (str) => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const character = str.charCodeAt(i); + hash = (hash << 5) - hash + character; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash); +}; + +const deterministicRandom = (face: Face) => { + const hash = + hashCode(face.body.id) + + hashCode(face.body.color) + + hashCode(face.head.id) + + hashCode("" + face.fatness) + + hashCode(face.hair.id) + + hashCode(face.hair.color); + return (hash % 1000) / 1000; +}; + +const ageHair = (hairId: String) => { + switch (hairId) { + case "afro": + return "short"; + case "afro2": + return "short"; + case "blowoutFade": + return "cropFade2"; + case "cornrows": + return "short-fade"; + case "curly3": + return "short3"; + case "dreads": + return "short-fade"; + case "emo": + return "short2"; + case "faux-hawk": + return "short3"; + case "fauxhawk-fade": + return "short-fade"; + case "high": + return "short"; + case "juice": + return "short2"; + case "longHair": + return "short-fade"; + case "shaggy2": + return "shaggy1"; + case "short-bald": + return "short-bald"; + case "shortBangs": + return "short-bald"; + case "spike": + return "short"; + case "spike2": + return "short"; + case "spike3": + return "short"; + case "spike4": + return "short"; + case "tall-fade": + return "crop-fade"; + default: + return "short-fade"; + } +}; + const drawFeature = (svg: SVGSVGElement, face: Face, info: FeatureInfo) => { - const feature = face[info.name]; + const feature = Object.assign({}, face[info.name]); if (!feature || !svgs[info.name]) { return; } + if (face.aging && face.aging.enabled) { + if ( + info.name === "hair" && + face.aging.age + face.aging.maturity / 2 >= 30 && + deterministicRandom(face) < 0.5 + ) + feature.id = ageHair(feature.id); + else if ( + info.name === "hairBg" && + face.aging.age + face.aging.maturity / 2 >= 27 && + deterministicRandom(face) < 0.75 + ) + feature.id = "none"; + } // @ts-ignore let featureSVGString = svgs[info.name][feature.id]; @@ -111,14 +193,15 @@ const drawFeature = (svg: SVGSVGElement, face: Face, info: FeatureInfo) => { // @ts-ignore if (feature.shave) { + let shave; + if (face.aging && face.aging.enabled) + if (face.aging.age + face.aging.maturity > 23) shave = feature.shave; + else shave = "rgba(0,0,0,0)"; + else shave = feature.shave; // @ts-ignore - featureSVGString = featureSVGString.replace("$[faceShave]", feature.shave); - } - - // @ts-ignore - if (feature.shave) { + featureSVGString = featureSVGString.replace("$[faceShave]", shave); // @ts-ignore - featureSVGString = featureSVGString.replace("$[headShave]", feature.shave); + featureSVGString = featureSVGString.replace("$[headShave]", shave); } featureSVGString = featureSVGString.replace("$[skinColor]", face.body.color); @@ -311,6 +394,33 @@ const display = ( ]; for (const info of featureInfos) { + if (face.aging && face.aging.enabled) { + if ( + info.name === "miscLine" && + face.aging.age + face.aging.maturity >= 22 && + face.miscLine.id.startsWith("freckles") + ) + continue; + if ( + info.name === "miscLine" && + face.aging.age + face.aging.maturity < 25 && + face.miscLine.id.startsWith("chin") + ) + continue; + if ( + info.name === "smileLine" && + face.aging.age + face.aging.maturity < 27 + ) + continue; + if (info.name === "eyeLine" && face.aging.age + face.aging.maturity < 30) + continue; + if ( + info.name === "miscLine" && + face.aging.age + face.aging.maturity < 34 && + face.miscLine.id.startsWith("forehead") + ) + continue; + } drawFeature(svg, face, info); } }; diff --git a/src/generate.ts b/src/generate.ts index 5847a31..eff6656 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,9 +1,13 @@ import override, { Overrides } from "./override"; import svgsIndex from "./svgs-index"; -const getID = (type: string): string => { - // @ts-ignore - return svgsIndex[type][Math.floor(Math.random() * svgsIndex[type].length)]; +const getID = (type: string, canBeNone?: true): string => { + let id; + do { + // @ts-ignore + id = svgsIndex[type][Math.floor(Math.random() * svgsIndex[type].length)]; + } while (!canBeNone && id == "none"); + return id; }; type Race = "asian" | "black" | "brown" | "white"; @@ -74,7 +78,22 @@ const generate = (overrides?: Overrides, options?: { race?: Race }) => { palette.hair[Math.floor(Math.random() * palette.hair.length)]; const isFlipped = Math.random() < 0.5; + let aging; + if (overrides && overrides.aging) { + aging = overrides.aging; + // @ts-ignore + if (!overrides.aging.age) aging.age = Math.floor(Math.random() * 16 + 19); + // @ts-ignore + if (!overrides.aging.maturity) + aging.maturity = Math.floor(Math.random() * 5 - 2); + } else + aging = { + enabled: true, + age: Math.floor(Math.random() * 16 + 19), + maturity: Math.floor(Math.random() * 5 - 1), + }; const face = { + aging: aging, fatness: roundTwoDecimals(Math.random()), teamColors: defaultTeamColors, hairBg: { @@ -98,14 +117,26 @@ const generate = (overrides?: Overrides, options?: { race?: Race }) => { })`, }, eyeLine: { - id: Math.random() < 0.75 ? getID("eyeLine") : "none", + // @ts-ignore + id: + aging.enabled || Math.random() < 0.75 + ? getID("eyeLine", !aging.enabled) + : "none", }, smileLine: { - id: Math.random() < 0.75 ? getID("smileLine") : "none", + // @ts-ignore + id: + aging.enabled || Math.random() < 0.75 + ? getID("smileLine", !aging.enabled) + : "none", size: roundTwoDecimals(0.25 + 2 * Math.random()), }, miscLine: { - id: Math.random() < 0.5 ? getID("miscLine") : "none", + // @ts-ignore + id: + aging.enabled || Math.random() < 0.5 + ? getID("miscLine", !aging.enabled) + : "none", }, facialHair: { id: Math.random() < 0.5 ? getID("facialHair") : "none", diff --git a/tools/process-svgs.js b/tools/process-svgs.js index 89c5b30..de26288 100644 --- a/tools/process-svgs.js +++ b/tools/process-svgs.js @@ -15,11 +15,13 @@ const processSVGs = async () => { const svgs = {}; for (const folder of folders) { + if (folder === ".DS_Store") continue; svgs[folder] = {}; const subfolder = path.join(svgFolder, folder); const files = fs.readdirSync(subfolder); for (const file of files) { + if (file === ".DS_Store") continue; const key = path.basename(file, ".svg"); const contents = fs.readFileSync(path.join(subfolder, file), "utf8"); @@ -38,7 +40,7 @@ const processSVGs = async () => { ); const svgsIndex = { - ...svgs + ...svgs, }; for (const key of Object.keys(svgsIndex)) { svgsIndex[key] = Object.keys(svgsIndex[key]);