deckhearth/lib/scanner-video-layout.js

125 lines
3.8 KiB
JavaScript
Raw Permalink Normal View History

/** Trading card aspect ratio (width / height). */
export const CARD_ASPECT_RATIO = 5 / 7;
/**
* Layout math for a video element using CSS object-cover inside a container.
*/
export function computeObjectCoverLayout(videoWidth, videoHeight, containerWidth, containerHeight) {
if (!videoWidth || !videoHeight || !containerWidth || !containerHeight) {
return null;
}
const videoAspect = videoWidth / videoHeight;
const containerAspect = containerWidth / containerHeight;
let renderedWidth;
let renderedHeight;
let offsetX;
let offsetY;
if (videoAspect > containerAspect) {
renderedHeight = containerHeight;
renderedWidth = containerHeight * videoAspect;
offsetX = (containerWidth - renderedWidth) / 2;
offsetY = 0;
} else {
renderedWidth = containerWidth;
renderedHeight = containerWidth / videoAspect;
offsetX = 0;
offsetY = (containerHeight - renderedHeight) / 2;
}
return {
videoWidth,
videoHeight,
containerWidth,
containerHeight,
renderedWidth,
renderedHeight,
offsetX,
offsetY,
};
}
/** Map video-pixel bounds to percentage styles within an object-cover container. */
export function videoBoundsToContainerStyle(bounds, layout) {
const {
videoWidth,
videoHeight,
containerWidth,
containerHeight,
renderedWidth,
renderedHeight,
offsetX,
offsetY,
} = layout;
const left = offsetX + (bounds.x / videoWidth) * renderedWidth;
const top = offsetY + (bounds.y / videoHeight) * renderedHeight;
const width = (bounds.width / videoWidth) * renderedWidth;
const height = (bounds.height / videoHeight) * renderedHeight;
return {
left: `${(left / containerWidth) * 100}%`,
top: `${(top / containerHeight) * 100}%`,
width: `${(width / containerWidth) * 100}%`,
height: `${(height / containerHeight) * 100}%`,
};
}
/** Centered card guide frame in container pixel coordinates. */
export function computeCardGuideBounds(containerWidth, containerHeight) {
let width = containerWidth * 0.78;
let height = width / CARD_ASPECT_RATIO;
const maxHeight = containerHeight * 0.72;
if (height > maxHeight) {
height = maxHeight;
width = height * CARD_ASPECT_RATIO;
}
return {
left: (containerWidth - width) / 2,
top: (containerHeight - height) / 2,
width,
height,
};
}
/** Card guide as percentage styles for an object-cover container. */
export function cardGuideToContainerStyle(layout) {
const guide = computeCardGuideBounds(layout.containerWidth, layout.containerHeight);
return {
left: `${(guide.left / layout.containerWidth) * 100}%`,
top: `${(guide.top / layout.containerHeight) * 100}%`,
width: `${(guide.width / layout.containerWidth) * 100}%`,
height: `${(guide.height / layout.containerHeight) * 100}%`,
};
}
/** Map a container-space rectangle to video pixel bounds (inverse of object-cover). */
export function containerRectToVideoBounds(rect, layout) {
const { videoWidth, videoHeight, renderedWidth, renderedHeight, offsetX, offsetY } = layout;
const toVideoX = (containerX) => ((containerX - offsetX) / renderedWidth) * videoWidth;
const toVideoY = (containerY) => ((containerY - offsetY) / renderedHeight) * videoHeight;
const x1 = toVideoX(rect.left);
const y1 = toVideoY(rect.top);
const x2 = toVideoX(rect.left + rect.width);
const y2 = toVideoY(rect.top + rect.height);
const x = Math.max(0, Math.min(x1, x2));
const y = Math.max(0, Math.min(y1, y2));
const width = Math.min(videoWidth - x, Math.abs(x2 - x1));
const height = Math.min(videoHeight - y, Math.abs(y2 - y1));
return { x, y, width, height };
}
/** Centered card guide converted to video pixel bounds. */
export function layoutGuideToVideoBounds(layout) {
const guide = computeCardGuideBounds(layout.containerWidth, layout.containerHeight);
return containerRectToVideoBounds(guide, layout);
}