This commit is contained in:
2025-08-23 21:39:21 +08:00
parent 7bac32e3c4
commit f67b4db4ba
562 changed files with 37981 additions and 38280 deletions
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
+9
View File
@@ -0,0 +1,9 @@
<Application x:Class="InkCanvasForClassX.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:InkCanvasForClassX"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
+17
View File
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace InkCanvasForClassX
{
/// <summary>
/// App.xaml 的交互逻辑
/// </summary>
public partial class App : Application
{
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<OutputType>WinExe</OutputType>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<UseWPF>true</UseWPF>
<ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.1.4" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
<PackageReference Include="System.Runtime" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Windows\" />
</ItemGroup>
</Project>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
<ItemGroup>
<Compile Update="Libraries\InkCanvas.xaml.cs">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<Page Update="Libraries\InkCanvas.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project>
@@ -0,0 +1,12 @@
<UserControl x:Class="InkCanvasForClassX.Libraries.InkCanvas"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:InkCanvasForClassX.Libraries"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Background="Red">
<local:InkProjector x:Name="inkProjector" Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=ActualWidth}" Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=ActualHeight}" Margin="0,0,0,0"/>
</Grid>
</UserControl>
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace InkCanvasForClassX.Libraries
{
public partial class InkCanvas : UserControl
{
public static readonly DependencyProperty InkStrokesProperty =
DependencyProperty.Register(
name: "InkStrokes",
propertyType: typeof(StrokeCollection),
ownerType: typeof(InkCanvas),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: new StrokeCollection(),
propertyChangedCallback: new PropertyChangedCallback(OnInkStrokesChanged))
);
public StrokeCollection InkStrokes {
get => (StrokeCollection)GetValue(InkStrokesProperty);
set {
Trace.WriteLine("Set InkStrokes");
SetValue(InkStrokesProperty, value);
}
}
private static void OnInkStrokesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Trace.WriteLine("Update");
var control = (InkCanvas)d;
if (e.OldValue is StrokeCollection oldStrokes) {
oldStrokes.StrokesChanged -= control.OnStrokesChanged;
}
if (e.NewValue is StrokeCollection newStrokes) {
newStrokes.StrokesChanged += control.OnStrokesChanged;
control.inkProjector.Strokes = newStrokes;
}
}
private void OnStrokesChanged(object sender, StrokeCollectionChangedEventArgs e)
{
Trace.WriteLine("Strokes Collection Changed");
// Ensure that the InkStrokes dependency property updates
SetValue(InkStrokesProperty, sender as StrokeCollection);
inkProjector.Strokes = sender as StrokeCollection;
}
public InkCanvas()
{
InitializeComponent();
InkStrokes.StrokesChanged += OnStrokesChanged;
}
}
}
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
namespace InkCanvasForClassX.Libraries
{
/// <summary>
/// 該控件提供一個基於<c>DrawingVisual</c>的最小的<c>StrokeCollection</c>渲染控件,用以替代重量級的<c>InkCanvas</c>控件
/// </summary>
public class InkProjector : FrameworkElement {
private VisualCollection _children;
private DrawingVisual _layer = new DrawingVisual();
private StrokeCollection _strokes;
private PerfectFreehandJint _perfectFreehandJint = new PerfectFreehandJint();
public InkProjector()
{
_children = new VisualCollection(this) {
_layer // 初始化DrawingVisual
};
}
public StrokeCollection Strokes {
get => _strokes;
set {
_strokes = value;
DrawPerfectInk();
}
}
protected override int VisualChildrenCount => _children.Count;
protected override Visual GetVisualChild(int index) {
if (index < 0 || index >= _children.Count) throw new ArgumentOutOfRangeException();
return _children[index];
}
private void DrawInk() {
DrawingContext context = _layer.RenderOpen();
_strokes.Draw(context);
context.Close();
}
private void DrawPerfectInk() {
DrawingContext context = _layer.RenderOpen();
context.PushClip(new RectangleGeometry(new Rect(new Size(this.ActualWidth,this.ActualHeight))));
foreach (var stroke in _strokes) {
var stylusPtsList = new List<PerfectFreehandJint.StylusPointLite>();
foreach (var strokeStylusPoint in stroke.StylusPoints)
{
stylusPtsList.Add(new PerfectFreehandJint.StylusPointLite()
{
x = Math.Round(strokeStylusPoint.X, 2),
y = Math.Round(strokeStylusPoint.Y, 2),
pressure = strokeStylusPoint.PressureFactor,
});
}
context.DrawGeometry(new SolidColorBrush(Colors.Black), (System.Windows.Media.Pen)null, _perfectFreehandJint.GetGeometryStroke(stylusPtsList.ToArray(), new PerfectFreehandJint.StrokeOptions()
{
size = 2,
thinning = 0.5,
smoothing = 0.5,
streamline = 0.2,
simulatePressure = true,
easing = (x) => 1 - (1 - x) * (1 - x),
last = true,
start = new PerfectFreehandJint.StrokeCapOptions()
{
cap = true,
taper = 0,
easing = (x) => 1 - (1 - x) * (1 - x),
},
end = new PerfectFreehandJint.StrokeCapOptions()
{
cap = true,
taper = 0,
easing = (x) => 1 - (1 - x) * (1 - x),
},
}));
}
context.Pop();
context.Close();
}
protected override void OnMouseLeave(MouseEventArgs e) {
base.OnMouseLeave(e);
Trace.WriteLine("Mouse Move");
}
}
}
@@ -0,0 +1,640 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using InkCanvasForClassX.Libraries;
using InkCanvasForClassX.Libraries.Stroke;
namespace InkCanvasForClassX.Libraries
{
/// <summary>
/// 提供對JS庫<c>steveruizok/perfect-freehand</c>的C#包裝
/// </summary>
public class PerfectFreehand {
private static double Average(double a, double b) {
return (a + b) / 2;
}
public static string ConvertVectorsToSVGPath(Vector[] points, bool closed = true)
{
int len = points.Length;
if (len < 4)
{
return string.Empty;
}
Vector a = points[0];
Vector b = points[1];
Vector c = points[2];
StringBuilder result = new StringBuilder();
result.AppendFormat("M{0:F2},{1:F2} Q{2:F2},{3:F2} {4:F2},{5:F2} T",
a.X, a.Y, b.X, b.Y, Average(b.X, c.X), Average(b.Y, c.Y));
for (int i = 2, max = len - 1; i < max; i++)
{
a = points[i];
b = points[i + 1];
result.AppendFormat("{0:F2},{1:F2} ", Average(a.X, b.X), Average(a.Y, b.Y));
}
if (closed)
{
result.Append("Z");
}
return result.ToString();
}
/// <summary>
/// Get an array of points as objects with an adjusted point, pressure, vector, distance, and runningLength.
/// </summary>
/// <param name="points">原注釋: An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional in both cases. 請使用<c>StylusPointCollection</c></param>
/// <param name="options">An object with options.</param>
/// <returns></returns>
public static StrokePoint[] GetStrokePoints(StylusPointCollection points,
StrokeOptions options) {
var streamline = options.Streamline ?? 0.5;
var size = options.Size ?? 16;
var isComplete = options.Last ?? false;
// If we don't have any points, return an empty array.
if (points.Count == 0) return Array.Empty<StrokePoint>();
// Find the interpolation level between points.
double t = 0.15 + (1 - streamline) * 0.85;
// Purify the StylusPointCollection
var pts = new StylusPointCollection();
foreach (var stylusPoint in points) {
pts.Add(new StylusPoint(stylusPoint.X, stylusPoint.Y, stylusPoint.PressureFactor));
}
// Add extra points between the two, to help avoid "dash" lines
// for strokes with tapered start and ends. Don't mutate the
// input array!
if (points.Count == 2) {
var last = pts[1];
pts.RemoveAt(pts.Count - 1);
for (var i = 1; i < 5; i++) {
var _vec = Vector.InterpolateVectors(
new Vector(pts[0].X, pts[0].Y),
new Vector(last.X, last.Y),
i / 4
);
pts.Add(new StylusPoint(_vec.X, _vec.Y));
}
}
// If there's only one point, add another point at a 1pt offset.
// Don't mutate the input array!
if (pts.Count == 1) {
var onePt = new Vector(pts[0].X + 1, pts[0].Y + 1);
pts.Add(new StylusPoint(onePt.X, onePt.Y, pts[0].PressureFactor));
}
// The strokePoints array will hold the points for the stroke.
// Start it out with the first point, which needs no adjustment.
var strokePoints = new List<StrokePoint>() {
new StrokePoint() {
Point = new Vector(pts[0].X, pts[0].Y),
Pressure = pts[0].PressureFactor >= 0 ? pts[0].PressureFactor : 0.25,
Vector = new Vector(1, 1),
Distance = 0,
RunningLength = 0,
}
};
// A flag to see whether we've already reached out minimum length
var hasReachedMinimumLength = false;
// We use the runningLength to keep track of the total distance
double runningLength = 0;
// We're set this to the latest point, so we can use it to calculate
// the distance and vector of the next point.
var prev = strokePoints[0];
// const max = pts.length - 1
var max = pts.Count - 1;
// Iterate through all of the points, creating StrokePoints.
for (var i = 1; i < pts.Count; i++) {
var point = isComplete && i == max
? // If we're at the last point, and `options.last` is true,
// then add the actual input point.
new Vector(pts[i].X, pts[i].Y)
: // Otherwise, using the t calculated from the streamline
// option, interpolate a new point between the previous
// point the current point.
Vector.InterpolateVectors(prev.Point, new Vector(pts[i].X, pts[i].Y), t);
// If the new point is the same as the previous point, skip ahead.
if (prev.Point.IsEqual(point)) continue;
// How far is the new point from the previous point?
var distance = Vector.DistLengthVectors(point, prev.Point);
// Add this distance to the total "running length" of the line.
runningLength += distance;
// At the start of the line, we wait until the new point is a
// certain distance away from the original point, to avoid noise
if (i < max && !hasReachedMinimumLength) {
if (runningLength < size) continue;
hasReachedMinimumLength = true;
// TODO: Backfill the missing points so that tapering works correctly.
}
// Create a new strokepoint (it will be the new "previous" one).
prev = new StrokePoint() {
// The adjusted point
Point = point,
// The input pressure (or .5 if not specified)
Pressure = pts[i].PressureFactor >= 0 ? pts[i].PressureFactor : 0.5,
// The vector from the current point to the previous point
Vector = Vector.UnitVector(Vector.SubtractVectors(prev.Point, point)),
// The distance between the current point and the previous point
Distance = distance,
// The total distance so far
RunningLength = runningLength,
};
// Push it to the strokePoints array.
strokePoints.Add(prev);
}
// Set the vector of the first point to be the same as the second point.
strokePoints[0].Vector = strokePoints[1]?.Vector ?? new Vector(0, 0);
return strokePoints.ToArray();
}
/// <summary>
/// Compute a radius based on the pressure.
/// </summary>
public static double GetStrokeRadius(
double size,
double thinning,
double pressure,
Func<double, double> easing = null)
{
if (easing == null) {
easing = t => t; // 默認的 easing 函數
}
return size * easing(0.5 - thinning * (0.5 - pressure));
}
// This is the rate of change for simulated pressure. It could be an option.
private const double RATE_OF_PRESSURE_CHANGE = 0.275;
private const double PI = Math.PI;
private const double FIXED_PI = PI + 0.0001;
/// <summary>
/// Get an array of points (as `[x, y]`) representing the outline of a stroke.
/// </summary>
/// <param name="points">An array of StrokePoints as returned from `getStrokePoints`.</param>
/// <param name="options">An object with options.</param>
/// <returns></returns>
public static Vector[] GetStrokeOutlinePointsVectors(StrokePoint[] points, StrokeOptions options) {
var strokeOptions_Size = options.Size ?? 16;
var strokeOptions_Thinning = options.Thinning ?? 0.5;
var strokeOptions_Smoothing = options.Smoothing ?? 0.5;
var strokeOptions_SimulatePressure = options.SimulatePressure ?? true;
Func<double,double> strokeOptions_Easing = (t) => t;
var strokeOptions_Start = options.Start;
var strokeOptions_End = options.End;
var isComplete = options.Last ?? false;
var capStart = strokeOptions_Start != null ? strokeOptions_Start.Cap : true;
Func<double, double> taperStartEase = strokeOptions_Start != null
? strokeOptions_Start.Easing
: (s) => s * (2 - s);
var capEnd = strokeOptions_End != null ? strokeOptions_End.Cap : true;
Func<double, double> taperEndEase = strokeOptions_End != null
? strokeOptions_End.Easing
: (t) => --t * t * t + 1;
// We can't do anything with an empty array or a stroke with negative size.
if (points.Length == 0 || strokeOptions_Size <= 0) {
return new Vector[] { };
}
// The total length of the line
var totalLength = points[points.Length - 1].RunningLength;
var taperStart = strokeOptions_Start != null ? strokeOptions_Start.IsTaper == false ? 0 :
strokeOptions_Start.IsTaper ? Math.Max(strokeOptions_Size, totalLength) :
strokeOptions_Start.Taper != null ? (double)strokeOptions_Start.Taper : 0 : Double.NaN;
var taperEnd = strokeOptions_End != null ? strokeOptions_End.IsTaper == false ? 0 :
strokeOptions_End.IsTaper ? Math.Max(strokeOptions_Size, totalLength) :
strokeOptions_End.Taper != null ? (double)strokeOptions_End.Taper : 0 : Double.NaN;
// The minimum allowed distance between points (squared)
var minDistance = Math.Pow(strokeOptions_Size * strokeOptions_Smoothing, 2);
// Our collected left and right points
var leftPts = new List<Vector>();
var rightPts = new List<Vector>();
// Previous pressure (start with average of first five pressures,
// in order to prevent fat starts for every line. Drawn lines
// almost always start slow!
var _prevPressure_arrseg = new ArraySegment<StrokePoint>(points, 0, 10);
var prevPressure = _prevPressure_arrseg.Aggregate(points[0].Pressure,
(acc, curr) => {
var pressure = curr.Pressure;
if (strokeOptions_SimulatePressure) {
// Speed of change - how fast should the the pressure changing?
var sp = Math.Min(1, curr.Distance / strokeOptions_Size);
// Rate of change - how much of a change is there?
var rp = Math.Min(1, 1 - sp);
pressure = Math.Min(1, acc + (rp - acc) * (sp * RATE_OF_PRESSURE_CHANGE));
}
return (acc + pressure) / 2;
});
// The current radius
var radius = GetStrokeRadius(strokeOptions_Size, strokeOptions_Thinning, points[points.Length - 1].Pressure,
strokeOptions_Easing);
// The radius of the first saved point
double firstRadius = Double.NaN;
// Previous vector
var prevVector = points[0].Vector;
// Previous left and right points
var pl = points[0].Point;
var pr = pl;
// Temporary left and right points
var tl = pl;
var tr = pr;
// Keep track of whether the previous point is a sharp corner
// ... so that we don't detect the same corner twice
var isPrevPointSharpCorner = false;
/*
Find the outline's left and right points
Iterating through the points and populate the rightPts and leftPts arrays,
skipping the first and last pointsm, which will get caps later on.
*/
foreach (var _sp in points) {
var pressure = _sp.Pressure;
var point = _sp.Point;
var vector = _sp.Vector;
var distance = _sp.Distance;
var runningLength = _sp.RunningLength;
var i = Array.IndexOf<StrokePoint>(points, _sp);
// Removes noise from the end of the line
if (i < points.Length - 1 && totalLength - runningLength < 3) {
continue;
}
/*
Calculate the radius
If not thinning, the current point's radius will be half the size; or
otherwise, the size will be based on the current (real or simulated)
pressure.
*/
if (strokeOptions_Thinning == Double.NaN) {
if (strokeOptions_SimulatePressure) {
// If we're simulating pressure, then do so based on the distance
// between the current point and the previous point, and the size
// of the stroke. Otherwise, use the input pressure.
var sp = Math.Min(1, distance / strokeOptions_Size);
var rp = Math.Min(1, 1 - sp);
pressure = Math.Min(1, prevPressure + (rp - prevPressure) * (sp * RATE_OF_PRESSURE_CHANGE));
}
radius = GetStrokeRadius(strokeOptions_Size, strokeOptions_Thinning, pressure,
strokeOptions_Easing);
} else {
radius = strokeOptions_Size / 2;
}
if (firstRadius == Double.NaN) firstRadius = radius;
/*
Apply tapering
If the current length is within the taper distance at either the
start or the end, calculate the taper strengths. Apply the smaller
of the two taper strengths to the radius.
*/
var ts = runningLength < taperStart ? taperStartEase(runningLength / taperStart) : 1;
var te = runningLength < taperEnd ? taperEndEase(runningLength / taperEnd) : 1;
radius = Math.Max(0.01, radius * Math.Min(ts, te));
/* Add points to left and right */
/*
Handle sharp corners
Find the difference (dot product) between the current and next vector.
If the next vector is at more than a right angle to the current vector,
draw a cap at the current point.
*/
var nextVector = (i < points.Length - 1 ? points[i + 1] : points[i]).Vector;
var nextDpr = i < points.Length - 1 ? Vector.DotVectors(vector, nextVector) : 1.0;
var prevDpr = Vector.DotVectors(vector, prevVector);
var isPointSharpCorner = prevDpr < 0 && !isPrevPointSharpCorner;
var isNextPointSharpCorner = nextDpr < 0;
if (isPointSharpCorner || isNextPointSharpCorner) {
// It's a sharp corner. Draw a rounded cap and move on to the next point
// Considering saving these and drawing them later? So that we can avoid
// crossing future points.
var offset = Vector.MultiplyVector(Vector.PerpendicularRotationVector(prevVector), radius);
double step = 1D / 13D;
double t = 0D;
for (; t <= 1D; t += step) {
tl = Vector.RotateVectors(Vector.SubtractVectors(point, offset), point, FIXED_PI * t);
leftPts.Add(tl);
tr = Vector.RotateVectors(Vector.AddVectors(point, offset), point, FIXED_PI * -t);
rightPts.Add(tr);
}
pl = tl;
pr = tr;
if (isNextPointSharpCorner) {
isPrevPointSharpCorner = true;
}
continue;
}
isPrevPointSharpCorner = false;
// Handle the last point
if (i == points.Length - 1) {
var offset = Vector.MultiplyVector(Vector.PerpendicularRotationVector(vector), radius);
leftPts.Add(Vector.SubtractVectors(point, offset));
rightPts.Add(Vector.AddVectors(point, offset));
continue;
}
/*
Add regular points
Project points to either side of the current point, using the
calculated size as a distance. If a point's distance to the
previous point on that side greater than the minimum distance
(or if the corner is kinda sharp), add the points to the side's
points array.
*/
Vector _offset =
Vector.MultiplyVector(
Vector.PerpendicularRotationVector(Vector.InterpolateVectors(nextVector, vector, nextDpr)),
radius);
tl = Vector.SubtractVectors(point, _offset);
if (i <= 1 || Vector.DistLengthSquaredVectors(pl, tl) > minDistance) {
leftPts.Add(tl);
pl = tl;
}
tr = Vector.AddVectors(point, _offset);
if (i <= 1 || Vector.DistLengthSquaredVectors(pr, tr) > minDistance) {
rightPts.Add(tr);
pr = tr;
}
// Set variables for next iteration
prevPressure = pressure;
prevVector = vector;
}
/*
Drawing caps
Now that we have our points on either side of the line, we need to
draw caps at the start and end. Tapered lines don't have caps, but
may have dots for very short lines.
*/
var firstPoint = points[0].Point;
var lastPoint = points.Length > 1
? points[points.Length - 1].Point
: Vector.AddVectors(points[0].Point, new Vector(1, 1));
var startCap = new List<Vector>();
var endCap = new List<Vector>();
/*
Draw a dot for very short or completed strokes
If the line is too short to gather left or right points and if the line is
not tapered on either side, draw a dot. If the line is tapered, then only
draw a dot if the line is both very short and complete. If we draw a dot,
we can just return those points.
*/
if (points.Length == 1) {
if (!((strokeOptions_Start != null && (taperStart != 0 && strokeOptions_Start.IsTaper)) || (strokeOptions_End != null &&
(taperEnd != 0 && strokeOptions_End.IsTaper))) || isComplete) {
var start = Vector.ProjectVectors(firstPoint,
Vector.UnitVector(
Vector.PerpendicularRotationVector(Vector.SubtractVectors(firstPoint, lastPoint))), !double.IsNaN(firstRadius) ? -firstRadius : -radius);
var dotPts = new List<Vector>();
double step = 1D / 13D;
double t = step;
for (; t <= 1D; t += step) {
dotPts.Add(Vector.RotateVectors(start, firstPoint, FIXED_PI * 2 * t));
}
return dotPts.ToArray();
}
} else {
/*
Draw a start cap
Unless the line has a tapered start, or unless the line has a tapered end
and the line is very short, draw a start cap around the first point. Use
the distance between the second left and right point for the cap's radius.
Finally remove the first left and right points. :psyduck:
*/
if ((strokeOptions_Start != null && (taperStart != 0 && strokeOptions_Start.IsTaper)) || ((strokeOptions_End != null &&
(taperEnd != 0 && strokeOptions_End.IsTaper)) && points.Length == 1)) {
// The start point is tapered, noop
} else if (capStart) {
// Draw the round cap - add thirteen points rotating the right point around the start point to the left point
double step = 1D / 13D;
double t = step;
for (; t <= 1D; t += step) {
var pt = Vector.RotateVectors(rightPts[0], firstPoint, FIXED_PI * t);
startCap.Add(pt);
}
} else {
// Draw the flat cap - add a point to the left and right of the start point
var cornersVector = Vector.SubtractVectors(leftPts[0], rightPts[0]);
var offsetA = Vector.MultiplyVector(cornersVector, 0.5);
var offsetB = Vector.MultiplyVector(cornersVector, 0.51);
startCap.Add(Vector.SubtractVectors(firstPoint, offsetA));
startCap.Add(Vector.SubtractVectors(firstPoint, offsetB));
startCap.Add(Vector.AddVectors(firstPoint, offsetA));
startCap.Add(Vector.AddVectors(firstPoint, offsetB));
}
/*
Draw an end cap
If the line does not have a tapered end, and unless the line has a tapered
start and the line is very short, draw a cap around the last point. Finally,
remove the last left and right points. Otherwise, add the last point. Note
that This cap is a full-turn-and-a-half: this prevents incorrect caps on
sharp end turns.
*/
var direction =
Vector.PerpendicularRotationVector(Vector.NegateVector(points[points.Length - 1].Vector));
if ((strokeOptions_End != null &&
(taperEnd != 0 && strokeOptions_End.IsTaper)) ||
((strokeOptions_Start != null && (taperStart != 0 && strokeOptions_Start.IsTaper)) && points.Length == 1)) {
// Tapered end - push the last point to the line
endCap.Add(lastPoint);
} else if (capEnd) {
// Draw the round end cap
var start = Vector.ProjectVectors(lastPoint, direction, radius);
double step = 1D / 29D;
double t = step;
for (; t < 1D; t += step) {
endCap.Add(Libraries.Vector.RotateVectors(start, lastPoint, FIXED_PI * 3 * t));
}
} else {
// Draw the flat end cap
endCap.Add(Vector.AddVectors(lastPoint, Vector.MultiplyVector(direction, radius)));
endCap.Add(Vector.AddVectors(lastPoint, Vector.MultiplyVector(direction, radius * 0.99)));
endCap.Add(Vector.SubtractVectors(lastPoint, Vector.MultiplyVector(direction, radius)));
endCap.Add(Vector.SubtractVectors(lastPoint, Vector.MultiplyVector(direction, radius * 0.99)));
}
}
/*
Return the points in the correct winding order: begin on the left side, then
continue around the end cap, then come back along the right side, and finally
complete the start cap.
*/
rightPts.Reverse();
return leftPts.Concat(endCap).Concat(rightPts).Concat(startCap).ToArray();
}
}
namespace Stroke {
public class StrokeOptions
{
/// <summary>
/// The base size (diameter) of the stroke.
/// </summary>
public double? Size { get; set; }
/// <summary>
/// The effect of pressure on the stroke's size.
/// </summary>
public double? Thinning { get; set; }
/// <summary>
/// How much to soften the stroke's edges.
/// </summary>
public double? Smoothing { get; set; }
public double? Streamline { get; set; }
/// <summary>
/// An easing function to apply to each point's pressure.
/// </summary>
public Func<double, double> Easing { get; set; }
/// <summary>
/// Whether to simulate pressure based on velocity.
/// </summary>
public bool? SimulatePressure { get; set; }
/// <summary>
/// Cap, taper and easing for the start of the line.
/// </summary>
public StrokeCapOptions Start { get; set; }
/// <summary>
/// Cap, taper and easing for the end of the line.
/// </summary>
public StrokeCapOptions End { get; set; }
/// <summary>
/// Whether to handle the points as a completed stroke.
/// </summary>
public bool? Last { get; set; }
}
public class StrokeCapOptions
{
/// <summary>
/// Whether to apply a cap at the start/end of the line.
/// </summary>
public bool Cap { get; set; }
/// <summary>
/// The taper value at the start/end of the line.
/// </summary>
public double Taper { get; set; }
public bool IsTaper { get; set; }
/// <summary>
/// An easing function to apply to the taper.
/// </summary>
public Func<double, double> Easing { get; set; }
}
public class StrokePoint
{
/// <summary>
/// The point coordinates as [x, y].
/// </summary>
public Vector Point { get; set; }
/// <summary>
/// The pressure at the point.
/// </summary>
public double Pressure { get; set; }
/// <summary>
/// The distance from the previous point.
/// </summary>
public double Distance { get; set; }
/// <summary>
/// The vector at the point.
/// </summary>
public Vector Vector { get; set; }
/// <summary>
/// The running length of the stroke.
/// </summary>
public double RunningLength { get; set; }
}
}
}
@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using InkCanvasForClassX.Libraries.Stroke;
using Jint;
using Jint.Native;
namespace InkCanvasForClassX.Libraries
{
public class PerfectFreehandJint {
public class StylusPointLite
{
public double x;
public double y;
public double pressure;
}
public class StrokeOptions
{
public double? size { get; set; }
public double? thinning { get; set; }
public double? smoothing { get; set; }
public double? streamline { get; set; }
public Func<double, double> easing { get; set; }
public bool? simulatePressure { get; set; }
public StrokeCapOptions start { get; set; }
public StrokeCapOptions end { get; set; }
public bool? last { get; set; }
}
public class StrokeCapOptions
{
public bool? cap { get; set; }
public double? taper { get; set; }
public Func<double, double> easing { get; set; }
}
public class StrokePoint
{
public Array point { get; set; }
public double pressure { get; set; }
public double distance { get; set; }
public Array vector { get; set; }
public double runningLength { get; set; }
}
public readonly Engine JintEngine = new Engine();
public PerfectFreehandJint() {
// perfect-freehand
JintEngine.Execute("\"use strict\";function neg(t){return[-t[0],-t[1]]}function add(t,n){return[t[0]+n[0],t[1]+n[1]]}function sub(t,n){return[t[0]-n[0],t[1]-n[1]]}function mul(t,n){return[t[0]*n,t[1]*n]}function div(t,n){return[t[0]/n,t[1]/n]}function per(t){return[t[1],-t[0]]}function dpr(t,n){return t[0]*n[0]+t[1]*n[1]}function isEqual(t,n){return t[0]===n[0]&&t[1]===n[1]}function len(t){return Math.hypot(t[0],t[1])}function len2(t){return t[0]*t[0]+t[1]*t[1]}function dist2(t,n){return len2(sub(t,n))}function uni(t){return div(t,len(t))}function dist(t,n){return Math.hypot(t[1]-n[1],t[0]-n[0])}function med(t,n){return mul(add(t,n),.5)}function rotAround(t,n,e){const r=Math.sin(e),u=Math.cos(e),i=t[0]-n[0],s=t[1]-n[1],o=i*r+s*u;return[i*u-s*r+n[0],o+n[1]]}function lrp(t,n,e){return add(t,mul(sub(n,t),e))}function prj(t,n,e){return add(t,mul(n,e))}function getStrokeRadius(t,n,e,r=(t=>t)){return t*r(.5-n*(.5-e))}function getStrokePoints(t,n={}){var e;const{streamline:r=.5,size:u=16,last:i=!1}=n;if(0===t.length)return[];const s=.15+.85*(1-r);let o=Array.isArray(t[0])?t:t.map((({x:t,y:n,pressure:e=.5})=>[t,n,e]));if(2===o.length){const t=o[1];o=o.slice(0,-1);for(let n=1;n<5;n++)o.push(lrp(o[0],t,n/4))}1===o.length&&(o=[...o,[...add(o[0],[1,1]),...o[0].slice(2)]]);const c=[{point:[o[0][0],o[0][1]],pressure:o[0][2]>=0?o[0][2]:.25,vector:[1,1],distance:0,runningLength:0}];let l=!1,p=0,a=c[0];const d=o.length-1;for(let t=1;t<o.length;t++){const n=i&&t===d?o[t].slice(0,2):lrp(a.point,o[t],s);if(isEqual(a.point,n))continue;const e=dist(n,a.point);if(p+=e,t<d&&!l){if(p<u)continue;l=!0}a={point:n,pressure:o[t][2]>=0?o[t][2]:.5,vector:uni(sub(a.point,n)),distance:e,runningLength:p},c.push(a)}return c[0].vector=(null===(e=c[1])||void 0===e?void 0:e.vector)||[0,0],c}const{min:min,PI:PI}=Math,RATE_OF_PRESSURE_CHANGE=.275,FIXED_PI=PI+1e-4;function getStrokeOutlinePoints(t,n={}){const{size:e=16,smoothing:r=.5,thinning:u=.5,simulatePressure:i=!0,easing:s=(t=>t),start:o={},end:c={},last:l=!1}=n,{cap:p=!0,easing:a=(t=>t*(2-t))}=o,{cap:d=!0,easing:h=(t=>--t*t*t+1)}=c;if(0===t.length||e<=0)return[];const f=t[t.length-1].runningLength,g=!1===o.taper?0:!0===o.taper?Math.max(e,f):o.taper,m=!1===c.taper?0:!0===c.taper?Math.max(e,f):c.taper,E=Math.pow(e*r,2),P=[],v=[];let I,_=t.slice(0,10).reduce(((t,n)=>{let r=n.pressure;if(i){const u=min(1,n.distance/e),i=min(1,1-u);r=min(1,t+u*RATE_OF_PRESSURE_CHANGE*(i-t))}return(t+r)/2}),t[0].pressure),A=getStrokeRadius(e,u,t[t.length-1].pressure,s),S=t[0].vector,b=t[0].point,R=b,M=b,F=R,k=!1;for(let n=0;n<t.length;n++){let{pressure:r}=t[n];const{point:o,vector:c,distance:l,runningLength:p}=t[n];if(n<t.length-1&&f-p<3)continue;if(u){if(i){const t=min(1,l/e),n=min(1,1-t);r=min(1,_+t*RATE_OF_PRESSURE_CHANGE*(n-_))}A=getStrokeRadius(e,u,r,s)}else A=e/2;void 0===I&&(I=A);const d=p<g?a(p/g):1,D=f-p<m?h((f-p)/m):1;A=Math.max(.01,A*Math.min(d,D));const X=(n<t.length-1?t[n+1]:t[n]).vector,y=n<t.length-1?dpr(c,X):1,O=null!==y&&y<0;if(dpr(c,S)<0&&!k||O){const t=mul(per(S),A);for(let n=1/13,e=0;e<=1;e+=n)M=rotAround(sub(o,t),o,FIXED_PI*e),P.push(M),F=rotAround(add(o,t),o,FIXED_PI*-e),v.push(F);b=M,R=F,O&&(k=!0);continue}if(k=!1,n===t.length-1){const t=mul(per(c),A);P.push(sub(o,t)),v.push(add(o,t));continue}const x=mul(per(lrp(X,c,y)),A);M=sub(o,x),(n<=1||dist2(b,M)>E)&&(P.push(M),b=M),F=add(o,x),(n<=1||dist2(R,F)>E)&&(v.push(F),R=F),_=r,S=c}const D=t[0].point.slice(0,2),X=t.length>1?t[t.length-1].point.slice(0,2):add(t[0].point,[1,1]),y=[],O=[];if(1===t.length){if(!g&&!m||l){const t=prj(D,uni(per(sub(D,X))),-(I||A)),n=[];for(let e=1/13,r=e;r<=1;r+=e)n.push(rotAround(t,D,2*FIXED_PI*r));return n}}else{if(g||m&&1===t.length);else if(p)for(let t=1/13,n=t;n<=1;n+=t){const t=rotAround(v[0],D,FIXED_PI*n);y.push(t)}else{const t=sub(P[0],v[0]),n=mul(t,.5),e=mul(t,.51);y.push(sub(D,n),sub(D,e),add(D,e),add(D,n))}const n=per(neg(t[t.length-1].vector));if(m||g&&1===t.length)O.push(X);else if(d){const t=prj(X,n,A);for(let n=1/29,e=n;e<1;e+=n)O.push(rotAround(t,X,3*FIXED_PI*e))}else O.push(add(X,mul(n,A)),add(X,mul(n,.99*A)),sub(X,mul(n,.99*A)),sub(X,mul(n,A)))}return P.concat(O,v.reverse(),y)}function getStroke(t,n={}){return getStrokeOutlinePoints(getStrokePoints(t,n),n)}");
// getSvgPathFromStroke
JintEngine.Execute(
"\"use strict\";const average=(e,t)=>(e+t)/2;function getSvgPathFromStroke(e,t=!0){const o=e.length;if(o<4)return\"\";let r=e[0],a=e[1];const i=e[2];let F=`M${r[0].toFixed(2)},${r[1].toFixed(2)} Q${a[0].toFixed(2)},${a[1].toFixed(2)} ${average(a[0],i[0]).toFixed(2)},${average(a[1],i[1]).toFixed(2)} T`;for(let t=2,i=o-1;t<i;t++)r=e[t],a=e[t+1],F+=`${average(r[0],a[0]).toFixed(2)},${average(r[1],a[1]).toFixed(2)} `;return t&&(F+=\"Z\"),F}");
// getSvgPathStroke
JintEngine.Execute("function getSvgPathStroke(points,options,closed=true){return getSvgPathFromStroke(getStroke(points,options),closed)}");
}
public JsArray GetStroke(StylusPointLite[] points, StrokeOptions options ) {
return (JsArray)JintEngine.Invoke("getStrokePoints", points, options);
}
public string GetSVGPathStroke(StylusPointLite[] points, StrokeOptions options, bool closed = true) {
return ((JsString)JintEngine.Invoke("getSvgPathStroke", points, options, true)).ToString();
}
public Geometry GetGeometryStroke(StylusPointLite[] points, StrokeOptions options, bool closed = true) {
return Geometry.Parse("F1 "+((JsString)JintEngine.Invoke("getSvgPathStroke", points, options, true)).ToString());
}
}
}
+304
View File
@@ -0,0 +1,304 @@
using System;
namespace InkCanvasForClassX.Libraries
{
/// <summary>
/// 提供了對平面向量的坐標運算
/// </summary>
public class Vector {
private double _x = 0;
private double _y = 0;
public Vector(double x = 0, double y = 0) {
_x = x;
_y = y;
}
public double X {
get => _x;
set => _x = value;
}
public double Y {
get => _y;
set => _y = value;
}
/// <summary>
/// 將該向量改變為其相反向量
/// </summary>
public Vector Negate() {
_x = -_x;
_y = -_y;
return this;
}
/// <summary>
/// 提供一個<c>Vector</c>,返回該<c>Vector</c>的相反向量
/// </summary>
public static Vector NegateVector(Vector vec) {
return new Vector(-vec.X, -vec.Y);
}
/// <summary>
/// Csharp中的<c>Math.Hypot</c>實現
/// </summary>
private static double Hypot(params double[] values)
{
double sum = 0;
foreach (var value in values) {
sum += Math.Pow(value, 2);
}
return Math.Sqrt(sum);
}
/// <summary>
/// 獲取該向量的長度
/// </summary>
public double Length => Vector.Hypot(_x,_y);
/// <summary>
/// 獲取該向量的未開平方的長度
/// </summary>
public double LengthSquared => _x * _x + _y * _y;
/// <summary>
/// 將該向量和另一個向量相加
/// </summary>
public Vector Add(Vector vec)
{
_x = _x + vec.X;
_y = _y + vec.Y;
return this;
}
/// <summary>
/// 提供兩個<c>Vector</c>,返回相加後的<c>Vector</c>
/// </summary>
public static Vector AddVectors(Vector vec1, Vector vec2)
{
return new Vector(vec1.X + vec2.X, vec1.Y + vec2.Y);
}
/// <summary>
/// 將該向量減去另一個向量
/// </summary>
public Vector Subtract(Vector vec)
{
_x = _x - vec.X;
_y = _y - vec.Y;
return this;
}
/// <summary>
/// 提供兩個<c>Vector</c>,返回<c><paramref name="vec1"/>-<paramref name="vec2"/></c>後的<c>Vector</c>
/// </summary>
public static Vector SubtractVectors(Vector vec1, Vector vec2)
{
return new Vector(vec1.X - vec2.X, vec1.Y - vec2.Y);
}
/// <summary>
/// 將該向量除以一個數值
/// </summary>
public Vector DivideBy(double n)
{
_x = _x / n;
_y = _y / n;
return this;
}
/// <summary>
/// 提供兩個<c>Vector</c>,返回<c><paramref name="vec"/>/<paramref name="n"/></c>後的<c>Vector</c>
/// </summary>
public static Vector DivideByVector(Vector vec, double n)
{
return new Vector(vec.X / n, vec.Y / n);
}
/// <summary>
/// 將該向量乘以一個數值
/// </summary>
public Vector Multiply(double n)
{
_x = _x * n;
_y = _y * n;
return this;
}
/// <summary>
/// 提供兩個<c>Vector</c>,返回<c><paramref name="vec"/>*<paramref name="n"/></c>後的<c>Vector</c>
/// </summary>
public static Vector MultiplyVector(Vector vec, double n)
{
return new Vector(vec.X * n, vec.Y * n);
}
/// <summary>
/// 將該向量垂直旋轉
/// </summary>
public Vector PerpendicularRotation() {
var _t = _x;
_x = _y;
_y = - _t;
return this;
}
/// <summary>
/// 提供<c>Vector</c>,返回垂直旋轉後的<c>Vector</c>
/// </summary>
public static Vector PerpendicularRotationVector(Vector vec)
{
return new Vector(vec.Y, - vec.X);
}
/// <summary>
/// 提供兩個<c>Vector</c>,返回兩個<c>Vector</c>點乘後的数值
/// </summary>
public static double DotVectors(Vector vec1, Vector vec2)
{
return vec1.X * vec2.X + vec1.Y * vec2.Y;
}
/// <summary>
/// 判斷該向量和另外一個向量是否相等
/// </summary>
public bool IsEqual(Vector vec)
{
return Math.Abs(_x - vec.X) < 0.01 && Math.Abs(_y - vec.Y) < 0.01;
}
/// <summary>
/// 獲取該向量和另一個向量的平方距離
/// </summary>
public double DistLengthSquared(Vector vec) {
var subVec = Vector.SubtractVectors(this, vec);
return subVec.LengthSquared;
}
/// <summary>
/// 獲取一個向量和另一個向量的平方距離
/// </summary>
public static double DistLengthSquaredVectors(Vector vec1, Vector vec2)
{
var subVec = Vector.SubtractVectors(vec1, vec2);
return subVec.LengthSquared;
}
/// <summary>
/// 獲取該向量和另一個向量的距離
/// </summary>
public double DistLength(Vector vec) {
return Vector.Hypot(this._y - vec.Y, this._x - vec.X);
}
/// <summary>
/// 獲取一個向量和另一個向量的距離
/// </summary>
public static double DistLengthVectors(Vector vec1, Vector vec2)
{
return Vector.Hypot(vec1.Y - vec2.Y, vec1.X - vec2.X);
}
/// <summary>
/// 將該向量修改為其中間向量
/// </summary>
public Vector Med(Vector vec) {
var addVec = new Vector(_x, _y).Add(vec);
addVec.Multiply(0.5);
_x = addVec.X;
_y = addVec.Y;
return this;
}
/// <summary>
/// 獲取一個向量和另一個向量的中間向量
/// </summary>
public static Vector MedVectors(Vector vec1, Vector vec2) {
var addVec = Vector.AddVectors(vec1, vec2);
return addVec.Multiply(0.5);
}
/// <summary>
/// 將該向量圍繞另一個向量旋轉r弧度
/// </summary>
public Vector Rotate(Vector vec, double r) {
var sin = Math.Sin(r);
var cos = Math.Cos(r);
var px = _x - vec.X;
var py = _y - vec.Y;
var nx = px * cos - py * sin;
var ny = px * sin + py * cos;
_x = nx + vec.X;
_y = ny + vec.Y;
return this;
}
/// <summary>
/// 將一個向量圍繞另一個向量旋轉r弧度
/// </summary>
public static Vector RotateVectors(Vector vec1, Vector vec2, double r)
{
var sin = Math.Sin(r);
var cos = Math.Cos(r);
var px = vec1.X - vec2.X;
var py = vec1.Y - vec2.Y;
var nx = px * cos - py * sin;
var ny = px * sin + py * cos;
return new Vector(nx + vec2.X, ny + vec2.Y);
}
/// <summary>
/// 將該向量與另一個向量插值
/// </summary>
public Vector Interpolate(Vector vec, double t) {
Add(SubtractVectors(vec, this).Multiply(t));
return this;
}
/// <summary>
/// 將一個向量與另一個向量插值
/// </summary>
public static Vector InterpolateVectors(Vector vec1, Vector vec2, double t) {
return AddVectors(vec1, SubtractVectors(vec2, vec1).Multiply(t));
}
/// <summary>
/// 將該向量投影在向量<paramref name="vec"/>的方向上,並附加距離<paramref name="c"/>
/// </summary>
public Vector Project(Vector vec, double c) {
Add(vec.Multiply(c));
return this;
}
/// <summary>
/// 將<paramref name="vec1"/>投影在向量<paramref name="vec2"/>的方向上,並附加距離<paramref name="c"/>
/// </summary>
public static Vector ProjectVectors(Vector vec1, Vector vec2, double c) {
return AddVectors(vec1, MultiplyVector(vec2,c));
}
/// <summary>
/// 取得單位向量
/// </summary>
public Vector Unit()
{
return DivideByVector(this, Length);
}
/// <summary>
/// 取得單位向量
/// </summary>
public static Vector UnitVector(Vector vec)
{
return DivideByVector(vec, vec.Length);
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<Window x:Class="InkCanvasForClassX.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:InkCanvasForClassX"
xmlns:libraries="clr-namespace:InkCanvasForClassX.Libraries"
mc:Ignorable="d"
Title="MainWindow" Height="1300" Width="1300">
<Grid>
<libraries:InkCanvas x:Name="inkCanvas" Width="600" Height="600" Margin="0,0,0,600"/>
<!--<libraries:InkCanvas InkStrokes="{Binding ElementName=InkC, Path=Strokes}" x:Name="inkCanvas" Width="600" Height="600" Margin="0,0,0,600"/>-->
<InkCanvas Height="600" Width="600" Name="InkC" Margin="0,600,0,0" Background="Wheat"></InkCanvas>
<StackPanel>
<Button Click="ButtonBase1_OnClick">Change to None</Button>
<Button Click="ButtonBase2_OnClick">Change to Pen</Button>
</StackPanel>
</Grid>
</Window>
+152
View File
@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using InkCanvasForClassX.Libraries;
using InkCanvasForClassX.Libraries.Stroke;
namespace InkCanvasForClassX
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public static StylusPointCollection GenerateStylusPoints(int numberOfPoints, int maxX, int maxY)
{
Random random = new Random();
StylusPointCollection points = new StylusPointCollection();
for (int i = 0; i < numberOfPoints; i++)
{
double x = random.NextDouble() * maxX;
double y = random.NextDouble() * maxY;
float pressureFactor = 0.5F;
StylusPoint point = new StylusPoint(x, y, pressureFactor);
points.Add(point);
}
return points;
}
private bool isDragging = false;
private Point startPoint;
public MainWindow()
{
InitializeComponent();
InkC.StrokeCollected += (object sender, InkCanvasStrokeCollectedEventArgs e) => {
inkCanvas.InkStrokes = InkC.Strokes;
//var stylusPtsList = new List<PerfectFreehandJint.StylusPointLite>();
//foreach (var strokeStylusPoint in e.Stroke.StylusPoints) {
// stylusPtsList.Add(new PerfectFreehandJint.StylusPointLite()
// {
// x = Math.Round(strokeStylusPoint.X,2) ,
// y = Math.Round(strokeStylusPoint.Y,2),
// pressure = strokeStylusPoint.PressureFactor,
// });
//}
//var aaa = new PerfectFreehandJint();
//var ccc = aaa.GetSVGPathStroke(stylusPtsList.ToArray(), new PerfectFreehandJint.StrokeOptions() {
// size = 16,
// thinning = 0.5,
// smoothing = 0.5,
// streamline = 0.5,
// simulatePressure = true,
// easing = (t)=>t,
// last = true,
// start = new PerfectFreehandJint.StrokeCapOptions() {
// cap = true,
// taper = 0,
// easing = (t)=>t,
// },
// end = new PerfectFreehandJint.StrokeCapOptions()
// {
// cap = true,
// taper = 0,
// easing = (t) => t,
// },
//});
//Trace.WriteLine(ccc);
};
InkC.MouseRightButtonDown += Inkcanv_MouseRightButtonDown;
InkC.MouseRightButtonUp += Inkcanv_MouseRightButtonUp;
InkC.MouseMove += Inkcanv_MouseMove;
var a = GenerateStylusPoints(10, 1920, 1080);
foreach (var strokePoint in a)
{
Trace.WriteLine($"point x:{strokePoint.X} point y:{strokePoint.Y}");
}
var c = PerfectFreehand.GetStrokePoints(a, new StrokeOptions() {
Size = 16,
Thinning = 0.5,
Smoothing = 0.5,
SimulatePressure = true,
});
var s = PerfectFreehand.GetStrokeOutlinePointsVectors(c, new StrokeOptions() {
Size = 16,
Thinning = 0.5,
Smoothing = 0.5,
SimulatePressure = true,
});
foreach (var strokePoint in c) {
Trace.WriteLine($"point x:{strokePoint.Vector.X.ToString()} point y:{strokePoint.Vector.Y.ToString()}");
}
Trace.WriteLine(PerfectFreehand.ConvertVectorsToSVGPath(s));
}
private void Inkcanv_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
isDragging = true;
startPoint = e.GetPosition(InkC);
InkC.CaptureMouse();
}
private void Inkcanv_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
isDragging = false;
InkC.ReleaseMouseCapture();
}
private void Inkcanv_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging && e.RightButton == MouseButtonState.Pressed)
{
Point currentPoint = e.GetPosition(InkC);
System.Windows.Vector delta = currentPoint - startPoint;
foreach (Stroke stroke in InkC.Strokes)
{
stroke.Transform(new Matrix(1, 0, 0, 1, delta.X, delta.Y), false);
}
inkCanvas.InkStrokes = InkC.Strokes;
startPoint = currentPoint;
}
}
private void ButtonBase1_OnClick(object sender, RoutedEventArgs e) {
InkC.EditingMode = InkCanvasEditingMode.None;
}
private void ButtonBase2_OnClick(object sender, RoutedEventArgs e) {
InkC.EditingMode = InkCanvasEditingMode.Ink;
}
}
}
@@ -0,0 +1,55 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("InkCanvasForClassX")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("InkCanvasForClassX")]
[assembly: AssemblyCopyright("Copyright © 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
//若要开始生成可本地化的应用程序,请设置
//.csproj 文件中的 <UICulture>CultureYouAreCodingWith</UICulture>
//在 <PropertyGroup> 中。例如,如果你使用的是美国英语。
//使用的是美国英语,请将 <UICulture> 设置为 en-US。 然后取消
//对以下 NeutralResourceLanguage 特性的注释。 更新
//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //主题特定资源词典所处位置
//(未在页面中找到资源时使用,
//或应用程序资源字典中找到时使用)
ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
//(未在页面中找到资源时使用,
//、应用程序或任何主题专用资源字典中找到时使用)
)]
// 程序集的版本信息由下列四个值组成:
//
// 主版本
// 次版本
// 生成号
// 修订号
//
//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
//通过使用 "*",如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
+71
View File
@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本: 4.0.30319.42000
//
// 对此文件的更改可能导致不正确的行为,如果
// 重新生成代码,则所做更改将丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace InkCanvasForClassX.Properties
{
/// <summary>
/// 强类型资源类,用于查找本地化字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// 返回此类使用的缓存 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("InkCanvasForClassX.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+30
View File
@@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace InkCanvasForClassX.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
}
}
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>