资讯动态

纯前端复古手账风琴折相册排版生成器:基于 CSS 3D 折痕变换与连续视差展开交互

发布时间:2026/10/23 6:02:28 来源:尧图企业网站定制
在所有纸质手账与手作画册的装帧形式中风琴折Accordion Fold / Leporello一直是我心头最柔软的偏爱。不同于传统装订书册必须一页一页翻动的线性约束风琴折像一架徐徐拉开的手风琴将时间、旅途与回忆以连续不断的画卷形式展开。合拢时它不过是巴掌大小的精致小盒轻拉两端一整个深秋的银杏落叶、街角速写与胶片剪影便如全景山水般连绵起伏地陈列在眼前。在数字世界里我们看惯了垂直滚动的无限信息流与冷冰冰的左右轮播图那种属于纸张翻折、明暗交替的物理空间美感往往被扁平化的屏幕抹杀得一干二净。今天我想和大家分享如何在纯前端环境下不依赖任何第三方 3D 渲染重型库如 Three.js仅凭现代 CSS 3D 变换、变量驱动系统与轻量级手势算法打造一个极富沉浸感的“风琴折数字手账相册生成器”。空间拓扑与纸张物理折痕的数学拆解想要在二维屏幕上还原逼真的纸张风琴折叠核心在于将平面的相册网格抽象为由“脊Crest”与“谷Trough”交替相连的连续三维面板系统Panel Mesh。交替旋转轴心Alternating Transform Origin每一折纸张在展开或收拢时旋转的中心点是不断变化的。奇数页的折痕向内凹陷其旋转轴在右边缘right center偶数页向外凸起旋转轴在左边缘left center。通过 CSS 嵌套结构或统一的变换矩阵可以实现链式相对旋转。动态阴影与微表面高光Ambient Occlusion Crease Shadow纸张之所以看起来有厚度与立体感是因为折角越小时折缝深处的环境光吸收越多会呈现狭长的深色阴影而背光的纸面会有自然的线性渐变过渡。我们通过 CSS 自定义属性将展开进度--unfold-ratio实时映射为渐变遮罩的不透明度。连续视差透视Parallax Perspective风琴折完全拉平时是一条直线收拢时呈现 Z 字形折叠。当用户拖拽手账两端时不仅各页之间存在角度变化相册在屏幕中央的水平位移与视距perspective也会产生自然的弹性衰减带来宛如双手拉开真实风琴折纸张的阻尼感。核心实现单文件轻量化代码与手势交互下面是完整的风琴折排版生成器核心源码。它包含了 3D 场景容器的搭建、动态纸张折痕角度解算以及移动端与桌面端统一的水平拖拽展开交互。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title纯前端复古手账风琴折相册生成器/title style :root { --paper-base: #f7f2ea; --paper-shadow: rgba(43, 37, 30, 0.4); --ink-dark: #2c2724; --accent-rust: #b35939; --unfold-progress: 0.35; /* 0 表示完全折叠1 表示完全铺平 */ } * { box-sizing: border-box; margin: 0; padding: 0; } body { background-color: #1e1b18; color: var(--ink-dark); font-family: -apple-system, BlinkMacSystemFont, PingFang SC, Songti SC, serif; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; overflow-x: hidden; user-select: none; } .header-tip { position: absolute; top: 30px; color: #9e9488; font-size: 14px; letter-spacing: 0.1em; pointer-events: none; } .stage { width: 100vw; height: 480px; display: flex; align-items: center; justify-content: center; perspective: 1200px; cursor: grab; } .stage:active { cursor: grabbing; } .accordion-track { display: flex; transform-style: preserve-3d; transition: transform 0.1s cubic-bezier(0.2, 0, 0, 1); } .page-leaf { position: relative; width: 180px; height: 280px; background-color: var(--paper-base); border: 1px solid rgba(0, 0, 0, 0.08); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); transform-style: preserve-3d; transform-origin: left center; transition: transform 0.2s ease-out; overflow: hidden; display: flex; flex-direction: column; padding: 18px; } /* 模拟真实的纸张纤维纹理与折痕深处的渐变阴影 */ .page-leaf::after { content: ; position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; background: linear-gradient( to right, rgba(0, 0, 0, calc(0.35 * (1 - var(--unfold-progress)))), rgba(255, 255, 255, calc(0.15 * (1 - var(--unfold-progress)))) 50%, rgba(0, 0, 0, calc(0.4 * (1 - var(--unfold-progress)))) ); transition: opacity 0.2s ease; } .leaf-header { font-size: 11px; letter-spacing: 0.15em; color: #8c8275; margin-bottom: 8px; text-transform: uppercase; border-bottom: 1px dashed rgba(0, 0, 0, 0.1); padding-bottom: 4px; } .leaf-photo { width: 100%; height: 140px; background: #e8e2d5; border-radius: 4px; margin-bottom: 12px; display: flex; align-items: center; justify-content: center; font-size: 32px; box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.05); } .leaf-notes { font-size: 12px; line-height: 1.6; color: #59524c; flex: 1; } .leaf-notes span { display: block; margin-bottom: 4px; } .controller-panel { position: absolute; bottom: 40px; display: flex; align-items: center; gap: 16px; background: rgba(30, 27, 24, 0.8); padding: 12px 24px; border-radius: 100px; backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.08); color: #c9bfb5; font-size: 13px; } .slider { -webkit-appearance: none; width: 160px; height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.2); outline: none; } .slider::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent-rust); cursor: pointer; } /style /head body div classheader-tip✦ 左右拖拽或拨动下方滑动条徐徐展开秋日风琴手账 ✦/div div classstage idstage div classaccordion-track idtrack !-- 动态生成风琴折纸张叶片 -- /div /div div classcontroller-panel span收拢/span input typerange classslider idunfoldSlider min0.05 max1 step0.01 value0.35 span铺展/span /div script const sampleMemories [ { tag: OCT 04 • 银杏大道, icon: , note: 石板路落满了金黄扇叶Nil 在枯叶堆里捉阳光的小碎屑。 }, { tag: OCT 09 • 街角咖啡, icon: ☕, note: 点了两杯热肉桂燕麦拿铁蒸汽让木格玻璃窗蒙上一层细雾。 }, { tag: OCT 15 • 雨夜工作台, icon: ️, note: 桌上摊着刚画完的线稿听着屋檐水滴声写完最后一行渲染逻辑。 }, { tag: OCT 18 • 晚霞剪影, icon: , note: 天边烧透了橙红色的云霞像一整罐橘子果酱不小心翻倒在天幕。 }, { tag: OCT 22 • 风琴折装帧, icon: , note: 将前半个月的切片逐一折叠收藏纸张轻响宛如时光翻覆。 } ]; const track document.getElementById(track); const slider document.getElementById(unfoldSlider); const stage document.getElementById(stage); // 生成纸张节点 function buildAccordion() { track.innerHTML ; sampleMemories.forEach((item, index) { const leaf document.createElement(div); leaf.className page-leaf; leaf.dataset.index index; leaf.innerHTML div classleaf-header${item.tag}/div div classleaf-photo${item.icon}/div div classleaf-notes span第 ${index 1} 折 / 共 ${sampleMemories.length} 折/span ${item.note} /div ; track.appendChild(leaf); }); } // 更新风琴折展开角度与三维形变 function updateFold(progress) { document.documentElement.style.setProperty(--unfold-progress, progress); const leaves document.querySelectorAll(.page-leaf); // 最大折叠角为 75 度完全展开时为 0 度 const foldAngle (1 - progress) * 75; leaves.forEach((leaf, idx) { // 偶数页与奇数页反向旋转形成 Z 字风琴折 const direction idx % 2 0 ? 1 : -1; const currentAngle idx 0 ? 0 : direction * foldAngle; // 动态计算每个叶片在 3D 空间中的相对位移 leaf.style.transform rotateY(${currentAngle}deg); }); // 容器水平居中补偿 const pageW 180; const effectiveW pageW * Math.cos(foldAngle * Math.PI / 180); const totalW effectiveW * leaves.length; track.style.transform translateX(${-totalW * 0.05}px); } buildAccordion(); updateFold(parseFloat(slider.value)); // 滑动条响应 slider.addEventListener(input, (e) { updateFold(parseFloat(e.target.value)); }); // 鼠标与触摸拖拽手势解算 let isDragging false; let startX 0; let initialProgress 0.35; stage.addEventListener(mousedown, (e) { isDragging true; startX e.clientX; initialProgress parseFloat(slider.value); }); window.addEventListener(mousemove, (e) { if (!isDragging) return; const deltaX e.clientX - startX; // 拖动灵敏度水平拖拽 400px 对应完整展开周期 let nextProgress initialProgress deltaX / 400; nextProgress Math.max(0.05, Math.min(1, nextProgress)); slider.value nextProgress; updateFold(nextProgress); }); window.addEventListener(mouseup, () { isDragging false; }); // 移动端 Touch 兼容 stage.addEventListener(touchstart, (e) { isDragging true; startX e.touches[0].clientX; initialProgress parseFloat(slider.value); }, { passive: true }); window.addEventListener(touchmove, (e) { if (!isDragging) return; const deltaX e.touches[0].clientX - startX; let nextProgress initialProgress deltaX / 300; nextProgress Math.max(0.05, Math.min(1, nextProgress)); slider.value nextProgress; updateFold(nextProgress); }, { passive: true }); window.addEventListener(touchend, () { isDragging false; }); /script /body /html纸张美学的工程化打磨与心法将这个小工具部署到我的个人离线手账站后许多读者问起“为什么不用现成的轮播库做个 3D Coverflow而要大费周章地手写这一套风琴折叠拓扑”答案藏在那些极其微小的触感差异里纸张的连续感而非断裂感标准的轮播图Carousel在逻辑上将每一张幻灯片视为相互隔离的个体而风琴折是一整张长纸条通过压痕反复翻折而成的整体。在我们的代码中通过transform-style: preserve-3d与动态余弦投影宽度计算叶片与叶片之间的缝隙始终咬合在一起不会发生割裂漂移。光影对折叠深度的实时自适应在 CSS 的::after伪元素中我们没有使用固定的半透明黑色遮罩而是将--unfold-progress注入到线性渐变的关键帧中。当风琴折被深深收拢时纸面凹折处会呈现浓郁的阴影凸起处则泛出微光而当用户完全拉平它时阴影不透明度自然衰减归零还原出一张平展细腻的水彩白卡纸。触手可及的触觉阻尼在拖拽交互上我们加入了简单的边界限位与渐进阻尼。手势滑过屏幕时不仅是数值在递增由于透视点perspective: 1200px的存在离视点较近的边缘在视觉上放大远端的折叠在空间深处收拢营造出宛如端坐在深秋木桌前、亲手拉开一本手作曲谱般的仪式感。结语与秋日随想写完风琴折的交互逻辑天色已经完全暗了下来。我把今天白天拍下的几张秋日速写依次拖入相册节点中轻轻拨动滑动条看着那些暖黄色的画面随着纸张角度的起伏在屏幕上起承转合。手账排版的迷人之处正在于它抗拒了工业化批量生产中的无差别与同质化。每一次折叠的角度、每一道手压折痕的微小倾角都烙印着制作者当时当刻的情感与温度。希望这个纯前端的风琴折小方案也能为你手头的个人主页、作品集或相册工具带来一份不一样的诗意与厚度。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑