alpha
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
Reference in New Issue
Block a user