Created
August 27, 2017 18:42
-
-
Save tconkling/5e920fb2e1c32babfe7cbe3b924c58ac to your computer and use it in GitHub Desktop.
Starling StyledText
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
// | |
// aciv | |
package aciv.text { | |
import starling.text.TextFormat; | |
public class StyledText { | |
public var text :String; | |
public var runs :Vector.<StyleRun> = new <StyleRun>[]; | |
public var valid :Boolean; | |
public static function parse (string :String, defaultFormat :TextFormat, out :StyledText = null) :StyledText { | |
if (_parser == null) { | |
_parser = new StyledTextParser(); | |
} | |
return _parser.parse(string, defaultFormat, out); | |
} | |
public function reset () :void { | |
this.text = null; | |
this.valid = false; | |
this.runs.length = 0; | |
} | |
private static var _parser :StyledTextParser; | |
} | |
} | |
import aciv.text.StyleRun; | |
import aciv.text.StyledText; | |
import aspire.util.Log; | |
import aspire.util.StringUtil; | |
import starling.text.TextFormat; | |
class StyledTextParser { | |
public function parse (string :String, defaultFormat :TextFormat, out :StyledText = null) :StyledText { | |
if (out == null) { | |
out = new StyledText(); | |
} else { | |
out.reset(); | |
} | |
_inString = string; | |
_defaultColor = defaultFormat.color; | |
out.text = ""; | |
out.valid = parseInternal(out); | |
// If the input string was invalid, we just create a single StyleRun that encompasses the | |
// entire input string. | |
if (!out.valid) { | |
out.runs.length = 0; | |
out.text = _inString; | |
var singleRun :StyleRun = new StyleRun(); | |
singleRun.length = _inString.length; | |
singleRun.color = defaultFormat.color; | |
out.runs[0] = singleRun; | |
} | |
reset(); | |
return out; | |
} | |
/** Return true if parsing is successful */ | |
private function parseInternal (st :StyledText) :Boolean { | |
if (TOKEN == null) { | |
TOKEN = new Token(); | |
} | |
var tok :Token = eatNextToken(TOKEN); | |
for (; tok.type != Token.END; tok = eatNextToken(TOKEN)) { | |
if (tok.type == Token.POP_STYLE) { | |
if (!popStyle(st)) { | |
log.warning("Got a style close tag with no matching open tag", "string", _inString); | |
return false; | |
} | |
} else if (tok.type == Token.PUSH_STYLE) { | |
// parse our color | |
var newColor :uint; | |
try { | |
newColor = StringUtil.parseInteger(tok.value, 16); | |
} catch (e :Error) { | |
log.warning("Bad style tag; couldn't parse a color", "string", _inString, "tag", tok.value); | |
return false; | |
} | |
pushStyle(st, newColor); | |
} else { | |
appendText(st, tok.value); | |
} | |
} | |
if (_colorStack.length > 0) { | |
log.warning("Unclosed style tag", "string", _inString); | |
return false; | |
} | |
endCurRun(st); | |
return true; | |
} | |
private function eatNextToken (out :Token = null) :Token { | |
if (out == null) { | |
out = new Token(); | |
} | |
if (_inIdx >= _inString.length) { | |
return out.reset(Token.END); | |
} | |
if (startsWith(_inString, STYLE_END, _inIdx)) { | |
_inIdx += STYLE_END.length; | |
return out.reset(Token.POP_STYLE); | |
} | |
STYLE_START.lastIndex = _inIdx; | |
var match :Object = STYLE_START.exec(_inString); | |
if (match != null && match.index == _inIdx) { | |
var completeMatch :String = match[0]; | |
var style :String = match[1]; | |
_inIdx += completeMatch.length; | |
return out.reset(Token.PUSH_STYLE, style); | |
} | |
var textEndIdx :int = _inString.indexOf("{", _inIdx + 1); | |
var text :String = (textEndIdx > _inIdx ? | |
_inString.substring(_inIdx, textEndIdx) : | |
_inString.substr(_inIdx)); | |
_inIdx += text.length; | |
return out.reset(Token.TEXT, text); | |
} | |
private function pushStyle (st :StyledText, color :uint) :void { | |
_colorStack[_colorStack.length] = color; | |
beginNewRun(st); | |
} | |
private function popStyle (st :StyledText) :Boolean { | |
if (_colorStack.length > 0) { | |
_colorStack.pop(); | |
beginNewRun(st); | |
return true; | |
} else { | |
return false; | |
} | |
} | |
private function beginNewRun (st :StyledText) :void { | |
var color :uint = (_colorStack.length > 0 ? | |
_colorStack[_colorStack.length -1] : | |
_defaultColor); | |
// we may be able to continue our current run | |
if (_curRun == null || _curRun.color != color) { | |
endCurRun(st); | |
_curRun = new StyleRun(); | |
_curRun.color = color; | |
} | |
} | |
private function appendText (st :StyledText, text :String) :void { | |
if (_curRun == null) { | |
beginNewRun(st); | |
} | |
st.text += text; | |
_curRun.length += text.length; | |
} | |
private function endCurRun (st :StyledText) :void { | |
if (_curRun != null && _curRun.length > 0) { | |
st.runs[st.runs.length] = _curRun; | |
} | |
_curRun = null; | |
} | |
private function reset () :void { | |
_defaultColor = 0; | |
_inString = null; | |
_inIdx = 0; | |
_curRun = null; | |
_colorStack.length = 0; | |
} | |
private static function startsWith (str :String, substr :String, startIdx :int) :Boolean { | |
return str.indexOf(substr, startIdx) == startIdx; | |
} | |
// Internal parse state | |
private var _defaultColor :uint; | |
private var _inString :String; | |
private var _inIdx :int; // index into our input string | |
private var _curRun :StyleRun; | |
private const _colorStack: Vector.<uint> = new <uint>[]; | |
private static const log :Log = Log.getLog(StyledTextParser); | |
private static var TOKEN :Token; | |
private static const STYLE_START :RegExp = /{style color=#(.*?)}/g; | |
private static const STYLE_END :String = "{/style}"; | |
} | |
class Token { | |
public static const PUSH_STYLE :String = "PushStyle"; | |
public static const POP_STYLE :String = "PopStyle"; | |
public static const TEXT :String = "Text"; | |
public static const END :String = "End"; | |
public var type :String; | |
public var value :String; | |
public function reset (type :String, value :String = null) :Token { | |
this.type = type; | |
this.value = value; | |
return this; | |
} | |
} |
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
// | |
// aciv | |
package aciv.text { | |
import flash.geom.Matrix; | |
import flash.geom.Point; | |
import flash.geom.Rectangle; | |
import flash.text.StyleSheet; | |
import starling.core.Starling; | |
import starling.display.DisplayObject; | |
import starling.display.DisplayObjectContainer; | |
import starling.display.MeshBatch; | |
import starling.display.Quad; | |
import starling.display.Sprite; | |
import starling.events.Event; | |
import starling.rendering.Painter; | |
import starling.styles.MeshStyle; | |
import starling.text.BitmapFont; | |
import starling.text.TextField; | |
import starling.text.TextFieldAutoSize; | |
import starling.text.TextFormat; | |
import starling.text.TextOptions; | |
import starling.utils.RectangleUtil; | |
public class StyledTextField extends DisplayObjectContainer | |
{ | |
private var _text:String; | |
private var _options:TextOptions; | |
private var _format:TextFormat; | |
private var _textBounds:Rectangle; | |
private var _hitArea:Rectangle; | |
private var _font :BitmapFont; | |
private var _requiresRecomposition:Boolean; | |
private var _border:DisplayObjectContainer; | |
private var _meshBatch:MeshBatch; | |
private var _style:MeshStyle; | |
private var _recomposing:Boolean; | |
// helper objects | |
private static var sMatrix:Matrix = new Matrix(); | |
/** Create a new text field with the given properties. */ | |
public function StyledTextField(width:int, height:int, text:String="", | |
format:TextFormat=null, options:TextOptions=null) | |
{ | |
_text = text ? text : ""; | |
_hitArea = new Rectangle(0, 0, width, height); | |
_requiresRecomposition = true; | |
_format = format ? format.clone() : new TextFormat(); | |
_format.addEventListener(Event.CHANGE, setRequiresRecomposition); | |
_options = options ? options.clone() : new TextOptions(); | |
_options.addEventListener(Event.CHANGE, setRequiresRecomposition); | |
_meshBatch = new MeshBatch(); | |
_meshBatch.touchable = false; | |
_meshBatch.pixelSnapping = true; | |
addChild(_meshBatch); | |
} | |
/** Disposes the underlying texture data. */ | |
public override function dispose():void | |
{ | |
_format.removeEventListener(Event.CHANGE, setRequiresRecomposition); | |
_options.removeEventListener(Event.CHANGE, setRequiresRecomposition); | |
_meshBatch.clear(); | |
super.dispose(); | |
} | |
/** @inheritDoc */ | |
public override function render(painter:Painter):void | |
{ | |
if (_requiresRecomposition) recompose(); | |
super.render(painter); | |
} | |
/** Forces the text contents to be composed right away. | |
* Normally, it will only do so lazily, i.e. before being rendered. */ | |
private function recompose():void | |
{ | |
if (_requiresRecomposition) | |
{ | |
_recomposing = true; | |
_meshBatch.clear(); | |
var fontName:String = _format.font; | |
_font = BitmapFont(TextField.getCompositor(fontName)); | |
if (_font == null && fontName == BitmapFont.MINI) | |
{ | |
_font = new BitmapFont(); | |
TextField.registerCompositor(_font, fontName); | |
} | |
updateText(); | |
updateBorder(); | |
_requiresRecomposition = false; | |
_recomposing = false; | |
} | |
} | |
// font and border rendering | |
private function updateText():void | |
{ | |
var width:Number = _hitArea.width; | |
var height:Number = _hitArea.height; | |
// Horizontal autoSize does not work for HTML text, since it supports custom alignment. | |
// What should we do if one line is aligned to the left, another to the right? | |
if (isHorizontalAutoSize && !_options.isHtmlText) width = 100000; | |
if (isVerticalAutoSize) height = 100000; | |
_meshBatch.x = _meshBatch.y = 0; | |
_options.textureScale = Starling.contentScaleFactor; | |
StyledTextLayout.fillMeshBatch(_font, _meshBatch, width, height, _text, _format, _options); | |
if (_style) _meshBatch.style = _style; | |
if (_options.autoSize != TextFieldAutoSize.NONE) | |
{ | |
_textBounds = _meshBatch.getBounds(_meshBatch, _textBounds); | |
if (isHorizontalAutoSize) | |
{ | |
_meshBatch.x = _textBounds.x = -_textBounds.x; | |
_hitArea.width = _textBounds.width; | |
_textBounds.x = 0; | |
} | |
if (isVerticalAutoSize) | |
{ | |
_meshBatch.y = _textBounds.y = -_textBounds.y; | |
_hitArea.height = _textBounds.height; | |
_textBounds.y = 0; | |
} | |
} | |
else | |
{ | |
// hit area doesn't change, and text bounds can be created on demand | |
_textBounds = null; | |
} | |
} | |
private function updateBorder():void | |
{ | |
if (_border == null) return; | |
var width:Number = _hitArea.width; | |
var height:Number = _hitArea.height; | |
var topLine:Quad = _border.getChildAt(0) as Quad; | |
var rightLine:Quad = _border.getChildAt(1) as Quad; | |
var bottomLine:Quad = _border.getChildAt(2) as Quad; | |
var leftLine:Quad = _border.getChildAt(3) as Quad; | |
topLine.width = width; topLine.height = 1; | |
bottomLine.width = width; bottomLine.height = 1; | |
leftLine.width = 1; leftLine.height = height; | |
rightLine.width = 1; rightLine.height = height; | |
rightLine.x = width - 1; | |
bottomLine.y = height - 1; | |
topLine.color = rightLine.color = bottomLine.color = leftLine.color = _format.color; | |
} | |
/** Forces the text to be recomposed before rendering it in the upcoming frame. Any changes | |
* of the TextField itself will automatically trigger recomposition; changes in its | |
* parents or the viewport, however, need to be processed manually. For example, you | |
* might want to force recomposition to fix blurring caused by a scale factor change. | |
*/ | |
public function setRequiresRecomposition():void | |
{ | |
if (!_recomposing) | |
{ | |
_requiresRecomposition = true; | |
setRequiresRedraw(); | |
} | |
} | |
// properties | |
private function get isHorizontalAutoSize():Boolean | |
{ | |
return _options.autoSize == TextFieldAutoSize.HORIZONTAL || | |
_options.autoSize == TextFieldAutoSize.BOTH_DIRECTIONS; | |
} | |
private function get isVerticalAutoSize():Boolean | |
{ | |
return _options.autoSize == TextFieldAutoSize.VERTICAL || | |
_options.autoSize == TextFieldAutoSize.BOTH_DIRECTIONS; | |
} | |
/** Returns the bounds of the text within the text field. */ | |
public function get textBounds():Rectangle | |
{ | |
if (_requiresRecomposition) recompose(); | |
if (_textBounds == null) _textBounds = _meshBatch.getBounds(this); | |
return _textBounds.clone(); | |
} | |
/** @inheritDoc */ | |
public override function getBounds(targetSpace:DisplayObject, out:Rectangle=null):Rectangle | |
{ | |
if (_requiresRecomposition) recompose(); | |
getTransformationMatrix(targetSpace, sMatrix); | |
return RectangleUtil.getBounds(_hitArea, sMatrix, out); | |
} | |
/** @inheritDoc */ | |
public override function hitTest(localPoint:Point):DisplayObject | |
{ | |
if (!visible || !touchable || !hitTestMask(localPoint)) return null; | |
else if (_hitArea.containsPoint(localPoint)) return this; | |
else return null; | |
} | |
/** @inheritDoc */ | |
public override function set width(value:Number):void | |
{ | |
// different to ordinary display objects, changing the size of the text field should | |
// not change the scaling, but make the texture bigger/smaller, while the size | |
// of the text/font stays the same (this applies to the height, as well). | |
_hitArea.width = value / (scaleX || 1.0); | |
setRequiresRecomposition(); | |
} | |
/** @inheritDoc */ | |
public override function set height(value:Number):void | |
{ | |
_hitArea.height = value / (scaleY || 1.0); | |
setRequiresRecomposition(); | |
} | |
/** The displayed text. */ | |
public function get text():String { return _text; } | |
public function set text(value:String):void | |
{ | |
if (value == null) value = ""; | |
if (_text != value) | |
{ | |
_text = value; | |
setRequiresRecomposition(); | |
} | |
} | |
/** The format describes how the text will be rendered, describing the font name and size, | |
* color, alignment, etc. | |
* | |
* <p>Note that you can edit the font properties directly; there's no need to reassign | |
* the format for the changes to show up.</p> | |
* | |
* <listing> | |
* var textField:TextField = new TextField(100, 30, "Hello Starling"); | |
* textField.format.font = "Arial"; | |
* textField.format.color = Color.RED;</listing> | |
* | |
* @default Verdana, 12 pt, black, centered | |
*/ | |
public function get format():TextFormat { return _format; } | |
public function set format(value:TextFormat):void | |
{ | |
if (value == null) throw new ArgumentError("format cannot be null"); | |
_format.copyFrom(value); | |
} | |
/** The options that describe how the letters of a text should be assembled. | |
* This class basically collects all the TextField's properties that are needed | |
* during text composition. Since an instance of 'TextOptions' is passed to the | |
* constructor, you can pass custom options to the compositor. */ | |
protected function get options():TextOptions { return _options; } | |
/** Draws a border around the edges of the text field. Useful for visual debugging. | |
* @default false */ | |
public function get border():Boolean { return _border != null; } | |
public function set border(value:Boolean):void | |
{ | |
if (value && _border == null) | |
{ | |
_border = new Sprite(); | |
addChild(_border); | |
for (var i:int=0; i<4; ++i) | |
_border.addChild(new Quad(1.0, 1.0)); | |
updateBorder(); | |
} | |
else if (!value && _border != null) | |
{ | |
_border.removeFromParent(true); | |
_border = null; | |
} | |
} | |
/** Indicates whether the font size is automatically reduced if the complete text does | |
* not fit into the TextField. @default false */ | |
public function get autoScale():Boolean { return _options.autoScale; } | |
public function set autoScale(value:Boolean):void { _options.autoScale = value; } | |
/** Specifies the type of auto-sizing the TextField will do. | |
* Note that any auto-sizing will implicitly deactivate all auto-scaling. | |
* @default none */ | |
public function get autoSize():String { return _options.autoSize; } | |
public function set autoSize(value:String):void { _options.autoSize = value; } | |
/** Indicates if the text should be wrapped at word boundaries if it does not fit into | |
* the TextField otherwise. @default true */ | |
public function get wordWrap():Boolean { return _options.wordWrap; } | |
public function set wordWrap(value:Boolean):void { _options.wordWrap = value; } | |
/** Indicates if TextField should be batched on rendering. | |
* | |
* <p>This works only with bitmap fonts, and it makes sense only for TextFields with no | |
* more than 10-15 characters. Otherwise, the CPU costs will exceed any gains you get | |
* from avoiding the additional draw call.</p> | |
* | |
* @default false | |
*/ | |
public function get batchable():Boolean { return _meshBatch.batchable; } | |
public function set batchable(value:Boolean):void | |
{ | |
_meshBatch.batchable = value; | |
} | |
/** Indicates if text should be interpreted as HTML code. For a description | |
* of the supported HTML subset, refer to the classic Flash 'TextField' documentation. | |
* Clickable hyperlinks and images are not supported. Only works for | |
* TrueType fonts! @default false */ | |
public function get isHtmlText():Boolean { return _options.isHtmlText; } | |
public function set isHtmlText(value:Boolean):void { _options.isHtmlText = value; } | |
/** An optional style sheet to be used for HTML text. For more information on style | |
* sheets, please refer to the StyleSheet class in the ActionScript 3 API reference. | |
* @default null */ | |
public function get styleSheet():StyleSheet { return _options.styleSheet; } | |
public function set styleSheet(value:StyleSheet):void { _options.styleSheet = value; } | |
/** The padding (in points) that's added to the sides of text that's rendered to a Bitmap. | |
* If your text is truncated on the sides (which may happen if the font returns incorrect | |
* bounds), padding can make up for that. Value must be positive. @default 0.0 */ | |
public function get padding():Number { return _options.padding; } | |
public function set padding(value:Number):void { _options.padding = value; } | |
/** Controls whether or not the instance snaps to the nearest pixel. This can prevent the | |
* object from looking blurry when it's not exactly aligned with the pixels of the screen. | |
* @default true */ | |
public function get pixelSnapping():Boolean { return _meshBatch.pixelSnapping; } | |
public function set pixelSnapping(value:Boolean):void { _meshBatch.pixelSnapping = value } | |
/** The mesh style that is used to render the text. | |
* Note that a style instance may only be used on one mesh at a time. */ | |
public function get style():MeshStyle { return _meshBatch.style; } | |
public function set style(value:MeshStyle):void | |
{ | |
_meshBatch.style = _style = value; | |
setRequiresRecomposition(); | |
} | |
} | |
} |
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
// | |
// aciv | |
package aciv.text { | |
import starling.display.Image; | |
import starling.display.MeshBatch; | |
import starling.text.BitmapChar; | |
import starling.text.BitmapFont; | |
import starling.text.TextFormat; | |
import starling.text.TextOptions; | |
import starling.utils.Align; | |
/** Adapted from starling.text.BitmapFont */ | |
public class StyledTextLayout { | |
/** Draws text into a MeshBatch. */ | |
public static function fillMeshBatch(font :BitmapFont, meshBatch:MeshBatch, width:Number, height:Number, text:String, | |
format:TextFormat, options:TextOptions=null):void | |
{ | |
StyledText.parse(text, format, sStyledText); | |
var plainText :String = sStyledText.text; | |
var charLocations:Vector.<CharLocation> = | |
arrangeChars(font, width, height, plainText, format, options); | |
if (sHelperImage == null) { | |
sHelperImage = new Image(font.texture); | |
} else { | |
sHelperImage.texture = font.texture; | |
} | |
var curRunIdx :int = 0; | |
var curRun :StyleRun = sStyledText.runs[curRunIdx]; | |
var curRunEnd :int = curRun.length; | |
var textLength :int = plainText.length; | |
var charIdx :int = 0; | |
for (var textIdx :int = 0; textIdx < textLength; ++textIdx) { | |
if (textIdx >= curRunEnd) { | |
curRun = sStyledText.runs[++curRunIdx]; | |
curRunEnd += curRun.length; | |
} | |
// ArrangeChars omits non-printing characters from its output, | |
// so we skip over them here. | |
if (isNonPrintingChar(plainText.charCodeAt(textIdx))) { | |
continue; | |
} | |
var charLocation :CharLocation = charLocations[charIdx]; | |
sHelperImage.color = curRun.color; | |
sHelperImage.texture = charLocation.char.texture; | |
sHelperImage.readjustSize(); | |
sHelperImage.x = charLocation.x; | |
sHelperImage.y = charLocation.y; | |
sHelperImage.scale = charLocation.scale; | |
meshBatch.addMesh(sHelperImage); | |
charIdx++; | |
} | |
CharLocation.rechargePool(); | |
sStyledText.reset(); | |
} | |
private static function isNonPrintingChar (charCode :int) :Boolean { | |
switch (charCode) { | |
case CHAR_SPACE: | |
case CHAR_TAB: | |
case CHAR_NEWLINE: | |
case CHAR_CARRIAGE_RETURN: | |
return true; | |
default: | |
return false; | |
} | |
} | |
/** Arranges the characters of a text inside a rectangle, adhering to the given settings. | |
* Returns a Vector of CharLocations. */ | |
private static function arrangeChars(font :BitmapFont, width:Number, height:Number, text:String, | |
format :TextFormat, options:TextOptions):Vector.<CharLocation> | |
{ | |
if (text == null || text.length == 0) return CharLocation.vectorFromPool(); | |
if (options == null) options = sDefaultOptions; | |
var kerning:Boolean = format.kerning; | |
var leading:Number = format.leading; | |
var hAlign:String = format.horizontalAlign; | |
var vAlign:String = format.verticalAlign; | |
var fontSize:Number = format.size; | |
var autoScale:Boolean = options.autoScale; | |
var wordWrap:Boolean = options.wordWrap; | |
var finished:Boolean = false; | |
var charLocation:CharLocation; | |
var numChars:int; | |
var containerWidth:Number; | |
var containerHeight:Number; | |
var scale:Number; | |
var i:int, j:int; | |
if (fontSize < 0) fontSize *= - font.size; | |
while (!finished) | |
{ | |
sLines.length = 0; | |
scale = fontSize / font.size; | |
containerWidth = (width - 2 * font.padding) / scale; | |
containerHeight = (height - 2 * font.padding) / scale; | |
if (font.lineHeight <= containerHeight) | |
{ | |
var lastWhiteSpace:int = -1; | |
var lastCharID:int = -1; | |
var currentX:Number = 0; | |
var currentY:Number = 0; | |
var currentLine:Vector.<CharLocation> = CharLocation.vectorFromPool(); | |
numChars = text.length; | |
for (i=0; i<numChars; ++i) | |
{ | |
var lineFull:Boolean = false; | |
var charID:int = text.charCodeAt(i); | |
var char:BitmapChar = font.getChar(charID); | |
if (charID == CHAR_NEWLINE || charID == CHAR_CARRIAGE_RETURN) | |
{ | |
lineFull = true; | |
} | |
else if (char == null) | |
{ | |
trace("[Starling] Font: "+ font.name + " missing character: " + text.charAt(i) + " id: "+ charID); | |
} | |
else | |
{ | |
if (charID == CHAR_SPACE || charID == CHAR_TAB) | |
lastWhiteSpace = i; | |
if (kerning) | |
currentX += char.getKerning(lastCharID); | |
charLocation = CharLocation.instanceFromPool(char); | |
charLocation.x = currentX + char.xOffset; | |
charLocation.y = currentY + char.yOffset; | |
currentLine[currentLine.length] = charLocation; // push | |
currentX += char.xAdvance; | |
lastCharID = charID; | |
if (charLocation.x + char.width > containerWidth) | |
{ | |
if (wordWrap) | |
{ | |
// when autoscaling, we must not split a word in half -> restart | |
if (autoScale && lastWhiteSpace == -1) | |
break; | |
// remove characters and add them again to next line | |
var numCharsToRemove:int = lastWhiteSpace == -1 ? 1 : i - lastWhiteSpace; | |
for (j=0; j<numCharsToRemove; ++j) // faster than 'splice' | |
currentLine.pop(); | |
if (currentLine.length == 0) | |
break; | |
i -= numCharsToRemove; | |
} | |
else | |
{ | |
if (autoScale) break; | |
currentLine.pop(); | |
// continue with next line, if there is one | |
while (i < numChars - 1 && text.charCodeAt(i) != CHAR_NEWLINE) | |
++i; | |
} | |
lineFull = true; | |
} | |
} | |
if (i == numChars - 1) | |
{ | |
sLines[sLines.length] = currentLine; // push | |
finished = true; | |
} | |
else if (lineFull) | |
{ | |
sLines[sLines.length] = currentLine; // push | |
if (lastWhiteSpace == i) | |
currentLine.pop(); | |
if (currentY + leading + 2 * font.lineHeight <= containerHeight) | |
{ | |
currentLine = CharLocation.vectorFromPool(); | |
currentX = 0; | |
currentY += font.lineHeight + leading; | |
lastWhiteSpace = -1; | |
lastCharID = -1; | |
} | |
else | |
{ | |
break; | |
} | |
} | |
} // for each char | |
} // if (font.lineHeight <= containerHeight) | |
if (autoScale && !finished && fontSize > 3) | |
fontSize -= 1; | |
else | |
finished = true; | |
} // while (!finished) | |
var finalLocations:Vector.<CharLocation> = CharLocation.vectorFromPool(); | |
var numLines:int = sLines.length; | |
var bottom:Number = currentY + font.lineHeight; | |
var yOffset:int = 0; | |
if (vAlign == Align.BOTTOM) yOffset = containerHeight - bottom; | |
else if (vAlign == Align.CENTER) yOffset = (containerHeight - bottom) / 2; | |
for (var lineID:int=0; lineID<numLines; ++lineID) | |
{ | |
var line:Vector.<CharLocation> = sLines[lineID]; | |
numChars = line.length; | |
if (numChars == 0) continue; | |
var xOffset:int = 0; | |
var lastLocation:CharLocation = line[line.length-1]; | |
var right:Number = lastLocation.x - lastLocation.char.xOffset | |
+ lastLocation.char.xAdvance; | |
if (hAlign == Align.RIGHT) xOffset = containerWidth - right; | |
else if (hAlign == Align.CENTER) xOffset = (containerWidth - right) / 2; | |
for (var c:int=0; c<numChars; ++c) | |
{ | |
charLocation = line[c]; | |
charLocation.x = scale * (charLocation.x + xOffset + font.offsetX) + font.padding; | |
charLocation.y = scale * (charLocation.y + yOffset + font.offsetY) + font.padding; | |
charLocation.scale = scale; | |
if (charLocation.char.width > 0 && charLocation.char.height > 0) | |
finalLocations[finalLocations.length] = charLocation; | |
} | |
} | |
return finalLocations; | |
} | |
// helper objects | |
private static var sHelperImage :Image; | |
private static var sStyledText :StyledText = new StyledText(); | |
private static var sLines:Array = []; | |
private static var sDefaultOptions:TextOptions = new TextOptions(); | |
private static const CHAR_SPACE:int = 32; | |
private static const CHAR_TAB:int = 9; | |
private static const CHAR_NEWLINE:int = 10; | |
private static const CHAR_CARRIAGE_RETURN:int = 13; | |
} | |
} | |
import starling.text.BitmapChar; | |
/** Coped from starling.text.BitmapFont */ | |
class CharLocation | |
{ | |
public var char:BitmapChar; | |
public var scale:Number; | |
public var x:Number; | |
public var y:Number; | |
public function CharLocation(char:BitmapChar) | |
{ | |
reset(char); | |
} | |
private function reset(char:BitmapChar):CharLocation | |
{ | |
this.char = char; | |
return this; | |
} | |
// pooling | |
private static var sInstancePool:Vector.<CharLocation> = new <CharLocation>[]; | |
private static var sVectorPool:Array = []; | |
private static var sInstanceLoan:Vector.<CharLocation> = new <CharLocation>[]; | |
private static var sVectorLoan:Array = []; | |
public static function instanceFromPool(char:BitmapChar):CharLocation | |
{ | |
var instance:CharLocation = sInstancePool.length > 0 ? | |
sInstancePool.pop() : new CharLocation(char); | |
instance.reset(char); | |
sInstanceLoan[sInstanceLoan.length] = instance; | |
return instance; | |
} | |
public static function vectorFromPool():Vector.<CharLocation> | |
{ | |
var vector:Vector.<CharLocation> = sVectorPool.length > 0 ? | |
sVectorPool.pop() : new <CharLocation>[]; | |
vector.length = 0; | |
sVectorLoan[sVectorLoan.length] = vector; | |
return vector; | |
} | |
public static function rechargePool():void | |
{ | |
var instance:CharLocation; | |
var vector:Vector.<CharLocation>; | |
while (sInstanceLoan.length > 0) | |
{ | |
instance = sInstanceLoan.pop(); | |
instance.char = null; | |
sInstancePool[sInstancePool.length] = instance; | |
} | |
while (sVectorLoan.length > 0) | |
{ | |
vector = sVectorLoan.pop(); | |
vector.length = 0; | |
sVectorPool[sVectorPool.length] = vector; | |
} | |
} | |
} |
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
// | |
// aciv | |
package aciv.text { | |
public class StyleRun { | |
public var length :uint; | |
public var color :uint; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment