CSS 3D transforms are flattened on SVG elements

#41310059 Filed 2017-03-29 (crbug.com/706231) Status: New / P3

Summary

Blink flattens every transform on an SVG child element to a 2D affine matrix, so perspective(), rotateX()/rotateY() foreshortening and translateZ() have no 3D effect inside SVG, while the identical transform on an HTML element renders correctly.

Firefox already supports this (bugzilla 1478088); WebKit plans support with LBSE (webkit 242833).

Test cases

Each case renders the same green 100×100 square once as an SVG <rect> and once as an HTML <div>; the HTML panel is the reference. The Expected tab shows the captured HTML baseline, the Diff tab the pixel difference to it.

Case 1: rotateY(50deg) under parent perspective: 550px

<svg style="perspective: 550px">
  <rect style="transform: rotateY(50deg); transform-origin: 100px 100px" .../>
</svg>

SVG (this browser)

HTML reference (this browser)

checking...
Expected rendering (HTML reference baseline)
Pixel diff SVG vs expected

Case 2: perspective(550px) rotateY(50deg) in the transform list

This is the construction from the original 2017 report: the perspective() function at the front of the element's own transform stack.

<rect style="transform: perspective(550px) rotateY(50deg);
             transform-origin: 100px 100px" .../>

SVG (this browser)

HTML reference (this browser)

checking...
Expected rendering (HTML reference baseline)
Pixel diff SVG vs expected

Case 3: translateZ(-100px) under parent perspective: 300px

With perspective 300px and z = -100px the square should shrink to 300 / (300 + 100) = 75% (75×75). Flattened SVG stays 100×100.

<svg style="perspective: 300px">
  <rect style="transform: translateZ(-100px); transform-origin: 100px 100px" .../>
</svg>

SVG (this browser)

HTML reference (this browser)

checking...
Expected rendering (HTML reference baseline)
Pixel diff SVG vs expected

Expected vs actual

Expected The SVG panel matches the HTML panel in every case: perspective foreshortening on the rotated square (near edge taller than far edge) and the 75% scale for translateZ(-100px).

Actual (Chrome) The SVG square only shrinks horizontally by cos(50°) with parallel edges (cases 1 and 2) and does not scale at all for translateZ (case 3): the 3D components are dropped when the transform is flattened to 2D affine.

Root cause

// third_party/blink/renderer/core/layout/svg/transform_helper.cc
AffineTransform TransformHelper::ComputeTransform(...) {
  ...
  // Flatten any 3D transform.
  return AffineTransform::FromTransform(transform);
}

SVG children are painted through 2D AffineTransform paths, so the perspective row and z-axis components of the computed gfx::Transform are discarded before painting.

Fix status

Patch CL 8462268 implements this behind the experimental runtime flag SvgCss3DTransforms: the SVG child transform paint node gets the unflattened 3D matrix, the svg root's perspective applies to its children, and hit testing maps through the same paint-property-tree projection that rendering uses.

With --enable-blink-features=SvgCss3DTransforms all three cases render pixel-identical to the HTML reference and all hit-test probes match the 3D projection; without the flag all fail.

Links

CL 8462268 Issue 41310059 Firefox support bug WebKit LBSE bug