2

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>

3
  • It might also be possible to achieve the same effect by using a pattern, as described here: stackoverflow.com/questions/56822537/… The pattern would have to take the input image into account, which I do not know how to do. But that approach might work... Commented Apr 22, 2023 at 12:00
  • There are a couple of half-tone techniques that people use. This is one I did using a true half-tone technique: codepen.io/mullany/pen/AoKQoz It used to look pretty good and then Chrome introduced bug #1314516 so it doesn't look so great anymore. If I was to try this again I would use use feTurbulence to generate noise and then process it several times using different feColorMatrix's to generate screens for different luminance levels. Commented Apr 22, 2023 at 21:22
  • 1
    Yeah, so the noise approach looks cool, but it's not a half-tone. codepen.io/mullany/pen/MWPJVLr?editors=1000 Commented Apr 22, 2023 at 22:30

1 Answer 1

2

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.

2
  • Thanks @ccprog - lots of good information! For some reason, when I run your example locally, feColorMatrix produce a black image. Oh my page background was black and the matrix sets white as transparent. The CSS solution you linked to does look appealing in it's simplicity. While this image is an one off, it would be nice to have a reusable approach. I will play around with both and see Commented Apr 24, 2023 at 12:06
  • 1
    On a black background, 1. draw the luminanceToAlpha with white instead of black: set the fifth column in the first three lines to 1, and 2. inverse the alpha channel computation: positive values for the first three values of the last line, and the last value to zero.
    – ccprog
    Commented Apr 24, 2023 at 12:35

Not the answer you're looking for? Browse other questions tagged or ask your own question.