40 lines
1.6 KiB
TypeScript
40 lines
1.6 KiB
TypeScript
/**
|
||||
|
|
* Display size for a Giphy / Tenor link-preview GIF. The card used to force
|
|||
|
|
* every GIF into a full-width × 200px band with `object-fit: cover`, so square
|
|||
|
|
* and portrait GIFs lost most of their height and small ones were blown up.
|
|||
|
|
* Fit the GIF inside `maxWidth × maxHeight` keeping its aspect ratio, and
|
|||
|
|
* enlarge a small GIF at most `maxUpscale`× so it stays sharp.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
/** The GIF card's width (UrlPreview is 25rem); a phone shrinks it further via CSS. */
|
|||
|
|
export const GIF_PREVIEW_MAX_WIDTH = 400;
|
|||
|
|
export const GIF_PREVIEW_MAX_HEIGHT = 320;
|
|||
|
|
export const GIF_PREVIEW_MAX_UPSCALE = 2;
|
|||
|
|
|
|||
|
|
export type GifPreviewBox = { width: number; height: number };
|
|||
|
|
|
|||
|
|
// og:image:width / og:image:height are numbers from Synapse, but some sites
|
|||
|
|
// publish them as strings; anything else (missing, 0, NaN) means "unknown".
|
|||
|
|
const dimension = (value: unknown): number | undefined => {
|
|||
|
|
const n = typeof value === 'string' ? Number(value) : value;
|
|||
|
|
return typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : undefined;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** The box to render the GIF in, or `undefined` when its size isn't known. */
|
|||
|
|
export const gifPreviewBox = (
|
|||
|
|
rawWidth: unknown,
|
|||
|
|
rawHeight: unknown,
|
|||
|
|
maxWidth = GIF_PREVIEW_MAX_WIDTH,
|
|||
|
|
maxHeight = GIF_PREVIEW_MAX_HEIGHT,
|
|||
|
|
maxUpscale = GIF_PREVIEW_MAX_UPSCALE,
|
|||
|
|
): GifPreviewBox | undefined => {
|
|||
|
|
const width = dimension(rawWidth);
|
|||
|
|
const height = dimension(rawHeight);
|
|||
|
|
if (!width || !height) return undefined;
|
|||
|
|
const scale = Math.min(maxWidth / width, maxHeight / height, maxUpscale);
|
|||
|
|
return {
|
|||
|
|
width: Math.max(1, Math.round(width * scale)),
|
|||
|
|
height: Math.max(1, Math.round(height * scale)),
|
|||
|
|
};
|
|||
|
|
};
|