Add formula JSON import
This commit is contained in:
@@ -125,11 +125,13 @@ async function hydrateState(){
|
|||||||
const rawHistory=records.length?records:(source?.history||source?.population||[]);
|
const rawHistory=records.length?records:(source?.history||source?.population||[]);
|
||||||
const history=rawHistory.map(normalizeRecord).sort((a,b)=>a.id-b.id);
|
const history=rawHistory.map(normalizeRecord).sort((a,b)=>a.id-b.id);
|
||||||
const oldCurrent=source?.current;
|
const oldCurrent=source?.current;
|
||||||
|
const currentHistoryRecord=oldCurrent?history.find(item=>item.id===Number(oldCurrent.id)):null;
|
||||||
const current=oldCurrent?.genome?{
|
const current=oldCurrent?.genome?{
|
||||||
id:Number(oldCurrent.id)||history.at(-1)?.id||1,
|
id:Number(oldCurrent.id)||history.at(-1)?.id||1,
|
||||||
seed:Number(oldCurrent.seed)||randomSeed(),
|
seed:Number(oldCurrent.seed)||randomSeed(),
|
||||||
genome:normalizeGenome(oldCurrent.genome),
|
genome:normalizeGenome(oldCurrent.genome),
|
||||||
source:'restored',parents:oldCurrent.parents||[],inheritance:oldCurrent.inheritance||null,mutations:oldCurrent.mutations||[]
|
source:oldCurrent.source&&oldCurrent.source!=='restored'?oldCurrent.source:currentHistoryRecord?.source||'restored',
|
||||||
|
parents:oldCurrent.parents||[],inheritance:oldCurrent.inheritance||null,mutations:oldCurrent.mutations||[]
|
||||||
}: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);
|
||||||
@@ -300,7 +302,7 @@ const mutationRules={
|
|||||||
figureSides:[1,0,10,true],figureSpan:[.18,.12,1],figureScale:[.12,.08,.55],
|
figureSides:[1,0,10,true],figureSpan:[.18,.12,1],figureScale:[.12,.08,.55],
|
||||||
figureEvery:[1,1,4,true],figureSpin:[.3,-1.2,1.2],
|
figureEvery:[1,1,4,true],figureSpin:[.3,-1.2,1.2],
|
||||||
curveMode:[1,0,4,true],curveAmplitude:[.14,0,.6],curveFrequency:[.5,.35,3.5],
|
curveMode:[1,0,4,true],curveAmplitude:[.14,0,.6],curveFrequency:[.5,.35,3.5],
|
||||||
rootLayout:[1,0,3,true],rootSpread:[.45,0,2.8],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 crossoverGenomes(parentA,parentB,seed){
|
||||||
@@ -631,6 +633,44 @@ function exportFormula(){
|
|||||||
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');
|
||||||
}
|
}
|
||||||
|
function sanitizeImportedGenome(payload){
|
||||||
|
if(!payload||typeof payload!=='object'||Array.isArray(payload))throw new Error('The JSON root must be an object');
|
||||||
|
if(payload.format&&payload.format!=='fractalier-formula')throw new Error('This is not a Fractalier formula');
|
||||||
|
const candidate=payload.genome||payload;
|
||||||
|
if(!candidate||typeof candidate!=='object'||Array.isArray(candidate))throw new Error('The file does not contain a genome');
|
||||||
|
const defaults=baseGenome(),genome={...defaults},keys=Object.keys(defaults).filter(key=>key!=='family');
|
||||||
|
let recognized=0;
|
||||||
|
for(const key of keys){
|
||||||
|
if(typeof candidate[key]!=='number'||!Number.isFinite(candidate[key]))continue;
|
||||||
|
recognized++;
|
||||||
|
let value=candidate[key],rule=mutationRules[key];
|
||||||
|
if(rule){
|
||||||
|
const[,min,max,integer]=rule;
|
||||||
|
if(integer)value=Math.round(value);
|
||||||
|
value=clamp(value,min,max);
|
||||||
|
}
|
||||||
|
if((key==='tipSides'||key==='figureSides')&&value>0)value=clamp(Math.round(value),3,key==='tipSides'?8:10);
|
||||||
|
genome[key]=value;
|
||||||
|
}
|
||||||
|
if(recognized<5)throw new Error('No recognizable Fractalier genome was found');
|
||||||
|
genome.family=classifyFamily({...genome,family:null});
|
||||||
|
return genome;
|
||||||
|
}
|
||||||
|
function importFormulaPayload(payload){
|
||||||
|
const genome=sanitizeImportedGenome(payload);
|
||||||
|
const importedSeed=Number(payload?.seed);
|
||||||
|
const seed=Number.isFinite(importedSeed)?Math.abs(Math.trunc(importedSeed))>>>0:randomSeed();
|
||||||
|
const id=state.nextId++;
|
||||||
|
showFormula({id,seed,genome,source:'import',parents:[],inheritance:null,mutations:[]});
|
||||||
|
showToast(`Imported as ${formulaId(id)}`);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
async function importFormulaFile(file){
|
||||||
|
if(!file)return;
|
||||||
|
if(file.size>256*1024)throw new Error('The formula file is too large');
|
||||||
|
const payload=JSON.parse(await file.text());
|
||||||
|
return importFormulaPayload(payload);
|
||||||
|
}
|
||||||
async function exportVideo(){
|
async function exportVideo(){
|
||||||
if(!state.current||isBusy)return;
|
if(!state.current||isBusy)return;
|
||||||
closeExportMenu();
|
closeExportMenu();
|
||||||
@@ -849,7 +889,8 @@ function updateFormulaUI(){
|
|||||||
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 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}${mutations}`;
|
||||||
}else lineage.textContent='founder formula · no parents';
|
}else if(c?.source==='import')lineage.textContent='imported formula · external genome';
|
||||||
|
else lineage.textContent='founder formula · no parents';
|
||||||
updateSelectionUI();
|
updateSelectionUI();
|
||||||
}
|
}
|
||||||
function updateSelectionUI(){
|
function updateSelectionUI(){
|
||||||
@@ -888,7 +929,7 @@ function showArchivePreview(item,card){
|
|||||||
document.querySelector('#preview-id').textContent=formulaId(item.id);
|
document.querySelector('#preview-id').textContent=formulaId(item.id);
|
||||||
document.querySelector('#preview-meta').textContent=item.parents?.length===2
|
document.querySelector('#preview-meta').textContent=item.parents?.length===2
|
||||||
?`${formulaId(item.parents[0])} × ${formulaId(item.parents[1])} · ${familyName(item.genome.family)}`
|
?`${formulaId(item.parents[0])} × ${formulaId(item.parents[1])} · ${familyName(item.genome.family)}`
|
||||||
:`${familyName(item.genome.family)} · founder`;
|
:`${familyName(item.genome.family)} · ${item.source==='import'?'imported':'founder'}`;
|
||||||
preview.classList.remove('hidden');
|
preview.classList.remove('hidden');
|
||||||
const panel=ui.gallery.getBoundingClientRect(),cardBox=card.getBoundingClientRect(),previewWidth=preview.offsetWidth;
|
const panel=ui.gallery.getBoundingClientRect(),cardBox=card.getBoundingClientRect(),previewWidth=preview.offsetWidth;
|
||||||
const left=panel.left-previewWidth-12>=12?panel.left-previewWidth-12:Math.min(innerWidth-previewWidth-12,panel.right+12);
|
const left=panel.left-previewWidth-12>=12?panel.left-previewWidth-12:Math.min(innerWidth-previewWidth-12,panel.right+12);
|
||||||
@@ -977,6 +1018,15 @@ document.querySelector('#save-background').addEventListener('click',()=>download
|
|||||||
document.querySelector('#save-transparent').addEventListener('click',()=>downloadImage(false));
|
document.querySelector('#save-transparent').addEventListener('click',()=>downloadImage(false));
|
||||||
document.querySelector('#save-video').addEventListener('click',exportVideo);
|
document.querySelector('#save-video').addEventListener('click',exportVideo);
|
||||||
document.querySelector('#save-formula').addEventListener('click',exportFormula);
|
document.querySelector('#save-formula').addEventListener('click',exportFormula);
|
||||||
|
document.querySelector('#import-formula').addEventListener('click',()=>{
|
||||||
|
closeExportMenu();document.querySelector('#formula-file').click();
|
||||||
|
});
|
||||||
|
document.querySelector('#formula-file').addEventListener('change',async event=>{
|
||||||
|
const input=event.currentTarget,file=input.files?.[0];
|
||||||
|
try{await importFormulaFile(file)}
|
||||||
|
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=''}
|
||||||
|
});
|
||||||
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=>{
|
||||||
|
|||||||
+4
-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=35" />
|
<link rel="stylesheet" href="style.css?v=38" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
@@ -28,6 +28,8 @@
|
|||||||
<button id="save-transparent">PNG · transparent</button>
|
<button id="save-transparent">PNG · transparent</button>
|
||||||
<button id="save-video">WebM · growth video</button>
|
<button id="save-video">WebM · growth video</button>
|
||||||
<button id="save-formula">JSON · formula</button>
|
<button id="save-formula">JSON · formula</button>
|
||||||
|
<button id="import-formula">Import · formula JSON</button>
|
||||||
|
<input id="formula-file" type="file" accept=".json,application/json" hidden />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -111,6 +113,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=35"></script>
|
<script src="app.js?v=38"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user