diff --git a/fluxer_app/src/features/ui/utils/DimensionUtils.ts b/fluxer_app/src/features/ui/utils/DimensionUtils.ts index f1d9daabe..50039d487 100644 --- a/fluxer_app/src/features/ui/utils/DimensionUtils.ts +++ b/fluxer_app/src/features/ui/utils/DimensionUtils.ts @@ -23,15 +23,56 @@ interface DimensionResult { scale: number; } +export const MEDIA_MAX_WIDTH = 550; +export const MEDIA_MAX_HEIGHT = 350; + const DEFAULT_OPTIONS: Required = { - maxWidth: 550, - maxHeight: 400, + maxWidth: MEDIA_MAX_WIDTH, + maxHeight: MEDIA_MAX_HEIGHT, preserve: false, forceScale: false, aspectRatio: true, responsive: true, }; +export interface FitMediaInput extends MediaDimensions { + maxWidth: number; + maxHeight: number; + minWidth?: number; + minHeight?: number; +} + +export function fitMediaWithinBounds({ + width, + height, + maxWidth, + maxHeight, + minWidth = 0, + minHeight = 0, +}: FitMediaInput): MediaDimensions { + if (width === maxWidth && height === maxHeight) { + return {width, height}; + } + const widthRatio = width > maxWidth ? maxWidth / width : 1; + let fittedWidth = Math.max(Math.round(width * widthRatio), minWidth); + let fittedHeight = Math.max(Math.round(height * widthRatio), minHeight); + const heightRatio = fittedHeight > maxHeight ? maxHeight / fittedHeight : 1; + fittedWidth = Math.max(Math.round(fittedWidth * heightRatio), minWidth); + fittedHeight = Math.max(Math.round(fittedHeight * heightRatio), minHeight); + return {width: fittedWidth, height: fittedHeight}; +} + +export function mediaContainRatio({width, height, maxWidth, maxHeight}: FitMediaInput): number { + const widthRatio = width > maxWidth ? maxWidth / width : 1; + const scaledHeight = Math.round(height * widthRatio); + const heightRatio = scaledHeight > maxHeight ? maxHeight / scaledHeight : 1; + return Math.min(widthRatio * heightRatio, 1); +} + +export function mediaAspectRatioValue({width, height}: MediaDimensions): number { + return width / height; +} + export class MediaDimensionCalculator { private options: Required; @@ -43,100 +84,49 @@ export class MediaDimensionCalculator { const config = {...this.options, ...options}; const safeDimensions = this.normalizeDimensions(dimensions); if (config.preserve) { - return this.preserveDimensions(safeDimensions); + return this.preserveDimensions(safeDimensions, config); } - if (config.forceScale) { - return this.forceScaleDimensions(safeDimensions, config); - } - return this.calculateResponsiveDimensions(safeDimensions, config); + return this.containDimensions(safeDimensions, config); } - public calculateImage(dimensions: MediaDimensions, options?: Omit): DimensionResult { - return this.calculate(dimensions, {...options, forceScale: false}); + public calculateImage(dimensions: MediaDimensions, options?: DimensionOptions): DimensionResult { + return this.calculate(dimensions, options); } public calculateVideo(dimensions: MediaDimensions, options?: Omit): DimensionResult { return this.calculate(dimensions, {...options, preserve: false}); } - private preserveDimensions(dimensions: MediaDimensions): DimensionResult { + private preserveDimensions(dimensions: MediaDimensions, options: Required): DimensionResult { return { style: { width: remFromPx(dimensions.width), - aspectRatio: `${dimensions.width}/${dimensions.height}`, + maxWidth: '100%', + ...(options.aspectRatio && {aspectRatio: mediaAspectRatioValue(dimensions)}), }, dimensions: {...dimensions}, scale: 1, }; } - private forceScaleDimensions(dimensions: MediaDimensions, options: Required): DimensionResult { + private containDimensions(dimensions: MediaDimensions, options: Required): DimensionResult { const maxWidth = this.normalizeLimit(options.maxWidth, DEFAULT_OPTIONS.maxWidth); const maxHeight = this.normalizeLimit(options.maxHeight, DEFAULT_OPTIONS.maxHeight); - const scale = Math.min(1, maxWidth / dimensions.width, maxHeight / dimensions.height); - const scaledDimensions = { - width: Math.max(1, Math.round(dimensions.width * scale)), - height: Math.max(1, Math.round(dimensions.height * scale)), - }; + const fitted = fitMediaWithinBounds({...dimensions, maxWidth, maxHeight}); + const scale = mediaContainRatio({...dimensions, maxWidth, maxHeight}); return { style: { - width: `min(100%, ${remFromPx(scaledDimensions.width)})`, - height: 'auto', - maxWidth: '100%', - maxHeight: '100%', - ...(options.aspectRatio && { - aspectRatio: `${scaledDimensions.width}/${scaledDimensions.height}`, - }), + maxWidth: `min(100%, ${remFromPx(fitted.width)})`, + maxHeight: remFromPx(fitted.height), + width: options.responsive ? '100%' : `min(100%, ${remFromPx(fitted.width)})`, + display: 'block', + ...(options.aspectRatio && {aspectRatio: mediaAspectRatioValue(fitted)}), }, - dimensions: scaledDimensions, + dimensions: fitted, scale, }; } - private calculateResponsiveDimensions( - dimensions: MediaDimensions, - options: Required, - ): DimensionResult { - const maxWidth = this.normalizeLimit(options.maxWidth, DEFAULT_OPTIONS.maxWidth); - const maxHeight = this.normalizeLimit(options.maxHeight, DEFAULT_OPTIONS.maxHeight); - const isPortrait = dimensions.height > dimensions.width; - let style: CSSProperties; - let scaledDimensions: MediaDimensions; - let scale: number; - if (isPortrait) { - const targetWidth = Math.max(1, Math.round((maxHeight * dimensions.width) / dimensions.height)); - const maxAllowedWidth = Math.max(1, Math.min(dimensions.width, maxWidth, targetWidth)); - scale = maxAllowedWidth / dimensions.width; - scaledDimensions = { - width: maxAllowedWidth, - height: Math.max(1, Math.round(dimensions.height * scale)), - }; - style = { - maxWidth: `min(100%, ${remFromPx(maxAllowedWidth)})`, - width: options.responsive ? '100%' : `min(100%, ${remFromPx(maxAllowedWidth)})`, - ...(options.aspectRatio && { - aspectRatio: `${scaledDimensions.width}/${scaledDimensions.height}`, - }), - }; - } else { - const scaleByWidth = maxWidth / dimensions.width; - const scaleByHeight = maxHeight / dimensions.height; - scale = Math.min(1, scaleByWidth, scaleByHeight); - scaledDimensions = { - width: Math.max(1, Math.round(dimensions.width * scale)), - height: Math.max(1, Math.round(dimensions.height * scale)), - }; - style = { - maxWidth: `min(100%, ${remFromPx(maxWidth)})`, - width: options.responsive ? '100%' : `min(100%, ${remFromPx(scaledDimensions.width)})`, - ...(options.aspectRatio && { - aspectRatio: `${scaledDimensions.width}/${scaledDimensions.height}`, - }), - }; - } - return {style, dimensions: scaledDimensions, scale}; - } - private normalizeDimensions(dimensions: MediaDimensions): MediaDimensions { return { width: this.normalizeLimit(dimensions.width, 1), @@ -157,9 +147,8 @@ export class MediaDimensionCalculator { maxWidth = DEFAULT_OPTIONS.maxWidth, maxHeight = DEFAULT_OPTIONS.maxHeight, ): [number, number] { - const calculator = new MediaDimensionCalculator(); - const result = calculator.calculate({width, height}, {maxWidth, maxHeight, forceScale: true}); - return [result.dimensions.width, result.dimensions.height]; + const fitted = fitMediaWithinBounds({width, height, maxWidth, maxHeight}); + return [fitted.width, fitted.height]; } }