Default mobile to manual scan mode with guide tap and center shutter for explicit feedback. Split auto-detect pause from hard verification pause and fall back to guide bounds when shape detection misses. Co-authored-by: Cursor <cursoragent@cursor.com>
124 lines
3.8 KiB
JavaScript
124 lines
3.8 KiB
JavaScript
/** 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);
|
|
}
|