Skip to content

Instantly share code, notes, and snippets.

@filharvey
Last active February 29, 2020 09:35
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save filharvey/5756135 to your computer and use it in GitHub Desktop.
Save filharvey/5756135 to your computer and use it in GitHub Desktop.
DistanceFieldFont is an extension for starling and FeathersUI to use Distance Field Fonts instead of normal Bitmap fonts. You can read more on Distance Field Fonts here: http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf https://code.google.com/p/libgdx/wiki/DistanceFieldFonts Currently there is no easy way …
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