Prevent subpixel rendering with svg

2.2k views Asked by At

I'm working with SVGs currently and came to a dead end.

The SVG has lines, which should scale together with zooming (so that they stay in balance: 100% width 10px --> 10% width 1px for example)

i scale all stroke-widths with this code:

var svgPath = this._svgContainer.find('svg [class*="style"]');
for (var i = 0; i < svgPath.length; ++i) {
  var newStrokeWidth = this._oldStrokeWidth[i] * (1 / (width / imgData.w));

  $(svgPath[i]).css(
    'stroke-width', newStrokeWidth
  );
}

Where width is the new width after zoom and imgData.w is the original unscaled width.

The problem with this is, if i zoom in to far. The stroke with becomes to small and leads to sub-pixel rendering. And supposedly black lines get grey-ish.

My Idea was to clip the value at a certain point to prevent it. But as far as I know, I have to consider the Device Pixel ratio too, because of different screens (desktop, mobile, 4K)

Would be nice If someone can help me with an idea to fix my problem

2

There are 2 answers

0
breezertwo On BEST ANSWER

We finally found a solution for this, in case anyone has the same problems:

1) Because of the panning of this._$svgElement and the calculation of vpx in a completely different section of the code the element is 'between' pixels. ( 100.88945px for x for example). This causes lines to blur. I fixed this part with a simple Math.round().

this._hammerCanvas.on('panmove', (event: any) => {
        const translate3d = 'translate3d(' + Math.round(this._oldDeltaX + ((vpx === imgData.x) ? 0 : vpx) + event.deltaX) + 'px, ' + Math.round(this._oldDeltaY + ((vpy === imgData.y) ? 0 : vpy) + event.deltaY) + 'px, 0)';
        this._$svgElement.css({
          transform: translate3d
        });
}

2) To fix the problem between the SVG viewport and the line strength, I had to implement a method to calculate the strokewidth equal to 1 'real' pixel regarding the svgs dimension.

the updated code looks like this: (This is the inital code, after the SVG was loaded from the server. Inside the zooming, the old code from above is still the same)

    const pixelRatio = devicePixelRatio || 1;
    const widthRatio = this._initSVGWidth / svgContainerWidth;
    const heightRatio = this._initSVGHeight / svgContainerHeight;
    this._svgZoomFactor = Math.max(widthRatio, heightRatio);
    const strokeWidth1px = this.computeStrokeWidth1px(widthRatio, heightRatio);

    for (let i = 0; i < svgPaths.length; ++i) {
      this._initalStrokeWidth[i] = parseFloat($(svgPaths[i]).css('stroke-width'));

      const newStrokeWidth = Math.max(strokeWidth1px / pixelRatio, this._svgZoomFactor * this._initalStrokeWidth[i]);

      $(svgPaths[i])[0].setAttribute('style', 'stroke-width:' + newStrokeWidth);
      this._oldStrokeWidth[i] = newStrokeWidth;
    }

and the compute:

  protected computeStrokeWidth1px (widthRatio: number, heightRatio: number): number {
    const viewBox = this._$svgElement[0].getAttribute('viewBox').split(' ');
    const viewBoxWidthRatio = parseFloat(viewBox[2]) / this._$svgElement.width();
    const viewBoxHeightRatio = parseFloat(viewBox[3]) / this._$svgElement.height();
    return widthRatio > heightRatio ? viewBoxWidthRatio : viewBoxHeightRatio;
  }
1
Zdravek Sprite On
var newStrokeWidth = this._oldStrokeWidth[i] * (1 / (width / imgData.w));
newStrokeWidth = (newStrokeWidth < 1) ? 1 : newStrokeWidth;

newStrokeWidth will always be 1 or greater