The help topic below has been updated with the solution for allowing the RSCL to set the gradient center point, focal point, and radius and applies to XE8 and up.
In Delphi XE8 and beyond, 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.
•There is no way to set the Gradient Origin Offset at all
•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 center 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:
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);
// RadialGradBrushProp.GradientOriginOffset := TD2D1Point2F(TPointF.Create(0, 0));
// assume RotationCenter in range 0-1, translate gradient center by rectangle.TopLeft
aPoint := TPointF.Create(ARect.Width, ARect.Height);
GradCenter := (AGradient.RadialTransform.RotationCenter.Point * aPoint) + ARect.TopLeft;
RadialGradBrushProp.Center := TD2D1Point2F(GradCenter);
// gradient offset will use position to tell how far from center it should deviate
// since Position hasn't been used and is usually (0,0), move it back to 0.5,0.5
GradOffset := (TPointF.Create(0.5, 0.5) - AGradient.RadialTransform.Position.Point) *
aPoint + ARect.TopLeft;
GradOffset := ARect.CenterPoint - GradOffset;
RadialGradBrushProp.GradientOriginOffset := TD2D1Point2F(GradOffset);
// 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);
// 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);
GradCol := nil;
end;
As a bonus, the above code allows the RSCL to use the full power of radial gradients: set center point, set focal point, and set radius. The code uses the AGradient.RadialTransform.Scale property as a proxy for setting the radius of the gradient (as a value from 0 (no radius) to 0.5 (full radius of the arect) and up. The AGradient.RadialTransform.Position property is used as a proxy for setting the gradient origin offset ((0,0) means focal point is right on gradient center point. Setting gradient from -0.5 to 0.5 skews the gradient and moves the focal point).