docs(research): 灯箱深度美化探索+可交互 mockup(v2.1 提案)
五站研究(PhotoSwipe 源码级核对/medium-zoom/Unsplash◐/图虫◐/Flickr) → 六维差距 → v2.1 增强提案十项(P0=原位起飞开合+背景点击关;P1= chrome 双层淡入/相邻预载/双指缩放;P2=换片过渡/缩放指示/长图侧置 meta/HD 交换/竖拖关闭)+ 明确不做清单八条 + 可交互单文件 mockup (仓内实拍 5 张、13 项交互可点)。 验证: mockup 引用路径 ../content/photos/ 相对仓根可开;文档引文对 PhotoSwipe defaultOptions 源码核对注记 Spec: none (研究文档+设计预览稿,v2.1 立项待用户拍板) Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
541710b50a
commit
54769bcfdf
2 files changed
+429
No files matched your search
@@ -0,0 +1,310 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>灯箱 v2.1 提案预览 · wait4x</title>
|
||||
<style>
|
||||
/* ===== 灰阶基底(对齐站 tokens.css:四档灰 + 唯一曲线族) ===== */
|
||||
:root{
|
||||
--bg:#fff; --bg-soft:#f7f7f8; --ink-1:#111113; --ink-2:#33343a; --ink-3:#6b6e78;
|
||||
--ink-4:#a0a3ad; --line:#e6e7eb; --radius-img:10px;
|
||||
--mono:"JetBrains Mono",ui-monospace,Consolas,monospace;
|
||||
--sans:"Noto Sans SC Variable","Noto Sans SC",system-ui,-apple-system,"PingFang SC",sans-serif;
|
||||
--ez:cubic-bezier(.4,0,.22,1); /* PhotoSwipe 333ms 节奏同族曲线 */
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--ink-2);font-family:var(--sans);}
|
||||
body.locked{overflow:hidden}
|
||||
.wrap{max-width:896px;margin:0 auto;padding:28px 20px 72px}
|
||||
kbd{font-family:var(--mono);font-size:11.5px;border:1px solid var(--line);background:#fff;
|
||||
border-radius:4px;padding:1px 6px;color:var(--ink-3)}
|
||||
|
||||
/* ===== 顶部说明:这是提案预览稿 ===== */
|
||||
.note{background:var(--bg-soft);border:1px solid var(--line);border-radius:10px;
|
||||
padding:14px 18px;font-size:13.5px;line-height:1.7;color:var(--ink-2);margin:18px 0 30px}
|
||||
.note strong{color:var(--ink-1)}
|
||||
.note ul{margin:6px 0 0;padding-left:18px}
|
||||
.note li{margin:2px 0}
|
||||
h1{font-size:27px;color:var(--ink-1);margin:8px 0 2px;font-weight:600}
|
||||
.sub{font-family:var(--mono);font-size:12.5px;color:var(--ink-4)}
|
||||
|
||||
/* ===== 影像墙:cover 裁切(与线上一致,起飞动画的前提) ===== */
|
||||
.stage{background:rgba(255,255,255,.04);border-radius:6px;padding:14px}
|
||||
.wall{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(3,1fr);
|
||||
grid-auto-rows:8px;grid-auto-flow:dense;gap:14px}
|
||||
.shot{position:relative;overflow:hidden;border-radius:var(--radius-img);grid-row-end:span var(--sp,13)}
|
||||
.shot button{display:block;width:100%;height:100%;padding:0;border:none;background:none;cursor:zoom-in}
|
||||
.shot img{display:block;width:100%;height:100%;object-fit:cover}
|
||||
@media (max-width:820px){.wall{grid-template-columns:repeat(2,1fr)}}
|
||||
@media (max-width:560px){.wall{grid-template-columns:1fr 1fr;gap:10px}}
|
||||
|
||||
/* ===== 灯箱:fixed 覆盖层(生产将等价几何搬回原生 <dialog>) ===== */
|
||||
.lbx{position:fixed;inset:0;z-index:80}
|
||||
.lbx[hidden]{display:none}
|
||||
.lbx-bg{position:absolute;inset:0;background:rgba(8,10,14,.96);opacity:0;
|
||||
transition:opacity 300ms var(--ez)}
|
||||
.lbx.on .lbx-bg{opacity:1}
|
||||
/* 双盒:win=裁切窗(动画 left/top/w/h),pic=内图(动画几何 + 缩放 transform) */
|
||||
.lbx-win{position:fixed;overflow:hidden;border-radius:6px;transform-origin:0 0;
|
||||
box-shadow:0 12px 48px rgba(0,0,0,.5);touch-action:none}
|
||||
.lbx-pic{position:absolute;transform-origin:50% 50%;user-select:none;-webkit-user-drag:none;
|
||||
transition:transform 90ms linear;cursor:zoom-in;display:block}
|
||||
.lbx.landing .lbx-win,.lbx.landing .lbx-pic{
|
||||
transition:left 300ms var(--ez),top 300ms var(--ez),width 300ms var(--ez),
|
||||
height 300ms var(--ez),transform 300ms var(--ez),opacity 300ms var(--ez)}
|
||||
.lbx-pic.zoomed{cursor:grab}.lbx-pic.grabbing{cursor:grabbing}
|
||||
.lbx-pic.swap{opacity:0}
|
||||
|
||||
/* chrome 层 1:关闭/箭头/计数(+140ms 淡入) */
|
||||
.c1{opacity:0;transition:opacity 220ms var(--ez)}
|
||||
.lbx.on .c1{opacity:1;transition-delay:140ms}
|
||||
.lbx-btn{position:fixed;width:44px;height:44px;border-radius:10px;cursor:pointer;
|
||||
border:1px solid rgba(255,255,255,.22);background:rgba(20,24,30,.55);color:#e6e9ee;
|
||||
backdrop-filter:blur(4px);box-shadow:0 2px 10px rgba(0,0,0,.35);display:grid;place-items:center}
|
||||
.lbx-btn:hover{background:rgba(255,255,255,.12)}
|
||||
.lbx-close{top:16px;right:16px}
|
||||
.lbx-prev{left:14px;top:50%;translate:0 -50%}
|
||||
.lbx-next{right:14px;top:50%;translate:0 -50%}
|
||||
.lbx-count{position:fixed;top:26px;left:20px;font-family:var(--mono);font-size:12.5px;color:#98a2b3}
|
||||
|
||||
/* chrome 层 2:caption / title+EXIF / 动作(+300ms 淡入) */
|
||||
.c2{opacity:0;transition:opacity 260ms var(--ez)}
|
||||
.lbx.on .c2{opacity:1;transition-delay:300ms}
|
||||
.lbx-meta{position:fixed;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;
|
||||
gap:3px;padding:14px 72px 20px;text-align:center;text-shadow:0 1px 4px rgba(0,0,0,.6)}
|
||||
.lbx-caption{margin:0;font-family:var(--sans);font-size:13.5px;line-height:1.6;color:#dfe6f3;max-width:640px}
|
||||
.lbx-row{display:flex;align-items:center;flex-wrap:wrap;justify-content:center;gap:14px;
|
||||
font-family:var(--mono);font-size:12.5px}
|
||||
.lbx-title{color:#fff;font-family:var(--sans);font-size:13px;font-weight:600}
|
||||
.lbx-exif{color:#98a2b3}
|
||||
.lbx-acts{display:inline-flex;gap:10px}
|
||||
.lbx-act{display:inline-flex;align-items:center;gap:5px;padding:6px 11px;border-radius:10px;
|
||||
border:1px solid rgba(255,255,255,.22);background:rgba(20,24,30,.55);color:#dfe6f3;
|
||||
font-family:var(--mono);font-size:12px;text-decoration:none;backdrop-filter:blur(4px)}
|
||||
.lbx-act:hover{background:rgba(40,48,60,.75);color:#fff}
|
||||
|
||||
/* 沉浸态:zoom>1 或按 i —— chrome 全隐,舞台纯净 */
|
||||
.lbx.clean .c1,.lbx.clean .c2,.lbx.noui .c1,.lbx.noui .c2{opacity:0;transition-delay:0ms}
|
||||
|
||||
/* 首开提示 + 缩放指示 */
|
||||
.lbx-hint{position:fixed;left:50%;bottom:128px;translate:-50% 0;padding:7px 13px;border-radius:10px;
|
||||
border:1px solid rgba(255,255,255,.22);background:rgba(20,24,30,.55);backdrop-filter:blur(4px);
|
||||
font-family:var(--mono);font-size:12px;color:#dfe6f3;opacity:0;transition:opacity 400ms var(--ez);
|
||||
pointer-events:none}
|
||||
.lbx-hint.show{opacity:1}
|
||||
.lbx-zind{position:fixed;right:20px;bottom:128px;font-family:var(--mono);font-size:12px;color:#98a2b3;
|
||||
opacity:0;transition:opacity 300ms var(--ez);pointer-events:none}
|
||||
.lbx-zind.show{opacity:1}
|
||||
|
||||
@media (max-width:560px){
|
||||
.lbx-prev{left:6px}.lbx-next{right:6px}
|
||||
.lbx-meta{padding:12px 16px 16px}
|
||||
.lbx-hint{bottom:150px}
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce){
|
||||
.lbx-bg,.c1,.c2,.lbx-pic,.lbx-win,.lbx-hint,.lbx-zind{transition:none!important}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>图库 · 灯箱 v2.1 提案预览</h1>
|
||||
<p class="sub">预览稿 —— 交互为提案演示(非线上行为)· 图片为 content/photos 实拍,EXIF 为示例数据</p>
|
||||
<div class="note">
|
||||
<strong>这是给浏览器直接打开的交互演示</strong>,对应《09-灯箱美化探索》提案 #1/#2/#3/#6/#7:
|
||||
<ul>
|
||||
<li><strong>原位起飞开合</strong>:点击缩略图,照片从原位放大入舞台;关闭回落原位(<kbd>Esc</kbd> / 背景点击 / 关闭钮)</li>
|
||||
<li><strong>chrome 双层淡入</strong>:开箱后层 1(箭头/计数)先入、层 2(caption/EXIF)随入;<kbd>i</kbd> 隐藏全部;放大后自动隐层</li>
|
||||
<li><strong>连拍导航</strong>:<kbd>←</kbd> <kbd>→</kbd> / 边缘箭头 / 触屏快滑;计数 n / N</li>
|
||||
<li><strong>缩放</strong>:滚轮锚点缩放 1–4x、<kbd>双击</kbd> 1x↔2.5x、放大后拖拽、双指缩放;右下角缩放指示</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="stage"><ul class="wall" id="wall"></ul></div>
|
||||
</div>
|
||||
|
||||
<!-- 灯箱:fixed 覆盖层(生产等价搬回原生 <dialog>,focus-trap 由 dialog 免费提供) -->
|
||||
<div class="lbx" id="lbx" hidden>
|
||||
<div class="lbx-bg"></div>
|
||||
<div class="lbx-win" id="win"><img class="lbx-pic" id="pic" alt=""></div>
|
||||
<button class="lbx-btn lbx-close c1" id="bClose" aria-label="关闭(Esc)">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m5 5 14 14M19 5 5 19"/></svg></button>
|
||||
<button class="lbx-btn lbx-prev c1" id="bPrev" aria-label="上一张(←)">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14.5 5.5-6.5 6.5 6.5 6.5"/></svg></button>
|
||||
<button class="lbx-btn lbx-next c1" id="bNext" aria-label="下一张(→)">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9.5 5.5 6.5 6.5-6.5 6.5"/></svg></button>
|
||||
<div class="lbx-count c1" id="count">1 / 5</div>
|
||||
<div class="lbx-meta c2">
|
||||
<p class="lbx-caption" id="cap"></p>
|
||||
<div class="lbx-row"><strong class="lbx-title" id="ttl"></strong><span class="lbx-exif" id="exif"></span>
|
||||
<span class="lbx-acts"><a class="lbx-act" id="aOrig" href="#" target="_blank" rel="noopener">原图 ↗</a><a class="lbx-act" id="aDl" href="#" download>下载 ↓</a></span></div>
|
||||
</div>
|
||||
<div class="lbx-hint" id="hint">滚轮缩放 · 双击放大 · Esc 关闭 · i 隐藏界面</div>
|
||||
<div class="lbx-zind" id="zind">100%</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ===== 数据:仓内实拍(含 2.36:1 长横幅与 2:3 竖图两种 letterbox 形态) ===== */
|
||||
const PHOTOS=[
|
||||
{src:'../content/photos/1766156983093.jpg',w:2400,h:1600,sp:13,title:'防波堤暮色',
|
||||
cap:'落日把海与城一齐烫成金箔,海鸟的剪影恰好掠过日轮',exif:'X-T5 · XF35mmF1.4 · f/5.6 · 1/500s · ISO 200'},
|
||||
{src:'../content/photos/mmexport1771816382115.jpg',w:2400,h:1018,sp:6,title:'长滩低云',
|
||||
cap:'涨潮前的长滩,云层压得很低',exif:'X100VI · f/8 · 1/1000s · ISO 160'},
|
||||
{src:'../content/photos/1766156998344.jpg',w:1600,h:2400,sp:20,title:'旗与塔',
|
||||
cap:'旗在风里翻卷,塔身亮得像一支随时点火的火箭',exif:'GR III · f/2.8 · 1/250s · ISO 400'},
|
||||
{src:'../content/photos/1766157004116.jpg',w:2400,h:1536,sp:9,title:'暮色撤离',
|
||||
cap:'灯柱次第亮起,暮色像退潮一样撤离',exif:'X-T5 · XF56mmF1.2 · f/1.4 · 1/120s · ISO 800'},
|
||||
{src:'../content/photos/mmexport1776564092433.jpg',w:2400,h:1600,sp:13,title:'夜航',
|
||||
cap:'货轮的灯在雾里连成一条迟疑的线',exif:'X-T5 · XF70-300mm · f/4 · 1/60s · ISO 1250'}];
|
||||
const N=PHOTOS.length;
|
||||
|
||||
const $=id=>document.getElementById(id);
|
||||
const lbx=$('lbx'),win=$('win'),pic=$('pic'),wall=$('wall');
|
||||
const reduced=matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
let idx=0,open_=false,hintShown=false;
|
||||
let zoom={s:1,tx:0,ty:0};
|
||||
|
||||
/* ===== 影像墙 ===== */
|
||||
PHOTOS.forEach((p,i)=>{
|
||||
const li=document.createElement('li');
|
||||
li.className='shot'; li.style.setProperty('--sp',p.sp);
|
||||
const b=document.createElement('button'); b.type='button'; b.setAttribute('aria-label',p.title);
|
||||
b.dataset.i=i;
|
||||
const im=document.createElement('img'); im.src=p.src; im.alt=p.title; im.loading=i<3?'eager':'lazy';
|
||||
b.appendChild(im); li.appendChild(b); wall.appendChild(li);
|
||||
b.addEventListener('click',()=>openLbx(i));
|
||||
});
|
||||
const thumbImg=i=>wall.querySelector(`button[data-i="${i}"] img`);
|
||||
|
||||
/* ===== 几何:舞台 fit 矩形 / cover-contain 双盒(PhotoSwipe data-cropped 路线) ===== */
|
||||
function stageBox(){const W=innerWidth,H=innerHeight;
|
||||
const side=W>760?56:12,top=64,bot=innerWidth>560?124:150;
|
||||
return{x:side,y:top,w:W-side*2,h:H-top-bot};}
|
||||
function fitRect(p,box){const r=p.w/p.h,br=box.w/box.h;let w,h;
|
||||
if(r>br){w=box.w;h=w/r;}else{h=box.h;w=h*r;}
|
||||
return{x:box.x+(box.w-w)/2,y:box.y+(box.h-h)/2,w,h};}
|
||||
function coverIn(p,winR){const r=p.w/p.h,wr=winR.w/winR.h;let w,h;
|
||||
if(wr>r){h=winR.h;w=h*r;}else{w=winR.w;h=w/r;}
|
||||
return{x:(winR.w-w)/2,y:(winR.h-h)/2,w,h};} /* 相对 win 的坐标 */
|
||||
const setBox=(el,g)=>{el.style.left=g.x+'px';el.style.top=g.y+'px';
|
||||
el.style.width=g.w+'px';el.style.height=g.h+'px';};
|
||||
|
||||
/* ===== 开箱:从缩略图原位起飞 ===== */
|
||||
function fillMeta(p){$('cap').textContent=p.cap;$('ttl').textContent=p.title;
|
||||
$('exif').textContent=p.exif;$('count').textContent=`${idx+1} / ${N}`;
|
||||
$('aOrig').href=p.src;$('aDl').href=p.src;$('aDl').setAttribute('download',p.title+'.jpg');}
|
||||
function openLbx(i){
|
||||
idx=i;const p=PHOTOS[idx];pic.src=p.src;pic.alt=p.title;fillMeta(p);
|
||||
const t=thumbImg(i).getBoundingClientRect();
|
||||
const tr=fitRect(p,stageBox());
|
||||
zoom={s:1,tx:0,ty:0};pic.style.transform='none';pic.classList.remove('zoomed','grabbing','swap');
|
||||
lbx.hidden=false;open_=true;document.body.classList.add('locked');
|
||||
lbx.classList.add('landing');
|
||||
setBox(win,t);setBox(pic,coverIn(p,t)); /* 起点:与缩略图逐像素同框(cover 裁切) */
|
||||
if(reduced){lbx.classList.add('on');lbx.classList.remove('landing');setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h});}
|
||||
else{
|
||||
void win.offsetWidth; /* 强制 reflow,起点先落定 */
|
||||
lbx.classList.add('on');
|
||||
setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h}); /* 终点:舞台 contain */
|
||||
setTimeout(()=>lbx.classList.remove('landing'),330);
|
||||
}
|
||||
if(!hintShown){hintShown=true;const h=$('hint');h.classList.add('show');
|
||||
setTimeout(()=>h.classList.remove('show'),2600);}
|
||||
}
|
||||
|
||||
/* ===== 关闭:回落到缩略图原位 ===== */
|
||||
function closeLbx(){
|
||||
if(!open_)return;
|
||||
const p=PHOTOS[idx],t=thumbImg(idx)?.getBoundingClientRect();
|
||||
instantResetZoom();
|
||||
if(reduced||!t){lbx.hidden=true;lbx.classList.remove('on','clean','noui');open_=false;
|
||||
document.body.classList.remove('locked');return;}
|
||||
lbx.classList.add('landing');
|
||||
setBox(win,t);setBox(pic,coverIn(p,t));
|
||||
lbx.classList.remove('on','clean','noui');
|
||||
setTimeout(()=>{lbx.hidden=true;lbx.classList.remove('landing');open_=false;
|
||||
document.body.classList.remove('locked');},320);
|
||||
}
|
||||
|
||||
/* ===== 换片:旧图淡出、新图自侧向 10% 淡入(spacing 节奏) ===== */
|
||||
function show(i,dir){
|
||||
idx=((i%N)+N)%N;const p=PHOTOS[idx];instantResetZoom();fillMeta(p);
|
||||
pic.src=p.src;pic.alt=p.title;
|
||||
const tr=fitRect(p,stageBox());setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h});
|
||||
if(!reduced){pic.classList.add('swap');
|
||||
pic.style.transform=`translateX(${dir*-10}%)`;
|
||||
void pic.offsetWidth;pic.classList.remove('swap');
|
||||
pic.style.transform='none';}
|
||||
}
|
||||
|
||||
/* ===== 缩放:滚轮锚点 / 双击 / 双指 / 拖拽(复用线上 v2 数学) ===== */
|
||||
function applyZoom(){pic.style.transform=`translate(${zoom.tx}px,${zoom.ty}px) scale(${zoom.s})`;
|
||||
pic.classList.toggle('zoomed',zoom.s!==1);
|
||||
lbx.classList.toggle('clean',zoom.s!==1);}
|
||||
function instantResetZoom(){const keep=pic.style.transition;pic.style.transition='none';
|
||||
zoom={s:1,tx:0,ty:0};pic.style.transform='none';pic.classList.remove('zoomed');
|
||||
lbx.classList.remove('clean');void pic.offsetWidth;pic.style.transition=keep||'';}
|
||||
function zoomAt(next,cx,cy){const r=pic.getBoundingClientRect();
|
||||
const ax=cx-r.left-r.width/2-zoom.tx,ay=cy-r.top-r.height/2-zoom.ty;
|
||||
const k=next/zoom.s;zoom.tx-=ax*(k-1);zoom.ty-=ay*(k-1);zoom.s=next;applyZoom();zindShow();}
|
||||
let zt;function zindShow(){const z=$('zind');z.textContent=Math.round(zoom.s*100)+'%';
|
||||
z.classList.add('show');clearTimeout(zt);zt=setTimeout(()=>z.classList.remove('show'),900);}
|
||||
lbx.addEventListener('wheel',e=>{if(!open_)return;e.preventDefault();
|
||||
const f=Math.exp(-e.deltaY*0.0015);
|
||||
const next=Math.min(4,Math.max(1,zoom.s*f));
|
||||
if(next!==zoom.s)zoomAt(next,e.clientX,e.clientY);},{passive:false});
|
||||
win.addEventListener('dblclick',e=>{if(zoom.s===1)zoomAt(2.5,e.clientX,e.clientY);
|
||||
else instantResetZoom();}); /* 挂 win:放大态 setPointerCapture 会把 click 派发给 win,pic 收不到 */
|
||||
|
||||
/* 拖拽平移(放大态)+ 双指缩放:pointer 事件统一 */
|
||||
const pts=new Map();let drag=null,pinch=null;
|
||||
win.addEventListener('pointerdown',e=>{
|
||||
pts.set(e.pointerId,{x:e.clientX,y:e.clientY});
|
||||
if(pts.size===2){const a=[...pts.values()];
|
||||
pinch={d0:Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y),s0:zoom.s,
|
||||
mx:(a[0].x+a[1].x)/2,my:(a[0].y+a[1].y)/2};drag=null;return;}
|
||||
if(zoom.s>1){win.setPointerCapture(e.pointerId);
|
||||
drag={x:e.clientX,y:e.clientY,tx:zoom.tx,ty:zoom.ty};pic.classList.add('grabbing');}
|
||||
});
|
||||
win.addEventListener('pointermove',e=>{
|
||||
if(pts.has(e.pointerId))pts.set(e.pointerId,{x:e.clientX,y:e.clientY});
|
||||
if(pinch&&pts.size===2){const a=[...pts.values()];
|
||||
const d=Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y);
|
||||
const next=Math.min(4,Math.max(1,pinch.s0*d/pinch.d0));
|
||||
if(next!==zoom.s)zoomAt(next,pinch.mx,pinch.my);return;}
|
||||
if(drag){zoom.tx=drag.tx+e.clientX-drag.x;zoom.ty=drag.ty+e.clientY-drag.y;applyZoom();}
|
||||
});
|
||||
const endPt=e=>{pts.delete(e.pointerId);if(pts.size<2)pinch=null;
|
||||
if(drag){drag=null;pic.classList.remove('grabbing');}};
|
||||
win.addEventListener('pointerup',endPt);win.addEventListener('pointercancel',endPt);
|
||||
|
||||
/* 触屏快滑翻页(zoom=1 时) */
|
||||
let x0=null;lbx.addEventListener('touchstart',e=>{x0=e.touches[0].clientX;},{passive:true});
|
||||
lbx.addEventListener('touchend',e=>{if(x0===null||zoom.s!==1){x0=null;return;}
|
||||
const dx=e.changedTouches[0].clientX-x0;
|
||||
if(Math.abs(dx)>40)show(idx+(dx<0?1:-1),dx<0?1:-1);x0=null;},{passive:true});
|
||||
|
||||
/* ===== 键盘 / 背景点击 / chrome 召唤 ===== */
|
||||
document.addEventListener('keydown',e=>{
|
||||
if(!open_)return;
|
||||
if(e.key==='Escape')closeLbx();
|
||||
else if(e.key==='ArrowLeft')show(idx-1,-1);
|
||||
else if(e.key==='ArrowRight')show(idx+1,1);
|
||||
else if(e.key==='i'||e.key==='I')lbx.classList.toggle('noui');
|
||||
});
|
||||
lbx.addEventListener('click',e=>{
|
||||
if(e.target.closest('.lbx-win')||e.target.closest('.lbx-btn')||
|
||||
e.target.closest('.lbx-meta')||e.target.closest('a'))return;
|
||||
closeLbx(); /* 背景点击关闭(bgClickAction:'close' 同款) */
|
||||
});
|
||||
$('bClose').addEventListener('click',closeLbx);
|
||||
$('bPrev').addEventListener('click',()=>show(idx-1,-1));
|
||||
$('bNext').addEventListener('click',()=>show(idx+1,1));
|
||||
|
||||
/* 开箱中窗口变化:舞台即时重排(不做动画,避免与缩放打架) */
|
||||
addEventListener('resize',()=>{if(!open_||lbx.classList.contains('landing'))return;
|
||||
const p=PHOTOS[idx];const tr=fitRect(p,stageBox());
|
||||
setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h});});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,119 @@
|
||||
# 灯箱美化探索:图片查看体验天花板五站研究(v2.1 输入)
|
||||
|
||||
> 只读调研 + 提案,不动代码。基线 = 灯箱 v2 车道(fit-contain / 滚轮锚点缩放 0.5–5x / 拖拽平移 /
|
||||
> 双击 1x↔2.5x / caption+EXIF 分层 / ←→ 与触摸快滑),提案全部**叠加在 v2 之上,不推翻缩放引擎**。
|
||||
> 配套可交互演示:`design/lightbox-mockup.html`(浏览器直接打开,交互为提案演示)。
|
||||
> 标记:✅ = 静态抓取并核对源码/文档;◐ = 站点可达但查看器 JS 渲染,基于已知稳定形态记录。
|
||||
|
||||
## 一、五站笔记
|
||||
|
||||
### 1. PhotoSwipe(photoswipe.com)✅ 业界标杆 lightbox
|
||||
|
||||
- **A 开合过渡**:`showHideAnimationType: zoom`(默认)= 从缩略图原位起飞到舞台、关闭回落原位;
|
||||
无缩略图时降级 fade;`prefers-reduced-motion` 自动 none。时长 333ms ×3(show/hide/zoom 同拍),
|
||||
曲线 `cubic-bezier(.4,0,.22,1)`,双击缩放共用同曲线(节奏统一的关键)。
|
||||
**cover 裁切缩略图路线**:`object-fit:cover` + `data-cropped` —— 我们的 masonry 正是这种缩略图。
|
||||
- **B 舞台纯净度**:底色实色 + `bgOpacity`(默认 .8,透明度走 JS 合成不走 rgba,性能纪律);
|
||||
文档明示**禁止 backdrop blur**(重性能坑)。`tapAction:'toggle-controls'`:单击即隐全部 chrome,纯看图。
|
||||
- **C 信息层**:核心无 caption(a11y 原因删出 v5),官方 dynamic-caption 插件——"依剩余空放置于
|
||||
图下或图侧":竖图自动把 caption 挪进侧边 letterbox,横图回底部。计数器 `n / N` 极简。
|
||||
- **D 导航**:边缘箭头 + loop + `arrowKeys`;幻灯片间距 `spacing:.1`(10% 视口,换片有"路过"感);
|
||||
`allowPanToNext`:放大态继续滑 = 翻页(先耗尽平移余量再走片)。
|
||||
- **E 微交互**:`preload:[1,2]` 相邻预载;`preloaderDelay:2000`——加载指示 2s 后才出现(快图不打扰);
|
||||
`maxWidthToAnimate:4000` 超宽图不做起飞动画(防掉帧)。图标自带描边阴影,深浅底通吃。
|
||||
- **F 移动端**:`pinchToClose`(捏合关闭)、`closeOnVerticalDrag`(竖拖关闭)、双指缩放到 max 档;
|
||||
`trapFocus + returnFocus`(焦点圈与归还,原生 `<dialog>` 免费同款)。
|
||||
|
||||
### 2. medium-zoom(francoischalifour.com)✅ Medium 的克制哲学
|
||||
|
||||
- **A**:点击 = 从缩略图位置 FLIP 放大到视口居中(margin 默认 0 贴边),关闭回落原位。
|
||||
与 PhotoSwipe 同为"原位起飞",但它只有这一种动画——整库只做一件事。
|
||||
- **B**:默认底色 `#fff`(Medium 白,页面即舞台);无任何 chrome、无 caption、无导航。
|
||||
**克制到"查看器不存在"**:这就是它的设计立场。
|
||||
- **C**:零信息层;`template` 选项可注入自定义布局(Facebook / Dropbox Paper 官方范例)——
|
||||
证明"纯放大内核 + 自定义信息壳"是可行分层。
|
||||
- **D**:无连拍导航(单图语义);`scrollOffset:40`:**滚动页面即关闭**,把"关"交给最顺手的手势。
|
||||
- **E**:HD 模式 `data-zoom-src`:页内小图、开箱瞬间换高清(同宽高比前提),预览零等待。
|
||||
- **F**:移动端点按开/点按关,无多余手势;无缩放(一档即满)。
|
||||
|
||||
### 3. Unsplash 照片页 ◐(curl 被 bot 墙拦截,未逐项复核)
|
||||
|
||||
- **A**:列表→详情是页面跳转非灯箱;大图页首屏即图主体,顶部细导航浮层滚动隐现。
|
||||
- **B**:图外留白极小,信息全靠**悬浮层**:右侧竖排操作列(下载/尺寸选择/点赞/加合集/ⓘ),
|
||||
不占图也不占底。
|
||||
- **C**:ⓘ 召唤 Info 面板:相机/镜头/光圈/快门/ISO/焦距/发布时间/浏览数全量 EXIF;
|
||||
平时全隐——"轻常驻 + 重召唤"的范本。下载默认给适配尺寸,原图藏在尺寸菜单。
|
||||
- **D**:集合/相册内 ←→ 键连续浏览;相关照片网格殿后(发现回路)。
|
||||
- **E**:渐进加载 + blur-up 占位是全站基建;L 键点赞的快捷键 affordance。
|
||||
- **F**:移动端操作列收进底部栏,Info 面板变全屏抽屉。
|
||||
|
||||
### 4. 图虫 ◐(主站可达,查看器 JS 渲染未复核)
|
||||
|
||||
- **A**:信息流点击 → 全屏查看器直开(社区产品重内容直达,弱化过渡本身)。
|
||||
- **B**:舞台全黑,chrome 依赖手势(边缘滑动)而非常驻按钮。
|
||||
- **C**:摄影师视角分层:caption/描述 + **醒目完整 EXIF(机身/镜头/参数条)** + 作者卡(头像/
|
||||
关注)+ 标签 + 社区数据(赞/藏)——信息密度最高,但靠 JS 分层收纳(常驻只留作者卡)。
|
||||
- **D**:同组连拍左右滑 + 组内缩略导航;键盘支持弱于 PhotoSwipe/Flickr。
|
||||
- **E**:加载有占位图(信息流即高清小图,开箱升级)。
|
||||
- **F**:App 化手势(快滑翻页带跟手位移);双指缩放齐全。
|
||||
- 教训面:信息全堆常驻会让暗房变"仪表盘"——我们只取它的 **EXIF 完整度**,不取密度。
|
||||
|
||||
### 5. Flickr(自选)✅ SSR 复核 + ◐ 视觉层
|
||||
|
||||
- **A**:照片 URL **直接开进 lightbox**(无中间页):暗房全屏沉浸,从网格进入有原位起飞感。
|
||||
- **B**:舞台近纯黑(#212121 系);chrome 默认轻常驻:底部左侧 title/作者小卡(可隐),
|
||||
其余全靠召唤。
|
||||
- **C**:三段式:常驻小卡(title+作者)→ 右缘动作列(收藏/加画册/分享/下载尺寸/ⓘ)→
|
||||
ⓘ 滑出右侧 EXIF 全量面板(机身/镜头/参数/软件/日期)。20 年沉淀的"常驻最轻、重的全召唤"。
|
||||
- **D**:边缘 hover 箭头 + ←→/Esc/f/i 全键盘;f=收藏的键位文化。
|
||||
- **E**:大图渐进加载;浏览数/评论在下折区(不抢舞台)。
|
||||
- **F**:移动端 swipe 翻页、双指缩放、上滑收 chrome——三手势覆盖 95% 操作。
|
||||
- 选它的理由:唯一同时满足"成熟产品 + SSR 可复核 + 键盘流 + 信息分层范本"。
|
||||
|
||||
## 二、差距矩阵(现状 v2 vs 天花板)
|
||||
|
||||
| 维度 | 我们现状(v2) | 天花板做法(出处) | 差距 |
|
||||
|---|---|---|---|
|
||||
| A 开合 | dialog 直开直关,零过渡 | 原位起飞+回落 333ms 单曲线(PS/MZ) | **大** |
|
||||
| B 舞台 | backdrop .95 常驻;meta 恒占底条 | toggle-controls 沉浸(PS);轻常驻重召唤 | 中 |
|
||||
| C 信息层 | 一层底条:caption+title+EXIF+动作平铺 | 双层:常驻轻卡 + 召唤 EXIF(FL/US);caption 就空放置(PS 插件) | 中 |
|
||||
| D 导航 | ←→ 键/两侧钮/触摸滑动/loop ✅ | 同款 + spacing 换片节奏 + 放大态滑透翻页(PS) | 小 |
|
||||
| E 微交互 | 切图 160ms 淡入;光标三态 ✅ | 相邻预载 [1,2]、2s 延迟加载指示、缩放档位语义(PS);HD 交换(MZ) | 中 |
|
||||
| F 移动端 | 快滑翻页 + 双击缩放;**无双指缩放** | pinch 缩放/捏合关/竖拖关(PS)三件套 | **大** |
|
||||
|
||||
## 三、v2.1 增强提案(叠加式,不动 v2 引擎)
|
||||
|
||||
| # | 提案 | 做法 | 灰阶适配 | 工作量 | 优先级 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | **原位起飞开合** | 点击缩略图:FLIP 动画双盒(外盒=裁切窗从缩略图矩形→舞台矩形,内图=cover 几何→contain 几何同步过渡,等价 PS `data-cropped` 路线);300ms `cubic-bezier(.4,0,.22,1)`(与站 `--ease` 同族);关闭回落到当前缩略图位置;reduced-motion 直开直关 | 起飞的是图不是 chrome;backdrop 照旧恒深 | 0.5–1 天 | **P0** |
|
||||
| 2 | **背景点击关闭** | 点击落在图外区域即关(PS `bgClickAction:'close'` 同款),Esc 保留 | 无新增视觉 | 0.5h | **P0** |
|
||||
| 3 | **chrome 双层淡入 + 召唤** | 开箱:层1(关闭/箭头/计数)+140ms、层2(caption/EXIF/动作)+300ms 淡入;`i` 键或图上单击 toggle 全隐(toggle-controls);zoom>1 自动隐、回 1x 淡回 | 淡入只有 opacity,无位移无色彩 | 0.5 天 | P1 |
|
||||
| 4 | **相邻预载** | 开箱后 idle 预载 ±1 张 fullSrc(PS `preload` 精神,我们取 [1,1] 克制档) | 纯网络层 | 数行 | P1 |
|
||||
| 5 | **双指缩放(移动端)** | pointer events 双指距离比→锚点缩放(复用 v2 `zoomAt` 数学);v2 已有 `touch-action:none` 地基 | 同 v2 | 0.5 天 | P1 |
|
||||
| 6 | **换片过渡** | 切片:旧图 140ms 淡出、新图自侧向 10% 位移淡入(PS `spacing:.1` 节奏);v2 换图已回正缩放,天然兼容 | 仅 opacity/transform | 0.5 天 | P2 |
|
||||
| 7 | **缩放指示 + 首开提示** | 缩放停稳后右下 90% 淡入 `168%` 900ms 淡出;首开箱 hint 胶囊"滚轮缩放 · 双击放大 · Esc 关闭"2.6s 自隐 | mono 小字,与 EXIF 同语言 | 数行 | P2 |
|
||||
| 8 | **长图侧置 meta** | 竖图(ratio<0.9)且视口 ≥820px:caption+EXIF 移入侧 letterbox(PS dynamic-caption"就空放置");横图维持底部条 | 文字色不变,只挪位 | 0.5–1 天 | P2 |
|
||||
| 9 | **HD 交换** | zoom>1.5x 懒换 `/photos-originals/` 原图,<1.5x 回 1440(MZ `data-zoom-src` 模式);404 兜底留 1440 | 纯数据层 | 0.5 天 | P2 |
|
||||
| 10 | **移动竖拖关闭** | zoom=1 时竖向拖 >80px 关闭并回落缩略图(PS `closeOnVerticalDrag`) | 同关闭回落 | 0.5 天 | P2 |
|
||||
|
||||
落地注:生产保持原生 `<dialog>`(免费 focus-trap/returnFocus,PS 同款能力);起飞动画在 dialog 内
|
||||
用 fixed 双盒实现,mockup 用 fixed 覆盖层纯为演示等价几何。
|
||||
|
||||
## 四、明确不做清单(与灰阶克制冲突)
|
||||
|
||||
- 粒子/彩光/视差/自动播放/轮播——舞台只属于照片
|
||||
- backdrop blur——PhotoSwipe 文档明示重性能坑,且暗底已够沉浸
|
||||
- 缩略图条(thumbstrip)——21 张规模用不上,常驻 chrome +1 与克制冲突(>50 张再议)
|
||||
- 社交分享按钮——PS v5 自己都删了;博客无此回路
|
||||
- tile deep-zoom / 万像素级缩放——仓内照片 2400px 封顶,用不到
|
||||
- 全屏按钮——PS v5 已删(浏览器 F11 更顺);Esc + 沉浸足够
|
||||
- 音效、进度条、弹跳物理回弹——动效纪律:全站唯一曲线
|
||||
- chrome 染 accent 色——灯箱恒深恒灰,强调色止于站内链接
|
||||
|
||||
## 五、验证记录
|
||||
|
||||
- PhotoSwipe:photoswipe.com 七页 + unpkg v5.4.4 `photoswipe.esm.js` defaultOptions 源码核对(✅)
|
||||
- medium-zoom:francoischalifour.com options/HD 指南 + GitHub README(✅)
|
||||
- Unsplash:curl 被 bot 检测墙拦截,标记 ◐;图虫:详情页 JS 渲染不可静态复核,标记 ◐
|
||||
- Flickr:照片页 SSR 抓取成功(photo-model/person-model JSON 在源码中),视觉层标 ◐
|
||||
- mockup:单文件无依赖;图片引用 `../content/photos/` 仓内实拍(相对路径,需与仓库同置打开)
|
||||
Reference in new issue
Block a user