Yhazrin 躺在草地上的白色姓名字母上

Yhazrin

所有文章
设计设计 · 前端渐变有了颗粒,为什么就不再像塑料?从 Ian 分享的 ElevenLabs 封面观察出发,拆开色团、灰度颗粒和白色细线;做六张不用图片的交互封面,再回答评论里最具体的问题:缩成手机小图,颗粒还在吗?前端 · 设计图标动效的阻尼调整一个图标的回弹,单独看很舒服。放进 95 个里,它变成了默认质感。先看在线画廊的实拍,再用一个只改阻尼比的对照实验,弄清「过冲」到底由哪个数字决定,以及一条真正拦得住它的约束该长什么样。前端 · 设计纸卡动效拆解打开菜单、翻过卡片、切换场景。先看原样 Demo 和实录,再用一个只改旋转中心的小实验,弄清网页的手感从哪里来。Vord 首页设计稿,来自项目 Docs/EditorialVordmacOS 词汇本 · 离线词典与间隔复习macOS 词汇本OverLaunch 原项目 OWEmblem 矢量标志OverLaunchApple Silicon Mac 的国服守望先锋启动器在 Mac 上启动守望先锋
手绘图标11 个 SVG 图标11 个手绘 SVGMiWarp 当前原生应用的空白首页,来自真实窗口截图MiWarp本地优先的 AI 桌面工作台本地 AI 工作台World 原项目首屏的真实方块空岛场景World可滚动探索的方块作品世界探索方块空岛
MidtownAdam Burgess · Overwatch · Blizzard Entertainment
0:00−1:57
原版小猫
小米

小米

还没有添加图片

智谱

智谱

还没有添加图片

返回看板
×
渐变有了颗粒,为什么就不再像塑料?

设计 · 前端 · 动效

渐变有了颗粒,为什么就不再像塑料?

从 Ian 分享的 ElevenLabs 封面观察出发,拆开色团、灰度颗粒和白色细线;做六张不用图片的交互封面,再回答评论里最具体的问题:缩成手机小图,颗粒还在吗?

把一个渐变铺满方块,中央放上白字,通常不会难看。但它很容易显得过分干净:颜色均匀得像塑料,几个径向色团又像刚从演示文稿里拖出来的光斑。

Ian(伊恩)在 2026 年 10 月 22 日的帖子里,把 ElevenLabs 的封面质感拆成了三件事:大色团、灰度颗粒、克制的文字。他形容它像失焦的胶片照片。这个比喻很准确——画面没有一个需要辨认的实体,但有光线、深浅和材料。

本文依据用户提供的原帖与评论文字,以及 ElevenLabs Blog 和其官方品牌指南整理。下方六张封面是本文独立写的 HTML、CSS 与 Canvas 实验,不是原站源码,不是 Ian 的原始文件,也没有使用他们的封面图片。

先把“磨砂”从玻璃里拿出来

这里没有隔着一层玻璃去模糊后方网页。色团本身就是背景,不需要 backdrop-filter。我们想要的是一种磨砂般的表面,不是玻璃面板的折射效果。

ElevenLabs 的官方指南明确把渐变与模拟触觉的纹理放在一起:Agents 偏蓝,Creative 偏橙,API 则使用单色体系;图形还包括圆与 Chladni 纹样。它不是“把所有东西都加噪点”,而是颜色、图形与文字各自有角色。博客的分类、标题、日期仍然清晰,封面的情绪不会替代编辑层级。

在这个实验里,最重要的取舍是:背景可以没有明确形状,文字必须有明确位置。 中心只放两行英文,不再加角标、按钮、第三行说明。纹样在边缘出现,中间通过径向遮罩退开。你先读到标题,再注意到背景里有细节。

六张不用图片的颗粒渐变封面
查看实际运行的源码

实际执行:颗粒、弹簧与材质选择

javascript
/* Procedural surface only. Never writes host card geometry, scale, order or colour allocation. */
(()=>{'use strict';
 const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
 const palettes=[
  {name:'Twilight',top:'#b7b6e4',bottom:'#f0a24a',blobs:['#8ea2e4','#e7a4cc','#f6c15c','#ee6d42'],duration:24},
  {name:'Earth',top:'#9eb8b4',bottom:'#4a261c',blobs:['#6f9e98','#e0a86a','#e07a40','#6a3024'],duration:26},
  {name:'Tidal',top:'#d4e2e8',bottom:'#0e3c4c',blobs:['#c5d8e4','#6aacc0','#1d7084','#0c4556'],duration:22},
  {name:'Moss',top:'#d0d8d6',bottom:'#1c4c34',blobs:['#c2d4cc','#7aaa78','#3d7a52','#184830'],duration:28},
  {name:'Violet',top:'#d4c0ea',bottom:'#2c184c',blobs:['#e0c4f0','#eea4d0','#8a62b8','#3c2268'],duration:25},
  {name:'Oxide',top:'#d4dadd',bottom:'#8e3c2c',blobs:['#dce2e4','#f2c4a8','#e08058','#a44636'],duration:20}
 ];
 function hash(s){let h=2166136261;for(const c of String(s)){h^=c.charCodeAt(0);h=Math.imul(h,16777619);}return h>>>0;}
 function noise(size=128){let seed=97131;const bytes=new Uint8ClampedArray(size*size*4);for(let i=0;i<bytes.length;i+=4){seed=(Math.imul(seed,1664525)+1013904223)>>>0;const a=seed>>>24;seed=(Math.imul(seed,1664525)+1013904223)>>>0;const b=seed>>>24;const v=88+((a+b)>>2);bytes[i]=bytes[i+1]=bytes[i+2]=v;bytes[i+3]=255;}return bytes;}
 // Mass=1, k=70, c=2*zeta*sqrt(k). Analytic under-damped step is frame-rate independent.
 function spring(s,target,dt){const w=Math.sqrt(70),a=.86*w,wd=w*Math.sqrt(1-.86**2),d=s.p-target,b=(s.v+a*d)/wd,e=Math.exp(-a*dt),c=Math.cos(wd*dt),n=Math.sin(wd*dt);return{p:target+e*(d*c+b*n),v:e*((-a*d+wd*b)*c+(-a*b-wd*d)*n)};}
 const near=(a,b)=>Math.min(a.x+a.w,b.x+b.w)-Math.max(a.x,b.x)>3&&Math.min(Math.abs(a.y+a.h-b.y),Math.abs(b.y+b.h-a.y))<18||Math.min(a.y+a.h,b.y+b.h)-Math.max(a.y,b.y)>3&&Math.min(Math.abs(a.x+a.w-b.x),Math.abs(b.x+b.w-a.x))<18;
 function select(nodes,box){
  const eligible=nodes.filter(n=>n.eligible&&n.w>=100&&n.h>=76),limit=Math.min(3,Math.max(1,Math.ceil(nodes.length*.22))),budget=box.w*box.h*.18,picked=[];let area=0;
  // Keep prior admissions first; deterministic hash breaks ties. Zoom/hover cannot reshuffle skins.
  eligible.sort((a,b)=>Number(b.existing)-Number(a.existing)||hash(a.id)-hash(b.id));
  for(const n of eligible){if(picked.length>=limit)break;if(area+n.w*n.h>budget||picked.some(p=>near(p,n)))continue;picked.push(n);area+=n.w*n.h;}
  return picked.map(n=>n.id);
 }
 globalThis.JournalFrost={palettes,noise,spring,select};
 if(typeof document==='undefined')return;
 const wall=document.querySelector('[data-frost-wall]'),board=document.querySelector('[data-bubble-board]');if(!wall&&!board)return;
 const media=matchMedia('(prefers-reduced-motion: reduce)'),states=new Map(),active=new Set();
 let raf=0,last=0,queueTimer=0,dead=false,tile=null,readerLocked=false;
 const calm=()=>media.matches||document.body.classList.contains('reduced-motion')||wall?.dataset.reduce==='true';
 const paused=()=>document.hidden||readerLocked||!!document.querySelector('#reader[open],#visual-reader[open],.desk-preferences[open],[data-memory-dialog][open]')||board?.dataset.depthMoving==='true'||board?.dataset.arriving==='true';
 function pattern(kind){
  const ns='http://www.w3.org/2000/svg',svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 400 400');svg.setAttribute('class','frost-pattern');svg.setAttribute('aria-hidden','true');
  const add=(tag,attrs)=>{const n=document.createElementNS(ns,tag);for(const[k,v]of Object.entries(attrs))n.setAttribute(k,v);n.setAttribute('vector-effect','non-scaling-stroke');svg.append(n);};
  if(kind==='rays')for(let i=0;i<36;i++){const a=i*Math.PI/18;add('path',{d:`M200 200L${200+310*Math.cos(a)} ${200+310*Math.sin(a)}`});}
  if(kind==='flow')for(let j=0;j<16;j++){let d='';for(let y=-20;y<=420;y+=4){const x=28+j*23+Math.sin(y*.018+j*.55)*16;d+=(y===-20?'M':'L')+x+' '+y;}add('path',{d});}
  if(kind==='contours')for(let i=0;i<13;i++){const r=30+i*17;add('ellipse',{cx:168,cy:236,rx:r,ry:r*.82,transform:'rotate(-24 168 236)'});}
  if(kind==='waves')for(let j=0;j<15;j++){let d='';for(let x=-20;x<=420;x+=4){const y=44+j*23+Math.sin(x*.021+j*.19)*22;d+=(x===-20?'M':'L')+x+' '+y;}add('path',{d});}
  if(kind==='dots')for(let y=20;y<400;y+=19)for(let x=20;x<400;x+=19)add('circle',{cx:x,cy:y,r:.65,fill:'white',stroke:'none'});
  if(kind==='lens')for(let i=0;i<17;i++){const d=12+i*9;add('path',{d:`M200 12Q${200-d*1.65} 200 200 388Q${200+d*1.65} 200 200 12Z`});}
  return svg;
 }
 function makeTile(){if(tile)return tile;tile=document.createElement('canvas');tile.width=tile.height=128;const ctx=tile.getContext('2d');const image=ctx.createImageData(128,128);image.data.set(noise());ctx.putImageData(image,0,0);return tile;}
 // Large projected cards stay at the original 0.4 overlay. Smaller ones rise
 // smoothly and stop at 0.64 — a bound, not an ever-increasing opacity.
 function grainOpacity(px){const t=clamp((160-px)/96,0,1),s=t*t*(3-2*t);return .4+s*.24;}
 function drawGrain(s,r){
  // Bitmap is the projected device pixels, and the CSS box matches that bitmap.
  // scale(layout/projected) cancels the camera so the tile is not enlarged and then filtered down.
  const dpr=devicePixelRatio||1,lw=s.card.offsetWidth,lh=s.card.offsetHeight,sx=lw?r.width/lw:1,sy=lh?r.height/lh:1,c=s.canvas;
  const w=Math.max(1,Math.ceil(r.width*dpr)),h=Math.max(1,Math.ceil(r.height*dpr));
  c.style.setProperty('--frost-grain',grainOpacity(Math.min(r.width,r.height)).toFixed(3));
  if(Math.abs(sx-1)<.01&&Math.abs(sy-1)<.01)c.style.width=c.style.height=c.style.transform='';
  else if(lw&&lh){const cw=w/dpr,ch=h/dpr;c.style.width=cw+'px';c.style.height=ch+'px';c.style.transform=`scale(${(lw/cw).toFixed(5)},${(lh/ch).toFixed(5)})`;}
  if(c.dataset.noisePixels===w+'×'+h&&c.width===w&&c.height===h)return;c.width=w;c.height=h;const ctx=c.getContext('2d');if(!ctx)return;ctx.imageSmoothingEnabled=false;ctx.fillStyle=ctx.createPattern(makeTile(),'repeat');ctx.fillRect(0,0,w,h);c.dataset.noiseDpr=String(dpr);c.dataset.noisePixels=w+'×'+h;
 }
 function parkGrain(){for(const s of states.values())s.canvas.style.width=s.canvas.style.height=s.canvas.style.transform='';}
 function paletteIndex(card){const t=card.dataset.fieldTone||card.dataset.boardTone||'';return t==='lilac'||t==='violet'?4:t==='sage'||t==='moss'?3:t==='coral'||t==='red'?0:t==='sand'||t==='amber'?1:t==='blue'?2:t==='mist'?2:hash(card.dataset.contentId)%6;}
 function mount(card,index,kind){
  if(states.has(card))return states.get(card);
  const layer=document.createElement('span');layer.className='frost-surface';layer.setAttribute('aria-hidden','true');
  layer.innerHTML='<span class="frost-parallax"><span class="frost-clouds"><i></i><i></i><i></i><i></i></span></span><span class="frost-shade"></span><span class="frost-glow"></span><canvas class="frost-grain"></canvas>';
  if(kind)layer.append(pattern(kind));card.prepend(layer);card.dataset.frosted='true';
  const p=palettes[index];for(const[k,v]of Object.entries({top:p.top,bottom:p.bottom,...Object.fromEntries(p.blobs.map((v,i)=>['blob-'+i,v])),duration:p.duration+'s'}))layer.style.setProperty('--frost-'+k,v);
  const s={card,layer,canvas:layer.querySelector('canvas'),index,x:{p:0,v:0},y:{p:0,v:0},glow:{p:0,v:0},tx:0,ty:0,tg:0,visible:false,rect:null};states.set(card,s);intersection.observe(card);sizeObserver.observe(card);return s;
 }
 function reset(s){s.tx=s.ty=s.tg=0;active.add(s);wake();}
 function remove(card){const s=states.get(card);if(!s)return;intersection.unobserve(card);sizeObserver.unobserve(card);s.layer.remove();delete card.dataset.frosted;states.delete(card);active.delete(s);}
 function write(s){s.layer.style.setProperty('--frost-x',s.x.p+'px');s.layer.style.setProperty('--frost-y',s.y.p+'px');s.layer.style.setProperty('--frost-light',s.glow.p);s.layer.style.setProperty('--frost-glow-x',(50+s.x.p*(100/24))+'%');s.layer.style.setProperty('--frost-glow-y',(50+s.y.p*(100/24))+'%');}
 function frame(now){
  raf=0;if(dead||calm()||paused()){last=0;return;}const dt=last?Math.min(.05,(now-last)/1000):1/60;last=now;
  for(const s of active){if(!s.visible){active.delete(s);continue;}s.x=spring(s.x,s.tx,dt);s.y=spring(s.y,s.ty,dt);s.glow=spring(s.glow,s.tg,dt);write(s);
   if(Math.abs(s.x.p-s.tx)+Math.abs(s.y.p-s.ty)+Math.abs(s.glow.p-s.tg)<.015&&Math.abs(s.x.v)+Math.abs(s.y.v)+Math.abs(s.glow.v)<.04){s.x={p:s.tx,v:0};s.y={p:s.ty,v:0};s.glow={p:s.tg,v:0};write(s);active.delete(s);}
  }if(active.size)raf=requestAnimationFrame(frame);else last=0;
 }
 function wake(){if(!raf&&!calm()&&!paused()&&active.size)raf=requestAnimationFrame(frame);}
 function sync(){
  const stop=calm()||paused();for(const s of states.values()){s.card.dataset.frostRunning=String(!stop&&s.visible);if(stop){s.tx=s.ty=s.tg=0;s.x={p:0,v:0};s.y={p:0,v:0};s.glow={p:0,v:0};write(s);}}
  if(stop){if(raf)cancelAnimationFrame(raf);raf=0;last=0;active.clear();}else wake();
 }
 const intersection=new IntersectionObserver(entries=>{for(const e of entries){const s=states.get(e.target);if(!s)continue;s.visible=e.isIntersecting&&!s.card.hidden;queue();}sync();});
 const sizeObserver=new ResizeObserver(()=>queue());
 function reconcile(){
  queueTimer=0;if(dead||document.hidden)return;
  if(board&&(paused()||board.dataset.bubbleState==='changing'||board.dataset.hoverState==='expanded'||board.dataset.depthHover==='true')){sync();return;}
  // Batch geometry reads, then selection and writes. Never a per-pointer DOM measurement.
  const cards=board?[...board.querySelectorAll('.discovery-row>.desk-card,[data-depth-card]')]:[...wall.querySelectorAll('[data-frost-card]')],rects=new Map(cards.map(c=>[c,c.getBoundingClientRect()]));
  if(board){
   const area=board.getBoundingClientRect(),visible=cards.filter(c=>{const r=rects.get(c),cs=getComputedStyle(c);return !c.hidden&&!c.inert&&cs.visibility!=='hidden'&&+cs.opacity>.9&&r.width>0&&r.height>0;});
   const nodes=visible.map(c=>{const r=rects.get(c),article=c.dataset.contentKind==='article'&&!c.classList.contains('has-cover'),product=c.dataset.cardPresentation==='product-entry'&&c.dataset.surfaceMode==='solid';return{id:c.dataset.contentId,x:r.x,y:r.y,w:r.width,h:r.height,eligible:!c.hasAttribute('data-brand-surface')&&!c.dataset.previewKind&&(article||product),existing:states.has(c)};});
   const selected=new Set(select(nodes,{w:area.width,h:area.height}));
   for(const card of [...states.keys()])if(!selected.has(card.dataset.contentId)&&(!visible.includes(card)||!card.matches(':hover,:focus-within')))remove(card);
   let twilight=[...states.values()].some(s=>s.index===0);
   for(const card of visible)if(selected.has(card.dataset.contentId)&&!states.has(card)){let index=paletteIndex(card);if(!twilight){index=0;twilight=true;}mount(card,index,null);}
   board.dataset.frostedCount=String(states.size);
  }
  for(const s of states.values()){const r=rects.get(s.card);if(r&&r.width&&r.height&&s.visible)drawGrain(s,r);}sync();
 }
 function queue(){clearTimeout(queueTimer);if(!dead&&!document.hidden)queueTimer=setTimeout(reconcile,160);}
 const target=wall||board;
 target.addEventListener('pointermove',e=>{if(e.pointerType==='touch'||calm()||paused())return;const card=e.target.closest('[data-frosted]'),s=states.get(card);if(!s||!s.visible)return;
  if(!s.rect)s.rect=card.getBoundingClientRect();const r=s.rect;s.tx=clamp((e.clientX-r.x)/r.width-.5,-.5,.5)*24;s.ty=clamp((e.clientY-r.y)/r.height-.5,-.5,.5)*24;s.tg=1;active.add(s);wake();
 },{passive:true});
 target.addEventListener('pointerover',e=>{const s=states.get(e.target.closest('[data-frosted]'));if(s&&!s.card.contains(e.relatedTarget))s.rect=null;});
 target.addEventListener('pointerout',e=>{const s=states.get(e.target.closest('[data-frosted]'));if(s&&!s.card.contains(e.relatedTarget)){s.rect=null;reset(s);}});
 target.addEventListener('focusin',e=>{const s=states.get(e.target.closest('[data-frosted]'));if(s&&!calm()){s.tg=.45;active.add(s);wake();}});
 target.addEventListener('focusout',e=>{const s=states.get(e.target.closest('[data-frosted]'));if(s&&!s.card.contains(e.relatedTarget))reset(s);});
 if(wall)for(const card of wall.querySelectorAll('[data-frost-card]'))mount(card,+card.dataset.frostCard,card.dataset.frostPattern);
 // An added article/project is discovered through DOM membership, not a hard-coded list.
 const mutations=new MutationObserver(changes=>{if(board?.dataset.depthMoving==='true')parkGrain();if(changes.some(m=>m.type==='childList'&&([...m.addedNodes,...m.removedNodes].some(n=>n.nodeType===1&&!n.matches?.('.frost-surface')))||m.type==='attributes'))queue();});
 if(board){mutations.observe(board,{childList:true,subtree:true,attributes:true,attributeFilter:['data-field-tone','data-surface-mode','data-depth-moving','data-bubble-state','data-hover-state','data-depth-hover','data-arriving']});}
 const pauseObserver=new MutationObserver(()=>{sync();queue();});pauseObserver.observe(document.body,{attributes:true,attributeFilter:['class']});
 for(const d of document.querySelectorAll('dialog'))pauseObserver.observe(d,{attributes:true,attributeFilter:['open']});
 media.addEventListener('change',()=>{sync();queue();});document.addEventListener('visibilitychange',()=>{sync();queue();});
 window.addEventListener('resize',()=>{for(const s of states.values())s.rect=null;queue();});
 window.addEventListener('journal:reader-lock',()=>{readerLocked=true;sync();});window.addEventListener('journal:reader-release',()=>{readerLocked=false;sync();queue();});
 for(const name of ['journal:layout-ready','journal:depth-change','journal:board-turn','journal:theme-change','journal:frost-controls'])window.addEventListener(name,()=>{for(const s of states.values())s.rect=null;sync();queue();});
 window.addEventListener('pagehide',e=>{if(raf)cancelAnimationFrame(raf);raf=0;last=0;clearTimeout(queueTimer);if(e.persisted)return;dead=true;intersection.disconnect();sizeObserver.disconnect();mutations.disconnect();pauseObserver.disconnect();active.clear();});
 window.addEventListener('pageshow',()=>{sync();queue();});queue();
})();

实际执行:渐变、漂移与纹样遮罩

css
/* Shared procedural material. Host layout remains owned by the existing geometry engine. */
.frost-surface{position:absolute;inset:0;display:block;overflow:hidden;border-radius:inherit;pointer-events:none;isolation:isolate;z-index:0;background:linear-gradient(180deg,var(--frost-top),var(--frost-bottom));--frost-x:0px;--frost-y:0px;--frost-light:0}
.frost-surface>span,.frost-surface>canvas,.frost-surface>svg{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
.frost-parallax{translate:var(--frost-x) var(--frost-y)}
.frost-clouds{position:absolute;inset:-36%;display:block}
.frost-clouds>i{position:absolute;inset:0;display:block;animation:frost-drift var(--frost-duration,24s) ease-in-out infinite alternate;animation-play-state:paused;transform-origin:38% 42%;background:radial-gradient(ellipse at 14% 8%,var(--frost-blob-0),transparent 74%)}
.frost-clouds>i:nth-child(2){background:radial-gradient(ellipse at 92% 24%,var(--frost-blob-1),transparent 70%);animation-delay:-7s;animation-direction:alternate-reverse;transform-origin:75% 25%}
.frost-clouds>i:nth-child(3){background:radial-gradient(ellipse at 10% 94%,var(--frost-blob-2),transparent 72%);animation-delay:-13s;transform-origin:20% 78%}
.frost-clouds>i:nth-child(4){background:radial-gradient(ellipse at 90% 96%,var(--frost-blob-3),transparent 68%);animation-delay:-19s;animation-direction:alternate-reverse;transform-origin:85% 85%}
[data-frost-running=true] .frost-clouds>i{animation-play-state:running}
.frost-shade{background:radial-gradient(ellipse at 50% 42%,transparent 46%,#140e1818 100%)}
.frost-surface>.frost-grain{mix-blend-mode:overlay;opacity:var(--frost-grain,.4);image-rendering:pixelated;transform-origin:0 0}
.frost-glow{background:radial-gradient(ellipse at var(--frost-glow-x,50%) var(--frost-glow-y,50%),#fff9e94d,transparent 48%);opacity:var(--frost-light)}
.frost-surface>.frost-pattern{stroke:#fff;fill:none;stroke-width:.8px;opacity:.3;mask-image:radial-gradient(ellipse at center,transparent 21%,#0006 43%,#000 70%);translate:calc(var(--frost-x)*-.4) calc(var(--frost-y)*-.4)}
@keyframes frost-drift{from{transform:translate(-1.5%,-1%) scale(1.02)}to{transform:translate(2.5%,2%) scale(1.08)}}
/* Selected owned shells only. Brand surfaces, original media, controls and prompt artwork never match. */
.desk-card[data-frosted=true]{isolation:isolate;--card-ink:#fff}
.desk-card[data-frosted=true]>.frost-surface{z-index:0}
.desk-card[data-frosted=true]>:not(.frost-surface){z-index:1}
.desk-card[data-frosted=true] :is(.card-title,.card-summary,.card-topic,.card-meta,.tile-arrow,.card-word){color:#fff}
.desk-card[data-frosted=true] .card-copy{text-shadow:0 1px 10px #0002}
.desk-card[data-frosted=true] .project-compact-mark{color:#fff}
.desk-card[data-frosted=true][data-content-kind=article]>.card-media{background:transparent}
body.reduced-motion .frost-clouds>i,[data-frost-wall][data-reduce=true] .frost-clouds>i{animation:none}
body.reduced-motion .frost-parallax,body.reduced-motion .frost-pattern,[data-frost-wall][data-reduce=true] .frost-parallax,[data-frost-wall][data-reduce=true] .frost-pattern{translate:none}
body.reduced-motion .frost-glow,[data-frost-wall][data-reduce=true] .frost-glow{display:none}
@media(prefers-reduced-motion:reduce){.frost-clouds>i{animation:none}.frost-parallax,.frost-pattern{translate:none}.frost-glow{display:none}}

实际执行:六张封面与对照控件

html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Soft fields — 颗粒渐变封面实验</title>
<link rel="stylesheet" href="surface.css">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="../journal-kit/component.css">
<script src="surface.js" defer></script>
<script src="demo.js" defer></script>
</head>
<body class="journal-component" data-component-contract="journal-controls/v1">
<main class="frost-lab">
<header><h1>柔和的颜色,清晰的颗粒。</h1><p>本文独立实验 · 将指针移到封面上,观察色团与细线的反向移动。</p></header>
<div class="jc-controls">
<button type="button" data-jc-reset>回到初始状态</button>
<label><input type="checkbox" data-jc-reduce>减少动态</label>
<label><input type="checkbox" data-grain-enabled checked>颗粒</label>
<label><input type="checkbox" data-pattern-enabled checked>细线</label>
</div>
<section class="jc-stage" aria-label="六张程序化封面"><div class="frost-wall" data-frost-wall>
<article class="frost-cover" data-frost-card="0" tabindex="0" aria-label="Slow Light,紫粉到橙金,无纹样"><h2>Slow<br>Light</h2></article>
<article class="frost-cover" data-frost-card="1" data-frost-pattern="flow" tabindex="0" aria-label="Quiet Ground,灰蓝到深棕,流线"><h2>Quiet<br>Ground</h2></article>
<article class="frost-cover" data-frost-card="2" data-frost-pattern="contours" tabindex="0" aria-label="Deep Listening,雾蓝到深海蓝,等高线"><h2>Deep<br>Listening</h2></article>
<article class="frost-cover" data-frost-card="3" data-frost-pattern="waves" tabindex="0" aria-label="Soft Signals,雾蓝到墨绿,声波"><h2>Soft<br>Signals</h2></article>
<article class="frost-cover" data-frost-card="4" data-frost-pattern="dots" tabindex="0" aria-label="After Hours,薰衣草到深夜紫,点阵"><h2>After<br>Hours</h2></article>
<article class="frost-cover" data-frost-card="5" data-frost-pattern="lens" tabindex="0" aria-label="Warm Silence,天灰到锈红,透镜"><h2>Warm<br>Silence</h2></article>
</div></section>
<p class="jc-status" role="status" aria-live="polite">六组相邻色系 · 128×128 物理像素噪点平铺 · k=70 / ζ=0.86</p>
<footer class="frost-credit">灵感:Ian (伊恩) 的公开演示说明与 ElevenLabs Blog;此处是独立代码实验,不是原站源码或原作者作品。<a href="standalone.html" download="soft-fields.html">下载单文件 HTML</a></footer>
</main>
</body>
</html>

实际执行:3×2几何与Inter排版

css
@font-face{font-family:Inter;src:url('InterVariable.woff2') format('woff2');font-weight:100 900;font-display:swap;font-style:normal}
.frost-lab{max-width:1080px;margin:0 auto}
.frost-wall{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}
.frost-cover{position:relative;aspect-ratio:1;overflow:hidden;border-radius:20px;isolation:isolate;container-type:inline-size;background:#6b596c}
.frost-cover::after{content:"";position:absolute;inset:0;border:.5px solid #0002;border-radius:inherit;pointer-events:none;z-index:4}
.frost-cover>h2{position:absolute;inset:50% 0 auto;margin:0;display:block;transform:translateY(-50%);text-align:center;color:#fff;font-family:Inter,sans-serif;font-size:8cqw;font-weight:400;line-height:1.16;letter-spacing:-.035em;z-index:2;pointer-events:none;text-shadow:0 1px 12px #0001}
.frost-wall[data-grain=false] .frost-grain,.frost-wall[data-pattern=false] .frost-pattern{display:none}
.frost-credit{margin-top:24px;max-width:75ch;color:#65736c;font-size:12px;line-height:1.8}
.frost-credit>a{color:inherit;margin-left:1em}
@media(max-width:540px){.frost-wall{gap:10px}}

实际执行:开关与复位

javascript
(()=>{'use strict';
 const wall=document.querySelector('[data-frost-wall]'),reduce=document.querySelector('[data-jc-reduce]'),grain=document.querySelector('[data-grain-enabled]'),pattern=document.querySelector('[data-pattern-enabled]'),status=document.querySelector('.jc-status'),system=matchMedia('(prefers-reduced-motion: reduce)');
 function update(){wall.dataset.reduce=String(reduce.checked||system.matches);wall.dataset.grain=String(grain.checked);wall.dataset.pattern=String(pattern.checked);window.dispatchEvent(new Event('journal:frost-controls'));status.textContent=(system.matches?'系统减少动态已启用;':reduce.checked?'动态已停止;':'k=70 / ζ=0.86;')+(grain.checked?'灰度颗粒:开':'灰度颗粒:关')+','+(pattern.checked?'五种细线:开':'五种细线:关')+'。噪点保持一个物理像素,不随卡片尺寸缩小。';}
 for(const input of [reduce,grain,pattern])input.addEventListener('change',update);
 system.addEventListener('change',update);
 document.querySelector('[data-jc-reset]').addEventListener('click',()=>{reduce.checked=false;grain.checked=pattern.checked=true;wall.dataset.reduce='true';window.dispatchEvent(new Event('journal:frost-controls'));wall.getAnimations({subtree:true}).forEach(a=>{a.currentTime=0;});update();});update();
})();

本文独立实验:3×2 方形封面;可关闭颗粒与细线做对照、开启减少动态和复位。色团、SVG纹样与Canvas噪点均由代码生成,不是ElevenLabs或Ian的原始实现。

显示

外观
字体