跳转到正文

实现自动拼图

0.0%

这事半个月前就计划好了,本想放到 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

参考



评论加载中…