diff --git a/examples/CharacterText/bounding_box.ts b/examples/CharacterText/bounding_box.ts
new file mode 100644
index 0000000..0804d00
--- /dev/null
+++ b/examples/CharacterText/bounding_box.ts
@@ -0,0 +1,29 @@
+import * as txt from "txt";
+import createHiDPICanvas from "../../lib/hidpi-canvas";
+export default function init() {
+ const canvas = createHiDPICanvas(500, 500, 2);
+ document.body.appendChild(canvas);
+ const stage = new createjs.Stage(canvas);
+
+ const charText = new txt.CharacterText({
+ text: "The fox\n jumped over...",
+ font: "raleway",
+ tracking: 20,
+ minSize: 70,
+ width: 500,
+ height: 500,
+ size: 120,
+ x: 100,
+ y: 100,
+ debug: true,
+ });
+ stage.addChild(charText);
+
+ charText.layout();
+
+ console.log(charText.getBounds());
+
+ stage.update();
+
+ return stage;
+}
diff --git a/examples/Graphics/bounding_box.ts b/examples/Graphics/bounding_box.ts
new file mode 100644
index 0000000..16e35c7
--- /dev/null
+++ b/examples/Graphics/bounding_box.ts
@@ -0,0 +1,31 @@
+import * as txt from "txt";
+import createHiDPICanvas from "../../lib/hidpi-canvas";
+import svgPath from "../fixtures/svg-glyph";
+
+export default function init() {
+ const canvas = createHiDPICanvas(500, 300, 2);
+ document.body.appendChild(canvas);
+ const stage = new createjs.Stage(canvas);
+
+ const shape = new createjs.Shape();
+
+ shape.graphics.beginFill("#000");
+ shape.graphics.decodeSVGPath(svgPath);
+ shape.graphics.endFill();
+ shape.y = 30;
+ stage.addChild(shape);
+
+ let boundingBox = new createjs.Rectangle();
+ boundingBox = txt.svgPathBoundingBox(svgPath);
+ const boundaryLine = new createjs.Shape();
+ boundaryLine.y = 30;
+ boundaryLine.graphics
+ .beginStroke("#dd0000")
+ .setStrokeStyle(2)
+ .setStrokeDash([15, 5])
+ .rect(boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height);
+ stage.addChild(boundaryLine);
+
+ stage.update();
+ return stage;
+}
diff --git a/examples/Graphics/pathGraphics.ts b/examples/Graphics/pathGraphics.ts
index ae9da0b..98eb1d9 100755
--- a/examples/Graphics/pathGraphics.ts
+++ b/examples/Graphics/pathGraphics.ts
@@ -1,54 +1,56 @@
import createHiDPICanvas from "../../lib/hidpi-canvas";
+
+function createPathShape(path, strokeColor, fillColor = null) {
+ const shape = new createjs.Shape();
+
+ if (fillColor) {
+ shape.graphics.beginFill(fillColor);
+ }
+ shape.graphics
+ .setStrokeStyle(4)
+ .beginStroke(strokeColor)
+ .decodeSVGPath(path);
+
+ return shape;
+}
+
export default function init() {
const canvas = createHiDPICanvas(1080, 420, 1);
document.body.appendChild(canvas);
const stage = new createjs.Stage(canvas);
- const a = new createjs.Shape();
- a.graphics.setStrokeStyle(4);
- a.graphics.beginStroke("#00F");
- a.graphics.beginFill("#F00");
- a.graphics.decodeSVGPath("M 300 200 h -150 a 150 150 0 1 0 150 -150 z");
+ const a = createPathShape(
+ "M 300 200 h -150 a 150 150 0 1 0 150 -150 z",
+ "#00F",
+ "#F00"
+ );
stage.addChild(a);
- const b = new createjs.Shape();
- b.graphics.setStrokeStyle(4);
- b.graphics.beginStroke("#000");
- b.graphics.beginFill("#FF0");
- b.graphics.decodeSVGPath("M 275 175 v -150 a 150 150 0 0 0 -150 150 z");
+ const b = createPathShape(
+ "M 275 175 v -150 a 150 150 0 0 0 -150 150 z",
+ "#000",
+ "#FF0"
+ );
+
stage.addChild(b);
- const c = new createjs.Shape();
- c.graphics.setStrokeStyle(4);
- c.graphics.beginStroke("#F00");
- c.graphics.decodeSVGPath(
- "M 600 400 l 50 -25 a25 25 -30 0 1 50 -25 l 50 -25 a25 50 -30 0 1 50 -25 l 50 -25 a25 75 -30 0 1 50 -25 l 50 -25 a 25 100 -30 0 1 50 -25 l50 -25"
+ const c = createPathShape(
+ "M 600 400 l 50 -25 a25 25 -30 0 1 50 -25 l 50 -25 a25 50 -30 0 1 50 -25 l 50 -25 a25 75 -30 0 1 50 -25 l 50 -25 a 25 100 -30 0 1 50 -25 l50 -25",
+ "#F00"
);
stage.addChild(c);
- let d = new createjs.Shape();
- d.graphics.setStrokeStyle(4);
- d.graphics.beginStroke("#F00");
- d.graphics.decodeSVGPath("M 600,75 a100,50 0 0,0 100,50");
+ const d = createPathShape("M 600,75 a100,50 0 0,0 100,50", "#F00");
stage.addChild(d);
- d = new createjs.Shape();
- d.graphics.setStrokeStyle(4);
- d.graphics.beginStroke("#0F0");
- d.graphics.decodeSVGPath("M 600,75 a100,50 0 0,1 100,50");
- stage.addChild(d);
+ const e = createPathShape("M 600,75 a100,50 0 0,1 100,50", "#0F0");
+ stage.addChild(e);
- d = new createjs.Shape();
- d.graphics.setStrokeStyle(4);
- d.graphics.beginStroke("#00F");
- d.graphics.decodeSVGPath("M 600,75 a100,50 0 1,0 100,50");
- stage.addChild(d);
+ const f = createPathShape("M 600,75 a100,50 0 1,0 100,50", "#00F");
+ stage.addChild(f);
- d = new createjs.Shape();
- d.graphics.setStrokeStyle(4);
- d.graphics.beginStroke("#F0F");
- d.graphics.decodeSVGPath("M 600,75 a100,50 0 1,1 100,50");
- stage.addChild(d);
+ const g = createPathShape("M 600,75 a100,50 0 1,1 100,50", "#F0F");
+ stage.addChild(g);
stage.update();
return stage;
diff --git a/examples/character-text.ts b/examples/character-text.ts
index 72e0888..371089a 100644
--- a/examples/character-text.ts
+++ b/examples/character-text.ts
@@ -4,6 +4,7 @@ import autosize_expand from "./CharacterText/autosize_expand";
import autosize_reduce from "./CharacterText/autosize_reduce";
import autosize_reduce_expand from "./CharacterText/autosize_reduce_expand";
import autosize_reduce_layout from "./CharacterText/autosize_reduce_layout";
+import bounding_box from "./CharacterText/bounding_box";
import cache from "./CharacterText/cache";
import character_case from "./CharacterText/case";
import child_events from "./CharacterText/child_events";
@@ -64,6 +65,7 @@ export const visual = {
export const nonVisual = {
accessibility,
+ bounding_box,
cache,
complete,
child_events,
diff --git a/examples/graphics.ts b/examples/graphics.ts
index 261da33..f74ef24 100644
--- a/examples/graphics.ts
+++ b/examples/graphics.ts
@@ -1,5 +1,16 @@
import pathGraphics from "./Graphics/pathGraphics";
import pathGraphics2 from "./Graphics/pathGraphics2";
import pathGraphics3 from "./Graphics/pathGraphics3";
+import bounding_box from "./Graphics/bounding_box";
-export default { pathGraphics, pathGraphics2, pathGraphics3 };
+export const visual = {
+ pathGraphics,
+ pathGraphics2,
+ pathGraphics3
+};
+
+export const nonVisual = {
+ bounding_box
+};
+
+export default { ...visual, ...nonVisual };
diff --git a/examples/index.ts b/examples/index.ts
index 061b8db..30cd281 100644
--- a/examples/index.ts
+++ b/examples/index.ts
@@ -5,7 +5,7 @@ txt.FontLoader.path = "../font/";
import { visual as CharacterTextVisual } from "./character-text";
import { visual as TextVisual } from "./text";
import { visual as PathTextVisual } from "./path-text";
-import Graphics from "./graphics";
+import { visual as GraphicsVisual } from "./graphics";
import { nonVisual as CharacterTextNonVisual } from "./character-text";
import { nonVisual as TextNonVisual } from "./text";
@@ -16,7 +16,7 @@ export const visualExamples = {
CharacterText: CharacterTextVisual,
Text: TextVisual,
PathText: PathTextVisual,
- Graphics
+ Graphics: GraphicsVisual
};
export const nonVisualExamples = {
diff --git a/index.html b/index.html
index 6accee6..a7ac084 100644
--- a/index.html
+++ b/index.html
@@ -250,7 +250,9 @@
CharacterText
Fit - singleLine autoReduce + autoExpand
-
+
+ Bounding box
+
@@ -312,6 +314,9 @@ Graphics
Glyph Rendering (inverted)
+
+ SVG Path Bounds
+
diff --git a/src/Align.ts b/src/Align.ts
index 833aaf1..20640d2 100644
--- a/src/Align.ts
+++ b/src/Align.ts
@@ -20,3 +20,27 @@ enum Align {
}
export default Align;
+
+export function topAligned(alignment: number): boolean {
+ return (
+ alignment === Align.TOP_LEFT ||
+ alignment === Align.TOP_CENTER ||
+ alignment === Align.TOP_RIGHT
+ );
+}
+
+export function middleAligned(alignment: number): boolean {
+ return (
+ alignment === Align.MIDDLE_LEFT ||
+ alignment === Align.MIDDLE_CENTER ||
+ alignment === Align.MIDDLE_RIGHT
+ );
+}
+
+export function bottomAligned(alignment: number): boolean {
+ return (
+ alignment === Align.BOTTOM_LEFT ||
+ alignment === Align.BOTTOM_CENTER ||
+ alignment === Align.BOTTOM_RIGHT
+ );
+}
diff --git a/src/Character.ts b/src/Character.ts
index ba10ef5..7e9261a 100644
--- a/src/Character.ts
+++ b/src/Character.ts
@@ -109,8 +109,8 @@ export default class Character extends createjs.Shape {
(this._font.ascent - this._font.descent) * this.scaleX;
this.measuredWidth = this.scaleX * this._glyph.offset * this._font.units;
- const ha = new createjs.Shape();
- ha.graphics
+ const hitArea = new createjs.Shape();
+ hitArea.graphics
.beginFill("#000")
.drawRect(
0,
@@ -118,7 +118,9 @@ export default class Character extends createjs.Shape {
this._glyph.offset * this._font.units,
this._font.ascent - this._font.descent
);
- this.hitArea = ha;
+ this.hitArea = hitArea;
+
+ this._glyph.boundingLine();
}
setGlyph(glyph: Glyph) {
diff --git a/src/CharacterText.ts b/src/CharacterText.ts
index cd510c6..bc8517b 100644
--- a/src/CharacterText.ts
+++ b/src/CharacterText.ts
@@ -1,5 +1,5 @@
import TextContainer from "./TextContainer";
-import Align from "./Align";
+import Align, { topAligned, middleAligned, bottomAligned } from "./Align";
import FontLoader from "./FontLoader";
import { ConstructObj, Style } from "./Interfaces";
import Font from "./Font";
@@ -300,7 +300,6 @@ export default class CharacterText extends TextContainer {
*/
characterLayout(): boolean {
//char layout
- const len = this.text.length;
let char: Character;
const defaultStyle: Style = {
size: this.size,
@@ -322,9 +321,9 @@ export default class CharacterText extends TextContainer {
this.lines.push(currentLine);
this.block.addChild(currentLine);
- // loop over characters
- // place into lines
- for (let i = 0; i < len; i++) {
+ // loop over characters, and place into lines
+ for (let i = 0; i < this.text.length; i++) {
+ // apply custom character styles
if (this.style !== null && this.style[i] !== undefined) {
currentStyle = this.style[i];
// make sure style contains properties needed.
@@ -350,7 +349,7 @@ export default class CharacterText extends TextContainer {
// new line has no character
if (this.text.charAt(i) == "\n" || this.text.charAt(i) == "\r") {
//only if not last char
- if (i < len - 1) {
+ if (i < this.text.length - 1) {
if (firstLine === true) {
vPosition = currentStyle.size;
currentLine.measuredHeight = currentStyle.size;
@@ -578,11 +577,7 @@ export default class CharacterText extends TextContainer {
}
//TOP ALIGNED
- if (
- this.align === a.TOP_LEFT ||
- this.align === a.TOP_CENTER ||
- this.align === a.TOP_RIGHT
- ) {
+ if (topAligned(this.align)) {
if (fnt.top == 0) {
this.block.y = (this.lines[0].measuredHeight * fnt.ascent) / fnt.units;
} else {
@@ -592,22 +587,14 @@ export default class CharacterText extends TextContainer {
}
//MIDDLE ALIGNED
- } else if (
- this.align === a.MIDDLE_LEFT ||
- this.align === a.MIDDLE_CENTER ||
- this.align === a.MIDDLE_RIGHT
- ) {
+ } else if (middleAligned(this.align)) {
this.block.y =
this.lines[0].measuredHeight +
(this.height - measuredHeight) / 2 +
(this.lines[0].measuredHeight * fnt.middle) / fnt.units;
//BOTTOM ALIGNED
- } else if (
- this.align === a.BOTTOM_LEFT ||
- this.align === a.BOTTOM_CENTER ||
- this.align === a.BOTTOM_RIGHT
- ) {
+ } else if (bottomAligned(this.align)) {
this.block.y =
this.height -
this.lines[this.lines.length - 1].y +
diff --git a/src/Glyph.ts b/src/Glyph.ts
index 16c4a2e..df1d77d 100755
--- a/src/Glyph.ts
+++ b/src/Glyph.ts
@@ -1,17 +1,23 @@
+import { svgPathBoundingBox } from "./SVGPath";
+
/**
* Represents a single Glyph within a Font.
*/
-
export default class Glyph {
/** SVG path data */
path = "";
+ private _bounds: createjs.Rectangle = null;
offset: number;
kerning: any = {};
+
private _graphic: createjs.Graphics = null;
+ private _boundaryLine: createjs.Graphics = null;
_fill: createjs.Graphics.Fill;
_stroke: createjs.Graphics.Stroke;
_strokeStyle: createjs.Graphics.StrokeStyle;
+ static debug = false;
+
graphic() {
if (this._graphic == null) {
this._graphic = new createjs.Graphics();
@@ -39,8 +45,40 @@ export default class Glyph {
return this._graphic;
}
+ getBounds() {
+ if (!this._bounds) {
+ this._bounds = svgPathBoundingBox(this.path);
+ }
+ return this._bounds;
+ }
+
+ boundingLine() {
+ if (this._boundaryLine == null) {
+ const bounds = this.getBounds();
+ this._boundaryLine = Glyph.buildBoundaryGraphics(bounds);
+ }
+ }
+
+ static buildBoundaryGraphics(bounds: createjs.Rectangle): createjs.Graphics {
+ const boundary = new createjs.Graphics();
+ boundary.append(
+ new createjs.Graphics.Rect(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height
+ )
+ );
+ boundary.append(new createjs.Graphics.StrokeDash([10, 4]));
+ boundary.append(new createjs.Graphics.Stroke("#FF00FF", true));
+ return boundary;
+ }
+
draw(ctx: CanvasRenderingContext2D): boolean {
this._graphic.draw(ctx);
+ if (Glyph.debug) {
+ this._boundaryLine.draw(ctx);
+ }
return true;
}
diff --git a/src/Graphics.ts b/src/Graphics.ts
index 75c3cc9..4f14e52 100644
--- a/src/Graphics.ts
+++ b/src/Graphics.ts
@@ -4,6 +4,9 @@ import { parsePathData } from "./SVGPath";
export default class Graphics {
/**
* Build up createjs Graphics commands based on path data.
+ *
+ * Adapted from KineticJS
+ * @see https://github.com/ericdrowell/KineticJS/blob/master/src/plugins/Path.js#L41
*/
static init(target, svgpath: string) {
const ca = parsePathData(svgpath);
diff --git a/src/PathBounds.ts b/src/PathBounds.ts
new file mode 100644
index 0000000..9c54acf
--- /dev/null
+++ b/src/PathBounds.ts
@@ -0,0 +1,342 @@
+function getBoundsOfCurve(x, y, x1, y1, x2, y2, tempX, tempY) {
+ // TODO: implement getBoundsOfCurve
+ return [];
+}
+
+function getBoundsOfArc(fx, fy, rx, ry, rot, large, sweep, tx, ty) {
+ // TODO: implement getBoundsOfArc
+ return [];
+}
+
+export default function pathBounds(path) {
+ const aX = [],
+ aY = [];
+ let current, // current instruction
+ previous = null,
+ subpathStartX = 0,
+ subpathStartY = 0,
+ x = 0, // current x
+ y = 0, // current y
+ controlX = 0, // current control point x
+ controlY = 0, // current control point y
+ tempX,
+ tempY,
+ bounds;
+
+ for (let i = 0, len = path.length; i < len; ++i) {
+ current = path[i].points.flat();
+
+ current.unshift(path[i].command);
+
+ switch (
+ path[i].command // first letter
+ ) {
+ case "l": // lineto, relative
+ x += current[1];
+ y += current[2];
+ bounds = [];
+ break;
+
+ case "L": // lineto, absolute
+ x = current[1];
+ y = current[2];
+ bounds = [];
+ break;
+
+ case "h": // horizontal lineto, relative
+ x += current[1];
+ bounds = [];
+ break;
+
+ case "H": // horizontal lineto, absolute
+ x = current[1];
+ bounds = [];
+ break;
+
+ case "v": // vertical lineto, relative
+ y += current[1];
+ bounds = [];
+ break;
+
+ case "V": // verical lineto, absolute
+ y = current[1];
+ bounds = [];
+ break;
+
+ case "m": // moveTo, relative
+ x += current[1];
+ y += current[2];
+ subpathStartX = x;
+ subpathStartY = y;
+ bounds = [];
+ break;
+
+ case "M": // moveTo, absolute
+ x = current[1];
+ y = current[2];
+ subpathStartX = x;
+ subpathStartY = y;
+ bounds = [];
+ break;
+
+ case "c": // bezierCurveTo, relative
+ tempX = x + current[5];
+ tempY = y + current[6];
+ controlX = x + current[3];
+ controlY = y + current[4];
+ bounds = getBoundsOfCurve(
+ x,
+ y,
+ x + current[1], // x1
+ y + current[2], // y1
+ controlX, // x2
+ controlY, // y2
+ tempX,
+ tempY
+ );
+ x = tempX;
+ y = tempY;
+ break;
+
+ case "C": // bezierCurveTo, absolute
+ controlX = current[3];
+ controlY = current[4];
+ bounds = getBoundsOfCurve(
+ x,
+ y,
+ current[1],
+ current[2],
+ controlX,
+ controlY,
+ current[5],
+ current[6]
+ );
+ x = current[5];
+ y = current[6];
+ break;
+
+ case "s": // shorthand cubic bezierCurveTo, relative
+ // transform to absolute x,y
+ tempX = x + current[3];
+ tempY = y + current[4];
+
+ if (previous[0].match(/[CcSs]/) === null) {
+ // If there is no previous command or if the previous command was not a C, c, S, or s,
+ // the control point is coincident with the current point
+ controlX = x;
+ controlY = y;
+ } else {
+ // calculate reflection of previous control points
+ controlX = 2 * x - controlX;
+ controlY = 2 * y - controlY;
+ }
+
+ bounds = getBoundsOfCurve(
+ x,
+ y,
+ controlX,
+ controlY,
+ x + current[1],
+ y + current[2],
+ tempX,
+ tempY
+ );
+ // set control point to 2nd one of this command
+ // "... the first control point is assumed to be
+ // the reflection of the second control point on
+ // the previous command relative to the current point."
+ controlX = x + current[1];
+ controlY = y + current[2];
+ x = tempX;
+ y = tempY;
+ break;
+
+ case "S": // shorthand cubic bezierCurveTo, absolute
+ tempX = current[3];
+ tempY = current[4];
+ if (previous[0].match(/[CcSs]/) === null) {
+ // If there is no previous command or if the previous command was not a C, c, S, or s,
+ // the control point is coincident with the current point
+ controlX = x;
+ controlY = y;
+ } else {
+ // calculate reflection of previous control points
+ controlX = 2 * x - controlX;
+ controlY = 2 * y - controlY;
+ }
+ bounds = getBoundsOfCurve(
+ x,
+ y,
+ controlX,
+ controlY,
+ current[1],
+ current[2],
+ tempX,
+ tempY
+ );
+ x = tempX;
+ y = tempY;
+ // set control point to 2nd one of this command
+ // "... the first control point is assumed to be
+ // the reflection of the second control point on
+ // the previous command relative to the current point."
+ controlX = current[1];
+ controlY = current[2];
+ break;
+
+ case "q": // quadraticCurveTo, relative
+ // transform to absolute x,y
+ tempX = x + current[3];
+ tempY = y + current[4];
+ controlX = x + current[1];
+ controlY = y + current[2];
+ bounds = getBoundsOfCurve(
+ x,
+ y,
+ controlX,
+ controlY,
+ controlX,
+ controlY,
+ tempX,
+ tempY
+ );
+ x = tempX;
+ y = tempY;
+ break;
+
+ case "Q": // quadraticCurveTo, absolute
+ controlX = current[1];
+ controlY = current[2];
+ bounds = getBoundsOfCurve(
+ x,
+ y,
+ controlX,
+ controlY,
+ controlX,
+ controlY,
+ current[3],
+ current[4]
+ );
+ x = current[3];
+ y = current[4];
+ break;
+
+ case "t": // shorthand quadraticCurveTo, relative
+ // transform to absolute x,y
+ tempX = x + current[1];
+ tempY = y + current[2];
+ if (previous[0].match(/[QqTt]/) === null) {
+ // If there is no previous command or if the previous command was not a Q, q, T or t,
+ // assume the control point is coincident with the current point
+ controlX = x;
+ controlY = y;
+ } else {
+ // calculate reflection of previous control point
+ controlX = 2 * x - controlX;
+ controlY = 2 * y - controlY;
+ }
+
+ bounds = getBoundsOfCurve(
+ x,
+ y,
+ controlX,
+ controlY,
+ controlX,
+ controlY,
+ tempX,
+ tempY
+ );
+ x = tempX;
+ y = tempY;
+
+ break;
+
+ case "T":
+ tempX = current[1];
+ tempY = current[2];
+
+ if (previous[0].match(/[QqTt]/) === null) {
+ // If there is no previous command or if the previous command was not a Q, q, T or t,
+ // assume the control point is coincident with the current point
+ controlX = x;
+ controlY = y;
+ } else {
+ // calculate reflection of previous control point
+ controlX = 2 * x - controlX;
+ controlY = 2 * y - controlY;
+ }
+ bounds = getBoundsOfCurve(
+ x,
+ y,
+ controlX,
+ controlY,
+ controlX,
+ controlY,
+ tempX,
+ tempY
+ );
+ x = tempX;
+ y = tempY;
+ break;
+
+ case "na": // TODO: implement getBoundsOfArc
+ bounds = getBoundsOfArc(
+ x,
+ y,
+ current[1],
+ current[2],
+ current[3],
+ current[4],
+ current[5],
+ current[6] + x,
+ current[7] + y
+ );
+ x += current[6];
+ y += current[7];
+ break;
+
+ case "nA": // TODO: implement getBoundsOfArc absolute
+ bounds = getBoundsOfArc(
+ x,
+ y,
+ current[1],
+ current[2],
+ current[3],
+ current[4],
+ current[5],
+ current[6],
+ current[7]
+ );
+ x = current[6];
+ y = current[7];
+ break;
+
+ case "z":
+ case "Z":
+ x = subpathStartX;
+ y = subpathStartY;
+ break;
+ }
+ previous = current;
+ bounds.forEach(function(point) {
+ aX.push(point.x);
+ aY.push(point.y);
+ });
+ aX.push(x);
+ aY.push(y);
+ }
+
+ const minX = Math.min(...aX) || 0,
+ minY = Math.min(...aY) || 0,
+ maxX = Math.max(...aX) || 0,
+ maxY = Math.max(...aY) || 0,
+ deltaX = maxX - minX,
+ deltaY = maxY - minY;
+
+ return {
+ left: minX,
+ top: minY,
+ width: deltaX,
+ height: deltaY
+ };
+}
diff --git a/src/SVGPath.ts b/src/SVGPath.ts
index a8061e3..cbb9697 100644
--- a/src/SVGPath.ts
+++ b/src/SVGPath.ts
@@ -1,36 +1,68 @@
-export function parsePathData(data) {
+import pathBounds from "./PathBounds";
+
+/**
+ * Useful SVG path tutorial on MDN
+ * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
+ * @param data
+ */
+
+export function svgPathBoundingBox(svgpath: string) {
+ // TODO: access a cached array?
+ const ca = parsePathData(svgpath);
+ const bounds = pathBounds(ca);
+ return new createjs.Rectangle(
+ bounds.left,
+ bounds.top,
+ bounds.width,
+ bounds.height
+ );
+}
+
+/**
+ * Adapted from KineticJS
+ * @see https://github.com/ericdrowell/KineticJS/blob/master/src/plugins/Path.js#L210
+ */
+export function parsePathData(data: string) {
if (!data) {
return [];
}
+ // command string
let cs = data;
+
+ // command chars
const cc = [
- "m",
- "M",
- "l",
- "L",
- "v",
- "V",
- "h",
- "H",
- "z",
- "Z",
- "c",
- "C",
- "q",
- "Q",
- "t",
- "T",
- "s",
- "S",
- "a",
- "A"
+ // Path Data Segment must begin with a moveTo
+ "m", //m (x y)+ Relative moveTo (subsequent points are treated as lineTo)
+ "M", //M (x y)+ Absolute moveTo (subsequent points are treated as lineTo)
+ "l", //l (x y)+ Relative lineTo
+ "L", //L (x y)+ Absolute LineTo
+ "v", //v (y)+ Relative vertical lineTo
+ "V", //V (y)+ Absolute vertical lineTo
+ "h", //h (x)+ Relative horizontal lineTo
+ "H", //H (x)+ Absolute horizontal lineTo
+ "z", //z (closepath)
+ "Z", //Z (closepath)
+ "c", //c (x1 y1 x2 y2 x y)+ Relative Bezier curve
+ "C", //C (x1 y1 x2 y2 x y)+ Absolute Bezier curve
+ "q", //q (x1 y1 x y)+ Relative Quadratic Bezier
+ "Q", //Q (x1 y1 x y)+ Absolute Quadratic Bezier
+ "t", //t (x y)+ Shorthand/Smooth Relative Quadratic Bezier
+ "T", //T (x y)+ Shorthand/Smooth Absolute Quadratic Bezier
+ "s", //s (x2 y2 x y)+ Shorthand/Smooth Relative Bezier curve
+ "S", //S (x2 y2 x y)+ Shorthand/Smooth Absolute Bezier curve
+ "a", //a (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Relative Elliptical Arc
+ "A" //A (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Absolute Elliptical Arc
];
+ // convert white spaces to commas
cs = cs.replace(new RegExp(" ", "g"), ",");
+ // create pipes so that we can split the data
for (let n = 0; n < cc.length; n++) {
cs = cs.replace(new RegExp(cc[n], "g"), "|" + cc[n]);
}
+ // create array
const arr = cs.split("|");
const ca = [];
+ // init context point
let cpx = 0;
let cpy = 0;
const arrLength = arr.length;
@@ -39,16 +71,20 @@ export function parsePathData(data) {
let str = arr[n];
let c = str.charAt(0);
str = str.slice(1);
+ // remove ,- for consistency
str = str.replace(new RegExp(",-", "g"), "-");
+ // add commas so that it's easy to split
str = str.replace(new RegExp("-", "g"), ",-");
str = str.replace(new RegExp("e,-", "g"), "e-");
- let p = str.split(",");
- if (p.length > 0 && p[0] === "") {
- p.shift();
+ const segments = str.split(",");
+ if (segments.length > 0 && segments[0] === "") {
+ segments.shift();
}
- const pLength = p.length;
- for (let i = 0; i < pLength; i++) {
- p[i] = parseFloat(p[i]);
+ let p = [];
+
+ // convert strings to floats
+ for (let i = 0; i < segments.length; i++) {
+ p[i] = parseFloat(segments[i]);
}
if (c === "z" || c === "Z") {
p = [true];
@@ -56,17 +92,21 @@ export function parsePathData(data) {
while (p.length > 0) {
if (isNaN(p[0])) {
+ // case for a trailing comma before next command
break;
}
let cmd = null;
let points = [];
const startX = cpx,
startY = cpy;
- let prevCmd, ctlPtx, ctlPty;
- let rx, ry, psi, fa, fs, x1, y1;
+ // Move var from within the switch to up here (jshint)
+ let prevCmd, ctlPtx, ctlPty; // Ss, Tt
+ let rx, ry, psi, fa, fs, x1, y1; // Aa
let dx, dy;
+ // convert l, H, h, V, and v to L
switch (c) {
+ // Note: Keep the lineTo's above the moveTo's in this switch
case "l":
cpx += p.shift();
cpy += p.shift();
@@ -79,7 +119,7 @@ export function parsePathData(data) {
cpy = p.shift();
points.push(cpx, cpy);
break;
-
+ // Note: lineTo handlers need to be above this point
case "m":
dx = p.shift();
dy = p.shift();
@@ -91,6 +131,7 @@ export function parsePathData(data) {
cmd = "M";
points.push(cpx, cpy);
c = "l";
+ // subsequent points are treated as relative lineTo
break;
case "M":
@@ -101,6 +142,7 @@ export function parsePathData(data) {
startPoint = [cpx, cpy];
}
points.push(cpx, cpy);
+ // subsequent points are treated as absolute lineTo
c = "L";
break;
diff --git a/src/Text.ts b/src/Text.ts
index 5de61e6..8344e89 100644
--- a/src/Text.ts
+++ b/src/Text.ts
@@ -1,5 +1,5 @@
import TextContainer from "./TextContainer";
-import Align from "./Align";
+import Align, { topAligned, middleAligned, bottomAligned } from "./Align";
import FontLoader from "./FontLoader";
import Word from "./Word";
import Line from "./Line";
@@ -451,7 +451,7 @@ export default class Text extends TextContainer {
// place into text
let measuredHeight = 0;
let line;
- const a = Align;
+
const fnt: Font = FontLoader.getFont(this.font);
const len = this.lines.length;
@@ -471,54 +471,42 @@ export default class Text extends TextContainer {
}
measuredHeight += line.measuredHeight;
- if (this.align === a.TOP_CENTER) {
+ if (this.align === Align.TOP_CENTER) {
//move to center
line.x = (this.width - line.measuredWidth) / 2;
- } else if (this.align === a.TOP_RIGHT) {
+ } else if (this.align === Align.TOP_RIGHT) {
//move to right
line.x = this.width - line.measuredWidth;
- } else if (this.align === a.MIDDLE_CENTER) {
+ } else if (this.align === Align.MIDDLE_CENTER) {
//move to center
line.x = (this.width - line.measuredWidth) / 2;
- } else if (this.align === a.MIDDLE_RIGHT) {
+ } else if (this.align === Align.MIDDLE_RIGHT) {
//move to right
line.x = this.width - line.measuredWidth;
- } else if (this.align === a.BOTTOM_CENTER) {
+ } else if (this.align === Align.BOTTOM_CENTER) {
//move to center
line.x = (this.width - line.measuredWidth) / 2;
- } else if (this.align === a.BOTTOM_RIGHT) {
+ } else if (this.align === Align.BOTTOM_RIGHT) {
//move to right
line.x = this.width - line.measuredWidth;
}
}
//TOP ALIGNED
- if (
- this.align === a.TOP_LEFT ||
- this.align === a.TOP_CENTER ||
- this.align === a.TOP_RIGHT
- ) {
+ if (topAligned(this.align)) {
this.block.y =
(this.lines[0].measuredHeight * fnt.ascent) / fnt.units +
(this.lines[0].measuredHeight * fnt.top) / fnt.units;
//MIDDLE ALIGNED
- } else if (
- this.align === a.MIDDLE_LEFT ||
- this.align === a.MIDDLE_CENTER ||
- this.align === a.MIDDLE_RIGHT
- ) {
+ } else if (middleAligned(this.align)) {
this.block.y =
this.lines[0].measuredHeight +
(this.height - measuredHeight) / 2 +
(this.lines[0].measuredHeight * fnt.middle) / fnt.units;
//BOTTOM ALIGNED
- } else if (
- this.align === a.BOTTOM_LEFT ||
- this.align === a.BOTTOM_CENTER ||
- this.align === a.BOTTOM_RIGHT
- ) {
+ } else if (bottomAligned(this.align)) {
this.block.y =
this.height -
this.lines[this.lines.length - 1].y +
diff --git a/src/index.ts b/src/index.ts
index 8d42aa2..39ba6e8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,5 @@
import "./GraphicsMixin";
+import copyEventListeners from "./utils/apply-shape-event-listeners";
export { default as Accessibility } from "./Accessibility";
export { default as Align } from "./Align";
@@ -16,8 +17,7 @@ export { default as Path, PathAlign, PathFit } from "./Path";
export { default as PathText } from "./PathText";
export { default as VerticalAlign } from "./VerticalAlign";
export { default as Word } from "./Word";
-
-import copyEventListeners from "./utils/apply-shape-event-listeners";
+export { svgPathBoundingBox } from "./SVGPath";
export const Util = {
copyEventListeners
diff --git a/src/types/createjs.d.ts b/src/types/createjs.d.ts
new file mode 100644
index 0000000..c03b55c
--- /dev/null
+++ b/src/types/createjs.d.ts
@@ -0,0 +1,8 @@
+// TODO: get this addded into the @types/easeljs package
+declare namespace createjs {
+ namespace Graphics {
+ class StrokeDash {
+ constructor(segments: Array, offset?: number);
+ }
+ }
+}
diff --git a/testem.js b/testem.js
index 831cc3a..bddfe85 100644
--- a/testem.js
+++ b/testem.js
@@ -12,17 +12,17 @@ let serve_files = [
{ src: coverageServer.clientFile },
{
src:
- "node_modules/@recreatejs/jasmine-pixelmatch/dist/jasmine-pixelmatch.js"
+ "node_modules/@recreatejs/jasmine-pixelmatch/dist/jasmine-pixelmatch.js",
},
{ src: "dist/easeljs.js" },
{ src: "dist/pathseg.js" },
{ src: "dist/txt.instrumented.umd.js" },
{ src: "dist/examples.umd.js" },
- { src: "dist/tests.umd.js" }
+ { src: "dist/tests.umd.js" },
];
-if (!process.env.HEADLESS) {
- serve_files.push({ src: "!dist/esnext/tests/_headless.js" });
+if (process.env.HEADLESS) {
+ serve_files.push({ src: "dist/esnext/tests/_headless.js" });
}
module.exports = {
@@ -30,14 +30,14 @@ module.exports = {
launch_in_ci: ["Chrome"],
browser_args: {
Chrome: chromeArgs,
- Firefox: firefoxArgs
+ Firefox: firefoxArgs,
},
test_page: "testem.mustache",
src_files: ["src/**/*.ts", "examples/**/*.ts"],
serve_files,
css_files: [],
routes: {
- "/images": "images"
+ "/images": "images",
},
proxies: coverageServer.proxies,
before_tests: function(config, data, callback) {
@@ -45,5 +45,5 @@ module.exports = {
},
after_tests: function(config, data, callback) {
coverageServer.shutdownCoverageServer(callback);
- }
+ },
};
diff --git a/tests/get-bounds.ts b/tests/get-bounds.ts
new file mode 100644
index 0000000..5f428c3
--- /dev/null
+++ b/tests/get-bounds.ts
@@ -0,0 +1,50 @@
+import * as txt from "txt";
+import * as txtExamples from "examples";
+import { removeCanvas } from "./helpers";
+describe("Text", function() {
+ let canvas;
+ let stage;
+ beforeEach(function() {
+ canvas = txtExamples.createHiDPICanvas(300, 300, 2);
+ document.body.appendChild(canvas);
+
+ stage = new createjs.Stage(canvas);
+ });
+
+ afterEach(function() {
+ removeCanvas();
+ });
+
+ it("getBounds of Glyph", function() {
+ const glyph = new txt.Glyph();
+ glyph.offset = 1063 / 2048;
+ glyph.path =
+ "M492 -246v226q-72 1 -136 28.5t-111 75t-74.5 111.5t-27.5 137v57l129 21v-78q0 -47 17 -88t47 -72t70 -49.5t86 -20.5v582q-66 24 -128.5 52.5t-111.5 72.5t-79 108t-30 158v27q0 72 27.5 135.5t74.5 111.5t111 76t136 29v184h102v-184q71 -1 134 -29.5t110 -76 t74.5 -111t27.5 -135.5v-37l-129 -21v58q0 46 -17 87t-46 71.5t-69 49.5t-85 21v-555q65 -25 129 -55t114.5 -75t81.5 -110.5t31 -160.5v-43q0 -73 -27.5 -137t-75.5 -112t-112 -75.5t-137 -27.5h-4v-226h-102zM821 375q0 57 -18 99.5t-48.5 74t-72 54.5t-88.5 42v-543 q47 0 88.5 18t72 49.5t48.5 73t18 89.5v43v0zM272 1075q0 -53 17 -92.5t47 -69.5t70 -53t86 -43v514q-46 -2 -86 -21t-70 -49.5t-47 -71.5t-17 -87v-27v0z";
+ const bounds = glyph.getBounds();
+ expect(bounds.width).toBeGreaterThan(0);
+ });
+
+ it("getBounds of Text", function() {
+ const text = new txt.Text({
+ text: "First poiretone",
+ font: "poiretone",
+ width: 400,
+ height: 400,
+ size: 100,
+ x: 0,
+ y: 0,
+ accessibilityPriority: 0
+ });
+
+ stage.addChild(text);
+
+ const bounds = text.getBounds();
+
+ expect(bounds).not.toBeNull();
+
+ expect(bounds.x).toBe(0);
+ expect(bounds.y).toBe(0);
+ expect(bounds.width).toBe(400);
+ expect(bounds.height).toBe(400);
+ });
+});