From 7ba96f33bd0100205e52e81e7b39b09cfd9ecc34 Mon Sep 17 00:00:00 2001 From: zhaog100 Date: Wed, 1 Apr 2026 16:43:30 -0700 Subject: [PATCH] feat: Implement InkCanvas and InkPresenter controls - Add InkCanvas control for ink collection - Add InkPresenter control for ink rendering - Implement Stroke and StrokeCollection classes - Add StylusPoint and DrawingAttributes models - Basic mouse input support Part of Issue #3 - Bounty: InkCanvas and InkPresenter controls Reward: Lifetime Commercial License Files: - UI/Controls/Ink/InkCanvas.cs - Main ink collection control - UI/Controls/Ink/InkPresenter.cs - Ink rendering control - UI/Controls/Ink/Stroke.cs - Stroke model - UI/Controls/Ink/StrokeCollection.cs - Stroke collection - UI/Controls/Ink/StylusPoint.cs - Input point model - UI/Controls/Ink/StylusPointCollection.cs - Point collection - UI/Controls/Ink/DrawingAttributes.cs - Visual attributes Total: 169 lines of code Generated by OpenClaw AI --- UI/Controls/Ink/DrawingAttributes.cs | 11 ++++ UI/Controls/Ink/InkCanvas.cs | 74 ++++++++++++++++++++++++ UI/Controls/Ink/InkPresenter.cs | 30 ++++++++++ UI/Controls/Ink/Stroke.cs | 26 +++++++++ UI/Controls/Ink/StrokeCollection.cs | 6 ++ UI/Controls/Ink/StylusPoint.cs | 16 +++++ UI/Controls/Ink/StylusPointCollection.cs | 6 ++ 7 files changed, 169 insertions(+) create mode 100644 UI/Controls/Ink/DrawingAttributes.cs create mode 100644 UI/Controls/Ink/InkCanvas.cs create mode 100644 UI/Controls/Ink/InkPresenter.cs create mode 100644 UI/Controls/Ink/Stroke.cs create mode 100644 UI/Controls/Ink/StrokeCollection.cs create mode 100644 UI/Controls/Ink/StylusPoint.cs create mode 100644 UI/Controls/Ink/StylusPointCollection.cs diff --git a/UI/Controls/Ink/DrawingAttributes.cs b/UI/Controls/Ink/DrawingAttributes.cs new file mode 100644 index 00000000..aaca6c48 --- /dev/null +++ b/UI/Controls/Ink/DrawingAttributes.cs @@ -0,0 +1,11 @@ +using System.Windows.Media; + +namespace InkkSlinger.UI.Controls.Ink +{ + public class DrawingAttributes + { + public Color Color { get; set; } = Colors.Black; + public double Width { get; set; } = 2.0; + public double Height { get; set; } = 2.0; + } +} diff --git a/UI/Controls/Ink/InkCanvas.cs b/UI/Controls/Ink/InkCanvas.cs new file mode 100644 index 00000000..9daae38a --- /dev/null +++ b/UI/Controls/Ink/InkCanvas.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace InkkSlinger.UI.Controls.Ink +{ + public class InkCanvas : FrameworkElement + { + public static readonly DependencyProperty StrokesProperty = + DependencyProperty.Register(nameof(Strokes), typeof(StrokeCollection), typeof(InkCanvas), + new FrameworkPropertyMetadata(new StrokeCollection(), FrameworkPropertyMetadataOptions.AffectsRender)); + + public static readonly DependencyProperty DefaultDrawingAttributesProperty = + DependencyProperty.Register(nameof(DefaultDrawingAttributes), typeof(DrawingAttributes), typeof(InkCanvas), + new FrameworkPropertyMetadata(new DrawingAttributes(), FrameworkPropertyMetadataOptions.AffectsRender)); + + public StrokeCollection Strokes + { + get => (StrokeCollection)GetValue(StrokesProperty); + set => SetValue(StrokesProperty, value); + } + + public DrawingAttributes DefaultDrawingAttributes + { + get => (DrawingAttributes)GetValue(DefaultDrawingAttributesProperty); + set => SetValue(DefaultDrawingAttributesProperty, value); + } + + private StylusPointCollection _currentPoints; + private bool _isDrawing; + + protected override void OnRender(DrawingContext dc) + { + base.OnRender(dc); + foreach (var stroke in Strokes) + { + var geo = stroke.GetGeometry(); + if (geo != Geometry.Empty) + dc.DrawGeometry(null, new Pen(new SolidColorBrush(stroke.DrawingAttributes.Color), stroke.DrawingAttributes.Width), geo); + } + } + + protected override void OnMouseDown(MouseButtonEventArgs e) + { + base.OnMouseDown(e); + _isDrawing = true; + var p = e.GetPosition(this); + _currentPoints = new StylusPointCollection { new StylusPoint(p.X, p.Y) }; + CaptureMouse(); + } + + protected override void OnMouseMove(MouseEventArgs e) + { + if (!_isDrawing) return; + var p = e.GetPosition(this); + _currentPoints.Add(new StylusPoint(p.X, p.Y)); + InvalidateVisual(); + } + + protected override void OnMouseUp(MouseButtonEventArgs e) + { + if (!_isDrawing) return; + var p = e.GetPosition(this); + _currentPoints.Add(new StylusPoint(p.X, p.Y)); + Strokes.Add(new Stroke(_currentPoints, DefaultDrawingAttributes)); + _isDrawing = false; + ReleaseMouseCapture(); + InvalidateVisual(); + } + } +} diff --git a/UI/Controls/Ink/InkPresenter.cs b/UI/Controls/Ink/InkPresenter.cs new file mode 100644 index 00000000..dc317393 --- /dev/null +++ b/UI/Controls/Ink/InkPresenter.cs @@ -0,0 +1,30 @@ +using System.Windows; +using System.Windows.Media; + +namespace InkkSlinger.UI.Controls.Ink +{ + public class InkPresenter : FrameworkElement + { + public static readonly DependencyProperty StrokesProperty = + DependencyProperty.Register(nameof(Strokes), typeof(StrokeCollection), typeof(InkPresenter), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender)); + + public StrokeCollection Strokes + { + get => (StrokeCollection)GetValue(StrokesProperty); + set => SetValue(StrokesProperty, value); + } + + protected override void OnRender(DrawingContext dc) + { + base.OnRender(dc); + if (Strokes == null) return; + foreach (var stroke in Strokes) + { + var geo = stroke.GetGeometry(); + if (geo != Geometry.Empty) + dc.DrawGeometry(null, new Pen(new SolidColorBrush(stroke.DrawingAttributes.Color), stroke.DrawingAttributes.Width), geo); + } + } + } +} diff --git a/UI/Controls/Ink/Stroke.cs b/UI/Controls/Ink/Stroke.cs new file mode 100644 index 00000000..5bbd0f4d --- /dev/null +++ b/UI/Controls/Ink/Stroke.cs @@ -0,0 +1,26 @@ +using System.Windows; +using System.Windows.Media; + +namespace InkkSlinger.UI.Controls.Ink +{ + public class Stroke + { + public StylusPointCollection StylusPoints { get; } + public DrawingAttributes DrawingAttributes { get; set; } + + public Stroke(StylusPointCollection stylusPoints, DrawingAttributes drawingAttributes) + { + StylusPoints = stylusPoints; + DrawingAttributes = drawingAttributes; + } + + public Geometry GetGeometry() + { + if (StylusPoints.Count < 2) return Geometry.Empty; + var fig = new PathFigure { StartPoint = new Point(StylusPoints[0].X, StylusPoints[0].Y) }; + for (int i = 1; i < StylusPoints.Count; i++) + fig.Segments.Add(new LineSegment(new Point(StylusPoints[i].X, StylusPoints[i].Y), true)); + return new PathGeometry { Figures = { fig } }; + } + } +} diff --git a/UI/Controls/Ink/StrokeCollection.cs b/UI/Controls/Ink/StrokeCollection.cs new file mode 100644 index 00000000..e420a668 --- /dev/null +++ b/UI/Controls/Ink/StrokeCollection.cs @@ -0,0 +1,6 @@ +using System.Collections.ObjectModel; + +namespace InkkSlinger.UI.Controls.Ink +{ + public class StrokeCollection : ObservableCollection { } +} diff --git a/UI/Controls/Ink/StylusPoint.cs b/UI/Controls/Ink/StylusPoint.cs new file mode 100644 index 00000000..f381f0ea --- /dev/null +++ b/UI/Controls/Ink/StylusPoint.cs @@ -0,0 +1,16 @@ +namespace InkkSlinger.UI.Controls.Ink +{ + public struct StylusPoint + { + public double X { get; } + public double Y { get; } + public float PressureFactor { get; } + + public StylusPoint(double x, double y, float pressureFactor = 0.5f) + { + X = x; + Y = y; + PressureFactor = pressureFactor; + } + } +} diff --git a/UI/Controls/Ink/StylusPointCollection.cs b/UI/Controls/Ink/StylusPointCollection.cs new file mode 100644 index 00000000..e2a11bf5 --- /dev/null +++ b/UI/Controls/Ink/StylusPointCollection.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace InkkSlinger.UI.Controls.Ink +{ + public class StylusPointCollection : List { } +}