Sketch on recipe visualization

This commit is contained in:
Sam Becker 2025-02-21 17:24:19 -06:00
parent 381dd43263
commit 338426114e
7 changed files with 193 additions and 27 deletions

View File

@ -0,0 +1,29 @@
import SiteGrid from '@/components/SiteGrid';
import { getPhoto, getPhotos } from '@/photo/db/query';
import PhotoRecipeOverlay from '@/photo/PhotoRecipeOverlay';
export default async function AdminRecipePage({
params,
}: {
params: Promise<{ photoId: string }>
}) {
const { photoId } = await params;
const photo = await getPhoto(photoId);
const photosHidden = await getPhotos({ hidden: 'only' });
const { filmSimulation } = photo!;
const { fujifilmRecipe } = photosHidden[0];
return (
<SiteGrid
contentMain={photo && fujifilmRecipe && filmSimulation
? <PhotoRecipeOverlay
backgroundImageUrl={photo.url}
recipe={fujifilmRecipe}
simulation={filmSimulation}
/>
: <div>
Can&apos;t find photo/recipe
</div>}
/>
);
}

View File

@ -1,22 +1,7 @@
import SiteGrid from '@/components/SiteGrid';
import { getPhotos } from '@/photo/db/query';
import PhotoRecipeOverlay from '@/photo/PhotoRecipeOverlay';
import { redirect } from 'next/navigation';
export default async function AdminRecipePage() {
const photos = await getPhotos({ limit: 1});
const photosHidden = await getPhotos({ hidden: 'only' });
const { fujifilmRecipe, filmSimulation } = photosHidden[0];
return (
<SiteGrid
contentMain={photos[0] && fujifilmRecipe && filmSimulation
? <PhotoRecipeOverlay
backgroundImageUrl={photos[0].url}
recipe={fujifilmRecipe}
simulation={filmSimulation}
/>
: <div>
Can&apos;t find photo/recipe
</div>}
/>
);
redirect(`/admin/recipe/${photos[0].id}`);
}

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx/lite';
import { JSX, RefObject } from 'react';
import { HTMLAttributes, JSX, RefObject } from 'react';
/*
MAX WIDTHS
@ -18,6 +18,7 @@ export default function SiteGrid({
contentSide,
sideFirstOnMobile,
sideHiddenOnMobile,
...props
}: {
containerRef?: RefObject<HTMLDivElement | null>
className?: string
@ -25,9 +26,10 @@ export default function SiteGrid({
contentSide?: JSX.Element
sideFirstOnMobile?: boolean
sideHiddenOnMobile?: boolean
}) {
} & HTMLAttributes<HTMLDivElement>) {
return (
<div
{...props}
ref={containerRef}
className={clsx(
'grid',

View File

@ -58,7 +58,7 @@ export default function PhotoRecipe({
'component-surface shadow-xs',
'space-y-3',
)}>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<PhotoFilmSimulation {...{ simulation, className: 'grow' }} />
<div className="bg-dim border-medium rounded-md px-1">
<span>DR</span>
@ -66,7 +66,7 @@ export default function PhotoRecipe({
</div>
</div>
<div
className="uppercase space-y-3"
className="uppercase space-y-2"
>
<div>
{whiteBalanceFormatted.length <= 8 && 'AWB: '}
@ -80,17 +80,17 @@ export default function PhotoRecipe({
<span className="text-extra-dim">{')'}</span>
</>}
</div>
<div className="flex gap-3 *:w-full">
<div className="flex gap-2 *:w-full">
{renderDataSquare('Highlight', highlight)}
{renderDataSquare('Shadow', shadow)}
</div>
<div className="flex gap-3 *:w-full">
<div className="flex gap-2 *: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">
<div className="flex gap-2 *:w-full">
{renderDataSquare('Chrome', colorChromeEffect)}
{renderDataSquare('FX Blue', colorChromeFXBlue)}
</div>

View File

@ -5,7 +5,7 @@ import clsx from 'clsx/lite';
const addSign = (value = 0) => value < 0 ? value : `+${value}`;
export default function PhotoRecipe({
export default function PhotoRecipeFrostDark({
recipe: {
dynamicRange,
whiteBalance,

View File

@ -0,0 +1,150 @@
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import { FilmSimulation } from '@/simulation';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import clsx from 'clsx/lite';
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 PhotoRecipeFrostLight({
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') => (
<div className={clsx(
'flex flex-col items-center justify-center gap-0.5',
'bg-white/25 border border-white/20 rounded-md p-1',
)}>
<div>{typeof value === 'number' ? addSign(value) : value}</div>
<div className={clsx(
'text-[10px] leading-none tracking-wide font-medium text-black/50',
)}>
{label}
</div>
</div>
);
return <div className="flex gap-8">
<div className={clsx(
'w-[17rem] self-start',
'p-3',
'rounded-lg shadow-2xl',
'bg-white/60 backdrop-blur-xl border border-white/30',
'space-y-3',
'text-[13px] text-main',
'saturate-200',
)}>
<div className="flex items-center gap-2">
<PhotoFilmSimulation
contrast="high"
className="grow"
simulation={simulation}
/>
<div className="bg-white/60 rounded-md px-1">
<span>DR</span>
<span>{dynamicRange ?? 100}</span>
</div>
</div>
<div
className="uppercase space-y-2"
>
<div>
{whiteBalanceFormatted.length <= 8 && 'AWB: '}
{whiteBalanceFormatted}
{hasCustomizedWhiteBalance && <>
{' '}
<span className="text-extra-dim">{'('}</span>
R {addSign(whiteBalance?.red ?? 0)}
<span className="text-extra-dim">/</span>
B {addSign(whiteBalance?.blue ?? 0)}
<span className="text-extra-dim">{')'}</span>
</>}
</div>
<div className="flex gap-2 *:w-full">
{renderDataSquare('Highlight', highlight || random.highlight)}
{renderDataSquare('Shadow', shadow || random.shadow)}
</div>
<div className="flex gap-2 *:w-full">
{/* TODO: Confirm color vs saturation label */}
{renderDataSquare('Color', color || random.color)}
{renderDataSquare('Sharpness', sharpness || random.sharpness)}
{renderDataSquare('Clarity', clarity || random.clarity)}
</div>
<div className="flex gap-2 *:w-full">
{renderDataSquare('Color Chrome', colorChromeEffect)}
{renderDataSquare('FX Blue', colorChromeFXBlue)}
</div>
<div>
{highISONoiseReduction !== undefined
? <>
<span className="inline-flex min-w-[130px]">High ISO NR: </span>
<span>{addSign(highISONoiseReduction)}</span>
</>
: <>
<span>Noise Reduction: </span>
<span>{noiseReductionBasic}</span>
</>
}
</div>
{grainEffect &&
<div>
<span className="inline-flex min-w-[130px]">Grain:</span>
{grainEffect.roughness}
<span className="text-extra-dim">{' / '}</span>
{grainEffect.size}
</div>}
{hasBWAdjustments &&
<div>
<span className="inline-flex min-w-[130px]">BW Adjustment: </span>
{addSign(bwAdjustment)}
<span className="text-extra-dim">{' / '}</span>
MG: {addSign(bwMagentaGreen)}
</div>}
</div>
</div>
</div>;
}

View File

@ -4,7 +4,7 @@ import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import { FilmSimulation } from '@/simulation';
import clsx from 'clsx/lite';
import ImageLarge from '@/components/image/ImageLarge';
import PhotoRecipeFrost from './PhotoRecipeFrost';
import PhotoRecipeFrostLight from './PhotoRecipeFrostLight';
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
import { useState } from 'react';
import PhotoRecipe from './PhotoRecipe';
@ -43,7 +43,7 @@ export default function PhotoRecipeOverlay({
'flex items-center justify-center',
)}>
{isFrosted
? <PhotoRecipeFrost
? <PhotoRecipeFrostLight
recipe={recipe}
simulation={simulation}
/> : <PhotoRecipe