FontUtils library
@author zz85 / http://www.lab4games.net/zz85/blog @author alteredq / http://alteredqualia.com/
For Text operations in three.js (See TextGeometry)
It uses techniques used in:
typeface.js and canvastext For converting fonts and rendering with javascript http://typeface.neocracy.org
Triangulation ported from AS3 Simple Polygon Triangulation http://actionsnippet.com/?p=1462
A Method to triangulate shapes with holes http://www.sakri.net/blog/2009/06/12/an-approach-to-triangulating-polygons-with-holes/
Properties
var EPSILON #
This code is a quick port of code written in C++ which was submitted to flipcode.com by John W. Ratcliff // July 22, 2000 See original code and more information here: http://www.flipcode.com/archives/EfficientPolygonTriangulation.shtml
ported to actionscript by Zevan Rosser www.actionsnippet.com
ported to javascript by Joshua Koo http://www.lab4games.net/zz85/blog
var EPSILON = 0.0000000001
Functions
dynamic snip(contour, u, v, w, n, verts) #
snip( contour, u, v, w, n, verts ) {
var p;
var ax, ay, bx, by;
var cx, cy, px, py;
ax = contour[ verts[ u ] ].x;
ay = contour[ verts[ u ] ].y;
bx = contour[ verts[ v ] ].x;
by = contour[ verts[ v ] ].y;
cx = contour[ verts[ w ] ].x;
cy = contour[ verts[ w ] ].y;
if ( EPSILON > (((bx-ax)*(cy-ay)) - ((by-ay)*(cx-ax))) ) return false;
for ( p = 0; p < n; p++ ) {
if( (p == u) || (p == v) || (p == w) ) continue;
px = contour[ verts[ p ] ].x;
py = contour[ verts[ p ] ].y;
if ( insideTriangle( ax, ay, bx, by, cx, cy, px, py ) ) return false;
}
return true;
}
dynamic insideTriangle(ax, ay, bx, by, cx, cy, px, py) #
insideTriangle( ax, ay,
bx, by,
cx, cy,
px, py ) {
var aX, aY, bX, bY;
var cX, cY, apx, apy;
var bpx, bpy, cpx, cpy;
var cCROSSap, bCROSScp, aCROSSbp;
aX = cx - bx; aY = cy - by;
bX = ax - cx; bY = ay - cy;
cX = bx - ax; cY = by - ay;
apx= px -ax; apy= py - ay;
bpx= px - bx; bpy= py - by;
cpx= px - cx; cpy= py - cy;
aCROSSbp = aX*bpy - aY*bpx;
cCROSSap = cX*apy - cY*apx;
bCROSScp = bX*cpy - bY*cpx;
return ( (aCROSSbp >= 0.0) && (bCROSScp >= 0.0) && (cCROSSap >= 0.0) );
}
dynamic area(contour) #
area( contour ) {
var n = contour.length;
var a = 0.0;
for( var p = n - 1, q = 0; q < n; p = q++ ) {
a += contour[ p ].x * contour[ q ].y - contour[ q ].x * contour[ p ].y;
}
return a * 0.5;
}
List<List<Vector2>> process(List<Vector2> contour, bool indices) #
List<List<Vector2>> process( List<Vector2> contour, bool indices ) {
var n = contour.length;
if ( n < 3 ) return null;
var result = [],
verts = new List(n),
vertIndices = [];
/* we want a counter-clockwise polygon in verts */
num u, v, w;
if ( area( contour ) > 0.0 ) {
for ( v = 0; v < n; v++ ) verts[ v ] = v;
} else {
for ( v = 0; v < n; v++ ) verts[ v ] = ( n - 1 ) - v;
}
num nv = n;
/* remove nv - 2 vertices, creating 1 triangle every time */
var count = 2 * nv; /* error detection */
for( v = nv - 1; nv > 2; ) {
/* if we loop, it is probably a non-simple polygon */
if ( ( count-- ) <= 0 ) {
//** Triangulate: ERROR - probable bad polygon!
//throw ( "Warning, unable to triangulate polygon!" );
//return null;
// Sometimes warning is fine, especially polygons are triangulated in reverse.
print( "Warning, unable to triangulate polygon!" );
if ( indices ) return vertIndices;
return result;
}
/* three consecutive vertices in current polygon, <u,v,w> */
u = v; if ( nv <= u ) u = 0; /* previous */
v = u + 1; if ( nv <= v ) v = 0; /* new v */
w = v + 1; if ( nv <= w ) w = 0; /* next */
if ( snip( contour, u, v, w, nv, verts ) ) {
var a, b, c, s, t;
/* true names of the vertices */
a = verts[ u ];
b = verts[ v ];
c = verts[ w ];
/* output Triangle */
result.add( [ contour[ a ],
contour[ b ],
contour[ c ] ] );
vertIndices.addAll( [ verts[ u ], verts[ v ], verts[ w ] ] );
/* remove v from the remaining polygon */
s = v;
for( t = v + 1; t < nv; t++ ) {
verts[ s ] = verts[ t ];
s++;
}
nv--;
/* reset error detection counter */
count = 2 * nv;
}
}
if ( indices ) return vertIndices;
return result;
}
dynamic generateShapes(text, [int size = 100, int curveSegments = 4, String font = "helvetiker", String weight = "normal", String style = "normal"]) #
generateShapes( text, [ int size = 100,
int curveSegments = 4,
String font = "helvetiker",
String weight = "normal",
String style = "normal"] ) {
var face = _faces[font][weight][style];
if (_faces == null) {
face = new FontFace(size: size, divisions: curveSegments);
_faces[font][weight][style] = face;
}
_size = size;
_divisions = curveSegments;
_face = font;
_weight = weight;
_style = style;
// Get a Font data json object
var data = drawText( text );
var paths = data["paths"];
var shapes = [];
var pl = paths.length;
for ( var p = 0; p < pl; p ++ ) {
shapes.addAll(paths[ p ].toShapes() );
}
return shapes;
}
dynamic extractGlyphPoints(String c, face, scale, offset, path) #
extractGlyphPoints ( String c, face, scale, offset, path ) {
List<Vector2> pts = [];
var i, i2, divisions,
outline, action, length,
scaleX, scaleY,
x, y, cpx, cpy, cpx0, cpy0, cpx1, cpy1, cpx2, cpy2,
laste;
var glyph = face["glyphs"][ c ];
if (glyph == null) glyph = face["glyphs"][ '?' ];
if ( glyph == null ) return null;
if ( glyph["o"] != null) {
outline = glyph["_cachedOutline"];
if (outline == null) {
glyph["_cachedOutline"] = glyph["o"].split( ' ' );
outline = glyph["_cachedOutline"];
}
length = outline.length;
scaleX = scale;
scaleY = scale;
for ( i = 0; i < length; ) {
action = outline[ i ++ ];
//console.log( action );
switch( action ) {
case 'm':
// Move To
x = int.parse(outline[ i++ ]) * scaleX + offset;
y = int.parse(outline[ i++ ]) * scaleY;
path.moveTo( x, y );
break;
case 'l':
// Line To
x = int.parse(outline[ i++ ]) * scaleX + offset;
y = int.parse(outline[ i++ ]) * scaleY;
path.lineTo(x,y);
break;
case 'q':
// QuadraticCurveTo
cpx = int.parse(outline[ i++ ]) * scaleX + offset;
cpy = int.parse(outline[ i++ ]) * scaleY;
cpx1 = int.parse(outline[ i++ ]) * scaleX + offset;
cpy1 = int.parse(outline[ i++ ]) * scaleY;
path.quadraticCurveTo(cpx1, cpy1, cpx, cpy);
if (pts.length > 0) laste = pts[ pts.length - 1 ];
if ( laste != null ) {
cpx0 = laste.x;
cpy0 = laste.y;
for ( i2 = 1; i2 <= divisions; i2 ++ ) {
var t = i2 / divisions;
var tx = ShapeUtils.b2( t, cpx0, cpx1, cpx );
var ty = ShapeUtils.b2( t, cpy0, cpy1, cpy );
}
}
break;
case 'b':
// Cubic Bezier Curve
cpx = int.parse(outline[ i++ ]) * scaleX + offset;
cpy = int.parse(outline[ i++ ]) * scaleY;
cpx1 = int.parse(outline[ i++ ]) * scaleX + offset;
cpy1 = int.parse(outline[ i++ ]) * -scaleY;
cpx2 = int.parse(outline[ i++ ]) * scaleX + offset;
cpy2 = int.parse(outline[ i++ ]) * -scaleY;
path.bezierCurveTo( cpx, cpy, cpx1, cpy1, cpx2, cpy2 );
if (pts.length > 0) laste = pts[ pts.length - 1 ];
if ( laste != null ) {
cpx0 = laste.x;
cpy0 = laste.y;
for ( i2 = 1; i2 <= divisions; i2 ++ ) {
var t = i2 / divisions;
var tx = ShapeUtils.b3( t, cpx0, cpx1, cpx2, cpx );
var ty = ShapeUtils.b3( t, cpy0, cpy1, cpy2, cpy );
}
}
break;
}
}
}
return { "offset": glyph["ha"]*scale, "path":path};
}
dynamic drawText(String text) #
drawText( String text ) {
var characterPts = [], allPts = [];
// RenderText
var i, p,
face = getFace(),
scale = _size / face["resolution"],
offset = 0,
chars = text.split( '' ),
length = chars.length;
var fontPaths = [];
for ( i = 0; i < length; i ++ ) {
var path = new Path();
var ret = extractGlyphPoints( chars[ i ], face, scale, offset, path );
offset += ret["offset"];
fontPaths.add( ret["path"] );
}
// get the width
var width = offset / 2;
//
// for ( p = 0; p < allPts.length; p++ ) {
//
// allPts[ p ].x -= width;
//
// }
//var extract = this.extractPoints( allPts, characterPts );
//extract.contour = allPts;
//extract.paths = fontPaths;
//extract.offset = width;
return { "paths" : fontPaths, "offset" : width };
}
dynamic loadFace(data) #
loadFace( data ) {
var family = data["familyName"].toLowerCase();
if (_faces[ family ] == null) _faces[ family ] = {};
if (_faces[ family ][ data["cssFontWeight"] ] == null) _faces[ family ][ data["cssFontWeight"] ] = {};
_faces[ family ][ data["cssFontWeight"] ][ data["cssFontStyle"] ] = data;
// TODO - Parse data
var face = _faces[ family ][ data["cssFontWeight"] ][ data["cssFontStyle"] ] = data;
return data;
}