clone

a photo toolkit for your phone that never uploads: fuji film looks for raw files, social crops, error level analysis

Size canvas box to the picture so the line overlay aligns for any aspect

Dami committed cfc3fd2
commit
cfc3fd2e51a7e27b5b5016512d369afddcab42af
parent
175cac3adadb9beff9160feac2a6988b865667c0
author
Dami <dami.etoile@gmail.com>
3 files changed, +22 -7
Mapp.mjs11+++++++++--
Mfuji.mjs12++++++++++--
Mstyle.css6+++---
M app.mjs +9 -2
@@ -29,10 +29,17 @@ function syncControls(){
2929 function setBusy(value) {busy=value;$('busy').hidden=!value;syncControls();}3030 function currentParams(){const s=Number($('strength').value)/100;return [Number($('rotation').value),base[1]*s,base[2]*s,base[3]*s];}3131 function updateLabels(){const r=Number($('rotation').value);$('rotation-value').value=`${r>0?'+':''}${r.toFixed(2)}°`;$('strength-value').value=`${$('strength').value}%`;}32+// Size the canvas box to the picture inside the stage, so the overlay covers exactly the picture.33+function layout(){34+ if(!canvas.width)return;35+ const cs=getComputedStyle(stage),w=stage.clientWidth-parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight),h=stage.clientHeight-parseFloat(cs.paddingTop)-parseFloat(cs.paddingBottom);36+ const s=Math.min(w/canvas.width,h/canvas.height),cw=Math.max(1,Math.floor(canvas.width*s)),ch=Math.max(1,Math.floor(canvas.height*s));37+ canvas.style.width=overlay.style.width=cw+'px';canvas.style.height=overlay.style.height=ch+'px';38+}3239 function draw(){3340 const before=showBefore||holdBefore,source=before||!rendered?original:rendered;3441 if(!source)return;35- canvas.width=source.width;canvas.height=source.height;ctx.drawImage(source,0,0);42+ canvas.width=source.width;canvas.height=source.height;ctx.drawImage(source,0,0);layout();3643 $('compare').setAttribute('aria-pressed',String(before&&Boolean(rendered)));3744 drawOverlay();3845 }
@@ -106,6 +113,7 @@ $('reset').onclick=()=>{clearTimeout(renderTimer);renderId++;resetValues();cropM
106113 107114 // Press and hold the photo to peek at the original.108115 const stage=$('stage');116+new ResizeObserver(()=>{layout();drawOverlay();}).observe(stage);109117 stage.addEventListener('pointerdown',e=>{if(!rendered||busy||e.target.closest('button'))return;holdBefore=true;draw();});110118 for(const type of ['pointerup','pointercancel','pointerleave'])stage.addEventListener(type,()=>{if(!holdBefore)return;holdBefore=false;draw();});111119 stage.addEventListener('contextmenu',e=>e.preventDefault());
@@ -152,7 +160,6 @@ async function save(format){
152160 status(`Saved ${c.width} x ${c.height}`);153161 }catch(e){status(e.message,true);}finally{setBusy(false);}154162 }155-new ResizeObserver(drawOverlay).observe(canvas);156163 if('serviceWorker'in navigator){navigator.serviceWorker.register('./sw.js').catch(()=>{});let hadController=Boolean(navigator.serviceWorker.controller);navigator.serviceWorker.addEventListener('controllerchange',()=>{if(hadController&&!original)location.reload();hadController=true;});}157164 syncCrop();158165 try{await rpc('init');}catch(e){status(e.message,true);}finally{setBusy(false);}
M fuji.mjs +10 -2
@@ -23,7 +23,15 @@ const NEUTRAL_URLS={100:new URL('./assets/luts/neutral-dr100.png',import.meta.ur
2323 const FROM_CAMERA={'Provia':'provia','Velvia':'velvia','Astia':'astia','Classic Chrome':'classic_chrome','Pro Neg. Hi':'pro_neg_high','Pro Neg. Std':'pro_neg_std','Eterna':'eterna',2424 'Acros':'acros','Acros+R':'acros-red','Acros+Ye':'acros-yellow','Acros+G':'acros-green','Monochrome':'mono','Monochrome+R':'mono-red','Monochrome+Ye':'mono-yellow','Monochrome+G':'mono-green','Sepia':'sepia'};2525 26-const tool=$('tool'),canvas=$('canvas'),ctx=canvas.getContext('2d'),photo=$('photo');26+const tool=$('tool'),canvas=$('canvas'),ctx=canvas.getContext('2d'),photo=$('photo'),stageEl=$('stage');27+// Size the picture box to fit the stage, same for the developed canvas and the camera JPEG.28+function layout(){29+ const cs=getComputedStyle(stageEl),w=stageEl.clientWidth-parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight),h=stageEl.clientHeight-parseFloat(cs.paddingTop)-parseFloat(cs.paddingBottom);30+ for(const [el,iw,ih] of [[canvas,canvas.width,canvas.height],[photo,photo.naturalWidth,photo.naturalHeight]]){31+ if(!iw||!ih)continue;const s=Math.min(w/iw,h/ih);el.style.width=Math.max(1,Math.floor(iw*s))+'px';el.style.height=Math.max(1,Math.floor(ih*s))+'px';32+ }33+}34+new ResizeObserver(layout).observe(stageEl);photo.addEventListener('load',layout);2735 let current=null,busy=false,toastTimer,sim='provia',dr=100,showCamera=false,holdCamera=false,renderId=0,cameraUrl=null;2836 const loaded=new Set(),loading=new Map();2937
@@ -54,7 +62,7 @@ async function render(){
5462 await Promise.all([ensureLut('neutral',dr,NEUTRAL_URLS[dr]||NEUTRAL_URLS[100]),ensureLut('sim',sim,LUT_URLS[sim])]);5563 const r=await rpc('render',{dr:NEUTRAL_URLS[dr]?dr:100,sim});5664 if(id!==renderId)return;57- canvas.width=r.w;canvas.height=r.h;ctx.putImageData(new ImageData(r.data,r.w,r.h),0,0);65+ canvas.width=r.w;canvas.height=r.h;ctx.putImageData(new ImageData(r.data,r.w,r.h),0,0);layout();5866 }catch(e){status(e.message,true);}5967 }6068 async function openFile(file){
M style.css +3 -3