Add coordinates and color legend to PNG export
This commit is contained in:
+141
-6
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user