基于网格布局的交互式图片拼接工具,支持实时调整布局和导出高清拼图。
- 📷 拖放上传 - 直接拖放图片文件到网格区域
- 🖱️ 点击选择 - 点击区域打开文件浏览器
- ⌨️ 剪贴板粘贴 - 使用 Ctrl+V 直接粘贴截图
- 支持 1-9 张图片的多种布局模式
- 可拖动边框 - 拖动分隔线实时调整每个区域的大小
- 智能初始布局(左一右二、2x2网格、2x3网格等)
- ✕ 删除功能 - 鼠标悬停时在图片右上角显示删除按钮
- 🔀 拖拽换位 - 在布局调整阶段,直接将已有图片拖拽到另一个格子即可互换位置
- 🎯 智能裁剪 - 自动 Center Crop 填充容器
- 调整图片间距(0-50px)
- 设置圆角大小(0-50px)
- 自定义背景颜色
- 多种画布比例(16:9、4:3、1:1、9:16、3:4)
- 🎛️ 点击"进入精细调整"将网格布局转化为自由画布
- 移动 - 拖动任意元素自由定位,可相互重叠
- 缩放 - 拖动 8 个边角/边缘 handle 等比或拉伸缩放,旋转后仍保持正确的数学关系
- 旋转 - 拖动元素正上方的旋转手柄顺时针/逆时针旋转
- 图层管理 - 上移/下移/置顶/置底调整元素叠放顺序
- 属性面板 - 精确输入 X/Y/宽/高/旋转角度/圆角数值
- 图片裁剪 - 调整图片在元素框内的内容缩放(0.1x–2.0x)及 X/Y 偏移,独立于元素大小
- 自适应图片大小 - 一键将容器宽高比调整为与图片原始比例一致,使图片完整显示、无裁剪
- 💾 导出为 2400px 宽度的高清 PNG 图片
- 保持所有样式设置和布局比例
- Phase 2 精细调整后同样支持导出和复制到剪贴板
npm installnpm run devnpm run buildPhase 1 - 布局调整
- 选择图片数量 - 在右侧控制面板选择要拼接的图片数量
- 添加图片 - 通过拖放、点击或粘贴方式添加图片
- 调整布局 - 拖动图片之间的分隔线调整区域大小;拖动已有图片到另一格可互换位置
- 自定义样式 - 调整间距、圆角、背景色等参数
- 删除图片 - 鼠标悬停在图片上,点击右上角的删除按钮
- 导出拼图 - 点击"导出图片"按钮下载最终作品
Phase 2 - 精细调整
- 进入精细调整 - 点击控制面板的"进入精细调整"按钮,网格布局将转化为自由画布
- 移动元素 - 拖动图片元素到任意位置(支持超出画布边界)
- 缩放元素 - 拖动 8 个 handle 调整大小,旋转后仍保持正确方向
- 旋转元素 - 拖动元素上方的蓝色圆形旋转手柄
- 调整图层 - 在属性面板点击图层按钮调整元素的叠放顺序
- 精确调整 - 在属性面板直接输入数值精确控制位置、尺寸、旋转角度、圆角
- 图片裁剪 - 在属性面板"图片裁剪"区域调整内容缩放(<1 显示更多内容,>1 放大聚焦)及偏移
- 自适应图片大小 - 点击"图片裁剪"区域的"自适应图片大小"按钮,容器高度自动调整为与图片原始比例一致,同时重置缩放和偏移
- 导出结果 - 在精细调整面板点击导出或复制,输出 2400px 宽高清 PNG
- React 18 - UI 框架
- Vite 5 - 构建工具
- Canvas API - 图片渲染和导出
- 原生 CSS - 样式设计
GridFlow/
├── src/
│ ├── components/
│ │ ├── ImageCell.jsx # 图片单元格组件(拖放/点击/粘贴)
│ │ ├── GridLayout.jsx # Phase 1 网格布局组件
│ │ ├── Divider.jsx # 可拖动分隔线组件
│ │ ├── ControlPanel.jsx # 控制面板(Phase 1 & 2 双模式)
│ │ ├── FinetuneCanvas.jsx # Phase 2 自由画布容器
│ │ └── FinetuneElementItem.jsx # Phase 2 可移动/缩放/旋转元素
│ ├── utils/
│ │ └── exportCanvas.js # 导出功能(Phase 1 & 2)
│ ├── App.jsx # 主应用组件(含阶段切换逻辑)
│ └── main.jsx # 应用入口
├── index.html
├── vite.config.js
└── package.json
2026-03-14 — Phase 2 新增自适应图片大小
在精细调整阶段,选中含有图片的元素后,控制面板"图片裁剪"区域顶部新增"自适应图片大小"按钮:
- 保持容器宽度不变,自动将高度调整为
width × (naturalHeight / naturalWidth),使容器宽高比与图片原始比例完全一致 - 同步将
cropZoom重置为 1、cropOffsetX/Y归零,因容器已能完整显示图片,裁剪参数无需保留 - 容器宽高比与图片一致后,cover 填充与 contain 等效,图片完整呈现、无裁剪、无黑边
实现细节
ControlPanel.jsx:在selectedElement.src区块的"图片裁剪"标题下方新增.fit-image-button,点击时通过onElementChange更新height / cropZoom / cropOffsetX / cropOffsetY,逻辑完全内联,无需改动App.jsxControlPanel.css:新增.fit-image-button样式(蓝紫色描边,hover 填充为实色)
2026-02-28 — 布局阶段图片拖拽换位 / 修复精细调整导出与预览不一致
在布局调整阶段,已上传图片的格子现在可直接拖拽到其他格子进行换位:
- 鼠标悬停在有图片的格子上显示
grab光标,提示可拖拽 - 拖拽中源格子半透明淡出,目标格子显示蓝色描边高亮
- 松手后两格图片完整互换(含宽高元数据);拖到同一格无操作
- 外部文件拖入行为不受影响(通过
dataTransfer类型区分内部/外部拖拽)
进入精细调整阶段后,导出图片与预览显示的图片缩放比例不一致。根因分三层:
- cropOffset 系数错误:canvas 导出中偏移量基于缩放后尺寸 (
baseW × cropZoom) 计算,而 CSStranslate(%)始终以元素原始尺寸为基准,修正为以baseW(cover 原始尺寸)为基准 - calculateLayout vs CSS Grid 精度差:Phase 2 元素原先由
calculateLayout手工计算初始化,结果与浏览器 CSS Grid 实际渲染尺寸存在padding/2的偏差,修正为在handleEnterFinetune中通过querySelectorAll('.image-cell') + getBoundingClientRect()直接读取 DOM 真实位置和尺寸 - img 渲染与 background-size: cover 行为不等价:Phase 1 使用 CSS 背景
background-size: cover会将图片缩放至容器大小;Phase 2 使用<img minWidth/minHeight: 100%; width: auto>时,对自然尺寸已超过容器的图片(如照片)不会触发缩放,导致视觉上"放大",修正为在FinetuneElementItem中用 JS 显式计算 cover 尺寸,与 canvas 导出公式完全一致
实现细节
exportCanvas.jsrenderFinetunedToCanvas:cropOffset 改为相对于baseW/baseH(cover 原始尺寸),而非dw/dh(已乘 cropZoom 的绘制尺寸)App.jsxhandleEnterFinetune:用querySelectorAll('.image-cell')+getBoundingClientRect()读取真实格子位置,替代calculateLayout计算;同时传递naturalWidth/naturalHeight(来自images[cellId].width/height)FinetuneElementItem.jsx:移除minWidth/minHeight/objectFitCSS 方案,改为根据naturalWidth/naturalHeight和当前width/height显式计算baseW/baseH/drawW/drawH/drawLeft/drawTop,直接设为 img 的绝对像素样式ImageCell.jsx:新增draggable={!!image}、onDragStart(写入application/gridflow-cell类型标识)、onDragEnd、isDragSource/isDragTarget状态;handleDrop优先检测内部拖拽并调用onImageSwapApp.jsx新增handleImageSwap,原子性交换imagesmap 中两个 key 的值
2026-02-27 — 精细调整阶段新增图片裁剪控制
选中精细调整阶段中的图片元素后,控制面板新增"图片裁剪"区域,支持独立于元素尺寸的图片内容调整:
- 内容缩放(0.10x–2.00x,步进 0.05):小于 1 时图片缩小、显示更多内容(可留白);大于 1 时图片放大、聚焦更少区域
- 裁剪偏移 X / Y(-100%–+100%):在缩放基础上左右/上下平移图片显示位置
- 导出与复制完全同步裁剪参数,确保 2400px 高清输出与预览一致
实现细节
App.jsx初始化 elements 时新增cropOffsetX: 0, cropOffsetY: 0, cropZoom: 1默认值FinetuneElementItem.jsx改用<div overflow:hidden>+<img>嵌套结构;图片通过transform: translate(-50%,-50%) translate(cropOffsetX%, cropOffsetY%) scale(cropZoom)实现缩放与偏移,minWidth/minHeight: 100%保证基础 cover 填充exportCanvas.js先按 cover 规则计算baseW/baseH,再乘以cropZoom得到绘制尺寸,最后加上cropOffsetX/Y相对于缩放后图片尺寸的偏移量
2026-02-23 — 新增精细调整阶段(Phase 2)
在 Phase 1 布局调整完成后,可点击"进入精细调整"进入 Phase 2。Phase 1 的网格布局将转化为自由画布,每张图片成为可独立操作的元素:
- 拖动移动、8 个 handle 缩放、旋转手柄旋转
- 属性面板精确输入坐标、尺寸、角度、圆角
- 图层顺序管理(上移/下移/置顶/置底)
- 导出和复制功能延续到 Phase 2,输出 2400px 高清 PNG
实现细节
App.jsx新增phase/elements/canvasDisplaySize/selectedIdstate,handleEnterFinetune通过getBoundingClientRect获取显示尺寸后调用calculateLayout初始化 elements 数组FinetuneElementItem.jsx核心交互:旋转角度 =atan2(dy, dx) + 90°;resize 时通过局部坐标系变换矩阵(cosθ/sinθ)计算新 center 和尺寸,边缘 handle 投影到对应轴保证单轴约束exportCanvas.js新增exportFinetuned/copyFinetuned,将 display 坐标缩放到 2400px,用ctx.translate + rotate + clip + drawImage渲染每个元素
2026-02-17 — 复制到剪贴板 / 修复粘贴目标
在控制面板导出区域新增「📋 复制图片」按钮,可将当前拼图直接复制到系统剪贴板,方便粘贴到微信、飞书、Figma 等应用中,无需先下载文件。
- 按钮带有状态反馈:复制中 → 已复制 / 复制失败,2 秒后自动重置
- 使用绿色渐变样式,与蓝紫色的导出按钮区分
实现细节
- 从
exportToImage中提取公共的renderToCanvas()函数,复用 Canvas 渲染逻辑 - 新增
copyToClipboard()导出函数,通过navigator.clipboard.write+ClipboardItem将 PNG 写入剪贴板 - 需要浏览器支持 Clipboard API(主流现代浏览器均已支持)
之前每个 ImageCell 都在 document 上注册了 paste 监听器,导致粘贴时所有网格同时接收图片。现已修复为仅将图片粘贴到鼠标悬停的网格中。
实现细节
- 在
ImageCell中新增isHovered状态,通过onMouseEnter/onMouseLeave追踪鼠标位置 paste事件处理函数中增加if (!isHovered) return守卫,仅悬停的 cell 响应粘贴
- Phase 2 图片内部 pan/zoom - 精细调整阶段支持调整图片在元素框内的显示位置(类似 object-position)
- Phase 2 键盘快捷键 - Delete 删除选中元素,方向键微移,Esc 取消选中
ISC

