Improve high resolution PNG export

This commit is contained in:
wuyanwanwu
2026-08-14 22:37:35 +08:00
parent a65536e31e
commit 7d45dd8073
13 changed files with 40 additions and 32 deletions
+23 -18
View File
@@ -1026,9 +1026,6 @@ export default function Home() {
moved: boolean;
} | null>(null);
const suppressCanvasClickRef = useRef(false);
const cropDialogImage = cropDialogPurpose === "pattern-import"
? patternImportImageRef.current
: sourceImageRef.current;
useEffect(() => {
logUsage({ event: "page_view" });
@@ -1152,7 +1149,9 @@ export default function Home() {
useEffect(() => {
if (!cropDialogOpen) return;
const canvas = cropDialogCanvasRef.current;
const image = cropDialogImage;
const image = cropDialogPurpose === "pattern-import"
? patternImportImageRef.current
: sourceImageRef.current;
if (!canvas || !image) return;
const maximumWidth = 1200;
const maximumHeight = 760;
@@ -1162,7 +1161,7 @@ export default function Home() {
const ctx = canvas.getContext("2d")!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
}, [cropDialogOpen, cropDialogPurpose, sourceUrl, patternImportUrl, cropDialogImage]);
}, [cropDialogOpen, cropDialogPurpose, sourceUrl, patternImportUrl]);
useEffect(() => {
if (!cropDialogOpen) return;
@@ -1397,7 +1396,9 @@ export default function Home() {
let width = Math.abs(endX - startX);
let height = Math.abs(endY - startY);
if (cropSelectionMode === "square") {
const image = cropDialogImage;
const image = cropDialogPurpose === "pattern-import"
? patternImportImageRef.current
: sourceImageRef.current;
if (!image) return;
const normalizedSquareRatio = image.naturalHeight / image.naturalWidth;
if (width / Math.max(height, 0.0001) > normalizedSquareRatio) width = height * normalizedSquareRatio;
@@ -1430,7 +1431,9 @@ export default function Home() {
const chooseCropSelectionMode = (mode: CropSelectionMode) => {
setCropSelectionMode(mode);
if (mode !== "square") return;
const image = cropDialogImage;
const image = cropDialogPurpose === "pattern-import"
? patternImportImageRef.current
: sourceImageRef.current;
if (!image) return;
const currentCenterX = draftCropRect.x + draftCropRect.width / 2;
const currentCenterY = draftCropRect.y + draftCropRect.height / 2;
@@ -1754,14 +1757,16 @@ export default function Home() {
const exportPng = () => {
if (!pixels.length) return;
const longestSide = Math.max(gridWidth, gridHeight);
const cell = longestSide <= 120 ? 36 : longestSide <= 180 ? 30 : 24;
const ruler = Math.max(42, cell + 12);
const maximumExportSide = 8192;
const preferredCell = longestSide <= 120 ? 56 : longestSide <= 180 ? 40 : 32;
const cell = Math.max(24, Math.min(preferredCell, Math.floor((maximumExportSide - 72) / longestSide)));
const ruler = Math.max(48, cell + 16);
const gridPixelWidth = gridWidth * cell;
const gridPixelHeight = gridHeight * cell;
const canvasWidth = ruler + gridPixelWidth;
const legendPadding = 18;
const legendItemWidth = 78;
const legendItemHeight = 36;
const legendItemWidth = 128;
const legendItemHeight = 42;
const legendColumns = Math.max(1, Math.floor((canvasWidth - legendPadding * 2) / legendItemWidth));
const legendRows = Math.ceil(usedColors.length / legendColumns);
const legendHeight = usedColors.length > 0 ? legendPadding * 2 + legendRows * legendItemHeight : 0;
@@ -1805,9 +1810,9 @@ export default function Home() {
const label = color.code;
const [red, green, blue] = hexToRgb(color.hex);
ctx.fillStyle = red * 0.299 + green * 0.587 + blue * 0.114 > 160 ? "#18211d" : "#ffffff";
let fontSize = Math.max(6, Math.floor(cell * 0.29));
let fontSize = Math.max(8, Math.floor(cell * 0.31));
ctx.font = `700 ${fontSize}px Arial`;
while (fontSize > 6 && ctx.measureText(label).width > cell - 3) {
while (fontSize > 8 && ctx.measureText(label).width > cell - 5) {
fontSize -= 1;
ctx.font = `700 ${fontSize}px Arial`;
}
@@ -1855,7 +1860,7 @@ export default function Home() {
const y = legendTop + legendPadding + row * legendItemHeight;
ctx.fillStyle = color.hex;
ctx.beginPath();
ctx.arc(x + 11, y + 11, 9, 0, Math.PI * 2);
ctx.arc(x + 12, y + 13, 10, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = "rgba(32,38,36,.28)";
ctx.lineWidth = 1;
@@ -1864,7 +1869,7 @@ export default function Home() {
ctx.font = "700 13px Arial";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(color.code, x + 26, y + 11);
ctx.fillText(`${color.code} · ${color.count}`, x + 28, y + 13);
});
}
@@ -1879,8 +1884,8 @@ export default function Home() {
link.href = url;
link.click();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
setStatus("PNG 已生成:格内为 MARD 色号,底部为色块和对应色号");
logUsage({ event: "download_png", width: gridWidth, height: gridHeight, colors: usedColors.length });
setStatus(`高清 PNG 已生成:${canvas.width} × ${canvas.height}px,底部图例包含色号和用量`);
logUsage({ event: "download_png", width: gridWidth, height: gridHeight, colors: usedColors.length, export_width: canvas.width, export_height: canvas.height });
}, "image/png");
};
@@ -1981,7 +1986,7 @@ export default function Home() {
<label><input type="checkbox" checked={showGrid} onChange={(e) => setShowGrid(e.target.checked)} /> </label>
<label><input type="checkbox" checked={showCodes} onChange={(e) => setShowCodes(e.target.checked)} /> </label>
<button className="save-button" onClick={savePattern}></button>
<button onClick={exportPng}> PNG</button>
<button onClick={exportPng}> PNG</button>
</div>
</div>
<div className="pattern-legend">