Add recipe details to OG images
This commit is contained in:
parent
6be19f4979
commit
19aeaf4ef3
@ -134,6 +134,7 @@ export default function FieldSetWithStatus({
|
||||
value={value}
|
||||
options={tagOptions}
|
||||
onChange={onChange}
|
||||
showMenuOnDelete={tagOptionsLimit === 1}
|
||||
className={clsx(Boolean(error) && 'error')}
|
||||
readOnly={readOnly || pending || loading}
|
||||
placeholder={placeholder}
|
||||
|
||||
@ -15,6 +15,7 @@ export default function TagInput({
|
||||
value = '',
|
||||
options = [],
|
||||
onChange,
|
||||
showMenuOnDelete,
|
||||
className,
|
||||
readOnly,
|
||||
placeholder,
|
||||
@ -26,6 +27,7 @@ export default function TagInput({
|
||||
value?: string
|
||||
options?: AnnotatedTag[]
|
||||
onChange?: (value: string) => void
|
||||
showMenuOnDelete?: boolean
|
||||
className?: string
|
||||
readOnly?: boolean
|
||||
placeholder?: string
|
||||
@ -202,7 +204,9 @@ export default function TagInput({
|
||||
case 'Backspace':
|
||||
if (inputText === '' && selectedOptions.length > 0) {
|
||||
removeOption(selectedOptions[selectedOptions.length - 1]);
|
||||
hideMenu();
|
||||
if (!showMenuOnDelete) {
|
||||
hideMenu();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
@ -217,6 +221,7 @@ export default function TagInput({
|
||||
}, [
|
||||
inputText,
|
||||
removeOption,
|
||||
showMenuOnDelete,
|
||||
hideMenu,
|
||||
selectedOptions,
|
||||
selectedOptionIndex,
|
||||
|
||||
@ -53,9 +53,8 @@ export default function CameraImageResponse({
|
||||
marginRight: height * .015,
|
||||
}}
|
||||
/>,
|
||||
}}>
|
||||
{formatCameraText(camera).toLocaleUpperCase()}
|
||||
</ImageCaption>
|
||||
title: formatCameraText(camera).toLocaleUpperCase(),
|
||||
}} />
|
||||
</ImageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -41,9 +41,8 @@ export default function FilmSimulationImageResponse({
|
||||
height={height * .081}
|
||||
style={{ transform: `translateY(${height * .001}px)`}}
|
||||
/>,
|
||||
}}>
|
||||
{labelForFilmSimulation(simulation).medium.toLocaleUpperCase()}
|
||||
</ImageCaption>
|
||||
title: labelForFilmSimulation(simulation).medium.toLocaleUpperCase(),
|
||||
}} />
|
||||
</ImageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -39,9 +39,8 @@ export default function FocalLengthImageResponse({
|
||||
marginRight: height * .01,
|
||||
}}
|
||||
/>,
|
||||
}}>
|
||||
{formatFocalLength(focal)}
|
||||
</ImageCaption>
|
||||
title: formatFocalLength(focal),
|
||||
}} />
|
||||
</ImageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -25,9 +25,12 @@ export default function HomeImageResponse({
|
||||
height,
|
||||
}}
|
||||
/>
|
||||
<ImageCaption {...{ width, height, fontFamily }}>
|
||||
{SITE_DOMAIN_OR_TITLE}
|
||||
</ImageCaption>
|
||||
<ImageCaption {...{
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
title: SITE_DOMAIN_OR_TITLE,
|
||||
}} />
|
||||
</ImageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -47,9 +47,8 @@ export default function PhotoImageResponse({
|
||||
...photo.make === 'Apple' && { icon: <AiFillApple style={{
|
||||
marginRight: height * .01,
|
||||
}} /> },
|
||||
}}>
|
||||
{caption}
|
||||
</ImageCaption>}
|
||||
title: caption,
|
||||
}} />}
|
||||
</ImageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,6 +5,9 @@ import ImageContainer from './components/ImageContainer';
|
||||
import type { NextImageSize } from '@/platforms/next-image';
|
||||
import { formatTag } from '@/tag';
|
||||
import { TbChecklist } from 'react-icons/tb';
|
||||
import { generateRecipeText, getPhotoWithRecipeFromPhotos } from '@/recipe';
|
||||
|
||||
const MAX_RECIPE_LINES = 8;
|
||||
|
||||
export default function RecipeImageResponse({
|
||||
recipe,
|
||||
@ -18,7 +21,22 @@ export default function RecipeImageResponse({
|
||||
width: NextImageSize
|
||||
height: number
|
||||
fontFamily: string
|
||||
}) {
|
||||
}) {
|
||||
const photo = getPhotoWithRecipeFromPhotos(photos);
|
||||
|
||||
let recipeLines = photo?.recipeData && photo.filmSimulation
|
||||
? generateRecipeText({
|
||||
recipe: photo.recipeData,
|
||||
simulation: photo.filmSimulation!,
|
||||
iso: photo.iso!.toString(),
|
||||
})
|
||||
: [];
|
||||
|
||||
if (recipeLines && recipeLines.length > MAX_RECIPE_LINES) {
|
||||
recipeLines = recipeLines.slice(0, MAX_RECIPE_LINES);
|
||||
recipeLines[MAX_RECIPE_LINES - 1] = '•••';
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageContainer solidBackground={photos.length === 0}>
|
||||
<ImagePhotoGrid
|
||||
@ -26,12 +44,22 @@ export default function RecipeImageResponse({
|
||||
photos,
|
||||
width,
|
||||
height,
|
||||
gap: 0,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
tw="flex absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to right, rgba(0, 0, 0, .5) 40%, transparent 75%)',
|
||||
}}
|
||||
/>
|
||||
<ImageCaption {...{
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
legacyBottomAlignment: false,
|
||||
gap: '0',
|
||||
icon: <TbChecklist
|
||||
size={height * .087}
|
||||
style={{
|
||||
@ -39,8 +67,38 @@ export default function RecipeImageResponse({
|
||||
marginRight: height * .02,
|
||||
}}
|
||||
/>,
|
||||
title: formatTag(recipe).toLocaleUpperCase(),
|
||||
}}>
|
||||
{formatTag(recipe).toLocaleUpperCase()}
|
||||
{photo?.recipeData &&
|
||||
<div
|
||||
tw="opacity-60"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingTop: height * .03,
|
||||
lineHeight: 1.22,
|
||||
}}
|
||||
>
|
||||
{recipeLines.map(text => (
|
||||
<div tw="flex" key={text}>
|
||||
<div
|
||||
tw="flex"
|
||||
style={{
|
||||
width: height * .141,
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>}
|
||||
</ImageCaption>
|
||||
</ImageContainer>
|
||||
);
|
||||
|
||||
@ -48,9 +48,8 @@ export default function TagImageResponse({
|
||||
marginRight: height * .02,
|
||||
}}
|
||||
/>,
|
||||
}}>
|
||||
{formatTag(tag).toLocaleUpperCase()}
|
||||
</ImageCaption>
|
||||
title: formatTag(tag).toLocaleUpperCase(),
|
||||
}} />
|
||||
</ImageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,19 +7,26 @@ export default function ImageCaption({
|
||||
height,
|
||||
fontFamily,
|
||||
icon,
|
||||
title,
|
||||
gap = '1rem', // Mimic mono font space metric
|
||||
children,
|
||||
legacyBottomAlignment = OG_TEXT_BOTTOM_ALIGNMENT,
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
fontFamily: string
|
||||
icon?: ReactNode
|
||||
children: ReactNode
|
||||
title?: string
|
||||
gap?: string
|
||||
children?: ReactNode
|
||||
legacyBottomAlignment?: boolean
|
||||
}) {
|
||||
const paddingEdge = height * .07;
|
||||
const paddingContent = height * .6;
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'absolute',
|
||||
paddingLeft: height * .0875,
|
||||
paddingRight: height * .0875,
|
||||
@ -27,11 +34,11 @@ export default function ImageCaption({
|
||||
backgroundBlendMode: 'multiply',
|
||||
fontFamily,
|
||||
fontSize: height *.08,
|
||||
gap: '1rem', // Mimic mono font space metric
|
||||
gap,
|
||||
lineHeight: 1.2,
|
||||
left: 0,
|
||||
right: 0,
|
||||
...OG_TEXT_BOTTOM_ALIGNMENT
|
||||
...legacyBottomAlignment
|
||||
? {
|
||||
paddingTop: paddingContent,
|
||||
paddingBottom: paddingEdge,
|
||||
@ -49,7 +56,7 @@ export default function ImageCaption({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: height * .034,
|
||||
...OG_TEXT_BOTTOM_ALIGNMENT
|
||||
...legacyBottomAlignment
|
||||
? { marginBottom: -height * .008 }
|
||||
: { marginTop: -height * .008 },
|
||||
}}>
|
||||
@ -63,9 +70,10 @@ export default function ImageCaption({
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,7 +6,13 @@ import clsx from 'clsx/lite';
|
||||
import { ReactNode, RefObject } from 'react';
|
||||
import { IoCloseCircle } from 'react-icons/io5';
|
||||
import { motion } from 'framer-motion';
|
||||
import { addSign, formatRecipe, formatWhiteBalance, RecipeProps } from '.';
|
||||
import {
|
||||
addSign,
|
||||
formatNoiseReduction,
|
||||
formatRecipe,
|
||||
formatWhiteBalance,
|
||||
RecipeProps,
|
||||
} from '.';
|
||||
|
||||
export default function PhotoRecipeOverlay({
|
||||
ref,
|
||||
@ -23,8 +29,6 @@ export default function PhotoRecipeOverlay({
|
||||
const {
|
||||
dynamicRange,
|
||||
whiteBalance,
|
||||
highISONoiseReduction,
|
||||
noiseReductionBasic,
|
||||
highlight,
|
||||
shadow,
|
||||
color,
|
||||
@ -118,7 +122,7 @@ export default function PhotoRecipeOverlay({
|
||||
'basis-2/3',
|
||||
)}
|
||||
{renderDataSquare(
|
||||
highISONoiseReduction ?? noiseReductionBasic ?? 'OFF',
|
||||
formatNoiseReduction(recipe),
|
||||
'ISO NR',
|
||||
'basis-1/3',
|
||||
)}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from '@/utility/string';
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation';
|
||||
|
||||
export type RecipeWithCount = {
|
||||
recipe: string
|
||||
@ -53,6 +54,45 @@ export const descriptionForRecipePhotos = (
|
||||
explicitDateRange,
|
||||
);
|
||||
|
||||
export const generateRecipeText = ({
|
||||
recipe,
|
||||
simulation,
|
||||
}: RecipeProps) => {
|
||||
const lines = [
|
||||
`${labelForFilmSimulation(simulation).large.toLocaleUpperCase()}`,
|
||||
`DR${recipe.dynamicRange.development} NR${formatNoiseReduction(recipe)}`,
|
||||
// eslint-disable-next-line max-len
|
||||
`${formatWhiteBalance(recipe).toLocaleUpperCase()} ${formatWhiteBalanceColor(recipe)}`,
|
||||
];
|
||||
|
||||
if (recipe.highlight || recipe.shadow) {
|
||||
// eslint-disable-next-line max-len
|
||||
lines.push(`HIGH/SHAD ${addSign(recipe.highlight)}/${addSign(recipe.shadow)}`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
lines.push(`COL${addSign(recipe.color)} SHARP${addSign(recipe.sharpness)} CLAR${addSign(recipe.clarity)}`);
|
||||
|
||||
if (recipe.colorChromeEffect) {
|
||||
lines.push(`CHROME ${recipe.colorChromeEffect.toLocaleUpperCase()}`);
|
||||
}
|
||||
|
||||
if (recipe.colorChromeFXBlue) {
|
||||
lines.push(`FX BLUE ${recipe.colorChromeFXBlue.toLocaleUpperCase()}`);
|
||||
}
|
||||
|
||||
if (recipe.grainEffect.roughness !== 'off') {
|
||||
lines.push(`GRAIN ${formatGrain(recipe)}`);
|
||||
}
|
||||
|
||||
if (recipe.bwAdjustment || recipe.bwMagentaGreen) {
|
||||
// eslint-disable-next-line max-len
|
||||
lines.push(`BW ADJ ${addSign(recipe.bwAdjustment)} BW M/G ${addSign(recipe.bwMagentaGreen)}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
|
||||
export const generateMetaForRecipe = (
|
||||
recipe: string,
|
||||
photos: Photo[],
|
||||
@ -96,3 +136,25 @@ export const formatWhiteBalance = ({ whiteBalance }: FujifilmRecipe) =>
|
||||
: whiteBalance.type
|
||||
.replace(/auto./i, '')
|
||||
.replaceAll('-', ' ');
|
||||
|
||||
export const formatWhiteBalanceColor = ({
|
||||
whiteBalance: { red, blue },
|
||||
}: FujifilmRecipe) =>
|
||||
(red || blue)
|
||||
? `(R${addSign(red)}/B${addSign(blue)})`
|
||||
: '';
|
||||
|
||||
export const formatGrain = ({ grainEffect }: FujifilmRecipe) =>
|
||||
grainEffect.roughness === 'off'
|
||||
? 'OFF'
|
||||
: grainEffect.roughness === 'weak'
|
||||
? `WEAK/${grainEffect.size.toLocaleUpperCase()}`
|
||||
: `STRONG/${grainEffect.size.toLocaleUpperCase()}`;
|
||||
|
||||
export const formatNoiseReduction = ({
|
||||
highISONoiseReduction,
|
||||
noiseReductionBasic,
|
||||
}: FujifilmRecipe) =>
|
||||
highISONoiseReduction
|
||||
? addSign(highISONoiseReduction)
|
||||
: noiseReductionBasic ?? 'OFF';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user