Update recipe card design, add temp debug path

This commit is contained in:
Sam Becker 2025-02-20 22:18:40 -06:00
parent 66ccc5cf03
commit 486c6dc1ae
4 changed files with 196 additions and 88 deletions

View File

@ -5,18 +5,20 @@ import clsx from 'clsx/lite';
export default async function AdminRecipePage() { export default async function AdminRecipePage() {
const photos = await getPhotos({ hidden: 'only' }); const photos = await getPhotos({ hidden: 'only' });
const { fujifilmRecipe } = photos[0]; const { fujifilmRecipe, filmSimulation } = photos[0];
return ( return (
<SiteGrid <SiteGrid
contentMain={<div className={clsx( contentMain={<div className={clsx(
'w-full min-h-[600px]', 'w-full min-h-[min(500px,70vh)]',
'flex items-center justify-center', 'flex items-center justify-center',
)}> )}>
{fujifilmRecipe && {(fujifilmRecipe && filmSimulation) &&
<PhotoRecipe recipe={fujifilmRecipe} /> <PhotoRecipe
recipe={fujifilmRecipe}
simulation={filmSimulation}
/>
} }
</div>} </div>}
/> />
); );
} }

View File

@ -1,25 +1,128 @@
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import { FilmSimulation } from '@/simulation';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import clsx from 'clsx/lite'; 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: { export default function PhotoRecipe({
recipe: {
dynamicRange, dynamicRange,
whiteBalance,
highISONoiseReduction,
noiseReductionBasic,
highlight, highlight,
shadow, shadow,
color, color,
highISONoiseReduction,
noiseReductionLegacy,
sharpness, sharpness,
clarity, clarity,
grainEffect,
colorChromeEffect, colorChromeEffect,
colorChromeFXBlue, colorChromeFXBlue,
whiteBalance, grainEffect,
bwAdjustment, bwAdjustment,
bwMagentaGreen, bwMagentaGreen,
} }: { recipe: FujifilmRecipe }) { },
return <div className=""> 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') => (
<div className={clsx(
'flex flex-col items-center justify-center',
'bg-dim rounded-md p-0.5',
)}>
<div>{typeof value === 'number' ? addSign(value) : value}</div>
<div>{label}</div>
</div>
);
return <div className="flex gap-8">
<div className={clsx(
'w-[18rem] self-start',
'p-3',
'component-surface shadow-xs',
'space-y-3',
)}>
<div className="flex items-center gap-2">
<PhotoFilmSimulation {...{ simulation, className: 'grow' }} />
<div className="bg-dim rounded-md p-0.5">
<span>DR</span>
<span>{dynamicRange ?? 100}</span>
</div>
</div>
<div
className="text-sm uppercase space-y-3"
>
<div>
{whiteBalanceFormatted.length <= 8 && 'AWB: '}
{whiteBalanceFormatted}
{hasCustomizedWhiteBalance && <>
{' '}
<span className="text-extra-dim">{'('}</span>
R{addSign(whiteBalance?.red ?? 0)}
<span className="text-extra-extra-dim"> / </span>
B{addSign(whiteBalance?.blue ?? 0)}
<span className="text-extra-dim">{')'}</span>
</>}
</div>
<div className="flex gap-3 *:w-full">
{renderDataSquare('Highlight', highlight)}
{renderDataSquare('Shadow', shadow)}
</div>
<div className="flex gap-3 *:w-full">
{/* TODO: Confirm color vs saturation label */}
{renderDataSquare('Color', color)}
{renderDataSquare('Sharp', sharpness)}
{renderDataSquare('Clarity', clarity)}
</div>
<div className="flex gap-3 *:w-full">
{renderDataSquare('Chrome', colorChromeEffect)}
{renderDataSquare('FX Blue', colorChromeFXBlue)}
</div>
<div>
{highISONoiseReduction !== undefined
? <>
<span>High ISO NR: </span>
<span>{addSign(highISONoiseReduction)}</span>
</>
: <>
<span>Noise Reduction: </span>
<span>{noiseReductionBasic}</span>
</>
}
</div>
{grainEffect &&
<div>
Grain:
{' '}
{grainEffect.roughness}
<span className="text-extra-dim">{' / '}</span>
{grainEffect.size}
</div>}
{hasBWAdjustments &&
<div>
BW Adjustment:
{' '}
{addSign(bwAdjustment)}
{' '}
MG:
{addSign(bwMagentaGreen)}
</div>}
</div>
</div>
<div className={clsx( <div className={clsx(
'px-3 py-2 max-w-[16rem]', 'px-3 py-2 max-w-[16rem]',
'text-left text-xs', 'text-left text-xs',
@ -47,9 +150,9 @@ export default function PhotoRecipe({ recipe: {
<div>High ISO NR</div> <div>High ISO NR</div>
<div>{addSign(highISONoiseReduction)}</div> <div>{addSign(highISONoiseReduction)}</div>
</>} </>}
{noiseReductionLegacy !== undefined && <> {noiseReductionBasic !== undefined && <>
<div>NR</div> <div>NR</div>
<div>{noiseReductionLegacy}</div> <div>{noiseReductionBasic}</div>
</>} </>}
{sharpness !== undefined && <> {sharpness !== undefined && <>
<div>Sharpness</div> <div>Sharpness</div>

View File

@ -1,19 +1,19 @@
import { parseFujifilmMakerNote } from '.'; import { parseFujifilmMakerNote } from '.';
const TAG_ID_DEVELOPMENT_DYNAMIC_RANGE = 0x1403; 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_HIGHLIGHT = 0x1041;
const TAG_ID_SHADOW = 0x1040; const TAG_ID_SHADOW = 0x1040;
const TAG_ID_SATURATION = 0x1003; 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_SHARPNESS = 0x1001;
const TAG_ID_CLARITY = 0x100f; 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_EFFECT = 0x1048;
const TAG_ID_COLOR_CHROME_FX_BLUE = 0x104e; const TAG_ID_COLOR_CHROME_FX_BLUE = 0x104e;
const TAG_ID_WHITE_BALANCE = 0x1002; const TAG_ID_GRAIN_EFFECT_ROUGHNESS = 0x1047;
const TAG_ID_WHITE_BALANCE_FINE_TUNE = 0x100a; const TAG_ID_GRAIN_EFFECT_SIZE = 0x104c;
const TAG_ID_BW_ADJUSTMENT = 0x1049; const TAG_ID_BW_ADJUSTMENT = 0x1049;
const TAG_ID_BW_MAGENTA_GREEN = 0x104b; const TAG_ID_BW_MAGENTA_GREEN = 0x104b;
@ -21,39 +21,39 @@ type WeakStrong = 'off' | 'weak' | 'strong';
export type FujifilmRecipe = Partial<{ export type FujifilmRecipe = Partial<{
dynamicRange: number 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: { whiteBalance: {
type: string type: string
red: number red: number
blue: 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 bwAdjustment: number
bwMagentaGreen: number bwMagentaGreen: number
}>; }>;
const DEFAULT_GRAIN_EFFECT = {
roughness: 'off',
size: 'off',
} as const;
const DEFAULT_WHITE_BALANCE = { const DEFAULT_WHITE_BALANCE = {
type: 'auto', type: 'auto',
red: 0, red: 0,
blue: 0, blue: 0,
} as const; } as const;
const DEFAULT_GRAIN_EFFECT = {
roughness: 'off',
size: 'off',
} as const;
export const processTone = (value: number) => export const processTone = (value: number) =>
value === 0 ? 0 : -(value / 16); value === 0 ? 0 : -(value / 16);
@ -165,42 +165,6 @@ export const getFujifilmRecipeFromMakerNote = (
case TAG_ID_DEVELOPMENT_DYNAMIC_RANGE: case TAG_ID_DEVELOPMENT_DYNAMIC_RANGE:
recipe.dynamicRange = numbers[0]; recipe.dynamicRange = numbers[0];
break; 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: case TAG_ID_WHITE_BALANCE:
if (!recipe.whiteBalance) { if (!recipe.whiteBalance) {
recipe.whiteBalance = DEFAULT_WHITE_BALANCE; recipe.whiteBalance = DEFAULT_WHITE_BALANCE;
@ -214,6 +178,42 @@ export const getFujifilmRecipeFromMakerNote = (
recipe.whiteBalance.red = processWhiteBalanceComponent(numbers[0]); recipe.whiteBalance.red = processWhiteBalanceComponent(numbers[0]);
recipe.whiteBalance.blue = processWhiteBalanceComponent(numbers[1]); recipe.whiteBalance.blue = processWhiteBalanceComponent(numbers[1]);
break; 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: case TAG_ID_BW_ADJUSTMENT:
recipe.bwAdjustment = numbers[0]; recipe.bwAdjustment = numbers[0];
break; break;

View File

@ -9,10 +9,11 @@ import EntityLink, {
EntityLinkExternalProps, EntityLinkExternalProps,
} from '@/components/primitives/EntityLink'; } from '@/components/primitives/EntityLink';
import { LuChevronsUpDown } from 'react-icons/lu'; import { LuChevronsUpDown } from 'react-icons/lu';
import clsx from 'clsx'; import clsx from 'clsx/lite';
import { useState } from 'react'; import { useState } from 'react';
import PhotoRecipe from '@/photo/PhotoRecipe'; import PhotoRecipe from '@/photo/PhotoRecipe';
import Tooltip from '@/components/Tooltip'; import Tooltip from '@/components/Tooltip';
export default function PhotoFilmSimulation({ export default function PhotoFilmSimulation({
simulation, simulation,
type = 'icon-last', type = 'icon-last',
@ -21,17 +22,19 @@ export default function PhotoFilmSimulation({
prefetch, prefetch,
countOnHover, countOnHover,
recipe, recipe,
className,
}: { }: {
simulation: FilmSimulation simulation: FilmSimulation
countOnHover?: number countOnHover?: number
recipe?: FujifilmRecipe recipe?: FujifilmRecipe
className?: string
} & EntityLinkExternalProps) { } & EntityLinkExternalProps) {
const { small, medium, large } = labelForFilmSimulation(simulation); const { small, medium, large } = labelForFilmSimulation(simulation);
const [shouldShowRecipe, setShouldShowRecipe] = useState(false); const [shouldShowRecipe, setShouldShowRecipe] = useState(false);
return ( return (
<div className="space-y-baseline"> <div className={clsx('space-y-baseline', className)}>
<div className="flex items-center gap-2 *:w-auto"> <div className="flex items-center gap-2 *:w-auto">
<EntityLink <EntityLink
label={medium} label={medium}
@ -61,7 +64,7 @@ export default function PhotoFilmSimulation({
</Tooltip>} </Tooltip>}
</div> </div>
{recipe && shouldShowRecipe && {recipe && shouldShowRecipe &&
<PhotoRecipe recipe={recipe} />} <PhotoRecipe {...{ recipe, simulation }} />}
</div> </div>
); );
} }