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



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

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-fitobject-positionCSS属性的更改具有鲁棒性,并且我还希望能够在没有鼠标事件的情况下转换裸坐标。

我该如何进行这样的转换?

我不认为有任何原生API可以为我们提供这一点,这意味着我们必须自己计算object-fit的每个值
下面是我做的一个快速实现,我没有彻底测试,但似乎运行得很好
请注意,演示使用了<canvas>而不是<video>,但object-fitobject-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;
}
/**
* Parses the component values of "object-position"
* Returns the position in px
*/
function parsePositionAsPx(str, bboxSize, objectSize) {
const num = parseFloat(str);
if (str.match(/%$/)) {
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;
return {
x: x * xRatio,
y: y * 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>

最新更新