@@ -26,11 +26,11 @@ const minRemainingCropPercent = 10;
2626const autoDetectCropPadding = 0.012 ;
2727
2828const presetSettings = {
29- clean : { brightness : 16 , contrast : 114 , grain : 1 , vignette : 2 , threshold : 188 , shadowBoost : 0.14 , binaryMix : 0.58 , lineNoise : 0.014 , tonerNoise : 0.006 , scannerAge : 8 } ,
30- classic : { brightness : 8 , contrast : 132 , grain : 6 , vignette : 5 , threshold : 166 , shadowBoost : 0.28 , binaryMix : 0.74 , lineNoise : 0.028 , tonerNoise : 0.012 , scannerAge : 34 } ,
31- "high-contrast" : { brightness : 3 , contrast : 152 , grain : 3 , vignette : 1 , threshold : 150 , shadowBoost : 0.4 , binaryMix : 0.88 , lineNoise : 0.018 , tonerNoise : 0.009 , scannerAge : 18 } ,
32- thermal : { brightness : 6 , contrast : 162 , grain : 4 , vignette : 0 , threshold : 154 , shadowBoost : 0.36 , binaryMix : 0.92 , lineNoise : 0.02 , tonerNoise : 0.014 , processingScale : 0.76 , pureMono : true , streakStrength : 0.02 , dropoutChance : 0.0011 , scannerAge : 42 , dotMatrixColumns : true } ,
33- fax : { brightness : 0 , contrast : 172 , grain : 7 , vignette : 1 , threshold : 142 , shadowBoost : 0.46 , binaryMix : 1 , lineNoise : 0.045 , tonerNoise : 0.022 , processingScale : 0.42 , pureMono : true , streakStrength : 0.055 , dropoutChance : 0.0024 , scannerAge : 72 } ,
29+ clean : { brightness : 16 , contrast : 114 , grain : 1 , vignette : 2 , threshold : 188 , shadowBoost : 0.14 , binaryMix : 0.58 , lineNoise : 0.014 , tonerNoise : 0.006 , scannerAge : 8 , thermalFade : 0 , ocrFirstRecommended : false } ,
30+ classic : { brightness : 8 , contrast : 132 , grain : 6 , vignette : 5 , threshold : 166 , shadowBoost : 0.28 , binaryMix : 0.74 , lineNoise : 0.028 , tonerNoise : 0.012 , scannerAge : 34 , thermalFade : 0 , ocrFirstRecommended : false } ,
31+ "high-contrast" : { brightness : 3 , contrast : 152 , grain : 3 , vignette : 1 , threshold : 150 , shadowBoost : 0.4 , binaryMix : 0.88 , lineNoise : 0.018 , tonerNoise : 0.009 , scannerAge : 18 , thermalFade : 0 , ocrFirstRecommended : true } ,
32+ thermal : { brightness : 6 , contrast : 162 , grain : 4 , vignette : 0 , threshold : 154 , shadowBoost : 0.36 , binaryMix : 0.92 , lineNoise : 0.02 , tonerNoise : 0.014 , processingScale : 0.76 , pureMono : true , streakStrength : 0.02 , dropoutChance : 0.0011 , scannerAge : 42 , thermalFade : 28 , dotMatrixColumns : true , ocrFirstRecommended : false } ,
33+ fax : { brightness : 0 , contrast : 172 , grain : 7 , vignette : 1 , threshold : 142 , shadowBoost : 0.46 , binaryMix : 1 , lineNoise : 0.045 , tonerNoise : 0.022 , processingScale : 0.42 , pureMono : true , streakStrength : 0.055 , dropoutChance : 0.0024 , scannerAge : 72 , thermalFade : 0 , ocrFirstRecommended : false } ,
3434} ;
3535
3636const presetDescriptions = {
@@ -130,11 +130,15 @@ const elements = {
130130 scanLookToggle : document . querySelector ( "#scan-look-toggle" ) ,
131131 preset : document . querySelector ( "#scan-preset" ) ,
132132 presetDescription : document . querySelector ( "#preset-description" ) ,
133+ presetRecommendation : document . querySelector ( "#preset-recommendation" ) ,
134+ presetComparisonNote : document . querySelector ( "#preset-comparison-note" ) ,
135+ presetComparisonGrid : document . querySelector ( "#preset-comparison-grid" ) ,
133136 brightness : document . querySelector ( "#brightness" ) ,
134137 contrast : document . querySelector ( "#contrast" ) ,
135138 grain : document . querySelector ( "#grain" ) ,
136139 vignette : document . querySelector ( "#vignette" ) ,
137140 scannerAge : document . querySelector ( "#scanner-age" ) ,
141+ thermalFade : document . querySelector ( "#thermal-fade" ) ,
138142 ocrFirstMode : document . querySelector ( "#ocr-first-mode" ) ,
139143 controlsGrid : document . querySelector ( ".controls-grid" ) ,
140144} ;
@@ -170,6 +174,7 @@ function getControls() {
170174 grain : Number ( elements . grain . value ) ,
171175 vignette : Number ( elements . vignette . value ) ,
172176 scannerAge : Number ( elements . scannerAge . value ) ,
177+ thermalFade : Number ( elements . thermalFade . value ) ,
173178 ocrFirstMode : elements . ocrFirstMode . checked ,
174179 pageMargin : Number ( elements . pageMargin . value ) ,
175180 imageFit : elements . imageFit . value ,
@@ -199,11 +204,44 @@ function getScanControls(controls = getControls()) {
199204 grain : controls . grain ,
200205 vignette : controls . vignette ,
201206 scannerAge : controls . scannerAge ,
207+ thermalFade : controls . thermalFade ,
202208 ocrFirstMode : controls . ocrFirstMode ,
203209 autoCleanup : controls . autoCleanup ,
204210 } ;
205211}
206212
213+ function getPresetLabel ( presetKey ) {
214+ const option = elements . preset . querySelector ( `[value="${ presetKey } "]` ) ;
215+ return option ? option . textContent : presetKey ;
216+ }
217+
218+ function getPresetRecommendationText ( presetKey ) {
219+ const preset = presetSettings [ presetKey ] ;
220+ if ( ! preset ) {
221+ return "" ;
222+ }
223+
224+ const parts = [
225+ `Brightness ${ preset . brightness >= 0 ? "+" : "" } ${ preset . brightness } ` ,
226+ `Contrast ${ preset . contrast } ` ,
227+ `Grain ${ preset . grain } ` ,
228+ `Vignette ${ preset . vignette } ` ,
229+ `Scanner age ${ preset . scannerAge ?? 0 } ` ,
230+ ] ;
231+
232+ if ( ( preset . thermalFade ?? 0 ) > 0 ) {
233+ parts . push ( `Thermal fade ${ preset . thermalFade } ` ) ;
234+ }
235+
236+ parts . push ( `OCR-first ${ preset . ocrFirstRecommended ? "on" : "off" } ` ) ;
237+ return `Recommended defaults: ${ parts . join ( ", " ) } .` ;
238+ }
239+
240+ function updatePresetSpecificControlState ( ) {
241+ const thermalEnabled = elements . scanLookToggle . checked && elements . preset . value === "thermal" ;
242+ elements . thermalFade . disabled = ! thermalEnabled ;
243+ }
244+
207245function getEffectiveControls ( entry , controls = getControls ( ) ) {
208246 if ( ! entry ?. scanSettings ) {
209247 return controls ;
@@ -1141,9 +1179,10 @@ function buildRenderCanvas(sourceImage, controls, options = {}, adjustments = cr
11411179function updateScanControlState ( ) {
11421180 const useScanLook = elements . scanLookToggle . checked ;
11431181 elements . controlsGrid . classList . toggle ( "disabled" , ! useScanLook ) ;
1144- [ elements . preset , elements . brightness , elements . contrast , elements . grain , elements . vignette , elements . scannerAge , elements . ocrFirstMode ] . forEach ( ( input ) => {
1182+ [ elements . preset , elements . brightness , elements . contrast , elements . grain , elements . vignette , elements . scannerAge , elements . thermalFade , elements . ocrFirstMode ] . forEach ( ( input ) => {
11451183 input . disabled = ! useScanLook ;
11461184 } ) ;
1185+ updatePresetSpecificControlState ( ) ;
11471186}
11481187
11491188function syncPresetToControls ( ) {
@@ -1153,11 +1192,15 @@ function syncPresetToControls() {
11531192 elements . grain . value = preset . grain ;
11541193 elements . vignette . value = preset . vignette ;
11551194 elements . scannerAge . value = preset . scannerAge ?? 20 ;
1195+ elements . thermalFade . value = preset . thermalFade ?? 0 ;
11561196 updatePresetDescription ( ) ;
11571197}
11581198
11591199function updatePresetDescription ( ) {
11601200 elements . presetDescription . textContent = presetDescriptions [ elements . preset . value ] ;
1201+ elements . presetRecommendation . textContent = getPresetRecommendationText ( elements . preset . value ) ;
1202+ updatePresetSpecificControlState ( ) ;
1203+ renderPresetComparison ( ) ;
11611204}
11621205
11631206function canvasToBlob ( canvas , mimeType , quality ) {
@@ -1314,9 +1357,99 @@ function applyDotMatrixColumns(data, width, height, ageFactor) {
13141357 }
13151358}
13161359
1360+ function getPresetComparisonEntry ( ) {
1361+ if ( state . activeAdjustId ) {
1362+ return state . files . find ( ( file ) => file . id === state . activeAdjustId ) || null ;
1363+ }
1364+
1365+ return getVisibleEntries ( ) [ 0 ] || state . files [ 0 ] || null ;
1366+ }
1367+
1368+ function getPresetControlsForComparison ( baseControls , presetKey ) {
1369+ const preset = presetSettings [ presetKey ] ;
1370+ return {
1371+ ...baseControls ,
1372+ preset : presetKey ,
1373+ brightness : preset . brightness ,
1374+ contrast : preset . contrast ,
1375+ grain : preset . grain ,
1376+ vignette : preset . vignette ,
1377+ scannerAge : preset . scannerAge ?? 0 ,
1378+ thermalFade : preset . thermalFade ?? 0 ,
1379+ } ;
1380+ }
1381+
1382+ function renderPresetComparison ( ) {
1383+ if ( ! elements . presetComparisonGrid || ! elements . presetComparisonNote ) {
1384+ return ;
1385+ }
1386+
1387+ elements . presetComparisonGrid . innerHTML = "" ;
1388+
1389+ if ( ! elements . scanLookToggle . checked ) {
1390+ elements . presetComparisonNote . textContent = "Enable black-and-white scan to compare presets live." ;
1391+ return ;
1392+ }
1393+
1394+ const sourceEntry = getPresetComparisonEntry ( ) ;
1395+ if ( ! sourceEntry ) {
1396+ elements . presetComparisonNote . textContent = "Upload a page to compare presets side by side." ;
1397+ return ;
1398+ }
1399+
1400+ elements . presetComparisonNote . textContent = `Comparing presets on ${ sourceEntry . name } .` ;
1401+ const baseControls = getControls ( ) ;
1402+
1403+ Object . keys ( presetSettings ) . forEach ( ( presetKey ) => {
1404+ const card = document . createElement ( "article" ) ;
1405+ card . className = "preset-comparison-card" ;
1406+ if ( presetKey === baseControls . preset ) {
1407+ card . classList . add ( "is-active" ) ;
1408+ }
1409+
1410+ const title = document . createElement ( "h3" ) ;
1411+ title . className = "preset-comparison-title" ;
1412+ title . textContent = getPresetLabel ( presetKey ) ;
1413+
1414+ const frame = document . createElement ( "div" ) ;
1415+ frame . className = "preset-comparison-frame" ;
1416+ const canvas = document . createElement ( "canvas" ) ;
1417+
1418+ try {
1419+ const rendered = buildRenderCanvas (
1420+ sourceEntry . sourceImage ,
1421+ getPresetControlsForComparison ( baseControls , presetKey ) ,
1422+ { maxDimension : 240 } ,
1423+ getEntryAdjustments ( sourceEntry )
1424+ ) ;
1425+ canvas . width = rendered . width ;
1426+ canvas . height = rendered . height ;
1427+ const context = canvas . getContext ( "2d" ) ;
1428+ context . imageSmoothingEnabled = true ;
1429+ context . imageSmoothingQuality = "medium" ;
1430+ context . drawImage ( rendered , 0 , 0 ) ;
1431+ } catch {
1432+ canvas . width = 1 ;
1433+ canvas . height = 1 ;
1434+ }
1435+
1436+ frame . appendChild ( canvas ) ;
1437+
1438+ const description = document . createElement ( "p" ) ;
1439+ description . className = "preset-comparison-description" ;
1440+ description . textContent = presetDescriptions [ presetKey ] ;
1441+
1442+ card . appendChild ( title ) ;
1443+ card . appendChild ( frame ) ;
1444+ card . appendChild ( description ) ;
1445+ elements . presetComparisonGrid . appendChild ( card ) ;
1446+ } ) ;
1447+ }
1448+
13171449function applyScanEffect ( sourceImage , controls , dimensions = { width : sourceImage . width , height : sourceImage . height } ) {
13181450 const preset = presetSettings [ controls . preset ] ;
13191451 const ageFactor = Math . max ( 0 , Math . min ( 1 , ( controls . scannerAge ?? preset . scannerAge ?? 0 ) / 100 ) ) ;
1452+ const thermalFade = preset . dotMatrixColumns ? Math . max ( 0 , Math . min ( 1 , ( controls . thermalFade ?? preset . thermalFade ?? 0 ) / 100 ) ) : 0 ;
13201453 const ocrFirstMode = Boolean ( controls . ocrFirstMode ) ;
13211454 const processingScale = Math . max ( 0.35 , ( preset . processingScale ?? 1 ) - ( ( preset . pureMono ? 0.12 : 0.04 ) * ageFactor ) ) ;
13221455 const processingWidth = Math . max ( 1 , Math . round ( dimensions . width * processingScale ) ) ;
@@ -1382,6 +1515,15 @@ function applyScanEffect(sourceImage, controls, dimensions = { width: sourceImag
13821515 grayscaleValue = grayscaleValue < rowThreshold ? 0 : 255 ;
13831516 }
13841517
1518+ if ( thermalFade > 0 && grayscaleValue < 255 ) {
1519+ const fadePattern = ( ( Math . sin ( ( x * 0.18 ) + ( y * 0.05 ) ) + Math . cos ( ( y * 0.11 ) - ( x * 0.03 ) ) ) * 0.25 ) + 0.5 + ( Math . random ( ) * 0.12 ) ;
1520+ const fadeLift = thermalFade * ( 24 + ( fadePattern * 112 ) ) ;
1521+ grayscaleValue = clamp ( grayscaleValue + fadeLift ) ;
1522+ if ( grayscaleValue < 128 && fadePattern > ( 0.74 - ( thermalFade * 0.18 ) ) ) {
1523+ grayscaleValue = 255 ;
1524+ }
1525+ }
1526+
13851527 if ( ocrFirstMode ) {
13861528 if ( grayscaleValue < 150 ) {
13871529 grayscaleValue = Math . max ( 0 , grayscaleValue * 0.42 ) ;
@@ -1620,6 +1762,7 @@ function renderPreviews() {
16201762 elements . activeFiltersSummary . textContent = getActiveFilterSummary ( ) ;
16211763 updateDownloadButtonLabel ( ) ;
16221764 updateVisibleActionLabels ( ) ;
1765+ renderPresetComparison ( ) ;
16231766 if ( hiddenByFilter ) {
16241767 setStatus (
16251768 `${ state . files . length } page${ state . files . length > 1 ? "s" : "" } loaded, but the current page filter is hiding all of them. Switch the Pages filter to Show all pages to restore the preview.${ elements . exportVisibleOnly . checked ? " Visible-only export is also hiding downloads." : "" } `
@@ -2713,6 +2856,11 @@ elements.selectDetectedBlanks.addEventListener("change", updateBlankActionLabel)
27132856 } ) ;
27142857} ) ;
27152858
2859+ elements . thermalFade . addEventListener ( "input" , ( ) => {
2860+ scheduleRenderPreviews ( ) ;
2861+ renderAdjustPreview ( ) ;
2862+ } ) ;
2863+
27162864elements . ocrFirstMode . addEventListener ( "change" , ( ) => {
27172865 scheduleRenderPreviews ( ) ;
27182866 renderAdjustPreview ( ) ;
0 commit comments