假设我有一个视频元素,我想在其中处理鼠标事件:
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 属性的更改具有鲁棒性,并且我还希望能够在没有鼠标事件的情况下转换裸坐标。
如何进行这样的转换?
我认为没有任何本机 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>