film-roll:把照片排成一卷胶片
最近做了一个小组件 film-roll,把照片排在一条从胶卷壳体里拉出来的胶片上。黑色片基、上下两排齿孔、右侧的胶卷筒,连在一起就是一卷可以横向浏览的照片。
组件已经开源,也发布到了 npm。可以先打开 在线示例,左右滑动胶卷条,点开照片看原图,再试试切换胶卷壳体。

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。