Left ArrowBack

notes / SVG / filters / feDisplacementMap

feDisplacementMap

3 rows of squares. The 1st, 2 squares labled source & map. The 2nd, a square with feImage & feDisplacementMap written in it. The 3rd, a square labeled output.

feDisplacementMap

Planted

Status: seed

<feDisplacementMap> is a SVG filter primitive. It distorts an input (the source) to take the shape of a 2nd input (the map). If your unfamiliar with SVG filters, you can find a note explaining the fundamentals here. The primitive takes 2 inputs:

  • in (source) &
  • in2 (map).

The source asset can passed straight to the in attribute. The map asset 1st needs to be captured using the <feImage>. The result can then be passed to the in2 attribute.

Basic Example

localhost:3000

Console

<p>Source:</p>
<svg viewBox="0 0 400 100">
    <text
        x="40"
        y="72"
        font-size="108"
        fill="hsl(170, 100%, 25%)"
        font-weight="700">
        my text
    </text>
</svg>

<p>Map:</p>
<img
    alt="A towel."
    src="https://garden.bradwoods.io/images/towel.png" />

<p>Filtered:</p>
<svg viewBox="0 0 400 260">
    <filter 
        id="myFilter" 
        x="-50%" 
        y="-50%" 
        width="200%" 
        height="200%">

        <feImage
            href="https://garden.bradwoods.io/images/towel.png"
            x="0"
            y="0"
            width="100%"
            height="100%"
            preserveAspectRatio="none"
            result="MAP_RESULT" />

        <feDisplacementMap
            in="SourceGraphic"
            in2="MAP_RESULT"
            scale="15"
            xChannelSelector="R"
            yChannelSelector="R"
            result="DISPLACED_TEXT_RESULT" />

<!-- The map asset is also being rendered in the background. -->
<!-- To view just the result of feDisplacementMap, remove <feMerge> below. -->
        <feMerge>
            <feMergeNode in="MAP_RESULT"></feMergeNode>
            <feMergeNode in="DISPLACED_TEXT_RESULT"></feMergeNode>
        </feMerge>
    </filter>

    <text
        x="40"
        y="140"
        font-size="108"
        filter="url(#myFilter)"
        fill="hsl(170, 100%, 25%)"
        font-weight="700">
        my text
    </text>
</svg>

The word 'error'

Loading Asset Problem

The <feImage> primitive is used to load the asset used as the map. When using this primitive, normally we would use the asset's URL as the value of the href attribute. For example: <feImage href="https://garden.bradwoods.io/images/towel.png" ... />.

Due to security concerns, if:

  • this asset is coming from another domain &
  • the <feImage> is being used as the map for <feDisplacementMap>, we have to use a different approach.

One solution is to use a blob (used in the example above):

  1. 1. fetch the asset data on the client,
  2. 2. turn it into a blob &
  3. 3. use this blob as the href value of <feImage>.

/index.js

const myFeImage = document.querySelector('feImage');
const url = feImage.getAttribute('href');

fetch(url)
    .then((response) =>  response.blob())
    .then((blob) => {
        const objURL = URL.createObjectURL(blob);

        myFeImage.setAttribute('href', objURL);
    });

Another solution is to use a data URL (used in examples below). This can be used when you have defined an SVG image in the DOM that you want to use as the map:

  1. 1. capture the <svg> element,
  2. 2. encode it,
  3. 3. capture your <feImage> element &
  4. 4. use the encoded URI as the href value of <feImage>.

/index.js

const uRIComponent = document.getElementById('myMapSvgAsset').outerHTML;
const encodedURI = encodeURIComponent(uRIComponent);
const myFeImage = document.querySelector('feImage');

// A URL encoded fragment has to be an SVG Element with a namespace attribute.
myFeImage.setAttribute(`href`, `data:image/svg+xml;charset=utf-8,${encodedURI}`);
2 cogs

How it Works

<feDisplacementMap> has the following attributes:

  • scale: defines the strength & direction of the distortion.
  • xChannelSelector: defines which of the map assets 4 color channels (red, green, blue, alpha) should be applied to distort the x-axis.
  • yChannelSelector: defines which of the map assets 4 color channels (red, green, blue, alpha) should be applied to distort the y-axis.

Irrelevant of which color channel is selected for an axis, the following applies:

  • A value of 127 will result in no change.
  • A value above 127 will move the pixel towards the scale value.
  • A value below 127 will move the pixel away from the scale value.

Imagine we set xChannelSelector to red & the yChannelSelector to blue. Then apply a map asset that covered the source with 1 color, rgba(127, 127, 127, 1). This would result in no pixels moving.

localhost:3000

Console

<p>Source:</p>
<svg viewBox="0 0 128 128">
    <image 
        href="https://garden.bradwoods.io/images/monaLisa.png" 
        x="0" 
        y="0" 
        height="128" 
        width="128" />
</svg>

<p>Map:</p>
<svg 
    id="myMap"
    viewBox="0 0 128 128" 
    xmlns="http://www.w3.org/2000/svg">
    <rect
        fill="rgba(127, 127, 127, 1)" 
        x="0"
        y="0"
        height="128"
        width="128" />
</svg>

<p>Filtered:</p>
<svg viewBox="0 0 128 128">
    <defs>
        <filter  
            id="myFilter"
            filterUnits="userSpaceOnUse"
            color-interpolation-filters="sRGB"
            width="128" 
            height="128" 
            x="0" 
            y="0">
<!-- href set in index.js using a data URL -->
            <feImage
                x="0"
                y="0"
                width="128"
                height="128"
                result="MAP_RESULT" />

            <feDisplacementMap
                in="SourceGraphic"
                in2="MAP_RESULT"
                scale="100"
                xChannelSelector="R"
                yChannelSelector="B" />
        </filter>
    </defs>
    <image 
        href="https://garden.bradwoods.io/images/monaLisa.png" 
        x="0" 
        y="0" 
        height="128px" 
        width="128px"
        filter="url(#myFilter)" />
</svg>

If you decrease the red channel's value to rgba(51, 127, 127, 1), the output will translate to the right. This is because:

  • the xChannelSelector is set to red,
  • the x-position of the image is 0 &
  • the scale is set to 100.

If you increase the blue channel's value to rgba(127, 127, 204, 1), the output will translate up. This is because:

  • the yChannelSelector is set to blue,
  • the y-position of the image is 0 &
  • the scale is set to 100.
A square filled with a red to blue gradient.

The Identity Map

The Identity Map is a special map that will scale an image equally in all directions. The color values gradually decline from a maximum at 1 edge to a minimum at the opposite edge.

Create an Identity Map

  1. 1. Create a layer & fill it with black,
  2. 2. create another layer and fill it with a left to right gradient from rgba(255, 0, 0, 1) to rgba(255, 0, 0, 0),
  3. 3. create another layer and fill with a top to bottom gradient from rgba(0, 0, 255, 1) to rgba(0, 0, 255, 0) &
  4. 4. set the blending mode of this last layer to screen.

localhost:3000

Console

<svg 
    width="128" 
    height="128" 
    preserveAspectRatio="xMidYMid meet" 
    viewBox="0 0 128 128">
    <defs>
        <style type="text/css">
            .redGradient {
                fill: url(#redGradient);
            }

            .blueGradient {
                fill: url(#blueGradient);
                mix-blend-mode: screen;
            }

            .redSymbol {
                display: block;
            }
        </style>

        <linearGradient 
            id="redGradient" 
            x1="0" 
            x2="1" 
            y1="0" 
            y2="0" 
            color-interpolation="sRGB" 
            gradientUnits="objectBoundingBox">
            <stop 
                offset="0%" 
                stop-color="#FF0000" />
            <stop 
                offset="100%" 
                stop-color="#FF0000" 
                stop-opacity="0" />
        </linearGradient>

        <linearGradient 
            id="blueGradient" 
            x1="0" 
            x2="0" 
            y1="0" 
            y2="1" 
            color-interpolation="sRGB" 
            gradientUnits="objectBoundingBox">
                <stop 
                    offset="0%" 
                    stop-color="#0000FF" />
                <stop 
                    offset="100%" 
                    stop-color="#0000FF" 
                    stop-opacity="0" />
        </linearGradient>

        <rect 
            class="redGradient" 
            id="redGradientRect" 
            width="128" 
            height="128" 
            x="0" 
            y="0" />
    </defs>

    <rect width="128" height="128" x="0" y="0" fill="black" />
    <use href="#redGradientRect" class="redSymbol" />
    <rect width="128" height="128" x="0" y="0" class="blueGradient" />
</svg>

Use an Identity Map

localhost:3000

Console

<p>Source:</p>
<svg viewBox="0 0 128 128">
    <image 
        href="https://garden.bradwoods.io/images/monaLisa.png" 
        x="0" 
        y="0" 
        height="128" 
        width="128" />
</svg>

<p>Map:</p>
<svg 
    id="myMap"
    width="128" 
    height="128" 
    preserveAspectRatio="xMidYMid meet" 
    viewBox="0 0 128 128"
    xmlns="http://www.w3.org/2000/svg">
    <defs>
        <style type="text/css">
            .redGradient {
                fill: url(#redGradient);
            }

            .blueGradient {
                fill: url(#blueGradient);
                mix-blend-mode: screen;
            }

            .redSymbol {
                display: block;
            }
        </style>

        <linearGradient 
            id="redGradient" 
            x1="0" 
            x2="1" 
            y1="0" 
            y2="0" 
            color-interpolation="sRGB" 
            gradientUnits="objectBoundingBox">
            <stop 
                offset="0%" 
                stop-color="#FF0000" />
            <stop 
                offset="100%" 
                stop-color="#FF0000" 
                stop-opacity="0" />
        </linearGradient>

        <linearGradient 
            id="blueGradient" 
            x1="0" 
            x2="0" 
            y1="0" 
            y2="1" 
            color-interpolation="sRGB" 
            gradientUnits="objectBoundingBox">
                <stop 
                    offset="0%" 
                    stop-color="#0000FF" />
                <stop 
                    offset="100%" 
                    stop-color="#0000FF" 
                    stop-opacity="0" />
        </linearGradient>

        <rect 
            class="redGradient" 
            id="redGradientRect" 
            width="128" 
            height="128" 
            x="0" 
            y="0" />
    </defs>

    <rect 
        fill="black"
        height="128" 
        width="128" 
        x="0" 
        y="0" />
    <use
        class="redSymbol" 
        href="#redGradientRect" />
    <rect 
        class="blueGradient" 
        height="128"
        width="128"
        x="0" 
        y="0" />
</svg>

<p>Filtered:</p>
<svg viewBox="0 0 128 128">
    <defs>
        <filter  
            id="myFilter"
            filterUnits="userSpaceOnUse"
            color-interpolation-filters="sRGB"
            width="128" 
            height="128" 
            x="0" 
            y="0">
<!-- href set in index.js using a data URL -->
            <feImage
                x="0"
                y="0"
                width="128"
                height="128"
                result="MAP_RESULT" />
            <feDisplacementMap
                in="SourceGraphic"
                in2="MAP_RESULT"
                scale="60"
                xChannelSelector="R"
                yChannelSelector="B" />
        </filter>
    </defs>
    <image 
        href="https://garden.bradwoods.io/images/monaLisa.png" 
        x="0" 
        y="0" 
        height="128px" 
        width="128px"
        filter="url(#myFilter)" />
</svg>

If you increase or decrease (negative numbers are valid) the scale value, the output will zoom in & out.

Examples

Sophisticated

localhost:3000

Console

<svg viewBox="0 0 400 260">
    <filter 
        id="conform" 
        x="-50%" 
        y="-50%" 
        width="200%" 
        height="200%">

        <feImage
            href="https://garden.bradwoods.io/images/towel.png"
            x="0"
            y="0"
            width="100%"
            height="100%"
            preserveAspectRatio="none"
            result="MAP_RESULT" />

<!-- desaturate the image -->
        <feColorMatrix
            in="MAP_RESULT"
            type="saturate"
            values="0"
            result="MAP_DESATURATE_RESULT" />

<!-- decrease level of details so the effect on text is more realistic -->
        <feGaussianBlur
            in="MAP_DESATURATE_RESULT"
            stdDeviation="0.25"
            result="MAP_BLUR_RESULT" />

<!-- use the displacement map to distort the text -->
        <feDisplacementMap
            in="SourceGraphic"
            in2="MAP_BLUR_RESULT"
            scale="15"
            xChannelSelector="R"
            yChannelSelector="R"
            result="DISPLACED_TEXT_RESULT" />

<!-- add the image as a background behind the text again -->
        <feImage
            href="https://garden.bradwoods.io/images/towel.png"
            x="0"
            y="0"
            width="100%"
            height="100%"
            preserveAspectRatio="none"
            result="BG" />

<!-- desaturate the text -->
        <feColorMatrix
            in="DISPLACED_TEXT_RESULT"
            result="TEXTURED_TEXT_2"
            type="matrix"
            values="1 0 0 0 0 
            0 1 0 0 0 
            0 0 1 0 0 
            0 0 0 .9 0" />

<!--  blend the text with the background image -->
        <feBlend
            in="BG"
            in2="TEXTURED_TEXT_2"
            mode="multiply"
            result="BLENDED_TEXT" />

<!-- layer the text on top of the background image -->
        <feMerge>
            <feMergeNode in="BG"></feMergeNode>
            <feMergeNode in="BLENDED_TEXT"></feMergeNode>
        </feMerge>
    </filter>

    <text
        x="40"
        y="140"
        font-size="108"
        filter="url(#conform)"
        fill="hsl(170, 100%, 25%)"
        font-weight="700">
        my text
    </text>
</svg>

Glass Lense

localhost:3000

Console

<p>Source:</p>
<svg viewBox="0 0 256 256">
    <image 
        href="https://garden.bradwoods.io/images/monaLisa.png" 
        x="0" 
        y="0" 
        height="256" 
        width="256" />
</svg>

<p>Map:</p>
<svg 
    id="myMap"
    width="256" 
    height="256" 
    preserveAspectRatio="xMidYMid meet" 
    viewBox="0 0 256 256"
    xmlns="http://www.w3.org/2000/svg">
    <defs>
        <style type="text/css">
            .blueGradient {
                mix-blend-mode: screen;
            }

            .redSymbol {
                display: block;
            }
        </style>

        <linearGradient 
            id="redGradient" 
            x1="0" 
            x2="1" 
            y1="0" 
            y2="0" 
            color-interpolation="sRGB" 
            gradientUnits="objectBoundingBox">
            <stop 
                offset="0%" 
                stop-color="#FF0000" />
            <stop 
                offset="100%" 
                stop-color="#FF0000" 
                stop-opacity="0" />
        </linearGradient>

        <linearGradient 
            id="blueGradient" 
            x1="0" 
            x2="0" 
            y1="0" 
            y2="1" 
            color-interpolation="sRGB" 
            gradientUnits="objectBoundingBox">
                <stop 
                    offset="0%" 
                    stop-color="#0000FF" />
                <stop 
                    offset="100%" 
                    stop-color="#0000FF" 
                    stop-opacity="0" />
        </linearGradient>

        <rect 
            class="redGradient"
            fill="url(#redGradient)"
            id="redGradientRect" 
            width="256" 
            height="256" 
            x="0" 
            y="0" />

<!-- This only allows distortion at the edges  -->
        <radialGradient id="grayGradient">
            <stop 
                offset="0%" 
                stop-color="rgba(127, 127, 127, 1)" />
<!-- Adjust this offset to change the distortion thickness (how much is revealed)  -->
            <stop 
                offset="75%" 
                stop-color="rgba(127, 127, 127, 1)" />
            <stop 
                offset="100%" 
                stop-color="rgba(127, 127, 127, 0)" />
        </radialGradient>

        <circle
            id="maskCircle"
            cx="128" 
            cy="128"
            r="100" />  

        <mask id="myMask">  
            <use 
                href="#maskCircle" 
                fill="white" />
        </mask> 
    </defs>

    <rect 
        fill="black"
        width="256" 
        height="256" 
        x="0" 
        y="0" />
    <use 
        class="redSymbol"
        href="#redGradientRect" />
    <rect 
        class="blueGradient"
        fill="url(#blueGradient)"
        width="256" 
        height="256" 
        x="0" 
        y="0" />
    <circle 
        fill="url(#grayGradient)"
        cx="128" 
        cy="128"
        r="100" />
</svg>

<p>Filtered:</p>
<svg id="filtered" viewBox="0 0 256 256">
    <defs>
        <filter  
            id="myFilter"
            filterUnits="userSpaceOnUse"
            color-interpolation-filters="sRGB"
            width="256" 
            height="256" 
            x="0" 
            y="0">

<!-- href set in index.js using a data URL -->
            <feImage
                x="0"
                y="0"
                width="256"
                height="256"
                result="MAP_RESULT" />

            <feDisplacementMap
                in="SourceGraphic"
                in2="MAP_RESULT"
                scale="50"
                xChannelSelector="R"
                yChannelSelector="B" />

        </filter>
    </defs>
    <image 
        href="https://garden.bradwoods.io/images/monaLisa.png" 
        x="0" 
        y="0" 
        height="246px" 
        width="246px"
        filter="url(#myFilter)"
        mask="url(#myMask)" />
    <use 
        href="#maskCircle" 
        stroke-width="1" 
        stroke="#1c1c1c" 
        fill="none" />
</svg>

Other

Where to Next?

A sci-fi robot taxi driver with no lower body