Transition an SVG Filter

2020-07-08 07:29发布

问题:

I'm trying to create a material design like shadow on an SVG circle. I want this shadow to grow with a nice transition when you click on the circle, but at the moment I'm struggling to even figure out if it's possible to animate this transition, so I'm hoping someone might be able to help.

I've added a small example of what I've got so far, a circle with a dropshadow which changes on mouseover. I spent quite a while trying to do the dropshadow in CSS but came to the conclusion that I don't think it's possible right now.

Now that I've got the shadows though, I can't find a way to animate them. I've found some example using animation tags for single properties (e.g. colour of the circle) and found examples using keyframes for CSS transitions, but here I want to modify the actual filter itself. Is this possible and can someone illustrate how you might achieve this - ideally I'm trying to achieve IE10/FF/Chrome compatibility so I'd be interested to know if there are any complications with the solution?

circle {
    fill: #8BC34A;
    stroke: white;
    stroke-width: 2px;
    filter: url(#f1);
    transition: 2s ease;
}

circle:hover {
    filter: url(#f2);
    transition: 2s ease;
}
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="500" height="500" viewPort="0 0 200 200">
  <defs>
    <filter id="f1" x="-40%" y="-40%" height="200%" width="200%">
      <feOffset result="offOut" in="SourceAlpha" dx="0" dy="0" />
      <feGaussianBlur result="blurOut" in="offOut" stdDeviation="10" />
      <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
    </filter>
    <filter id="f2" x="-40%" y="-40%" height="200%" width="200%">
      <feOffset result="offOut" in="SourceAlpha" dx="0" dy="0" />
      <feGaussianBlur result="blurOut" in="offOut" stdDeviation="30" />
      <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
    </filter>
  </defs>
  <circle r="100" cx="150" cy="150" />
</svg>

UPDATE

After trying a few things, I've put some examples together, although none of them quite do what I want. I need to be able to toggle the transitions on a single/several element (rather than every circle in the SVG), for which I may have several hundred. I also ultimately want to be changing both the size of the circle (lifting as per material design) and then increasing the side of the shadow underneath.

/*************************/
/* JavaScript Animations */
/*************************/
(function() { 
    var svg = d3.select("#svg_javaScriptAnimation");
    setInterval(function() {
        
        // Animate
        svg.selectAll(".circle")
           .transition()
           .duration(1950)
           .attr("r", 130);
        
        svg.selectAll(".jA_shadow")
           .transition()
           .duration(1950)
           .attr("r", 130);
        
         svg.selectAll(".jA_shadow_expanding")
           .transition()
           .duration(1950)
           .attr("r", 140);
        
        svg.selectAll(".jA_shadow_large")
           .transition()
           .duration(1950)
           .attr("r", 110);
        
        // Reset
         svg.selectAll(".circle")
           .transition()
           .delay(1960)
           .duration(1)
           .attr("r", 110);
        
         svg.selectAll(".jA_shadow")
           .transition()
           .delay(1960)
           .duration(1)
           .attr("r", 110);
        
         svg.selectAll(".jA_shadow_expanding")
           .transition()
           .delay(1960)
           .duration(1)
           .attr("r", 110);
        
        svg.selectAll(".jA_shadow_large")
           .transition()
           .delay(1960)
           .duration(1)
           .attr("r", 80);
    }, 2000);
})();
circle {
   fill: #8BC34A;
   stroke: white;
   stroke-width: 2px;
}

text {
    fill: white;
}

/*****************/
/* CSS KeyFrames */
/*****************/
#svg_keyframes{
  animation:filters 2s infinite;
}

@-webkit-keyframes filters {
  0%{ 
    -webkit-filter:drop-shadow(0px 16px 10px #333); 
  }
  100% { 
    -webkit-filter:drop-shadow(0px 16px 30px #333); 
  }
}

/***********************************/
/* CSS KeyFrames using SVG Filters */
/***********************************/

.kf_Shadow1 {
    -webkit-animation-name: shadow-expand; / Chrome, Safari, Opera /
    -webkit-animation-duration: 2s; / Chrome, Safari, Opera /
    -webkit-animation-iteration-count: infinite;
    animation-name: shadow-expand;
    animation-duration: 2s;
    animation-iteration-count: infinite;
}

.kf_Fill1 {
    -webkit-animation-name: circle-fill; / Chrome, Safari, Opera /
    -webkit-animation-duration: 2s; / Chrome, Safari, Opera /
    -webkit-animation-iteration-count: infinite;
    animation-name: circle-fill;
    animation-duration: 2s;
    animation-iteration-count: infinite;
}

.kf_DropShadow1 {
    -webkit-animation-name: drop-shadow-expand; / Chrome, Safari, Opera /
    -webkit-animation-duration: 2s; / Chrome, Safari, Opera /
    -webkit-animation-iteration-count: infinite;
    animation-name: drop-shadow-expand;
    animation-duration: 2s;
    animation-iteration-count: infinite;
}

/* Demonstrate that fill works correctly */
@keyframes circle-fill {
    0% { fill: #FF0000; }       
    25% { fill: #BB0033; }       
    50% { fill: #990066; }        
    75% { fill: #4400aa; }         
    100% { fill: #0000ff; }       
}

/* Demonstrate that filter doesn't work as hoped */
@keyframes shadow-expand {
    0% { filter: url(#f1); -webkit-filter: url(#f1);}       
    25% { filter: url(#f2); -webkit-filter: url(#f1);}       
    50% { filter: url(#f3); -webkit-filter: url(#f1);}       
    75% { filter: url(#f4); -webkit-filter: url(#f1);}       
    100% { filter: url(#f5); -webkit-filter: url(#f1);}       
}

@keyframes drop-shadow-expand {
    0% { -webkit-filter:drop-shadow(0px 16px 10px #333); }       
    25% { -webkit-filter:drop-shadow(0px 16px 15px #333); }       
    50% { -webkit-filter:drop-shadow(0px 16px 20px #333); }       
    75% { -webkit-filter:drop-shadow(0px 16px 25px #333); }       
    100% { -webkit-filter:drop-shadow(0px 16px 30px #333); }       
}

/*************************/
/* SVG Filter Animations */
/*************************/

.fA_shadow {
  filter: url(#f1);
}

/*************************/
/* JavaScript Animations */
/*************************/
.jA_shadow {
    filter: url(#f1);
    stroke: none !important;
}

.jA_shadow_expanding {
    filter: url(#f1);
    stroke: none !important;
    fill: #CCC !important;
}

.jA_shadow_large {
    filter: url(#f2);
     stroke: none !important;
    fill: #CCC !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<h1>CSS Keyframes</h1>
<p>The downside here is that the animation seems to require attaching to the svg element, which causes all of the circles to animate their drop shadows</p>
<svg id="svg_keyframes" width="1000" height="280">
    <g transform="translate(120, 140)">
        <circle r="110"/>
        <text dx="-1.5em">Circle 1</text>
    </g>
    <g transform="translate(420, 140)">
        <circle r="110"/>
        <text dx="-1.5em">Circle 2</text>
    </g>
</svg>


<h1>CSS Keyframes referencing SVG Filters</h1>
<p>Unfortunately it seems that this approach simply doesn't work. The idea was that the class would change triggering a keyframe which would progressively change the filter being applied by specifying gradually expanding filters</p>
<svg id="svg_filterKeyFrames" width="1000" height="280">
    <defs>
        <filter id="f1" x="-40%" y="-40%" height="200%" width="200%">
            <feOffset result="offOut" in="SourceAlpha" dx="0" dy="4" />
            <feGaussianBlur result="blurOut" in="matrixOut" stdDeviation="10" />
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
        </filter>
        <filter id="f2" x="-40%" y="-40%" height="200%" width="200%">
            <feOffset result="offOut" in="SourceAlpha" dx="0" dy="7" />
            <feGaussianBlur result="blurOut" in="matrixOut" stdDeviation="15" />
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
        </filter>
         <filter id="f3" x="-40%" y="-40%" height="200%" width="200%">
            <feOffset result="offOut" in="SourceAlpha" dx="0" dy="10" />
            <feGaussianBlur result="blurOut" in="matrixOut" stdDeviation="20" />
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
        </filter>
         <filter id="f4" x="-40%" y="-40%" height="200%" width="200%">
            <feOffset result="offOut" in="SourceAlpha" dx="0" dy="13" />
            <feGaussianBlur result="blurOut" in="matrixOut" stdDeviation="25" />
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
        </filter>
         <filter id="f5" x="-40%" y="-40%" height="200%" width="200%">
            <feOffset result="offOut" in="SourceAlpha" dx="0" dy="16" />
            <feGaussianBlur result="blurOut" in="matrixOut" stdDeviation="30" />
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
        </filter>
    </defs>
     <g transform="translate(120, 140)">
        <circle class="kf_Shadow1" r="110"/>
        <text dx="-4.5em">Shadow should change</text>
    </g>
    <g transform="translate(420, 140)">
        <circle class="kf_Fill1" r="110"/>
        <text dx="-4.5em">Colour should change</text>
    </g>
     <g transform="translate(720, 140)">
        <circle class="kf_DropShadow1" r="110"/>
        <text dx="-5.5em">Drop Shadow should change</text>
    </g>
</svg>

<h1>SVG Filters Animations</h1>
<p>SVG filter animations are another way to tackle this problem, but it seems that they behave very similar to a CSS filter in that because they are shared all of the elements update</p>
<svg id="svg_filterAnimation" width="1000" height="280">
    <defs>
        <filter id="f1" x="-40%" y="-40%" height="200%" width="200%">
            <feOffset result="offOut" in="SourceAlpha" dx="0" dy="4" />
            <feGaussianBlur id="blur1" result="blurOut" in="matrixOut" stdDeviation="10" />
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
        </filter>
    </defs>
     <g transform="translate(120, 140)">
        <circle class="fA_shadow" r="110"/>
        <text dx="-1.5em">Circle 1</text>
    </g>
     <g transform="translate(420, 140)">
        <circle class="fA_shadow" r="110"/>
        <text dx="-1.5em">Circle 2</text>
    </g>
    <animate xlink:href="#blur1" attributeName="stdDeviation" from="10" to="30" dur="2s" begin="0s" repeatCount="indefinite"/>
</svg>

<h1>JavaScript Animations</h1>
<p>Animation via JavaScript is another approach, this uses D3 but the issue here is changing the size of gaussian blur that operates on the shadow is incredibly difficult as demonstrated in Circle 2</p>
<svg id="svg_javaScriptAnimation" width="1000" height="280">
    <defs>
        <filter id="f1" x="-40%" y="-40%" height="200%" width="200%">
            <feOffset result="offOut" in="SourceAlpha" dx="0" dy="4" />
            <feGaussianBlur id="blur1" result="blurOut" in="matrixOut" stdDeviation="10" />
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
        </filter>
         <filter id="f2" x="-40%" y="-40%" height="200%" width="200%">
            <feOffset result="offOut" in="SourceAlpha" dx="0" dy="4" />
            <feGaussianBlur id="blur1" result="blurOut" in="matrixOut" stdDeviation="30" />
            <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
        </filter>
    </defs>
     <g transform="translate(120, 140)">
        <circle class="jA_shadow" r="110"/>
        <circle class="circle" r="110"/>
        <text dx="-1.5em">Circle 1</text>
    </g>
     <g transform="translate(420, 140)">
        <circle class="jA_shadow_expanding" r="110"/>
        <circle class="circle" r="110"/>
        <text dx="-1.5em">Circle 2</text>
    </g>
     <g transform="translate(720, 140)">
        <circle class="jA_shadow_large" r="80"/>
        <circle class="circle" r="110"/>
        <text dx="-1.5em">Circle 3</text>
    </g>
    <animate xlink:href="#blur1" attributeName="stdDeviation" from="10" to="30" dur="2s" begin="0s" repeatCount="indefinite"/>
</svg>

回答1:

CSS transition and CSS animation can only be used in cases where what you want to animate is controlled by CSS. Say, you can use it if you want to animate stroke-width. But it is very limited.

It is possible to use <animate> to animate SVG filters. Say, you can add an id="blur1" to <feGaussianBlur> of f1 and use this to animate it: jsfiddle

<animate xlink:href="#blur1" attributeName="stdDeviation"
from="10" to="30" dur="1s" begin="0s" repeatCount="indefinite"/>

The begin attribute can theoretically be bound to an event, say mouseover, but your mileage may vary because it is bound to the filter task, which is not useful at all.

A third alternative is to animate it using JavaScript requestAnimationFrame. You will need to write a lot of code, and it will not be GPU-accelerated, but you always get what you want.