object-fit: 获取结果尺寸

26

当使用新的CSS特性object-fit时,如何通过JavaScript访问浏览器选择的结果尺寸?

假设foo.jpg是100x200像素。浏览器页面/视口宽度为400px,高度为300px。然后给定以下CSS代码:

img.foo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: 25% 0;
}
浏览器现在会在左侧第二个四分之一的底部以正确的纵横比显示图像。这导致图像尺寸如下:
  • 宽度:150px
  • 高度:300px
  • 左边:62.5px
  • 右边:212.5px
那么,哪个JavaScript调用(允许使用jQuery)会给我手动计算出的这些数字呢? (注意:JavaScript本身并不知道CSS信息,因为用户可能会覆盖它们,甚至添加类似min-width的内容)
为了试验代码,我创建了一个fiddle:https://jsfiddle.net/sydeo244/

如果您使用 $('img.foo').outerWidth(),您将获得元素的总计算宽度。不清楚您在询问什么。 - Marcos Pérez Gude
不,outerWidth()在这种情况下不起作用,因为它会导致“完整”图像的宽度 - 因此由于width:100%而返回400,而不是所需的150 - Chris
没有特殊的属性可以直接给你这个值,你需要利用img元素生成的大小,然后使用naturalWidth/naturalHeight属性获取其宽高比,然后计算其渲染大小(基本上与您手动计算的方式相同)。 - Asons
4个回答

20
感谢 @bfred,我不需要编写最初的方法。
这里是他的一个扩展(并重新编写)版本,它也可以计算 object-position 值。

function getRenderedSize(contains, cWidth, cHeight, width, height, pos){
  var oRatio = width / height,
      cRatio = cWidth / cHeight;
  return function() {
    if (contains ? (oRatio > cRatio) : (oRatio < cRatio)) {
      this.width = cWidth;
      this.height = cWidth / oRatio;
    } else {
      this.width = cHeight * oRatio;
      this.height = cHeight;
    }      
    this.left = (cWidth - this.width)*(pos/100);
    this.right = this.width + this.left;
    return this;
  }.call({});
}

function getImgSizeInfo(img) {
  var pos = window.getComputedStyle(img).getPropertyValue('object-position').split(' ');
  return getRenderedSize(true,
                         img.width,
                         img.height,
                         img.naturalWidth,
                         img.naturalHeight,
                         parseInt(pos[0]));
}

document.querySelector('#foo').addEventListener('load', function(e) {
  console.log(getImgSizeInfo(e.target));
});
#container {
  width: 400px;
  height: 300px;
  border: 1px solid blue;
}

#foo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: 25% 0;
}
<div id="container">
  <img id="foo" src="http://dummyimage.com/100x200/000/fff.jpg"/>
</div>

小贴士

似乎 object-position 可以有多于2个的值,当需要时,您需要调整(或添加)哪个参数返回左侧位置值


手动“压缩”会使代码难以阅读;让uglifyjs只为浏览器完成这项艰巨的工作。实际上,uglifyjs比手动压缩的代码更加紧凑。(146字节对179字节) - fregante
@bfred.it 我同意你的看法...稍微“解压”一下 :) - Asons
谢谢,我希望有一个API是我不知道的。但是答案很明确,它不存在(还没有?)。由于您的答案完成了任务,因此它是正确的,我接受了它。 - Chris

14

有一个叫做 intrinsic-scale 的 npm 包可以计算这个值,但它不支持等效的 object-positionhttps://www.npmjs.com/package/intrinsic-scale

以下是完整代码:

// adapted from: https://www.npmjs.com/package/intrinsic-scale
function getObjectFitSize(contains /* true = contain, false = cover */, containerWidth, containerHeight, width, height){
    var doRatio = width / height;
    var cRatio = containerWidth / containerHeight;
    var targetWidth = 0;
    var targetHeight = 0;
    var test = contains ? (doRatio > cRatio) : (doRatio < cRatio);

    if (test) {
        targetWidth = containerWidth;
        targetHeight = targetWidth / doRatio;
    } else {
        targetHeight = containerHeight;
        targetWidth = targetHeight * doRatio;
    }

    return {
        width: targetWidth,
        height: targetHeight,
        x: (containerWidth - targetWidth) / 2,
        y: (containerHeight - targetHeight) / 2
    };
}

使用方法如下:

getObjectFitSize(true, img.width, img.height, img.naturalWidth, img.naturalHeight);

非常好的答案。顺便说一下,您可以通过以下方式对画布进行类似的调用(这种情况比img要少得多,但这正是我所需要的):getObjectFitSize(true, c.scrollWidth, c.scrollHeight, c.width, c.height); 其中“c”是画布。画布很奇怪,因为宽度和高度实际上是“自然”尺寸,scrollWidth和scrollHeight只是一个技巧,用于获取实际高度和宽度(因为高度和宽度被覆盖以表示“自然”高度和宽度)。 - DoomGoober

4

这里是一个更全面的算法,经过测试,用于确定图像在屏幕上的显示方式。

var imageComputedStyle = window.getComputedStyle(image);
var imageObjectFit = imageComputedStyle.getPropertyValue("object-fit");
coordinates = {};
var imagePositions = imageComputedStyle.getPropertyValue("object-position").split(" ");
var horizontalPercentage = parseInt(imagePositions[0]) / 100;
var verticalPercentage = parseInt(imagePositions[1]) / 100;
var naturalRatio = image.naturalWidth / image.naturalHeight;
var visibleRatio = image.width / image.height;
if (imageObjectFit === "none")
{
  coordinates.sourceWidth = image.width;
  coordinates.sourceHeight = image.height;
  coordinates.sourceX = (image.naturalWidth - image.width) * horizontalPercentage;
  coordinates.sourceY = (image.naturalHeight - image.height) * verticalPercentage;
  coordinates.destinationWidthPercentage = 1;
  coordinates.destinationHeightPercentage = 1;
  coordinates.destinationXPercentage = 0;
  coordinates.destinationYPercentage = 0;
}
else if (imageObjectFit === "contain" || imageObjectFit === "scale-down")
{
  // TODO: handle the "scale-down" appropriately, once its meaning will be clear
  coordinates.sourceWidth = image.naturalWidth;
  coordinates.sourceHeight = image.naturalHeight;
  coordinates.sourceX = 0;
  coordinates.sourceY = 0;
  if (naturalRatio > visibleRatio)
  {
    coordinates.destinationWidthPercentage = 1;
    coordinates.destinationHeightPercentage = (image.naturalHeight / image.height) / (image.naturalWidth / image.width);
    coordinates.destinationXPercentage = 0;
    coordinates.destinationYPercentage = (1 - coordinates.destinationHeightPercentage) * verticalPercentage;
  }
  else
  {
    coordinates.destinationWidthPercentage = (image.naturalWidth / image.width) / (image.naturalHeight / image.height);
    coordinates.destinationHeightPercentage = 1;
    coordinates.destinationXPercentage = (1 - coordinates.destinationWidthPercentage) * horizontalPercentage;
    coordinates.destinationYPercentage = 0;
  }
}
else if (imageObjectFit === "cover")
{
  if (naturalRatio > visibleRatio)
  {
    coordinates.sourceWidth = image.naturalHeight * visibleRatio;
    coordinates.sourceHeight = image.naturalHeight;
    coordinates.sourceX = (image.naturalWidth - coordinates.sourceWidth) * horizontalPercentage;
    coordinates.sourceY = 0;
  }
  else
  {
    coordinates.sourceWidth = image.naturalWidth;
    coordinates.sourceHeight = image.naturalWidth / visibleRatio;
    coordinates.sourceX = 0;
    coordinates.sourceY = (image.naturalHeight - coordinates.sourceHeight) * verticalPercentage;
  }
  coordinates.destinationWidthPercentage = 1;
  coordinates.destinationHeightPercentage = 1;
  coordinates.destinationXPercentage = 0;
  coordinates.destinationYPercentage = 0;
}
else
{
  if (imageObjectFit !== "fill")
  {
    console.error("unexpected 'object-fit' attribute with value '" + imageObjectFit + "' relative to");
  }
  coordinates.sourceWidth = image.naturalWidth;
  coordinates.sourceHeight = image.naturalHeight;
  coordinates.sourceX = 0;
  coordinates.sourceY = 0;
  coordinates.destinationWidthPercentage = 1;
  coordinates.destinationHeightPercentage = 1;
  coordinates.destinationXPercentage = 0;
  coordinates.destinationYPercentage = 0;
}

其中image是HTML <img>元素,coordinates包含以下属性,假设我们认为sourceFrame是由图像定义的矩形,如果它完全打印,则其自然尺寸,而printFrame是实际显示的区域,即printFrame.width = image.widthprintFrame.height = image.height

  • sourceX:应剪切sourceFrame的左上点的水平位置,
  • sourceY:应剪切sourceFrame的左上点的垂直位置,
  • sourceWidth:应该剪切多少sourceFrame的水平空间,
  • sourceHeight:应该剪切多少sourceFrame的垂直空间,
  • destinationXPercentage:图像将被打印在printFrame上的左上点的水平位置的百分比,相对于printFrame的宽度,
  • destinationYPercentage:图像将被打印在printFrame上的左上点的垂直位置的百分比,相对于printFrame的高度,
  • destinationWidthPercentage:图像将被打印在printFrame上的宽度的百分比,相对于printFrame的宽度,
  • destinationHeightPercentage:图像将被打印在printFrame上的高度的百分比,相对于printFrame的高度。

抱歉,不处理scale-down情况,因为其定义不是很清楚。


嗨,希望你不介意,我稍微修改了你的代码,以支持视频元素。Stackoverflow不允许我在评论中放置详细信息,所以我将其放在了这个gist中:https://gist.github.com/ciaranj/7177fb342102e571db2784dc831f868b - ciaranj
感谢您抽出时间查看这个要点,并且也适应了视频的内容。您做得很好。 - Édouard Mercier

1

以下是更新后的 TypeScript 代码,可处理所有值,包括 object-fit: scale-downobject-position,其中包括相对、绝对和关键字值:

type Rect = {
  x: number;
  y: number;
  width: number;
  height: number;
};

const dom2rect = (rect: DOMRect): Rect => {
  const { x, y, width, height } = rect;
  return { x, y, width, height };
};

const intersectRects = (a: Rect, b: Rect): Rect | null => {
  const x = Math.max(a.x, b.x);
  const y = Math.max(a.y, b.y);
  const width = Math.min(a.x + a.width, b.x + b.width) - x;
  const height = Math.min(a.y + a.height, b.y + b.height) - y;

  if (width <= 0 || height <= 0) return null;

  return { x, y, width, height };
};

type ObjectRects = {
  container: Rect; // client-space size of container element
  content: Rect; // natural size of content
  positioned: Rect; // scaled rect of content relative to container element (may overlap out of container)
  visible: Rect | null; // intersection of container & positioned rect
};

const parsePos = (str: string, ref: number): number => {
  switch (str) {
    case "left":
    case "top":
      return 0;

    case "center":
      return ref / 2;

    case "right":
    case "bottom":
      return ref;

    default:
      const num = parseFloat(str);
      if (str.endsWith("%")) return (num / 100) * ref;
      else if (str.endsWith("px")) return num;
      else
        throw new Error(`unexpected unit object-position unit/value: '${str}'`);
  }
};

const getObjectRects = (
  image: HTMLImageElement | HTMLVideoElement
): ObjectRects => {
  const style = window.getComputedStyle(image);
  const objectFit = style.getPropertyValue("object-fit");

  const naturalWidth =
    image instanceof HTMLImageElement ? image.naturalWidth : image.videoWidth;
  const naturalHeight =
    image instanceof HTMLImageElement ? image.naturalHeight : image.videoHeight;

  const content = { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
  const container = dom2rect(image.getBoundingClientRect());

  let scaleX = 1;
  let scaleY = 1;

  switch (objectFit) {
    case "none":
      break;

    case "fill":
      scaleX = container.width / naturalWidth;
      scaleY = container.height / naturalHeight;
      break;

    case "contain":
    case "scale-down": {
      let scale = Math.min(
        container.width / naturalWidth,
        container.height / naturalHeight
      );

      if (objectFit === "scale-down") scale = Math.min(1, scale);

      scaleX = scale;
      scaleY = scale;
      break;
    }

    case "cover": {
      const scale = Math.max(
        container.width / naturalWidth,
        container.height / naturalHeight
      );
      scaleX = scale;
      scaleY = scale;
      break;
    }

    default:
      throw new Error(`unexpected 'object-fit' value ${objectFit}`);
  }

  const positioned = {
    x: 0,
    y: 0,
    width: naturalWidth * scaleX,
    height: naturalHeight * scaleY,
  };

  const objectPos = style.getPropertyValue("object-position").split(" ");
  positioned.x = parsePos(objectPos[0], container.width - positioned.width);
  positioned.y = parsePos(objectPos[1], container.height - positioned.height);

  const containerInner = { x: 0, y: 0, width: container.width, height: container.height };

  return {
    container,
    content,
    positioned,
    visible: intersectRects(containerInner, positioned),
  };
};

您可以调整返回值,仅输出您需要的内容。


parsePos 丢失。 - Joey Clover
很好的发现 @JoeyClover,我刚刚添加了缺失的函数。 - s-ol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接