Skip to content

这事半个月前就计划好了,本想放到 Photosuite 下个版本里

拖到现在,若不是为了昨天拍的一套图,我也懒得折腾

就是为了这口醋,才包的这顿饺子

我的目的很简单:不引入新的语法,而是通过换行来实现拼图

在 Markdown 中连续插入多张图片,只要它们之间没有非空白内容,就应被视为一个整体自动组合

![图1](1.jpg)
![图2](2.jpg)
![图3](3.jpg)

如何检测相邻图片

拼图分组思路由贪心算法实现:

当扫描到一张图片时,以它为起点向后遍历,将所有连续相邻的图片依次纳入同一组

一旦相邻关系被打断,就停止扩展,并根据组内图片数量决定是否生成拼图(不足两张则原样保留)

难点在于“相邻”的定义:

因为 Markdown 编译为 HTML 后,图片容器之间可能夹杂换行符或空白文本节点

所有,不能直接依赖 nextSibling 进行判断

src/modules/imageGrid.ts
/**
* 检查两个容器是否相邻
*
* @param container1 - 第一个容器
* @param container2 - 第二个容器
* @returns 是否相邻
*/
function areContainersAdjacent(container1: HTMLElement, container2: HTMLElement): boolean {
// 获取两个容器的父元素
const parent1 = container1.parentElement;
const parent2 = container2.parentElement;
// 如果父元素不同,不是相邻的
if (parent1 !== parent2 || !parent1) return false;
// 获取父元素的所有子元素
const siblings = Array.from(parent1.children);
const index1 = siblings.indexOf(container1);
const index2 = siblings.indexOf(container2);
// 检查是否相邻(中间只能有文本节点或空白节点)
if (index2 !== index1 + 1) {
// 检查中间是否只有空白文本节点
let hasNonWhitespace = false;
let node = container1.nextSibling;
while (node && node !== container2) {
if (node.nodeType === Node.ELEMENT_NODE) {
hasNonWhitespace = true;
break;
}
if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
hasNonWhitespace = true;
break;
}
node = node.nextSibling;
}
return !hasNonWhitespace && node === container2;
}
return true;
}

在解决了相邻判定的问题之后,分组逻辑本身就变得很简单了

当 Photosuite 扫描到一张图片时,会从当前位置向后查找连续相邻的图片,并将它们归为一组

出于美观考虑,我这里将单组图片数量限制为最多三张

src/modules/imageGrid.ts
function processImageGrids(root: Element) {
// 获取所有图片元素
const images = Array.from(root.querySelectorAll("img"));
// 用于标记已处理的图片
const processed = new Set<Element>();
for (let i = 0; i < images.length; i++) {
const img = images[i];
// 跳过已处理的图片
if (processed.has(img)) continue;
// 检查是否可以形成拼图
const gridImages = [img];
processed.add(img);
// 查找连续的图片(最多3张)
for (let j = i + 1; j < images.length && gridImages.length < 3; j++) {
const nextImg = images[j];
// 检查两个图片容器是否相邻
const currentContainer = ensurePhotosuiteContainer(gridImages[gridImages.length - 1]);
const nextContainer = ensurePhotosuiteContainer(nextImg);
if (areContainersAdjacent(currentContainer, nextContainer)) {
gridImages.push(nextImg);
processed.add(nextImg);
} else {
break;
}
}
// 如果找到了多张连续的图片(2-3张),创建拼图
if (gridImages.length >= 2) {
createImageGrid(gridImages);
}
}
}

我用 Boardmix 做了一个流程图,通俗易懂:

如何处理不同比例的图片

把图片简单地放进一个 Flex 容器并不难

如果一张是横图(16:9),另一张是竖图(9:16),直接使用 flex: 1 只会让它们宽度相同,却无法保证高度一致,结果要么高低不齐,要么图片被强行拉伸

要让多张图片在同一行里等高对齐,关键不在 Flex,而在比例关系

直观来说:哪张图片更“扁”,就应该占更宽的位置;哪张更“瘦”,就占得窄一些,这样它们的高度才能最终一致

用更具体的话说:

每张图片在一行中所占的宽度,应该和它本身的宽高比成正比

宽高比越大(越横),分到的宽度就越多;宽高比越小(越竖),分到的宽度就越少

因此,在等高布局下,可以把每张图片的宽度理解为:

每张图片的宽度占比 ≈ 自身宽高比 ÷ 所有图片宽高比之和

其中:宽高比 = 宽 / 高

基于这个思路,我没有让 Flex 自由分配空间,而是先计算好每张图片应占的宽度,再通过 flex-basis 精确控制布局

具体实现上,会先异步获取每张图片的宽高比,计算出总比例后,将容器宽度按比例拆分。同时考虑到图片之间的间距(gap),使用 calc() 对最终宽度进行修正,避免累计误差

src/modules/imageGrid.ts
/**
* 异步更新拼图项宽度
* 基于图片宽高比计算宽度,使得所有图片高度一致
*/
async function updateGridDimensions(images: HTMLImageElement[], gridItems: HTMLElement[]) {
const ratios: number[] = [];
// 获取所有图片的宽高比
for (const img of images) {
const ratio = await resolveImageRatio(img);
ratios.push(ratio);
}
// 计算总比例
const totalRatio = ratios.reduce((sum, r) => sum + r, 0);
const gapCount = gridItems.length - 1;
// 设置每张图片的宽度百分比
gridItems.forEach((item, index) => {
if (totalRatio > 0) {
const ratio = ratios[index];
const percent = (ratio / totalRatio) * 100;
// 使用 calc 计算实际宽度:(比例% * 100) - (gap总宽 * 比例占比)
// 公式: calc(33.33% - (2 * var(--gap) * 0.3333))
const widthCalc = `calc(${percent}% - (${gapCount} * var(--photosuite-grid-gap, 4px)) * ${ratio / totalRatio})`;
// 设置 flex-basis 和 max-width
item.style.flex = `0 0 ${widthCalc}`;
item.style.maxWidth = widthCalc;
}
});
}

这样一来,不管图片比例多么悬殊,它们在拼图中都会自然对齐

高度一致、宽度合理、边缘整齐,同时也避免了任何形式的拉伸或裁剪

布局样式

样式只负责布局:Flex + gap + 响应式间距,没有额外装饰,所有空间都留给图片本身

这里通过 CSS 变量统一管理间距;在移动端则适当缩小 gap,以保证有限屏幕宽度下的视觉紧凑度

src/styles/image-grid.scss
/* 拼图容器 */
.photosuite-grid {
--photosuite-grid-gap: 4px;
display: flex;
gap: var(--photosuite-grid-gap);
width: 100%;
margin: 0;
padding: 0;
align-items: flex-start;
}
/* 移动端保持拼图布局,但减小间距 */
@media (max-width: 768px) {
.photosuite-grid {
--photosuite-grid-gap: 2px;
}
}

在交互层面,加了点人情味,当鼠标悬停在图片上时(拼图状态),会有一个轻微的放大效果

注:使用该功能无需配置,默认为 imageGrid: true

拼图状态下不显示 EXIF 和标签,它们在编译阶段并未生成

至此,完成

感谢 Claude Code、Gemini、ChatGPT 对项目的大力支持

安装

Photosuite 已发布至 npm,可直接安装:

pnpm add photosuite
# or
npm install photosuite
# or
yarn add photosuite

参考