SVG waves with feDisplacementMap
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.