如何在 HTML 视频元素的客户端坐标和图片坐标之间进行转换?

问题描述 投票:0回答:1

假设我有一个视频元素,我想在其中处理鼠标事件:

const v = document.querySelector('video');

v.onclick = (ev) => {
  ev.preventDefault();
  console.info(`x=${event.offsetX}, y=${event.offsetY}`);
};
document.querySelector('#play').onclick =
  (ev) => v.paused ? v.play() : v.pause();
document.querySelector('#fit').onchange =
  (ev) => v.style.objectFit = ev.target.value;
<button id="play">play/pause</button>
<label>object-fit: <select id="fit">
  <option>contain</option>
  <option>cover</option>
  <option>fill</option>
  <option>none</option>
  <option>scale-down</option>
</select></label>
<video
  style="width: 80vw; height: 80vh; display: block; margin: 0 auto; background: pink;"
  src="https://www.w3schools.com/html/mov_bbb.mp4"
>
</video>

事件对象为我提供相对于玩家元素(粉红色)边界框的坐标。如何在这些坐标和实际缩放图片的坐标之间进行转换?

我并不特别关心溢出的坐标(超出图片框的坐标)是否被剪切或推断,我不介意舍入错误,我什至不太关心我得到它们的单位(百分比或像素)。然而,我希望该解决方案对

object-fit
object-position
CSS 属性的更改具有鲁棒性,并且我还希望能够在没有鼠标事件的情况下转换裸坐标。

如何进行这样的转换?

javascript dom html5-video
1个回答
4
投票

我认为没有任何本机 API 可以为我们提供这一点,这意味着我们必须自己计算

object-fit
的每个值。
下面是我制作的一个快速实现,我没有彻底测试,但似乎工作得很好。
请注意,演示使用
<canvas>
而不是
<video>
,但
object-fit
object-position
实际上在那里工作相同,因此您可以在您的情况下使用相同的功能,
<canvas>
允许检查我们是否轻松纠正。

// Some helpers
/**
  * Returns the intrinsic* width & height of most media sources in the Web API
  * (at least the closest we can get to it)
  */
function getResourceDimensions(source) {
  if (source.videoWidth) {
    return { width: source.videoWidth, height: source.videoHeight };
  }
  if (source.naturalWidth) {
    return { width: source.naturalWidth, height: source.naturalHeight };
  }
  if (source.width) {
    return { width: source.width, height: source.height };
  }
  return null;
}
function isRelative(length) {
  return length?.match?.(/%$/);
}
/**
  * Parses the component values of "object-position"
  * Returns the position in px
  */
function parsePositionAsPx(str, bboxSize, objectSize) {
  const num = parseFloat(str);
  if (isRelative(str)) {
    const ratio = num / 100;
    return (bboxSize * ratio) - (objectSize * ratio);
  }
  return num;
}
function parseObjectPosition(position, bbox, object) {
  const [left, top] = position.split(" ");
  return {
    left: parsePositionAsPx(left, bbox.width, object.width),
    top:  parsePositionAsPx(top, bbox.height, object.height)
   };
}

// The actual implementation
function relativeToObject(x, y, elem) {
  let { objectFit, objectPosition } = getComputedStyle(elem);
  const bbox   = elem.getBoundingClientRect();
  const object = getResourceDimensions(elem);
  
  if (objectFit === "scale-down") {
    objectFit = (bbox.width < object.width || bbox.height < object.height)
      ? "contain" : "none";
  }
  if (objectFit === "none") {
    const {left, top} = parseObjectPosition(objectPosition, bbox, object);
    return {
      x: x - left,
      y: y - top
    };
  }
  if (objectFit === "contain") {
    const objectRatio = object.height / object.width;
    const bboxRatio   = bbox.height / bbox.width;
    const outWidth    = bboxRatio > objectRatio
      ? bbox.width : bbox.height / objectRatio;
    const outHeight   = bboxRatio > objectRatio
      ? bbox.width * objectRatio : bbox.height;
    const {left, top} = parseObjectPosition(objectPosition, bbox, {width: outWidth, height: outHeight});
    return {
      x: (x - left) * (object.width / outWidth),
      y: (y - top)  * (object.height / outHeight)
    };
  }
  if (objectFit === "fill") {
    const xRatio = object.width / bbox.width;
    const yRatio = object.height / bbox.height;
    // Relative positioning is discarded with `object-fit: fill`,
    // so we need to check here if it's relative or not.
    const [posLeft, posTop] = objectPosition.split(" ");
    const {left, top} = parseObjectPosition(objectPosition, bbox, object);
    return {
      x: (x - (isRelative(posLeft) ? 0 : left)) * xRatio,
      y: (y - (isRelative(posTop) ? 0 : top)) * yRatio,
    };
  }
  if (objectFit === "cover") {
    const minRatio = Math.min(bbox.width / object.width, bbox.height / object.height);
    let outWidth  = object.width  * minRatio;
    let outHeight = object.height * minRatio;
    let outRatio  = 1;
    if (outWidth < bbox.width) {
      outRatio = bbox.width / outWidth;
    }
    if (Math.abs(outRatio - 1) < 1e-14 && outHeight < bbox.height) {
      outRatio = bbox.height / outHeight;
    }
    outWidth  *= outRatio;
    outHeight *= outRatio;
    
    const { left, top } = parseObjectPosition(objectPosition, bbox, {width: outWidth, height: outHeight});
    return {
      x: (x - left) * (object.width / outWidth),
      y: (y - top)  * (object.height / outHeight)
    };
  }
};


// Example: draw a rectangle around the mouse position
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
function init() {
  ctx.fillStyle = ctx.createRadialGradient(canvas.width/2, canvas.height/2, 0, canvas.width/2, canvas.height/2, Math.hypot(canvas.width/2, canvas.height/2));
  ctx.fillStyle.addColorStop(0, "red");
  ctx.fillStyle.addColorStop(1, "green");
  ctx.fillRect(0, 0, canvas.width, canvas.height);
}
canvas.onmousemove = (evt) => {
  const canvasRect = canvas.getBoundingClientRect();
  const mouseX = evt.clientX - canvasRect.left;
  const mouseY = evt.clientY - canvasRect.top;
  const {x, y} = relativeToObject(mouseX, mouseY, canvas);
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.strokeStyle = "white";
  ctx.strokeRect(x - 5, y - 5, 10, 10);
};

// To update our test settings
document.getElementById("object_fit_select").onchange = (evt) => {
  canvas.style.setProperty("--object-fit", evt.target.value);
};
document.getElementById("object_position_x").oninput = (evt) => {
  canvas.style.setProperty("--object-position-x", evt.target.value);
};
document.getElementById("object_position_y").oninput = (evt) => {
  canvas.style.setProperty("--object-position-y", evt.target.value);
};
document.getElementById("canvas_width").oninput = (evt) => {
  canvas.width = evt.target.value;
  init();
};
document.getElementById("canvas_height").oninput = (evt) => {
  canvas.height = evt.target.value;
  init();
};
init();
canvas {
  --object-fit: contain;
  --object-position-x: center;
  --object-position-y: center;
  width: 500px;
  height: 500px;
  object-fit: var(--object-fit);
  object-position: var(--object-position-x) var(--object-position-y);
  outline: 1px solid;
  width: 100%;
  height: 100%;
  background: pink;
}
.resizable {
  resize: both;
  overflow: hidden;
  width: 500px;
  height: 500px;
  outline: 1px solid;
}
<label>object-fit: <select id="object_fit_select">
  <option>contain</option>
  <option>cover</option>
  <option>fill</option>
  <option>none</option>
  <option>scale-down</option>
  </select></label><br>
<label>x-position: <input id="object_position_x" value="center"></label><br>
<label>y-position: <input id="object_position_y" value="center"></label><br>
<label>canvas width: <input id="canvas_width" value="600"></label><br>
<label>canvas height: <input id="canvas_height" value="150"></label><br>
<div class="resizable">
  <canvas id="canvas" width="600" height="150"></canvas>
</div>

© www.soinside.com 2019 - 2024. All rights reserved.