This commit is contained in:
2026-11-05 09:57:11 -05:00
parent d34388bf2c
commit 89e392cc2e
81 changed files with 12410 additions and 1135 deletions
+42 -15
View File
@@ -5,16 +5,16 @@ import { ATLAS, BODY_PROFILES } from './media.mjs';
export const CHARACTER_OPTIONS = Object.freeze(Object.fromEntries(Object.entries({
bodies: BODY_PROFILES,
skin: ['porcelain','sand','honey','olive','umber','ebony'],
faces: ['calm','angular','soft','weathered'],
faces: ['calm','angular','soft','weathered','lined'],
eyes: ['hazel','brown','green','blue','grey'],
hairStyles: ['cropped','sidepart','wavy','braids','long','shaved','none'],
hairStyles: ['cropped','sidepart','wavy','braids','long','shaved','none','curly','topknot','undercut'],
hairColors: ['ash','flax','chestnut','auburn','black','silver'],
facialHair: ['none','stubble','moustache','short_beard','full_beard'],
garments: ['tunic','workshirt','jerkin','coat'],
facialHair: ['none','stubble','moustache','short_beard','full_beard','goatee','braided_beard'],
garments: ['tunic','workshirt','jerkin','coat','hooded_cloak','travel_coat','wool_mantle'],
clothColors: ['sage','teal','slate','mauve','ochre','rust','linen','charcoal'],
trousers: ['earth','slate','moss','charcoal'], boots: ['walnut','black','tan'],
accents: ['none','scarf','apron','sash','vest'],
details: ['none','freckles','scar','freckles_scar'],
accents: ['none','scarf','apron','sash','vest','satchel','tool_belt','pauldrons','hood'],
details: ['none','freckles','scar','freckles_scar','birthmark'],
}).map(([key, values])=>[key,Object.freeze([...values])])));
const palette = entries => Object.freeze(Object.fromEntries(Object.entries(entries)
@@ -66,6 +66,7 @@ function faceLayer(id) {
angular: [[1,2],[2,2],[5,2],[6,1],[4,4],[4,5],[2,6],[3,6]],
soft: [[2,2],[5,2],[3,5],[4,5],[3,6]],
weathered: [[1,2],[2,2],[5,2],[6,2],[1,4],[6,4],[4,4],[2,5],[5,5],[3,6],[4,6]],
lined: [[1,1],[2,2],[5,2],[6,1],[1,3],[6,3],[2,4],[5,4],[4,4],[3,6],[4,6]],
};
for (const [x,y] of marks[id]) dot(out,'face',x,y,y===6 ? [112,70,58] : INK);
return out;
@@ -85,12 +86,15 @@ function hairLayer(style, color) {
if (style==='sidepart') return y<1 || (y===1 && x<6) || (y===2 && x<2);
if (style==='wavy') return y===0 || (y===1 && x%3!==0) || (y===2 && x===0);
if (style==='braids') return y<1 || (x===0 || x===7) && y<6;
if (style==='curly') return y===0 || (y===1 && (x+y)%3!==0) || (y===2 && x%3===0);
if (style==='topknot') return y===0 && x>=2 && x<=5 || y===1 && x>=3 && x<=4 || y===2 && x<2;
if (style==='undercut') return y===0 || y===1 && x>4 || y===2 && x===7;
return y<1 || (x<1 || x>6) && y<7;
};
paint(out,'face',(x,y)=>fringe(x,y) ? strand(x,y) : null);
paint(out,'hair',(x,y)=>style==='shaved' && (x+y)%2 ? null : strand(x,y));
paint(out,'side',(x,y)=> {
const limit = {cropped:2,sidepart:3,wavy:4,braids:7,long:8,shaved:1}[style];
const limit = {cropped:2,sidepart:3,wavy:4,braids:7,long:8,shaved:1,curly:3,topknot:2,undercut:3}[style];
if (y>=limit || (y>=3 && x>=3 && x<=5)) return null; // exposed ear
if (style==='shaved' && x%2) return null;
return strand(x,y);
@@ -101,6 +105,7 @@ function hairLayer(style, color) {
paint(out,'sleeve',(x,y)=>y<2 && (x===0 || x===7) ? strand(x,y) : null);
if (style==='braids') for (const x of [0,7]) dot(out,'face',x,5,THREAD);
}
if (style==='topknot') paint(out,'hair',(x,y)=>x>=2&&x<=5&&y<2?strand(x,y):null);
return out;
}
function beardLayer(style,color) {
@@ -108,10 +113,11 @@ function beardLayer(style,color) {
paint(out,'face',(x,y)=> {
const moustache = y===5 && x>=2 && x<=5;
const lower = y>=6 && x>=1 && x<=6;
const mask = style==='moustache' ? moustache : style==='stubble' ?
const chin = (y===6 && x>=3 && x<=4) || (y===7 && x===3);
const mask = style==='moustache' ? moustache : style==='goatee' ? moustache || chin : style==='braided_beard' ? moustache || lower || y===4&&x>=2&&x<=5 : style==='stubble' ?
(lower && (x+y)%2===0) : style==='short_beard' ? moustache || (lower && x>=2 && x<=5) :
moustache || lower || (y===4 && (x===1 || x===6));
return mask ? tint(HAIR[color],(x+y)%3===0 ? 9 : -4) : null;
return mask ? tint(HAIR[color],style==='braided_beard'&&y>=6&&(y%2===0)?15:(x+y)%3===0 ? 9 : -4) : null;
});
if (style==='full_beard') paint(out,'side',(x,y)=>y>=5 && x<3 ? HAIR[color] : null);
return out;
@@ -128,11 +134,18 @@ function garmentLayer(style,color) {
if (style==='workshirt' && y===3 && x>=5 && x<=6) c=tint(base,-25);
if (style==='jerkin' && (x===1 || x===6 || y===5)) c=LEATHER;
if (style==='jerkin' && y===2 && (x===3 || x===4)) c=THREAD;
if (style==='coat' && (x===3 || x===4)) c=tint(base,-28);
if (style==='coat' && x===5 && [2,4,6].includes(y)) c=THREAD;
if (style==='coat' && y===0 && (x===1 || x===6)) c=tint(base,17);
if (style==='coat' || style==='travel_coat' || style==='hooded_cloak') {
if (x===3 || x===4) c=tint(base,-28);
if (x===5 && [2,4,6].includes(y)) c=THREAD;
if (y===0 && (x===1 || x===6)) c=tint(base,17);
}
if (style==='hooded_cloak' && y>=3 && (x===0 || x===7)) c=tint(base,-32);
if (style==='travel_coat' && y>=3 && (x===1 || x===6)) c=LEATHER;
if (style==='wool_mantle' && y<3) c=tint(base,(x+y)%2?12:-14);
}
if (tile==='back' && (style==='coat' && x===4 || style==='jerkin' && y===5)) c=tint(base,-23);
if (tile==='back' && ((style==='coat'||style==='travel_coat'||style==='hooded_cloak') && (x===3||x===4) || style==='jerkin' && y===5)) c=tint(base,-23);
if (tile==='back' && style==='hooded_cloak' && y<6) c=tint(base,-35);
if (tile==='back' && style==='wool_mantle' && y<4) c=tint(base,(x+y)%2?8:-10);
if (tile==='sleeve' && style==='jerkin' && y>=3) c=fabric(CLOTH.linen,x,y);
if (tile==='sleeve' && style==='workshirt' && y===5) c=THREAD;
return c;
@@ -152,22 +165,35 @@ function bootsLayer(id) {
function accentLayer(style,color) {
const out=image(); if (style==='none') return out;
const base=CLOTH[color];
if (style==='hood') {
for (const tile of ['face','hair','side']) paint(out,tile,(x,y)=>
tile==='face' ? (y<2||x<2||x>5?fabric(base,x,y):null) :
tile==='hair' ? (y<5?fabric(base,x,y):null) : (x<2||x>5)&&y<5?fabric(base,x,y):null);
return out;
}
for (const tile of ['tunic','back','sleeve']) paint(out,tile,(x,y)=> {
let mask=false;
if (style==='scarf') mask=y===0 || tile==='tunic' && x===2 && y<4;
if (style==='apron') mask=tile==='tunic' ? x>=1 && x<=6 && y>=2 : tile==='back' && y===3;
if (style==='sash') mask=tile!=='sleeve' && (x===y || x===y+1);
if (style==='vest') mask=tile!=='sleeve' && (tile==='back' || x<=2 || x>=5);
if (style==='tool_belt') mask=tile==='tunic' && (y===5 || y===6) || tile==='sleeve'&&y===6;
if (style==='pauldrons') mask=tile==='sleeve'&&y<3 || tile==='tunic'&&y<1;
if (style==='satchel') mask=tile==='back'&&x>=4&&y>=2 || tile==='sleeve'&&x>=5&&y>=2;
return mask ? fabric(base,x,y) : null;
});
if (style==='apron') for (const x of [3,4,5]) dot(out,'tunic',x,5,tint(base,-23));
if (style==='vest') for (const y of [2,4]) dot(out,'tunic',5,y,THREAD);
if (style==='tool_belt') for(const x of [1,2,5,6]) dot(out,'tunic',x,5,LEATHER);
if (style==='satchel') for(const [x,y] of [[4,3],[5,3],[4,4],[5,4],[4,5]]) dot(out,'back',x,y,THREAD);
if (style==='pauldrons') for(const tile of ['sleeve']) for(const x of [1,6]) dot(out,tile,x,1,THREAD);
return out;
}
function detailLayer(id) {
const out=image();
if (id.includes('freckles')) for (const [x,y] of [[1,4],[2,5],[5,5],[6,4]]) dot(out,'face',x,y,[119,76,51]);
if (id.includes('scar')) for (const [x,y] of [[6,2],[6,3],[7,4]]) dot(out,'face',x,y,[158,109,91]);
if (id==='birthmark') for(const [x,y] of [[6,4],[7,4],[6,5]]) dot(out,'face',x,y,[143,82,75]);
return out;
}
@@ -213,8 +239,9 @@ export function characterLayers(options = {}) {
// Clothing precedes long hair: shoulder locks must not be erased by tunics.
return [name(`skin_${p.skin}`),name(`face_${p.face}`),name(`eyes_${p.eyes}`),
name(`garment_${p.garment}_${p.cloth}`),name(`trousers_${p.trousers}`),name(`boots_${p.boots}`),
name(`accent_${p.accent}_${p.accentColor}`),name(`hair_${p.hair}_${p.hairColor}`),
name(`beard_${p.beard}_${p.hairColor}`),name(`detail_${p.detail}`)];
name(`hair_${p.hair}_${p.hairColor}`),name(`beard_${p.beard}_${p.hairColor}`),
name(`accent_${p.accent}_${p.accentColor}`),
name(`detail_${p.detail}`)];
}
export function characterTexture(options = {}) { return characterLayers(options).join('^'); }
export function characterMesh(body = 'balanced') {