You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 09:38:09