mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 06:58:12 +02:00
Improve image editing by using a new capture method
Replace html2canvas with html-to-image for DOM capture in the image editor. 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/RUoRc2A
This commit is contained in:
1 parent
c0e082a460
commit
b9c61e2f62
3 files changed
+42
-134
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import html2canvas from "html2canvas";
|
import { toPng } from 'html-to-image';
|
||||||
import Sidebar from "@/components/sidebar";
|
import Sidebar from "@/components/sidebar";
|
||||||
import TopBar from "@/components/topbar";
|
import TopBar from "@/components/topbar";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -93,165 +93,65 @@ export default function ImageEditor() {
|
|||||||
const [ribbonPublicId, setRibbonPublicId] = useState<string | null>(null);
|
const [ribbonPublicId, setRibbonPublicId] = useState<string | null>(null);
|
||||||
const [isGeneratingRibbon, setIsGeneratingRibbon] = useState(false);
|
const [isGeneratingRibbon, setIsGeneratingRibbon] = useState(false);
|
||||||
|
|
||||||
// Save edited image mutation - Client-side capture matching preview
|
// Save edited image mutation - DOM capture with html-to-image
|
||||||
const saveImageMutation = useMutation({
|
const saveImageMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (!selectedMedia || !previewRef.current) {
|
if (!selectedMedia || !previewRef.current) {
|
||||||
throw new Error("Aucune image sélectionnée");
|
throw new Error("Aucune image sélectionnée");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load original image to get real dimensions
|
// Get the preview image element to capture its dimensions
|
||||||
const img = new Image();
|
const previewImg = previewRef.current.querySelector('img');
|
||||||
img.crossOrigin = 'anonymous';
|
if (!previewImg) {
|
||||||
img.src = selectedMedia.originalUrl;
|
throw new Error("Image de preview non trouvée");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load original image to get natural dimensions
|
||||||
|
const originalImg = new Image();
|
||||||
|
originalImg.crossOrigin = 'anonymous';
|
||||||
|
originalImg.src = selectedMedia.originalUrl;
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
img.onload = resolve;
|
originalImg.onload = resolve;
|
||||||
img.onerror = reject;
|
originalImg.onerror = reject;
|
||||||
});
|
});
|
||||||
|
|
||||||
const width = img.naturalWidth;
|
const naturalWidth = originalImg.naturalWidth;
|
||||||
const height = img.naturalHeight;
|
const naturalHeight = originalImg.naturalHeight;
|
||||||
|
const displayWidth = previewImg.clientWidth;
|
||||||
|
const displayHeight = previewImg.clientHeight;
|
||||||
|
|
||||||
// Create canvas at exact image size
|
// Calculate scale factor
|
||||||
const canvas = document.createElement('canvas');
|
const scale = naturalWidth / displayWidth;
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
const ctx = canvas.getContext('2d')!;
|
|
||||||
|
|
||||||
// Draw base image
|
// Capture preview as data URL using html-to-image at high resolution
|
||||||
ctx.drawImage(img, 0, 0, width, height);
|
const dataUrl = await toPng(previewRef.current, {
|
||||||
|
quality: 1,
|
||||||
// Draw ribbon if enabled (matching CSS preview)
|
pixelRatio: scale, // Render at natural image resolution
|
||||||
if (ribbon.enabled && ribbon.text) {
|
canvasWidth: naturalWidth,
|
||||||
const ribbonSize = 150;
|
canvasHeight: naturalHeight,
|
||||||
|
skipAutoScale: true
|
||||||
// Calculate position based on corner
|
|
||||||
let x = 0;
|
|
||||||
let y = 0;
|
|
||||||
if (ribbon.position === 'north_east') {
|
|
||||||
x = width - ribbonSize;
|
|
||||||
} else if (ribbon.position === 'south_west') {
|
|
||||||
y = height - ribbonSize;
|
|
||||||
} else if (ribbon.position === 'south_east') {
|
|
||||||
x = width - ribbonSize;
|
|
||||||
y = height - ribbonSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw triangle
|
|
||||||
ctx.save();
|
|
||||||
ctx.beginPath();
|
|
||||||
if (ribbon.position === 'north_west') {
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
ctx.lineTo(x + ribbonSize, y);
|
|
||||||
ctx.lineTo(x, y + ribbonSize);
|
|
||||||
} else if (ribbon.position === 'north_east') {
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
ctx.lineTo(x + ribbonSize, y);
|
|
||||||
ctx.lineTo(x + ribbonSize, y + ribbonSize);
|
|
||||||
} else if (ribbon.position === 'south_west') {
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
ctx.lineTo(x, y + ribbonSize);
|
|
||||||
ctx.lineTo(x + ribbonSize, y + ribbonSize);
|
|
||||||
} else { // south_east
|
|
||||||
ctx.moveTo(x + ribbonSize, y);
|
|
||||||
ctx.lineTo(x, y + ribbonSize);
|
|
||||||
ctx.lineTo(x + ribbonSize, y + ribbonSize);
|
|
||||||
}
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fillStyle = ribbon.color === 'red' ? '#dc2626' : '#eab308';
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// Draw text (rotated to match CSS)
|
|
||||||
const fontSize = ribbon.text.length <= 5 ? 22 : ribbon.text.length <= 8 ? 18 : ribbon.text.length <= 11 ? 15 : 12;
|
|
||||||
ctx.font = `bold ${fontSize}px sans-serif`;
|
|
||||||
ctx.fillStyle = 'white';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
|
|
||||||
const centerX = x + ribbonSize / 2;
|
|
||||||
const centerY = y + ribbonSize / 2;
|
|
||||||
const textOffset = ribbonSize * 0.2;
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(centerX, centerY);
|
|
||||||
if (ribbon.position === 'north_west') {
|
|
||||||
ctx.rotate(-45 * Math.PI / 180);
|
|
||||||
ctx.fillText(ribbon.text, -textOffset, 0);
|
|
||||||
} else if (ribbon.position === 'north_east') {
|
|
||||||
ctx.rotate(45 * Math.PI / 180);
|
|
||||||
ctx.fillText(ribbon.text, textOffset, 0);
|
|
||||||
} else if (ribbon.position === 'south_west') {
|
|
||||||
ctx.rotate(-135 * Math.PI / 180);
|
|
||||||
ctx.fillText(ribbon.text, -textOffset, 0);
|
|
||||||
} else { // south_east
|
|
||||||
ctx.rotate(135 * Math.PI / 180);
|
|
||||||
ctx.fillText(ribbon.text, textOffset, 0);
|
|
||||||
}
|
|
||||||
ctx.restore();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw price badge if enabled (matching CSS preview)
|
|
||||||
if (priceBadge.enabled && priceBadge.price) {
|
|
||||||
const padding = 16;
|
|
||||||
const badgeText = `€${priceBadge.price}`;
|
|
||||||
ctx.font = `bold ${priceBadge.size}px sans-serif`;
|
|
||||||
const textWidth = ctx.measureText(badgeText).width;
|
|
||||||
const badgeWidth = textWidth + padding * 2;
|
|
||||||
const badgeHeight = priceBadge.size + padding;
|
|
||||||
|
|
||||||
let x = padding;
|
|
||||||
let y = padding;
|
|
||||||
|
|
||||||
if (priceBadge.position === 'north_east') {
|
|
||||||
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
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to blob
|
|
||||||
const blob = await new Promise<Blob>((resolve) => {
|
|
||||||
canvas.toBlob((blob) => resolve(blob!), 'image/jpeg', 0.95);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Convert data URL to blob
|
||||||
|
const response = await fetch(dataUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
// Upload
|
// Upload
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', blob, `edited_${Date.now()}.jpg`);
|
formData.append('file', blob, `edited_${Date.now()}.jpg`);
|
||||||
|
|
||||||
const response = await fetch('/api/media/upload', {
|
const uploadResponse = await fetch('/api/media/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!uploadResponse.ok) {
|
||||||
throw new Error("Erreur lors de l'upload");
|
throw new Error("Erreur lors de l'upload");
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return uploadResponse.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/media"] });
|
queryClient.invalidateQueries({ queryKey: ["/api/media"] });
|
||||||
|
|||||||
Generated
+7
@@ -61,6 +61,7 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
@@ -5962,6 +5963,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-to-image": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/html2canvas": {
|
"node_modules/html2canvas": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
|
|||||||
Reference in new issue
Block a user