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:
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%). Note how the bottom right ellipse has lost its centered gradient because it is drawn in a rectangle that is not at the origin. |
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.
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).