svgsvg-filterssvg.jsimage-effects

Halftone image effect with SVG filters


I am trying to translate specifications from Photoshop to svg filters.

The recipe is:

  1. Filter > Pixelate > Color Halftone
  2. All 4 channels 45 degrees
  3. Radius of the dots between 5-8

The process, in Photoshop, is illustrated here: http://www.photoshopsupport.com/tutorials/cb/halftone.html

I got the before- and after-image (only the after-image, looks horrible when scaled):

  1. Before: https://firefund-assets.s3.amazonaws.com/hero-200/hero-img-1920x660.jpg
  2. After: https://firefund-assets.s3.amazonaws.com/hero-200/hero-200-1920x660.jpg

I have found a complete list of SVG filters and I have found several recipes on SO that explains steps to create a halftone image effect but not with SVG filters.

Single-level halftone: Input: Pixels from your image; preconstructed "screen" containing threshold values. At runtime: For each color channel, for each pixel, select one threshold value (index into threshold array modulo the array dimensions). One comparison between the pixel and the threshold determines whether the output value is on or off

feConvolveMatrix seems to be able to set a threshold to turn off/on pixels but I don't understand the MDN documentation. It might even be the wrong filter to use.

I have also found this Portuguese recipe1 that looks like it could be translated to feConvolveMatrix but because I am not sure how to use feConvolveMatrix and kernelMatrix, then I can not.

My initially attempt has failed, as seen below.

.hero-200__img {
    filter: url(#halftone);
}
<img class="hero-200__img" alt="Hero" width="1920" height="660" src="https://firefund-assets.s3.amazonaws.com/hero-200/hero-img-1920x660.jpg">

    <!--
    1. Color Halftone
    2. All 4 channels 45degrees
    3. Radius of the dots between 5-8
    -->
<svg viewBox="0 0 100 100" width="1920" height="660">
    <filter id="halftone">
        <!-- black/white -->
        <feColorMatrix in="SourceGraphic" type="saturate" values="0"/>
        <!-- 45deg -->
        <feConvolveMatrix kernelMatrix="0.125 0 0
                                        0 0.125 0
                                        0 0 0.125"/>
        <!-- dots -->
        <feMorphology operator="dilate" radius="5"/>
    </filter>
</svg>

The input image (SourceGraphic) is already black/white but I've added it here in case the input image will change.

I'm happy to pull in svg.js, especially svg.filter.js, in case I need to do some of these calculations in JS.

1: English translation: https://www-inf-pucrs-br.translate.goog/~pinho/CG/Aulas/Img/IMG.htm?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp

UPDATE

Doing step 2 might be better with feComponentTransfer. Below is my experiment with adjusting the tableValues for all channels by 20%.

.hero-200__img {
    filter: url(#halftone);
}
<img class="hero-200__img" width="1920" height="660" src="https://firefund-assets.s3.amazonaws.com/hero-200/hero-img-1920x660.jpg">

<svg viewBox="0 0 1000 1000" width="1920" height="660">
    <filter id="halftone">
        <!-- black/white -->
        <feColorMatrix in="SourceGraphic" type="saturate" values="0"/>

        <!-- 45deg  -->
        <feComponentTransfer>
            <feFuncR type="discrete" tableValues="0 0.2 0.4 0.6 0.8 1"/>
            <feFuncG type="discrete" tableValues="0 0.2 0.4 0.6 0.8 1"/>
            <feFuncB type="discrete" tableValues="0 0.2 0.4 0.6 0.8 1"/>
            <feFuncA type="discrete" tableValues="0 0.2 0.4 0.6 0.8 1"/>
        </feComponentTransfer>

        <!-- dots -->
        <feMorphology operator="dilate" radius="2"/>
    </filter>
</svg>


Solution

  • feConvolveMatrix does not help with the main problem: somehow, you need to have a dotted pattern in the background. A convolve matrix computes every pixel in the same way, but does not divide them up into "dots" and "gaps".

    I actually remember the way you would produce these sort of images for photochemical offset printing: you stacked a transparent foil with a dot pattern with the photo you wanted to use, and photographed that with high exposure and the focus a bit off. Or simply placed the stack in a photocopier, which also would transpose grey values to different-sized dots.

    Here is what I would do: draw the dots as a SVG pattern yourself. It has a major drawback, though. You need to compose two pictures, the one you want to show, and the dot pattern. <filter> implementations are pretty unreliable when importing vector elements with <feImage>, so it is generally prefered to start out with the vector image and add the pixel grafic as an extra source inside the filter definition. In other words: every picture shown this way has its own filter definition, you cannot reuse them.

    The main values to play around with are the amount of blurring for the dot (stdDeviation, it needs to be adjusted to fit the dot size/circle radius and distance) and the cutoff point for the alpha value (intercept, the higher the value, the "darker" the result).

    <svg width="1200" height="500">
      <!--the dot pattern-->
      <pattern id="dots" patternUnits="userSpaceOnUse" width="8" height="8">
        <circle r="2" cx="0" cy="4" />
        <circle r="2" cx="8" cy="4" />
        <circle r="2" cx="4" cy="0" />
        <circle r="2" cx="4" cy="8" />
      </pattern>
      <filter id="halftone" filterUnits="userSpaceOnUse">
        <!--import the image-->
        <feImage href="https://firefund-assets.s3.amazonaws.com/hero-200/hero-img-1920x660.jpg"
                 x="0" y="0" width="100%" height="100%" result="photo" />
        <!--luminanceToAlpha, but inversed, so that darker parts get higher opacity-->
        <feColorMatrix type="matrix"
               values="0 0 0 0 0 
                       0 0 0 0 0 
                       0 0 0 0 0 
                       -0.2125 -0.7154 -0.0721 0 1"/>
        <!--compose with the dot pattern-->
        <feComposite operator="in" in2="SourceGraphic" />
        <!--the following is the "blob effect": first, make blurred borders-->
        <feGaussianBlur stdDeviation="1" />
        <!--then, produce a "sharp" border for a constant alpha value: the
            darker the blurred dot, the larger the blob-->
        <feComponentTransfer>
          <feFuncA type="linear" slope="18" intercept="-5"/>
        </feComponentTransfer>
        <!--crop away everything outside the limits of the photo-->
        <feComposite operator="in" in2="photo" />
      </filter>
      <!--an area where the picture will be shown, initially dotted, then the
          photo is mixed in with the filter-->
      <rect width="100%" height="100%" fill="url(#dots)" filter="url(#halftone)" />
    </svg>

    If you think it is unacceptable not to have a reusable filter, the best way to go about it is to draw the dots with CSS (radial-gradient) and use CSS filter functions, like described as part of this article.