Add coordinates and color legend to PNG export

This commit is contained in:
wuyanwanwu
2026-08-14 21:38:37 +08:00
parent 4f8d46a523
commit 365af027b8
12 changed files with 155 additions and 20 deletions
+141 -6
View File
@@ -79,6 +79,17 @@ function createHistoryId() {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}
function rowNumberToLetters(rowNumber: number) {
let value = Math.max(1, Math.floor(rowNumber));
let letters = "";
while (value > 0) {
value -= 1;
letters = String.fromCharCode(65 + (value % 26)) + letters;
value = Math.floor(value / 26);
}
return letters;
}
function openHistoryDatabase() {
return new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(HISTORY_DB_NAME, 1);
@@ -1256,12 +1267,136 @@ export default function Home() {
}, [history, historyQuery]);
const exportPng = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const link = document.createElement("a");
link.download = `拼豆图纸-${gridWidth}x${gridHeight}.png`;
link.href = canvas.toDataURL("image/png");
link.click();
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 gridPixelWidth = gridWidth * cell;
const gridPixelHeight = gridHeight * cell;
const canvasWidth = ruler + gridPixelWidth;
const legendPadding = 18;
const legendItemWidth = 78;
const legendItemHeight = 36;
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;
const canvas = document.createElement("canvas");
canvas.width = canvasWidth;
canvas.height = ruler + gridPixelHeight + legendHeight;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#f0eee7";
ctx.fillRect(ruler, 0, gridPixelWidth, ruler);
ctx.fillRect(0, ruler, ruler, gridPixelHeight);
ctx.strokeStyle = "rgba(32,38,36,.34)";
ctx.lineWidth = 1;
ctx.font = `700 ${Math.max(9, Math.floor(cell * 0.32))}px Arial`;
ctx.fillStyle = "#45504b";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (let column = 0; column < gridWidth; column += 1) {
const x = ruler + column * cell;
ctx.strokeRect(x + 0.5, 0.5, cell - 1, ruler - 1);
ctx.fillText(String(column + 1), x + cell / 2, ruler / 2);
}
for (let row = 0; row < gridHeight; row += 1) {
const y = ruler + row * cell;
ctx.strokeRect(0.5, y + 0.5, ruler - 1, cell - 1);
ctx.fillText(rowNumberToLetters(row + 1), ruler / 2, y + cell / 2);
}
pixels.forEach(({ color }, index) => {
const column = index % gridWidth;
const row = Math.floor(index / gridWidth);
const x = ruler + column * cell;
const y = ruler + row * cell;
if (color) {
ctx.fillStyle = color.hex;
ctx.fillRect(x, y, cell, cell);
const label = `${rowNumberToLetters(row + 1)}${column + 1}`;
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));
ctx.font = `700 ${fontSize}px Arial`;
while (fontSize > 6 && ctx.measureText(label).width > cell - 3) {
fontSize -= 1;
ctx.font = `700 ${fontSize}px Arial`;
}
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(label, x + cell / 2, y + cell / 2);
}
ctx.strokeStyle = "rgba(32,38,36,.24)";
ctx.lineWidth = 1;
ctx.strokeRect(x + 0.5, y + 0.5, cell - 1, cell - 1);
});
ctx.strokeStyle = "rgba(32,38,36,.58)";
ctx.lineWidth = 2;
for (let column = 5; column < gridWidth; column += 5) {
const x = ruler + column * cell;
ctx.beginPath();
ctx.moveTo(x, ruler);
ctx.lineTo(x, ruler + gridPixelHeight);
ctx.stroke();
}
for (let row = 5; row < gridHeight; row += 5) {
const y = ruler + row * cell;
ctx.beginPath();
ctx.moveTo(ruler, y);
ctx.lineTo(ruler + gridPixelWidth, y);
ctx.stroke();
}
ctx.strokeStyle = "#202624";
ctx.lineWidth = 2;
ctx.strokeRect(ruler, ruler, gridPixelWidth, gridPixelHeight);
const legendTop = ruler + gridPixelHeight;
if (usedColors.length > 0) {
ctx.strokeStyle = "#d7d2c7";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, legendTop + 0.5);
ctx.lineTo(canvasWidth, legendTop + 0.5);
ctx.stroke();
usedColors.forEach((color, index) => {
const column = index % legendColumns;
const row = Math.floor(index / legendColumns);
const x = legendPadding + column * legendItemWidth;
const y = legendTop + legendPadding + row * legendItemHeight;
ctx.fillStyle = color.hex;
ctx.beginPath();
ctx.arc(x + 11, y + 11, 9, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = "rgba(32,38,36,.28)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = "#202624";
ctx.font = "700 13px Arial";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(color.code, x + 26, y + 11);
});
}
canvas.toBlob((blob) => {
if (!blob) {
setStatus("PNG 生成失败,请减少图纸格数后重试");
return;
}
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = `拼豆图纸-${gridWidth}x${gridHeight}.png`;
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 });
}, "image/png");
};
return (