Last active
October 8, 2019 01:49
-
-
Save johnsogg/2bab311ea798fd301084 to your computer and use it in GitHub Desktop.
Demonstration of GetPathForGlyph and CGPath in Xamarin.iOS for getting a vector representation of character glyphs. Includes quadratic and cubic Bezier functions.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Drawing; | |
using MonoTouch.CoreGraphics; | |
using MonoTouch.CoreText; | |
namespace Mystuff | |
{ | |
public class FontPathHelper | |
{ | |
char m_char; | |
CTFont m_font; | |
public FontPathHelper(char c, CTFont f) | |
{ | |
m_char = c; | |
m_font = f; | |
} | |
public CGPath GetPath() | |
{ | |
ushort[] glyphs = new ushort[1]; | |
m_font.GetGlyphsForCharacters(new char[] { m_char }, glyphs); // loads up 'glyphs' | |
CGPath path = m_font.GetPathForGlyph(glyphs[0]); | |
return path; | |
} | |
/// <summary> | |
/// Creates a 'flattened path' for the given character and font. This implementation is | |
/// just to demonstrate how to create a CGPath, how to traverse it, and how to create a | |
/// piecewise linear approximation. | |
/// | |
/// Note that CGPath might contain several sub-paths. This is because many characters | |
/// have holes or non-contiguous diacriticals (e.g. the dot atop a lower-case i). | |
/// | |
/// The tricky part that this does not address is how to sample the curved parts (quadratic | |
/// and cubic Bezier path elements). In this implementation we're just taking ten steps, | |
/// regardless of how big it is. In a real application (e.g. drawing to screen or generating | |
/// STL output for 3D printers) we'd probably use a fancier algorithm to collect fewer | |
/// points depending on the needed resolution. Something like this (I haven't tried it): | |
/// | |
/// http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm | |
/// Ramer-Douglas-Peucker algorithm | |
/// </summary> | |
public List<List<PointF>> FlattenedPath() | |
{ | |
// Using the CGPath from GetPath(), create a piecewise linear representation of each | |
// sub-path. The returned list describes each subpath, which is in turn composed of | |
// individual points. | |
List<List<PointF>> ret = new List<List<PointF>>(); | |
CGPath path = GetPath(); | |
List<PointF> segment = null; // holds current segment | |
PointF currentEndpoint; // holds most recently recorded point | |
int numSteps = 10; // don't go overboard. | |
path.Apply(((CGPathElement element) => | |
{ | |
// Console.WriteLine("\t\tType: {0}", element.Type); | |
// Console.WriteLine ("\t\tPoint 1: {0}", element.Point1); | |
// Console.WriteLine ("\t\tPoint 2: {0}", element.Point2); | |
// Console.WriteLine ("\t\tPoint 3: {0}", element.Point3); | |
switch (element.Type) | |
{ | |
case CGPathElementType.MoveToPoint: | |
segment = new List<PointF>(); | |
ret.Add(segment); | |
currentEndpoint = RecordPoint(segment, element.Point1); | |
break; | |
case CGPathElementType.AddLineToPoint: | |
currentEndpoint = RecordPoint(segment, element.Point1); | |
break; | |
case CGPathElementType.AddQuadCurveToPoint: | |
currentEndpoint = RecordQuadraticPath(segment, numSteps, currentEndpoint, element.Point1, element.Point2); | |
break; | |
case CGPathElementType.AddCurveToPoint: | |
currentEndpoint = RecordCubicPath(segment, numSteps, currentEndpoint, element.Point1, element.Point2, element.Point3); | |
break; | |
case CGPathElementType.CloseSubpath: | |
segment = null; // ensures error unless next element is a MoveToPoint | |
break; | |
} | |
})); | |
return ret; | |
} | |
static PointF RecordPoint(List<PointF> segment, PointF src) | |
{ | |
PointF ret = new PointF(src.X, src.Y); | |
segment.Add(ret); | |
return ret; | |
} | |
static PointF RecordQuadraticPath(List<PointF> segment, int n, PointF p0, PointF p1, PointF p2) | |
{ | |
// records points using a quadratic bezier with t in (0..1], with n steps. | |
PointF pt; | |
for (int i=1; i <= n; i++) // Do not want t=0, so start at i=1. | |
{ | |
float t = ((float) i) / ((float) n); | |
pt = BezierQuad(t, p0, p1, p2); | |
segment.Add(pt); | |
} | |
return pt; | |
} | |
static PointF BezierQuad(float t, PointF p0, PointF p1, PointF p2) | |
{ | |
float one_minus_t = (1f - t); | |
float one_minus_t_sq = one_minus_t * one_minus_t; | |
float t_sq = t * t; | |
float term0 = one_minus_t_sq; | |
float term1 = 2f * one_minus_t * t; | |
float term2 = t_sq; | |
float x = term0 * p0.X + term1 * p1.X + term2 * p2.X; | |
float y = term0 * p0.Y + term1 * p1.Y + term2 * p2.Y; | |
// Console.WriteLine("Bezier Quad, t={0}: {1}, {2}", t.ToString("N2"), x.ToString("N2"), y.ToString("N2")); | |
return new PointF(x, y); | |
} | |
static PointF RecordCubicPath(List<PointF> segment, int n, PointF p0, PointF p1, PointF p2, PointF p3) | |
{ | |
// records points using a cubic bezier with t in (0..1], with n steps. | |
PointF pt; | |
for (int i=1; i <= n; i++) // Do not want t=0, so start at i=1. | |
{ | |
float t = ((float) i) / ((float) n); | |
pt = BezierCubic(t, p0, p1, p2, p3); | |
segment.Add(pt); | |
} | |
return pt; | |
} | |
static PointF BezierCubic(float t, PointF p0, PointF p1, PointF p2, PointF p3) | |
{ | |
float one_minus_t = 1f - t; | |
float one_minus_t_sq = one_minus_t * one_minus_t; | |
float one_minus_t_cb = one_minus_t * one_minus_t * one_minus_t; | |
float t_sq = t * t; | |
float t_cb = t * t * t; | |
float term0 = one_minus_t_cb; | |
float term1 = 3f * one_minus_t_sq * t; | |
float term2 = 3f * one_minus_t * t_sq; | |
float term3 = t_cb; | |
float x = term0 * p0.X + term1 * p1.X + term2 * p2.X + term3 * p3.X; | |
float y = term0 * p0.Y + term1 * p1.Y + term2 * p2.Y + term3 * p3.Y; | |
// Console.WriteLine("Bezier Cubic, t={0}: {1}, {2}", t.ToString("N2"), x.ToString("N2"), y.ToString("N2")); | |
return new PointF(x, y); | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment