Fix image scaling and ribbon positioning in the editor

Refactor image saving logic in `image-editor.tsx` to use a canvas element for accurate scaling and positioning of the image and overlaid elements like ribbons and price badges.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/Hpj2VzR
This commit is contained in:
michaelschal committed 2025-10-11 09:21:39 +00:00
1 parent 48452300e8
commit 838c2079a6
1 file changed
+77 -61
+77 -61
View File
@@ -96,7 +96,7 @@ export default function ImageEditor() {
// Save edited image mutation // Save edited image mutation
const saveImageMutation = useMutation({ const saveImageMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
if (!previewRef.current || !selectedMedia) { if (!selectedMedia) {
throw new Error("Aucune image sélectionnée"); throw new Error("Aucune image sélectionnée");
} }
@@ -110,78 +110,94 @@ export default function ImageEditor() {
img.onerror = reject; img.onerror = reject;
}); });
const naturalWidth = img.naturalWidth; const width = img.naturalWidth;
const naturalHeight = img.naturalHeight; const height = img.naturalHeight;
// Create a temporary container at natural image size // Create canvas at exact image size
const tempContainer = document.createElement('div'); const canvas = document.createElement('canvas');
tempContainer.style.position = 'absolute'; canvas.width = width;
tempContainer.style.top = '-10000px'; canvas.height = height;
tempContainer.style.left = '-10000px'; const ctx = canvas.getContext('2d')!;
tempContainer.style.width = `${naturalWidth}px`;
tempContainer.style.height = `${naturalHeight}px`;
document.body.appendChild(tempContainer);
// Clone and append image // Draw the base image
const imgClone = img.cloneNode() as HTMLImageElement; ctx.drawImage(img, 0, 0, width, height);
imgClone.style.width = '100%';
imgClone.style.height = '100%';
imgClone.style.objectFit = 'contain';
tempContainer.appendChild(imgClone);
// Add ribbon if enabled // Draw ribbon if enabled
if (ribbon.enabled && ribbon.text) { if (ribbon.enabled && ribbon.text) {
const ribbonContainer = document.createElement('div'); const ribbonSize = 150;
ribbonContainer.className = `ribbon-container ${ribbon.position}`; const positions = {
top_left: { x: 0, y: 0, rotation: 0 },
top_right: { x: width - ribbonSize, y: 0, rotation: 90 },
bottom_left: { x: 0, y: height - ribbonSize, rotation: -90 },
bottom_right: { x: width - ribbonSize, y: height - ribbonSize, rotation: 180 }
};
const ribbonTriangle = document.createElement('div'); const pos = positions[ribbon.position as keyof typeof positions];
ribbonTriangle.className = `ribbon-triangle ${ribbon.color}`;
ribbonContainer.appendChild(ribbonTriangle);
const ribbonTextElem = document.createElement('div'); ctx.save();
ribbonTextElem.className = 'ribbon-text'; ctx.translate(pos.x + ribbonSize / 2, pos.y + ribbonSize / 2);
ribbonTextElem.textContent = ribbon.text; ctx.rotate((pos.rotation * Math.PI) / 180);
ribbonTextElem.style.fontSize = ribbon.text.length <= 5 ? '22px' :
ribbon.text.length <= 8 ? '18px' :
ribbon.text.length <= 11 ? '15px' : '12px';
ribbonContainer.appendChild(ribbonTextElem);
tempContainer.appendChild(ribbonContainer); // Draw triangle
ctx.beginPath();
ctx.moveTo(-ribbonSize / 2, -ribbonSize / 2);
ctx.lineTo(ribbonSize / 2, ribbonSize / 2);
ctx.lineTo(-ribbonSize / 2, ribbonSize / 2);
ctx.closePath();
ctx.fillStyle = ribbon.color === 'red' ? '#dc2626' : '#eab308';
ctx.fill();
// Draw text
ctx.rotate(-45 * Math.PI / 180);
ctx.fillStyle = 'white';
ctx.font = `bold ${ribbon.text.length <= 5 ? 22 : ribbon.text.length <= 8 ? 18 : ribbon.text.length <= 11 ? 15 : 12}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(ribbon.text, 0, 0);
ctx.restore();
} }
// Add price badge if enabled // Draw price badge if enabled
if (priceBadge.enabled && priceBadge.price) { if (priceBadge.enabled && priceBadge.price) {
const badge = document.createElement('div'); const padding = 16;
badge.className = `absolute ${ const badgeText = `€${priceBadge.price}`;
priceBadge.position === "north_east" ? "top-4 right-4" : ctx.font = `bold ${priceBadge.size}px sans-serif`;
priceBadge.position === "south_east" ? "bottom-4 right-4" : const textWidth = ctx.measureText(badgeText).width;
priceBadge.position === "south_west" ? "bottom-4 left-4" : const badgeWidth = textWidth + padding * 2;
"top-4 left-4" const badgeHeight = priceBadge.size + padding;
} px-4 py-2 rounded-full text-white font-bold z-10 flex items-center justify-center ${
priceBadge.color === "red" ? "bg-red-600" : "bg-yellow-500" let x = padding;
}`; let y = padding;
badge.style.fontSize = `${priceBadge.size}px`;
badge.textContent = `€${priceBadge.price}`; if (priceBadge.position === 'north_east') {
tempContainer.appendChild(badge); x = width - badgeWidth - padding;
} else if (priceBadge.position === 'south_west') {
y = height - badgeHeight - padding;
} else if (priceBadge.position === 'south_east') {
x = width - badgeWidth - padding;
y = height - badgeHeight - padding;
}
// Draw rounded rectangle background
const radius = badgeHeight / 2;
ctx.fillStyle = priceBadge.color === 'red' ? '#dc2626' : '#eab308';
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + badgeWidth - radius, y);
ctx.arc(x + badgeWidth - radius, y + radius, radius, -Math.PI / 2, Math.PI / 2);
ctx.lineTo(x + radius, y + badgeHeight);
ctx.arc(x + radius, y + radius, radius, Math.PI / 2, -Math.PI / 2);
ctx.closePath();
ctx.fill();
// Draw text
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(badgeText, x + badgeWidth / 2, y + badgeHeight / 2);
} }
// Wait for styles to apply
await new Promise(resolve => setTimeout(resolve, 100));
// Capture the temporary container
const canvas = await html2canvas(tempContainer, {
backgroundColor: null,
scale: 1,
useCORS: true,
allowTaint: true,
logging: false,
width: naturalWidth,
height: naturalHeight
});
// Clean up
document.body.removeChild(tempContainer);
// Convert canvas to blob // Convert canvas to blob
const blob = await new Promise<Blob>((resolve) => { const blob = await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => resolve(blob!), 'image/jpeg', 0.95); canvas.toBlob((blob) => resolve(blob!), 'image/jpeg', 0.95);