现代 CSS 高阶技巧:实现平滑内凹圆角的工程化实践

通过 数学计算 + CSS mask 复合遮罩 实现的真正几何内凹效果:

在这里插入图片描述

背景是一张图片,用来证明中间的凹陷是透明的。


完整代码

app.js

import FormPage from "./pages/formPage";
import "./App.css";
const App = () => {
  return (
    <div className="box">
      <div className="block"></div>
    </div>
  );
};

export default App;

app.css


.box {
  background: url(../src/pages/img/10.jpg) 100%;
  height: 500px;
}

.block {
  /* 增大圆角半径 */
  --r: 6px;
  /* 适当增加斜切长度 */
  --s: 69px;
  /* 微调角度使过渡更自然 */
  --a: 44deg;
  
  /* 更柔和的边缘过渡 */
  --_m: 0/calc(2*var(--r)) var(--r) no-repeat radial-gradient(50% 100% at bottom, #000 calc(100% - 0.8px), transparent);

  /* 自动计算的水平偏移 */
  --_d: (var(--s) + var(--r)) * cos(var(--a));


  width: 300px;
  height: 100px;
  background-color: antiquewhite;
  border-radius: 150px;
  mask:
    calc(50% + var(--_d)) var(--_m), calc(50% - var(--_d)) var(--_m),
    radial-gradient(var(--s) at 50% calc(-1*sin(var(--a))*var(--s)),
      transparent 100%, #000 calc(100% + 0.8px)) 0 calc(var(--r)*(1 - sin(var(--a)))),
    linear-gradient(90deg, #000 calc(50% - var(--_d)), transparent 0 calc(50% + var(--_d)), #000 0);
  mask-repeat: no-repeat;
}

整体思路

这段代码的核心思想是:

使用多个 mask 图层组合,通过 radial-gradient 和 linear-gradient 的叠加,形成一个“中间凹进去、边缘过渡柔和”的视觉效果。

它利用了 CSS 中的变量(--r, --s, --a)、三角函数和 mask 属性,实现了动态可配置的内凹圆角效果。


变量解析(CSS Custom Properties)

--r: 6px;        /* 凹陷区域的半径(控制凹陷大小) */
--s: 69px;       /* 圆弧的半径(控制凹陷的位置) */
--a: 44deg;      /* 角度(用于三角函数计算) */

这些变量可以方便地调整最终的视觉效果。


计算变量(关键逻辑)

--_m:定义一个遮罩图层(更柔和的边缘过渡)

--_m: 0 / calc(2*var(--r)) var(--r) no-repeat
    radial-gradient(50% 100% at bottom, #000 calc(100% - 0.8px), transparent);
  • 创建一个宽度为 2 * --r,高度为 --r 的径向渐变。
  • 渐变从底部开始,颜色从不透明到透明,形成一个边缘模糊的遮罩条带
  • 这个条带会在最终 mask 中作为“边缘柔化”层使用。

--_d:水平偏移距离(基于三角函数自动计算)

--_d: (var(--s) + var(--r)) * cos(var(--a));
  • 使用余弦函数计算出一个水平方向上的偏移值。
  • 这个偏移值决定了凹陷区域在水平方向上的位置。
  • 结合下面的 mask 设置,使凹陷区域对称分布在中心两侧。

Mask 图层详解(这是整个效果的关键)

mask:
  calc(50% + var(--_d)) var(--_m),
  calc(50% - var(--_d)) var(--_m),
  radial-gradient(var(--s) at 50% calc(-1*sin(var(--a))*var(--s)),
    transparent 100%, #000 calc(100% + 0.8px)) 0 calc(var(--r)*(1 - sin(var(--a)))),
  linear-gradient(90deg, #000 calc(50% - var(--_d)), transparent 0 calc(50% + var(--_d)), #000 0);

我们来逐行拆解这四个 mask 图层:


🔹 第一层 & 第二层(边缘柔化层)

calc(50% + var(--_d)) var(--_m),
calc(50% - var(--_d)) var(--_m)
  • 这两个图层使用的是之前定义好的 --_m 遮罩条带。
  • 分别放置在中心左右各偏移 --_d 的位置。
  • 作用是柔和边缘,避免生硬的裁剪边界。

🔸 第三层(核心凹陷层)

radial-gradient(var(--s) at 50% calc(-1*sin(var(--a))*var(--s)),
  transparent 100%, #000 calc(100% + 0.8px))
  0 calc(var(--r)*(1 - sin(var(--a))));
  • 创建一个以中心为圆心、向上偏移一定距离的径向渐变。
  • 半径为 --s,位于垂直方向上偏移 sin(a) * s
  • 渐变从透明到黑色,超出部分变为不透明。
  • 最后定位在 0 calc(...),即垂直方向向下偏移一点,让凹陷区域更贴合整体形状。

✅ 这一层是形成“内凹”视觉的核心图层


🔷 第四层(背景遮罩层)

linear-gradient(90deg, #000 calc(50% - var(--_d)), transparent 0 calc(50% + var(--_d)), #000 0)
  • 水平方向的线性渐变:

    • 左侧和右侧为黑色(显示区域)
    • 中间一段为透明(隐藏区域)
  • 作用是遮住中间的凹陷区域,只保留两边的内容。


最终效果总结

图层作用
第一、二层边缘柔化处理(避免锯齿感)
第三层核心凹陷区域(模拟“内凹”形状)
第四层背景遮罩(只显示两边,中间隐藏)

结合起来就形成了一个中间凹陷、边缘柔和、对称分布的视觉效果,非常适合用在按钮、卡片等需要轻微凹陷质感的 UI 元素中。


示例效果预览(文字描述)

想象一个椭圆形的盒子(border-radius: 150px),原本是完整的圆形。但在它的正中央,有一个向上弯曲的凹陷区域,就像轻轻按下按钮时那种感觉,边缘还有轻微的阴影过渡。

这种效果常见于 macOS 的菜单栏按钮、iOS 控件等现代 UI 设计中。


如何调整?

你可以通过修改以下变量来实时调整视觉效果:

变量默认值调整建议影响
--r6px增大 → 凹陷更大凹陷区域大小
--s69px增大 → 凹陷更深凹陷位置与深度
--a44deg增大 → 凹陷更靠上凹陷角度与位置
background-colorantiquewhite更浅/深色整体对比度和质感

在小程序中实现实现平滑内凹圆角


在支付宝小程序中使用 canvas 实现带内凹圆角矩形

小程序的结构:

<view class="mask-layer">
  <canvas style="width:100%;height:100%" id="canvas" type="2d" onReady="onCanvasReady"></canvas>
</view>

小程序的canvas逻辑:


Page({
  onCanvasReady() {
    const systemInfo = my.getSystemInfoSync();
    const screenWidth = systemInfo.windowWidth;

    my.createSelectorQuery().select('#canvas').node().exec((res) => {
      const canvas = res[0].node;
      if (!canvas || !canvas.getContext) return;

      const ctx = canvas.getContext('2d');

      // 设置 canvas 像素尺寸(避免模糊)
      canvas.width = screenWidth;
      canvas.height = 200;

      // 动态计算参数
      let width = screenWidth * 0.9;
      let x = (screenWidth - width) / 2; // 居中显示
      let y = 0;
      let height = 100;
      let radius = Math.min(width, height) / 2;
      let indentationWidth = width * 0.35;
      let indentationDepth = 15;

      // 设置颜色
      ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';

      // 调用绘制函数
      this.drawRoundedIndentedRect(ctx, x, y, width, height, radius, indentationWidth, indentationDepth);
    });
  },

  drawRoundedIndentedRect(ctx, x, y, width, height, radius, indentationWidth, indentationDepth) {
    ctx.beginPath();
    ctx.moveTo(x + radius, y);
    ctx.lineTo(x + width / 2 - indentationWidth / 2, y);

    ctx.bezierCurveTo(
      x + width / 2 - indentationWidth / 4, y,
      x + width / 2 - indentationWidth / 4, y + indentationDepth,
      x + width / 2, y + indentationDepth
    );

    ctx.bezierCurveTo(
      x + width / 2 + indentationWidth / 4, y + indentationDepth,
      x + width / 2 + indentationWidth / 4, y,
      x + width / 2 + indentationWidth / 2, y
    );

    ctx.lineTo(x + width - radius, y);
    ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
    ctx.lineTo(x + width, y + height - radius);
    ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
    ctx.lineTo(x + radius, y + height);
    ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
    ctx.lineTo(x, y + radius);
    ctx.quadraticCurveTo(x, y, x + radius, y);

    ctx.closePath();
    ctx.fill();
  }
});

在 css 中使用 canvas 实现平滑内凹圆角

直接展示源码了,和上边小程的差不多

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas - 带凹陷的圆角矩形</title>
    <style>
        canvas {
            display: block;
            margin: 40px auto;
            box-shadow: 0px 0px 5px #ccc;
            border-radius: 8px;
            background: url("./截屏2025-05-28 11.15.50.png") no-repeat center center;
            background-size: cover;
        }
    </style>
</head>

<body>
    <canvas id="canvas" width="500" height="200">
        当前浏览器不支持canvas元素,请升级或更换浏览器!
    </canvas>

    <script>
        function draw() {
            const canvas = document.getElementById('canvas');
            if (!canvas || !canvas.getContext) return;

            const ctx = canvas.getContext('2d');

            // 动态设置画布大小
            canvas.width = window.innerWidth * 0.9;
            canvas.height = 150;

            ctx.clearRect(0, 0, canvas.width, canvas.height);

            let width = canvas.width * 0.9;
            let height = 80;
            let x = (canvas.width - width) / 2;
            let y = 40;

            let radius = Math.min(width, height) / 2;
            let indentationWidth = width * 0.7;
            let indentationDepth = 20;

            ctx.fillStyle = '#bfc';
            drawRoundedIndentedRect(ctx, x, y, width, height, radius, indentationWidth, indentationDepth);
        }

        function drawRoundedIndentedRect(ctx, x, y, width, height, radius, indentationWidth, indentationDepth) {
            ctx.beginPath();

            ctx.moveTo(x + radius, y);
            ctx.lineTo(x + width / 2 - indentationWidth / 2, y);

            ctx.bezierCurveTo(
                x + width / 2 - indentationWidth / 4, y,
                x + width / 2 - indentationWidth / 4, y + indentationDepth,
                x + width / 2, y + indentationDepth
            );

            ctx.bezierCurveTo(
                x + width / 2 + indentationWidth / 4, y + indentationDepth,
                x + width / 2 + indentationWidth / 4, y,
                x + width / 2 + indentationWidth / 2, y
            );

            ctx.lineTo(x + width - radius, y);
            ctx.quadraticCurveTo(x + width, y, x + width, y + radius);

            ctx.lineTo(x + width, y + height - radius);
            ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);

            ctx.lineTo(x + radius, y + height);
            ctx.quadraticCurveTo(x, y + height, x, y + height - radius);

            ctx.lineTo(x, y + radius);
            ctx.quadraticCurveTo(x, y, x + radius, y);

            ctx.closePath();
            ctx.fill();
        }

        window.addEventListener('load', draw);
        window.addEventListener('resize', draw);
    </script>
</body>

</html>

嘿嘿!
简单点,直接找UI 要背景图,直接用就好 !!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值