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
@@ -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);
}
}
}