Add support for CSS 'object-fit' property

This commit is contained in:
Corey Cahill 2022-06-16 13:42:23 -04:00
parent 6020386bbe
commit ef09e7ae86
5 changed files with 211 additions and 8 deletions

View File

@ -41,6 +41,7 @@ import {listStyleImage} from './property-descriptors/list-style-image';
import {listStylePosition} from './property-descriptors/list-style-position'; import {listStylePosition} from './property-descriptors/list-style-position';
import {listStyleType} from './property-descriptors/list-style-type'; import {listStyleType} from './property-descriptors/list-style-type';
import {marginBottom, marginLeft, marginRight, marginTop} from './property-descriptors/margin'; import {marginBottom, marginLeft, marginRight, marginTop} from './property-descriptors/margin';
import {objectFit} from './property-descriptors/object-fit';
import {overflow, OVERFLOW} from './property-descriptors/overflow'; import {overflow, OVERFLOW} from './property-descriptors/overflow';
import {overflowWrap} from './property-descriptors/overflow-wrap'; import {overflowWrap} from './property-descriptors/overflow-wrap';
import {paddingBottom, paddingLeft, paddingRight, paddingTop} from './property-descriptors/padding'; import {paddingBottom, paddingLeft, paddingRight, paddingTop} from './property-descriptors/padding';
@ -126,6 +127,7 @@ export class CSSParsedDeclaration {
marginRight: CSSValue; marginRight: CSSValue;
marginBottom: CSSValue; marginBottom: CSSValue;
marginLeft: CSSValue; marginLeft: CSSValue;
objectFit: ReturnType<typeof objectFit.parse>;
opacity: ReturnType<typeof opacity.parse>; opacity: ReturnType<typeof opacity.parse>;
overflowX: OVERFLOW; overflowX: OVERFLOW;
overflowY: OVERFLOW; overflowY: OVERFLOW;
@ -194,6 +196,7 @@ export class CSSParsedDeclaration {
this.marginRight = parse(context, marginRight, declaration.marginRight); this.marginRight = parse(context, marginRight, declaration.marginRight);
this.marginBottom = parse(context, marginBottom, declaration.marginBottom); this.marginBottom = parse(context, marginBottom, declaration.marginBottom);
this.marginLeft = parse(context, marginLeft, declaration.marginLeft); this.marginLeft = parse(context, marginLeft, declaration.marginLeft);
this.objectFit = parse(context, objectFit, declaration.objectFit);
this.opacity = parse(context, opacity, declaration.opacity); this.opacity = parse(context, opacity, declaration.opacity);
const overflowTuple = parse(context, overflow, declaration.overflow); const overflowTuple = parse(context, overflow, declaration.overflow);
this.overflowX = overflowTuple[0]; this.overflowX = overflowTuple[0];

View File

@ -0,0 +1,31 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export const enum OBJECT_FIT {
FILL = 'fill',
CONTAIN = 'contain',
COVER = 'cover',
NONE = 'none',
SCALE_DOWN = 'scale-down'
}
export const objectFit: IPropertyIdentValueDescriptor<OBJECT_FIT> = {
name: 'object-fit',
initialValue: 'fill',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (_context: Context, objectFit: string) => {
switch (objectFit) {
case 'contain':
return OBJECT_FIT.CONTAIN;
case 'cover':
return OBJECT_FIT.COVER;
case 'none':
return OBJECT_FIT.NONE;
case 'scale-down':
return OBJECT_FIT.SCALE_DOWN;
case 'fill':
default:
return OBJECT_FIT.FILL;
}
}
};

View File

@ -44,6 +44,7 @@ import {PAINT_ORDER_LAYER} from '../../css/property-descriptors/paint-order';
import {Renderer} from '../renderer'; import {Renderer} from '../renderer';
import {Context} from '../../core/context'; import {Context} from '../../core/context';
import {DIRECTION} from '../../css/property-descriptors/direction'; import {DIRECTION} from '../../css/property-descriptors/direction';
import {calculateObjectFitBounds} from '../object-fit';
export type RenderConfigurations = RenderOptions & { export type RenderConfigurations = RenderOptions & {
backgroundColor: Color | null; backgroundColor: Color | null;
@ -274,18 +275,25 @@ export class CanvasRenderer extends Renderer {
const box = contentBox(container); const box = contentBox(container);
const path = calculatePaddingBoxPath(curves); const path = calculatePaddingBoxPath(curves);
this.path(path); this.path(path);
const {src, dest} = calculateObjectFitBounds(
container.styles.objectFit,
container.intrinsicWidth,
container.intrinsicHeight,
box.width,
box.height
);
this.ctx.save(); this.ctx.save();
this.ctx.clip(); this.ctx.clip();
this.ctx.drawImage( this.ctx.drawImage(
image, image,
0, src.left,
0, src.top,
container.intrinsicWidth, src.width,
container.intrinsicHeight, src.height,
box.left, box.left + dest.left,
box.top, box.top + dest.top,
box.width, dest.width,
box.height dest.height
); );
this.ctx.restore(); this.ctx.restore();
} }

117
src/render/object-fit.ts Normal file
View File

@ -0,0 +1,117 @@
import {Bounds} from '../css/layout/bounds';
import {OBJECT_FIT} from '../css/property-descriptors/object-fit';
export const calculateObjectFitBounds = (
objectFit: OBJECT_FIT,
naturalWidth: number,
naturalHeight: number,
clientWidth: number,
clientHeight: number
): {src: Bounds; dest: Bounds} => {
const naturalRatio = naturalWidth / naturalHeight;
const clientRatio = clientWidth / clientHeight;
// 'object-position' is not currently supported, so use default value of 50% 50%.
const objectPositionX = 0.5;
const objectPositionY = 0.5;
let srcX: number,
srcY: number,
srcWidth: number,
srcHeight: number,
destX: number,
destY: number,
destWidth: number,
destHeight: number;
if (objectFit === OBJECT_FIT.SCALE_DOWN) {
objectFit =
naturalWidth < clientWidth && naturalHeight < clientHeight
? OBJECT_FIT.NONE // src is smaller on both axes
: OBJECT_FIT.CONTAIN; // at least one axes is greater or equal in size
}
switch (objectFit) {
case OBJECT_FIT.CONTAIN:
srcX = 0;
srcY = 0;
srcWidth = naturalWidth;
srcHeight = naturalHeight;
if (naturalRatio < clientRatio) {
// snap to top/bottom
destY = 0;
destHeight = clientHeight;
destWidth = destHeight * naturalRatio;
destX = (clientWidth - destWidth) * objectPositionX;
} else {
// snap to left/right
destX = 0;
destWidth = clientWidth;
destHeight = destWidth / naturalRatio;
destY = (clientHeight - destHeight) * objectPositionY;
}
break;
case OBJECT_FIT.COVER:
destX = 0;
destY = 0;
destWidth = clientWidth;
destHeight = clientHeight;
if (naturalRatio < clientRatio) {
// fill left/right
srcX = 0;
srcWidth = naturalWidth;
srcHeight = clientHeight * (naturalWidth / clientWidth);
srcY = (naturalHeight - srcHeight) * objectPositionY;
} else {
// fill top/bottom
srcY = 0;
srcHeight = naturalHeight;
srcWidth = clientWidth * (naturalHeight / clientHeight);
srcX = (naturalWidth - srcWidth) * objectPositionX;
}
break;
case OBJECT_FIT.NONE:
if (naturalWidth < clientWidth) {
srcX = 0;
srcWidth = naturalWidth;
destX = (clientWidth - naturalWidth) * objectPositionX;
destWidth = naturalWidth;
} else {
srcX = (naturalWidth - clientWidth) * objectPositionX;
srcWidth = clientWidth;
destX = 0;
destWidth = clientWidth;
}
if (naturalHeight < clientHeight) {
srcY = 0;
srcHeight = naturalHeight;
destY = (clientHeight - naturalHeight) * objectPositionY;
destHeight = naturalHeight;
} else {
srcY = (naturalHeight - clientHeight) * objectPositionY;
srcHeight = clientHeight;
destY = 0;
destHeight = clientHeight;
}
break;
case OBJECT_FIT.FILL:
default:
srcX = 0;
srcY = 0;
srcWidth = naturalWidth;
srcHeight = naturalHeight;
destX = 0;
destY = 0;
destWidth = clientWidth;
destHeight = clientHeight;
break;
}
return {
src: new Bounds(srcX, srcY, srcWidth, srcHeight),
dest: new Bounds(destX, destY, destWidth, destHeight)
};
};

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>Image tests for 'object-fit'</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
</head>
<body>
<!-- same size -->
<img src="../../assets/image.jpg" style="object-fit: contain;width:75px;height:75px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: cover;width:75px;height:75px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: fill;width:75px;height:75px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: none;width:75px;height:75px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: scale-down;width:75px;height:75px;border:5px solid black;" />
<!-- larger size -->
<img src="../../assets/image.jpg" style="object-fit: contain;width:250px;height:150px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: cover;width:250px;height:150px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: fill;width:250px;height:150px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: none;width:250px;height:150px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: scale-down;width:250px;height:150px;border:5px solid black;" />
<!-- larger width, smaller height -->
<img src="../../assets/image.jpg" style="object-fit: contain;width:250px;height:50px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: cover;width:250px;height:50px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: fill;width:250px;height:50px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: none;width:250px;height:50px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: scale-down;width:250px;height:50px;border:5px solid black;" />
<!-- smaller width, larger height -->
<img src="../../assets/image.jpg" style="object-fit: contain;width:50px;height:150px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: cover;width:50px;height:150px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: fill;width:50px;height:150px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: none;width:50px;height:150px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: scale-down;width:50px;height:150px;border:5px solid black;" />
<!-- smaller size -->
<img src="../../assets/image.jpg" style="object-fit: contain;width:60px;height:40px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: cover;width:60px;height:40px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: fill;width:60px;height:40px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: none;width:60px;height:40px;border:5px solid black;" />
<img src="../../assets/image.jpg" style="object-fit: scale-down;width:60px;height:40px;border:5px solid black;" />
</body>
</html>