/** 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); }