Реагирование: расчет динамических координат наложения «Сжать до соответствия» (Содержать) для изображения с разными соотJavascript

Форум по Javascript
Ответить
Anonymous
 Реагирование: расчет динамических координат наложения «Сжать до соответствия» (Содержать) для изображения с разными соот

Сообщение Anonymous »

Я создаю пользовательский интерфейс для печати фотографий в React, где пользователи выбирают физический размер печати (например, 6x4, 5x7) и нуждаются в наложении, представляющем «Бумагу».
Мне нужно реализовать функцию «Сжимать по размеру». В отличие от стандартной обрезки (которая заполняет область), функция «Сжать по размеру» должна обеспечивать видимость всего изображения внутри области печати без какой-либо обрезки. Если пропорции не совпадают, это должно привести к появлению «белых рамок» (наложение больше изображения в одном измерении).
Настройка:
  • У меня есть тег , отображаемый на экране. Я могу получить его размеры с помощью getBoundingClientRect() (назовем это imgBox).
  • У меня выбран размер печати (например, widthIn: 6, heightIn: 4).
  • Мне нужно нарисовать (наложение) по центру изображения.

Требование:
  • Наложение должно поддерживать соотношение сторон выбранного размера печати.
  • Все изображение должно помещаться внутри наложения.
  • Наложение должно быть центрировано относительно центра изображения точка.

Текущая логика (которая не может правильно вычислить логику «Содержать»):
import React, { useState, useEffect, useRef } from "react";
import { sizes } from "./sizes";

function PrintOrderScreen({ selectedPhotos = [], onBack, startIndex = 0, quantities, setQuantities, photoOverlays, setPhotoOverlays = () => { }, croppedImages = {}, setCroppedImages = () => { }, }) {
const [currentIndex, setCurrentIndex] = useState(0);
const [imageResolution, setImageResolution] = useState(null);
const imgRef = useRef(null);
const [imgBox, setImgBox] = useState({ width: 0, height: 0 });
const [showSelectedOnly, setShowSelectedOnly] = useState(false);
const allSizes = sizes.flatMap((cat) => cat.items);
const startResize = useRef(null);
const [activeCategory, setActiveCategory] = useState("");
const sizesContainerRef = useRef(null);
const categoryRefs = useRef({});
const categoryScrollRef = useRef(null);
const resizeState = useRef(null);
const PRINT_SCALE = 80; // px per inch
const getDpiFrame = (size) => {
if (!size) return { width: 0, height: 0 };

// 1 inch = 150 px at 150dpi (or 300 if you prefer)
const requiredW = size.widthIn * 150;
const requiredH = size.heightIn * 150;

return {
width: requiredW,
height: requiredH
};
};
const getOverlayForCurrent = () => {
const current = selectedPhotos[currentIndex];
if (!current) return null;
return photoOverlays[current.name]?.[current.selectedSize] || null;
};

const handleShrinkToFit = () => {
const printRatio = selectedSize.widthIn / selectedSize.heightIn;
const imageRatio = imgBox.width / imgBox.height;

let overlayW, overlayH;

// Need help here: How to calculate overlayW and overlayH
// so the image is fully contained within the print ratio?

if (printRatio > imageRatio) {
// ???
} else {
// ???
}

// Then center it
const left = imgBox.left + (imgBox.width - overlayW) / 2;
const top = imgBox.top + (imgBox.height - overlayH) / 2;
};
const ensureOverlayForSize = (size) => {
const current = selectedPhotos[currentIndex];
if (!current) return;

const sizeLabel = size.label;
if (photoOverlays[current.name]?.[sizeLabel]) return;

const box = getRenderedImageBox();
if (!box.width || !box.height) return;

const frame = getCenteredOverlayFrame(size, box);

setPhotoOverlays(prev => ({
...prev,
[current.name]: {
...prev[current.name],
[sizeLabel]: {
fitMode: "crop",
rotation: 0,
scale: 1,
frame,
initialFrame: frame,
},
},
}));
};

const handleSelectSize = (size) => {
const current = selectedPhotos[currentIndex];
if (!current) return;

current.selectedSize = size.label;

const existing = photoOverlays[current.name]?.[size.label];
if (existing) {
updateOverlayForCurrent(existing);
return;
}

const box = getRenderedImageBox();
if (!box.width || !box.height) return;

const frame = getCenteredOverlayFrame(size, box);

updateOverlayForCurrent({
fitMode: "crop",
rotation: 0,
scale: 1,
frame,
initialFrame: frame,
});
};

const getPrintFrameFromSize = (size) => {
return {
width: size.widthIn * PRINT_SCALE,
height: size.heightIn * PRINT_SCALE,
};
};

const getCenteredOverlayFrame = (size, imageBox) => {
const printFrame = getPrintFrameFromSize(size);

const left =
imageBox.left + (imageBox.width - printFrame.width) / 2;
const top =
imageBox.top + (imageBox.height - printFrame.height) / 2;

return {
width: printFrame.width,
height: printFrame.height,
left,
top,
};
};
const hasSelectedSize =
Object.values(quantities[currentPhoto.name] || {}).some(q => q > 0);

return (

{/* LEFT SIDE */}



{currentIndex + 1} of {selectedPhotos.length} photos
{currentPhoto.name}


Done



{/* ⬅️ Prev Button (outside image left side) */}
{selectedPhotos.length > 1 && (



)}

{/* 🖼️ Image Container */}

ref={imgRef}
key={currentPhoto.name}
src={currentPhoto.url}
alt={currentPhoto.name}
className="print-preview"
style={{
objectFit: "contain",
objectPosition: "center",
width: "70%",
height: "auto",
display: "block",
filter: getFilterStyle(currentPhoto.filterApplied),
}}
draggable={false}
/>

{/* 🟩 Crop Overlay */}
{currentOverlay?.frame &&
Object.values(quantities[currentPhoto.name] || {}).some(
(qty) => qty > 0
) && (

{/* Resize Handle */}
handleResizeStart(e)}
>



)}

{/* ⚠ Low Resolution Warning */}
{allSizes.some(
(s) =>
(quantities[currentPhoto.name]?.[s.label] || 0) > 0 &&
!checkSizeFit(s)
) && ⚠ Low Resolution}


{/* ➡️ Next Button (outside image right side) */}
{selectedPhotos.length > 1 && (



)}


Center Shrink to Fit
Rotate Reset



{/* RIGHT SIDE PANEL */}

{/* HEADER */}


{showSelectedOnly ? (

className="fa-solid fa-arrow-left"
style={{ cursor: "pointer", fontSize: "16px", color: "#333" }}
title="Show all sizes"
onClick={handleToggleallSizes}
>

Selected Print Sizes

) : (
"Select Print Sizes"
)}


{Object.values(quantities[currentPhoto.name] || {}).some((q) => q > 0) && (

{showSelectedOnly ? "Show All Sizes" : "Show Selected Sizes"}

)}

{/* HORIZONTAL CATEGORY BAR */}
{!showSelectedOnly && (

{
categoryScrollRef.current.scrollBy({ left: -150, behavior: "smooth" });
}}
>




{sizes.map((cat) => (
{
const el = categoryRefs.current[cat.category];
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "start" });
}
}}
>
{cat.category}


))}

{
categoryScrollRef.current.scrollBy({ left: 150, behavior: "smooth" });
}}
>



)}


{/* ───────────────────────────────────
BUILD CATEGORY LIST SAFELY
─────────────────────────────────── */}
{(() => {
// 1. full category structure from sizes.js
const allCategories = sizes;

// 2. build filtered structure safely
const structured = showSelectedOnly
? allCategories
.map((cat) => ({
...cat,
items: (cat.items || []).filter(
(s) => (quantities[currentPhoto.name]?.[s.label] || 0) > 0
),
}))
.filter((cat) => cat.items.length > 0)
: allCategories;

// 3. handle empty case
if (!structured.length) {
return (

No sizes selected yet.

);
}

// 4. final safe mapping
return structured.map((cat) => (
className="size-category-block"
data-category={cat.category}
ref={(el) => (categoryRefs.current[cat.category] = el)}>

{/* CATEGORY BUTTON / HEADER */}

{cat.category}

{/* CATEGORY ITEMS */}
{(cat.items || []).map((s) => {
const qty = quantities[currentPhoto.name]?.[s.label] || 0;
const ok = checkSizeFit(s);

return (
handleSelectSize(s)}
style={{
backgroundColor: qty > 0 ? "#2962FF" : "transparent",
color: qty > 0 ? "white" : "inherit",
borderRadius: "6px",
padding: "8px 12px",
marginBottom: "6px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
transition: "0.2s ease",
}}
>

{s.label} – ${s.price.toFixed(2)} each{" "}
{!ok && (
className="fa-solid fa-triangle-exclamation"
style={{
color: "yellow",
textShadow: "0 0 2px black",
marginLeft: "8px",
}}
>

)}



0 ? "active" : ""}
onClick={(e) => {
e.stopPropagation();
updateQuantity(s.label, -1);
}}
>
-

{qty}
0 ? "active" : ""}
onClick={(e) => {
e.stopPropagation();
updateQuantity(s.label, 1);
}}
>
+



);
})}

));
})()}





);
}

export default PrintOrderScreen;


Подробнее здесь: https://stackoverflow.com/questions/798 ... -for-an-im
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Javascript»