Skip to content

Instantly share code, notes, and snippets.

@Kalinovych
Forked from filharvey/DistanceFieldFont
Created February 9, 2014 19:10
Show Gist options
  • Save Kalinovych/8904482 to your computer and use it in GitHub Desktop.
Save Kalinovych/8904482 to your computer and use it in GitHub Desktop.
package starling.extensions.DistanceFieldFont
{
import flash.geom.Rectangle;
import flash.utils.Dictionary;
import starling.display.Image;
import starling.text.BitmapChar;
import starling.textures.Texture;
import starling.textures.TextureSmoothing;
import starling.utils.HAlign;
import starling.utils.VAlign;
public class DistanceFieldFont
{
/** Use this constant for the <code>fontSize</code> property of the TextField class to
* render the bitmap font in exactly the size it was created. */
public static const NATIVE_SIZE:int = -1;
/** The font name of the embedded minimal bitmap font. Use this e.g. for debug output. */
public static const MINI:String = "mini";
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;
private var mTexture:Texture;
private var mChars:Dictionary;
private var mName:String;
private var mSize:Number;
private var mLineHeight:Number;
private var mBaseline:Number;
private var mHelperImage:Image;
private var mCharLocationPool:Vector.<CharLocation>;
/** Creates a bitmap font by parsing an XML file and uses the specified texture.
* If you don't pass any data, the "mini" font will be created. */
public function DistanceFieldFont(texture:Texture=null, fontXml:XML=null)
{
// if no texture is passed in, we create the minimal, embedded font
if (texture == null && fontXml == null)
{
throw new ArgumentError("A Texture font and XML file is needed.");
}
mName = "unknown";
mLineHeight = mSize = mBaseline = 14;
mTexture = texture;
mChars = new Dictionary();
mHelperImage = new Image(texture);
mCharLocationPool = new <CharLocation>[];
if (fontXml) parseFontXml(fontXml);
}
/** Disposes the texture of the bitmap font! */
public function dispose():void
{
if (mTexture)
mTexture.dispose();
}
private function parseFontXml(fontXml:XML):void
{
var scale:Number = mTexture.scale;
var frame:Rectangle = mTexture.frame;
mName = fontXml.info.attribute("face");
mSize = parseFloat(fontXml.info.attribute("size")) / scale;
mLineHeight = parseFloat(fontXml.common.attribute("lineHeight")) / scale;
mBaseline = parseFloat(fontXml.common.attribute("base")) / scale;
if (fontXml.info.attribute("smooth").toString() == "0")
smoothing = TextureSmoothing.NONE;
if (mSize <= 0)
{
trace("[Starling] Warning: invalid font size in '" + mName + "' font.");
mSize = (mSize == 0.0 ? 16.0 : mSize * -1.0);
}
for each (var charElement:XML in fontXml.chars.char)
{
var id:int = parseInt(charElement.attribute("id"));
var xOffset:Number = parseFloat(charElement.attribute("xoffset")) / scale;
var yOffset:Number = parseFloat(charElement.attribute("yoffset")) / scale;
var xAdvance:Number = parseFloat(charElement.attribute("xadvance")) / scale;
var region:Rectangle = new Rectangle();
region.x = parseFloat(charElement.attribute("x")) / scale + frame.x;
region.y = parseFloat(charElement.attribute("y")) / scale + frame.y;
region.width = parseFloat(charElement.attribute("width")) / scale;
region.height = parseFloat(charElement.attribute("height")) / scale;
var texture:Texture = Texture.fromTexture(mTexture, region);
var bitmapChar:BitmapChar = new BitmapChar(id, texture, xOffset, yOffset, xAdvance);
addChar(id, bitmapChar);
}
for each (var kerningElement:XML in fontXml.kernings.kerning)
{
var first:int = parseInt(kerningElement.attribute("first"));
var second:int = parseInt(kerningElement.attribute("second"));
var amount:Number = parseFloat(kerningElement.attribute("amount")) / scale;
if (second in mChars) getChar(second).addKerning(first, amount);
}
}
/** Returns a single bitmap char with a certain character ID. */
public function getChar(charID:int):BitmapChar
{
return mChars[charID];
}
/** Adds a bitmap char with a certain character ID. */
public function addChar(charID:int, bitmapChar:BitmapChar):void
{
mChars[charID] = bitmapChar;
}
/** Draws text into a QuadBatch. */
public function fillQuadBatch(quadBatch:DistanceFieldQuadBatch, width:Number, height:Number, text:String,
fontSize:Number=-1, color:uint=0xffffff,
hAlign:String="center", vAlign:String="center",
autoScale:Boolean=true,
kerning:Boolean=true):void
{
var charLocations:Vector.<CharLocation> = arrangeChars(width, height, text, fontSize,
hAlign, vAlign, autoScale, kerning);
var numChars:int = charLocations.length;
mHelperImage.color = color;
if (numChars > 8192)
throw new ArgumentError("Bitmap Font text is limited to 8192 characters.");
for (var i:int=0; i<numChars; ++i)
{
var charLocation:CharLocation = charLocations[i];
mHelperImage.texture = charLocation.char.texture;
mHelperImage.readjustSize();
mHelperImage.x = charLocation.x;
mHelperImage.y = charLocation.y;
mHelperImage.scaleX = mHelperImage.scaleY = charLocation.scale;
quadBatch.addImage(mHelperImage);
}
}
/** Arranges the characters of a text inside a rectangle, adhering to the given settings.
* Returns a Vector of CharLocations. */
private function arrangeChars(width:Number, height:Number, text:String, fontSize:Number=-1,
hAlign:String="center", vAlign:String="center",
autoScale:Boolean=true, kerning:Boolean=true):Vector.<CharLocation>
{
if (text == null || text.length == 0) return new <CharLocation>[];
if (fontSize < 0) fontSize *= -mSize;
var lines:Vector.<Vector.<CharLocation>>;
var finished:Boolean = false;
var charLocation:CharLocation;
var numChars:int;
var containerWidth:Number;
var containerHeight:Number;
var scale:Number;
while (!finished)
{
scale = fontSize / mSize;
containerWidth = width / scale;
containerHeight = height / scale;
lines = new Vector.<Vector.<CharLocation>>();
if (mLineHeight <= containerHeight)
{
var lastWhiteSpace:int = -1;
var lastCharID:int = -1;
var currentX:Number = 0;
var currentY:Number = 0;
var currentLine:Vector.<CharLocation> = new <CharLocation>[];
numChars = text.length;
for (var i:int=0; i<numChars; ++i)
{
var lineFull:Boolean = false;
var charID:int = text.charCodeAt(i);
var char:BitmapChar = getChar(charID);
if (charID == CHAR_NEWLINE || charID == CHAR_CARRIAGE_RETURN)
{
lineFull = true;
}
else if (char == null)
{
trace("[Starling] Missing character: " + charID);
}
else
{
if (charID == CHAR_SPACE || charID == CHAR_TAB)
lastWhiteSpace = i;
if (kerning)
currentX += char.getKerning(lastCharID);
charLocation = mCharLocationPool.length ?
mCharLocationPool.pop() : new CharLocation(char);
charLocation.char = char;
charLocation.x = currentX + char.xOffset;
charLocation.y = currentY + char.yOffset;
currentLine.push(charLocation);
currentX += char.xAdvance;
lastCharID = charID;
if (charLocation.x + char.width > containerWidth)
{
// remove characters and add them again to next line
var numCharsToRemove:int = lastWhiteSpace == -1 ? 1 : i - lastWhiteSpace;
var removeIndex:int = currentLine.length - numCharsToRemove;
currentLine.splice(removeIndex, numCharsToRemove);
if (currentLine.length == 0)
break;
i -= numCharsToRemove;
lineFull = true;
}
}
if (i == numChars - 1)
{
lines.push(currentLine);
finished = true;
}
else if (lineFull)
{
lines.push(currentLine);
if (lastWhiteSpace == i)
currentLine.pop();
if (currentY + 2*mLineHeight <= containerHeight)
{
currentLine = new <CharLocation>[];
currentX = 0;
currentY += mLineHeight;
lastWhiteSpace = -1;
lastCharID = -1;
}
else
{
break;
}
}
} // for each char
} // if (mLineHeight <= containerHeight)
if (autoScale && !finished)
{
fontSize -= 1;
lines.length = 0;
}
else
{
finished = true;
}
} // while (!finished)
var finalLocations:Vector.<CharLocation> = new <CharLocation>[];
var numLines:int = lines.length;
var bottom:Number = currentY + mLineHeight;
var yOffset:int = 0;
if (vAlign == VAlign.BOTTOM) yOffset = containerHeight - bottom;
else if (vAlign == VAlign.CENTER) yOffset = (containerHeight - bottom) / 2;
for (var lineID:int=0; lineID<numLines; ++lineID)
{
var line:Vector.<CharLocation> = lines[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 == HAlign.RIGHT) xOffset = containerWidth - right;
else if (hAlign == HAlign.CENTER) xOffset = (containerWidth - right) / 2;
for (var c:int=0; c<numChars; ++c)
{
charLocation = line[c];
charLocation.x = scale * (charLocation.x + xOffset);
charLocation.y = scale * (charLocation.y + yOffset);
charLocation.scale = scale;
if (charLocation.char.width > 0 && charLocation.char.height > 0)
finalLocations.push(charLocation);
// return to pool for next call to "arrangeChars"
mCharLocationPool.push(charLocation);
}
}
return finalLocations;
}
/** The name of the font as it was parsed from the font file. */
public function get name():String { return mName; }
/** The native size of the font. */
public function get size():Number { return mSize; }
/** The height of one line in pixels. */
public function get lineHeight():Number { return mLineHeight; }
public function set lineHeight(value:Number):void { mLineHeight = value; }
/** The smoothing filter that is used for the texture. */
public function get smoothing():String { return mHelperImage.smoothing; }
public function set smoothing(value:String):void { mHelperImage.smoothing = value; }
/** The baseline of the font. */
public function get baseline():Number { return mBaseline; }
}
}
import starling.text.BitmapChar;
class CharLocation
{
public var char:BitmapChar;
public var scale:Number;
public var x:Number;
public var y:Number;
public function CharLocation(char:BitmapChar)
{
this.char = char;
}
}
package starling.extensions.DistanceFieldFont
{
import flash.geom.Matrix;
import flash.geom.Point;
import flash.text.TextFormatAlign;
import feathers.core.FeathersControl;
import feathers.core.ITextRenderer;
import feathers.text.BitmapFontTextFormat;
import starling.core.RenderSupport;
import starling.display.Image;
import starling.text.BitmapChar;
import starling.text.BitmapFont;
import starling.textures.Texture;
import starling.textures.TextureSmoothing;
public class DistanceFieldFontTextRenderer extends FeathersControl implements ITextRenderer
{
/**
* @private
*/
private static var HELPER_IMAGE:Image;
/**
* @private
*/
private static const HELPER_MATRIX:Matrix = new Matrix();
/**
* @private
*/
private static const HELPER_POINT:Point = new Point();
/**
* @private
*/
private static const CHARACTER_ID_SPACE:int = 32;
/**
* @private
*/
private static const CHARACTER_ID_TAB:int = 9;
/**
* @private
*/
private static const CHARACTER_ID_LINE_FEED:int = 10;
/**
* @private
*/
private static const CHARACTER_ID_CARRIAGE_RETURN:int = 13;
/**
* @private
*/
private static var CHARACTER_BUFFER:Vector.<CharLocation>;
/**
* @private
*/
private static var CHAR_LOCATION_POOL:Vector.<CharLocation>;
/**
* Constructor.
*/
public function DistanceFieldFontTextRenderer()
{
if(!CHAR_LOCATION_POOL)
{
//compiler doesn't like referencing CharLocation class in a
//static constant
CHAR_LOCATION_POOL = new <CharLocation>[];
}
if(!CHARACTER_BUFFER)
{
CHARACTER_BUFFER = new <CharLocation>[];
}
this.isQuickHitAreaEnabled = true;
}
/**
* @private
*/
protected var _characterBatch:DistanceFieldQuadBatch;
/**
* @private
*/
protected var _locations:Vector.<CharLocation>;
/**
* @private
*/
protected var _images:Vector.<Image>;
/**
* @private
*/
protected var _imagesCache:Vector.<Image>;
/**
* @private
*/
protected var currentTextFormat:BitmapFontTextFormat;
/**
* @private
*/
protected var _textFormat:BitmapFontTextFormat;
/**
* The font and styles used to draw the text.
*/
public function get textFormat():BitmapFontTextFormat
{
return this._textFormat;
}
/**
* @private
*/
public function set textFormat(value:BitmapFontTextFormat):void
{
if(this._textFormat == value)
{
return;
}
this._textFormat = value;
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @private
*/
protected var _disabledTextFormat:BitmapFontTextFormat;
/**
* The font and styles used to draw the text when the label is disabled.
*/
public function get disabledTextFormat():BitmapFontTextFormat
{
return this._disabledTextFormat;
}
/**
* @private
*/
public function set disabledTextFormat(value:BitmapFontTextFormat):void
{
if(this._disabledTextFormat == value)
{
return;
}
this._disabledTextFormat = value;
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @private
*/
protected var _text:String = null;
/**
* The text to display.
*/
public function get text():String
{
return this._text;
}
/**
* @private
*/
public function set text(value:String):void
{
if(this._text == value)
{
return;
}
this._text = value;
this.invalidate(INVALIDATION_FLAG_DATA);
}
/**
* @private
*/
protected var _smoothing:String = TextureSmoothing.BILINEAR;
[Inspectable(type="String",enumeration="bilinear,trilinear,none")]
/**
* A smoothing value passed to each character image.
*
* @see starling.textures.TextureSmoothing
*/
public function get smoothing():String
{
return this._smoothing;
}
/**
* @private
*/
public function set smoothing(value:String):void
{
if(this._smoothing == value)
{
return;
}
this._smoothing = value;
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @private
*/
protected var _wordWrap:Boolean = false;
/**
* If the width or maxWidth values are set, then the text will continue
* on the next line, if it is too long.
*/
public function get wordWrap():Boolean
{
return _wordWrap;
}
/**
* @private
*/
public function set wordWrap(value:Boolean):void
{
if(this._wordWrap == value)
{
return;
}
this._wordWrap = value;
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @private
*/
protected var _snapToPixels:Boolean = true;
/**
* Determines if characters should be snapped to the nearest whole pixel
* when rendered.
*/
public function get snapToPixels():Boolean
{
return _snapToPixels;
}
/**
* @private
*/
public function set snapToPixels(value:Boolean):void
{
if(this._snapToPixels == value)
{
return;
}
this._snapToPixels = value;
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @private
*/
protected var _truncationText:String = "...";
/**
* The text to display at the end of the label if it is truncated.
*/
public function get truncationText():String
{
return _truncationText;
}
/**
* @private
*/
public function set truncationText(value:String):void
{
if(this._truncationText == value)
{
return;
}
this._truncationText = value;
this.invalidate(INVALIDATION_FLAG_DATA);
}
/**
* @private
*/
protected var _useSeparateBatch:Boolean = true;
/**
* Determines if the characters are batched normally by Starling or if
* they're batched separately. Batching separately may improve
* performance for text that changes often, while batching normally
* may be better when a lot of text is displayed on screen at once.
*/
public function get useSeparateBatch():Boolean
{
return this._useSeparateBatch;
}
/**
* @private
*/
public function set useSeparateBatch(value:Boolean):void
{
if(this._useSeparateBatch == value)
{
return;
}
this._useSeparateBatch = value;
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @inheritDoc
*/
public function get baseline():Number
{
if(!this._textFormat)
{
return 0;
}
const font:BitmapFont = this._textFormat.font;
const formatSize:Number = this._textFormat.size;
const fontSizeScale:Number = isNaN(formatSize) ? 1 : (formatSize / font.size);
if(isNaN(font.baseline))
{
return font.lineHeight * fontSizeScale;
}
return font.baseline * fontSizeScale;
}
/**
* @private
*/
override public function dispose():void
{
this.moveLocationsToPool();
super.dispose();
}
/**
* @private
*/
override public function render(support:RenderSupport, parentAlpha:Number):void
{
var offsetX:Number = 0;
var offsetY:Number = 0;
if(this._snapToPixels)
{
this.getTransformationMatrix(this.stage, HELPER_MATRIX);
offsetX = Math.round(HELPER_MATRIX.tx) - HELPER_MATRIX.tx;
offsetY = Math.round(HELPER_MATRIX.ty) - HELPER_MATRIX.ty;
}
if(this._locations)
{
const locationCount:int = this._locations.length;
for(var i:int = 0; i < locationCount; i++)
{
var location:CharLocation = this._locations[i];
var image:Image = this._images[i];
image.x = offsetX + location.x;
image.y = offsetY + location.y;
}
}
else
{
this._characterBatch.x = offsetX;
this._characterBatch.y = offsetY;
}
super.render(support, parentAlpha);
}
/**
* @inheritDoc
*/
public function measureText(result:Point = null):Point
{
if(this.isInvalid(INVALIDATION_FLAG_STYLES) || this.isInvalid(INVALIDATION_FLAG_STATE))
{
this.refreshTextFormat();
}
if(!result)
{
result = new Point();
}
else
{
result.x = result.y = 0;
}
if(!this.currentTextFormat || !this._text)
{
return result;
}
const font:BitmapFont = this.currentTextFormat.font;
const customSize:Number = this.currentTextFormat.size;
const customLetterSpacing:Number = this.currentTextFormat.letterSpacing;
const isKerningEnabled:Boolean = this.currentTextFormat.isKerningEnabled;
const scale:Number = isNaN(customSize) ? 1 : (customSize / font.size);
const lineHeight:Number = font.lineHeight * scale;
const maxLineWidth:Number = !isNaN(this.explicitWidth) ? this.explicitWidth : this._maxWidth;
const isAligned:Boolean = this.currentTextFormat.align != TextFormatAlign.LEFT;
var maxX:Number = 0;
var currentX:Number = 0;
var currentY:Number = 0;
var previousCharID:Number = NaN;
var charCount:int = this._text.length;
var startXOfPreviousWord:Number = 0;
var widthOfWhitespaceAfterWord:Number = 0;
var wordCountForLine:int = 0;
var line:String = "";
var word:String = "";
for(var i:int = 0; i < charCount; i++)
{
var charID:int = this._text.charCodeAt(i);
if(charID == CHARACTER_ID_LINE_FEED || charID == CHARACTER_ID_CARRIAGE_RETURN) //new line \n or \r
{
currentX = Math.max(0, currentX - customLetterSpacing);
maxX = Math.max(maxX, currentX);
previousCharID = NaN;
currentX = 0;
currentY += lineHeight;
startXOfPreviousWord = 0;
wordCountForLine = 0;
widthOfWhitespaceAfterWord = 0;
continue;
}
var charData:BitmapChar = font.getChar(charID);
if(!charData)
{
trace("Missing character " + String.fromCharCode(charID) + " in font " + font.name + ".");
continue;
}
if(isKerningEnabled && !isNaN(previousCharID))
{
currentX += charData.getKerning(previousCharID);
}
var offsetX:Number = charData.xAdvance * scale;
if(this._wordWrap)
{
var previousCharIsWhitespace:Boolean = previousCharID == CHARACTER_ID_SPACE || previousCharID == CHARACTER_ID_TAB;
if(charID == CHARACTER_ID_SPACE || charID == CHARACTER_ID_TAB)
{
if(!previousCharIsWhitespace)
{
widthOfWhitespaceAfterWord = 0;
}
widthOfWhitespaceAfterWord += offsetX;
}
else if(previousCharIsWhitespace)
{
startXOfPreviousWord = currentX;
wordCountForLine++;
line += word;
word = "";
}
if(wordCountForLine > 0 && (currentX + offsetX) > maxLineWidth)
{
maxX = Math.max(maxX, startXOfPreviousWord - widthOfWhitespaceAfterWord);
previousCharID = NaN;
currentX -= startXOfPreviousWord;
currentY += lineHeight;
startXOfPreviousWord = 0;
widthOfWhitespaceAfterWord = 0;
wordCountForLine = 0;
line = "";
}
}
currentX += offsetX + customLetterSpacing;
previousCharID = charID;
word += String.fromCharCode(charID);
}
currentX = Math.max(0, currentX - customLetterSpacing);
maxX = Math.max(maxX, currentX);
result.x = maxX;
result.y = currentY + font.lineHeight * scale;
return result;
}
/**
* @private
*/
override protected function draw():void
{
const dataInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_DATA);
const stylesInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_STYLES);
const sizeInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_SIZE);
const stateInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_STATE);
if(stylesInvalid || stateInvalid)
{
this.refreshTextFormat();
}
if(dataInvalid || stylesInvalid || sizeInvalid)
{
this.refreshBatching();
if(!this.currentTextFormat || !this._text)
{
this.setSizeInternal(0, 0, false);
return;
}
this.layoutCharacters(HELPER_POINT);
this.setSizeInternal(HELPER_POINT.x, HELPER_POINT.y, false);
}
}
/**
* @private
*/
protected function refreshBatching():void
{
this.moveLocationsToPool();
if(this._useSeparateBatch)
{
if(!this._characterBatch)
{
this._characterBatch = new DistanceFieldQuadBatch();
this._characterBatch.touchable = false;
this.addChild(this._characterBatch);
}
this._characterBatch.reset();
this._locations = null;
if(this._images)
{
const imageCount:int = this._images.length;
for(var i:int = 0; i < imageCount; i++)
{
var image:Image = this._images[i];
image.removeFromParent(true);
}
}
this._images = null;
this._imagesCache = null;
}
else
{
if(this._characterBatch)
{
this._characterBatch.removeFromParent(true);
this._characterBatch = null;
}
if(!this._locations)
{
this._locations = new <CharLocation>[];
}
if(!this._images)
{
this._images = new <Image>[];
}
if(!this._imagesCache)
{
this._imagesCache = new <Image>[];
}
}
}
/**
* @private
*/
protected function layoutCharacters(result:Point = null):Point
{
if(!result)
{
result = new Point();
}
const font:BitmapFont = this.currentTextFormat.font;
const customSize:Number = this.currentTextFormat.size;
const customLetterSpacing:Number = this.currentTextFormat.letterSpacing;
const isKerningEnabled:Boolean = this.currentTextFormat.isKerningEnabled;
const scale:Number = isNaN(customSize) ? 1 : (customSize / font.size);
const lineHeight:Number = font.lineHeight * scale;
const maxLineWidth:Number = !isNaN(this.explicitWidth) ? this.explicitWidth : this._maxWidth;
const textToDraw:String = this.getTruncatedText();
const isAligned:Boolean = this.currentTextFormat.align != TextFormatAlign.LEFT;
CHARACTER_BUFFER.length = 0;
if(!this._useSeparateBatch)
{
//cache the old images for reuse
const temp:Vector.<Image> = this._imagesCache;
this._imagesCache = this._images;
this._images = temp;
}
var maxX:Number = 0;
var currentX:Number = 0;
var currentY:Number = 0;
var previousCharID:Number = NaN;
var isWordComplete:Boolean = false;
var startXOfPreviousWord:Number = 0;
var widthOfWhitespaceAfterWord:Number = 0;
var wordLength:int = 0;
var wordCountForLine:int = 0;
const charCount:int = textToDraw ? textToDraw.length : 0;
for(var i:int = 0; i < charCount; i++)
{
isWordComplete = false;
var charID:int = textToDraw.charCodeAt(i);
if(charID == CHARACTER_ID_LINE_FEED || charID == CHARACTER_ID_CARRIAGE_RETURN) //new line \n or \r
{
currentX = Math.max(0, currentX - customLetterSpacing);
if(this._wordWrap || isAligned)
{
this.alignBuffer(maxLineWidth, currentX, 0);
this.addBufferToBatch(0);
}
maxX = Math.max(maxX, currentX);
previousCharID = NaN;
currentX = 0;
currentY += lineHeight;
startXOfPreviousWord = 0;
widthOfWhitespaceAfterWord = 0;
wordLength = 0;
wordCountForLine = 0;
continue;
}
var charData:BitmapChar = font.getChar(charID);
if(!charData)
{
trace("Missing character " + String.fromCharCode(charID) + " in font " + font.name + ".");
continue;
}
if(isKerningEnabled && !isNaN(previousCharID))
{
currentX += charData.getKerning(previousCharID);
}
var offsetX:Number = charData.xAdvance * scale;
if(this._wordWrap)
{
var previousCharIsWhitespace:Boolean = previousCharID == CHARACTER_ID_SPACE || previousCharID == CHARACTER_ID_TAB;
if(charID == CHARACTER_ID_SPACE || charID == CHARACTER_ID_TAB)
{
if(!previousCharIsWhitespace)
{
widthOfWhitespaceAfterWord = 0;
}
widthOfWhitespaceAfterWord += offsetX;
}
else if(previousCharIsWhitespace)
{
startXOfPreviousWord = currentX;
wordLength = 0;
wordCountForLine++;
isWordComplete = true;
}
//we may need to move to a new line at the same time
//that our previous word in the buffer can be batched
//so we need to add the buffer here rather than after
//the next section
if(isWordComplete && !isAligned)
{
this.addBufferToBatch(0);
}
if(wordCountForLine > 0 && (currentX + offsetX) > maxLineWidth)
{
if(isAligned)
{
this.trimBuffer(wordLength);
this.alignBuffer(maxLineWidth, startXOfPreviousWord - widthOfWhitespaceAfterWord, wordLength);
this.addBufferToBatch(wordLength);
}
this.moveBufferedCharacters(-startXOfPreviousWord, lineHeight, 0);
maxX = Math.max(maxX, startXOfPreviousWord - widthOfWhitespaceAfterWord);
previousCharID = NaN;
currentX -= startXOfPreviousWord;
currentY += lineHeight;
startXOfPreviousWord = 0;
widthOfWhitespaceAfterWord = 0;
wordLength = 0;
isWordComplete = false;
wordCountForLine = 0;
}
}
if(this._wordWrap || isAligned || !this._useSeparateBatch)
{
var charLocation:CharLocation = CHAR_LOCATION_POOL.length > 0 ? CHAR_LOCATION_POOL.shift() : new CharLocation();
charLocation.char = charData;
charLocation.x = currentX + charData.xOffset * scale;
charLocation.y = currentY + charData.yOffset * scale;
charLocation.scale = scale;
if(this._wordWrap || isAligned)
{
CHARACTER_BUFFER.push(charLocation);
wordLength++;
}
else
{
this.addLocation(charLocation);
}
}
else
{
this.addCharacterToBatch(charData, currentX + charData.xOffset * scale, currentY + charData.yOffset * scale, scale);
}
currentX += offsetX + customLetterSpacing;
previousCharID = charID;
}
currentX = Math.max(0, currentX - customLetterSpacing);
if(this._wordWrap || isAligned)
{
this.alignBuffer(maxLineWidth, currentX, 0);
this.addBufferToBatch(0);
}
maxX = Math.max(maxX, currentX);
if(!this._useSeparateBatch)
{
//clear the cache of old images that are no longer needed
const cacheLength:int = this._imagesCache.length;
for(i = 0; i < cacheLength; i++)
{
var image:Image = this._imagesCache.shift();
image.removeFromParent(true);
}
}
result.x = maxX;
result.y = currentY + font.lineHeight * scale;
return result;
}
/**
* @private
*/
protected function trimBuffer(skipCount:int):void
{
var countToRemove:int = 0;
const charCount:int = CHARACTER_BUFFER.length - skipCount;
for(var i:int = charCount - 1; i >= 0; i--)
{
var charLocation:CharLocation = CHARACTER_BUFFER[i];
var charData:BitmapChar = charLocation.char;
var charID:int = charData.charID;
if(charID == CHARACTER_ID_SPACE || charID == CHARACTER_ID_TAB)
{
countToRemove++;
}
else
{
break;
}
}
if(countToRemove > 0)
{
CHARACTER_BUFFER.splice(i + 1, countToRemove);
}
}
/**
* @private
*/
protected function alignBuffer(maxLineWidth:Number, currentLineWidth:Number, skipCount:int):void
{
const align:String = this.currentTextFormat.align;
if(align == TextFormatAlign.CENTER)
{
this.moveBufferedCharacters((maxLineWidth - currentLineWidth) / 2, 0, skipCount);
}
else if(align == TextFormatAlign.RIGHT)
{
this.moveBufferedCharacters(maxLineWidth - currentLineWidth, 0, skipCount);
}
}
/**
* @private
*/
protected function addBufferToBatch(skipCount:int):void
{
const charCount:int = CHARACTER_BUFFER.length - skipCount;
for(var i:int = 0; i < charCount; i++)
{
var charLocation:CharLocation = CHARACTER_BUFFER.shift();
if(this._useSeparateBatch)
{
this.addCharacterToBatch(charLocation.char, charLocation.x, charLocation.y, charLocation.scale);
charLocation.char = null;
CHAR_LOCATION_POOL.push(charLocation);
}
else
{
this.addLocation(charLocation);
}
}
}
/**
* @private
*/
protected function addLocation(location:CharLocation):void
{
var image:Image;
const charData:BitmapChar = location.char;
const texture:Texture = charData.texture;
if(this._imagesCache.length > 0)
{
image = this._imagesCache.shift();
image.texture = texture;
image.readjustSize();
}
else
{
image = new Image(texture);
this.addChild(image);
}
image.scaleX = image.scaleY = location.scale;
image.smoothing = this._smoothing;
image.color = this.currentTextFormat.color;
this._images.push(image);
this._locations.push(location);
}
/**
* @private
*/
protected function moveBufferedCharacters(xOffset:Number, yOffset:Number, skipCount:int):void
{
const charCount:int = CHARACTER_BUFFER.length - skipCount;
for(var i:int = 0; i < charCount; i++)
{
var charLocation:CharLocation = CHARACTER_BUFFER[i];
charLocation.x += xOffset;
charLocation.y += yOffset;
}
}
/**
* @private
*/
protected function addCharacterToBatch(charData:BitmapChar, x:Number, y:Number, scale:Number, support:RenderSupport = null, parentAlpha:Number = 1):void
{
if(!HELPER_IMAGE)
{
HELPER_IMAGE = new Image(charData.texture);
}
else
{
HELPER_IMAGE.texture = charData.texture;
HELPER_IMAGE.readjustSize();
}
HELPER_IMAGE.scaleX = HELPER_IMAGE.scaleY = scale;
HELPER_IMAGE.x = x;
HELPER_IMAGE.y = y;
HELPER_IMAGE.color = this.currentTextFormat.color;
HELPER_IMAGE.smoothing = this._smoothing;
if(support)
{
support.pushMatrix();
support.transformMatrix(HELPER_IMAGE);
support.batchQuad(HELPER_IMAGE, parentAlpha, HELPER_IMAGE.texture, this._smoothing);
support.popMatrix();
}
else
{
this._characterBatch.addImage(HELPER_IMAGE);
}
}
/**
* @private
*/
protected function refreshTextFormat():void
{
if(!this._isEnabled && this._disabledTextFormat)
{
this.currentTextFormat = this._disabledTextFormat;
}
else
{
this.currentTextFormat = this._textFormat;
}
}
/**
* @private
*/
protected function getTruncatedText():String
{
if(!this._text)
{
//this shouldn't be called if _text is null, but just in case...
return "";
}
//if the maxWidth is infinity or the string is multiline, don't
//allow truncation
if(this._maxWidth == Number.POSITIVE_INFINITY || this._wordWrap || this._text.indexOf(String.fromCharCode(CHARACTER_ID_LINE_FEED)) >= 0 || this._text.indexOf(String.fromCharCode(CHARACTER_ID_CARRIAGE_RETURN)) >= 0)
{
return this._text;
}
const font:BitmapFont = this.currentTextFormat.font;
const customSize:Number = this.currentTextFormat.size;
const customLetterSpacing:Number = this.currentTextFormat.letterSpacing;
const isKerningEnabled:Boolean = this.currentTextFormat.isKerningEnabled;
const scale:Number = isNaN(customSize) ? 1 : (customSize / font.size);
var currentX:Number = 0;
var previousCharID:Number = NaN;
var charCount:int = this._text.length;
var truncationIndex:int = -1;
for(var i:int = 0; i < charCount; i++)
{
var charID:int = this._text.charCodeAt(i);
var charData:BitmapChar = font.getChar(charID);
if(!charData)
{
continue;
}
var currentKerning:Number = 0;
if(isKerningEnabled && !isNaN(previousCharID))
{
currentKerning = charData.getKerning(previousCharID);
}
currentX += currentKerning + charData.xAdvance * scale;
if(currentX > this._maxWidth)
{
truncationIndex = i;
break;
}
currentX += customLetterSpacing;
previousCharID = charID;
}
if(truncationIndex >= 0)
{
//first measure the size of the truncation text
charCount = this._truncationText.length;
for(i = 0; i < charCount; i++)
{
charID = this._truncationText.charCodeAt(i);
charData = font.getChar(charID);
if(!charData)
{
continue;
}
currentKerning = 0;
if(isKerningEnabled && !isNaN(previousCharID))
{
currentKerning = charData.getKerning(previousCharID);
}
currentX += currentKerning + charData.xAdvance * scale + customLetterSpacing;
previousCharID = charID;
}
currentX -= customLetterSpacing;
//then work our way backwards until we fit into the maxWidth
for(i = truncationIndex; i >= 0; i--)
{
charID = this._text.charCodeAt(i);
previousCharID = i > 0 ? this._text.charCodeAt(i - 1) : NaN;
charData = font.getChar(charID);
if(!charData)
{
continue;
}
currentKerning = 0;
if(isKerningEnabled && !isNaN(previousCharID))
{
currentKerning = charData.getKerning(previousCharID);
}
currentX -= (currentKerning + charData.xAdvance * scale + customLetterSpacing);
if(currentX <= this._maxWidth)
{
return this._text.substr(0, i) + this._truncationText;
}
}
return this._truncationText;
}
return this._text;
}
/**
* @private
*/
protected function moveLocationsToPool():void
{
if(!this._locations)
{
return;
}
const locationCount:int = this._locations.length;
for(var i:int = 0; i < locationCount; i++)
{
var location:CharLocation = this._locations.shift();
location.char = null;
CHAR_LOCATION_POOL.push(location);
}
}
}
}
import starling.text.BitmapChar;
class CharLocation
{
public var char:BitmapChar;
public var scale:Number;
public var x:Number;
public var y:Number;
}
package starling.extensions.DistanceFieldFont
{
import com.adobe.utils.AGALMiniAssembler;
import flash.display3D.Context3D;
import flash.display3D.Context3DProgramType;
import flash.display3D.Context3DTextureFormat;
import flash.display3D.Context3DVertexBufferFormat;
import flash.display3D.IndexBuffer3D;
import flash.display3D.VertexBuffer3D;
import flash.geom.Matrix;
import flash.geom.Matrix3D;
import flash.geom.Rectangle;
import flash.utils.Dictionary;
import flash.utils.getQualifiedClassName;
import starling.core.RenderSupport;
import starling.core.Starling;
import starling.core.starling_internal;
import starling.display.BlendMode;
import starling.display.DisplayObject;
import starling.display.DisplayObjectContainer;
import starling.display.Image;
import starling.display.Quad;
import starling.errors.MissingContextError;
import starling.events.Event;
import starling.filters.FragmentFilter;
import starling.filters.FragmentFilterMode;
import starling.textures.Texture;
import starling.textures.TextureSmoothing;
import starling.utils.MatrixUtil;
import starling.utils.VertexData;
use namespace starling_internal;
/******************************************
*
* http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf
*
* https://code.google.com/p/libgdx/wiki/DistanceFieldFonts
*
*****************************************/
public class DistanceFieldQuadBatch extends DisplayObject
{
private static const QUAD_PROGRAM_NAME:String = "DQB_q";
private var mNumQuads:int;
private var mSyncRequired:Boolean;
private var mTinted:Boolean;
private var mTexture:Texture;
private var mSmoothing:String;
private var mVertexData:VertexData;
private var mVertexBuffer:VertexBuffer3D;
private var mIndexData:Vector.<uint>;
private var mIndexBuffer:IndexBuffer3D;
/** Helper objects. */
private static var sHelperMatrix:Matrix = new Matrix();
private static var sRenderAlpha:Vector.<Number> = new <Number>[1.0, 1.0, 1.0, 1.0];
private static var sRenderMatrix:Matrix3D = new Matrix3D();
private static var sProgramNameCache:Dictionary = new Dictionary();
public var sDistanceConstants:Vector.<Number> = new <Number>[0.5 - 1.0/16.0, 0.5 + 1.0/16.0, 3, 0.5];
public var sDistanceConstants2:Vector.<Number> = new <Number>[1, 1, 1, 1];
/** Creates a new DistanceQuadBatch instance with empty batch data. */
public function DistanceFieldQuadBatch()
{
mVertexData = new VertexData(0, true);
mIndexData = new <uint>[];
mNumQuads = 0;
mTinted = false;
mSyncRequired = false;
// Handle lost context. We use the conventional event here (not the one from Starling)
// so we're able to create a weak event listener; this avoids memory leaks when people
// forget to call "dispose" on the QuadBatch.
Starling.current.stage3D.addEventListener(Event.CONTEXT3D_CREATE,
onContextCreated, false, 0, true);
}
/** Disposes vertex- and index-buffer. */
public override function dispose():void
{
Starling.current.stage3D.removeEventListener(Event.CONTEXT3D_CREATE, onContextCreated);
if (mVertexBuffer) mVertexBuffer.dispose();
if (mIndexBuffer) mIndexBuffer.dispose();
super.dispose();
}
private function onContextCreated(event:Object):void
{
createBuffers();
registerPrograms();
}
/** Creates a duplicate of the QuadBatch object. */
public function clone():DistanceFieldQuadBatch
{
var clone:DistanceFieldQuadBatch = new DistanceFieldQuadBatch();
clone.mVertexData = mVertexData.clone(0, mNumQuads * 4);
clone.mIndexData = mIndexData.slice(0, mNumQuads * 6);
clone.mNumQuads = mNumQuads;
clone.mTinted = mTinted;
clone.mTexture = mTexture;
clone.mSmoothing = mSmoothing;
clone.mSyncRequired = true;
clone.blendMode = blendMode;
clone.alpha = alpha;
return clone;
}
private function expand(newCapacity:int=-1):void
{
var oldCapacity:int = capacity;
if (newCapacity < 0) newCapacity = oldCapacity * 2;
if (newCapacity == 0) newCapacity = 16;
if (newCapacity <= oldCapacity) return;
mVertexData.numVertices = newCapacity * 4;
for (var i:int=oldCapacity; i<newCapacity; ++i)
{
mIndexData[int(i*6 )] = i*4;
mIndexData[int(i*6+1)] = i*4 + 1;
mIndexData[int(i*6+2)] = i*4 + 2;
mIndexData[int(i*6+3)] = i*4 + 1;
mIndexData[int(i*6+4)] = i*4 + 3;
mIndexData[int(i*6+5)] = i*4 + 2;
}
createBuffers();
registerPrograms();
}
private function createBuffers():void
{
var numVertices:int = mVertexData.numVertices;
var numIndices:int = mIndexData.length;
var context:Context3D = Starling.context;
if (mVertexBuffer) mVertexBuffer.dispose();
if (mIndexBuffer) mIndexBuffer.dispose();
if (numVertices == 0) return;
if (context == null) throw new MissingContextError();
mVertexBuffer = context.createVertexBuffer(numVertices, VertexData.ELEMENTS_PER_VERTEX);
mVertexBuffer.uploadFromVector(mVertexData.rawData, 0, numVertices);
mIndexBuffer = context.createIndexBuffer(numIndices);
mIndexBuffer.uploadFromVector(mIndexData, 0, numIndices);
mSyncRequired = false;
}
/** Uploads the raw data of all batched quads to the vertex buffer. */
private function syncBuffers():void
{
if (mVertexBuffer == null)
createBuffers();
else
{
// as 3rd parameter, we could also use 'mNumQuads * 4', but on some GPU hardware (iOS!),
// this is slower than updating the complete buffer.
mVertexBuffer.uploadFromVector(mVertexData.rawData, 0, mVertexData.numVertices);
mSyncRequired = false;
}
}
/** Renders the current batch with custom settings for model-view-projection matrix, alpha
* and blend mode. This makes it possible to render batches that are not part of the
* display list. */
public function renderCustom(mvpMatrix:Matrix, parentAlpha:Number=1.0,
blendMode:String=null):void
{
if (mNumQuads == 0) return;
if (mSyncRequired) syncBuffers();
var pma:Boolean = mVertexData.premultipliedAlpha;
var context:Context3D = Starling.context;
var tinted:Boolean = mTinted || (parentAlpha != 1.0);
var programName:String = mTexture ?
getImageProgramName(tinted, mTexture.mipMapping, mTexture.repeat, mTexture.format, mSmoothing) :
QUAD_PROGRAM_NAME;
sRenderAlpha[0] = sRenderAlpha[1] = sRenderAlpha[2] = pma ? parentAlpha : 1.0;
sRenderAlpha[3] = parentAlpha;
MatrixUtil.convertTo3D(mvpMatrix, sRenderMatrix);
RenderSupport.setBlendFactors(pma, blendMode ? blendMode : this.blendMode);
context.setProgram(Starling.current.getProgram(programName));
context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 0, sRenderAlpha, 1);
context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 1, sRenderMatrix, true);
context.setVertexBufferAt(0, mVertexBuffer, VertexData.POSITION_OFFSET,
Context3DVertexBufferFormat.FLOAT_2);
if (mTexture == null || tinted)
context.setVertexBufferAt(1, mVertexBuffer, VertexData.COLOR_OFFSET,
Context3DVertexBufferFormat.FLOAT_4);
if (mTexture)
{
context.setTextureAt(0, mTexture.base);
context.setVertexBufferAt(2, mVertexBuffer, VertexData.TEXCOORD_OFFSET,
Context3DVertexBufferFormat.FLOAT_2);
}
// fc0 -> constants [0.5 - 1.0/16.0, 0.5 + 1.0/16.0, 3, 0.5]
context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, sDistanceConstants, 1);
context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 1, sDistanceConstants2, 1);
context.drawTriangles(mIndexBuffer, 0, mNumQuads * 2);
if (mTexture)
{
context.setTextureAt(0, null);
context.setVertexBufferAt(2, null);
}
context.setVertexBufferAt(1, null);
context.setVertexBufferAt(0, null);
}
/** Resets the batch. The vertex- and index-buffers remain their size, so that they
* can be reused quickly. */
public function reset():void
{
mNumQuads = 0;
mTexture = null;
mSmoothing = null;
mSyncRequired = true;
}
/** Adds an image to the batch. This method internally calls 'addQuad' with the correct
* parameters for 'texture' and 'smoothing'. */
public function addImage(image:Image, parentAlpha:Number=1.0, modelViewMatrix:Matrix=null,
blendMode:String=null):void
{
addQuad(image, parentAlpha, image.texture, image.smoothing, modelViewMatrix, blendMode);
}
/** Adds a quad to the batch. The first quad determines the state of the batch,
* i.e. the values for texture, smoothing and blendmode. When you add additional quads,
* make sure they share that state (e.g. with the 'isStageChange' method), or reset
* the batch. */
public function addQuad(quad:Quad, parentAlpha:Number=1.0, texture:Texture=null,
smoothing:String=null, modelViewMatrix:Matrix=null,
blendMode:String=null):void
{
if (modelViewMatrix == null)
modelViewMatrix = quad.transformationMatrix;
var tinted:Boolean = texture ? (quad.tinted || parentAlpha != 1.0) : false;
var alpha:Number = parentAlpha * quad.alpha;
var vertexID:int = mNumQuads * 4;
if (mNumQuads + 1 > mVertexData.numVertices / 4) expand();
if (mNumQuads == 0)
{
this.blendMode = blendMode ? blendMode : quad.blendMode;
mTexture = texture;
mTinted = tinted;
mSmoothing = smoothing;
mVertexData.setPremultipliedAlpha(quad.premultipliedAlpha);
}
quad.copyVertexDataTo(mVertexData, vertexID);
mVertexData.transformVertex(vertexID, modelViewMatrix, 4);
if (alpha != 1.0)
mVertexData.scaleAlpha(vertexID, alpha, 4);
mSyncRequired = true;
mNumQuads++;
}
/** Adds another QuadBatch to this batch. Just like the 'addQuad' method, you have to
* make sure that you only add batches with an equal state. */
public function addQuadBatch(quadBatch:DistanceFieldQuadBatch, parentAlpha:Number=1.0,
modelViewMatrix:Matrix=null, blendMode:String=null):void
{
if (modelViewMatrix == null)
modelViewMatrix = quadBatch.transformationMatrix;
var tinted:Boolean = quadBatch.mTinted || parentAlpha != 1.0;
var alpha:Number = parentAlpha * quadBatch.alpha;
var vertexID:int = mNumQuads * 4;
var numQuads:int = quadBatch.numQuads;
if (mNumQuads + numQuads > capacity) expand(mNumQuads + numQuads);
if (mNumQuads == 0)
{
this.blendMode = blendMode ? blendMode : quadBatch.blendMode;
mTexture = quadBatch.mTexture;
mTinted = tinted;
mSmoothing = quadBatch.mSmoothing;
mVertexData.setPremultipliedAlpha(quadBatch.mVertexData.premultipliedAlpha, false);
}
quadBatch.mVertexData.copyTo(mVertexData, vertexID, 0, numQuads*4);
mVertexData.transformVertex(vertexID, modelViewMatrix, numQuads*4);
if (alpha != 1.0)
mVertexData.scaleAlpha(vertexID, alpha, numQuads*4);
mSyncRequired = true;
mNumQuads += numQuads;
}
/** Indicates if specific quads can be added to the batch without causing a state change.
* A state change occurs if the quad uses a different base texture, has a different
* 'tinted', 'smoothing', 'repeat' or 'blendMode' setting, or if the batch is full
* (one batch can contain up to 8192 quads). */
public function isStateChange(tinted:Boolean, parentAlpha:Number, texture:Texture,
smoothing:String, blendMode:String, numQuads:int=1):Boolean
{
if (mNumQuads == 0) return false;
else if (mNumQuads + numQuads > 8192) return true; // maximum buffer size
else if (mTexture == null && texture == null) return false;
else if (mTexture != null && texture != null)
return mTexture.base != texture.base ||
mTexture.repeat != texture.repeat ||
mSmoothing != smoothing ||
mTinted != (tinted || parentAlpha != 1.0) ||
this.blendMode != blendMode;
else return true;
}
// display object methods
/** @inheritDoc */
public override function getBounds(targetSpace:DisplayObject, resultRect:Rectangle=null):Rectangle
{
if (resultRect == null) resultRect = new Rectangle();
var transformationMatrix:Matrix = targetSpace == this ?
null : getTransformationMatrix(targetSpace, sHelperMatrix);
return mVertexData.getBounds(transformationMatrix, 0, mNumQuads*4, resultRect);
}
/** @inheritDoc */
public override function render(support:RenderSupport, parentAlpha:Number):void
{
if (mNumQuads)
{
support.finishQuadBatch();
support.raiseDrawCount();
renderCustom(support.mvpMatrix, alpha * parentAlpha, support.blendMode);
}
}
// compilation (for flattened sprites)
/** Analyses an object that is made up exclusively of quads (or other containers)
* and creates a vector of QuadBatch objects representing it. This can be
* used to render the container very efficiently. The 'flatten'-method of the Sprite
* class uses this method internally. */
public static function compile(object:DisplayObject,
quadBatches:Vector.<DistanceFieldQuadBatch>):void
{
compileObject(object, quadBatches, -1, new Matrix());
}
private static function compileObject(object:DisplayObject,
quadBatches:Vector.<DistanceFieldQuadBatch>,
quadBatchID:int,
transformationMatrix:Matrix,
alpha:Number=1.0,
blendMode:String=null,
ignoreCurrentFilter:Boolean=false):int
{
var i:int;
var quadBatch:DistanceFieldQuadBatch;
var isRootObject:Boolean = false;
var objectAlpha:Number = object.alpha;
var container:DisplayObjectContainer = object as DisplayObjectContainer;
var quad:Quad = object as Quad;
var batch:DistanceFieldQuadBatch = object as DistanceFieldQuadBatch;
var filter:FragmentFilter = object.filter;
if (quadBatchID == -1)
{
isRootObject = true;
quadBatchID = 0;
objectAlpha = 1.0;
blendMode = object.blendMode;
if (quadBatches.length == 0) quadBatches.push(new DistanceFieldQuadBatch());
else quadBatches[0].reset();
}
if (filter && !ignoreCurrentFilter)
{
if (filter.mode == FragmentFilterMode.ABOVE)
{
quadBatchID = compileObject(object, quadBatches, quadBatchID,
transformationMatrix, alpha, blendMode, true);
}
quadBatchID = compileObject(filter.compile(object), quadBatches, quadBatchID,
transformationMatrix, alpha, blendMode);
if (filter.mode == FragmentFilterMode.BELOW)
{
quadBatchID = compileObject(object, quadBatches, quadBatchID,
transformationMatrix, alpha, blendMode, true);
}
}
else if (container)
{
var numChildren:int = container.numChildren;
var childMatrix:Matrix = new Matrix();
for (i=0; i<numChildren; ++i)
{
var child:DisplayObject = container.getChildAt(i);
var childVisible:Boolean = child.alpha != 0.0 && child.visible &&
child.scaleX != 0.0 && child.scaleY != 0.0;
if (childVisible)
{
var childBlendMode:String = child.blendMode == BlendMode.AUTO ?
blendMode : child.blendMode;
childMatrix.copyFrom(transformationMatrix);
RenderSupport.transformMatrixForObject(childMatrix, child);
quadBatchID = compileObject(child, quadBatches, quadBatchID, childMatrix,
alpha*objectAlpha, childBlendMode);
}
}
}
else if (quad || batch)
{
var texture:Texture;
var smoothing:String;
var tinted:Boolean;
var numQuads:int;
if (quad)
{
var image:Image = quad as Image;
texture = image ? image.texture : null;
smoothing = image ? image.smoothing : null;
tinted = quad.tinted;
numQuads = 1;
}
else
{
texture = batch.mTexture;
smoothing = batch.mSmoothing;
tinted = batch.mTinted;
numQuads = batch.mNumQuads;
}
quadBatch = quadBatches[quadBatchID];
if (quadBatch.isStateChange(tinted, alpha*objectAlpha, texture,
smoothing, blendMode, numQuads))
{
quadBatchID++;
if (quadBatches.length <= quadBatchID) quadBatches.push(new DistanceFieldQuadBatch());
quadBatch = quadBatches[quadBatchID];
quadBatch.reset();
}
if (quad)
quadBatch.addQuad(quad, alpha, texture, smoothing, transformationMatrix, blendMode);
else
quadBatch.addQuadBatch(batch, alpha, transformationMatrix, blendMode);
}
else
{
throw new Error("Unsupported display object: " + getQualifiedClassName(object));
}
if (isRootObject)
{
// remove unused batches
for (i=quadBatches.length-1; i>quadBatchID; --i)
quadBatches.pop().dispose();
}
return quadBatchID;
}
// properties
public function get numQuads():int { return mNumQuads; }
public function get tinted():Boolean { return mTinted; }
public function get texture():Texture { return mTexture; }
public function get smoothing():String { return mSmoothing; }
public function get premultipliedAlpha():Boolean { return mVertexData.premultipliedAlpha; }
private function get capacity():int { return mVertexData.numVertices / 4; }
// program management
private static function registerPrograms():void
{
var target:Starling = Starling.current;
if (target.hasProgram(QUAD_PROGRAM_NAME)) return; // already registered
var assembler:AGALMiniAssembler = new AGALMiniAssembler();
var vertexProgramCode:String;
var fragmentProgramCode:String;
// this is the input data we'll pass to the shaders:
//
// va0 -> position
// va1 -> color
// va2 -> texCoords
// vc0 -> alpha
// vc1 -> mvpMatrix
// fs0 -> texture
// fc0 -> constants [0.5 - 1.0/16.0, 0.5 + 1.0/16.0, 3, 1]
// Quad:
vertexProgramCode =
"m44 op, va0, vc1 \n" + // 4x4 matrix transform to output clipspace
"mul v0, va1, vc0 \n"; // multiply alpha (vc0) with color (va1)
fragmentProgramCode =
"mov oc, v0 \n"; // output color
target.registerProgram(QUAD_PROGRAM_NAME,
assembler.assemble(Context3DProgramType.VERTEX, vertexProgramCode),
assembler.assemble(Context3DProgramType.FRAGMENT, fragmentProgramCode));
// Image:
// Each combination of tinted/repeat/mipmap/smoothing has its own fragment shader.
for each (var tinted:Boolean in [true, false])
{
vertexProgramCode = tinted ?
"m44 op, va0, vc1 \n" + // 4x4 matrix transform to output clipspace
"mul v0, va1, vc0 \n" + // multiply alpha (vc0) with color (va1)
"mov v1, va2 \n" // pass texture coordinates to fragment program
:
"m44 op, va0, vc1 \n" + // 4x4 matrix transform to output clipspace
"mov v1, va2 \n"; // pass texture coordinates to fragment program
/*
// const float smoothing = 1.0/16.0;
// float distance = texture2D(u_texture, v_texCoord).a;
// float alpha = smoothstep(0.5 - smoothing, 0.5 + smoothing, distance);
// gl_FragColor = vec4(v_color.rgb, alpha);
// fc0.x = 0.5 - smoothing // a
// fc0.y = 0.5 + smoothing // b
// fc0.z = 3
// fc0.w = 0.5
*/
// soft edges
fragmentProgramCode = tinted ?
"tex ft1, v1, fs0 <???> \n" + // sample texture 0
"mov ft3, fc0 \n" +
"sub ft2.x, ft1.w, ft3.x \n" + // smooth step
"sub ft2.y, ft3.y, ft3.x \n" +
"div ft2.x, ft2.x, ft2.y \n" +
"sat ft2.x, ft2.x \n" +
"add ft2.y, ft2.x, ft2.x \n" +
"sub ft2.y, ft3.z, ft2.y \n" +
"mul ft2.x, ft2.x, ft2.x \n" +
"mul ft2.x, ft2.y, ft2.x \n" +
"sub ft4.w, ft2.x, ft3.w \n" + // kill texture if under threshold
"kil ft4.w \n" +
"mov ft1.xyz, v0.xyz\n" + // Place vertex color
"mov ft1.w, ft2.x\n" + // place smooth alpha
"mov oc, ft1 \n"
:
"tex ft1, v1, fs0 <???> \n" + // sample texture 0
"mov ft3, fc0 \n" +
"sub ft2.x, ft1.w, ft3.x \n" + // smooth step
"sub ft2.y, ft3.y, ft3.x \n" +
"div ft2.x, ft2.x, ft2.y \n" +
"sat ft2.x, ft2.x \n" +
"add ft2.y, ft2.x, ft2.x \n" +
"sub ft2.y, ft3.z, ft2.y \n" +
"mul ft2.x, ft2.x, ft2.x \n" +
"mul ft2.x, ft2.y, ft2.x \n" +
"sub ft4.w, ft2.x, ft3.w \n" + // kill texture if under threshold
"kil ft4.w \n" +
"mov ft1.xyz, fc1.xxx\n" + // Place vertex color
"mov ft1.w, ft2.x\n" + // place smooth alpha
"mov oc, ft1 \n"
// Hard Edges
/* fragmentProgramCode = tinted ?
"tex ft1, v1, fs0 <???> \n" + // sample texture 0
"mov ft3, fc0 \n" +
"mov ft2.xyzw, ft1.wwww \n" +
"sub ft4.w, ft2.x, fc0.w \n" +
"kil ft4.w \n" +
"mul ft1.xyz, ft2.xxx, v0.xyz\n" + // multiply color with texel color
"mov ft1.w, ft2.x\n" +
"mov oc, ft1 \n"
:
"tex ft1, v1, fs0 <???> \n" + // sample texture 0
"mov ft3, fc0 \n" +
"mov ft2.xyzw, ft1.wwww \n" +
"sub ft4.w, ft2.x, fc0.w \n" +
"kil ft4.w \n" +
"mov oc, fc1 \n"
*/
var smoothingTypes:Array = [
TextureSmoothing.NONE,
TextureSmoothing.BILINEAR,
TextureSmoothing.TRILINEAR
];
var formats:Array = [
Context3DTextureFormat.BGRA,
Context3DTextureFormat.COMPRESSED,
"compressedAlpha" // use explicit string for compatibility
];
for each (var repeat:Boolean in [true, false])
{
for each (var mipmap:Boolean in [true, false])
{
for each (var smoothing:String in smoothingTypes)
{
for each (var format:String in formats)
{
var options:Array = ["2d", repeat ? "repeat" : "clamp"];
if (format == Context3DTextureFormat.COMPRESSED)
options.push("dxt1");
else if (format == "compressedAlpha")
options.push("dxt5");
if (smoothing == TextureSmoothing.NONE)
options.push("nearest", mipmap ? "mipnearest" : "mipnone");
else if (smoothing == TextureSmoothing.BILINEAR)
options.push("linear", mipmap ? "mipnearest" : "mipnone");
else
options.push("linear", mipmap ? "miplinear" : "mipnone");
target.registerProgram(
getImageProgramName(tinted, mipmap, repeat, format, smoothing),
assembler.assemble(Context3DProgramType.VERTEX, vertexProgramCode),
assembler.assemble(Context3DProgramType.FRAGMENT,
fragmentProgramCode.replace("???", options.join()))
);
}
}
}
}
}
}
private static function getImageProgramName(tinted:Boolean, mipMap:Boolean=true,
repeat:Boolean=false, format:String="bgra",
smoothing:String="bilinear"):String
{
var bitField:uint = 0;
if (tinted) bitField |= 1;
if (mipMap) bitField |= 1 << 1;
if (repeat) bitField |= 1 << 2;
if (smoothing == TextureSmoothing.NONE)
bitField |= 1 << 3;
else if (smoothing == TextureSmoothing.TRILINEAR)
bitField |= 1 << 4;
if (format == Context3DTextureFormat.COMPRESSED)
bitField |= 1 << 5;
else if (format == "compressedAlpha")
bitField |= 1 << 6;
var name:String = sProgramNameCache[bitField];
if (name == null)
{
name = "DQB_i." + bitField.toString(16);
sProgramNameCache[bitField] = name;
}
return name;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment