效實(shí)戰(zhàn):愛(ài)心信封打開(kāi)UI動(dòng)畫(huà)的3D變換與粒子系統(tǒng)實(shí)現(xiàn))
1. 一個(gè)信封動(dòng)畫(huà)為什么值得單獨(dú)拿出來(lái)講前陣子幫一個(gè)做社交產(chǎn)品的朋友救火他們的節(jié)日活動(dòng)頁(yè)上線前一天運(yùn)營(yíng)臨時(shí)要求把開(kāi)屏的靜態(tài)賀卡換成能打開(kāi)的信封。需求聽(tīng)起來(lái)簡(jiǎn)單一個(gè)信封點(diǎn)一下翻蓋打開(kāi)愛(ài)心飄出來(lái)。結(jié)果前端同學(xué)折騰了一下午做出來(lái)的效果要么像紙片被硬掰開(kāi)要么愛(ài)心飄得像PPT動(dòng)畫(huà)最后找到我。這類愛(ài)心信封打開(kāi)UI動(dòng)畫(huà)特效看著是個(gè)小玩意實(shí)際上它是前端動(dòng)效里非常典型的一個(gè)綜合題它同時(shí)涉及3D變換、路徑動(dòng)畫(huà)、粒子系統(tǒng)、緩動(dòng)曲線、時(shí)序編排這幾塊內(nèi)容。做得好用戶會(huì)忍不住多點(diǎn)兩下做得糙一眼就是廉價(jià)模板。我后來(lái)花了大概兩個(gè)小時(shí)重構(gòu)了一版把關(guān)鍵點(diǎn)整理出來(lái)這篇就當(dāng)作一次完整的項(xiàng)目復(fù)盤(pán)。先說(shuō)清楚這個(gè)特效到底是什么、能干什么。它本質(zhì)上是一段可嵌入網(wǎng)頁(yè)或App的交互式動(dòng)畫(huà)組件初始狀態(tài)是一個(gè)合上的信封封口處有一顆愛(ài)心或者一個(gè)蠟封用戶點(diǎn)擊或自動(dòng)觸發(fā)后信封蓋沿頂部邊緣向后翻轉(zhuǎn)信封內(nèi)部升起一張卡片或一束愛(ài)心粒子同時(shí)伴隨輕微的縮放和光暈反饋。它解決的問(wèn)題是——在節(jié)日祝福、表白互動(dòng)、會(huì)員權(quán)益開(kāi)啟、消息拆封這類場(chǎng)景里用一個(gè)有儀式感的動(dòng)效替代干巴巴的彈窗提升用戶的情緒峰值和停留時(shí)長(zhǎng)。適合誰(shuí)看如果你是有一定CSS和JavaScript基礎(chǔ)的前端想搞清楚這類動(dòng)效背后的實(shí)現(xiàn)邏輯這篇能直接抄作業(yè)如果你是剛?cè)腴T的新手也能從里面搞明白為什么我的動(dòng)畫(huà)看起來(lái)就是不對(duì)勁這個(gè)經(jīng)典問(wèn)題。我會(huì)把方案選型、參數(shù)計(jì)算、踩坑記錄全部攤開(kāi)講盡量做到你照著做就能復(fù)現(xiàn)。2. 方案選型為什么我最后沒(méi)用現(xiàn)成的動(dòng)畫(huà)庫(kù)2.1 三種主流實(shí)現(xiàn)路徑的取舍接到這個(gè)需求擺在面前的路其實(shí)有三條我先把它們擺出來(lái)對(duì)比一下這也是我做任何動(dòng)效項(xiàng)目前的固定動(dòng)作。方案實(shí)現(xiàn)方式優(yōu)勢(shì)劣勢(shì)適用場(chǎng)景純CSS動(dòng)畫(huà)transform transition keyframes性能好、代碼輕、無(wú)需依賴時(shí)序控制弱、復(fù)雜路徑難做簡(jiǎn)單翻轉(zhuǎn)、淡入淡出JS動(dòng)畫(huà)庫(kù)引入第三方動(dòng)效庫(kù)驅(qū)動(dòng)時(shí)間軸強(qiáng)大、緩動(dòng)豐富體積增加、定制受限復(fù)雜時(shí)序編排Canvas/WebGL逐幀繪制粒子與圖形粒子效果自由度高開(kāi)發(fā)成本高、文字渲染麻煩大量粒子、3D場(chǎng)景我最終的選擇是混合方案信封本體的翻轉(zhuǎn)用CSS 3D transform愛(ài)心粒子的飄散用輕量的DOMCSS動(dòng)畫(huà)時(shí)序編排用原生JavaScript的requestAnimationFrame配合transitionend事件來(lái)串。為什么不上動(dòng)畫(huà)庫(kù)因?yàn)檫@個(gè)特效的時(shí)序其實(shí)只有四五個(gè)階段引入一個(gè)幾十KB的庫(kù)屬于殺雞用牛刀而且活動(dòng)頁(yè)對(duì)首屏體積很敏感能省則省。這里有個(gè)經(jīng)驗(yàn)不是所有動(dòng)畫(huà)都值得上庫(kù)。判斷標(biāo)準(zhǔn)很簡(jiǎn)單——如果你的動(dòng)畫(huà)階段少于6個(gè)、沒(méi)有復(fù)雜的路徑跟隨和物理模擬原生方案完全夠用而且可控性更強(qiáng)。反過(guò)來(lái)如果你要做的是幾十個(gè)元素錯(cuò)落有致的編排那老老實(shí)實(shí)上庫(kù)別硬扛。2.2 為什么堅(jiān)持用CSS 3D而不是2D模擬信封翻蓋這個(gè)動(dòng)作很多人第一反應(yīng)是用2D的scaleY從1變到0來(lái)壓扁蓋子模擬翻轉(zhuǎn)。我試過(guò)效果很假因?yàn)檎鎸?shí)的翻蓋是繞一條軸在三維空間里轉(zhuǎn)透視關(guān)系會(huì)變化。所以必須用transform: rotateX()配合perspective。關(guān)鍵點(diǎn)在于transform-origin的設(shè)置。信封蓋是繞頂部邊緣翻轉(zhuǎn)的所以原點(diǎn)要設(shè)在top而不是默認(rèn)的center。這一點(diǎn)如果搞錯(cuò)蓋子會(huì)從中間對(duì)折看起來(lái)像信封被腰斬。我見(jiàn)過(guò)不少新手栽在這里調(diào)半天以為是緩動(dòng)的問(wèn)題其實(shí)是原點(diǎn)錯(cuò)了。.envelope-flap { transform-origin: top center; transform-style: preserve-3d; transition: transform 0.6s cubic-bezier(0.68, -0.55, 0.27, 1.55); } .envelope-flap.open { transform: rotateX(-180deg); }注意那個(gè)cubic-bezier我特意用了一個(gè)帶回彈的曲線。信封蓋打開(kāi)時(shí)如果勻速轉(zhuǎn)過(guò)去會(huì)顯得很機(jī)械加一點(diǎn)過(guò)沖再回彈就有了紙張的彈性感。這個(gè)曲線參數(shù)是我調(diào)了七八次才定下來(lái)的后面會(huì)詳細(xì)講怎么調(diào)。2.3 愛(ài)心粒子DOM還是Canvas愛(ài)心飄散這塊我糾結(jié)了一下。用Canvas畫(huà)粒子性能最好但要處理文字、要處理不同DPR、還要自己寫(xiě)粒子生命周期代碼量翻倍。而這個(gè)特效里愛(ài)心數(shù)量不多大概15到25個(gè)用DOM元素配合CSS動(dòng)畫(huà)完全扛得住而且每個(gè)愛(ài)心還能單獨(dú)加旋轉(zhuǎn)、縮放、透明度變化寫(xiě)起來(lái)直觀。所以結(jié)論是粒子數(shù)量在50個(gè)以內(nèi)優(yōu)先DOM超過(guò)50個(gè)再考慮Canvas。這是個(gè)很實(shí)用的經(jīng)驗(yàn)閾值別一上來(lái)就上Canvas很多時(shí)候是過(guò)度設(shè)計(jì)。3. 核心細(xì)節(jié)拆解讓動(dòng)畫(huà)高級(jí)的幾個(gè)關(guān)鍵點(diǎn)3.1 透視距離perspective的取值邏輯perspective決定了3D效果的鏡頭距離。值越小透視越夸張翻轉(zhuǎn)看起來(lái)越近值越大越接近正交投影看起來(lái)越平。這個(gè)參數(shù)沒(méi)有標(biāo)準(zhǔn)答案但有個(gè)經(jīng)驗(yàn)公式perspective大約取信封寬度的1.5到2.5倍。假設(shè)信封寬度是300px那perspective取450到750px之間比較舒服。我實(shí)測(cè)下來(lái)取600px左右時(shí)翻蓋的立體感剛好不會(huì)因?yàn)橥敢曁珡?qiáng)導(dǎo)致蓋子邊緣變形夸張。如果你把perspective設(shè)成100px蓋子會(huì)像被廣角鏡頭懟臉拍非常怪。.envelope-wrapper { perspective: 600px; perspective-origin: center center; }perspective-origin默認(rèn)是中心一般不用改。但如果你希望觀察視角稍微偏上模擬人從上往下看信封可以把origin的Y值調(diào)到40%左右會(huì)更有俯視感。3.2 緩動(dòng)曲線的選擇與自定義緩動(dòng)曲線是動(dòng)效的靈魂這句話一點(diǎn)不夸張。同一個(gè)位移用linear和用ease-out觀感天差地別。這個(gè)項(xiàng)目里我用到了三種曲線分別對(duì)應(yīng)不同階段翻蓋打開(kāi)cubic-bezier(0.68, -0.55, 0.27, 1.55)帶過(guò)沖回彈模擬紙張彈性愛(ài)心升起cubic-bezier(0.25, 0.46, 0.45, 0.94)先快后慢模擬上浮減速整體縮放反饋cubic-bezier(0.34, 1.56, 0.64, 1)輕微彈跳那個(gè)帶負(fù)值的貝塞爾曲線第一個(gè)控制點(diǎn)Y是-0.55意味著動(dòng)畫(huà)會(huì)先往回走一點(diǎn)再?zèng)_出去這就是回彈感的來(lái)源。你可以用在線貝塞爾調(diào)試工具慢慢拖但我建議直接記住幾個(gè)常用的回彈用(0.68, -0.55, 0.27, 1.55)柔和減速用(0.25, 0.46, 0.45, 0.94)快速啟動(dòng)用(0.4, 0, 0.2, 1)。提示緩動(dòng)曲線不要貪多一個(gè)動(dòng)效里用超過(guò)三種曲線會(huì)顯得雜亂。通常進(jìn)入用減速、退出用加速、強(qiáng)調(diào)用回彈就夠了。3.3 時(shí)序編排讓動(dòng)作有先后而不是一鍋粥新手做動(dòng)效最容易犯的錯(cuò)就是所有元素同時(shí)動(dòng)。信封蓋在翻、愛(ài)心在飄、卡片在升全擠在一起看起來(lái)就是一團(tuán)亂。真實(shí)的動(dòng)效是有節(jié)奏的像交響樂(lè)有先后、有主次。這個(gè)特效我編排的時(shí)序是這樣的0ms用戶點(diǎn)擊信封整體輕微縮小0.95倍給一個(gè)被按下的反饋持續(xù)150ms150ms信封蓋開(kāi)始翻轉(zhuǎn)持續(xù)600ms400ms蓋子翻到一半時(shí)愛(ài)心粒子開(kāi)始從信封口升起每個(gè)愛(ài)心錯(cuò)開(kāi)30到80ms700ms卡片從信封內(nèi)升起持續(xù)500ms1000ms整體恢復(fù)原始縮放光暈淡出注意第3步愛(ài)心是在蓋子翻到一半時(shí)就開(kāi)始飄的不是等蓋子完全打開(kāi)。這樣有溢出的感覺(jué)更自然。如果等蓋子全開(kāi)再飄愛(ài)心會(huì)有明顯的停頓感。實(shí)現(xiàn)上我用了一個(gè)簡(jiǎn)單的時(shí)序控制器基于setTimeout和transitionend組合function playEnvelopeAnimation() { const wrapper document.querySelector(.envelope-wrapper); const flap document.querySelector(.envelope-flap); const hearts document.querySelectorAll(.heart-particle); const card document.querySelector(.envelope-card); wrapper.classList.add(pressed); setTimeout(() { wrapper.classList.remove(pressed); flap.classList.add(open); }, 150); setTimeout(() { hearts.forEach((heart, i) { setTimeout(() heart.classList.add(float), i * 50); }); }, 400); setTimeout(() { card.classList.add(rise); }, 700); setTimeout(() { wrapper.classList.add(glow); }, 1000); }這段代碼不復(fù)雜但時(shí)序的毫秒數(shù)是我反復(fù)調(diào)出來(lái)的。你可以把它當(dāng)成一個(gè)模板改數(shù)字就能調(diào)整節(jié)奏。3.4 愛(ài)心粒子的隨機(jī)化處理如果所有愛(ài)心都從同一個(gè)點(diǎn)、以同樣的速度、沿同樣的路徑飄出去看起來(lái)就像復(fù)制粘貼。真實(shí)感來(lái)自隨機(jī)。我給每個(gè)愛(ài)心加了三個(gè)維度的隨機(jī)水平偏移-40px到40px之間隨機(jī)旋轉(zhuǎn)角度-30deg到30deg隨機(jī)動(dòng)畫(huà)時(shí)長(zhǎng)1.2s到1.8s隨機(jī)hearts.forEach((heart) { const offsetX (Math.random() - 0.5) * 80; const rotate (Math.random() - 0.5) * 60; const duration 1.2 Math.random() * 0.6; heart.style.setProperty(--offset-x, ${offsetX}px); heart.style.setProperty(--rotate, ${rotate}deg); heart.style.setProperty(--duration, ${duration}s); });配合CSS變量使用.heart-particle.float { animation: floatUp var(--duration) cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } keyframes floatUp { 0% { transform: translate(0, 0) rotate(0) scale(0.5); opacity: 0; } 20% { opacity: 1; } 100% { transform: translate(var(--offset-x), -180px) rotate(var(--rotate)) scale(1); opacity: 0; } }這個(gè)隨機(jī)化是讓動(dòng)畫(huà)活起來(lái)的關(guān)鍵。我見(jiàn)過(guò)太多人做完發(fā)現(xiàn)效果很死板問(wèn)題往往就出在沒(méi)做隨機(jī)。4. 完整實(shí)操?gòu)牧愦钜粋€(gè)能跑的信封動(dòng)畫(huà)4.1 結(jié)構(gòu)搭建與層級(jí)規(guī)劃先把HTML骨架搭好。這里的關(guān)鍵是層級(jí)關(guān)系信封蓋必須在最上層z-index最高卡片在信封內(nèi)部愛(ài)心粒子在最外層容器里絕對(duì)定位。div classenvelope-wrapper div classenvelope-body div classenvelope-card祝福內(nèi)容/div div classenvelope-flap/div div classenvelope-front/div /div div classhearts-layer span classheart-particle/span !-- 重復(fù)若干 -- /div /divenvelope-front是信封正面的下半部分它要蓋住卡片讓卡片看起來(lái)是藏在信封里的。翻蓋打開(kāi)后卡片從envelope-front后面升起來(lái)。這個(gè)遮擋關(guān)系如果搞錯(cuò)卡片會(huì)浮在信封外面穿幫。4.2 信封本體的CSS實(shí)現(xiàn)信封本體我用純CSS畫(huà)不用圖片這樣縮放不失真、顏色好改。.envelope-body { position: relative; width: 300px; height: 200px; background: linear-gradient(135deg, #ff8fa3, #ff5c7a); border-radius: 8px; transform-style: preserve-3d; transition: transform 0.15s ease; } .envelope-wrapper.pressed .envelope-body { transform: scale(0.95); } .envelope-flap { position: absolute; top: 0; left: 0; width: 100%; height: 100px; background: linear-gradient(135deg, #ff6b85, #ff3d5e); clip-path: polygon(0 0, 100% 0, 50% 100%); transform-origin: top center; transform-style: preserve-3d; transition: transform 0.6s cubic-bezier(0.68, -0.55, 0.27, 1.55); z-index: 3; } .envelope-flap.open { transform: rotateX(-180deg); }那個(gè)clip-path把矩形裁成三角形就是信封蓋的形狀。z-index: 3保證它在最上層。翻蓋打開(kāi)后rotateX(-180deg)它會(huì)翻到信封背面去這時(shí)候因?yàn)閎ackface-visibility默認(rèn)可見(jiàn)你會(huì)看到蓋子的背面顏色會(huì)有點(diǎn)怪。解決辦法是給蓋子背面加個(gè)稍深的顏色或者設(shè)置backface-visibility: hidden讓它翻過(guò)去就消失——但消失會(huì)顯得突兀所以我一般給背面單獨(dú)處理。4.3 卡片升起與遮擋處理卡片初始藏在信封里用translateY往下壓配合overflow: hidden或者被envelope-front遮擋。.envelope-card { position: absolute; left: 10px; right: 10px; bottom: 10px; height: 160px; background: #fff; border-radius: 6px; transform: translateY(120px); transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); z-index: 1; } .envelope-card.rise { transform: translateY(-60px); } .envelope-front { position: absolute; bottom: 0; left: 0; width: 100%; height: 120px; background: linear-gradient(135deg, #ff8fa3, #ff5c7a); border-radius: 0 0 8px 8px; z-index: 2; }envelope-front的z-index: 2比卡片高所以卡片升起時(shí)會(huì)被它擋住下半部分看起來(lái)就是從信封里抽出來(lái)的。這個(gè)遮擋是藏的關(guān)鍵很多人卡片直接浮在外面就是漏了這層。4.4 愛(ài)心粒子的生成與動(dòng)畫(huà)愛(ài)心我用CSS的偽元素畫(huà)或者直接用字符。用字符最簡(jiǎn)單但字體依賴用CSS畫(huà)更可控。.heart-particle { position: absolute; left: 50%; bottom: 80px; width: 16px; height: 16px; background: #ff3d5e; transform: rotate(45deg); opacity: 0; } .heart-particle::before, .heart-particle::after { content: ; position: absolute; width: 16px; height: 16px; background: #ff3d5e; border-radius: 50%; } .heart-particle::before { top: -8px; left: 0; } .heart-particle::after { top: 0; left: -8px; }這是經(jīng)典的CSS愛(ài)心畫(huà)法一個(gè)旋轉(zhuǎn)45度的正方形加上兩個(gè)圓角偽元素拼成心形。簡(jiǎn)單可靠顏色好改。4.5 參數(shù)計(jì)算動(dòng)畫(huà)時(shí)長(zhǎng)與錯(cuò)開(kāi)間隔這里講一下時(shí)長(zhǎng)怎么定??倳r(shí)長(zhǎng)控制在1.5到2秒比較合適太短看不清太長(zhǎng)用戶等得煩。我的分配是按壓反饋150ms翻蓋600ms愛(ài)心飄散1200到1800ms錯(cuò)開(kāi)50ms一個(gè)卡片升起500ms光暈300ms愛(ài)心錯(cuò)開(kāi)間隔50ms如果有20個(gè)愛(ài)心最后一個(gè)比第一個(gè)晚1秒開(kāi)始加上自身1.5秒的動(dòng)畫(huà)總時(shí)長(zhǎng)約2.5秒。如果覺(jué)得太長(zhǎng)把錯(cuò)開(kāi)間隔降到30ms或者減少愛(ài)心數(shù)量。這個(gè)平衡點(diǎn)要根據(jù)實(shí)際頁(yè)面來(lái)調(diào)沒(méi)有絕對(duì)標(biāo)準(zhǔn)。注意錯(cuò)開(kāi)間隔不要小于20ms否則人眼分辨不出先后等于同時(shí)動(dòng)隨機(jī)化的意義就沒(méi)了。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 翻蓋動(dòng)畫(huà)穿?;蜷W爍這是最高頻的問(wèn)題。表現(xiàn)是翻蓋轉(zhuǎn)到90度附近時(shí)突然閃一下或者和信封本體穿插。原因通常是transform-style: preserve-3d沒(méi)設(shè)或者父元素的perspective缺失。瀏覽器在2D和3D渲染之間切換時(shí)會(huì)出現(xiàn)這種閃爍。解決辦法確保從perspective容器到翻蓋的整條鏈路上所有需要參與3D變換的元素都設(shè)了transform-style: preserve-3d。另外給翻蓋加will-change: transform能提前讓瀏覽器開(kāi)啟GPU加速減少閃爍。5.2 愛(ài)心飄散方向不對(duì)或位置偏移愛(ài)心應(yīng)該從信封口也就是翻蓋打開(kāi)后的開(kāi)口處升起。如果發(fā)現(xiàn)愛(ài)心從信封中間或者底部冒出來(lái)檢查bottom值。信封高度200px翻蓋占100px開(kāi)口大概在100px高度處所以愛(ài)心的初始bottom設(shè)在80到100px之間比較合適。如果愛(ài)心飄的方向歪了檢查translate的X和Y值。Y必須是負(fù)值向上X是隨機(jī)偏移。有人寫(xiě)成正值愛(ài)心就往下掉了。5.3 移動(dòng)端性能卡頓移動(dòng)端做這類動(dòng)畫(huà)最容易卡在粒子數(shù)量上。20個(gè)DOM元素同時(shí)做transform和opacity動(dòng)畫(huà)中低端機(jī)可能掉幀。優(yōu)化手段只對(duì)transform和opacity做動(dòng)畫(huà)這兩個(gè)屬性走合成層不觸發(fā)重排重繪給粒子加will-change: transform, opacity粒子數(shù)量控制在20個(gè)以內(nèi)避免在動(dòng)畫(huà)過(guò)程中讀取offsetWidth這類會(huì)強(qiáng)制同步布局的屬性我實(shí)測(cè)下來(lái)20個(gè)粒子在主流中端機(jī)上能穩(wěn)定60幀超過(guò)30個(gè)就開(kāi)始有壓力了。5.4 點(diǎn)擊重復(fù)觸發(fā)導(dǎo)致動(dòng)畫(huà)錯(cuò)亂用戶手快連點(diǎn)動(dòng)畫(huà)會(huì)疊加狀態(tài)就亂了。必須加鎖let isAnimating false; function playEnvelopeAnimation() { if (isAnimating) return; isAnimating true; // ... 動(dòng)畫(huà)邏輯 setTimeout(() { isAnimating false; }, 2500); }這個(gè)鎖的釋放時(shí)間要略大于動(dòng)畫(huà)總時(shí)長(zhǎng)寧可多等一點(diǎn)也別讓動(dòng)畫(huà)沒(méi)播完就解鎖。5.5 常見(jiàn)問(wèn)題速查表問(wèn)題現(xiàn)象可能原因排查方向翻蓋從中間對(duì)折transform-origin沒(méi)設(shè)top檢查原點(diǎn)設(shè)置3D效果扁平缺perspective或值過(guò)大調(diào)整perspective到寬度1.5-2.5倍動(dòng)畫(huà)機(jī)械生硬用了linear緩動(dòng)換成帶緩動(dòng)的貝塞爾曲線愛(ài)心同時(shí)出現(xiàn)沒(méi)做錯(cuò)開(kāi)延遲加setTimeout錯(cuò)開(kāi)卡片浮在信封外遮擋層級(jí)錯(cuò)誤檢查z-index和envelope-front移動(dòng)端掉幀粒子過(guò)多或?qū)傩圆划?dāng)減數(shù)量、只用transform/opacity連點(diǎn)動(dòng)畫(huà)錯(cuò)亂沒(méi)加狀態(tài)鎖加isAnimating標(biāo)志位6. 進(jìn)階玩法與可擴(kuò)展方向6.1 加入音效與觸覺(jué)反饋動(dòng)效做到位之后加一個(gè)輕微的拆封音效情緒價(jià)值會(huì)翻倍。用Web Audio API或者簡(jiǎn)單的audio標(biāo)簽都行。移動(dòng)端還能調(diào)用振動(dòng)API給一個(gè)短振動(dòng)模擬拆信封的手感。不過(guò)音效要謹(jǐn)慎默認(rèn)靜音、用戶交互后再播放別一進(jìn)頁(yè)面就響那會(huì)被罵。6.2 用CSS變量做主題化把顏色、時(shí)長(zhǎng)、粒子數(shù)量都抽成CSS變量一套代碼就能適配不同節(jié)日主題。情人節(jié)用粉色春節(jié)用紅色生日用金色。改幾個(gè)變量的事不用動(dòng)結(jié)構(gòu)。:root { --envelope-color: #ff5c7a; --envelope-color-dark: #ff3d5e; --heart-color: #ff3d5e; --flap-duration: 0.6s; }6.3 響應(yīng)式適配信封尺寸在小屏上要縮小。用clamp()或者媒體查詢都行。關(guān)鍵是perspective要跟著寬度變否則小屏上透視會(huì)過(guò)強(qiáng)??梢杂肑S動(dòng)態(tài)計(jì)算也可以直接用相對(duì)單位。.envelope-wrapper { perspective: calc(var(--envelope-width) * 2); }6.4 無(wú)障礙與降級(jí)不是所有用戶都喜歡動(dòng)畫(huà)系統(tǒng)層面有減少動(dòng)態(tài)效果的設(shè)置。用prefers-reduced-motion媒體查詢做降級(jí)對(duì)這類用戶直接顯示靜態(tài)信封點(diǎn)擊后瞬間切換狀態(tài)不做動(dòng)畫(huà)。這是專業(yè)度的體現(xiàn)也是很多團(tuán)隊(duì)會(huì)忽略的點(diǎn)。media (prefers-reduced-motion: reduce) { .envelope-flap, .heart-particle, .envelope-card { transition: none; animation: none; } }我在實(shí)際項(xiàng)目里踩過(guò)的坑基本都寫(xiě)在上面的排查表里了。這個(gè)信封動(dòng)畫(huà)看著簡(jiǎn)單但真正把它做高級(jí)靠的是對(duì)透視、緩動(dòng)、時(shí)序、隨機(jī)化這幾個(gè)點(diǎn)的理解而不是堆代碼量。你把這幾個(gè)點(diǎn)吃透做其他動(dòng)效也是同樣的思路。最后分享一個(gè)小技巧調(diào)動(dòng)畫(huà)的時(shí)候把瀏覽器開(kāi)發(fā)者工具里的動(dòng)畫(huà)速度調(diào)慢到0.25倍很多平時(shí)看不出來(lái)的細(xì)節(jié)問(wèn)題會(huì)暴露得一清二楚這個(gè)習(xí)慣幫我省了大量返工時(shí)間。