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