Hide Comments
Hide Comments

XE8 Notes on Radial Gradients

Comments (0)

The Problem

 

In Delphi XE8, Embarcadero broke (or perhaps more correctly, broke further) how radial gradients are rendered on Windows using their FMX.Canvas.D2D.pas file.  This issue affects all SVGs that use a radial gradient that has a gradient focal point that is not exactly in the middle of the shape to render.  Prior to XE8, the FMX.Canvas.D2D unit created a radial gradient brush whose center was modified by the Brush.Gradient.RadialTransform.RotationCenter.  This allowed the SVG library to correctly render radial gradients that had their center or focal point not equal to 50%, 50% (i.e., the center).  The screenshots below show the old pre-XE8 behavior:

 

Radial Gradient (Centered Origin 50%, 50%)
Radial Gradient (Centered Origin 50%, 50%)
Radial Gradient (Origin Down and Right 75%, 75%)
Radial Gradient (Origin Down and Right 75%, 75%)

 

In the screenshots above, we set up the top Ellipse's gradient brush using RotationCenter and copied the brush to the canvas to draw 2 additional ellipses:

 

procedure TForm24.PaintBox1Paint(Sender: TObject; Canvas: TCanvas);
var
  aSize: Single;
begin
  aSize := PaintBox1.Height*0.33;
  Canvas.Fill := Ellipse1.Fill;
  // draw upper left
  Canvas.FillEllipse(RectF(0,0,aSize,aSize), 1);
  // draw bottom right
  Canvas.FillEllipse(RectF(PaintBox1.Width-aSize,PaintBox1.Height-aSize,PaintBox1.Width,PaintBox1.Height), 1);
end;

 

The code from XE7 and before in the FMX.Canvas.D2D.pas looked like this:

 

      rgradbrushprop.GradientOriginOffset := TD2D1Point2F(Point(0, 0));
      rgradbrushprop.Center := TD2D1Point2F(
        PointF(AGradient.RadialTransform.RotationCenter.X * RectWidth(ARect),
        AGradient.RadialTransform.RotationCenter.y * RectHeight(ARect)) + ARect.TopLeft);
      rgradbrushprop.RadiusX := RectWidth(ARect) / 2;
      rgradbrushprop.RadiusY := RectHeight(ARect) / 2;
      FTarget.CreateRadialGradientBrush(rgradbrushprop, nil, gradcol, ID2D1RadialGradientBrush(Result));

 

If you notice, Embarcadero set the center of the gradient to the center point of the rectangle (usually RotationCenter.Point := PointF(0.5, 0.5) and translated the center point by the rectangle being drawn (by adding ARect.TopLeft).  There were quite a few problems with this code:

 

First is a cosmetic problem.  Why was Embarcadero using RadialTransform.RotationCenter? Rotation Center seems a poor naming choice for the center of the gradient.  Probably, the RadialTransform.Position would have been a better choice.
Why were they setting the Center of the gradient and not the GradientOriginOffset?
There is no means to set the radius of the gradient (and that is why the SVG library cannot set the radius of a gradient)

 

However, there were a couple of things sorta right about the code:

 

You could set the gradient focal point
The center of the gradient was specified by using a value from 0 to 1 and was translated by where the rectangle was, meaning that it scaled to the rectangle it was being drawn in.  If you changed the rectangle size or location, the gradient would be drawn correctly.

 

In XE8, Embarcadero changed FMX.Canvas.D2D.pas to closely mirror their FMX.Canvas.GDIP.pas:

 

      RadialGradBrushProp.GradientOriginOffset := TD2D1Point2F(TPointF.Create(0, 0));
      RadialGradBrushProp.Center := TD2D1Point2F(TPointF.Create(ARect.Width * 0.5, ARect.Height * 0.5));
      RadialGradBrushProp.RadiusX := ARect.Width / 2;
      RadialGradBrushProp.RadiusY := ARect.Height / 2;
      FTarget.CreateRadialGradientBrush(RadialGradBrushProp, nil, GradCol, ID2D1RadialGradientBrush(Result));
      UpdateBrushMatrix(Result, AGradient.RadialTransform.Matrix);

 

All modifications to the radial gradient now occur in the UpdateBrushMatrix which applies the AGradient.RadialTransform's transformation matrix to the brush's matrix.  In some ways, this could be seen as an improvement as theoretically, you can modify the radial gradient how ever you like by using the RadialTransform.Matrix.  However, there are some BIG problems with this approach:

 

You cannot set TTransformation.Matrix property directly.  It is read-only.  The TTransformation.Matrix property is generated by the class when you modify the Position, Scale, and Skew properties.  But the Skew property is protected.  You can hack the class to get to the Skew property but it is certainly not straightforward.
Even worse, you need to set the transformation matrix using absolute values, not proportional values between 0 and 1.  To translate the gradient focal point, you need to know the rectangle it will be drawn in the future to create the gradient brush.  And because the gradient center was not offset by the ARect.TopLeft, you have to take that into account too.
Even worse than that, since you must use absolute values for the gradient, that gradient can only be used correctly with one rectangle or object.  In addition, even if the gradient on a TEllipse or other shape is set correctly, if that TEllipse is moved or resized, the gradient is wrong.  You cannot share that gradient with another rectangle.

 

The screenshots below show the problems with the new, XE8 behavior:

 

Radial Gradient in XE8 (Centered Origin 50%, 50%)
Radial Gradient in XE8 (Centered Origin 50%, 50%).  Note how the bottom right ellipse has lost its centered gradient because it is drawn in a rectangle that is not at the origin.
Radial Gradient in XE8 (Origin Down Right 75%, 75%)
Radial Gradient in XE8 (Origin Down Right 75%, 75%).  By clever manipulation of the transformation matrix, we set the focal point to (114,94), which works for the top Ellipse.  However, the same gradient does not work for either of the other ellipses since their rectangles are different.

 

Note that the FMX GDI+ never worked, but since that canvas was used rarely, this issue was ignored by the RSCL.

 

Interestingly, Embarcadero only made this change on Windows.  Testing on OSX and Android reveals that the previous behavior, using RotationCenter, is still in effect.

 

What does this mean for the RiverSoftAVG SVG Component Library?

 

First, because there is no easy way to set the gradient focal point correctly and even if we did, the behavior would break as soon as a SVG element was moved or resized, we are not going to change the code for how radial gradient's are set at this time.  Using the default Embarcadero XE8 DirectX2D code on Windows for radial gradient with a non-centered focal point, the gradient will be displayed incorrectly.  On other platforms and earlier versions of Delphi, the radial gradient will work as well as it ever did.

 

The Solution

 

However, you can get back proper rendering of Radial Gradients on Windows for your compiled applications by hacking the FMX.Canvas.D2D.pas file.  Perform the following steps:

 

1.Copy FMX.Canvas.D2D.pas from Delphi's source\fmx directory to your project's directory
2.Modify the TCanvasD2D.CreateD2DGradientBrush method by replacing the entire "begin { Radial }" with

 

    begin
      { Radial }
      for I := 0 to AGradient.Points.Count + Count - 1 do
        Grad[I].Position := 1 - Grad[I].Position;
      FTarget.CreateGradientStopCollection(@Grad[0], AGradient.Points.Count + Count, D2D1_GAMMA_2_2,
        D2D1_EXTEND_MODE_CLAMP, GradCol);
      // assume RotationCenter in range 0-1, modify the gradient origin offset
      RadialGradBrushProp.GradientOriginOffset := TD2D1Point2F(
        TPointF.Create((AGradient.RadialTransform.RotationCenter.X-0.5)*ARect.Width, (AGradient.RadialTransform.RotationCenter.Y-0.5)*ARect.Height)
        );
      // translate gradient center by rectangle.TopLeft
      RadialGradBrushProp.Center := TD2D1Point2F(TPointF.Create(ARect.Width * 0.5, ARect.Height * 0.5)+ARect.TopLeft);
      // bonus points, assume scale contains the percent of the radius to display
      // i.e., usually r=1 for the whole rectangle
      RadialGradBrushProp.RadiusX := AGradient.RadialTransform.Scale.X*(ARect.Width / 2);
      RadialGradBrushProp.RadiusY := AGradient.RadialTransform.Scale.Y*(ARect.Height / 2);
      FTarget.CreateRadialGradientBrush(RadialGradBrushProp, nil, GradCol, ID2D1RadialGradientBrush(Result));
//      UpdateBrushMatrix(Result, M);
      GradCol := nil;
    end;

 

As a bonus, the above code uses the AGradient.RadialTransform.Scale property as a proxy for setting the radius of the gradient (the RSCL has set the scale based on a SVG element's radius since February 2015 but it is unused without the above hack for each platform and version of Delphi you are using).

 

 

 

Comments (0)

RiverSoftAVG SVG Component Library (RSCL) © 2013-2015, Thomas G. Grubb