新增网格下载选项设置,允许用户自定义网格线显示、间隔和颜色,同时添加下载设置弹窗以提升用户体验。优化下载逻辑以支持新选项。

This commit is contained in:
zihanjian
2025-05-09 10:23:26 +08:00
parent 50ea85df9f
commit 7603812b7d

View File

@@ -121,6 +121,24 @@ import GridTooltip from '../components/GridTooltip';
import CustomPaletteEditor from '../components/CustomPaletteEditor';
import { loadPaletteSelections, savePaletteSelections, presetToSelections, PaletteSelections } from '../utils/localStorageUtils';
// ++ 添加/定义网格下载选项类型 ++
type GridDownloadOptions = {
showGrid: boolean;
gridInterval: number;
showCoordinates: boolean;
gridLineColor: string; // 新增网格线颜色字段
};
// ++ 定义可选的网格线颜色 ++
const gridLineColorOptions = [
{ name: '深灰色', value: '#555555' },
{ name: '红色', value: '#FF0000' },
{ name: '蓝色', value: '#0000FF' },
{ name: '绿色', value: '#008000' },
{ name: '紫色', value: '#800080' },
{ name: '橙色', value: '#FFA500' },
];
export default function Home() {
const [originalImageSrc, setOriginalImageSrc] = useState<string | null>(null);
const [granularity, setGranularity] = useState<number>(50);
@@ -153,6 +171,15 @@ export default function Home() {
const [customPaletteSelections, setCustomPaletteSelections] = useState<PaletteSelections>({});
const [isCustomPaletteEditorOpen, setIsCustomPaletteEditorOpen] = useState<boolean>(false);
const [isCustomPalette, setIsCustomPalette] = useState<boolean>(false);
// ++ 新增:下载设置相关状态 ++
const [isDownloadSettingsOpen, setIsDownloadSettingsOpen] = useState<boolean>(false);
const [downloadOptions, setDownloadOptions] = useState<GridDownloadOptions>({
showGrid: true,
gridInterval: 10,
showCoordinates: true,
gridLineColor: gridLineColorOptions[0].value, // 默认使用第一个颜色
});
const originalCanvasRef = useRef<HTMLCanvasElement>(null);
const pixelatedCanvasRef = useRef<HTMLCanvasElement>(null);
@@ -657,18 +684,24 @@ export default function Home() {
}, [originalImageSrc, granularity, similarityThreshold, customPaletteSelections, pixelationMode, remapTrigger]);
// --- Download function (ensure filename includes palette) ---
const handleDownloadImage = () => {
const handleDownloadImage = (options?: GridDownloadOptions) => {
if (!mappedPixelData || !gridDimensions || gridDimensions.N === 0 || gridDimensions.M === 0 || activeBeadPalette.length === 0) {
console.error("下载失败: 映射数据或尺寸无效。"); alert("无法下载图纸,数据未生成或无效。"); return;
}
const { N, M } = gridDimensions;
const downloadCellSize = 30;
const axisLabelSize = 20; // Space for axis labels
const gridLineColor = '#DDDDDD'; // Light grid lines
const thickGridLineColor = '#AAAAAA'; // Thicker grid lines
const gridInterval = 10; // For thicker grid lines (e.g., 10x10)
// Adjust canvas size to include axis labels
// 使用传入的options或者当前的downloadOptions
const currentOptions = options || downloadOptions;
// 根据当前下载选项确定是否需要绘制网格和坐标
const { showGrid, gridInterval, showCoordinates, gridLineColor } = currentOptions;
// 设置边距空间用于坐标轴标注(如果需要)
const axisLabelSize = showCoordinates ? 20 : 0;
// const gridLineColor = '#555555'; // 深色网格线 - 由 downloadOptions.gridLineColor 替代
// 调整画布大小
const downloadWidth = N * downloadCellSize + axisLabelSize;
const downloadHeight = M * downloadCellSize + axisLabelSize;
@@ -679,7 +712,7 @@ export default function Home() {
if (!ctx) { console.error("下载失败: 无法创建临时 Canvas Context。"); alert("无法下载图纸。"); return; }
ctx.imageSmoothingEnabled = false;
// Set a default background color for the entire canvas
// 设置背景色
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, downloadWidth, downloadHeight);
@@ -689,37 +722,40 @@ export default function Home() {
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Draw Axis Labels (Numbers)
ctx.fillStyle = '#333333'; // Text color for axis labels
const axisFontSize = Math.max(10, Math.floor(axisLabelSize * 0.6));
ctx.font = `${axisFontSize}px sans-serif`;
// 如果需要,绘制坐标轴数字
if (showCoordinates) {
ctx.fillStyle = '#333333'; // 坐标数字颜色
const axisFontSize = Math.max(10, Math.floor(axisLabelSize * 0.6));
ctx.font = `${axisFontSize}px sans-serif`;
// Top axis (X-axis numbers)
for (let i = 0; i < N; i++) {
if ((i + 1) % gridInterval === 0 || i === 0 || i === N -1) { // Label at intervals, and first/last
ctx.fillText((i + 1).toString(), axisLabelSize + (i * downloadCellSize) + (downloadCellSize / 2), axisLabelSize / 2);
// X轴顶部数字
for (let i = 0; i < N; i++) {
if ((i + 1) % gridInterval === 0 || i === 0 || i === N - 1) { // 在间隔处、起始处和结束处标注
ctx.fillText((i + 1).toString(), axisLabelSize + (i * downloadCellSize) + (downloadCellSize / 2), axisLabelSize / 2);
}
}
}
// Left axis (Y-axis numbers)
for (let j = 0; j < M; j++) {
if ((j + 1) % gridInterval === 0 || j === 0 || j === M-1 ) { // Label at intervals, and first/last
ctx.fillText((j + 1).toString(), axisLabelSize / 2, axisLabelSize + (j * downloadCellSize) + (downloadCellSize / 2));
// Y轴左侧数字
for (let j = 0; j < M; j++) {
if ((j + 1) % gridInterval === 0 || j === 0 || j === M - 1) { // 在间隔处、起始处和结束处标注
ctx.fillText((j + 1).toString(), axisLabelSize / 2, axisLabelSize + (j * downloadCellSize) + (downloadCellSize / 2));
}
}
// 重设字体以绘制单元格内的内容
ctx.font = `bold ${fontSize}px sans-serif`;
}
// Reset font for cell keys
ctx.font = `bold ${fontSize}px sans-serif`;
// 绘制所有单元格
for (let j = 0; j < M; j++) {
for (let i = 0; i < N; i++) {
const cellData = mappedPixelData[j][i];
// Offset drawing by axisLabelSize
// 由于坐标轴的存在,需要偏移绘制位置
const drawX = i * downloadCellSize + axisLabelSize;
const drawY = j * downloadCellSize + axisLabelSize;
// Determine fill color based on whether it's external background
// 根据是否是外部背景确定填充颜色
if (cellData && !cellData.isExternal) {
// Internal cell: fill with bead color and draw text
// 内部单元格:使用珠子颜色填充并绘制文本
const cellColor = cellData.color || '#FFFFFF';
const cellKey = cellData.key || '?';
@@ -728,36 +764,52 @@ export default function Home() {
ctx.fillStyle = getContrastColor(cellColor);
ctx.fillText(cellKey, drawX + downloadCellSize / 2, drawY + downloadCellSize / 2);
} else {
// External cell: fill with white (or leave transparent if background wasn't filled)
// No text needed for external background
ctx.fillStyle = '#FFFFFF'; // Ensure background cells are white
// 外部背景:填充白色
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(drawX, drawY, downloadCellSize, downloadCellSize);
}
// Draw border for ALL cells
// Determine grid line color
ctx.strokeStyle = ((i + 1) % gridInterval === 0 || (j + 1) % gridInterval === 0) && (i < N && j < M)
? thickGridLineColor
: gridLineColor;
ctx.lineWidth = ((i + 1) % gridInterval === 0 || (j + 1) % gridInterval === 0) ? 1.5 : 1;
// Use precise coordinates for sharp lines
// 绘制所有单元格的边框
ctx.strokeStyle = '#DDDDDD'; // 浅色线条作为基础网格
ctx.lineWidth = 0.5;
ctx.strokeRect(drawX + 0.5, drawY + 0.5, downloadCellSize, downloadCellSize);
}
}
// Draw main border around the grid area
ctx.strokeStyle = '#000000'; // Black border for the main grid
ctx.lineWidth = 1;
ctx.strokeRect(axisLabelSize + 0.5, axisLabelSize + 0.5, N * downloadCellSize, M * downloadCellSize);
// 如果需要,绘制分隔网格线
if (showGrid) {
ctx.strokeStyle = gridLineColor; // 使用用户选择的颜色
ctx.lineWidth = 1.5;
// 绘制垂直分隔线 - 在单元格之间而不是边框上
for (let i = gridInterval; i < N; i += gridInterval) {
const lineX = i * downloadCellSize + axisLabelSize;
ctx.beginPath();
ctx.moveTo(lineX, axisLabelSize);
ctx.lineTo(lineX, axisLabelSize + M * downloadCellSize);
ctx.stroke();
}
// 绘制水平分隔线 - 在单元格之间而不是边框上
for (let j = gridInterval; j < M; j += gridInterval) {
const lineY = j * downloadCellSize + axisLabelSize;
ctx.beginPath();
ctx.moveTo(axisLabelSize, lineY);
ctx.lineTo(axisLabelSize + N * downloadCellSize, lineY);
ctx.stroke();
}
}
// 绘制整个网格区域的主边框
ctx.strokeStyle = '#000000'; // 黑色边框
ctx.lineWidth = 1.5;
ctx.strokeRect(axisLabelSize + 0.5, axisLabelSize + 0.5, N * downloadCellSize, M * downloadCellSize);
try {
const dataURL = downloadCanvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = `bead-grid-${N}x${M}-keys-palette_${selectedPaletteKeySet}.png`; // Filename includes palette
link.download = `bead-grid-${N}x${M}-keys-palette_${selectedPaletteKeySet}.png`; // 文件名包含调色板
link.href = dataURL;
document.body.appendChild(link); link.click(); document.body.removeChild(link);
console.log("Grid image download initiated.");
@@ -1204,6 +1256,166 @@ export default function Home() {
importPaletteInputRef.current?.click();
};
// ++ 添加下载设置弹窗组件 ++
const DownloadSettingsModal = ({
isOpen,
onClose,
options,
onOptionsChange,
onDownload
}: {
isOpen: boolean,
onClose: () => void,
options: GridDownloadOptions,
onOptionsChange: (options: GridDownloadOptions) => void,
onDownload: (opts?: GridDownloadOptions) => void // 修改onDownload类型以接受可选参数
}) => {
if (!isOpen) return null;
// 临时状态用于表单输入
const [tempOptions, setTempOptions] = useState<GridDownloadOptions>({...options});
// 处理选项变更
const handleOptionChange = (key: keyof GridDownloadOptions, value: any) => {
setTempOptions(prev => ({
...prev,
[key]: value
}));
};
// 保存选项并立即使用新设置下载
const handleSave = () => {
// 更新父组件中的设置状态(虽然下载时不依赖这个更新)
onOptionsChange(tempOptions);
// 直接使用当前临时设置下载,不依赖状态更新
onDownload(tempOptions);
onClose();
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden w-full max-w-md">
<div className="p-5">
<div className="flex justify-between items-center border-b dark:border-gray-700 pb-3 mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100"></h3>
<button
onClick={onClose}
className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
<div className="space-y-4">
{/* 显示网格线选项 */}
<div className="flex items-center justify-between">
<label className="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">
线
</label>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={tempOptions.showGrid}
onChange={(e) => handleOptionChange('showGrid', e.target.checked)}
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
{/* 网格线设置 (仅当显示网格线时) */}
{tempOptions.showGrid && (
<div className="space-y-4 pl-2 border-l-2 border-gray-200 dark:border-gray-700 ml-1 pt-2 pb-1">
{/* 网格线间隔选项 */}
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
线 ( N 线)
</label>
<div className="flex items-center justify-between space-x-3">
<input
type="range"
min="5"
max="20"
step="1"
value={tempOptions.gridInterval}
onChange={(e) => handleOptionChange('gridInterval', parseInt(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
<span className="flex items-center justify-center min-w-[40px] text-sm font-medium text-gray-900 dark:text-gray-100">
{tempOptions.gridInterval}
</span>
</div>
</div>
{/* 网格线颜色选择 */}
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
线
</label>
<div className="flex flex-wrap gap-2">
{gridLineColorOptions.map(colorOpt => (
<button
key={colorOpt.value}
type="button"
onClick={() => handleOptionChange('gridLineColor', colorOpt.value)}
className={`w-8 h-8 rounded-full border-2 transition-all duration-150 flex items-center justify-center
${tempOptions.gridLineColor === colorOpt.value
? 'border-blue-500 ring-2 ring-blue-500 ring-offset-1 dark:ring-offset-gray-800'
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'}`}
title={colorOpt.name}
>
<span
className="block w-6 h-6 rounded-full"
style={{ backgroundColor: colorOpt.value }}
></span>
</button>
))}
</div>
</div>
</div>
)}
{/* 显示坐标选项 */}
<div className="flex items-center justify-between">
<label className="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">
</label>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={tempOptions.showCoordinates}
onChange={(e) => handleOptionChange('showCoordinates', e.target.checked)}
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
<div className="flex justify-end mt-6 space-x-3">
<button
onClick={onClose}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 rounded-lg transition-colors"
>
</button>
<button
onClick={handleSave}
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
>
</button>
</div>
</div>
</div>
</div>
);
};
return (
<>
{/* 添加自定义动画样式 */}
@@ -1736,9 +1948,9 @@ export default function Home() {
{/* ++ HIDE Download Buttons in manual mode ++ */}
{!isManualColoringMode && originalImageSrc && mappedPixelData && (
<div className="w-full md:max-w-2xl mt-4 flex flex-col sm:flex-row gap-2 sm:gap-3">
{/* Download Grid Button - Keeping styles bright */}
{/* Download Grid Button - 现在打开设置弹窗而不是直接下载 */}
<button
onClick={handleDownloadImage}
onClick={() => setIsDownloadSettingsOpen(true)}
disabled={!mappedPixelData || !gridDimensions || gridDimensions.N === 0 || gridDimensions.M === 0 || activeBeadPalette.length === 0}
className="flex-1 py-2.5 px-4 bg-gradient-to-r from-green-500 to-green-600 text-white text-sm sm:text-base rounded-lg hover:from-green-600 hover:to-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-all duration-300 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg hover:translate-y-[-1px] disabled:hover:translate-y-0 disabled:hover:shadow-md"
>
@@ -1858,6 +2070,15 @@ export default function Home() {
</div>
</div>
)}
{/* 添加下载设置弹窗 */}
<DownloadSettingsModal
isOpen={isDownloadSettingsOpen}
onClose={() => setIsDownloadSettingsOpen(false)}
options={downloadOptions}
onOptionsChange={setDownloadOptions}
onDownload={handleDownloadImage}
/>
</div>
</>
);