Modos de mistura Porter-Duff
Os modos de mistura Porter-Duff são nomeados em homenagem a Thomas Porter e Tom Duff, que desenvolveram uma álgebra de composição enquanto trabalhavam para a Lucasfilm. Seu artigo Compositing Digital Images foi publicado na edição de julho de 1984 da Computer Graphics, páginas 253 a 259. Esses modos de mistura são essenciais para a composição, que é montar várias imagens em uma cena composta:
Conceitos de Porter-Duff
Suponha que um retângulo acastanhado ocupe os dois terços esquerdo e superior da superfície da tela:
Essa área é chamada de destino ou, às vezes, de plano de fundo ou pano de fundo.
Você deseja desenhar o seguinte retângulo, que é do mesmo tamanho do destino. O retângulo é transparente, exceto por uma área azulada que ocupa os dois terços direito e inferior:
Isso é chamado de fonte ou, às vezes, de primeiro plano.
Quando você exibe a origem no destino, veja o que você espera:
Os pixels transparentes da fonte permitem que o plano de fundo apareça, enquanto os pixels azulados da fonte obscurecem o plano de fundo. Esse é o caso normal, e é referido no SkiaSharp como SKBlendMode.SrcOver
. Esse valor é a configuração padrão da propriedade quando um SKPaint
objeto é instanciado BlendMode
pela primeira vez.
No entanto, é possível especificar um modo de mesclagem diferente para um efeito diferente. Se você especificar SKBlendMode.DstOver
, na área em que a origem e o destino se cruzam, o destino aparecerá em vez da origem:
O SKBlendMode.DstIn
modo de mesclagem exibe apenas a área onde o destino e a origem se cruzam usando a cor de destino:
O modo de mistura de SKBlendMode.Xor
(OR exclusivo) faz com que nada apareça onde as duas áreas se sobrepõem:
Os retângulos coloridos de destino e origem dividem efetivamente a superfície de exibição em quatro áreas exclusivas que podem ser coloridas de várias maneiras correspondentes à presença dos retângulos de destino e origem:
Os retângulos superior direito e inferior esquerdo estão sempre em branco porque o destino e a origem são transparentes nessas áreas. A cor de destino ocupa a área superior esquerda, de modo que essa área pode ser colorida com a cor de destino ou não. Da mesma forma, a cor de origem ocupa a área inferior direita, de modo que essa área pode ser colorida com a cor de origem ou não. A interseção do destino e da origem no meio pode ser colorida com a cor de destino, a cor de origem ou não ser colorida.
O número total de combinações é 2 (para o canto superior esquerdo) vezes 2 (para o canto inferior direito) vezes 3 (para o centro) ou 12. Estes são os 12 modos básicos de composição Porter-Duff.
No final de Compositing Digital Images (página 256), Porter e Duff adicionam um 13º modo chamado plus (correspondente ao membro SkiaSharp SKBlendMode.Plus
e ao modo W3C Lighter (que não deve ser confundido com o modo W3C Lighten ). Esse Plus
modo adiciona as cores de destino e origem, um processo que será descrito com mais detalhes em breve.
Skia adiciona um modo 14 chamado Modulate
que é muito semelhante a Plus
exceto que as cores de destino e origem são multiplicadas. Pode ser tratado como um modo de mistura Porter-Duff adicional.
Aqui estão os 14 modos Porter-Duff, conforme definido no SkiaSharp. A tabela mostra como eles colorem cada uma das três áreas não em branco no diagrama acima:
Mode | Destino | Cruzamento | Origem |
---|---|---|---|
Clear |
|||
Src |
Origem | X | |
Dst |
X | Destino | |
SrcOver |
X | Origem | X |
DstOver |
X | Destino | X |
SrcIn |
Origem | ||
DstIn |
Destino | ||
SrcOut |
X | ||
DstOut |
X | ||
SrcATop |
X | Origem | |
DstATop |
Destino | X | |
Xor |
X | X | |
Plus |
X | Somar | X |
Modulate |
Product |
Esses modos de mesclagem são simétricos. A origem e o destino podem ser trocados e todos os modos ainda estão disponíveis.
A convenção de nomenclatura dos modos segue algumas regras simples:
- Src ou Dst por si só significa que apenas os pixels de origem ou destino estão visíveis.
- O sufixo Over indica o que é visível na interseção. Ou a origem ou o destino é desenhado "sobre" o outro.
- O sufixo In significa que apenas a interseção é colorida. A saída é restrita apenas à parte da origem ou destino que está "dentro" da outra.
- O sufixo Out significa que a interseção não é colorida. A saída é apenas a parte da origem ou destino que está "fora" da interseção.
- O sufixo ATop é a união de In e Out. Inclui a área onde a origem ou destino está "em cima" do outro.
Observe a diferença com os Plus
modos e Modulate
. Esses modos estão executando um tipo diferente de cálculo nos pixels de origem e destino. Eles são descritos com mais detalhes em breve.
A página Grade de Porter-Duff mostra todos os 14 modos em uma tela na forma de uma grade. Cada modo é uma instância separada do SKCanvasView
. Por essa razão, uma classe é derivada de SKCanvasView
named PorterDuffCanvasView
. O construtor estático cria dois bitmaps do mesmo tamanho, um com um retângulo acastanhado em sua área superior esquerda e outro com um retângulo azulado:
class PorterDuffCanvasView : SKCanvasView
{
static SKBitmap srcBitmap, dstBitmap;
static PorterDuffCanvasView()
{
dstBitmap = new SKBitmap(300, 300);
srcBitmap = new SKBitmap(300, 300);
using (SKPaint paint = new SKPaint())
{
using (SKCanvas canvas = new SKCanvas(dstBitmap))
{
canvas.Clear();
paint.Color = new SKColor(0xC0, 0x80, 0x00);
canvas.DrawRect(new SKRect(0, 0, 200, 200), paint);
}
using (SKCanvas canvas = new SKCanvas(srcBitmap))
{
canvas.Clear();
paint.Color = new SKColor(0x00, 0x80, 0xC0);
canvas.DrawRect(new SKRect(100, 100, 300, 300), paint);
}
}
}
···
}
O construtor de instância tem um parâmetro do tipo SKBlendMode
. Ele salva esse parâmetro em um campo.
class PorterDuffCanvasView : SKCanvasView
{
···
SKBlendMode blendMode;
public PorterDuffCanvasView(SKBlendMode blendMode)
{
this.blendMode = blendMode;
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Find largest square that fits
float rectSize = Math.Min(info.Width, info.Height);
float x = (info.Width - rectSize) / 2;
float y = (info.Height - rectSize) / 2;
SKRect rect = new SKRect(x, y, x + rectSize, y + rectSize);
// Draw destination bitmap
canvas.DrawBitmap(dstBitmap, rect);
// Draw source bitmap
using (SKPaint paint = new SKPaint())
{
paint.BlendMode = blendMode;
canvas.DrawBitmap(srcBitmap, rect, paint);
}
// Draw outline
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Stroke;
paint.Color = SKColors.Black;
paint.StrokeWidth = 2;
rect.Inflate(-1, -1);
canvas.DrawRect(rect, paint);
}
}
}
A OnPaintSurface
substituição desenha os dois bitmaps. O primeiro é desenhado normalmente:
canvas.DrawBitmap(dstBitmap, rect);
O segundo é desenhado com um SKPaint
objeto onde a BlendMode
propriedade foi definida para o argumento do construtor:
using (SKPaint paint = new SKPaint())
{
paint.BlendMode = blendMode;
canvas.DrawBitmap(srcBitmap, rect, paint);
}
O restante da OnPaintSurface
substituição desenha um retângulo ao redor do bitmap para indicar seus tamanhos.
A PorterDuffGridPage
classe cria quatorze instâncias de PorterDurffCanvasView
, uma para cada membro da blendModes
matriz. A ordem dos SKBlendModes
membros na matriz é um pouco diferente da tabela para posicionar modos semelhantes adjacentes uns aos outros. As 14 instâncias de PorterDuffCanvasView
são organizadas junto com rótulos em um Grid
:
public class PorterDuffGridPage : ContentPage
{
public PorterDuffGridPage()
{
Title = "Porter-Duff Grid";
SKBlendMode[] blendModes =
{
SKBlendMode.Src, SKBlendMode.Dst, SKBlendMode.SrcOver, SKBlendMode.DstOver,
SKBlendMode.SrcIn, SKBlendMode.DstIn, SKBlendMode.SrcOut, SKBlendMode.DstOut,
SKBlendMode.SrcATop, SKBlendMode.DstATop, SKBlendMode.Xor, SKBlendMode.Plus,
SKBlendMode.Modulate, SKBlendMode.Clear
};
Grid grid = new Grid
{
Margin = new Thickness(5)
};
for (int row = 0; row < 4; row++)
{
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
}
for (int col = 0; col < 3; col++)
{
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
}
for (int i = 0; i < blendModes.Length; i++)
{
SKBlendMode blendMode = blendModes[i];
int row = 2 * (i / 4);
int col = i % 4;
Label label = new Label
{
Text = blendMode.ToString(),
HorizontalTextAlignment = TextAlignment.Center
};
Grid.SetRow(label, row);
Grid.SetColumn(label, col);
grid.Children.Add(label);
PorterDuffCanvasView canvasView = new PorterDuffCanvasView(blendMode);
Grid.SetRow(canvasView, row + 1);
Grid.SetColumn(canvasView, col);
grid.Children.Add(canvasView);
}
Content = grid;
}
}
Eis o resultado:
Você vai querer se convencer de que a transparência é crucial para o bom funcionamento dos modos de mistura Porter-Duff. A PorterDuffCanvasView
classe contém um total de três chamadas para o Canvas.Clear
método. Todos eles usam o método sem parâmetros, que define todos os pixels como transparentes:
canvas.Clear();
Tente alterar qualquer uma dessas chamadas para que os pixels sejam definidos como branco opaco:
canvas.Clear(SKColors.White);
Após essa mudança, alguns dos modos de mistura parecerão funcionar, mas outros não. Se você definir o plano de fundo do bitmap de origem como branco, o SrcOver
modo não funcionará porque não há pixels transparentes no bitmap de origem para permitir que o destino seja exibido. Se você definir o plano de fundo do bitmap de destino ou da tela como branco, não funcionará DstOver
porque o destino não tem pixels transparentes.
Pode haver a tentação de substituir os bitmaps na página Grade de Porter-Duff por chamadas mais simples DrawRect
. Isso funcionará para o retângulo de destino, mas não para o retângulo de origem. O retângulo de origem deve abranger mais do que apenas a área de cor azulada. O retângulo de origem deve incluir uma área transparente que corresponda à área colorida do destino. Só então esses modos de mistura funcionarão.
Usando mattes com Porter-Duff
A página Brick-Wall Compositing mostra um exemplo de uma tarefa clássica de composição: uma imagem precisa ser montada a partir de várias peças, incluindo um bitmap com um plano de fundo que precisa ser eliminado. Aqui está o bitmap SeatedMonkey.jpg com o plano de fundo problemático:
Em preparação para a composição, um fosco correspondente foi criado, que é outro bitmap que é preto onde você deseja que a imagem apareça e transparente caso contrário. Esse arquivo é chamado SeatedMonkeyMatte.png e está entre os recursos na pasta Mídia no exemplo:
Este não é um fosco criado habilmente. Idealmente, o fosco deve incluir pixels parcialmente transparentes ao redor da borda dos pixels pretos, e esse fosco não.
O arquivo XAML para a página Brick-Wall Compositing instancia um SKCanvasView
e um Button
que orienta o usuário pelo processo de composição da imagem final:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Effects.BrickWallCompositingPage"
Title="Brick-Wall Compositing">
<StackLayout>
<skia:SKCanvasView x:Name="canvasView"
VerticalOptions="FillAndExpand"
PaintSurface="OnCanvasViewPaintSurface" />
<Button Text="Show sitting monkey"
HorizontalOptions="Center"
Margin="0, 10"
Clicked="OnButtonClicked" />
</StackLayout>
</ContentPage>
O arquivo code-behind carrega os dois bitmaps necessários e manipula o Clicked
evento do Button
. Para cada Button
clique, o campo é incrementado step
e uma nova Text
propriedade é definida para o Button
. Quando step
atinge 5, ele é definido de volta para 0:
public partial class BrickWallCompositingPage : ContentPage
{
SKBitmap monkeyBitmap = BitmapExtensions.LoadBitmapResource(
typeof(BrickWallCompositingPage),
"SkiaSharpFormsDemos.Media.SeatedMonkey.jpg");
SKBitmap matteBitmap = BitmapExtensions.LoadBitmapResource(
typeof(BrickWallCompositingPage),
"SkiaSharpFormsDemos.Media.SeatedMonkeyMatte.png");
int step = 0;
public BrickWallCompositingPage ()
{
InitializeComponent ();
}
void OnButtonClicked(object sender, EventArgs args)
{
Button btn = (Button)sender;
step = (step + 1) % 5;
switch (step)
{
case 0: btn.Text = "Show sitting monkey"; break;
case 1: btn.Text = "Draw matte with DstIn"; break;
case 2: btn.Text = "Draw sidewalk with DstOver"; break;
case 3: btn.Text = "Draw brick wall with DstOver"; break;
case 4: btn.Text = "Reset"; break;
}
canvasView.InvalidateSurface();
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
···
}
}
Quando o programa é executado pela primeira vez, nada é visível, exceto o Button
:
Pressionar o Button
once faz com que step
aumente para 1 e o manipulador PaintSurface
agora exibe SeatedMonkey.jpg:
public partial class BrickWallCompositingPage : ContentPage
{
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
···
float x = (info.Width - monkeyBitmap.Width) / 2;
float y = info.Height - monkeyBitmap.Height;
// Draw monkey bitmap
if (step >= 1)
{
canvas.DrawBitmap(monkeyBitmap, x, y);
}
···
}
}
Não há SKPaint
nenhum objeto e, portanto, nenhum modo de mistura. O bitmap aparece na parte inferior da tela:
Pressione o botão Button
novamente e step
incrementa para 2. Esta é a etapa crucial da exibição do arquivo SeatedMonkeyMatte.png :
public partial class BrickWallCompositingPage : ContentPage
{
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
···
// Draw matte to exclude monkey's surroundings
if (step >= 2)
{
using (SKPaint paint = new SKPaint())
{
paint.BlendMode = SKBlendMode.DstIn;
canvas.DrawBitmap(matteBitmap, x, y, paint);
}
}
···
}
}
O modo de mistura é SKBlendMode.DstIn
, o que significa que o destino será preservado em áreas correspondentes a áreas não transparentes da fonte. O restante do retângulo de destino correspondente ao bitmap original torna-se transparente:
O plano de fundo foi removido.
O próximo passo é desenhar um retângulo que se assemelhe a uma calçada em que o macaco está sentado. A aparência desta calçada é baseada em uma composição de dois shaders: um sombreador de cor sólida e um sombreador de ruído Perlin:
public partial class BrickWallCompositingPage : ContentPage
{
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
···
const float sidewalkHeight = 80;
SKRect rect = new SKRect(info.Rect.Left, info.Rect.Bottom - sidewalkHeight,
info.Rect.Right, info.Rect.Bottom);
// Draw gravel sidewalk for monkey to sit on
if (step >= 3)
{
using (SKPaint paint = new SKPaint())
{
paint.Shader = SKShader.CreateCompose(
SKShader.CreateColor(SKColors.SandyBrown),
SKShader.CreatePerlinNoiseTurbulence(0.1f, 0.3f, 1, 9));
paint.BlendMode = SKBlendMode.DstOver;
canvas.DrawRect(rect, paint);
}
}
···
}
}
Como essa calçada deve ir atrás do macaco, o modo blend é DstOver
. O destino aparece somente onde o plano de fundo é transparente:
O passo final é adicionar uma parede de tijolos. O programa usa o bloco de bitmap de parede de tijolo disponível como a propriedade BrickWallTile
estática na AlgorithmicBrickWallPage
classe. Uma transformação de conversão é adicionada SKShader.CreateBitmap
à chamada para deslocar os blocos de modo que a linha inferior seja um bloco completo:
public partial class BrickWallCompositingPage : ContentPage
{
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
···
// Draw bitmap tiled brick wall behind monkey
if (step >= 4)
{
using (SKPaint paint = new SKPaint())
{
SKBitmap bitmap = AlgorithmicBrickWallPage.BrickWallTile;
float yAdjust = (info.Height - sidewalkHeight) % bitmap.Height;
paint.Shader = SKShader.CreateBitmap(bitmap,
SKShaderTileMode.Repeat,
SKShaderTileMode.Repeat,
SKMatrix.MakeTranslation(0, yAdjust));
paint.BlendMode = SKBlendMode.DstOver;
canvas.DrawRect(info.Rect, paint);
}
}
}
}
Por conveniência, a DrawRect
chamada exibe esse sombreador em toda a tela, mas o modo limita DstOver
a saída apenas à área da tela que ainda é transparente:
Obviamente existem outras formas de compor essa cena. Poderia ser construído começando em segundo plano e progredindo para o primeiro plano. Mas usar os modos de mistura oferece mais flexibilidade. Em particular, o uso do fosco permite que o plano de fundo de um bitmap seja excluído da cena composta.
Como você aprendeu no artigo Recorte com caminhos e regiões, a SKCanvas
classe define três tipos de recorte, correspondentes aos ClipRect
métodos , ClipPath
e ClipRegion
. Os modos de mesclagem Porter-Duff adicionam outro tipo de recorte, que permite restringir uma imagem a qualquer coisa que você possa desenhar, incluindo bitmaps. O fosco usado na composição de tijolo-parede define essencialmente uma área de recorte.
Transparência e transições de gradiente
Os exemplos dos modos de mesclagem de Porter-Duff mostrados anteriormente neste artigo envolveram imagens que consistiam em pixels opacos e pixels transparentes, mas não pixels parcialmente transparentes. As funções de modo de mistura também são definidas para esses pixels. A tabela a seguir é uma definição mais formal dos modos de mesclagem Porter-Duff que usa a notação encontrada na Referência Skia SkBlendMode. (Porque SkBlendMode Reference é uma referência Skia, sintaxe C++ é usada.)
Conceitualmente, os componentes vermelho, verde, azul e alfa de cada pixel são convertidos de bytes em números de ponto flutuante no intervalo de 0 a 1. Para o canal alfa, 0 é totalmente transparente e 1 é totalmente opaco
A notação na tabela abaixo usa as seguintes abreviações:
- Da é o canal alfa de destino
- Dc é a cor RGB de destino
- Sa é o canal alfa de origem
- Sc é a cor RGB de origem
As cores RGB são pré-multiplicadas pelo valor alfa. Por exemplo, se Sc representa vermelho puro, mas Sa é 0x80, então a cor RGB é (0x80, 0, 0). Se Sa for 0, todos os componentes RGB também serão zero.
O resultado é mostrado entre colchetes com o canal alfa e a cor RGB separados por vírgula: [alfa, cor]. Para a cor, o cálculo é realizado separadamente para os componentes vermelho, verde e azul:
Mode | Operação |
---|---|
Clear |
[0, 0] |
Src |
[Sa, Sc] |
Dst |
[Da, Dc] |
SrcOver |
[Sa + Da· (1 – Sá), Sc + Dc· (1 – Sá) |
DstOver |
[Da + Sá· (1 – Da), dc + sc· (1 – Da) |
SrcIn |
[Sa· Da, Sc· Da] |
DstIn |
[Da· Sa, Dc·Sa] |
SrcOut |
[Sa· (1 – Da), Sc· (1 – Da)] |
DstOut |
[Da· (1 – Sá), Dc· (1 – Sa)] |
SrcATop |
[Da, Sc· Da + Dc· (1 – Sa)] |
DstATop |
[Sa, Dc·Sa + Sc· (1 – Da)] |
Xor |
[Sa + Da – 2· Sa· Da, Sc· (1 – Da) + DC· (1 – Sa)] |
Plus |
[Sa + Da, Sc + Dc] |
Modulate |
[Sa· Da, Sc· Dc] |
Essas operações são mais fáceis de analisar quando Da e Sa são 0 ou 1. Por exemplo, para o modo padrão SrcOver
, se Sa é 0, então Sc também é 0 e o resultado é [Da, Dc], o alfa de destino e a cor. Se Sa é 1, então o resultado é [Sa, Sc], a fonte alfa e cor, ou [1, Sc].
Os Plus
modos e Modulate
são um pouco diferentes dos outros em que novas cores podem resultar da combinação da origem e do destino. O Plus
modo pode ser interpretado com componentes de byte ou componentes de ponto flutuante. Na página Grade de Porter-Duff mostrada anteriormente, a cor de destino é (0xC0, 0x80, 0x00) e a cor de origem é (0x00, 0x80 0xC0). Cada par de componentes é adicionado, mas a soma é fixada em 0xFF. O resultado é a cor (0xC0, 0xFF, 0xC0). Essa é a cor mostrada na interseção.
Para o Modulate
modo, os valores RGB devem ser convertidos em ponto flutuante. A cor de destino é (0,75, 0,5, 0) e a origem é (0, 0,5, 0,75). Os componentes RGB são multiplicados juntos, e o resultado é (0, 0,25, 0). Essa é a cor mostrada na interseção na página Grade de Porter-Duff para esse modo.
A página Transparência Porter-Duff permite examinar como os modos de mesclagem Porter-Duff operam em objetos gráficos parcialmente transparentes. O arquivo XAML inclui um Picker
com os modos Porter-Duff:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
xmlns:skiaviews="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Effects.PorterDuffTransparencyPage"
Title="Porter-Duff Transparency">
<StackLayout>
<skiaviews:SKCanvasView x:Name="canvasView"
VerticalOptions="FillAndExpand"
PaintSurface="OnCanvasViewPaintSurface" />
<Picker x:Name="blendModePicker"
Title="Blend Mode"
Margin="10"
SelectedIndexChanged="OnPickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type skia:SKBlendMode}">
<x:Static Member="skia:SKBlendMode.Clear" />
<x:Static Member="skia:SKBlendMode.Src" />
<x:Static Member="skia:SKBlendMode.Dst" />
<x:Static Member="skia:SKBlendMode.SrcOver" />
<x:Static Member="skia:SKBlendMode.DstOver" />
<x:Static Member="skia:SKBlendMode.SrcIn" />
<x:Static Member="skia:SKBlendMode.DstIn" />
<x:Static Member="skia:SKBlendMode.SrcOut" />
<x:Static Member="skia:SKBlendMode.DstOut" />
<x:Static Member="skia:SKBlendMode.SrcATop" />
<x:Static Member="skia:SKBlendMode.DstATop" />
<x:Static Member="skia:SKBlendMode.Xor" />
<x:Static Member="skia:SKBlendMode.Plus" />
<x:Static Member="skia:SKBlendMode.Modulate" />
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
3
</Picker.SelectedIndex>
</Picker>
</StackLayout>
</ContentPage>
O arquivo code-behind preenche dois retângulos do mesmo tamanho usando um gradiente linear. O gradiente de destino é do canto superior direito para o inferior esquerdo. É acastanhado no canto superior direito, mas depois em direção ao centro começa a desaparecer para transparente, e é transparente no canto inferior esquerdo.
O retângulo de origem tem um gradiente do canto superior esquerdo para o inferior direito. O canto superior esquerdo é azulado, mas novamente desaparece para transparente, e é transparente no canto inferior direito.
public partial class PorterDuffTransparencyPage : ContentPage
{
public PorterDuffTransparencyPage()
{
InitializeComponent();
}
void OnPickerSelectedIndexChanged(object sender, EventArgs args)
{
canvasView.InvalidateSurface();
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Make square display rectangle smaller than canvas
float size = 0.9f * Math.Min(info.Width, info.Height);
float x = (info.Width - size) / 2;
float y = (info.Height - size) / 2;
SKRect rect = new SKRect(x, y, x + size, y + size);
using (SKPaint paint = new SKPaint())
{
// Draw destination
paint.Shader = SKShader.CreateLinearGradient(
new SKPoint(rect.Right, rect.Top),
new SKPoint(rect.Left, rect.Bottom),
new SKColor[] { new SKColor(0xC0, 0x80, 0x00),
new SKColor(0xC0, 0x80, 0x00, 0) },
new float[] { 0.4f, 0.6f },
SKShaderTileMode.Clamp);
canvas.DrawRect(rect, paint);
// Draw source
paint.Shader = SKShader.CreateLinearGradient(
new SKPoint(rect.Left, rect.Top),
new SKPoint(rect.Right, rect.Bottom),
new SKColor[] { new SKColor(0x00, 0x80, 0xC0),
new SKColor(0x00, 0x80, 0xC0, 0) },
new float[] { 0.4f, 0.6f },
SKShaderTileMode.Clamp);
// Get the blend mode from the picker
paint.BlendMode = blendModePicker.SelectedIndex == -1 ? 0 :
(SKBlendMode)blendModePicker.SelectedItem;
canvas.DrawRect(rect, paint);
// Stroke surrounding rectangle
paint.Shader = null;
paint.BlendMode = SKBlendMode.SrcOver;
paint.Style = SKPaintStyle.Stroke;
paint.Color = SKColors.Black;
paint.StrokeWidth = 3;
canvas.DrawRect(rect, paint);
}
}
}
Este programa demonstra que os modos de mesclagem Porter-Duff podem ser usados com objetos gráficos diferentes de bitmaps. No entanto, a fonte deve incluir uma área transparente. Esse é o caso aqui porque o gradiente preenche o retângulo, mas parte do gradiente é transparente.
Aqui estão três exemplos:
A configuração do destino e da origem é muito semelhante aos diagramas mostrados na página 255 do artigo original Porter-Duff Compositing Digital Images , mas esta página demonstra que os modos de mistura são bem comportados para áreas de transparência parcial.
Você pode usar gradientes transparentes para alguns efeitos diferentes. Uma possibilidade é o mascaramento, que é semelhante à técnica mostrada na seção Gradientes radiais para mascaramento da página de gradientes circulares SkiaSharp. Grande parte da página Máscara de composição é semelhante ao programa anterior. Ele carrega um recurso de bitmap e determina um retângulo no qual exibi-lo. Um gradiente radial é criado com base em um centro e raio pré-determinados:
public class CompositingMaskPage : ContentPage
{
SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
typeof(CompositingMaskPage),
"SkiaSharpFormsDemos.Media.MountainClimbers.jpg");
static readonly SKPoint CENTER = new SKPoint(180, 300);
static readonly float RADIUS = 120;
public CompositingMaskPage ()
{
Title = "Compositing Mask";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Find rectangle to display bitmap
float scale = Math.Min((float)info.Width / bitmap.Width,
(float)info.Height / bitmap.Height);
SKRect rect = SKRect.Create(scale * bitmap.Width, scale * bitmap.Height);
float x = (info.Width - rect.Width) / 2;
float y = (info.Height - rect.Height) / 2;
rect.Offset(x, y);
// Display bitmap in rectangle
canvas.DrawBitmap(bitmap, rect);
// Adjust center and radius for scaled and offset bitmap
SKPoint center = new SKPoint(scale * CENTER.X + x,
scale * CENTER.Y + y);
float radius = scale * RADIUS;
using (SKPaint paint = new SKPaint())
{
paint.Shader = SKShader.CreateRadialGradient(
center,
radius,
new SKColor[] { SKColors.Black,
SKColors.Transparent },
new float[] { 0.6f, 1 },
SKShaderTileMode.Clamp);
paint.BlendMode = SKBlendMode.DstIn;
// Display rectangle using that gradient and blend mode
canvas.DrawRect(rect, paint);
}
canvas.DrawColor(SKColors.Pink, SKBlendMode.DstOver);
}
}
A diferença desse programa é que o degradê começa com preto no centro e termina com transparência. Ele é exibido no bitmap com um modo de mesclagem de DstIn
, que mostra o destino somente nas áreas da origem que não são transparentes.
Após a DrawRect
chamada, toda a superfície da tela é transparente, exceto o círculo definido pelo gradiente radial. Uma última chamada é feita:
canvas.DrawColor(SKColors.Pink, SKBlendMode.DstOver);
Todas as áreas transparentes da tela são cor-de-rosa:
Você também pode usar modos Porter-Duff e gradientes parcialmente transparentes para transições de uma imagem para outra. A página Transições de gradiente inclui um Slider
para indicar um nível de progresso na transição de 0 para 1 e um Picker
para escolher o tipo de transição desejado:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Effects.GradientTransitionsPage"
Title="Gradient Transitions">
<StackLayout>
<skia:SKCanvasView x:Name="canvasView"
VerticalOptions="FillAndExpand"
PaintSurface="OnCanvasViewPaintSurface" />
<Slider x:Name="progressSlider"
Margin="10, 0"
ValueChanged="OnSliderValueChanged" />
<Label Text="{Binding Source={x:Reference progressSlider},
Path=Value,
StringFormat='Progress = {0:F2}'}"
HorizontalTextAlignment="Center" />
<Picker x:Name="transitionPicker"
Title="Transition"
Margin="10"
SelectedIndexChanged="OnPickerSelectedIndexChanged" />
</StackLayout>
</ContentPage>
O arquivo code-behind carrega dois recursos de bitmap para demonstrar a transição. Essas são as mesmas duas imagens usadas na página Bitmap Dissolve anteriormente neste artigo. O código também define uma enumeração com três membros correspondentes a três tipos de gradientes — linear, radial e de varredura. Esses valores são carregados no Picker
:
public partial class GradientTransitionsPage : ContentPage
{
SKBitmap bitmap1 = BitmapExtensions.LoadBitmapResource(
typeof(GradientTransitionsPage),
"SkiaSharpFormsDemos.Media.SeatedMonkey.jpg");
SKBitmap bitmap2 = BitmapExtensions.LoadBitmapResource(
typeof(GradientTransitionsPage),
"SkiaSharpFormsDemos.Media.FacePalm.jpg");
enum TransitionMode
{
Linear,
Radial,
Sweep
};
public GradientTransitionsPage ()
{
InitializeComponent ();
foreach (TransitionMode mode in Enum.GetValues(typeof(TransitionMode)))
{
transitionPicker.Items.Add(mode.ToString());
}
transitionPicker.SelectedIndex = 0;
}
void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
{
canvasView.InvalidateSurface();
}
void OnPickerSelectedIndexChanged(object sender, EventArgs args)
{
canvasView.InvalidateSurface();
}
···
}
O arquivo code-behind cria três SKPaint
objetos. O paint0
objeto não usa um modo de mesclagem. Esse objeto de pintura é usado para desenhar um retângulo com um gradiente que vai de preto a transparente, conforme indicado na colors
matriz. A positions
matriz é baseada na posição do , mas ajustada Slider
um pouco. Se o Slider
estiver em seu mínimo ou máximo, os progress
valores serão 0 ou 1 e um dos dois bitmaps deverá estar totalmente visível. A positions
matriz deve ser definida de acordo com esses valores.
Se o progress
valor for 0, a positions
matriz conterá os valores -0.1 e 0. O SkiaSharp ajustará esse primeiro valor para ser igual a 0, o que significa que o gradiente é preto apenas em 0 e transparente caso contrário. Quando progress
é 0,5, a matriz contém os valores 0,45 e 0,55. O gradiente é preto de 0 a 0,45, depois faz a transição para transparente e é totalmente transparente de 0,55 a 1. Quando progress
é 1, a matriz é 1 e 1.1, o positions
que significa que o gradiente é preto de 0 a 1.
As colors
matrizes e são usadas nos três métodos que SKShader
criam um gradienteposition
. Apenas um desses sombreadores é criado com base na Picker
seleção:
public partial class GradientTransitionsPage : ContentPage
{
···
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Assume both bitmaps are square for display rectangle
float size = Math.Min(info.Width, info.Height);
SKRect rect = SKRect.Create(size, size);
float x = (info.Width - size) / 2;
float y = (info.Height - size) / 2;
rect.Offset(x, y);
using (SKPaint paint0 = new SKPaint())
using (SKPaint paint1 = new SKPaint())
using (SKPaint paint2 = new SKPaint())
{
SKColor[] colors = new SKColor[] { SKColors.Black,
SKColors.Transparent };
float progress = (float)progressSlider.Value;
float[] positions = new float[]{ 1.1f * progress - 0.1f,
1.1f * progress };
switch ((TransitionMode)transitionPicker.SelectedIndex)
{
case TransitionMode.Linear:
paint0.Shader = SKShader.CreateLinearGradient(
new SKPoint(rect.Left, 0),
new SKPoint(rect.Right, 0),
colors,
positions,
SKShaderTileMode.Clamp);
break;
case TransitionMode.Radial:
paint0.Shader = SKShader.CreateRadialGradient(
new SKPoint(rect.MidX, rect.MidY),
(float)Math.Sqrt(Math.Pow(rect.Width / 2, 2) +
Math.Pow(rect.Height / 2, 2)),
colors,
positions,
SKShaderTileMode.Clamp);
break;
case TransitionMode.Sweep:
paint0.Shader = SKShader.CreateSweepGradient(
new SKPoint(rect.MidX, rect.MidY),
colors,
positions);
break;
}
canvas.DrawRect(rect, paint0);
paint1.BlendMode = SKBlendMode.SrcOut;
canvas.DrawBitmap(bitmap1, rect, paint1);
paint2.BlendMode = SKBlendMode.DstOver;
canvas.DrawBitmap(bitmap2, rect, paint2);
}
}
}
Esse gradiente é exibido no retângulo sem um modo de mesclagem. Após essa DrawRect
chamada, a tela simplesmente contém um gradiente de preto para transparente. A quantidade de preto aumenta com valores mais altos Slider
.
Nas quatro instruções finais do PaintSurface
manipulador, os dois bitmaps são exibidos. O SrcOut
modo de mesclagem significa que o primeiro bitmap é exibido somente nas áreas transparentes do plano de fundo. O DstOver
modo para o segundo bitmap significa que o segundo bitmap é exibido somente nas áreas onde o primeiro bitmap não é exibido.
As capturas de tela a seguir mostram os três tipos de transições diferentes, cada um na marca de 50%: