film-roll:把照片排成一卷胶片

最近做了一个小组件 film-roll,把照片排在一条从胶卷壳体里拉出来的胶片上。黑色片基、上下两排齿孔、右侧的胶卷筒,连在一起就是一卷可以横向浏览的照片。

组件已经开源,也发布到了 npm。可以先打开 在线示例,左右滑动胶卷条,点开照片看原图,再试试切换胶卷壳体。

film-roll 在示例网站中的桌面效果,照片沿胶卷条排列,右侧是 Kodak Gold 200 壳体


Table of contents

Open Table of contents

照片排在胶卷上

胶卷条里展示的是统一裁切成 3:2 的缩略图,对应 35mm 胶片常见的 36×24mm 画格比例。画格之间留出片基,两侧配上齿孔。桌面端可以用滚轮浏览,手机上直接左右滑动。容器变窄时,照片、齿孔和壳体会一起缩小,保持它们之间的比例。

右侧的壳体有几种可选样式,包括 Kodak Gold 200、UltraMax 400、Ektar 100、Fujicolor Superia X-TRA 400 和 Ilford HP5 Plus 400。默认用的是 Light Notes,接入时通过 cartridge 指定想用的样式就行。

根据实物照片重绘的五种胶卷壳体

这些品牌壳体是参照实物照片制作的非官方外观,品牌名称和标识归各自所有者所有。它们只负责展示,不会给照片自动套上对应的胶片滤镜。

壳体看起来有立体感,浏览器里实际加载的是已经渲染好的 WebP 图片。胶片、齿孔和照片排列由 CSS 完成,不需要在页面里运行 3D 引擎。

点开照片,看完整画面

胶卷条上的 3:2 缩略图用来快速浏览,点开后看到的是按实际比例显示的原图。竖幅、全景或其他比例的照片都会展开完整画面,不再裁切成胶卷条里的统一画格。

查看器做成了把一格胶片剪下来、放到观片台上的感觉:照片周围保留黑色片基,浅色背景衬在下面,边缘投下一层阴影。胶卷上的连续画格到了这里变成单独的一张,可以仔细看清照片里的细节。

像剪下的胶片放在观片台上,原图按实际比例展示,黑色片基上印有曝光信息

左右按钮和键盘方向键可以切换照片,手机上也支持滑动切换。打开查看器时,背景页面会锁定滚动,关闭后再恢复。

如果给照片传入了 exif 信息,查看器会把相机、焦距、快门、光圈和 ISO 印在照片上方的黑色边框里。这里的数据需要调用方提供,组件本身不读取图片文件里的 EXIF。

已有图片查看器的网站也可以只用胶卷条。传入 onFrameClick 后,点击事件会交给宿主页面处理,替换内置查看器。

接到 React 页面里

安装包,再从应用入口导入一次样式:

npm install film-roll

下面是一个带缩略图和曝光信息的例子,把图片路径替换成自己的即可:

"use client";

import { FilmRoll, type FilmRollFrame } from "film-roll";
import "film-roll/styles.css";

const frames: FilmRollFrame[] = [
  {
    src: "/photos/lake.jpg",
    previewSrc: "/photos/lake-preview.jpg",
    alt: "湖面与远山",
    exif: {
      camera: "Nikon Z 30",
      focalLength: 50,
      exposureTime: "1/640",
      aperture: 6.3,
      iso: 160,
    },
  },
  {
    src: "/photos/coast.jpg",
    alt: "海边的浪花",
  },
];

export default function PhotoRoll() {
  return (
    <FilmRoll
      frames={frames}
      cartridge="kodak-gold-200"
      ariaLabel="浏览这卷照片"
    />
  );
}

src 是查看器打开的原图,previewSrc 是胶卷条里的缩略图;不传 previewSrc 时,两处使用同一张图片。照片较大时,可以提前生成小尺寸预览,避免浏览胶卷条就加载一整组原图。

组件要求 React 18 或以上,一卷最多接受 72 张照片,更多照片需要拆成多卷。它负责展示和浏览,照片上传、存储、缩略图生成都留给接入它的网站。示例页里的文案、布局和样片也不包含在 npm 包里。

用 film-roll-skill 制作自己的壳体

除了组件,还提供了一个配套的 film-roll-skill。它用于让支持 Agent Skills 的编程助手根据胶卷实物照片制作壳体外观,不限定某一种助手,也不属于 film-roll 的 npm 依赖。

给助手一张参考图,它会参照标签上的配色、文字方向和版式绘制环绕贴图,再用 Blender 渲染成透明背景的 WebP。渲染环境需要 Python、bpy 和 Pillow,网页里仍然只加载最终的图片。也可以直接让它渲染已有的 Kodak Gold 200 等预设。

安装方式由所用的助手决定,可以把仓库地址交给它,让它按当前环境支持的 Agent Skills 机制安装。使用时,比如这样说:

以这张胶卷筒照片为参考,制作适用于 film-roll 的透明背景 WebP,保留标签的主要版式,不要添加伸出壳体的胶片尾巴。

生成结果默认保存在 skill 的 output/ 目录,不会自动替换组件里的资源。把 WebP 和配套的 film-cartridge.geometry.json 作为自定义 cartridge 传给组件,就能让胶卷条对齐新壳体的出片口。壳体模型是复用的 35mm 胶卷筒,标签是对参考图的重新绘制,不是对实物的精确扫描复刻。

一个在 iOS 上踩到的坑

开发时遇到过一个很迷惑的问题:桌面浏览器可以滚动,iOS 上用 Chrome 和 Safari 打开,胶卷条却怎么滑都没反应。照片上不行,齿孔边缘也不行。

用 WebKit 复现后,发现原因在布局上。照片画格只设置了 flex-basis,WebKit 在计算外层 max-content 宽度时没有把照片宽度算进去。于是滚动容器的内容宽度和可见宽度一样,浏览器认为根本没有东西可滚动。给画格补上明确的 width 后,横向滚动才恢复。

这个问题在 0.1.8 修复,也补了手机和桌面尺寸的 WebKit 回归测试。之前只在 Chromium 上测滑动,没能发现它,所以现在会单独检查 WebKit 里的整条画格宽度和滚动位置。

项目目前采用 MIT 许可证,适合放在个人摄影页、博客相册或者作品集里,按拍摄主题组织成几卷照片。想试试效果,可以打开 在线示例;源码和接入说明在 GitHub,安装包在 npm。