diff --git a/app/admin/recipe/page.tsx b/app/admin/recipe/page.tsx index 5a2a72d5..47889c51 100644 --- a/app/admin/recipe/page.tsx +++ b/app/admin/recipe/page.tsx @@ -5,18 +5,20 @@ import clsx from 'clsx/lite'; export default async function AdminRecipePage() { const photos = await getPhotos({ hidden: 'only' }); - const { fujifilmRecipe } = photos[0]; + const { fujifilmRecipe, filmSimulation } = photos[0]; return ( - {fujifilmRecipe && - + {(fujifilmRecipe && filmSimulation) && + } } /> ); } - diff --git a/src/photo/PhotoRecipe.tsx b/src/photo/PhotoRecipe.tsx index b8365de1..485e09e2 100644 --- a/src/photo/PhotoRecipe.tsx +++ b/src/photo/PhotoRecipe.tsx @@ -1,25 +1,128 @@ import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; +import { FilmSimulation } from '@/simulation'; +import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation'; import clsx from 'clsx/lite'; -const addSign = (value: number) => value < 0 ? value : `+${value}`; +const addSign = (value = 0) => value < 0 ? value : `+${value}`; -export default function PhotoRecipe({ recipe: { - dynamicRange, - highlight, - shadow, - color, - highISONoiseReduction, - noiseReductionLegacy, - sharpness, - clarity, - grainEffect, - colorChromeEffect, - colorChromeFXBlue, - whiteBalance, - bwAdjustment, - bwMagentaGreen, -} }: { recipe: FujifilmRecipe }) { - return
+export default function PhotoRecipe({ + recipe: { + dynamicRange, + whiteBalance, + highISONoiseReduction, + noiseReductionBasic, + highlight, + shadow, + color, + sharpness, + clarity, + colorChromeEffect, + colorChromeFXBlue, + grainEffect, + bwAdjustment, + bwMagentaGreen, + }, + simulation, +}: { + recipe: FujifilmRecipe + simulation: FilmSimulation +}) { + const whiteBalanceFormatted = (whiteBalance?.type ?? 'auto') + .replaceAll('auto', ' ') + .replaceAll('-', ' '); + + const hasCustomizedWhiteBalance = + Boolean(whiteBalance?.red) || + Boolean(whiteBalance?.blue); + + const hasBWAdjustments = + Boolean(bwAdjustment) || + Boolean(bwMagentaGreen); + + const renderDataSquare = (label: string, value: string | number = '0') => ( +
+
{typeof value === 'number' ? addSign(value) : value}
+
{label}
+
+ ); + + return
+
+
+ +
+ DR + {dynamicRange ?? 100} +
+
+
+
+ {whiteBalanceFormatted.length <= 8 && 'AWB: '} + {whiteBalanceFormatted} + {hasCustomizedWhiteBalance && <> + {' '} + {'('} + R{addSign(whiteBalance?.red ?? 0)} + / + B{addSign(whiteBalance?.blue ?? 0)} + {')'} + } +
+
+ {renderDataSquare('Highlight', highlight)} + {renderDataSquare('Shadow', shadow)} +
+
+ {/* TODO: Confirm color vs saturation label */} + {renderDataSquare('Color', color)} + {renderDataSquare('Sharp', sharpness)} + {renderDataSquare('Clarity', clarity)} +
+
+ {renderDataSquare('Chrome', colorChromeEffect)} + {renderDataSquare('FX Blue', colorChromeFXBlue)} +
+
+ {highISONoiseReduction !== undefined + ? <> + High ISO NR: + {addSign(highISONoiseReduction)} + + : <> + Noise Reduction: + {noiseReductionBasic} + + } +
+ {grainEffect && +
+ Grain: + {' '} + {grainEffect.roughness} + {' / '} + {grainEffect.size} +
} + {hasBWAdjustments && +
+ BW Adjustment: + {' '} + {addSign(bwAdjustment)} + {' '} + MG: + {addSign(bwMagentaGreen)} +
} +
+
High ISO NR
{addSign(highISONoiseReduction)}
} - {noiseReductionLegacy !== undefined && <> + {noiseReductionBasic !== undefined && <>
NR
-
{noiseReductionLegacy}
+
{noiseReductionBasic}
} {sharpness !== undefined && <>
Sharpness
diff --git a/src/platforms/fujifilm/recipe.ts b/src/platforms/fujifilm/recipe.ts index 1e5ed518..527eb0e5 100644 --- a/src/platforms/fujifilm/recipe.ts +++ b/src/platforms/fujifilm/recipe.ts @@ -1,19 +1,19 @@ import { parseFujifilmMakerNote } from '.'; const TAG_ID_DEVELOPMENT_DYNAMIC_RANGE = 0x1403; +const TAG_ID_WHITE_BALANCE = 0x1002; +const TAG_ID_WHITE_BALANCE_FINE_TUNE = 0x100a; +const TAG_ID_NOISE_REDUCTION = 0x100e; +const TAG_ID_NOISE_REDUCTION_BASIC = 0x100b; const TAG_ID_HIGHLIGHT = 0x1041; const TAG_ID_SHADOW = 0x1040; const TAG_ID_SATURATION = 0x1003; -const TAG_ID_NOISE_REDUCTION = 0x100e; -const TAG_ID_NOISE_REDUCTION_BASIC = 0x100b; const TAG_ID_SHARPNESS = 0x1001; const TAG_ID_CLARITY = 0x100f; -const TAG_ID_GRAIN_EFFECT_ROUGHNESS = 0x1047; -const TAG_ID_GRAIN_EFFECT_SIZE = 0x104c; const TAG_ID_COLOR_CHROME_EFFECT = 0x1048; const TAG_ID_COLOR_CHROME_FX_BLUE = 0x104e; -const TAG_ID_WHITE_BALANCE = 0x1002; -const TAG_ID_WHITE_BALANCE_FINE_TUNE = 0x100a; +const TAG_ID_GRAIN_EFFECT_ROUGHNESS = 0x1047; +const TAG_ID_GRAIN_EFFECT_SIZE = 0x104c; const TAG_ID_BW_ADJUSTMENT = 0x1049; const TAG_ID_BW_MAGENTA_GREEN = 0x104b; @@ -21,39 +21,39 @@ type WeakStrong = 'off' | 'weak' | 'strong'; export type FujifilmRecipe = Partial<{ dynamicRange: number - highlight: number - shadow: number - color: number - highISONoiseReduction: number - noiseReductionLegacy: string - sharpness: number - clarity: number - grainEffect: { - roughness: WeakStrong - size: 'off' | 'small' | 'large' - } - colorChromeEffect: WeakStrong - colorChromeFXBlue: WeakStrong whiteBalance: { type: string red: number blue: number } + highISONoiseReduction: number + noiseReductionBasic: string + highlight: number + shadow: number + color: number + sharpness: number + clarity: number + colorChromeEffect: WeakStrong + colorChromeFXBlue: WeakStrong + grainEffect: { + roughness: WeakStrong + size: 'off' | 'small' | 'large' + } bwAdjustment: number bwMagentaGreen: number }>; -const DEFAULT_GRAIN_EFFECT = { - roughness: 'off', - size: 'off', -} as const; - const DEFAULT_WHITE_BALANCE = { type: 'auto', red: 0, blue: 0, } as const; +const DEFAULT_GRAIN_EFFECT = { + roughness: 'off', + size: 'off', +} as const; + export const processTone = (value: number) => value === 0 ? 0 : -(value / 16); @@ -165,42 +165,6 @@ export const getFujifilmRecipeFromMakerNote = ( case TAG_ID_DEVELOPMENT_DYNAMIC_RANGE: recipe.dynamicRange = numbers[0]; break; - case TAG_ID_HIGHLIGHT: - recipe.highlight = processTone(numbers[0]); - break; - case TAG_ID_SHADOW: - recipe.shadow = processTone(numbers[0]); - break; - case TAG_ID_SATURATION: - recipe.color = processSaturation(numbers[0]); - break; - case TAG_ID_NOISE_REDUCTION: - recipe.highISONoiseReduction = processNoiseReduction(numbers[0]); - break; - case TAG_ID_NOISE_REDUCTION_BASIC: - recipe.noiseReductionLegacy = - processNoiseReductionLegacy(numbers[0]); - break; - case TAG_ID_SHARPNESS: - recipe.sharpness = processSharpness(numbers[0]); - break; - case TAG_ID_CLARITY: - recipe.clarity = processClarity(numbers[0]); - break; - case TAG_ID_GRAIN_EFFECT_ROUGHNESS: - if (!recipe.grainEffect) { recipe.grainEffect = DEFAULT_GRAIN_EFFECT; } - recipe.grainEffect.roughness = processWeakStrong(numbers[0]); - break; - case TAG_ID_GRAIN_EFFECT_SIZE: - if (!recipe.grainEffect) { recipe.grainEffect = DEFAULT_GRAIN_EFFECT; } - recipe.grainEffect.size = processGrainEffectSize(numbers[0]); - break; - case TAG_ID_COLOR_CHROME_EFFECT: - recipe.colorChromeEffect = processWeakStrong(numbers[0]); - break; - case TAG_ID_COLOR_CHROME_FX_BLUE: - recipe.colorChromeFXBlue = processWeakStrong(numbers[0]); - break; case TAG_ID_WHITE_BALANCE: if (!recipe.whiteBalance) { recipe.whiteBalance = DEFAULT_WHITE_BALANCE; @@ -214,6 +178,42 @@ export const getFujifilmRecipeFromMakerNote = ( recipe.whiteBalance.red = processWhiteBalanceComponent(numbers[0]); recipe.whiteBalance.blue = processWhiteBalanceComponent(numbers[1]); break; + case TAG_ID_NOISE_REDUCTION: + recipe.highISONoiseReduction = processNoiseReduction(numbers[0]); + break; + case TAG_ID_NOISE_REDUCTION_BASIC: + recipe.noiseReductionBasic = + processNoiseReductionLegacy(numbers[0]); + break; + case TAG_ID_HIGHLIGHT: + recipe.highlight = processTone(numbers[0]); + break; + case TAG_ID_SHADOW: + recipe.shadow = processTone(numbers[0]); + break; + case TAG_ID_SATURATION: + recipe.color = processSaturation(numbers[0]); + break; + case TAG_ID_SHARPNESS: + recipe.sharpness = processSharpness(numbers[0]); + break; + case TAG_ID_CLARITY: + recipe.clarity = processClarity(numbers[0]); + break; + case TAG_ID_COLOR_CHROME_EFFECT: + recipe.colorChromeEffect = processWeakStrong(numbers[0]); + break; + case TAG_ID_COLOR_CHROME_FX_BLUE: + recipe.colorChromeFXBlue = processWeakStrong(numbers[0]); + break; + case TAG_ID_GRAIN_EFFECT_ROUGHNESS: + if (!recipe.grainEffect) { recipe.grainEffect = DEFAULT_GRAIN_EFFECT; } + recipe.grainEffect.roughness = processWeakStrong(numbers[0]); + break; + case TAG_ID_GRAIN_EFFECT_SIZE: + if (!recipe.grainEffect) { recipe.grainEffect = DEFAULT_GRAIN_EFFECT; } + recipe.grainEffect.size = processGrainEffectSize(numbers[0]); + break; case TAG_ID_BW_ADJUSTMENT: recipe.bwAdjustment = numbers[0]; break; diff --git a/src/simulation/PhotoFilmSimulation.tsx b/src/simulation/PhotoFilmSimulation.tsx index 7c3bda3a..4248e519 100644 --- a/src/simulation/PhotoFilmSimulation.tsx +++ b/src/simulation/PhotoFilmSimulation.tsx @@ -9,10 +9,11 @@ import EntityLink, { EntityLinkExternalProps, } from '@/components/primitives/EntityLink'; import { LuChevronsUpDown } from 'react-icons/lu'; -import clsx from 'clsx'; +import clsx from 'clsx/lite'; import { useState } from 'react'; import PhotoRecipe from '@/photo/PhotoRecipe'; import Tooltip from '@/components/Tooltip'; + export default function PhotoFilmSimulation({ simulation, type = 'icon-last', @@ -21,17 +22,19 @@ export default function PhotoFilmSimulation({ prefetch, countOnHover, recipe, + className, }: { simulation: FilmSimulation countOnHover?: number recipe?: FujifilmRecipe + className?: string } & EntityLinkExternalProps) { const { small, medium, large } = labelForFilmSimulation(simulation); const [shouldShowRecipe, setShouldShowRecipe] = useState(false); return ( -
+
}
{recipe && shouldShowRecipe && - } + }
); }