效果图:

基础版本(绝对尺寸)
基本要点:
- 使用svg mask(遮罩)实现形状的裁剪。
- 使用
filter: drop-shadow实现裁剪后图形的阴影。 - svg使用绝对定位+不可选中避免影响文档流和防止干扰用户操作。(不能隐藏,隐藏后遮罩失效,包括
display:none、visibility: hidden)可以将透明度改为0。
<svg width="320" height="160" style="position: absolute; user-select: none">
<defs>
<mask id="rounded-grooves-mask" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse">
<!-- 整体保留区域:白色填充的矩形 -->
<rect x="0" y="0" width="320" height="180" fill="white" />
<!-- 要裁剪掉的两个圆(黑色会被“挖掉”) -->
<circle cx="0" cy="90" r="40" fill="black" />
<circle cx="320" cy="90" r="40" fill="black" />
</mask>
</defs>
</svg>
<div class="card-container" ref="card-area">
<div class="card"></div>
</div>
.card-container {
filter: drop-shadow(var(--shadow));
width: fit-content;
height: fit-content;
}
.card {
width: 320px;
height: 180px;
padding: 40px;
background-color: white;
mask: url(#rounded-grooves-mask);
mask-composite: exclude;
}
此种方法必须手动设置、更新svg尺寸、遮罩尺寸、圆心位置和半径,仅实现了“先裁剪,再添加阴影”的效果。
进阶版本(动态尺寸)

改进点:
- 使用ResizeObserver监听内容元素尺寸变化,更新响应式变量,svg中与尺寸相关的均绑定为响应式变量。
- 圆半径使用响应式变量。
完整vue3代码:
<template>
<svg :width="size.x" :height="size.y" style="position: absolute; user-select: none">
<defs>
<mask id="rounded-grooves-mask" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse">
<rect x="0" y="0" :width="size.x" :height="size.y" fill="white" />
<circle cx="0" :cy="size.y / 2" :r="props.radius" fill="black" />
<circle :cx="size.x" :cy="size.y / 2" :r="props.radius" fill="black" />
</mask>
</defs>
</svg>
<div
class="card-container"
ref="card-area"
:style="{
'--shadow': props.shadow,
}"
>
<div class="card"></div>
</div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, useTemplateRef } from "vue";
const props = defineProps({
radius: {
type: Number,
default: 40,
},
shadow: {
type: String,
default: "0px 0px 5px #929598",
},
});
const cardArea = useTemplateRef("card-area");
const size = ref({
x: 0,
y: 0,
});
let observer = null;
onMounted(() => {
observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
size.value = {
x: width,
y: height,
};
}
});
if (cardArea.value) {
observer.observe(cardArea.value);
}
});
onBeforeUnmount(() => {
observer?.disconnect();
});
</script>
<style scoped>
.card-container {
filter: drop-shadow(var(--shadow));
width: fit-content;
height: fit-content;
}
.card {
width: 320px;
height: 180px;
background-color: white;
mask: url(#rounded-grooves-mask);
mask-composite: exclude;
}
</style>
内容区域设置padding,使用插槽即可。
不可行的方法
box-shadow + clip-path
clip-path是对元素的“最终图像”的裁剪,使用box-shadow创建的阴影会被一并裁剪,导致阴影在裁剪的地方不显示。
clip-path:path() 或 svg + clip-path:url()
使用path或svg中的clipPath绘制路径时,不能正确处理第二个圆形的填充规则。设置fill-rule="evenodd"后,依然不能正确处理叠加关系,导致左侧半圆效果正常,而右侧半圆与原始图形的效果为“交集”,而非要求的差集。可能和圆形曲线绘制顺序有关,有待测试。
兼容性
https://caniuse.com/?search=mask
https://caniuse.com/?search=filter%3A%20drop-shadow
https://caniuse.com/?search=ResizeObserver


