Adjust DR schema, refine recipe behavior
This commit is contained in:
parent
22d94e1b4b
commit
34667efedf
@ -98,12 +98,16 @@ export default function PhotoLarge({
|
||||
|
||||
const showZoomControls = showZoomControlsProp && areZoomControlsShown;
|
||||
|
||||
const recipeRef = useRef<HTMLDivElement>(null);
|
||||
const refRecipe = useRef<HTMLDivElement>(null);
|
||||
const refRecipeTrigger = useRef<HTMLButtonElement>(null);
|
||||
const {
|
||||
shouldShowRecipe,
|
||||
toggleRecipe,
|
||||
recipeButtonRef,
|
||||
} = useRecipeState(recipeRef);
|
||||
hideRecipe,
|
||||
} = useRecipeState({
|
||||
ref: refRecipe,
|
||||
refTrigger: refRecipeTrigger,
|
||||
});
|
||||
|
||||
const tags = sortTags(photo.tags, primaryTag);
|
||||
|
||||
@ -173,23 +177,22 @@ export default function PhotoLarge({
|
||||
priority={priority}
|
||||
/>
|
||||
</ZoomControls>
|
||||
<AnimatePresence>
|
||||
<div className={clsx(
|
||||
'absolute inset-0',
|
||||
'flex items-center justify-center',
|
||||
)}>
|
||||
<div className={clsx(
|
||||
'absolute inset-0',
|
||||
'flex items-center justify-center',
|
||||
)}>
|
||||
<AnimatePresence>
|
||||
{shouldShowRecipe && photo.fujifilmRecipe && photo.filmSimulation &&
|
||||
<PhotoRecipe
|
||||
ref={recipeRef}
|
||||
ref={refRecipe}
|
||||
recipe={photo.fujifilmRecipe}
|
||||
simulation={photo.filmSimulation}
|
||||
iso={photo.isoFormatted}
|
||||
exposure={photo.exposureCompensationFormatted}
|
||||
onClose={toggleRecipe}
|
||||
externalTriggerRef={recipeButtonRef}
|
||||
onClose={hideRecipe}
|
||||
/>}
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const largePhotoContainerClassName = clsx(arePhotosMatted &&
|
||||
@ -309,7 +312,7 @@ export default function PhotoLarge({
|
||||
/>
|
||||
{photo.fujifilmRecipe &&
|
||||
<button
|
||||
ref={recipeButtonRef}
|
||||
ref={refRecipeTrigger}
|
||||
title="Fujifilm Recipe"
|
||||
onClick={toggleRecipe}
|
||||
className={clsx(
|
||||
|
||||
@ -1,26 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import {
|
||||
FujifilmRecipe,
|
||||
DEFAULT_GRAIN_EFFECT,
|
||||
DEFAULT_WHITE_BALANCE,
|
||||
} from '@/platforms/fujifilm/recipe';
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
|
||||
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
||||
import clsx from 'clsx/lite';
|
||||
import { ReactNode, useRef, RefObject } from 'react';
|
||||
import { ReactNode, RefObject } from 'react';
|
||||
import { IoCloseCircle } from 'react-icons/io5';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const addSign = (value = 0) => value < 0 ? value : `+${value}`;
|
||||
|
||||
export default function PhotoRecipe({
|
||||
ref: refExternal,
|
||||
ref,
|
||||
recipe: {
|
||||
dynamicRange,
|
||||
whiteBalance = DEFAULT_WHITE_BALANCE,
|
||||
whiteBalance,
|
||||
highISONoiseReduction,
|
||||
noiseReductionBasic,
|
||||
highlight,
|
||||
@ -30,7 +25,7 @@ export default function PhotoRecipe({
|
||||
clarity,
|
||||
colorChromeEffect,
|
||||
colorChromeFXBlue,
|
||||
grainEffect = DEFAULT_GRAIN_EFFECT,
|
||||
grainEffect,
|
||||
bwAdjustment,
|
||||
bwMagentaGreen,
|
||||
},
|
||||
@ -38,7 +33,6 @@ export default function PhotoRecipe({
|
||||
iso,
|
||||
exposure,
|
||||
onClose,
|
||||
externalTriggerRef,
|
||||
}: {
|
||||
ref?: RefObject<HTMLDivElement | null>
|
||||
recipe: FujifilmRecipe
|
||||
@ -46,15 +40,7 @@ export default function PhotoRecipe({
|
||||
iso?: string
|
||||
exposure?: string
|
||||
onClose?: () => void
|
||||
externalTriggerRef?: RefObject<HTMLElement | null>
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useClickInsideOutside({
|
||||
htmlElements: [ref, refExternal, externalTriggerRef],
|
||||
onClickOutside: onClose,
|
||||
});
|
||||
|
||||
const whiteBalanceTypeFormatted = whiteBalance.type
|
||||
.replace(/auto./i, '')
|
||||
.replaceAll('-', ' ');
|
||||
@ -89,7 +75,7 @@ export default function PhotoRecipe({
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={refExternal ?? ref}
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, translateY: -10 }}
|
||||
animate={{ opacity: 1, translateY: 0 }}
|
||||
exit={{ opacity: 0, translateY: -10 }}
|
||||
@ -119,7 +105,7 @@ export default function PhotoRecipe({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{renderRow(<>
|
||||
{renderDataSquare(`DR${dynamicRange ?? 100}`)}
|
||||
{renderDataSquare(`DR${dynamicRange.development}`)}
|
||||
{renderDataSquare(iso)}
|
||||
{renderDataSquare(exposure ?? '0ev')}
|
||||
</>)}
|
||||
|
||||
@ -6,12 +6,17 @@ import {
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { SEARCH_PARAM_SHOW } from '@/app/paths';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { RefObject, useCallback, useEffect, useState } from 'react';
|
||||
import { isElementEntirelyInViewport } from '@/utility/dom';
|
||||
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
||||
|
||||
export default function useRecipeState(
|
||||
ref?: RefObject<HTMLDivElement | null>,
|
||||
) {
|
||||
export default function useRecipeState({
|
||||
ref,
|
||||
refTrigger,
|
||||
}: {
|
||||
ref?: RefObject<HTMLElement | null>,
|
||||
refTrigger?: RefObject<HTMLElement | null>,
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const params = useSearchParams();
|
||||
|
||||
@ -19,28 +24,14 @@ export default function useRecipeState(
|
||||
photoId,
|
||||
...pathComponents
|
||||
} = getPathComponents(pathname);
|
||||
|
||||
const showRecipeInitially =
|
||||
params.get(SEARCH_PARAM_SHOW) === SEARCH_PARAM_SHOW_RECIPE;
|
||||
|
||||
const [shouldShowRecipe, setShouldShowRecipe] = useState(showRecipeInitially);
|
||||
|
||||
const recipeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const toggleRecipe = useCallback(() => {
|
||||
if (shouldShowRecipe) {
|
||||
setShouldShowRecipe(false);
|
||||
// Only remove query param for photo details
|
||||
if (photoId) {
|
||||
window.history.pushState(
|
||||
null,
|
||||
'',
|
||||
pathForPhoto({
|
||||
photo: photoId,
|
||||
...pathComponents,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const setVisibility = useCallback((shouldShow: boolean) => {
|
||||
if (shouldShow) {
|
||||
setShouldShowRecipe(true);
|
||||
// Only add query param for photo details
|
||||
if (photoId) {
|
||||
@ -54,8 +45,32 @@ export default function useRecipeState(
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setShouldShowRecipe(false);
|
||||
// Only remove query param for photo details
|
||||
if (photoId) {
|
||||
window.history.pushState(
|
||||
null,
|
||||
'',
|
||||
pathForPhoto({
|
||||
photo: photoId,
|
||||
...pathComponents,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [pathComponents, photoId, shouldShowRecipe]);
|
||||
}, [pathComponents, photoId]);
|
||||
|
||||
const showRecipe = useCallback(() => setVisibility(true), [setVisibility]);
|
||||
const hideRecipe = useCallback(() => setVisibility(false), [setVisibility]);
|
||||
const toggleRecipe = useCallback(() =>
|
||||
setVisibility(!shouldShowRecipe),
|
||||
[setVisibility, shouldShowRecipe]);
|
||||
|
||||
useClickInsideOutside({
|
||||
htmlElements: [ref, refTrigger],
|
||||
onClickOutside: hideRecipe,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldShowRecipe && !isElementEntirelyInViewport(ref?.current)) {
|
||||
@ -64,8 +79,9 @@ export default function useRecipeState(
|
||||
}, [ref, shouldShowRecipe]);
|
||||
|
||||
return {
|
||||
toggleRecipe,
|
||||
recipeButtonRef,
|
||||
shouldShowRecipe,
|
||||
showRecipe,
|
||||
hideRecipe,
|
||||
toggleRecipe,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { parseFujifilmMakerNote } from '.';
|
||||
|
||||
const TAG_ID_DYNAMIC_RANGE = 0x1400;
|
||||
const TAG_ID_DYNAMIC_RANGE_SETTING = 0x1402;
|
||||
const TAG_ID_DEVELOPMENT_DYNAMIC_RANGE = 0x1403;
|
||||
const TAG_ID_WHITE_BALANCE = 0x1002;
|
||||
const TAG_ID_WHITE_BALANCE_FINE_TUNE = 0x100a;
|
||||
@ -19,41 +21,65 @@ const TAG_ID_BW_MAGENTA_GREEN = 0x104b;
|
||||
|
||||
type WeakStrong = 'off' | 'weak' | 'strong';
|
||||
|
||||
export type FujifilmRecipe = Partial<{
|
||||
dynamicRange: number
|
||||
export type FujifilmRecipe = {
|
||||
dynamicRange: {
|
||||
range: 'standard' | 'wide'
|
||||
// eslint-disable-next-line max-len
|
||||
setting: 'auto' | 'manual' | 'standard' | 'wide-1' | 'wide-2' | 'film-simulation'
|
||||
development: number
|
||||
}
|
||||
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
|
||||
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
|
||||
}>;
|
||||
bwAdjustment?: number
|
||||
bwMagentaGreen?: number
|
||||
};
|
||||
|
||||
export const DEFAULT_WHITE_BALANCE = {
|
||||
const DEFAULT_DYNAMIC_RANGE = {
|
||||
range: 'standard',
|
||||
setting: 'auto',
|
||||
development: 100,
|
||||
} as const;
|
||||
|
||||
const DEFAULT_WHITE_BALANCE = {
|
||||
type: 'auto',
|
||||
red: 0,
|
||||
blue: 0,
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_GRAIN_EFFECT = {
|
||||
const DEFAULT_GRAIN_EFFECT = {
|
||||
roughness: 'off',
|
||||
size: 'off',
|
||||
} as const;
|
||||
|
||||
export const processDynamicRangeSettings = (
|
||||
value: number,
|
||||
): FujifilmRecipe['dynamicRange']['setting'] => {
|
||||
switch (value) {
|
||||
case 0x001: return 'manual';
|
||||
case 0x100: return 'standard';
|
||||
case 0x200: return 'wide-1';
|
||||
case 0x201: return 'wide-1';
|
||||
case 0x8000: return 'film-simulation';
|
||||
default: return 'auto';
|
||||
}
|
||||
};
|
||||
|
||||
export const processTone = (value: number) =>
|
||||
value === 0 ? 0 : -(value / 16);
|
||||
|
||||
@ -156,25 +182,31 @@ export const processWhiteBalanceComponent = (value: number) => value / 20;
|
||||
export const getFujifilmRecipeFromMakerNote = (
|
||||
bytes: Buffer,
|
||||
): FujifilmRecipe => {
|
||||
const recipe: FujifilmRecipe = {};
|
||||
const recipe: FujifilmRecipe = {
|
||||
dynamicRange: DEFAULT_DYNAMIC_RANGE,
|
||||
whiteBalance: DEFAULT_WHITE_BALANCE,
|
||||
grainEffect: DEFAULT_GRAIN_EFFECT,
|
||||
};
|
||||
|
||||
parseFujifilmMakerNote(
|
||||
bytes,
|
||||
(tag, numbers) => {
|
||||
switch (tag) {
|
||||
case TAG_ID_DYNAMIC_RANGE:
|
||||
recipe.dynamicRange.range = numbers[0] === 3
|
||||
? 'wide'
|
||||
: 'standard';
|
||||
break;
|
||||
case TAG_ID_DYNAMIC_RANGE_SETTING:
|
||||
recipe.dynamicRange.setting = processDynamicRangeSettings(numbers[0]);
|
||||
break;
|
||||
case TAG_ID_DEVELOPMENT_DYNAMIC_RANGE:
|
||||
recipe.dynamicRange = numbers[0];
|
||||
recipe.dynamicRange.development = numbers[0];
|
||||
break;
|
||||
case TAG_ID_WHITE_BALANCE:
|
||||
if (!recipe.whiteBalance) {
|
||||
recipe.whiteBalance = DEFAULT_WHITE_BALANCE;
|
||||
}
|
||||
recipe.whiteBalance.type = processWhiteBalanceType(numbers[0]);
|
||||
break;
|
||||
case TAG_ID_WHITE_BALANCE_FINE_TUNE:
|
||||
if (!recipe.whiteBalance) {
|
||||
recipe.whiteBalance = DEFAULT_WHITE_BALANCE;
|
||||
}
|
||||
recipe.whiteBalance.red = processWhiteBalanceComponent(numbers[0]);
|
||||
recipe.whiteBalance.blue = processWhiteBalanceComponent(numbers[1]);
|
||||
break;
|
||||
@ -182,8 +214,7 @@ export const getFujifilmRecipeFromMakerNote = (
|
||||
recipe.highISONoiseReduction = processNoiseReduction(numbers[0]);
|
||||
break;
|
||||
case TAG_ID_NOISE_REDUCTION_BASIC:
|
||||
recipe.noiseReductionBasic =
|
||||
processNoiseReductionLegacy(numbers[0]);
|
||||
recipe.noiseReductionBasic = processNoiseReductionLegacy(numbers[0]);
|
||||
break;
|
||||
case TAG_ID_HIGHLIGHT:
|
||||
recipe.highlight = processTone(numbers[0]);
|
||||
@ -207,11 +238,9 @@ export const getFujifilmRecipeFromMakerNote = (
|
||||
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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user