Vue十字准星组件扫描动画椭圆元素缩放时被裁剪的问题
解决SVG遮罩下缩放元素被裁剪的问题
问题场景
开发的Vue十字准星组件中,当状态切换为Scan时,椭圆元素执行scale(1.3)的扫描动画,此时椭圆会被遮罩裁剪;移除g元素上的mask='url(#mask0_0_1)'属性后裁剪消失,但丢失了所需的遮罩效果。
问题原因
当前遮罩使用了maskUnits="userSpaceOnUse"属性,该属性会将遮罩的坐标和尺寸绑定到SVG的固定视口(400x338)。当椭圆通过动画缩放超过1倍后,部分区域超出了遮罩的原始范围,因此被裁剪。overflow: visible对遮罩无效,因为遮罩的作用范围是由其定义的尺寸决定的。
解决方案
推荐使用objectBoundingBox类型的遮罩单位,让遮罩基于应用它的元素的边界框自适应尺寸,具体修改如下:
修改遮罩定义
将遮罩的maskUnits改为objectBoundingBox,并将遮罩内的路径坐标转换为相对值(通过transform缩放实现):
<mask id="mask0_0_1" style="mask-type:alpha" maskUnits="objectBoundingBox" x="0" y="0" width="1" height="1"> <path transform="scale(0.0025, 0.002958)" fill-rule="evenodd" clip-rule="evenodd" d="M400 0H0V338H400V0ZM200.076 240.1C232.991 240.1 261.381 223.28 274.462 199H395V140H275.147C262.391 115.02 233.577 97.5947 200.076 97.5947C154.747 97.5947 118 129.496 118 168.848C118 208.199 154.747 240.1 200.076 240.1Z" fill="#D9D9D9" /> </mask>
maskUnits="objectBoundingBox":让遮罩的坐标系统基于应用它的g元素的边界框,范围为0(左上角)到1(右下角)。transform="scale(0.0025, 0.002958)":将原路径的绝对坐标(基于400x338视口)转换为0-1的相对坐标,其中0.0025 = 1/400,0.002958 ≈ 1/338。
备选方案(灵活度较低)
如果不想修改maskUnits,可以直接扩大遮罩的原始范围,确保缩放后的椭圆不会超出遮罩边界:
<mask id="mask0_0_1" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="-200" y="-200" width="800" height="738"> <!-- 原路径保持不变 --> </mask>
这种方法需要预估元素的最大缩放比例,适合固定缩放倍数的场景。
修改后的完整Crosshair.vue代码
<script setup lang="ts"> enum crosshairState { Default, Pulse, Scan } withDefaults(defineProps<{ state?: crosshairState warningColors?: boolean }>(), { state: crosshairState.Default }) </script> <template> <svg :class="{'hide': state === crosshairState.Scan}" width="400" height="338" viewBox="0 0 400 338" fill="none" xmlns="http://www.w3.org/2000/svg" style="overflow: visible;" preserveAspectRatio="xMidYMid meet"> <mask id="mask0_0_1" style="mask-type:alpha" maskUnits="objectBoundingBox" x="0" y="0" width="1" height="1"> <path transform="scale(0.0025, 0.002958)" fill-rule="evenodd" clip-rule="evenodd" d="M400 0H0V338H400V0ZM200.076 240.1C232.991 240.1 261.381 223.28 274.462 199H395V140H275.147C262.391 115.02 233.577 97.5947 200.076 97.5947C154.747 97.5947 118 129.496 118 168.848C118 208.199 154.747 240.1 200.076 240.1Z" fill="#D9D9D9" /> </mask> <g mask='url(#mask0_0_1)' style="overflow: visible;"> <ellipse v-if="state === crosshairState.Scan" cx="218.674" cy="169.497" rx="171.674" ry="148.497" fill="none" opacity="0.5" stroke-width="2" :stroke="warningColors ? '#C02A4F' : '#95F39B'"/> <ellipse :class="{'pulse' : state === crosshairState.Pulse, 'scan': state === crosshairState.Scan, 'opacity-40': warningColors }" cx="218.674" cy="169.497" rx="171.674" ry="148.497" :fill="warningColors ? '#C02A4F' : '#95F39B'"/> </g> <path d="M275.299 140.3C262.621 115.16 233.711 97.5947 200.076 97.5947C154.747 97.5947 118 129.496 118 168.848C118 208.199 154.747 240.1 200.076 240.1C233.122 240.1 261.607 223.146 274.618 198.71" :stroke="warningColors ? '#C02A4F' : '#58C160'" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/> <rect :class="{'rect-pulse' : state === crosshairState.Pulse, 'rect-scan' : state === crosshairState.Scan}" x="271.3" y="136.3" width="119" height="8" rx="4" :fill="warningColors ? '#C02A4F' : '#58C160'"/> <rect :class="{'rect-pulse' : state === crosshairState.Pulse, 'rect-scan' : state === crosshairState.Scan}" x="271.3" y="194.71" width="119" height="8" rx="4" :fill="warningColors ? '#C02A4F' : '#58C160'"/> </svg> </template> <style> @keyframes pulseAnimation { 0%, 100% { opacity: 0; rx: 0px; ry: 0px; } 50% { opacity: 1; rx: 171.674px; ry: 148.497px; } } @keyframes rectPulseAnimation { 0%, 100% { width: 0px; } 50% { width: 119px; } } @keyframes scanAnimation { 0% { transform: scale(0); opacity: 0; fill: #95F39B; } 50% { transform: scale(1); opacity: 1; fill: #95F39B; } 85% { transform: scale(1.3); fill: white; } 100% { transform: scale(1); fill: #95F39B; } } @keyframes rectScanAnimation { 0% { width: 0px; } 75% { width: 125px; } 100% { width: 119px; } } @keyframes hide { 0% { opacity: 1 } 100% { opacity: 0; } } .pulse { animation: pulseAnimation 1.5s infinite ease-in-out; transform-origin: center; } .rect-pulse { animation: rectPulseAnimation 1.5s infinite; } .scan { animation: scanAnimation 3s ease forwards; transform-origin: center; } .rect-scan { animation: rectScanAnimation 3s ease forwards; transform-origin: center; } .hide { animation: hide 0.5s forwards; animation-delay: 3.5s; } </style>
内容的提问来源于stack exchange,提问作者Sasquatch
相关产品推荐
相关产品推荐

