From 333fc7cfc18145ac7ced3a9536aed9506832118e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Su=C3=A1rez?= Date: Fri, 7 Apr 2023 14:26:41 +0200 Subject: [PATCH] Created captcha control --- .../ViewModels/MainViewModel.cs | 3 + src/AlohaKit.Gallery/Views/CaptchaView.xaml | 61 ++++++++++ .../Views/CaptchaView.xaml.cs | 42 +++++++ src/AlohaKit/Controls/Captcha/Captcha.cs | 110 ++++++++++++++++++ .../Controls/Captcha/CaptchaDrawable.cs | 102 ++++++++++++++++ src/AlohaKit/Controls/Captcha/CaptchaLevel.cs | 9 ++ 6 files changed, 327 insertions(+) create mode 100644 src/AlohaKit.Gallery/Views/CaptchaView.xaml create mode 100644 src/AlohaKit.Gallery/Views/CaptchaView.xaml.cs create mode 100644 src/AlohaKit/Controls/Captcha/Captcha.cs create mode 100644 src/AlohaKit/Controls/Captcha/CaptchaDrawable.cs create mode 100644 src/AlohaKit/Controls/Captcha/CaptchaLevel.cs diff --git a/src/AlohaKit.Gallery/ViewModels/MainViewModel.cs b/src/AlohaKit.Gallery/ViewModels/MainViewModel.cs index 3fb9752..32c9326 100644 --- a/src/AlohaKit.Gallery/ViewModels/MainViewModel.cs +++ b/src/AlohaKit.Gallery/ViewModels/MainViewModel.cs @@ -16,6 +16,9 @@ protected override IEnumerable CreateItems() => new[] new SectionModel(typeof(ButtonView), "Button", "The Button responds to a tap or click."), + + new SectionModel(typeof(CaptchaView), "Captcha", + "Displays a distorted word."), new SectionModel(typeof(CheckBoxView), "CheckBox", "CheckBox is a type of button that can either be checked or empty."), diff --git a/src/AlohaKit.Gallery/Views/CaptchaView.xaml b/src/AlohaKit.Gallery/Views/CaptchaView.xaml new file mode 100644 index 0000000..a3e38ad --- /dev/null +++ b/src/AlohaKit.Gallery/Views/CaptchaView.xaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AlohaKit.Gallery/Views/CaptchaView.xaml.cs b/src/AlohaKit.Gallery/Views/CaptchaView.xaml.cs new file mode 100644 index 0000000..30a19bc --- /dev/null +++ b/src/AlohaKit.Gallery/Views/CaptchaView.xaml.cs @@ -0,0 +1,42 @@ +namespace AlohaKit.Gallery.Views; + +public partial class CaptchaView : ContentPage +{ + public CaptchaView() + { + InitializeComponent(); + UpdateColors(); + } + + void OnTextColorEntryTextChanged(object sender, TextChangedEventArgs e) + { + UpdateColors(); + } + + void UpdateColors() + { + var textColor = GetColorFromString(TextColorEntry.Text); + + if (textColor != null) + { + TextColorEntry.BackgroundColor = textColor; + + Captcha.TextColor = textColor; + } + } + + Color GetColorFromString(string value) + { + if (string.IsNullOrEmpty(value)) + return null; + + try + { + return Color.FromArgb(value[0].Equals('#') ? value : $"#{value}"); + } + catch (Exception) + { + return null; + } + } +} \ No newline at end of file diff --git a/src/AlohaKit/Controls/Captcha/Captcha.cs b/src/AlohaKit/Controls/Captcha/Captcha.cs new file mode 100644 index 0000000..02af001 --- /dev/null +++ b/src/AlohaKit/Controls/Captcha/Captcha.cs @@ -0,0 +1,110 @@ +namespace AlohaKit.Controls +{ + public class Captcha : GraphicsView + { + public Captcha() + { + HeightRequest = 50; + WidthRequest = 150; + + Drawable = CaptchaDrawable = new CaptchaDrawable(); + } + + public CaptchaDrawable CaptchaDrawable { get; set; } + + public static readonly BindableProperty LevelProperty = + BindableProperty.Create(nameof(Level), typeof(CaptchaLevel), typeof(Captcha), CaptchaLevel.Normal, + propertyChanged: (bindableObject, oldValue, newValue) => + { + if (newValue != null && bindableObject is Captcha captcha) + { + captcha.UpdateLevel(); + } + }); + + public CaptchaLevel Level + { + get => (CaptchaLevel)GetValue(LevelProperty); + set => SetValue(LevelProperty, value); + } + + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(Captcha), Colors.Black, + propertyChanged: (bindableObject, oldValue, newValue) => + { + if (newValue != null && bindableObject is Captcha captcha) + { + captcha.UpdateTextColor(); + } + }); + + public Color TextColor + { + get => (Color)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + protected override void OnParentChanged() + { + base.OnParentChanged(); + + if (Parent != null) + { + UpdateLevel(); + UpdateTextColor(); + } + } + + void UpdateLevel() + { + if (CaptchaDrawable == null) + return; + + if (CaptchaDrawable.Level != Level) + { + CaptchaDrawable.Level = Level; + + var word = GenerateRandomWord(GetWordLength(Level)); + CaptchaDrawable.Word = word; + + Invalidate(); + } + } + + void UpdateTextColor() + { + if (CaptchaDrawable == null) + return; + + CaptchaDrawable.TextColor = TextColor; + + Invalidate(); + } + + string GenerateRandomWord(int length) + { + var random = new Random(); + + const string chars = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789"; + + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + } + + int GetWordLength(CaptchaLevel level) + { + switch (level) + { + case CaptchaLevel.Weak: + return 4; + default: + case CaptchaLevel.Normal: + return 6; + case CaptchaLevel.Strong: + return 8; + } + } + } +} \ No newline at end of file diff --git a/src/AlohaKit/Controls/Captcha/CaptchaDrawable.cs b/src/AlohaKit/Controls/Captcha/CaptchaDrawable.cs new file mode 100644 index 0000000..a3fbded --- /dev/null +++ b/src/AlohaKit/Controls/Captcha/CaptchaDrawable.cs @@ -0,0 +1,102 @@ +namespace AlohaKit.Controls +{ + public class CaptchaDrawable : IDrawable + { + public string Word { get; set; } + public CaptchaLevel Level { get; set; } + public Color TextColor { get; set; } + + public void Draw(ICanvas canvas, RectF dirtyRect) + { + DrawText(canvas, dirtyRect); + DrawArtifacts(canvas, dirtyRect); + } + + void DrawText(ICanvas canvas, RectF dirtyRect) + { + canvas.SaveState(); + + var height = dirtyRect.Height; + var width = dirtyRect.Width; + + int minLetterDistanceY = 0; + int maxLetterDistanceY = (int)height / Word.Length; + + int minLetterDistanceX = 6; + int maxLetterDistanceX = (int)width / Word.Length; + + var coordRandom = new Random(); + + var letterPoint = new PointF(coordRandom.Next(minLetterDistanceY, maxLetterDistanceX), height / 2); + + foreach (var l in Word) + { + letterPoint.Y += coordRandom.Next(0, maxLetterDistanceY); + + canvas.FontSize = 24; + canvas.FontColor = TextColor; + + canvas.DrawString(l.ToString(), letterPoint.X, letterPoint.Y,HorizontalAlignment.Center); + + letterPoint.X += coordRandom.Next(minLetterDistanceX, maxLetterDistanceX); + } + + canvas.RestoreState(); + } + + void DrawArtifacts(ICanvas canvas, RectF dirtyRect) + { + canvas.SaveState(); + + var randomLines = new Random(); + + for (var i = 0; i < GetArtifactLength(Level); i++) + { + var x1 = randomLines.Next(0, (int)dirtyRect.Width); + var y1 = randomLines.Next(0, (int)dirtyRect.Height); + var x2 = randomLines.Next(0, (int)dirtyRect.Width); + var y2 = randomLines.Next(0, (int)dirtyRect.Height); + + canvas.StrokeColor = TextColor.WithAlpha(0.8f); + + var randomStrokeSize = new Random(); + canvas.StrokeSize = randomStrokeSize.Next(1, GetArtifactWidth(Level)); + + var p1 = new PointF(x1, y1); + var p2 = new PointF(x2, y2); + + canvas.DrawLine(p1, p2); + } + + canvas.RestoreState(); + } + + int GetArtifactLength(CaptchaLevel level) + { + switch (level) + { + case CaptchaLevel.Weak: + return 4; + default: + case CaptchaLevel.Normal: + return 6; + case CaptchaLevel.Strong: + return 8; + } + } + + int GetArtifactWidth(CaptchaLevel level) + { + switch (level) + { + case CaptchaLevel.Weak: + return 2; + default: + case CaptchaLevel.Normal: + return 3; + case CaptchaLevel.Strong: + return 4; + } + } + } +} \ No newline at end of file diff --git a/src/AlohaKit/Controls/Captcha/CaptchaLevel.cs b/src/AlohaKit/Controls/Captcha/CaptchaLevel.cs new file mode 100644 index 0000000..85d4562 --- /dev/null +++ b/src/AlohaKit/Controls/Captcha/CaptchaLevel.cs @@ -0,0 +1,9 @@ +namespace AlohaKit.Controls +{ + public enum CaptchaLevel + { + Weak, + Normal, + Strong + } +} \ No newline at end of file