mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(app): contain embedded media inside its box instead of overflowing (#1871)
This commit is contained in:
1 parent
5834e32aa5
commit
eda29c0e34
1 file changed
+61
-72
@@ -23,15 +23,56 @@ interface DimensionResult {
|
|||||||
scale: number;
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MEDIA_MAX_WIDTH = 550;
|
||||||
|
export const MEDIA_MAX_HEIGHT = 350;
|
||||||
|
|
||||||
const DEFAULT_OPTIONS: Required<DimensionOptions> = {
|
const DEFAULT_OPTIONS: Required<DimensionOptions> = {
|
||||||
maxWidth: 550,
|
maxWidth: MEDIA_MAX_WIDTH,
|
||||||
maxHeight: 400,
|
maxHeight: MEDIA_MAX_HEIGHT,
|
||||||
preserve: false,
|
preserve: false,
|
||||||
forceScale: false,
|
forceScale: false,
|
||||||
aspectRatio: true,
|
aspectRatio: true,
|
||||||
responsive: 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 {
|
export class MediaDimensionCalculator {
|
||||||
private options: Required<DimensionOptions>;
|
private options: Required<DimensionOptions>;
|
||||||
|
|
||||||
@@ -43,100 +84,49 @@ export class MediaDimensionCalculator {
|
|||||||
const config = {...this.options, ...options};
|
const config = {...this.options, ...options};
|
||||||
const safeDimensions = this.normalizeDimensions(dimensions);
|
const safeDimensions = this.normalizeDimensions(dimensions);
|
||||||
if (config.preserve) {
|
if (config.preserve) {
|
||||||
return this.preserveDimensions(safeDimensions);
|
return this.preserveDimensions(safeDimensions, config);
|
||||||
}
|
}
|
||||||
if (config.forceScale) {
|
return this.containDimensions(safeDimensions, config);
|
||||||
return this.forceScaleDimensions(safeDimensions, config);
|
|
||||||
}
|
|
||||||
return this.calculateResponsiveDimensions(safeDimensions, config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public calculateImage(dimensions: MediaDimensions, options?: Omit<DimensionOptions, 'forceScale'>): DimensionResult {
|
public calculateImage(dimensions: MediaDimensions, options?: DimensionOptions): DimensionResult {
|
||||||
return this.calculate(dimensions, {...options, forceScale: false});
|
return this.calculate(dimensions, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public calculateVideo(dimensions: MediaDimensions, options?: Omit<DimensionOptions, 'preserve'>): DimensionResult {
|
public calculateVideo(dimensions: MediaDimensions, options?: Omit<DimensionOptions, 'preserve'>): DimensionResult {
|
||||||
return this.calculate(dimensions, {...options, preserve: false});
|
return this.calculate(dimensions, {...options, preserve: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
private preserveDimensions(dimensions: MediaDimensions): DimensionResult {
|
private preserveDimensions(dimensions: MediaDimensions, options: Required<DimensionOptions>): DimensionResult {
|
||||||
return {
|
return {
|
||||||
style: {
|
style: {
|
||||||
width: remFromPx(dimensions.width),
|
width: remFromPx(dimensions.width),
|
||||||
aspectRatio: `${dimensions.width}/${dimensions.height}`,
|
maxWidth: '100%',
|
||||||
|
...(options.aspectRatio && {aspectRatio: mediaAspectRatioValue(dimensions)}),
|
||||||
},
|
},
|
||||||
dimensions: {...dimensions},
|
dimensions: {...dimensions},
|
||||||
scale: 1,
|
scale: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private forceScaleDimensions(dimensions: MediaDimensions, options: Required<DimensionOptions>): DimensionResult {
|
private containDimensions(dimensions: MediaDimensions, options: Required<DimensionOptions>): DimensionResult {
|
||||||
const maxWidth = this.normalizeLimit(options.maxWidth, DEFAULT_OPTIONS.maxWidth);
|
const maxWidth = this.normalizeLimit(options.maxWidth, DEFAULT_OPTIONS.maxWidth);
|
||||||
const maxHeight = this.normalizeLimit(options.maxHeight, DEFAULT_OPTIONS.maxHeight);
|
const maxHeight = this.normalizeLimit(options.maxHeight, DEFAULT_OPTIONS.maxHeight);
|
||||||
const scale = Math.min(1, maxWidth / dimensions.width, maxHeight / dimensions.height);
|
const fitted = fitMediaWithinBounds({...dimensions, maxWidth, maxHeight});
|
||||||
const scaledDimensions = {
|
const scale = mediaContainRatio({...dimensions, maxWidth, maxHeight});
|
||||||
width: Math.max(1, Math.round(dimensions.width * scale)),
|
|
||||||
height: Math.max(1, Math.round(dimensions.height * scale)),
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
style: {
|
style: {
|
||||||
width: `min(100%, ${remFromPx(scaledDimensions.width)})`,
|
maxWidth: `min(100%, ${remFromPx(fitted.width)})`,
|
||||||
height: 'auto',
|
maxHeight: remFromPx(fitted.height),
|
||||||
maxWidth: '100%',
|
width: options.responsive ? '100%' : `min(100%, ${remFromPx(fitted.width)})`,
|
||||||
maxHeight: '100%',
|
display: 'block',
|
||||||
...(options.aspectRatio && {
|
...(options.aspectRatio && {aspectRatio: mediaAspectRatioValue(fitted)}),
|
||||||
aspectRatio: `${scaledDimensions.width}/${scaledDimensions.height}`,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
dimensions: scaledDimensions,
|
dimensions: fitted,
|
||||||
scale,
|
scale,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateResponsiveDimensions(
|
|
||||||
dimensions: MediaDimensions,
|
|
||||||
options: Required<DimensionOptions>,
|
|
||||||
): 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 {
|
private normalizeDimensions(dimensions: MediaDimensions): MediaDimensions {
|
||||||
return {
|
return {
|
||||||
width: this.normalizeLimit(dimensions.width, 1),
|
width: this.normalizeLimit(dimensions.width, 1),
|
||||||
@@ -157,9 +147,8 @@ export class MediaDimensionCalculator {
|
|||||||
maxWidth = DEFAULT_OPTIONS.maxWidth,
|
maxWidth = DEFAULT_OPTIONS.maxWidth,
|
||||||
maxHeight = DEFAULT_OPTIONS.maxHeight,
|
maxHeight = DEFAULT_OPTIONS.maxHeight,
|
||||||
): [number, number] {
|
): [number, number] {
|
||||||
const calculator = new MediaDimensionCalculator();
|
const fitted = fitMediaWithinBounds({width, height, maxWidth, maxHeight});
|
||||||
const result = calculator.calculate({width, height}, {maxWidth, maxHeight, forceScale: true});
|
return [fitted.width, fitted.height];
|
||||||
return [result.dimensions.width, result.dimensions.height];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user