The Problem

(Update 2015-06-14: Note that the article below has been updated to set Radial Gradient Center Point and not GradientOriginOffset (or focal point) . Setting GradientOriginOffset can skew the gradient and lead to incorrect results).

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 anyone who uses a radial gradient and the center point/focal point to not be in the exact middle of the shape being rendered. For the RiverSoftAVG SVG Component Library, SVG tend to define these types of gradients all the time so that is how I became aware of the regression. 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:

In the screenshots above, we set up the top Ellipse’s gradient brushes 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 small 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.

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 center 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.

To translate the gradient center 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:

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. (Note XE8 is giving us problems deploying to iOS so we could not test its behavior)

What does this mean for the RiverSoftAVG SVG Component Library?

First, because there is no easy way to set the gradient center point or 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 (for Everyone)

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:

Copy FMX.Canvas.D2D.pas from Delphi’s source\fmx directory to your project’s directory

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(0, 0)); // assume RotationCenter in range 0-1, translate gradient center by rectangle.TopLeft RadialGradBrushProp.Center := TD2D1Point2F(TPointF.Create(AGradient.RadialTransform.RotationCenter.X * ARect.Width, AGradient.RadialTransform.RotationCenter.Y * ARect.Height) + 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;

Alternatively, you can download the changed file. 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).

Well, that’s it for now. I hope this helps others. Happy CodeSmithing!