94 private readonly SKPaint _fillPaint;
95 private readonly SKPaint _strokePaint;
96 private readonly SKPaint _imagePaint =
new()
99 FilterQuality = SKFilterQuality.Medium,
100 BlendMode = SKBlendMode.SrcOver
103 private SKColor? _borderColor;
104 private SKShader? _fillShader;
105 private SKImage? _fillImage;
106 private SKBitmap? _fillBitmap;
108 private float _imageScale = 1f;
109 private SKPoint _imageOffsetPx;
110 private bool _resourcesDisposed;
112 private bool _isFilled;
113 private float _cornerRadius;
114 private float[]? _dashPattern;
116 private bool _needsRebuildPaints =
true;
119 private bool _pulseFillEnabled;
120 private SKColor _pulseFillFrom, _pulseFillTo;
121 private float _pulseFillPeriodSec = 1f;
123 private bool _pulseBorderEnabled;
124 private SKColor _pulseBorderFrom, _pulseBorderTo;
125 private float _pulseBorderPeriodSec = 1f;
127 private enum PulseWave
138 private PulseWave _pulseFillWave = PulseWave.Sine;
139 private PulseWave _pulseBorderWave = PulseWave.Sine;
142 private long _pulseLastTick = 0;
143 private float _timeSec;
145 private DirectRectangle(Color color,
150 Rectangle? screenBounds,
151 Rectangle? worldBounds,
152 string? nickname =
null)
153 : base(renderSurfaceHost, mode, sceneLayer, view, screenBounds, worldBounds, nickname)
156 _fillPaint =
new SKPaint { IsAntialias =
true, Style = SKPaintStyle.Fill };
157 _strokePaint =
new SKPaint { IsAntialias =
true, Style = SKPaintStyle.Stroke, StrokeWidth = 1 };
186 Rectangle worldBounds,
187 string? nickname =
null)
213 Rectangle screenBounds,
214 string? nickname =
null)
215 : this(color, renderSurfaceHost,
DirectDrawingMode.
View, null, view, screenBounds, null, nickname) { }
236 var sk = color.ToSKColor();
238 _fillPaint.Color = sk;
239 _strokePaint.Color = sk;
240 _needsRebuildPaints =
true;
268 _borderColor = color.ToSKColor();
269 _needsRebuildPaints =
true;
295 var c = _fillPaint.Color;
296 var withA =
new SKColor(c.Red, c.Green, c.Blue, (
byte)alpha);
297 _fillPaint.Color = withA;
299 if (_borderColor is
null)
300 _strokePaint.Color = withA;
301 _needsRebuildPaints =
true;
327 _isFilled = isFilled;
328 _needsRebuildPaints =
true;
353 _strokePaint.StrokeWidth = width;
354 _needsRebuildPaints =
true;
384 _cornerRadius = radius;
413 _dashPattern =
new[] { dashLength, gapLength };
466 _fillPaint.BlendMode = mode;
467 _strokePaint.BlendMode = mode;
468 _imagePaint.BlendMode = mode;
508 _strokeAlign = align;
542 public DirectRectangle
PulseFill(Color from, Color to,
float periodSec,
bool enabled =
true,
bool triangle =
false)
544 _pulseFillEnabled = enabled;
545 _pulseFillFrom = from.ToSKColor();
546 _pulseFillTo = to.ToSKColor();
547 _pulseFillPeriodSec = MathF.Max(0.0001f, periodSec);
548 _pulseFillWave = triangle ? PulseWave.Triangle : PulseWave.Sine;
582 public DirectRectangle
PulseBorder(Color from, Color to,
float periodSec,
bool enabled =
true,
bool triangle =
false)
584 _pulseBorderEnabled = enabled;
585 _pulseBorderFrom = from.ToSKColor();
586 _pulseBorderTo = to.ToSKColor();
587 _pulseBorderPeriodSec = MathF.Max(0.0001f, periodSec);
588 _pulseBorderWave = triangle ? PulseWave.Triangle : PulseWave.Sine;
609 _pulseFillEnabled = _pulseBorderEnabled =
false;
653 SKShaderTileMode tileX = SKShaderTileMode.Repeat,
654 SKShaderTileMode tileY = SKShaderTileMode.Repeat,
656 SKPoint? offsetPx =
null,
657 SKFilterQuality filterQuality = SKFilterQuality.None)
659 ArgumentNullException.ThrowIfNull(bitmap);
661 scale =
float.IsFinite(scale) && scale > 0f
663 :
throw new ArgumentOutOfRangeException(nameof(scale),
"Pattern scale must be finite and greater than zero.");
665 var m = SKMatrix.CreateScale(scale, scale);
666 if (offsetPx is { } o)
667 m = m.PostConcat(SKMatrix.CreateTranslation(o.X, o.Y));
669 SKShader newShader = SKShader.CreateBitmap(bitmap, tileX, tileY, m);
671 _fillPaint.Shader =
null;
672 _fillShader?.Dispose();
673 _fillShader = newShader;
678 _fillPaint.Shader = _fillShader;
679 _fillPaint.FilterQuality = filterQuality;
707 SKPoint? offsetPx =
null,
708 SKFilterQuality filterQuality = SKFilterQuality.Medium)
710 ArgumentNullException.ThrowIfNull(bitmap);
718 filterQuality: filterQuality);
743 SKPoint? offsetPx =
null,
744 SKFilterQuality filterQuality = SKFilterQuality.Medium)
746 ArgumentNullException.ThrowIfNull(image);
754 filterQuality: filterQuality);
786 _fillPaint.Shader =
null;
826 if (_pulseLastTick > 0)
828 float dt = HighResTimer.GetDuration(_pulseLastTick, tick);
832 _pulseLastTick = tick;
834 bool changed =
false;
836 if (_pulseFillEnabled)
838 float t = PulseT(_timeSec, _pulseFillPeriodSec, _pulseFillWave);
839 var c = LerpColor(_pulseFillFrom, _pulseFillTo, t);
840 if (_fillPaint.Color != c) { _fillPaint.Color = c; changed =
true; }
843 if (_pulseBorderEnabled)
845 float t = PulseT(_timeSec, _pulseBorderPeriodSec, _pulseBorderWave);
846 var c = LerpColor(_pulseBorderFrom, _pulseBorderTo, t);
848 if (_strokePaint.Color != c)
850 _strokePaint.Color = c;
857 _needsRebuildPaints =
false;
893 if (backbuffer is RecordingBackbuffer && _fillImage?.IsTextureBacked ==
true)
894 throw new NotSupportedException(
"Desktop frame recording requires a CPU image, not a context-bound texture.");
896 var canvas = backbuffer.
Canvas;
898 if (_needsRebuildPaints)
901 var fillRect = destRectScreen.ToSKRect();
902 var strokeRect = fillRect;
904 bool willDrawStroke = !_isFilled || _borderColor.HasValue || _strokePaint.StrokeWidth > 0.01f;
905 float half = _strokePaint.StrokeWidth * 0.5f;
908 if (willDrawStroke && _strokeAlign !=
StrokeAlign.Center)
911 strokeRect.Inflate(-half, -half);
913 strokeRect.Inflate(half, half);
917 float strokeCornerRadius;
918 switch (_strokeAlign)
921 strokeCornerRadius = MathF.Max(0f, _cornerRadius - half);
924 strokeCornerRadius = _cornerRadius + half;
928 strokeCornerRadius = _cornerRadius;
933 _strokePaint.PathEffect = _dashPattern is { Length: > 0 }
934 ? SKPathEffect.CreateDash(_dashPattern, 0)
940 if (_fillImage is not
null || _fillBitmap is not
null)
942 DrawImageFill(canvas, fillRect);
944 else if (_cornerRadius > 0)
946 canvas.DrawRoundRect(fillRect, strokeCornerRadius, strokeCornerRadius, _fillPaint);
950 canvas.DrawRect(fillRect, _fillPaint);
957 var prevBlend = _strokePaint.BlendMode;
958 var prevColor = _strokePaint.Color;
959 var prevAA = _strokePaint.IsAntialias;
961 _strokePaint.IsAntialias =
false;
962 _strokePaint.BlendMode = SKBlendMode.Src;
966 var strokeColor = _pulseBorderEnabled ? _strokePaint.Color
967 : (_borderColor ?? prevColor);
969 _strokePaint.Color = strokeColor.WithAlpha(255);
971 if (_cornerRadius > 0)
972 canvas.DrawRoundRect(strokeRect, _cornerRadius, _cornerRadius, _strokePaint);
974 canvas.DrawRect(strokeRect, _strokePaint);
976 _strokePaint.IsAntialias = prevAA;
977 _strokePaint.BlendMode = prevBlend;
978 _strokePaint.Color = prevColor;
979 _strokePaint.StrokeJoin = SKStrokeJoin.Round;
980 _strokePaint.StrokeCap = SKStrokeCap.Round;
985 protected override void Dispose(
bool disposing)
987 if (disposing && !_resourcesDisposed)
989 _resourcesDisposed =
true;
990 _fillPaint.Shader =
null;
991 _fillShader?.Dispose();
993 _fillPaint.Dispose();
994 _strokePaint.Dispose();
995 _imagePaint.Dispose();
998 base.Dispose(disposing);
1001 private void SetFillImageCore(
1007 SKFilterQuality filterQuality)
1009 if (!
float.IsFinite(scale) || scale <= 0f)
1010 throw new ArgumentOutOfRangeException(nameof(scale),
"Image scale must be finite and greater than zero.");
1012 if (!Enum.IsDefined(mode))
1013 throw new ArgumentOutOfRangeException(nameof(mode));
1015 _fillPaint.Shader =
null;
1016 _fillShader?.Dispose();
1020 _fillBitmap = bitmap;
1021 _imageFillMode = mode;
1022 _imageScale = scale;
1023 _imageOffsetPx = offsetPx ?? SKPoint.Empty;
1024 _imagePaint.FilterQuality = filterQuality;
1030 private void DrawImageFill(
1034 SKImage? image = _fillImage;
1035 SKBitmap? bitmap = _fillBitmap;
1037 int sourceWidth = image?.Width ?? bitmap?.Width ?? 0;
1038 int sourceHeight = image?.Height ?? bitmap?.Height ?? 0;
1040 if (sourceWidth <= 0 || sourceHeight <= 0)
1047 using var clipPath =
new SKPath();
1049 if (_cornerRadius > 0f)
1050 clipPath.AddRoundRect(fillRect, _cornerRadius, _cornerRadius);
1052 clipPath.AddRect(fillRect);
1056 SKClipOperation.Intersect,
1057 antialias: _cornerRadius > 0f);
1059 var source =
new SKRect(
1067 if (image is not
null)
1086 SKRect destination = ComputeImageDestination(
1091 if (image is not
null)
1115 private void DrawRepeatedImage(
1121 float tileWidth = image.Width * _imageScale;
1122 float tileHeight = image.Height * _imageScale;
1124 float offsetX = PositiveModulo(_imageOffsetPx.X, tileWidth);
1125 float offsetY = PositiveModulo(_imageOffsetPx.Y, tileHeight);
1127 float startX = fillRect.Left + offsetX;
1128 float startY = fillRect.Top + offsetY;
1130 if (startX > fillRect.Left)
1131 startX -= tileWidth;
1133 if (startY > fillRect.Top)
1134 startY -= tileHeight;
1136 for (
float y = startY; y < fillRect.Bottom; y += tileHeight)
1138 for (
float x = startX; x < fillRect.Right; x += tileWidth)
1153 private void DrawRepeatedBitmap(
1159 float tileWidth = bitmap.Width * _imageScale;
1160 float tileHeight = bitmap.Height * _imageScale;
1162 float offsetX = PositiveModulo(_imageOffsetPx.X, tileWidth);
1163 float offsetY = PositiveModulo(_imageOffsetPx.Y, tileHeight);
1165 float startX = fillRect.Left + offsetX;
1166 float startY = fillRect.Top + offsetY;
1168 if (startX > fillRect.Left)
1169 startX -= tileWidth;
1171 if (startY > fillRect.Top)
1172 startY -= tileHeight;
1174 for (
float y = startY; y < fillRect.Bottom; y += tileHeight)
1176 for (
float x = startX; x < fillRect.Right; x += tileWidth)
1191 private static SKRect ComputeImageDestination(
1201 float x = bounds.MidX - source.Width * 0.5f;
1202 float y = bounds.MidY - source.Height * 0.5f;
1211 float scaleX = bounds.Width / source.Width;
1212 float scaleY = bounds.Height / source.Height;
1214 float scale = mode
switch
1220 MathF.Floor(MathF.Min(scaleX, scaleY))),
1224 float width = source.Width * scale;
1225 float height = source.Height * scale;
1226 float left = bounds.MidX - width * 0.5f;
1227 float top = bounds.MidY - height * 0.5f;
1236 private static float PositiveModulo(
float value,
float modulus)
1238 float remainder = value % modulus;
1239 return remainder < 0f
1240 ? remainder + modulus
1247 private void RebuildPaints()
1250 _fillPaint.IsAntialias =
true;
1251 _fillPaint.Style = SKPaintStyle.Fill;
1253 _strokePaint.IsAntialias =
true;
1254 _strokePaint.Style = SKPaintStyle.Stroke;
1257 if (!_pulseBorderEnabled)
1260 if (_borderColor.HasValue)
1262 _ = _strokePaint.Color;
1263 _strokePaint.Color = _borderColor.Value;
1267 _strokePaint.Color = _fillPaint.Color;
1270 _needsRebuildPaints =
false;
1302 [MethodImpl(MethodImplOptions.AggressiveInlining)]
1303 private static float PulseT(
float timeSec,
float periodSec, PulseWave wave)
1305 float phase = (timeSec / periodSec) % 1f;
1306 if (wave == PulseWave.Sine)
1309 return 0.5f * (1f + MathF.Sin(phase * MathF.PI * 2f));
1313 return phase < 0.5f ? (phase * 2f) : (1f - ((phase - 0.5f) * 2f));
1317 [MethodImpl(MethodImplOptions.AggressiveInlining)]
1318 private static SKColor LerpColor(SKColor a, SKColor b,
float t01)
1320 t01 = Math.Clamp(t01, 0f, 1f);
1322 byte r = (byte)(a.Red + (b.Red - a.Red) * t01);
1323 byte g = (byte)(a.Green + (b.Green - a.Green) * t01);
1324 byte bch = (byte)(a.Blue + (b.Blue - a.Blue) * t01);
1325 byte aA = (byte)(a.Alpha + (b.Alpha - a.Alpha) * t01);
1327 return new SKColor(r, g, bch, aA);