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}
|
value={value}
|
||||||
options={tagOptions}
|
options={tagOptions}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
showMenuOnDelete={tagOptionsLimit === 1}
|
||||||
className={clsx(Boolean(error) && 'error')}
|
className={clsx(Boolean(error) && 'error')}
|
||||||
readOnly={readOnly || pending || loading}
|
readOnly={readOnly || pending || loading}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export default function TagInput({
|
|||||||
value = '',
|
value = '',
|
||||||
options = [],
|
options = [],
|
||||||
onChange,
|
onChange,
|
||||||
|
showMenuOnDelete,
|
||||||
className,
|
className,
|
||||||
readOnly,
|
readOnly,
|
||||||
placeholder,
|
placeholder,
|
||||||
@ -26,6 +27,7 @@ export default function TagInput({
|
|||||||
value?: string
|
value?: string
|
||||||
options?: AnnotatedTag[]
|
options?: AnnotatedTag[]
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
|
showMenuOnDelete?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@ -202,7 +204,9 @@ export default function TagInput({
|
|||||||
case 'Backspace':
|
case 'Backspace':
|
||||||
if (inputText === '' && selectedOptions.length > 0) {
|
if (inputText === '' && selectedOptions.length > 0) {
|
||||||
removeOption(selectedOptions[selectedOptions.length - 1]);
|
removeOption(selectedOptions[selectedOptions.length - 1]);
|
||||||
hideMenu();
|
if (!showMenuOnDelete) {
|
||||||
|
hideMenu();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
@ -217,6 +221,7 @@ export default function TagInput({
|
|||||||
}, [
|
}, [
|
||||||
inputText,
|
inputText,
|
||||||
removeOption,
|
removeOption,
|
||||||
|
showMenuOnDelete,
|
||||||
hideMenu,
|
hideMenu,
|
||||||
selectedOptions,
|
selectedOptions,
|
||||||
selectedOptionIndex,
|
selectedOptionIndex,
|
||||||
|
|||||||
@ -53,9 +53,8 @@ export default function CameraImageResponse({
|
|||||||
marginRight: height * .015,
|
marginRight: height * .015,
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
}}>
|
title: formatCameraText(camera).toLocaleUpperCase(),
|
||||||
{formatCameraText(camera).toLocaleUpperCase()}
|
}} />
|
||||||
</ImageCaption>
|
|
||||||
</ImageContainer>
|
</ImageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,9 +41,8 @@ export default function FilmSimulationImageResponse({
|
|||||||
height={height * .081}
|
height={height * .081}
|
||||||
style={{ transform: `translateY(${height * .001}px)`}}
|
style={{ transform: `translateY(${height * .001}px)`}}
|
||||||
/>,
|
/>,
|
||||||
}}>
|
title: labelForFilmSimulation(simulation).medium.toLocaleUpperCase(),
|
||||||
{labelForFilmSimulation(simulation).medium.toLocaleUpperCase()}
|
}} />
|
||||||
</ImageCaption>
|
|
||||||
</ImageContainer>
|
</ImageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,9 +39,8 @@ export default function FocalLengthImageResponse({
|
|||||||
marginRight: height * .01,
|
marginRight: height * .01,
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
}}>
|
title: formatFocalLength(focal),
|
||||||
{formatFocalLength(focal)}
|
}} />
|
||||||
</ImageCaption>
|
|
||||||
</ImageContainer>
|
</ImageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,9 +25,12 @@ export default function HomeImageResponse({
|
|||||||
height,
|
height,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ImageCaption {...{ width, height, fontFamily }}>
|
<ImageCaption {...{
|
||||||
{SITE_DOMAIN_OR_TITLE}
|
width,
|
||||||
</ImageCaption>
|
height,
|
||||||
|
fontFamily,
|
||||||
|
title: SITE_DOMAIN_OR_TITLE,
|
||||||
|
}} />
|
||||||
</ImageContainer>
|
</ImageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,9 +47,8 @@ export default function PhotoImageResponse({
|
|||||||
...photo.make === 'Apple' && { icon: <AiFillApple style={{
|
...photo.make === 'Apple' && { icon: <AiFillApple style={{
|
||||||
marginRight: height * .01,
|
marginRight: height * .01,
|
||||||
}} /> },
|
}} /> },
|
||||||
}}>
|
title: caption,
|
||||||
{caption}
|
}} />}
|
||||||
</ImageCaption>}
|
|
||||||
</ImageContainer>
|
</ImageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,9 @@ import ImageContainer from './components/ImageContainer';
|
|||||||
import type { NextImageSize } from '@/platforms/next-image';
|
import type { NextImageSize } from '@/platforms/next-image';
|
||||||
import { formatTag } from '@/tag';
|
import { formatTag } from '@/tag';
|
||||||
import { TbChecklist } from 'react-icons/tb';
|
import { TbChecklist } from 'react-icons/tb';
|
||||||
|
import { generateRecipeText, getPhotoWithRecipeFromPhotos } from '@/recipe';
|
||||||
|
|
||||||
|
const MAX_RECIPE_LINES = 8;
|
||||||
|
|
||||||
export default function RecipeImageResponse({
|
export default function RecipeImageResponse({
|
||||||
recipe,
|
recipe,
|
||||||
@ -18,7 +21,22 @@ export default function RecipeImageResponse({
|
|||||||
width: NextImageSize
|
width: NextImageSize
|
||||||
height: number
|
height: number
|
||||||
fontFamily: string
|
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 (
|
return (
|
||||||
<ImageContainer solidBackground={photos.length === 0}>
|
<ImageContainer solidBackground={photos.length === 0}>
|
||||||
<ImagePhotoGrid
|
<ImagePhotoGrid
|
||||||
@ -26,12 +44,22 @@ export default function RecipeImageResponse({
|
|||||||
photos,
|
photos,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
gap: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
tw="flex absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(to right, rgba(0, 0, 0, .5) 40%, transparent 75%)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ImageCaption {...{
|
<ImageCaption {...{
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
|
legacyBottomAlignment: false,
|
||||||
|
gap: '0',
|
||||||
icon: <TbChecklist
|
icon: <TbChecklist
|
||||||
size={height * .087}
|
size={height * .087}
|
||||||
style={{
|
style={{
|
||||||
@ -39,8 +67,38 @@ export default function RecipeImageResponse({
|
|||||||
marginRight: height * .02,
|
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>
|
</ImageCaption>
|
||||||
</ImageContainer>
|
</ImageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -48,9 +48,8 @@ export default function TagImageResponse({
|
|||||||
marginRight: height * .02,
|
marginRight: height * .02,
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
}}>
|
title: formatTag(tag).toLocaleUpperCase(),
|
||||||
{formatTag(tag).toLocaleUpperCase()}
|
}} />
|
||||||
</ImageCaption>
|
|
||||||
</ImageContainer>
|
</ImageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,19 +7,26 @@ export default function ImageCaption({
|
|||||||
height,
|
height,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
icon,
|
icon,
|
||||||
|
title,
|
||||||
|
gap = '1rem', // Mimic mono font space metric
|
||||||
children,
|
children,
|
||||||
|
legacyBottomAlignment = OG_TEXT_BOTTOM_ALIGNMENT,
|
||||||
}: {
|
}: {
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
fontFamily: string
|
fontFamily: string
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
children: ReactNode
|
title?: string
|
||||||
|
gap?: string
|
||||||
|
children?: ReactNode
|
||||||
|
legacyBottomAlignment?: boolean
|
||||||
}) {
|
}) {
|
||||||
const paddingEdge = height * .07;
|
const paddingEdge = height * .07;
|
||||||
const paddingContent = height * .6;
|
const paddingContent = height * .6;
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
paddingLeft: height * .0875,
|
paddingLeft: height * .0875,
|
||||||
paddingRight: height * .0875,
|
paddingRight: height * .0875,
|
||||||
@ -27,11 +34,11 @@ export default function ImageCaption({
|
|||||||
backgroundBlendMode: 'multiply',
|
backgroundBlendMode: 'multiply',
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize: height *.08,
|
fontSize: height *.08,
|
||||||
gap: '1rem', // Mimic mono font space metric
|
gap,
|
||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
...OG_TEXT_BOTTOM_ALIGNMENT
|
...legacyBottomAlignment
|
||||||
? {
|
? {
|
||||||
paddingTop: paddingContent,
|
paddingTop: paddingContent,
|
||||||
paddingBottom: paddingEdge,
|
paddingBottom: paddingEdge,
|
||||||
@ -49,7 +56,7 @@ export default function ImageCaption({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: height * .034,
|
gap: height * .034,
|
||||||
...OG_TEXT_BOTTOM_ALIGNMENT
|
...legacyBottomAlignment
|
||||||
? { marginBottom: -height * .008 }
|
? { marginBottom: -height * .008 }
|
||||||
: { marginTop: -height * .008 },
|
: { marginTop: -height * .008 },
|
||||||
}}>
|
}}>
|
||||||
@ -63,9 +70,10 @@ export default function ImageCaption({
|
|||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,13 @@ import clsx from 'clsx/lite';
|
|||||||
import { ReactNode, RefObject } from 'react';
|
import { ReactNode, RefObject } from 'react';
|
||||||
import { IoCloseCircle } from 'react-icons/io5';
|
import { IoCloseCircle } from 'react-icons/io5';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { addSign, formatRecipe, formatWhiteBalance, RecipeProps } from '.';
|
import {
|
||||||
|
addSign,
|
||||||
|
formatNoiseReduction,
|
||||||
|
formatRecipe,
|
||||||
|
formatWhiteBalance,
|
||||||
|
RecipeProps,
|
||||||
|
} from '.';
|
||||||
|
|
||||||
export default function PhotoRecipeOverlay({
|
export default function PhotoRecipeOverlay({
|
||||||
ref,
|
ref,
|
||||||
@ -23,8 +29,6 @@ export default function PhotoRecipeOverlay({
|
|||||||
const {
|
const {
|
||||||
dynamicRange,
|
dynamicRange,
|
||||||
whiteBalance,
|
whiteBalance,
|
||||||
highISONoiseReduction,
|
|
||||||
noiseReductionBasic,
|
|
||||||
highlight,
|
highlight,
|
||||||
shadow,
|
shadow,
|
||||||
color,
|
color,
|
||||||
@ -118,7 +122,7 @@ export default function PhotoRecipeOverlay({
|
|||||||
'basis-2/3',
|
'basis-2/3',
|
||||||
)}
|
)}
|
||||||
{renderDataSquare(
|
{renderDataSquare(
|
||||||
highISONoiseReduction ?? noiseReductionBasic ?? 'OFF',
|
formatNoiseReduction(recipe),
|
||||||
'ISO NR',
|
'ISO NR',
|
||||||
'basis-1/3',
|
'basis-1/3',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
} from '@/utility/string';
|
} from '@/utility/string';
|
||||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||||
import { FilmSimulation } from '@/simulation';
|
import { FilmSimulation } from '@/simulation';
|
||||||
|
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation';
|
||||||
|
|
||||||
export type RecipeWithCount = {
|
export type RecipeWithCount = {
|
||||||
recipe: string
|
recipe: string
|
||||||
@ -53,6 +54,45 @@ export const descriptionForRecipePhotos = (
|
|||||||
explicitDateRange,
|
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 = (
|
export const generateMetaForRecipe = (
|
||||||
recipe: string,
|
recipe: string,
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
@ -96,3 +136,25 @@ export const formatWhiteBalance = ({ whiteBalance }: FujifilmRecipe) =>
|
|||||||
: whiteBalance.type
|
: whiteBalance.type
|
||||||
.replace(/auto./i, '')
|
.replace(/auto./i, '')
|
||||||
.replaceAll('-', ' ');
|
.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