The Skew Transform
See how the skew transform can create tilted graphical objects in SkiaSharp
In SkiaSharp, the skew transform tilts graphical objects, such as the shadow in this image:
The skew turns a rectangle into a parallelogram, but a skewed ellipse is still an ellipse.
Although Xamarin.Forms defines properties for translation, scaling, and rotations, there is no corresponding property in Xamarin.Forms for skew.
The Skew
method of SKCanvas
accepts two arguments for horizontal skew and vertical skew:
public void Skew (Single xSkew, Single ySkew)
A second Skew
method combines those arguments in a single SKPoint
value:
public void Skew (SKPoint skew)
However, it's unlikely that you'll be using either of these two methods in isolation.
The Skew Experiment page lets you experiment with skew values that range between –10 and 10. A text string is positioned in the upper-left corner of the page, with skew values obtained from two Slider
elements. Here is the PaintSurface
handler in the SkewExperimentPage
class:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Blue,
TextSize = 200
})
{
string text = "SKEW";
SKRect textBounds = new SKRect();
textPaint.MeasureText(text, ref textBounds);
canvas.Skew((float)xSkewSlider.Value, (float)ySkewSlider.Value);
canvas.DrawText(text, 0, -textBounds.Top, textPaint);
}
}
Values of the xSkew
argument shift the bottom of the text right for positive values or left for negative values. Values of ySkew
shift the right of the text down for positive values or up for negative values:
If the xSkew
value is the negative of the ySkew
value, the result is rotation, but also scaled somewhat.
The transform formulas are as follows:
x' = x + xSkew · y
y' = ySkew · x + y
For example, for a positive xSkew
value, the transformed x'
value increases as y
increases. That's what causes the tilt.
If a triangle 200 pixels wide and 100 pixels high is positioned with its upper-left corner at the point (0, 0) and is rendered with an xSkew
value of 1.5, the following parallelogram results:
The coordinates of the bottom edge have y
values of 100, so it is shifted 150 pixels to the right.
For non-zero values of xSkew
or ySkew
, only the point (0, 0) remains the same. That point can be considered the center of skewing. If you need the center of skewing to be something else (which is usually the case), there is no Skew
method that provides that. You'll need to explicitly combine Translate
calls with the Skew
call. To center the skewing at px
and py
, make the following calls:
canvas.Translate(px, py);
canvas.Skew(xSkew, ySkew);
canvas.Translate(-px, -py);
The composite transform formulas are:
x' = x + xSkew · (y – py)
y' = ySkew · (x – px) + y
If ySkew
is zero, then the px
value is not used. The value is irrelevant, and similarly for ySkew
and py
.
You might feel more comfortable specifying skew as an angle of tilt, such as the angle α in this diagram:
The ratio of the 150-pixel shift to the 100-pixel vertical is the tangent of that angle, in this example 56.3 degrees.
The XAML file of the Skew Angle Experiment page is similar to the Skew Angle page except that the Slider
elements range from –90 degrees to 90 degrees. The SkewAngleExperiment
code-behind file centers the text on the page and uses Translate
to set a center of skewing to the center of the page. A short SkewDegrees
method at the bottom of the code converts angles to skew values:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Blue,
TextSize = 200
})
{
float xCenter = info.Width / 2;
float yCenter = info.Height / 2;
string text = "SKEW";
SKRect textBounds = new SKRect();
textPaint.MeasureText(text, ref textBounds);
float xText = xCenter - textBounds.MidX;
float yText = yCenter - textBounds.MidY;
canvas.Translate(xCenter, yCenter);
SkewDegrees(canvas, xSkewSlider.Value, ySkewSlider.Value);
canvas.Translate(-xCenter, -yCenter);
canvas.DrawText(text, xText, yText, textPaint);
}
}
void SkewDegrees(SKCanvas canvas, double xDegrees, double yDegrees)
{
canvas.Skew((float)Math.Tan(Math.PI * xDegrees / 180),
(float)Math.Tan(Math.PI * yDegrees / 180));
}
As an angle approaches positive or negative 90 degrees, the tangent approaches infinity, but angles up to about 80 degrees or so are usable:
A small negative horizontal skew can mimic oblique or italic text, as the Oblique Text page demonstrates. The ObliqueTextPage
class shows how it's done:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint textPaint = new SKPaint()
{
Style = SKPaintStyle.Fill,
Color = SKColors.Maroon,
TextAlign = SKTextAlign.Center,
TextSize = info.Width / 8 // empirically determined
})
{
canvas.Translate(info.Width / 2, info.Height / 2);
SkewDegrees(canvas, -20, 0);
canvas.DrawText(Title, 0, 0, textPaint);
}
}
void SkewDegrees(SKCanvas canvas, double xDegrees, double yDegrees)
{
canvas.Skew((float)Math.Tan(Math.PI * xDegrees / 180),
(float)Math.Tan(Math.PI * yDegrees / 180));
}
The TextAlign
property of SKPaint
is set to Center
. Without any transforms, the DrawText
call with coordinates of (0, 0) would position the text with the horizontal center of the baseline at the upper-left corner. The SkewDegrees
skews the text horizontally 20 degrees relative to the baseline. The Translate
call moves the horizontal center of the text's baseline to the center of the canvas:
The Skew Shadow Text page demonstrates how to use a combination of a 45-degree skew and vertical scale to make a text shadow that tilts away from the text. Here's the pertinent part of the PaintSurface
handler:
using (SKPaint textPaint = new SKPaint())
{
textPaint.Style = SKPaintStyle.Fill;
textPaint.TextSize = info.Width / 6; // empirically determined
// Common to shadow and text
string text = "Shadow";
float xText = 20;
float yText = info.Height / 2;
// Shadow
textPaint.Color = SKColors.LightGray;
canvas.Save();
canvas.Translate(xText, yText);
canvas.Skew((float)Math.Tan(-Math.PI / 4), 0);
canvas.Scale(1, 3);
canvas.Translate(-xText, -yText);
canvas.DrawText(text, xText, yText, textPaint);
canvas.Restore();
// Text
textPaint.Color = SKColors.Blue;
canvas.DrawText(text, xText, yText, textPaint);
}
The shadow is displayed first and then the text:
The vertical coordinate passed to the DrawText
method indicates the position of the text relative to the baseline. That is the same vertical coordinate used for the center of skewing. This technique will not work if the text string contains descenders. For example, substitute the word "quirky" for "Shadow" and here's the result:
The shadow and text are still aligned at the baseline, but the effect just looks wrong. To fix it, you need to obtain the text bounds:
SKRect textBounds = new SKRect();
textPaint.MeasureText(text, ref textBounds);
The Translate
calls need to be adjusted by the height of the descenders:
canvas.Translate(xText, yText + textBounds.Bottom);
canvas.Skew((float)Math.Tan(-Math.PI / 4), 0);
canvas.Scale(1, 3);
canvas.Translate(-xText, -yText - textBounds.Bottom);
Now the shadow extends from the bottom of those descenders: