Search code examples
htmlsvgsvg-filters

Why isn't this feDisplacementMap filter working?


I'm trying to build an image within an SVG <defs> element for use in the in2 attribute of a displacement map filter. However, the displacement map filter isn't warping the element it's applied to.

The displacement map filter is being applied to a grid pattern so that I can see how the displacement works:

<svg viewBox="0 0 400 400" width="400" height="400">
  <defs>
    <pattern id="grid" viewBox="0,0,10,10" width="2.5%" height="2.5%">
      <rect x="0" y="0" width="1" height="10"/>
      <rect x="0" y="0" width="10" height="1"/>
    </pattern>
  </defs>
  <rect x="0" y="0" width="400" height="400" fill="url(#grid)"/>
</svg>

The image I'm intending to use as the in2 attribute of the displacement map is complex. When I just reference it via an <feImage> element within the filter, and apply that filter to the grid pattern, I see the image I've built, as expected:

<svg viewBox="0 0 400 400" width="400" height="400">
  <defs>
    <linearGradient id="gradientRed">
      <stop offset="0%" stop-color="rgba(255,0,0,0)"/>
      <stop offset="100%" stop-color="rgba(255,0,0,1)"/>
    </linearGradient>
    <linearGradient id="gradientGreen" gradientTransform="rotate(90)">
      <stop offset="0%" stop-color="rgba(0,255,0,0)"/>
      <stop offset="100%" stop-color="rgba(0,255,0,1)"/>
    </linearGradient>
    <rect id="rectRed" width="400" height="400" fill="url(#gradientRed)"/>
    <rect id="rectGreen" width="400" height="400" fill="url(#gradientGreen)"/>
    <rect id="rectBlack" width="400" height="400" fill="black"/>
    <filter id="filterIdentity">
      <feImage result="imageRed" href="#rectRed" x="0" y="0" width="400" height="400"/>
      <feImage result="imageGreen" href="#rectGreen" x="0" y="0" width="400" height="400"/>
      <feImage result="imageBlack" href="#rectBlack" x="0" y="0" width="400" height="400"/>
      <feBlend mode="screen" in="imageRed" in2="imageGreen" result="imageRedGreen"/>
      <feBlend mode="screen" in="imageRedGreen" in2="imageBlack"/>
    </filter>
    <mask id="maskCircle">
      <rect width="400" height="400" fill="black"/>
      <circle cx="200" cy="200" r="200" fill="white"/>
    </mask>
    <g id="shapeIdentity">
      <rect width="400" height="400" fill="black"/>
      <rect width="400" height="400" filter="url(#filterIdentity)" mask="url(#maskCircle)"/>
    </g>
    <filter id="filterDisplace">
      <feImage result="imageDisplace" href="#shapeIdentity" x="0" y="0" width="400" height="400"/>
    </filter>
    <pattern id="grid" viewBox="0,0,10,10" width="2.5%" height="2.5%">
      <rect x="0" y="0" width="1" height="10"/>
      <rect x="0" y="0" width="10" height="1"/>
    </pattern>
  </defs>
  <rect x="0" y="0" width="400" height="400" fill="url(#grid)" filter="url(#filterDisplace)"/>
</svg>

However when I use that same <feImage> element as the in2 attribute of a <feDisplacementMap> element, the filter does not warp the grid pattern it's applied to.

<svg viewBox="0 0 400 400" width="400" height="400">
  <defs>
    <linearGradient id="gradientRed">
      <stop offset="0%" stop-color="rgba(255,0,0,0)"/>
      <stop offset="100%" stop-color="rgba(255,0,0,1)"/>
    </linearGradient>
    <linearGradient id="gradientGreen" gradientTransform="rotate(90)">
      <stop offset="0%" stop-color="rgba(0,255,0,0)"/>
      <stop offset="100%" stop-color="rgba(0,255,0,1)"/>
    </linearGradient>
    <rect id="rectRed" width="400" height="400" fill="url(#gradientRed)"/>
    <rect id="rectGreen" width="400" height="400" fill="url(#gradientGreen)"/>
    <rect id="rectBlack" width="400" height="400" fill="black"/>
    <filter id="filterIdentity">
      <feImage result="imageRed" href="#rectRed" x="0" y="0" width="400" height="400"/>
      <feImage result="imageGreen" href="#rectGreen" x="0" y="0" width="400" height="400"/>
      <feImage result="imageBlack" href="#rectBlack" x="0" y="0" width="400" height="400"/>
      <feBlend mode="screen" in="imageRed" in2="imageGreen" result="imageRedGreen"/>
      <feBlend mode="screen" in="imageRedGreen" in2="imageBlack"/>
    </filter>
    <mask id="maskCircle">
      <rect width="400" height="400" fill="black"/>
      <circle cx="200" cy="200" r="200" fill="white"/>
    </mask>
    <g id="shapeIdentity">
      <rect width="400" height="400" fill="black"/>
      <rect width="400" height="400" filter="url(#filterIdentity)" mask="url(#maskCircle)"/>
    </g>
    <filter id="filterDisplace">
      <feImage result="imageDisplace" href="#shapeIdentity" x="0" y="0" width="400" height="400"/>
      <feDisplacementMap in="SourceGraphic" in2="imageDisplace" scale="20" yChannelSelector="R" xChannelSelector="G"/>
    </filter>
    <pattern id="grid" viewBox="0,0,10,10" width="2.5%" height="2.5%">
      <rect x="0" y="0" width="1" height="10"/>
      <rect x="0" y="0" width="10" height="1"/>
    </pattern>
  </defs>
  <rect x="0" y="0" width="400" height="400" fill="url(#grid)" filter="url(#filterDisplace)"/>
</svg>

What's going on here?


Solution

  • Well you're tripping over a bug in Chrome. Here is a version that works in Chrome by inlining your displacementMap image as a separate SVG fragment within a data URI fed to an feImage. Please note that the syntax here is incorrect - but correct syntax {xlink:href = url(...)} does not work. (Note - this doesn't work in Firefox because Firefox doesn't accept fragment references for feImage.)

    <svg viewBox="0 0 400 400" width="400" height="400">
      <defs>
        <filter id="filterDisplace">
          <feImage result="imageDisplace" xlink:href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400' width='400' height='400'%3E%3Cdefs%3E%3ClinearGradient id='gradientRed'%3E%3Cstop offset='0%25' stop-color='rgba(255,0,0,0)'/%3E%3Cstop offset='100%25' stop-color='rgba(255,0,0,1)'/%3E%3C/linearGradient%3E%3ClinearGradient id='gradientGreen' gradientTransform='rotate(90)'%3E%3Cstop offset='0%25' stop-color='rgba(0,255,0,0)'/%3E%3Cstop offset='100%25' stop-color='rgba(0,255,0,1)'/%3E%3C/linearGradient%3E%3Crect id='rectRed' width='400' height='400' fill='url(%23gradientRed)'/%3E%3Crect id='rectGreen' width='400' height='400' fill='url(%23gradientGreen)'/%3E%3Crect id='rectBlack' width='400' height='400' fill='black'/%3E%3Cfilter id='filterIdentity'%3E%3CfeImage result='imageRed' href='%23rectRed' x='0' y='0' width='400' height='400'/%3E%3CfeImage result='imageGreen' href='%23rectGreen' x='0' y='0' width='400' height='400'/%3E%3CfeImage result='imageBlack' href='%23rectBlack' x='0' y='0' width='400' height='400'/%3E%3CfeBlend mode='screen' in='imageRed' in2='imageGreen' result='imageRedGreen'/%3E%3CfeBlend mode='screen' in='imageRedGreen' in2='imageBlack'/%3E%3C/filter%3E%3Cmask id='maskCircle'%3E%3Crect width='400' height='400' fill='black'/%3E%3Ccircle cx='200' cy='200' r='200' fill='white'/%3E%3C/mask%3E%3C/defs%3E%3Crect width='400' height='400' fill='black'/%3E%3Crect width='400' height='400' filter='url(%23filterIdentity)' mask='url(%23maskCircle)'/%3E%3C/svg%3E" x="0" y="0" width="400" height="400"/>
          <feDisplacementMap in="SourceGraphic" in2="imageDisplace" scale="20" yChannelSelector="R" xChannelSelector="G"/>
        </filter>
        <pattern id="grid" viewBox="0,0,10,10" width="2.5%" height="2.5%">
          <rect x="0" y="0" width="1" height="10"/>
          <rect x="0" y="0" width="10" height="1"/>
        </pattern>
      </defs>
      <g filter="url(#filterDisplace)">
        <rect x="0" y="0" width="400" height="400" fill="url(#grid)" />
      </g>
    </svg>