diff --git a/app/admin/recipe/[photoId]/page.tsx b/app/admin/recipe/[photoId]/page.tsx index 20841206..aaacdcfb 100644 --- a/app/admin/recipe/[photoId]/page.tsx +++ b/app/admin/recipe/[photoId]/page.tsx @@ -20,6 +20,8 @@ export default async function AdminRecipePage({ backgroundImageUrl={photo.url} recipe={fujifilmRecipe} simulation={filmSimulation} + exposure={photo.exposureCompensationFormatted ?? '+0ev'} + iso={photo.isoFormatted ?? 'ISO 0'} /> :
Can't find photo/recipe diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index a4277ff4..b2cb49a6 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -5,7 +5,7 @@ export default function Badge({ className, type = 'large', dimContent, - highContrast, + contrast = 'low', uppercase, interactive, }: { @@ -13,7 +13,7 @@ export default function Badge({ className?: string type?: 'large' | 'small' | 'text-only' dimContent?: boolean - highContrast?: boolean + contrast?: 'low' | 'medium' | 'high' | 'frost' uppercase?: boolean interactive?: boolean }) { @@ -30,13 +30,15 @@ export default function Badge({ return clsx( 'px-[5px] h-[17px] md:h-[18px]', 'text-[0.7rem] font-medium rounded-[0.25rem]', - highContrast + contrast === 'high' ? 'text-invert bg-invert' - : 'text-medium bg-gray-300/30 dark:bg-gray-700/50', - interactive && (highContrast + : contrast === 'frost' + ? 'text-black bg-white/30' + : 'text-medium bg-gray-300/30 dark:bg-gray-700/50', + interactive && (contrast === 'high' ? 'hover:opacity-70' : 'hover:text-gray-900 dark:hover:text-gray-100'), - interactive && (highContrast + interactive && (contrast === 'high' ? 'active:opacity-90' : 'active:bg-gray-200 dark:active:bg-gray-700/60'), ); diff --git a/src/components/primitives/EntityLink.tsx b/src/components/primitives/EntityLink.tsx index a13f68d0..d0765640 100644 --- a/src/components/primitives/EntityLink.tsx +++ b/src/components/primitives/EntityLink.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ReactNode } from 'react'; +import { ComponentProps, ReactNode } from 'react'; import LabeledIcon, { LabeledIconType } from './LabeledIcon'; import Badge from '../Badge'; import { clsx } from 'clsx/lite'; @@ -10,7 +10,7 @@ import Spinner from '../Spinner'; export interface EntityLinkExternalProps { type?: LabeledIconType badged?: boolean - contrast?: 'low' | 'medium' | 'high' + contrast?: ComponentProps['contrast'] prefetch?: boolean } @@ -48,6 +48,8 @@ export default function EntityLink({ return 'text-dim'; case 'high': return 'text-main'; + case 'frost': + return 'text-invert'; default: return 'text-medium'; } @@ -88,7 +90,7 @@ export default function EntityLink({ {badged ? value < 0 ? value : `+${value}`; @@ -38,18 +39,18 @@ export default function PhotoRecipeFrostLight({ bwMagentaGreen, }, simulation, + exposure, + iso, }: { recipe: FujifilmRecipe simulation: FilmSimulation + exposure: string + iso: string }) { const whiteBalanceFormatted = (whiteBalance?.type ?? 'auto') .replaceAll('auto', ' ') .replaceAll('-', ' '); - const hasCustomizedWhiteBalance = - Boolean(whiteBalance?.red) || - Boolean(whiteBalance?.blue); - const hasBWAdjustments = Boolean(bwAdjustment) || Boolean(bwMagentaGreen); @@ -75,34 +76,46 @@ export default function PhotoRecipeFrostLight({ 'rounded-lg shadow-2xl', 'bg-white/60 backdrop-blur-xl border border-white/30', 'space-y-3', - 'text-[13px] text-main', + 'text-[13px] text-black', 'saturate-200', )}>
-
- DR - {dynamicRange ?? 100} -
+
-
-
- {whiteBalanceFormatted.length <= 8 && 'AWB: '} - {whiteBalanceFormatted} - {hasCustomizedWhiteBalance && <> - {' '} - {'('} - R {addSign(whiteBalance?.red ?? 0)} - / - B {addSign(whiteBalance?.blue ?? 0)} - {')'} - } +
+
+
+ DR{dynamicRange ?? 100} +
+
+ {iso} +
+
+ {exposure} +
+
+
+ {renderDataSquare( + `R${addSign(whiteBalance?.red)} / B${addSign(whiteBalance?.blue)}`, + whiteBalanceFormatted, + )}
{renderDataSquare('Highlight', highlight || random.highlight)} @@ -118,31 +131,24 @@ export default function PhotoRecipeFrostLight({ {renderDataSquare('Color Chrome', colorChromeEffect)} {renderDataSquare('FX Blue', colorChromeFXBlue)}
-
- {highISONoiseReduction !== undefined - ? <> - High ISO NR: - {addSign(highISONoiseReduction)} - - : <> - Noise Reduction: - {noiseReductionBasic} - - } -
{grainEffect && -
- Grain: - {grainEffect.roughness} - {' / '} - {grainEffect.size} +
+ {renderDataSquare( + highISONoiseReduction !== undefined + ? 'High ISO NR' + : 'Noise Reduction', + highISONoiseReduction ?? noiseReductionBasic, + )} + {renderDataSquare( + 'Grain', + // eslint-disable-next-line max-len + `${grainEffect.roughness} / ${grainEffect.size === 'large' ? 'LG' : grainEffect.size === 'small' ? 'SM' : 'OFF'}`, + )}
} {hasBWAdjustments && -
- BW Adjustment: - {addSign(bwAdjustment)} - {' / '} - MG: {addSign(bwMagentaGreen)} +
+ {renderDataSquare('BW Adjustment', bwAdjustment)} + {renderDataSquare('BW Magenta Green', bwMagentaGreen)}
}
diff --git a/src/photo/PhotoRecipeFrostLightV2.tsx b/src/photo/PhotoRecipeFrostLightV2.tsx new file mode 100644 index 00000000..5a0e006c --- /dev/null +++ b/src/photo/PhotoRecipeFrostLightV2.tsx @@ -0,0 +1,156 @@ +import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; +import { FilmSimulation } from '@/simulation'; +import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation'; +import clsx from 'clsx/lite'; +import { IoCloseCircle } from 'react-icons/io5'; + +const addSign = (value = 0) => value < 0 ? value : `+${value}`; + +const getRandomInt = () => { + const randomInt = Math.floor(Math.random() * 4) + 1; + return Math.random() >= 0.5 ? randomInt : -randomInt; +}; + +const random = { + highlight: getRandomInt(), + shadow: getRandomInt(), + color: getRandomInt(), + sharpness: getRandomInt(), + clarity: getRandomInt(), + colorChromeEffect: getRandomInt(), + colorChromeFXBlue: getRandomInt(), +}; + +export default function PhotoRecipeFrostLightV2({ + recipe: { + dynamicRange, + whiteBalance, + highISONoiseReduction, + noiseReductionBasic, + highlight, + shadow, + color, + sharpness, + clarity, + colorChromeEffect, + colorChromeFXBlue, + grainEffect, + bwAdjustment, + bwMagentaGreen, + }, + simulation, + exposure, + iso, +}: { + recipe: FujifilmRecipe + simulation: FilmSimulation + exposure: string + iso: string +}) { + const whiteBalanceFormatted = (whiteBalance?.type ?? 'auto') + .replaceAll('auto', ' ') + .replaceAll('-', ' '); + + 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} +
+
+ {iso} +
+
+ {exposure} +
+
+
+ {renderDataSquare( + `R${addSign(whiteBalance?.red)} / B${addSign(whiteBalance?.blue)}`, + whiteBalanceFormatted, + )} +
+
+ {renderDataSquare('Highlight', highlight || random.highlight)} + {renderDataSquare('Shadow', shadow || random.shadow)} +
+
+ {/* TODO: Confirm color vs saturation label */} + {renderDataSquare('Color', color || random.color)} + {renderDataSquare('Sharpness', sharpness || random.sharpness)} + {renderDataSquare('Clarity', clarity || random.clarity)} +
+
+ {renderDataSquare('Color Chrome', colorChromeEffect)} + {renderDataSquare('FX Blue', colorChromeFXBlue)} +
+ {grainEffect && +
+ {renderDataSquare( + highISONoiseReduction !== undefined + ? 'High ISO NR' + : 'Noise Reduction', + highISONoiseReduction ?? noiseReductionBasic, + )} + {renderDataSquare( + 'Grain', + // eslint-disable-next-line max-len + `${grainEffect.roughness} / ${grainEffect.size === 'large' ? 'LG' : grainEffect.size === 'small' ? 'SM' : 'OFF'}`, + )} +
} + {hasBWAdjustments && +
+ {renderDataSquare('BW Adjustment', bwAdjustment)} + {renderDataSquare('BW Magenta Green', bwMagentaGreen)} +
} +
+
+
; +} diff --git a/src/photo/PhotoRecipeOverlay.tsx b/src/photo/PhotoRecipeOverlay.tsx index 7cdca292..d8ca5cee 100644 --- a/src/photo/PhotoRecipeOverlay.tsx +++ b/src/photo/PhotoRecipeOverlay.tsx @@ -1,35 +1,24 @@ -'use client'; - import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; import { FilmSimulation } from '@/simulation'; import clsx from 'clsx/lite'; import ImageLarge from '@/components/image/ImageLarge'; -import PhotoRecipeFrostLight from './PhotoRecipeFrostLight'; -import FieldSetWithStatus from '@/components/FieldSetWithStatus'; -import { useState } from 'react'; -import PhotoRecipe from './PhotoRecipe'; +import PhotoRecipeFrostLightV2 from './PhotoRecipeFrostLightV2'; + export default function PhotoRecipeOverlay({ backgroundImageUrl, recipe, simulation, + exposure, + iso, }: { backgroundImageUrl: string recipe: FujifilmRecipe simulation: FilmSimulation + exposure: string + iso: string }) { - const [isFrosted, setIsFrosted] = useState(true); - return (
-
- setIsFrosted(!isFrosted)} - /> -
@@ -42,14 +31,12 @@ export default function PhotoRecipeOverlay({ 'absolute inset-0', 'flex items-center justify-center', )}> - {isFrosted - ? : } +