cssreactjssvgrecharts

Is there a way to prevent rasterization of SVG images while changing their orientation via CSS's "transform: rotate()"?


I'm trying to build a weather app with React. I want to achieve a slot machine effect while scrolling weather cards. Most of those cards have charts on them. To draw charts I'm using the "recharts" library, which generates SVG charts. To achive slot machine effect I'm using primarily transform: rotateX() depending on how far the card is from the center of a scrollable area. The problem is that when the page loads and transform is not applied, charts, text and SVGs look nice and "crisp", but when I start to scroll, there is an immediate change in sharpness, everything gets a little blurry, sometimes more, sometimes less, it's weird. I've read that it's most likely due to rasterization. I've googled a bit and haven't found a suitable solution for my case. I've found that "Charts.js" library wouldn't have such problems, because it uses Canvas instead of SVG, but I would like to stick to recharts here. Can I avoid this effect without switching to Canvas?

Problem occurs on desktop. On mobile devices everything seems to be okay. Here is the link to codesandbox that shows how it looks like:

https://codesandbox.io/p/sandbox/recharts-animation-vmdkkv

I've tried to handle this with CSS, by adding these to container. Didn't help

shape-rendering: geometricPrecision;
text-rendering: geometricPrecision;
image-rendering: optimizeQuality;
backface-visibility: hidden;

EDIT: Solution described in the right answer to this post worked in my case with will-change: transform added to the closest parent container (sandbox now is changed accordingly). This noticeably hurts performance on the first render though, but it works


Solution

  • Apperently, most browsers apply the 3D transforms to the first 2D rendering result.

    In other words: there is no true 3D-vector (or text) rasterization (calculating e.g vector edges in 3D space before rasterizing).

    The result are noticible anti-aliasing artifacts or blurry edges.

    A hacky workaround could be to

    body {
      padding: 1em;
    }
    
    .container {
      overflow: hidden;
      perspective: 100px;
      transform-style: preserve-3d;
      perspective-origin: 250px 250px;
      margin-bottom: 1em;
      transform-origin: center;
      outline: 1px solid red;
      width: 500px;
      height: 500px;
    
    
    }
    
    svg {
      display: block;
      outline: 1px solid #ccc;
      overflow: hidden;
      width: 500px;
      background: #fff;
    }
    <h3>No pre-scaling</h3>
    <div class="container">
      <div class="wrap" style="transform: rotateX(30deg) translateY(0px) translateZ(0px)">
        <svg viewBox="0 30 90 90">
          <path d="M 30 0 h30 v30 h30 v30 h-30 v30 h-30 v-30 h-30 v-30 h30z" />
          <text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="#fff">Text</text>
        </svg>
      </div>
    
    </div>
    
    <h3>Scaled before</h3>
    <div class="container" style="perspective:200px;">
      <div class="wrap" style="transform:scale(0.5) rotateX(30deg) translateZ(0px)">
        <svg viewBox="0 30 90 90" style="transform:scale(2); transform-origin: center;">
          <path d="M 30 0 h30 v30 h30 v30 h-30 v30 h-30 v-30 h-30 v-30 h30z" />
          <text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="#fff">Text</text>
        </svg>
      </div>
    </div>

    This will force the renderer to draw a transformed image based on a rasterized SVG with a higher resolution.
    Unfortunately, this may also introduce jagged edges for texts and diagonal lines.

    This blur effect is less noticeable on mobile devices as these usually have a higher pixel density. The same applies to 4K+ displays.