返回

Spring动画裁剪SVG Ellipse?一文解决

vue.js

Spring 动画导致 SVG Ellipse 元素被裁剪

当使用 Spring 动画控制 SVG 图形(例如 ellipse)缩放时,有时会发现元素在缩放超过一定程度时被裁剪。这通常与 mask 属性的使用相关。以下将分析问题原因,并提供几种可行的解决方案,以及相关的代码示例和操作步骤。

问题分析

该问题的根本原因是,当 SVG 中的元素应用了 mask 属性后,该元素会被限定在 mask 定义的区域内。当一个椭圆元素(或任何被 mask 包裹的元素)被 Spring 动画放大时,若其缩放后的边界超出 mask 定义的区域,超出的部分将被隐藏(裁剪),这就是为何会发生裁剪的原因。

mask 本身是一种限制可视区域的机制,其行为是符合预期的。 问题在于动画和mask区域不协调。理解 mask 属性的工作原理是解决问题的关键。它基于像素而非矢量计算,导致即使 mask 本身没有明确设置大小,其作用区域也可能受其他因素限制,如父元素的尺寸。

解决方案

以下提供几种针对该问题的解决方案,以及具体的代码和操作方法。

解决方案一:调整 mask 元素大小或 viewBox

核心思路是让 mask 的有效范围覆盖住元素动画缩放的最大值。可以通过修改 mask 元素或 SVG 的 viewBox 属性实现,扩大 mask 可见区域,使其能够容纳缩放的动画。

<svg width="450" height="400" viewBox="-50 -20 500 400" 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="userSpaceOnUse" x="-50" y="-20" width="500" height="400">
    <path 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>
  • 操作步骤:

    1. 计算 ellipse 缩放后覆盖的最小区域,或者预设一个足够大的 viewBox
    2. 调整 mask 元素的 x, y,width, height, 或 viewBox属性确保 mask 覆盖整个动画过程。
    3. 进行测试,观察裁剪是否被解决。

    说明: 修改后的 <svg>viewBox="-50 -20 500 400"<mask> 标签 x y width height 对应的进行了修改,以便在元素缩放时能完整显示。通过 viewBox 属性的设置,我们扩大了画布的大小,让整个图形有了更大的展示空间。与此同时,我们也相应调整了mask元素的位置和尺寸,以确保裁剪不再发生。这样做的一个重要好处是,我们不会因为mask的限制而丢失部分图像内容,从而实现动画效果的完整展示。
    此外,设置svg标签中的 overflow: visible;可以使svg中的子元素在超出父元素边界的时候可见,不裁剪。

解决方案二:移除 mask 元素或调整其使用方式

如果可以,移除 mask 属性是最直接的方法。但如果需要使用 mask,可以尝试其他方法:使用多个独立的 mask 区域,确保每个动画元素的mask区域都足够大,不会在缩放时发生裁剪, 也可以尝试其他图形元素的裁剪方式。

  <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="userSpaceOnUse" x="0" y="0" width="400" height="338">
          <path 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>
  • 操作步骤:

    1. 直接删除 g 标签的 mask='url(#mask0_0_1)' 属性,并且注释 mask 标签
    2. 测试图形动画效果。

    说明: 移除后虽然不在有裁剪效果,但同时可能会造成其他的显示效果问题,需根据实际业务和设计要求来考虑。此方案适用于在mask不是必须时。

解决方案三: 使用clipPath替代mask
可以使用 SVG 中的 clipPath 来裁剪元素,并可以根据动画变化进行调整,让动画更为灵活。
由于文章重点是spring动画导致的元素裁剪,对 clipPath 相关的知识不做展开叙述。可以自行了解。

 <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">
        <clipPath id="clipPath">
            <path 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"/>
        </clipPath>
         <g  style="overflow: visible;" clip-path='url(#clipPath)'>
              <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>

  • 操作步骤:

    1. 替换mask元素和对应的g标签mask属性, 使用clipPath来完成裁剪。
    2. 进行测试,观察裁剪是否被解决, 动画和mask显示效果是否符合预期。

    说明: 该方案核心是用 <clipPath> 来替代 <mask>,通过定义剪切路径,可以在动画的过程中对元素进行剪切,以实现更精准和可控的显示效果。 使用<clipPath> 有助于更好的解决因为mask 元素大小导致的动画裁剪问题,但clipPath 更侧重于形状裁剪。clipPath 通常用于简单规则形状裁剪,但 mask 功能更为强大。选择哪一种方式取决于项目的需求,确保选中的方式在整个过程中都是最合适且安全的。

附加建议

  • 在实现复杂动画效果时,提前规划,充分考虑动画可能产生的边界,并据此设置 SVG 的 viewBox 和 mask 的尺寸。
  • 使用 CSS 的 overflow: visible; 属性确保 SVG 内部的元素超出边界时能够正确显示,而不被裁剪。
  • 定期测试,并在不同浏览器中测试,确保效果一致,避免因为兼容性导致的显示异常。

以上解决方案旨在解决使用 spring 动画时 SVG 元素因 mask 导致的裁剪问题, 在实际开发过程中,需结合具体场景选择合适的方案。