关于图片上使用渐变模糊的探索

·
次浏览
AI 摘要生成中

在苹果的 App Store 中,许多应用程序截图上采用了渐变模糊效果,营造出一种柔和舒适的视觉效果。这种效果类似于在图片上叠加一层逐渐模糊的面纱,提供了视觉清晰度和深度感。

最简单的实现方式

从图片底部开始,逐渐增加模糊效果,直到顶部。没有叠加任何的颜色,只是在图片上叠加了不同程度的模糊图层。

Page.tsx
const getGradientStyle = (blur: number, opacity: number) => { return { backdropFilter: `blur(${blur}px)`, WebkitBackdropFilter: `blur(${blur}px)`, background: `linear-gradient(to top, rgba(0,0,0,${opacity}) 20%, rgba(0,0,0,0) 100%)`, }; }; <div className="will-change-filter absolute inset-x-0 bottom-0 isolate z-10 h-1/3 max-h-[100px] min-h-[64px]"> <div style={getGradientStyle(12, 0.8)} className="gradient-mask absolute inset-0 blur-md" ></div> <div style={getGradientStyle(6, 0.6)} className="gradient-mask absolute inset-0 blur-[6px]" ></div> <div style={getGradientStyle(3, 0.4)} className="gradient-mask absolute inset-0 blur-[3px]" ></div> <div style={getGradientStyle(2, 0.2)} className="gradient-mask absolute inset-0 blur-[2px]" ></div> <div style={getGradientStyle(1, 0.1)} className="gradient-mask absolute inset-0 blur-[1px]" ></div> </div>;
Page.tsx
const getGradientStyle = (blur: number, opacity: number) => { return { backdropFilter: `blur(${blur}px)`, WebkitBackdropFilter: `blur(${blur}px)`, background: `linear-gradient(to top, rgba(0,0,0,${opacity}) 20%, rgba(0,0,0,0) 100%)`, }; }; <div className="will-change-filter absolute inset-x-0 bottom-0 isolate z-10 h-1/3 max-h-[100px] min-h-[64px]"> <div style={getGradientStyle(12, 0.8)} className="gradient-mask absolute inset-0 blur-md" ></div> <div style={getGradientStyle(6, 0.6)} className="gradient-mask absolute inset-0 blur-[6px]" ></div> <div style={getGradientStyle(3, 0.4)} className="gradient-mask absolute inset-0 blur-[3px]" ></div> <div style={getGradientStyle(2, 0.2)} className="gradient-mask absolute inset-0 blur-[2px]" ></div> <div style={getGradientStyle(1, 0.1)} className="gradient-mask absolute inset-0 blur-[1px]" ></div> </div>;

相关的的CSS代码:

global.css
.gradient-mask { -webkit-mask-image: linear-gradient(0deg, #000 0, transparent); mask-image: linear-gradient(0deg, #000 0, transparent); }
global.css
.gradient-mask { -webkit-mask-image: linear-gradient(0deg, #000 0, transparent); mask-image: linear-gradient(0deg, #000 0, transparent); }



另外,还可以像下面图片中这样,从图片底部吸取颜色,作为渐变模糊的颜色,更接近苹果商店中的效果。这种方法,比较适合图片的下半部分区域有大面积的主色,看起来才会自然、融合。否则,还不如第一种效果。

两种效果对比:

first image
second image

结合吸取颜色的实现方式

Page.tsx
function Card({ title, media, description, year, type, location, camera, poster, }: GalleryCardType) { const [isPlaying, setIsPlaying] = useState(false); const [isHovered, setIsHovered] = useState(false); const [extractedColor, setExtractedColor] = useState<string | null>(null); const videoRef = useRef<HTMLVideoElement>(null); const cardRef = useRef<HTMLDivElement>(null); { /* 从图片底部提取颜色 */ } const extractColor = (imgSrc: string): Promise<string> => { return new Promise((resolve) => { const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = () => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("无法创建 canvas 上下文"); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0, img.width, img.height); const imageData = ctx.getImageData( 0, img.height - 1, img.width, 1, ).data; const r = imageData[0]; const g = imageData[1]; const b = imageData[2]; const color = `rgb(${r},${g},${b})`; resolve(color); }; img.src = imgSrc; }); }; useEffect(() => { if (type === "image") { extractColor(media).then(setExtractedColor); } }, [media, type]); const getGradientStyle = (blur: number, opacity: number) => { if (!extractedColor) { return { backdropFilter: `blur(${blur}px)`, WebkitBackdropFilter: `blur(${blur}px)`, background: `linear-gradient(to top, rgba(0,0,0,${opacity}) 20%, rgba(0,0,0,0) 100%)`, }; } const rgbMatch = extractedColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (!rgbMatch) { return { backdropFilter: `blur(${blur}px)`, WebkitBackdropFilter: `blur(${blur}px)`, background: `linear-gradient(to top, rgba(0,0,0,${opacity}) 20%, rgba(0,0,0,0) 100%)`, }; } const [, r, g, b] = rgbMatch.map(Number); return { backdropFilter: `blur(${blur}px)`, WebkitBackdropFilter: `blur(${blur}px)`, background: `linear-gradient(to top, rgba(${r},${g},${b},${opacity}) 20%, rgba(${r},${g},${b},0) 100%)`, }; }; return ( <div ref={cardRef} className="group overflow-hidden rounded-xl p-px" onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > <div className="group relative overflow-hidden"> <svg width="0" height="0"> <filter id="round-corners"> <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur" /> <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9" result="goo" /> <feComposite in="SourceGraphic" in2="goo" operator="atop" /> </filter> </svg> <div className="relative" style={{ filter: "url(#round-corners)", }} > {renderMedia()} {/* 渐变模糊效果 */} {(!isPlaying || type === "image") && ( <div className="will-change-filter absolute inset-x-0 bottom-0 isolate z-10 h-1/3 min-h-[64px] max-h-[100px]"> <div style={getGradientStyle(12, 0.8)} className="gradient-mask absolute inset-0 blur-md" ></div> <div style={getGradientStyle(6, 0.6)} className="gradient-mask absolute inset-0 blur-[6px]" ></div> <div style={getGradientStyle(3, 0.4)} className="gradient-mask absolute inset-0 blur-[3px]" ></div> <div style={getGradientStyle(2, 0.2)} className="gradient-mask absolute inset-0 blur-[2px]" ></div> <div style={getGradientStyle(1, 0.1)} className="gradient-mask absolute inset-0 blur-[1px]" ></div> </div> )} </div> </div> </div> ); }
Page.tsx
function Card({ title, media, description, year, type, location, camera, poster, }: GalleryCardType) { const [isPlaying, setIsPlaying] = useState(false); const [isHovered, setIsHovered] = useState(false); const [extractedColor, setExtractedColor] = useState<string | null>(null); const videoRef = useRef<HTMLVideoElement>(null); const cardRef = useRef<HTMLDivElement>(null); { /* 从图片底部提取颜色 */ } const extractColor = (imgSrc: string): Promise<string> => { return new Promise((resolve) => { const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = () => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("无法创建 canvas 上下文"); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0, img.width, img.height); const imageData = ctx.getImageData( 0, img.height - 1, img.width, 1, ).data; const r = imageData[0]; const g = imageData[1]; const b = imageData[2]; const color = `rgb(${r},${g},${b})`; resolve(color); }; img.src = imgSrc; }); }; useEffect(() => { if (type === "image") { extractColor(media).then(setExtractedColor); } }, [media, type]); const getGradientStyle = (blur: number, opacity: number) => { if (!extractedColor) { return { backdropFilter: `blur(${blur}px)`, WebkitBackdropFilter: `blur(${blur}px)`, background: `linear-gradient(to top, rgba(0,0,0,${opacity}) 20%, rgba(0,0,0,0) 100%)`, }; } const rgbMatch = extractedColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (!rgbMatch) { return { backdropFilter: `blur(${blur}px)`, WebkitBackdropFilter: `blur(${blur}px)`, background: `linear-gradient(to top, rgba(0,0,0,${opacity}) 20%, rgba(0,0,0,0) 100%)`, }; } const [, r, g, b] = rgbMatch.map(Number); return { backdropFilter: `blur(${blur}px)`, WebkitBackdropFilter: `blur(${blur}px)`, background: `linear-gradient(to top, rgba(${r},${g},${b},${opacity}) 20%, rgba(${r},${g},${b},0) 100%)`, }; }; return ( <div ref={cardRef} className="group overflow-hidden rounded-xl p-px" onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > <div className="group relative overflow-hidden"> <svg width="0" height="0"> <filter id="round-corners"> <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur" /> <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9" result="goo" /> <feComposite in="SourceGraphic" in2="goo" operator="atop" /> </filter> </svg> <div className="relative" style={{ filter: "url(#round-corners)", }} > {renderMedia()} {/* 渐变模糊效果 */} {(!isPlaying || type === "image") && ( <div className="will-change-filter absolute inset-x-0 bottom-0 isolate z-10 h-1/3 min-h-[64px] max-h-[100px]"> <div style={getGradientStyle(12, 0.8)} className="gradient-mask absolute inset-0 blur-md" ></div> <div style={getGradientStyle(6, 0.6)} className="gradient-mask absolute inset-0 blur-[6px]" ></div> <div style={getGradientStyle(3, 0.4)} className="gradient-mask absolute inset-0 blur-[3px]" ></div> <div style={getGradientStyle(2, 0.2)} className="gradient-mask absolute inset-0 blur-[2px]" ></div> <div style={getGradientStyle(1, 0.1)} className="gradient-mask absolute inset-0 blur-[1px]" ></div> </div> )} </div> </div> </div> ); }

从底部向上20%的位置开始渐变。

解决图像角落泛白问题

如果图像的底部2个角落出现泛白,可以尝试下面的方法。

  {/* 这段代码的作用是创建一个SVG滤镜,用于在图像上创建圆角。 */}
  <svg width="0" height="0">
    <filter id="round-corners">
      <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur" />
      <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 19 -9" result="goo" />
      <feComposite in="SourceGraphic" in2="goo" operator="atop" />
    </filter>
  </svg>
  {/* 下面这段代码的作用是将创建的SVG滤镜应用到一个相对定位的div上。 */}
  <div
    className="relative"
    style={{
      filter: 'url(#round-corners)',
    }}
  >
  {/* 这段代码的作用是创建一个SVG滤镜,用于在图像上创建圆角。 */}
  <svg width="0" height="0">
    <filter id="round-corners">
      <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur" />
      <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 19 -9" result="goo" />
      <feComposite in="SourceGraphic" in2="goo" operator="atop" />
    </filter>
  </svg>
  {/* 下面这段代码的作用是将创建的SVG滤镜应用到一个相对定位的div上。 */}
  <div
    className="relative"
    style={{
      filter: 'url(#round-corners)',
    }}
  >

Figma 中实现

借助一个插件,就可以轻松做到了:Progressive Blur