01先拆解,再写代码
P5 主菜单的每一项看起来像被撕下来贴上去的,实际上拆开只有三个 CSS 属性在工作:
transform: skewX(-10deg)—— 让方块变斜clip-path: polygon(...)—— 裁掉一个角,形成纸片- 内层再反向
skewX(10deg)—— 让文字保持正立
第三条是新手最容易漏的:外层斜切后,文字也会跟着歪,所以内容要包一层反向变换。
html
<a class="p5-btn" href="#"><span>阅读全文</span></a>css
.p5-btn { transform: skewX(-10deg); }
.p5-btn > span { display: inline-block; transform: skewX(10deg); }02选中态:一块滑出来的黑板
真正让菜单「活」的是选中态。不要用边框变色,做法是给按钮加一个伪元素当底板,用宽度动画滑出来:
css
.nav-item { position: relative; padding-left: 24px; }
.nav-item::before {
content: "";
position: absolute; left: 4px; top: 50%;
width: 10px; height: 10px;
background: var(--red);
transform: translateY(-50%) skewX(-20deg);
opacity: 0; transition: .16s;
}
.nav-item:hover::before, .nav-item.on::before { opacity: 1; }03撕纸字:中英文都要能玩
英文可以换字体(本站用的是 Persona5FanRansom),但中文没有对应字体,所以真正的解法是按字符包 span,给每个字随机旋转和底色:
js
// 把「怪盗笔记」拆成 4 个可独立旋转的字
[..."怪盗笔记"].forEach((ch, i) => {
const s = document.createElement('span');
s.className = 'ch ' + (i % 3 === 0 ? 'box' : i % 3 === 1 ? 'red' : '');
s.style.setProperty('--t', `rotate(${(i % 2 ? 1 : -1) * 1.6}deg)`);
s.textContent = ch;
host.appendChild(s);
});
双叶
把文字拆成 DOM 节点的写法性能不太行吧,长标题会掉帧。

我
所以只拆标题,正文一个节点都不动——首屏 JS 至今不到 30KB。
04转场:一帧都不能拖
P5 的切屏是「斜切三色擦除」,用三条动画延迟 70ms 的色块就能复刻:
css
@keyframes wipeSweep {
0% { transform: skewX(-12deg) translateX(-190%); }
55% { transform: skewX(-12deg) translateX(0); }
100% { transform: skewX(-12deg) translateX(400%); }
}时长定在 450ms:再慢就变成等待,再快就看不见颜色了。
05半调网点:一张背景图都不用
P5 的红色背景上永远铺着一层细网点,用 CSS 渐变就能生成:
css
.hero-dots {
background-image: radial-gradient(rgba(0,0,0,.55) 1.6px, transparent 1.7px);
background-size: 9px 9px;
mix-blend-mode: multiply; /* 关键:让网点吃进下层颜色 */
}两点经验:网点间距别超过 10px,否则会看到明显的摩尔纹;动画只改 background-position,永远不要动尺寸——那是唯一不会引发布局抖动的做法。
06性能与可访问性的底线
| 项目 | 目标 | 做法 |
|---|---|---|
| 动画开销 | 全部在合成层 | 只动 transform / opacity |
| 首屏 JS | < 30KB | 不引框架,只渲染可见内容 |
| 键盘可达 | Tab 走全站 | 用语义标签,不拦截默认行为 |
| 减弱动效 | 系统开关生效 | prefers-reduced-motion |

双叶
所以到底该不该用 clip-path?

我
能用 transform 就别用它——低端手机上会触发整块重绘。先问自己:这个形状真的必须裁吗?
