/*! * 图片灯箱样式 * Yongquan Yang * * ── 配色为什么做成两套 ────────────────────────────────────── * 深色底适合看照片:周围全黑时人眼对图里颜色的判断更准,也不会 * 被白底反射光干扰。浅色底适合看截图和文档图:黑底会让白底截图的 * 边缘发灰、像蒙了一层雾,而且深色底下一张 A4 白文档刺眼。 * 用户明确说「哪种效果好看」,所以两套都做出来、后台可选。 * * ── 为什么用 CSS 变量 ─────────────────────────────────────── * 两套配色只差几个颜色值,用变量可以只写一遍布局。 * 注意:主题**没有**全局 CSS 变量(主色各处硬编码),所以这里的 * 变量全部**定义在 .yang-lb 自己身上**,不依赖外部。 * * ── 层级 ──────────────────────────────────────────────────── * z-index 取 99999:主题导航栏用的是 1000 级别,layer.js 弹层是 * 19891015 以下。灯箱必须盖住**所有**内容(用户要求「把页面其他 * 内容都盖住」),所以取一个明显高于常规的整数,同时避开 2147483647 * 这类极端值(部分浏览器上会有渲染 bug)。 */ .yang-lb { /* ── 浅色(默认)──────────────────────────────────────────── * 用户明确选了白色底,并要求遮罩**半透明**(约 80%), * 不把底下正文完全盖死,而是「压暗一档、突出照片」。 * * 🔴 半透明带来的真问题(必须一起解决,否则一开就废): * 透明度 < 1 时,遮罩颜色会与**底下透上来的正文**复合。 * 本站正文是白底黑字,透上来的是白 + 黑字。 * · 白底透上来 → 遮罩几乎还是白,底部灰字(--lb-fg-dim)对比度够; * · 但**黑字/深色图片块透上来**时,那一片会变暗, * 灰字压在上面就可能掉到 4.5:1 以下,看不清「3 / 4」。 * 所以不能只调 alpha 就完事:底部说明条与控件必须自己 * 带一块**更实的底**(见 --lb-ctl-bg 与 .yang-lb-foot), * 让文字永远有确定的可读底色,不依赖底下恰好是什么。 * * ⚠️ 透明度用「白色 80% 不透明」= rgba(255,255,255,0.82), * 不是 `opacity: 0.82` —— 用 opacity 会让**整层**(含图片、 * 箭头、文字)一起变半透明,图片会跟着发灰、看不清, * 这不是用户要的效果。用户要的是「底透、图清楚」。 */ --lb-bg: rgba(255, 255, 255, 0.82); --lb-fg: #1f2430; --lb-fg-dim: rgba(31, 36, 48, 0.76); /* 控件底必须**接近不透明**:它盖在图片上方,底下可能是任意颜色 */ --lb-ctl-bg: rgba(255, 255, 255, 0.94); --lb-ctl-bg-hover: #ffffff; --lb-ctl-bd: rgba(31, 36, 48, 0.16); --lb-ctl-fg: #1f2430; --lb-shadow: 0 10px 40px rgba(31, 36, 48, 0.26); /* 底部说明条自己的底色:比遮罩更实,保证文字可读 */ --lb-foot-bg: rgba(255, 255, 255, 0.9); position: fixed; inset: 0; z-index: 99999; display: none; align-items: center; justify-content: center; background-color: var(--lb-bg); /* 背景模糊:让底下页面「退后」但不完全消失,用户仍知道自己没离开。 半透明遮罩下这一条更重要 —— 它把底下文字糊掉, 减少「字压在图上」的视觉干扰,同时保住「我还在这页」的空间感。 */ -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); color: var(--lb-fg); /* 覆盖层自己吃掉所有指针事件,底下的链接不会被误点 */ overscroll-behavior: contain; opacity: 0; transition: opacity 0.18s ease; } /* ── 深色遮罩(可选)───────────────────────────────────────── * 看照片时用这套:周围全黑,人眼对图里颜色的判断更准, * 不会被白底反射光干扰。透明度同样留一点,不全盖死。 */ .yang-lb[data-theme='dark'] { --lb-bg: rgba(12, 14, 18, 0.86); --lb-fg: #f2f4f7; --lb-fg-dim: rgba(242, 244, 247, 0.7); --lb-ctl-bg: rgba(40, 44, 52, 0.94); --lb-ctl-bg-hover: rgba(58, 63, 73, 0.98); --lb-ctl-bd: rgba(255, 255, 255, 0.2); --lb-ctl-fg: #f2f4f7; /* 图片外发光:深色底上没有它,深色图片的边界会糊在背景里 */ --lb-shadow: 0 12px 48px rgba(0, 0, 0, 0.62); --lb-foot-bg: rgba(12, 14, 18, 0.9); } .yang-lb.is-open { display: flex; opacity: 1; } /* 覆盖层打开时禁止底部页面滚动。 ⚠️ 不用 `body { overflow: hidden }` 写死在 CSS 里:那样关闭后 恢复不可控(要和 JS 里记住的原值配合)。JS 负责设置, 这里只兜一层「万一 JS 没设」的情况。 */ body.yang-lb-lock { overflow: hidden; } /* ── 图片舞台 ─────────────────────────────────────────────── */ .yang-lb-stage { /* 🔴 这里从 flex 改成 grid:要让「占位图」和「原图」**叠在同一个格子**里。 用 flex 的话两个子元素会并排(一个在左一个在右),必须绝对定位才能叠, 而 absolute 就不参与居中 → 又要手动算偏移,脆且难调。 grid-area 叠放是最省事的:两图都居中,cell 尺寸由两者较大者决定。 */ display: grid; place-items: center; /* 给左右箭头留出空间,避免大图被箭头压住 */ width: calc(100% - 132px); height: calc(100% - 96px); pointer-events: none; } /* ── 两层图片:占位层 + 原图层 ───────────────────────────── * * ── 为什么要两层 ──────────────────────────────────────────── * 用户反馈:「点一张图之后,先把这个白色背景渲染出来,然后图再出来, * 感觉速度特别慢」。实测(_v40_slow.py,自建慢速 HTTP 服务): * * 遮罩可见 31 ms ← 纯 CSS 底色,一帧就画完,从来不是瓶颈 * 图片有像素 6 秒后仍然没有 * * 线上原图实测最大 3.2 MB(`PUA的两面性与界定-scaled.png`), * 3 Mbps 下要 8 秒以上。这 8 秒里屏幕上是**整片白 + 一个 0×0 的空 img**, * 用户看到的就是「白背景先出来,图后到」。 * * 解法:点开的瞬间先把**正文里那张已经在缓存里的缩略图**放上去。 * 它早就下载并解码过了,0 延迟就有像素 —— 屏幕立刻「有东西」, * 只是糊一点;原图到了再淡入覆盖。这是业界通行的 blur-up, * 视觉上是「先糊后清」的连续过程,而不是「先空后突然出现」。 * * ⚠️ 占位图必须 `pointer-events: none`:它只是一层视觉垫底, * 点击应该落在原图上(点图不关闭),不能被垫底挡住。 */ .yang-lb-ph, .yang-lb-img { /* 叠在同一个格子里 —— 这是两层方案能成立的关键一行 */ grid-area: 1 / 1; display: block; max-width: 100%; max-height: 100%; /* 原图可能小于屏幕:不要为了填满而放大,放大只会变糊 */ width: auto; height: auto; object-fit: contain; } .yang-lb-ph { /* 垫底:放大后自然发糊,不额外加 blur 滤镜 (滤镜在大图上要额外一次全图离屏合成,慢网下雪上加霜) */ pointer-events: none; /* 没拿到缩略图时(比如外链图床)不留一个空框 */ display: none; } .yang-lb-ph.is-on { display: block; } .yang-lb-img { position: relative; /* 压住占位层,确保点击落在原图上 */ z-index: 1; box-shadow: var(--lb-shadow); border-radius: 2px; background-color: transparent; /* 图片本身要能接收点击(点图不关闭),但舞台是 none, 所以这里必须显式改回来 */ pointer-events: auto; /* 原图未就绪时先隐藏:直接显示会看到「半张图」的渐进渲染过程。 用 opacity 而不是 display,是为了让它**参与布局**, 该占的尺寸提前占住,避免 ready 那一刻整层跳动。 */ opacity: 0; } /* 原图已解码完成 → 淡入覆盖占位图 */ .yang-lb-img.is-ready { opacity: 1; transition: opacity 0.18s ease; } /* ⚠️ 占位图在原图就绪后不能立刻 display:none —— 那样 cell 尺寸会 瞬间由「占位图尺寸」变成「原图尺寸」,如果两者不等就有一次跳动。 让它淡出,淡出期间 cell 不会变(两个都在布局里)。 */ .yang-lb-ph.is-fade { opacity: 0; transition: opacity 0.18s ease; } /* ── 加载指示:慢网下必须让用户知道「在加载」而不是「坏了」───── * 3.2 MB 的原图在 3 Mbps 下要 8 秒。这 8 秒里如果只是「糊图慢慢等」, * 用户会怀疑是不是点坏了。一个很轻的脉冲点就够表达「还在来」。 * 用 animationend 之外的纯 CSS,不额外占 JS。 */ .yang-lb-spin { grid-area: 1 / 1; z-index: 2; width: 34px; height: 34px; border-radius: 50%; border: 2px solid var(--lb-ctl-bd); border-top-color: var(--lb-fg); animation: yang-lb-rot 0.8s linear infinite; display: none; pointer-events: none; } .yang-lb.is-loading .yang-lb-spin { display: block; } @keyframes yang-lb-rot { to { transform: rotate(360deg); } } /* ── 左右箭头 ─────────────────────────────────────────────── */ .yang-lb-nav { position: absolute; top: 50%; transform: translateY(-50%); width: 52px; height: 52px; padding: 0; display: flex; align-items: center; justify-content: center; border: 1px solid var(--lb-ctl-bd); border-radius: 50%; background-color: var(--lb-ctl-bg); color: var(--lb-ctl-fg); cursor: pointer; /* 触屏上不要 300ms 延迟 */ touch-action: manipulation; transition: background-color 0.15s ease, transform 0.15s ease; } .yang-lb-prev { left: 24px; } .yang-lb-next { right: 24px; } .yang-lb-nav:hover { background-color: var(--lb-ctl-bg-hover); } .yang-lb-nav:active { transform: translateY(-50%) scale(0.94); } .yang-lb-nav:focus-visible, .yang-lb-close:focus-visible { outline: 2px solid #00a2ff; outline-offset: 2px; } /* ── 关闭按钮 ─────────────────────────────────────────────── */ .yang-lb-close { position: absolute; top: 18px; right: 18px; width: 44px; height: 44px; padding: 0; display: flex; align-items: center; justify-content: center; border: 1px solid var(--lb-ctl-bd); border-radius: 50%; background-color: var(--lb-ctl-bg); color: var(--lb-ctl-fg); cursor: pointer; touch-action: manipulation; transition: background-color 0.15s ease, transform 0.15s ease; } .yang-lb-close:hover { background-color: var(--lb-ctl-bg-hover); transform: rotate(90deg); } /* ── 底部说明与计数 ───────────────────────────────────────── */ /* 🔴 这一条是「半透明遮罩」逼出来的改动。 原来用 `linear-gradient(to top, var(--lb-bg) 30%, transparent)`: 遮罩不透明时看着没问题,但遮罩只有 80% 时, 渐变到 transparent 的那一段**完全没有底**, 说明文字会直接压在透上来的正文上 —— 底下是深色图片或粗标题时 就彻底读不清了。 改成「一块实底 + 上沿渐变过渡」,文字永远落在确定底色上。 */ .yang-lb-foot { position: absolute; left: 0; right: 0; bottom: 0; padding: 14px 76px 18px; display: flex; align-items: baseline; justify-content: center; gap: 14px; background-color: var(--lb-foot-bg); /* 上沿做一小段渐变,避免出现一条生硬的分界线 */ box-shadow: inset 0 1px 0 var(--lb-ctl-bd); pointer-events: none; } .yang-lb-cap { font-size: 13px; line-height: 1.5; color: var(--lb-fg-dim); max-width: 76%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .yang-lb-count { font-size: 13px; line-height: 1.5; color: var(--lb-fg-dim); font-variant-numeric: tabular-nums; flex: none; } /* ── 正文里「可点」的视觉提示 ─────────────────────────────── */ /* 默认加一个放大光标。不用「图片上盖一个放大镜图标」那种做法: 它会在每张图角落加一个浮层,正文一多图就显得很脏。 */ .content a.yang-lb-item { cursor: zoom-in; display: inline-block; /* 默认是 inline,包着块级图时会让父级出现基线空隙 */ line-height: 0; } .content a.yang-lb-item > img { transition: opacity 0.15s ease; } .content a.yang-lb-item:hover > img { opacity: 0.9; } /* ── 窄屏 ─────────────────────────────────────────────────── */ @media (max-width: 720px) { .yang-lb-stage { /* 手机上左右箭头改成半透明压在图上,图片尽量占满宽 */ width: 100%; height: calc(100% - 88px); padding: 0 8px; } .yang-lb-nav { width: 42px; height: 42px; /* 压到图上,给图片腾出宽度。 ⚠️ 这里必须用**接近不透明**的底:箭头此刻直接压在图片上, 底下可能是任意颜色(白纸扫图 / 纯黑夜景都有可能)。 半透明底会随图片内容变化,对比度不可控。 用 0.88 的实底,两套配色各自保证 ≥ 3:1。 */ background-color: rgba(255, 255, 255, 0.88); border-color: rgba(31, 36, 48, 0.18); color: #1f2430; } .yang-lb[data-theme='dark'] .yang-lb-nav { background-color: rgba(24, 27, 33, 0.88); border-color: rgba(255, 255, 255, 0.24); color: #f2f4f7; } .yang-lb-prev { left: 6px; } .yang-lb-next { right: 6px; } .yang-lb-foot { padding: 10px 16px 14px; flex-direction: column; align-items: center; gap: 2px; } .yang-lb-cap { max-width: 100%; font-size: 12px; } } /* ── 尊重「减少动态效果」系统设置 ─────────────────────────── */ @media (prefers-reduced-motion: reduce) { .yang-lb, .yang-lb-img, .yang-lb-ph, .yang-lb-nav, .yang-lb-close { transition: none; animation: none; } /* 转圈是唯一的「还在加载」提示,减少动效下不能整个删掉, 否则慢网用户看不到任何反馈。改成静止的文字色圆环(不那么晃眼)。 */ .yang-lb-spin { animation: none; border-top-color: var(--lb-ctl-bd); border-right-color: var(--lb-fg); } .yang-lb-close:hover { transform: none; } }