Add configurable crossover controls
This commit is contained in:
@@ -19,6 +19,13 @@ const MAX_PATHS=3200;
|
|||||||
const REVEAL_TIME=2200;
|
const REVEAL_TIME=2200;
|
||||||
const PREVIEW_TIME=1800;
|
const PREVIEW_TIME=1800;
|
||||||
const HISTORY_PAGE=48;
|
const HISTORY_PAGE=48;
|
||||||
|
const MIX_OPTIONS=[.2,.35,.5,.65,.8];
|
||||||
|
const MUTATION_PROFILES=[
|
||||||
|
{name:'stable',strength:.55,structural:.025},
|
||||||
|
{name:'natural',strength:1,structural:.07},
|
||||||
|
{name:'active',strength:1.45,structural:.15},
|
||||||
|
{name:'volatile',strength:2,structural:.26}
|
||||||
|
];
|
||||||
|
|
||||||
let width=0,height=0,background;
|
let width=0,height=0,background;
|
||||||
let view={scale:1,offsetX:0,offsetY:0};
|
let view={scale:1,offsetX:0,offsetY:0};
|
||||||
@@ -26,7 +33,16 @@ let database=null,storedHistoryIds=new Set(),historyVisible=HISTORY_PAGE;
|
|||||||
let animationFrame=0,animationToken=0,previewAnimationFrame=0,previewToken=0,lastProgress=1,galleryOpen=false,toastTimer=0,isBusy=false;
|
let animationFrame=0,animationToken=0,previewAnimationFrame=0,previewToken=0,lastProgress=1,galleryOpen=false,toastTimer=0,isBusy=false;
|
||||||
let state=freshState();
|
let state=freshState();
|
||||||
|
|
||||||
function freshState(){return{version:6,history:[],nextId:1,current:null,selectedParents:[]}}
|
function normalizeBreeding(value){
|
||||||
|
const requested=Number(value?.mix);
|
||||||
|
const requestedLevel=Number(value?.mutationLevel);
|
||||||
|
const mix=MIX_OPTIONS.reduce((closest,option)=>Math.abs(option-requested)<Math.abs(closest-requested)?option:closest,.5);
|
||||||
|
return{
|
||||||
|
mix:Number.isFinite(requested)?mix:.5,
|
||||||
|
mutationLevel:Number.isFinite(requestedLevel)?clamp(Math.round(requestedLevel),0,3):1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function freshState(){return{version:7,history:[],nextId:1,current:null,selectedParents:[],breeding:normalizeBreeding({mix:.5,mutationLevel:1})}}
|
||||||
function baseGenome(){
|
function baseGenome(){
|
||||||
return{
|
return{
|
||||||
family:'arboreal',branches:2,angle:.52,scale:.68,depth:8,symmetry:1,anchors:1,anchorStart:.65,
|
family:'arboreal',branches:2,angle:.52,scale:.68,depth:8,symmetry:1,anchors:1,anchorStart:.65,
|
||||||
@@ -68,6 +84,10 @@ function familyName(family){
|
|||||||
}
|
}
|
||||||
function curveName(mode){return['linear','sine','cosine','tangent','harmonic'][Math.round(mode)]||'linear'}
|
function curveName(mode){return['linear','sine','cosine','tangent','harmonic'][Math.round(mode)]||'linear'}
|
||||||
function layoutName(mode){return['radial','fan','parallel','bilateral'][Math.round(mode)]||'radial'}
|
function layoutName(mode){return['radial','fan','parallel','bilateral'][Math.round(mode)]||'radial'}
|
||||||
|
function mixName(mix){
|
||||||
|
const a=Math.round(mix*100);
|
||||||
|
return`${a}% A · ${100-a}% B`;
|
||||||
|
}
|
||||||
function classifyFamily(genome){
|
function classifyFamily(genome){
|
||||||
if(genome.family)return genome.family;
|
if(genome.family)return genome.family;
|
||||||
if(genome.rootLayout===1)return'fan';
|
if(genome.rootLayout===1)return'fan';
|
||||||
@@ -92,7 +112,8 @@ function normalizeRecord(item){
|
|||||||
source:item?.source||'legacy',
|
source:item?.source||'legacy',
|
||||||
parents:item?.parents||[],
|
parents:item?.parents||[],
|
||||||
inheritance:item?.inheritance||null,
|
inheritance:item?.inheritance||null,
|
||||||
mutations:item?.mutations||[]
|
mutations:item?.mutations||[],
|
||||||
|
breeding:item?.breeding?normalizeBreeding(item.breeding):null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,29 +152,37 @@ async function hydrateState(){
|
|||||||
seed:Number(oldCurrent.seed)||randomSeed(),
|
seed:Number(oldCurrent.seed)||randomSeed(),
|
||||||
genome:normalizeGenome(oldCurrent.genome),
|
genome:normalizeGenome(oldCurrent.genome),
|
||||||
source:oldCurrent.source&&oldCurrent.source!=='restored'?oldCurrent.source:currentHistoryRecord?.source||'restored',
|
source:oldCurrent.source&&oldCurrent.source!=='restored'?oldCurrent.source:currentHistoryRecord?.source||'restored',
|
||||||
parents:oldCurrent.parents||[],inheritance:oldCurrent.inheritance||null,mutations:oldCurrent.mutations||[]
|
parents:oldCurrent.parents||[],inheritance:oldCurrent.inheritance||null,mutations:oldCurrent.mutations||[],
|
||||||
|
breeding:oldCurrent.breeding?normalizeBreeding(oldCurrent.breeding):currentHistoryRecord?.breeding||null
|
||||||
}:history.length?{...history.at(-1),source:'restored'}:null;
|
}:history.length?{...history.at(-1),source:'restored'}:null;
|
||||||
const nextId=Math.max(Number(source?.nextId||source?.nextOrganismId)||1,...history.map(item=>item.id+1),current?current.id+1:1);
|
const nextId=Math.max(Number(source?.nextId||source?.nextOrganismId)||1,...history.map(item=>item.id+1),current?current.id+1:1);
|
||||||
const selectedParents=(source?.selectedParents||[]).filter(id=>history.some(item=>item.id===id)).slice(0,2);
|
const selectedParents=(source?.selectedParents||[]).filter(id=>history.some(item=>item.id===id)).slice(0,2);
|
||||||
state={version:6,history,nextId,current,selectedParents};
|
const breeding=normalizeBreeding(source?.breeding);
|
||||||
|
state={version:7,history,nextId,current,selectedParents,breeding};
|
||||||
storedHistoryIds=new Set(records.map(item=>item.id));
|
storedHistoryIds=new Set(records.map(item=>item.id));
|
||||||
if(saved?.version!==6||(!records.length&&history.length))saveState();
|
if(saved?.version!==7||(!records.length&&history.length))saveState();
|
||||||
}catch(error){
|
}catch(error){
|
||||||
console.warn('IndexedDB is unavailable; using a temporary session',error);
|
console.warn('IndexedDB is unavailable; using a temporary session',error);
|
||||||
const history=(legacy?.population||[]).map(normalizeRecord);
|
const history=(legacy?.population||[]).map(normalizeRecord);
|
||||||
state={version:6,history,nextId:Math.max(1,...history.map(item=>item.id+1)),current:history.at(-1)||null,selectedParents:[]};
|
state={
|
||||||
|
version:7,history,nextId:Math.max(1,...history.map(item=>item.id+1)),current:history.at(-1)||null,
|
||||||
|
selectedParents:[],breeding:normalizeBreeding(legacy?.breeding)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function persistableCurrent(){
|
function persistableCurrent(){
|
||||||
if(!state.current)return null;
|
if(!state.current)return null;
|
||||||
const{id,seed,genome,source,parents,inheritance,mutations}=state.current;
|
const{id,seed,genome,source,parents,inheritance,mutations,breeding}=state.current;
|
||||||
return{id,seed,genome,source,parents,inheritance,mutations};
|
return{id,seed,genome,source,parents,inheritance,mutations,breeding};
|
||||||
}
|
}
|
||||||
function saveState(){
|
function saveState(){
|
||||||
if(!database){ui.storageStatus.textContent='storage unavailable';return}
|
if(!database){ui.storageStatus.textContent='storage unavailable';return}
|
||||||
try{
|
try{
|
||||||
const transaction=database.transaction([DB_STATE,DB_HISTORY],'readwrite');
|
const transaction=database.transaction([DB_STATE,DB_HISTORY],'readwrite');
|
||||||
transaction.objectStore(DB_STATE).put({version:6,nextId:state.nextId,current:persistableCurrent(),selectedParents:state.selectedParents,history:[]},'main');
|
transaction.objectStore(DB_STATE).put({
|
||||||
|
version:7,nextId:state.nextId,current:persistableCurrent(),selectedParents:state.selectedParents,
|
||||||
|
breeding:state.breeding,history:[]
|
||||||
|
},'main');
|
||||||
const historyStore=transaction.objectStore(DB_HISTORY);
|
const historyStore=transaction.objectStore(DB_HISTORY);
|
||||||
const additions=state.history.filter(item=>!storedHistoryIds.has(item.id));
|
const additions=state.history.filter(item=>!storedHistoryIds.has(item.id));
|
||||||
for(const item of additions)historyStore.put(item);
|
for(const item of additions)historyStore.put(item);
|
||||||
@@ -305,15 +334,24 @@ const mutationRules={
|
|||||||
rootLayout:[1,0,3,true],rootSpread:[.45,0,Math.PI*2],rootSpacing:[.045,.04,.24],
|
rootLayout:[1,0,3,true],rootSpread:[.45,0,Math.PI*2],rootSpacing:[.045,.04,.24],
|
||||||
saturation:[12,48,92],lightness:[10,48,86],growthOverlap:[.16,.2,.85]
|
saturation:[12,48,92],lightness:[10,48,86],growthOverlap:[.16,.2,.85]
|
||||||
};
|
};
|
||||||
function crossoverGenomes(parentA,parentB,seed){
|
function mutationCountFor(level,roll){
|
||||||
|
if(level===0)return roll<.92?0:1;
|
||||||
|
if(level===1)return roll<.8?0:roll<.98?1:2;
|
||||||
|
if(level===2)return roll<.55?0:roll<.9?1:2;
|
||||||
|
return roll<.25?0:roll<.6?1:roll<.87?2:3;
|
||||||
|
}
|
||||||
|
function crossoverGenomes(parentA,parentB,seed,options={}){
|
||||||
const a=normalizeGenome(parentA.genome),b=normalizeGenome(parentB.genome),child={},inherited={a:0,b:0};
|
const a=normalizeGenome(parentA.genome),b=normalizeGenome(parentB.genome),child={},inherited={a:0,b:0};
|
||||||
const keys=Object.keys(baseGenome()).filter(key=>key!=='family');
|
const keys=Object.keys(baseGenome()).filter(key=>key!=='family');
|
||||||
|
const breeding=normalizeBreeding(options),profile=MUTATION_PROFILES[breeding.mutationLevel];
|
||||||
|
const targetA=clamp(Math.round(keys.length*breeding.mix),1,keys.length-1);
|
||||||
|
const fromAKeys=new Set(keys.map((key,index)=>({key,index,score:randomFrom(seed,200+index)}))
|
||||||
|
.sort((left,right)=>left.score-right.score||left.index-right.index).slice(0,targetA).map(item=>item.key));
|
||||||
for(const[index,key]of keys.entries()){
|
for(const[index,key]of keys.entries()){
|
||||||
const fromA=index===0||index>0&&randomFrom(seed,200+index)<.5;
|
const fromA=fromAKeys.has(key);
|
||||||
child[key]=fromA?a[key]:b[key];inherited[fromA?'a':'b']++;
|
child[key]=fromA?a[key]:b[key];inherited[fromA?'a':'b']++;
|
||||||
}
|
}
|
||||||
if(inherited.b===0){const key=keys.at(-1);child[key]=b[key];inherited.a--;inherited.b++}
|
const mutations=[],mutationCount=mutationCountFor(breeding.mutationLevel,randomFrom(seed,280)),used=new Set();
|
||||||
const mutations=[],roll=randomFrom(seed,280),mutationCount=roll<.8?0:roll<.98?1:2,used=new Set();
|
|
||||||
for(let index=0;index<mutationCount;index++){
|
for(let index=0;index<mutationCount;index++){
|
||||||
let choice=Math.floor(randomFrom(seed,281+index)*keys.length);
|
let choice=Math.floor(randomFrom(seed,281+index)*keys.length);
|
||||||
while(used.has(choice)||!mutationRules[keys[choice]])choice=(choice+1)%keys.length;
|
while(used.has(choice)||!mutationRules[keys[choice]])choice=(choice+1)%keys.length;
|
||||||
@@ -329,7 +367,10 @@ function crossoverGenomes(parentA,parentB,seed){
|
|||||||
value=child[key]===0?1+Math.floor(randomFrom(seed,310+index)*4):randomFrom(seed,320+index)<.16?0:clamp(child[key]+direction,1,4);
|
value=child[key]===0?1+Math.floor(randomFrom(seed,310+index)*4):randomFrom(seed,320+index)<.16?0:clamp(child[key]+direction,1,4);
|
||||||
}else if(key==='rootLayout'){
|
}else if(key==='rootLayout'){
|
||||||
value=child[key]===0?1+Math.floor(randomFrom(seed,310+index)*3):randomFrom(seed,320+index)<.16?0:clamp(child[key]+direction,1,3);
|
value=child[key]===0?1+Math.floor(randomFrom(seed,310+index)*3):randomFrom(seed,320+index)<.16?0:clamp(child[key]+direction,1,3);
|
||||||
}else value=integer?child[key]+direction*amount:child[key]+direction*amount*(.45+randomFrom(seed,310+index)*.55);
|
}else{
|
||||||
|
const step=integer?Math.max(1,Math.round(amount*profile.strength)):amount*profile.strength;
|
||||||
|
value=integer?child[key]+direction*step:child[key]+direction*step*(.45+randomFrom(seed,310+index)*.55);
|
||||||
|
}
|
||||||
if(integer)value=Math.round(value);
|
if(integer)value=Math.round(value);
|
||||||
child[key]=key==='phase'||key==='hue'?((value%max)+max)%max:clamp(value,min,max);
|
child[key]=key==='phase'||key==='hue'?((value%max)+max)%max:clamp(value,min,max);
|
||||||
if(key==='rootLayout'&&child.rootLayout>0){
|
if(key==='rootLayout'&&child.rootLayout>0){
|
||||||
@@ -338,7 +379,7 @@ function crossoverGenomes(parentA,parentB,seed){
|
|||||||
}
|
}
|
||||||
mutations.push(key);
|
mutations.push(key);
|
||||||
}
|
}
|
||||||
if(a.figureSides===0&&b.figureSides===0&&child.figureSides===0&&randomFrom(seed,330)<.09){
|
if(a.figureSides===0&&b.figureSides===0&&child.figureSides===0&&randomFrom(seed,330)<profile.structural*1.25){
|
||||||
child.figureSides=3+Math.floor(randomFrom(seed,331)*6);
|
child.figureSides=3+Math.floor(randomFrom(seed,331)*6);
|
||||||
child.figureSpan=pick(seed,332,[.25,.5,.75,1]);
|
child.figureSpan=pick(seed,332,[.25,.5,.75,1]);
|
||||||
child.figureScale=range(seed,333,.12,.3);
|
child.figureScale=range(seed,333,.12,.3);
|
||||||
@@ -346,20 +387,20 @@ function crossoverGenomes(parentA,parentB,seed){
|
|||||||
child.figureSpin=range(seed,335,-.45,.45);
|
child.figureSpin=range(seed,335,-.45,.45);
|
||||||
mutations.push('figureSides');
|
mutations.push('figureSides');
|
||||||
}
|
}
|
||||||
if(a.curveMode===0&&b.curveMode===0&&child.curveMode===0&&randomFrom(seed,340)<.07){
|
if(a.curveMode===0&&b.curveMode===0&&child.curveMode===0&&randomFrom(seed,340)<profile.structural){
|
||||||
child.curveMode=1+Math.floor(randomFrom(seed,341)*4);
|
child.curveMode=1+Math.floor(randomFrom(seed,341)*4);
|
||||||
child.curveAmplitude=range(seed,342,.06,.24);
|
child.curveAmplitude=range(seed,342,.06,.24);
|
||||||
child.curveFrequency=range(seed,343,.6,2.2);
|
child.curveFrequency=range(seed,343,.6,2.2);
|
||||||
mutations.push('curveMode');
|
mutations.push('curveMode');
|
||||||
}
|
}
|
||||||
if(a.rootLayout===0&&b.rootLayout===0&&child.rootLayout===0&&randomFrom(seed,350)<.07){
|
if(a.rootLayout===0&&b.rootLayout===0&&child.rootLayout===0&&randomFrom(seed,350)<profile.structural){
|
||||||
child.rootLayout=1+Math.floor(randomFrom(seed,351)*3);
|
child.rootLayout=1+Math.floor(randomFrom(seed,351)*3);
|
||||||
child.rootSpread=child.rootLayout===1?range(seed,352,.7,2.4):range(seed,352,0,.6);
|
child.rootSpread=child.rootLayout===1?range(seed,352,.7,2.4):range(seed,352,0,.6);
|
||||||
child.rootSpacing=range(seed,353,.06,.16);
|
child.rootSpacing=range(seed,353,.06,.16);
|
||||||
mutations.push('rootLayout');
|
mutations.push('rootLayout');
|
||||||
}
|
}
|
||||||
child.family=classifyFamily({...child,family:null});
|
child.family=classifyFamily({...child,family:null});
|
||||||
return{genome:child,inherited,mutations};
|
return{genome:child,inherited,mutations,breeding};
|
||||||
}
|
}
|
||||||
|
|
||||||
function trigonometricWave(mode,phase){
|
function trigonometricWave(mode,phase){
|
||||||
@@ -540,7 +581,8 @@ function updateView(){
|
|||||||
}else if(width>760){
|
}else if(width>760){
|
||||||
left=356;right=width-28;top=82;bottom=height-112;
|
left=356;right=width-28;top=82;bottom=height-112;
|
||||||
}else{
|
}else{
|
||||||
left=22;right=width-22;top=Math.max(270,ui.formula.getBoundingClientRect().bottom+14);bottom=height-112;
|
left=22;right=width-22;top=Math.max(270,ui.formula.getBoundingClientRect().bottom+14);
|
||||||
|
bottom=Math.min(height-112,document.querySelector('.action-bar').getBoundingClientRect().top-14);
|
||||||
}
|
}
|
||||||
if(right-left<160){left=22;right=width-22}
|
if(right-left<160){left=22;right=width-22}
|
||||||
if(bottom-top<150&&width>760){top=76;bottom=height-98}
|
if(bottom-top<150&&width>760){top=76;bottom=height-98}
|
||||||
@@ -624,11 +666,11 @@ function downloadBlob(blob,filename){
|
|||||||
}
|
}
|
||||||
function exportFormula(){
|
function exportFormula(){
|
||||||
if(!state.current)return;
|
if(!state.current)return;
|
||||||
const{id,seed,genome,parents,inheritance,mutations}=state.current;
|
const{id,seed,genome,parents,inheritance,mutations,breeding}=state.current;
|
||||||
const payload={
|
const payload={
|
||||||
format:'fractalier-formula',version:1,id:formulaId(id),seed,
|
format:'fractalier-formula',version:1,id:formulaId(id),seed,
|
||||||
equation:'F[n+1] = union_r L_layout(r) union_i(s[n] R(i alpha[n] + n tau) K(kappa[n] + gamma T(f t + phi)) A[i](F[n])) union Cq(E[n]) union Pm^lambda(E[n])',
|
equation:'F[n+1] = union_r L_layout(r) union_i(s[n] R(i alpha[n] + n tau) K(kappa[n] + gamma T(f t + phi)) A[i](F[n])) union Cq(E[n]) union Pm^lambda(E[n])',
|
||||||
genome,parents:parents.map(formulaId),inheritance,mutations
|
genome,parents:parents.map(formulaId),inheritance,mutations,breeding
|
||||||
};
|
};
|
||||||
downloadBlob(new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}),`fractalier-${formulaId(id)}.json`);
|
downloadBlob(new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}),`fractalier-${formulaId(id)}.json`);
|
||||||
closeExportMenu();showToast('Formula JSON downloaded');
|
closeExportMenu();showToast('Formula JSON downloaded');
|
||||||
@@ -782,9 +824,9 @@ function animateFormula(onComplete){
|
|||||||
};
|
};
|
||||||
animationFrame=requestAnimationFrame(frame);
|
animationFrame=requestAnimationFrame(frame);
|
||||||
}
|
}
|
||||||
function runtimeFormula({id,seed,genome,source,parents=[],inheritance=null,mutations=[]}){
|
function runtimeFormula({id,seed,genome,source,parents=[],inheritance=null,mutations=[],breeding=null}){
|
||||||
const normalized=normalizeGenome(genome),paths=compileGeometry(normalized);
|
const normalized=normalizeGenome(genome),paths=compileGeometry(normalized);
|
||||||
return{id,seed,genome:normalized,source,parents,inheritance,mutations,paths,bounds:geometryBounds(paths)};
|
return{id,seed,genome:normalized,source,parents,inheritance,mutations,breeding,paths,bounds:geometryBounds(paths)};
|
||||||
}
|
}
|
||||||
function createFounderRecord(){
|
function createFounderRecord(){
|
||||||
const seed=randomSeed(),id=state.nextId++,genome=createGenome(seed);
|
const seed=randomSeed(),id=state.nextId++,genome=createGenome(seed);
|
||||||
@@ -796,9 +838,9 @@ function createFounderRecord(){
|
|||||||
function ensureInitialPopulation(){
|
function ensureInitialPopulation(){
|
||||||
let initialized=false;
|
let initialized=false;
|
||||||
if(state.current&&!state.history.some(item=>item.id===state.current.id)){
|
if(state.current&&!state.history.some(item=>item.id===state.current.id)){
|
||||||
const{id,seed,genome,source,parents=[],inheritance=null,mutations=[]}=state.current;
|
const{id,seed,genome,source,parents=[],inheritance=null,mutations=[],breeding=null}=state.current;
|
||||||
state.history.push({
|
state.history.push({
|
||||||
id,seed,genome:normalizeGenome(genome),source,parents,inheritance,mutations,createdAt:Date.now(),
|
id,seed,genome:normalizeGenome(genome),source,parents,inheritance,mutations,breeding,createdAt:Date.now(),
|
||||||
thumbnail:renderFormulaThumbnail(genome,seed),thumbnailVersion:4
|
thumbnail:renderFormulaThumbnail(genome,seed),thumbnailVersion:4
|
||||||
});
|
});
|
||||||
initialized=true;
|
initialized=true;
|
||||||
@@ -810,9 +852,9 @@ function ensureInitialPopulation(){
|
|||||||
}
|
}
|
||||||
function addCurrentToHistory(){
|
function addCurrentToHistory(){
|
||||||
if(!state.current||state.history.some(item=>item.id===state.current.id))return;
|
if(!state.current||state.history.some(item=>item.id===state.current.id))return;
|
||||||
const{id,seed,genome,source,parents,inheritance,mutations}=state.current;
|
const{id,seed,genome,source,parents,inheritance,mutations,breeding}=state.current;
|
||||||
state.history.push({
|
state.history.push({
|
||||||
id,seed,genome:{...genome},source,parents,inheritance,mutations,createdAt:Date.now(),
|
id,seed,genome:{...genome},source,parents,inheritance,mutations,breeding,createdAt:Date.now(),
|
||||||
thumbnail:renderFormulaThumbnail(genome,seed),thumbnailVersion:4
|
thumbnail:renderFormulaThumbnail(genome,seed),thumbnailVersion:4
|
||||||
});
|
});
|
||||||
saveState();renderArchive();
|
saveState();renderArchive();
|
||||||
@@ -837,10 +879,10 @@ function selectedRecords(){return state.selectedParents.map(id=>state.history.fi
|
|||||||
function crossSelected(){
|
function crossSelected(){
|
||||||
const selected=selectedRecords();if(selected.length!==2||isBusy)return;
|
const selected=selectedRecords();if(selected.length!==2||isBusy)return;
|
||||||
const seed=randomSeed(),id=state.nextId++;
|
const seed=randomSeed(),id=state.nextId++;
|
||||||
const result=crossoverGenomes(selected[0],selected[1],seed);
|
const result=crossoverGenomes(selected[0],selected[1],seed,state.breeding);
|
||||||
showFormula({
|
showFormula({
|
||||||
id,seed,genome:result.genome,source:'crossover',parents:[selected[0].id,selected[1].id],
|
id,seed,genome:result.genome,source:'crossover',parents:[selected[0].id,selected[1].id],
|
||||||
inheritance:result.inherited,mutations:result.mutations
|
inheritance:result.inherited,mutations:result.mutations,breeding:result.breeding
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function toggleParent(id){
|
function toggleParent(id){
|
||||||
@@ -887,8 +929,11 @@ function updateFormulaUI(){
|
|||||||
const lineage=document.querySelector('#lineage');
|
const lineage=document.querySelector('#lineage');
|
||||||
if(c?.parents?.length===2){
|
if(c?.parents?.length===2){
|
||||||
const inherited=c.inheritance?`${c.inheritance.a}/${c.inheritance.b} genes`:'mixed genes';
|
const inherited=c.inheritance?`${c.inheritance.a}/${c.inheritance.b} genes`:'mixed genes';
|
||||||
|
const breeding=c.breeding
|
||||||
|
?` · ${mixName(c.breeding.mix)} · ${MUTATION_PROFILES[c.breeding.mutationLevel].name}`
|
||||||
|
:'';
|
||||||
const mutations=c.mutations?.length?` · mutation: ${c.mutations.map(key=>geneLabels[key]||key).join(', ')}`:' · no mutation';
|
const mutations=c.mutations?.length?` · mutation: ${c.mutations.map(key=>geneLabels[key]||key).join(', ')}`:' · no mutation';
|
||||||
lineage.textContent=`${formulaId(c.parents[0])} × ${formulaId(c.parents[1])} · inherited ${inherited}${mutations}`;
|
lineage.textContent=`${formulaId(c.parents[0])} × ${formulaId(c.parents[1])} · inherited ${inherited}${breeding}${mutations}`;
|
||||||
}else if(c?.source==='import')lineage.textContent='imported formula · external genome';
|
}else if(c?.source==='import')lineage.textContent='imported formula · external genome';
|
||||||
else lineage.textContent='founder formula · no parents';
|
else lineage.textContent='founder formula · no parents';
|
||||||
updateSelectionUI();
|
updateSelectionUI();
|
||||||
@@ -903,7 +948,7 @@ function updateSelectionUI(){
|
|||||||
const crossLabel=ui.cross.querySelector('span');
|
const crossLabel=ui.cross.querySelector('span');
|
||||||
if(selected.length===2){
|
if(selected.length===2){
|
||||||
crossLabel.textContent=`Cross ${formulaId(selected[0].id)} × ${formulaId(selected[1].id)}`;
|
crossLabel.textContent=`Cross ${formulaId(selected[0].id)} × ${formulaId(selected[1].id)}`;
|
||||||
document.querySelector('#action-kicker').textContent='gene-by-gene crossover · rare mutation';
|
document.querySelector('#action-kicker').textContent=`${mixName(state.breeding.mix)} · ${MUTATION_PROFILES[state.breeding.mutationLevel].name} mutation`;
|
||||||
document.querySelector('#action-title').textContent=`${familyName(selected[0].genome.family)} × ${familyName(selected[1].genome.family)}`;
|
document.querySelector('#action-title').textContent=`${familyName(selected[0].genome.family)} × ${familyName(selected[1].genome.family)}`;
|
||||||
}else if(selected.length===1){
|
}else if(selected.length===1){
|
||||||
crossLabel.textContent='Choose parent B';
|
crossLabel.textContent='Choose parent B';
|
||||||
@@ -915,6 +960,10 @@ function updateSelectionUI(){
|
|||||||
document.querySelector('#action-title').textContent='Choose two parents from the Gallery';
|
document.querySelector('#action-title').textContent='Choose two parents from the Gallery';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function syncBreedingUI(){
|
||||||
|
document.querySelector('#gene-mix').value=String(state.breeding.mix);
|
||||||
|
document.querySelector('#mutation-level').value=String(state.breeding.mutationLevel);
|
||||||
|
}
|
||||||
function hideArchivePreview(){
|
function hideArchivePreview(){
|
||||||
previewToken++;cancelAnimationFrame(previewAnimationFrame);
|
previewToken++;cancelAnimationFrame(previewAnimationFrame);
|
||||||
document.querySelector('#genome-preview').classList.add('hidden');
|
document.querySelector('#genome-preview').classList.add('hidden');
|
||||||
@@ -1027,6 +1076,14 @@ document.querySelector('#formula-file').addEventListener('change',async event=>{
|
|||||||
catch(error){console.warn('Could not import the formula',error);showToast(error instanceof SyntaxError?'Invalid JSON file':error.message||'Formula import failed')}
|
catch(error){console.warn('Could not import the formula',error);showToast(error instanceof SyntaxError?'Invalid JSON file':error.message||'Formula import failed')}
|
||||||
finally{input.value=''}
|
finally{input.value=''}
|
||||||
});
|
});
|
||||||
|
document.querySelector('#gene-mix').addEventListener('change',event=>{
|
||||||
|
state.breeding=normalizeBreeding({...state.breeding,mix:Number(event.currentTarget.value)});
|
||||||
|
saveState();updateSelectionUI();
|
||||||
|
});
|
||||||
|
document.querySelector('#mutation-level').addEventListener('change',event=>{
|
||||||
|
state.breeding=normalizeBreeding({...state.breeding,mutationLevel:Number(event.currentTarget.value)});
|
||||||
|
saveState();updateSelectionUI();
|
||||||
|
});
|
||||||
document.addEventListener('click',closeExportMenu);
|
document.addEventListener('click',closeExportMenu);
|
||||||
document.addEventListener('pointerdown',requestPersistentStorage,{once:true,capture:true});
|
document.addEventListener('pointerdown',requestPersistentStorage,{once:true,capture:true});
|
||||||
document.addEventListener('keydown',event=>{
|
document.addEventListener('keydown',event=>{
|
||||||
@@ -1042,6 +1099,7 @@ addEventListener('beforeunload',saveState);
|
|||||||
|
|
||||||
async function boot(){
|
async function boot(){
|
||||||
await hydrateState();
|
await hydrateState();
|
||||||
|
syncBreedingUI();
|
||||||
await refreshStorageStatus();
|
await refreshStorageStatus();
|
||||||
ensureInitialPopulation();
|
ensureInitialPopulation();
|
||||||
galleryOpen=innerWidth>=1180;
|
galleryOpen=innerWidth>=1180;
|
||||||
|
|||||||
+23
-2
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#07100f" />
|
<meta name="theme-color" content="#07100f" />
|
||||||
<title>Fractalier — Mathematical Fractal Generator</title>
|
<title>Fractalier — Mathematical Fractal Generator</title>
|
||||||
<link rel="stylesheet" href="style.css?v=38" />
|
<link rel="stylesheet" href="style.css?v=39" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
@@ -97,6 +97,27 @@
|
|||||||
<span id="action-kicker">selective generation · choose A and B</span>
|
<span id="action-kicker">selective generation · choose A and B</span>
|
||||||
<strong id="action-title">Choose two parents from the Gallery</strong>
|
<strong id="action-title">Choose two parents from the Gallery</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="breeding-controls" aria-label="Crossover settings">
|
||||||
|
<label>
|
||||||
|
<span>inheritance</span>
|
||||||
|
<select id="gene-mix" aria-label="Genome inheritance proportion">
|
||||||
|
<option value="0.2">20% A · 80% B</option>
|
||||||
|
<option value="0.35">35% A · 65% B</option>
|
||||||
|
<option value="0.5" selected>50% A · 50% B</option>
|
||||||
|
<option value="0.65">65% A · 35% B</option>
|
||||||
|
<option value="0.8">80% A · 20% B</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>mutation</span>
|
||||||
|
<select id="mutation-level" aria-label="Mutation intensity">
|
||||||
|
<option value="0">Stable</option>
|
||||||
|
<option value="1" selected>Natural</option>
|
||||||
|
<option value="2">Active</option>
|
||||||
|
<option value="3">Volatile</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="primary" id="cross" disabled>
|
<button class="primary" id="cross" disabled>
|
||||||
<span>Cross A × B</span>
|
<span>Cross A × B</span>
|
||||||
@@ -113,6 +134,6 @@
|
|||||||
<span id="storage-status">checking local storage…</span>
|
<span id="storage-status">checking local storage…</span>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
<script src="app.js?v=38"></script>
|
<script src="app.js?v=39"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -97,6 +97,14 @@ nav .export-menu button{display:block;width:100%;border:0;text-align:left}
|
|||||||
.action-copy{min-width:0;padding-left:5px}
|
.action-copy{min-width:0;padding-left:5px}
|
||||||
.action-copy span{display:block;overflow:hidden;margin-bottom:4px;color:var(--muted);font:7px DM Mono,monospace;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase}
|
.action-copy span{display:block;overflow:hidden;margin-bottom:4px;color:var(--muted);font:7px DM Mono,monospace;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase}
|
||||||
.action-copy strong{display:block;overflow:hidden;font-size:10px;font-weight:500;text-overflow:ellipsis;white-space:nowrap}
|
.action-copy strong{display:block;overflow:hidden;font-size:10px;font-weight:500;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.breeding-controls{display:flex;gap:6px;flex:0 0 auto}
|
||||||
|
.breeding-controls label{display:block}
|
||||||
|
.breeding-controls label>span{display:block;margin:0 0 4px;color:var(--muted);font:7px DM Mono,monospace;text-transform:uppercase}
|
||||||
|
.breeding-controls select{
|
||||||
|
width:96px;height:29px;padding:0 22px 0 7px;border:1px solid var(--line);border-radius:0;
|
||||||
|
background:#0b1b17;color:#b6c6bc;cursor:pointer;font:8px DM Mono,monospace
|
||||||
|
}
|
||||||
|
.breeding-controls select:hover{border-color:var(--line-bright);color:var(--lime)}
|
||||||
.action-buttons{display:flex;gap:7px;flex:0 0 auto}
|
.action-buttons{display:flex;gap:7px;flex:0 0 auto}
|
||||||
.action-buttons button{height:48px;border:0;cursor:pointer}
|
.action-buttons button{height:48px;border:0;cursor:pointer}
|
||||||
.action-buttons button:disabled{cursor:wait;opacity:.58}
|
.action-buttons button:disabled{cursor:wait;opacity:.58}
|
||||||
@@ -133,15 +141,18 @@ footer span:last-child{text-align:right}
|
|||||||
.equation{margin-top:9px;padding:8px 0;font-size:7.5px}
|
.equation{margin-top:9px;padding:8px 0;font-size:7.5px}
|
||||||
.equation span{overflow:hidden;text-overflow:ellipsis}
|
.equation span{overflow:hidden;text-overflow:ellipsis}
|
||||||
.traits{gap:6px 8px;margin-top:8px}
|
.traits{gap:6px 8px;margin-top:8px}
|
||||||
.gallery-panel,.gallery-panel.open{top:72px;right:10px;bottom:112px;left:10px;width:auto}
|
.gallery-panel,.gallery-panel.open{top:72px;right:10px;bottom:158px;left:10px;width:auto}
|
||||||
.archive{grid-template-columns:repeat(3,1fr)}
|
.archive{grid-template-columns:repeat(3,1fr)}
|
||||||
.genome-preview{display:none!important}
|
.genome-preview{display:none!important}
|
||||||
.action-bar{right:10px;bottom:40px;left:10px;width:auto;padding:8px;transform:none}
|
.action-bar{right:10px;bottom:40px;left:10px;width:auto;flex-direction:column;gap:7px;padding:8px;transform:none}
|
||||||
.action-copy{display:none}
|
.action-copy{display:none}
|
||||||
|
.breeding-controls{width:100%}
|
||||||
|
.breeding-controls label{min-width:0;flex:1}
|
||||||
|
.breeding-controls select{width:100%}
|
||||||
.action-buttons{width:100%}
|
.action-buttons{width:100%}
|
||||||
.action-buttons button{height:46px}
|
.action-buttons button{height:46px}
|
||||||
.action-buttons .secondary,.action-buttons .primary{min-width:0;flex:1}
|
.action-buttons .secondary,.action-buttons .primary{min-width:0;flex:1}
|
||||||
.toast{bottom:101px}
|
.toast{bottom:154px}
|
||||||
footer{grid-template-columns:1fr auto;padding:0 10px}
|
footer{grid-template-columns:1fr auto;padding:0 10px}
|
||||||
footer>span:nth-child(2){display:none}
|
footer>span:nth-child(2){display:none}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user