First thing first: my English may not be as good as required for writing a post on Codepen. Please feel free to correct my spelling and my grammar in the comments below.

About <feDisplacementMap>

The filter primitive uses the pixel values of the image from in2 to spatially displace the image from in.

  <feDisplacementMap in2="img1" in="img2" scale="15" xChannelSelector="R" yChannelSelector="G"/>

The scale attribute defines the displacement scale.

The xChannelSelector attribute indicates which channel from in2 is used to displace the pixels from in along the x-axis. Possible values for the xChannelSelector attribute are:

  • R (the red channel)
  • G (the green channel)
  • B (the blue channel) or
  • A (the alpha channel)

The same goes for the yChannelSelector attribute, only this time the pixels are displaced along the y-axis.

A static example

In the following example I have two SVG elements: A first one, a witness where I'm displaying the radial gradient used as a value for in2, and a second one for the filtered image.

A few definitions:

In the <defs> element I'm creating a radial gradient like this:

     <radialGradient id="rg" r=".9">  
      <stop offset="0%" stop-color="#f00"></stop>
      <stop offset="10%" stop-color="#000"></stop>
      <stop offset="20%" stop-color="#f00"></stop>
      <stop offset="30%" stop-color="#000"></stop>
      <stop offset="40%" stop-color="#f00"></stop>
      <stop offset="50%" stop-color="#000"></stop>
      <stop offset="60%" stop-color="#f00"></stop>
      <stop offset="70%" stop-color="#000"></stop>
      <stop offset="80%" stop-color="#f00"></stop>
      <stop offset="90%" stop-color="#000"></stop> 
      <stop offset="100%" stop-color="#f00"></stop>
    </radialGradient>

In addition I'm creating a <rect> element filled with the gradient above.

  <rect id="witness" width="300" height="300" fill="url(#rg)"></rect>

Also in the <defs> I'm creating the filter.

Building the filter

For the filter I'm using primitiveUnits="objectBoundingBox". This means that any length values within the filter definitions represent fractions or percentages of the bounding box.

In the case of filters defaults for x and y are -10% and defaults for width and height are 120%. The default for filterUnits is "objectBoundingBox". I'm OK with these.

  <filter id="f" primitiveUnits="objectBoundingBox">
. . . .
</filter>

I'm using the feImage filter to reference and use the rect id="witness" defined above (a fragment in the same document). Unfortunately Firefox do not support fragments. I'll fix this latter.

  <filter id="f" primitiveUnits="objectBoundingBox" >
      <feImage result="pict2" xlink:href="#witness"></feImage>
      . . . . 
</filter>

The <feDisplacementMap> filter primitive will use the red channel (R) from this image (result="pict2") to displace the pixels from the source graphic (the dog image in this case).

  <filter id="f" primitiveUnits="objectBoundingBox" >
      <feImage result="pict2" xlink:href="#gradient"></feImage>
      <feDisplacementMap scale=".05" xChannelSelector="R" yChannelSelector="R" in2="pict2" in="SourceGraphic">
   </feDisplacementMap>
</filter>

Now I can apply this filter (#f) to the image:

  <image filter="url(#f)" xlink:href=". . . beagle400.jpg" width="300" height="300" id="Darwin" ></image>

And this is the result (for now it works only in Chrome and in Edge):

Animating the wave

In the second example I'm taking this a step further: I'm animating the radial gradient in a ripple movement.

Practically I'm animating the value of the offset of each stop element. First I need the array of the stop elements:

  let radGrd = Array.from(document.querySelectorAll("#rg stop"));

Now I can use the method map to update the value of the offset for each stop element.

  let displacement = 0;
let speed = 0.2;

function AnimateOffset() {
  radGrd.map((gr,i) =>{
    gr.setAttributeNS(null, "offset", (i - 2) * 10 + displacement + "%");
  });

  if (displacement <= 20) {
    displacement += speed;
  } else {
    displacement = 0;
  }
  window.requestAnimationFrame(AnimateOffset);
}
window.requestAnimationFrame(AnimateOffset);

And it works!!

Unfortunately it works only in Chrome and in Edge. The culprit is <feImage> and the bug 455986.

However there is a way to  fix  elude this problem. The solution is to use SVG as data:URI instead of fragments.

You can read in detail about this here in Codepen: Optimizing SVGs in data URI, a post by Taylor Hunt.

How to transform an SVG to data:URI

One way to do it is using the unencoded SVG code like this:

'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300px" height="300px" viewBox="0 0 300 300"><path fill="#FFFFFF" stroke="#ED1D24" d=" . . . . /><svg>'

See an example here.

And this works but, again, not in all browsers. It can be done better. There are a few rules to follow:

#1. Prepare the SVG as if it were an external SVG file (declaring namespaces):

  <svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='300 ' height='300'>
<defs>
   <radialGradient id="rg" r=".9">  
      <stop offset="0%" stop-color="#f00"></stop>
      <stop offset="10%" stop-color="#000"></stop>
      <stop offset="20%" stop-color="#f00"></stop>
      <stop offset="30%" stop-color="#000"></stop>
      <stop offset="40%" stop-color="#f00"></stop>
      <stop offset="50%" stop-color="#000"></stop>
      <stop offset="60%" stop-color="#f00"></stop>
      <stop offset="70%" stop-color="#000"></stop>
      <stop offset="80%" stop-color="#f00"></stop>
      <stop offset="90%" stop-color="#000"></stop> 
      <stop offset="100%" stop-color="#f00"></stop>
   </radialGradient> 
 </defs>  
   <rect id="witness" width="300" height="300" fill="url(#rg)"></rect>
</svg>

#2. Put all the attributes inside between single-quotes and remove the spaces and line breaks between tags:

  <svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='300 ' height='300'><defs><radialGradient id='rg' r=".9"><stop offset='0%' stop-color='#f00'></stop><stop offset='10% 'stop-color='#000'></stop><stop offset='20%' stop-color='#f00'></stop><stop offset='30%' stop-color='#000'></stop><stop offset='40%' stop-color='#f00'></stop><stop offset='50%' stop-color='#000'></stop><stop offset='60%' stop-color='#f00'></stop><stop offset='70%' stop-color='#000'></stop><stop offset='80% 'stop-color='#f00'></stop><stop offset='90%' stop-color='#f00'></stop><stop offset='100%' stop-color='#f00'></stop><radialGradient></defs><rect id='gradient' x='0' y='0' width='300' height='300' fill='url(#rg)' /></svg>

#3. Code all the unsafe characters but not the white space:

< becomes %3C
> becomes %3E

For example <svg> becomes %3Csvg%3E

# becomes %23

For examples #abcfef becomes %23abcdef

The hyphen - becomes %2D

For example stroke-width='2' becomes stroke%2Dwidth='2'

% becomes %25

For example width ='50%' becomes width ='50%25'

You may see an example here: How to transform an SVG to data:URI

Now <feImage> should look like this:

  <feImage result="pict2" xlink:href="data:image/svg+xml;utf8,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='300' height='300'%3E%3Cdefs%3E%3CradialGradient id='rg' r='.9'%3E%3Cstop offset='0%' stop%2Dcolor='%23f00'%3E%3C/stop%3E%3Cstop offset='10%' stop%2Dcolor='%23000'%3E%3C/stop%3E%3Cstop offset='20%' stop%2Dcolor='%23f00'%3E%3C/stop%3E%3Cstop offset='30%' stop%2Dcolor='%23000'%3E%3C/stop%3E%3Cstop offset='40%' stop%2Dcolor='%23f00'%3E%3C/stop%3E%3Cstop offset='50%' stop%2Dcolor='%23000'%3E%3C/stop%3E%3Cstop offset='60%' stop%2Dcolor='%23f00'%3E%3C/stop%3E%3Cstop offset='70%' stop%2Dcolor='%23000'%3E%3C/stop%3E%3Cstop offset='80%' stop%2Dcolor='%23f00'%3E%3C/stop%3E%3Cstop offset='90%' stop%2Dcolor='%23000'%3E%3C/stop%3E%3Cstop offset='100%' stop%2Dcolor='%23f00'%3E%3C/stop%3E%3C/radialGradient%3E%3C/defs%3E%3Crect id='gradient' x='0' y='0' width='300' height='300' fill='url(%23rg)'%3E%3C/rect%3E%3C/svg%3E">
</feImage>

In order to animate the whole thing I'm writing a function that builds the string for the data:URI. In the comments I'm showing the SVG markup equivalent.

  function setXlinkHref(){
  /*
  <svg width="300" height="300">
   <defs>
     <radialGradient id="rg" r=".9"> 
  */
  let xlinkHref = "data:image/svg+xml;utf8,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='300' height='300'%3E%3Cdefs%3E%3CradialGradient id='rg' r='.9'%3E"; 
  /*
       <stop offset='0%' stop-color='#f00'></stop>
       <stop offset='10% 'stop-color='#000'></stop>
       <stop offset='20%' stop-color='#f00'></stop>
       <stop offset='30%' stop-color='#000'></stop>
       <stop offset='40%' stop-color='#f00'></stop>
       <stop offset='50%' stop-color='#000'></stop>
       <stop offset='60%' stop-color='#f00'></stop>
       <stop offset='70%' stop-color='#000'></stop>
       <stop offset='80% 'stop-color='#f00'></stop>
       <stop offset='90%' stop-color='#f00'></stop> 
       <stop offset='100%' stop-color='#f00'></stop>
  */  
for(var i = 0; i < 11; i++){
  xlinkHref += `%3Cstop 
                offset='${((i-2)*10)+displacement}%25' 
                stop%2Dcolor='%23${(i%2 == 0)? 'f00' : '000'}'%3E%3C/stop%3E`;
}  
   /*
   </radialGradient> 
   <rect id="gradient" x="0" y="0" width="300" height="300" fill="url(#rg)"></rect>
  </svg>
   */  
xlinkHref += "%3C/radialGradient%3E%3C/defs%3E%3Crect id='gradient' x='0' y='0' width='300' height='300' fill='url(%23rg)'%3E%3C/rect%3E%3C/svg%3E";

return xlinkHref;
}

I'll be using the returned value of this function to animate the value for the <feimage>'s attribute xlink:href.

  filterFeImage.setAttributeNS(xlink, "href", xlinkHref);

How to fill an SVG path with an image

I need to add something more in the <defs>: a pattern. This is because I want to delete the <radialGradient> element and I need a way to paint an image on the <rect id="witness">.

  <defs>
. . . . . . . . 
<pattern id="imageFill" width="1" height="1"
viewBox="0 0 300 300" >
<image id="ripples" width="300" height="300"
xlink:href="" />
</pattern>
</defs> 

<rect id="witness" width="300" height="300" fill="url(#imageFill)" ></rect>

This is a technique I've learned from Amelia's new book.

The animation

The animation uses the returned value of setXlinkHref() to update the xlink:href attribute for the <feImage> and for the ripples image within the pattern.

    let xlinkHref = setXlinkHref();
  filterFeImage.setAttributeNS(xlink, "href", xlinkHref);
  ripples.setAttributeNS(xlink, "href", xlinkHref);

Also updates the displacement value (used to displace the ripples).

  function AnimateOffset() {

  let xlinkHref = setXlinkHref();
  filterFeImage.setAttributeNS(xlink, "href", xlinkHref);
  ripples.setAttributeNS(xlink, "href", xlinkHref);

  if (displacement <= 20) {
    displacement += speed;
  } else {
    displacement = 0;
  }

  window.requestAnimationFrame(AnimateOffset);
}

window.requestAnimationFrame(AnimateOffset);

And this is the final result:

This should work in Chrome, Opera, Firefox & Edge.

Thank you for reading. I hope you find it useful.


4,877 5 59