babylon.fontTexture.ts 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946
  1. module BABYLON {
  2. /**
  3. * This class given information about a given character.
  4. */
  5. export class CharInfo {
  6. /**
  7. * The normalized ([0;1]) top/left position of the character in the texture
  8. */
  9. topLeftUV: Vector2;
  10. /**
  11. * The normalized ([0;1]) right/bottom position of the character in the texture
  12. */
  13. bottomRightUV: Vector2;
  14. charWidth: number;
  15. }
  16. /**
  17. * This is an abstract base class to hold a Texture that will contain a FontMap
  18. */
  19. export abstract class BaseFontTexture extends Texture {
  20. constructor(url: string, scene: Scene, noMipmap: boolean = false, invertY: boolean = true, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE) {
  21. super(url, scene, noMipmap, invertY, samplingMode);
  22. this._cachedFontId = null;
  23. this._charInfos = new StringDictionary<CharInfo>();
  24. }
  25. /**
  26. * Is the Font is using Super Sampling (each font texel is doubled).
  27. */
  28. public get isSuperSampled(): boolean {
  29. return this._superSample;
  30. }
  31. /**
  32. * Is the Font was rendered using the Signed Distance Field algorithm
  33. * @returns {}
  34. */
  35. public get isSignedDistanceField(): boolean {
  36. return this._signedDistanceField;
  37. }
  38. /**
  39. * Get the Width (in pixel) of the Space character
  40. */
  41. public get spaceWidth(): number {
  42. return this._spaceWidth;
  43. }
  44. /**
  45. * Get the Line height (in pixel)
  46. */
  47. public get lineHeight(): number {
  48. return this._lineHeight;
  49. }
  50. /**
  51. * When the FontTexture is retrieved through the FontCache, there's a reference counter that is incremented for each use.
  52. * You also have the possibility to extend the lifetime of the FontTexture when passing it to another object by calling this method
  53. * Don't forget to call the corresponding decCachedFontTextureCounter method when you no longer have use of the FontTexture.
  54. * Each call to incCachedFontTextureCounter must have a corresponding call to decCachedFontTextureCounter.
  55. */
  56. abstract incCachedFontTextureCounter();
  57. /**
  58. * Decrement the reference counter, if it reaches 0 the FontTexture is disposed
  59. */
  60. abstract decCachedFontTextureCounter();
  61. /**
  62. * Is the font dynamically updated, if true is returned then you have to call the update() before using the font in rendering if new character were adding using getChar()
  63. */
  64. abstract get isDynamicFontTexture(): boolean;
  65. /**
  66. * Will fetch the new characters retrieved with getChar() to the texture.
  67. * If there were no new char, this call is harmless and quit in no time.
  68. * If there were new chars a texture lock/update is made, which is a costy operation.
  69. */
  70. abstract update(): void;
  71. /**
  72. * Measure the width/height that will take a given text
  73. * @param text the text to measure
  74. * @param tabulationSize the size (in space character) of the tabulation character, default value must be 4
  75. */
  76. measureText(text: string, tabulationSize?: number): Size {
  77. let maxWidth: number = 0;
  78. let curWidth: number = 0;
  79. let lineCount = 1;
  80. let charxpos: number = 0;
  81. // Parse each char of the string
  82. for (var char of text) {
  83. // Next line feed?
  84. if (char === "\n") {
  85. maxWidth = Math.max(maxWidth, curWidth);
  86. charxpos = 0;
  87. curWidth = 0;
  88. ++lineCount;
  89. continue;
  90. }
  91. // Tabulation ?
  92. if (char === "\t") {
  93. let nextPos = charxpos + tabulationSize;
  94. nextPos = nextPos - (nextPos % tabulationSize);
  95. curWidth += (nextPos - charxpos) * this.spaceWidth;
  96. charxpos = nextPos;
  97. continue;
  98. }
  99. if (char < " ") {
  100. continue;
  101. }
  102. let ci = this.getChar(char);
  103. if (!ci) {
  104. throw new Error(`Character ${char} is not supported by FontTexture ${this.name}`);
  105. }
  106. curWidth += ci.charWidth;
  107. ++charxpos;
  108. }
  109. maxWidth = Math.max(maxWidth, curWidth);
  110. return new Size(maxWidth, lineCount * this.lineHeight);
  111. }
  112. /**
  113. * Retrieve the CharInfo object for a given character
  114. * @param char the character to retrieve the CharInfo object from (e.g.: "A", "a", etc.)
  115. */
  116. abstract getChar(char: string): CharInfo;
  117. protected _charInfos: StringDictionary<CharInfo>;
  118. protected _lineHeight: number;
  119. protected _spaceWidth;
  120. protected _superSample: boolean;
  121. protected _signedDistanceField: boolean;
  122. protected _cachedFontId: string;
  123. }
  124. export class BitmapFontInfo {
  125. kerningDic = new StringDictionary<number>();
  126. charDic = new StringDictionary<CharInfo>();
  127. textureSize : Size;
  128. atlasName : string;
  129. padding : Vector4; // Left, Top, Right, Bottom
  130. lineHeight: number;
  131. textureUrl : string;
  132. textureFile : string;
  133. }
  134. export interface IBitmapFontLoader {
  135. loadFont(fontDataContent: any, scene: Scene, invertY: boolean): { bfi: BitmapFontInfo, errorMsg: string, errorCode: number };
  136. }
  137. export class BitmapFontTexture extends BaseFontTexture {
  138. public constructor( scene: Scene,
  139. bmFontUrl: string,
  140. textureUrl: string = null,
  141. noMipmap: boolean = false,
  142. invertY: boolean = true,
  143. samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE,
  144. onLoad: () => void = null,
  145. onError: (msg: string, code: number) => void = null)
  146. {
  147. super(null, scene, noMipmap, invertY, samplingMode);
  148. var xhr = new XMLHttpRequest();
  149. xhr.onreadystatechange = () => {
  150. if (xhr.readyState === XMLHttpRequest.DONE) {
  151. if (xhr.status === 200) {
  152. let ext = bmFontUrl.split('.').pop().split(/\#|\?/)[0];
  153. let plugins = BitmapFontTexture.plugins.get(ext.toLocaleLowerCase());
  154. if (!plugins) {
  155. if (onError) {
  156. onError("couldn't find a plugin for this file extension", -1);
  157. }
  158. return;
  159. }
  160. for (let p of plugins) {
  161. let ret = p.loadFont(xhr.response, scene, invertY);
  162. if (ret) {
  163. let bfi = ret.bfi;
  164. if (textureUrl != null) {
  165. bfi.textureUrl = textureUrl;
  166. } else {
  167. let baseUrl = bmFontUrl.substr(0, bmFontUrl.lastIndexOf("/") + 1);
  168. bfi.textureUrl = baseUrl + bfi.textureFile;
  169. }
  170. this._texture = scene.getEngine().createTexture(bfi.textureUrl, noMipmap, invertY, scene, samplingMode, () => {
  171. if (ret.bfi && onLoad) {
  172. onLoad();
  173. }
  174. });
  175. this._lineHeight = bfi.lineHeight;
  176. this._charInfos.copyFrom(bfi.charDic);
  177. let ci = this.getChar(" ");
  178. if (ci) {
  179. this._spaceWidth = ci.charWidth;
  180. } else {
  181. this._charInfos.first((k, v) => this._spaceWidth = v.charWidth);
  182. }
  183. if (!ret.bfi && onError) {
  184. onError(ret.errorMsg, ret.errorCode);
  185. }
  186. return;
  187. }
  188. }
  189. if (onError) {
  190. onError("No plugin to load this BMFont file format", -1);
  191. }
  192. } else {
  193. if (onError) {
  194. onError("Couldn't load file through HTTP Request, HTTP Status " + xhr.status, xhr.status);
  195. }
  196. }
  197. }
  198. }
  199. xhr.open("GET", bmFontUrl, true);
  200. xhr.send();
  201. }
  202. public static GetCachedFontTexture(scene: Scene, fontTexture: BitmapFontTexture): BitmapFontTexture {
  203. let dic = scene.getOrAddExternalDataWithFactory("BitmapFontTextureCache", () => new StringDictionary<BitmapFontTexture>());
  204. let ft = dic.get(fontTexture.uid);
  205. if (ft) {
  206. ++ft._usedCounter;
  207. return ft;
  208. }
  209. dic.add(fontTexture.uid, fontTexture);
  210. return ft;
  211. }
  212. public static ReleaseCachedFontTexture(scene: Scene, fontTexture: BitmapFontTexture) {
  213. let dic = scene.getExternalData<StringDictionary<BitmapFontTexture>>("BitmapFontTextureCache");
  214. if (!dic) {
  215. return;
  216. }
  217. var font = dic.get(fontTexture.uid);
  218. if (--font._usedCounter === 0) {
  219. dic.remove(fontTexture.uid);
  220. font.dispose();
  221. }
  222. }
  223. /**
  224. * Is the font dynamically updated, if true is returned then you have to call the update() before using the font in rendering if new character were adding using getChar()
  225. */
  226. get isDynamicFontTexture(): boolean {
  227. return false;
  228. }
  229. /**
  230. * This method does nothing for a BitmapFontTexture object as it's a static texture
  231. */
  232. update(): void {
  233. }
  234. /**
  235. * Retrieve the CharInfo object for a given character
  236. * @param char the character to retrieve the CharInfo object from (e.g.: "A", "a", etc.)
  237. */
  238. getChar(char: string): CharInfo {
  239. return this._charInfos.get(char);
  240. }
  241. /**
  242. * For FontTexture retrieved using GetCachedFontTexture, use this method when you transfer this object's lifetime to another party in order to share this resource.
  243. * When the other party is done with this object, decCachedFontTextureCounter must be called.
  244. */
  245. public incCachedFontTextureCounter() {
  246. ++this._usedCounter;
  247. }
  248. /**
  249. * Use this method only in conjunction with incCachedFontTextureCounter, call it when you no longer need to use this shared resource.
  250. */
  251. public decCachedFontTextureCounter() {
  252. let dic = this.getScene().getExternalData<StringDictionary<BitmapFontTexture>>("BitmapFontTextureCache");
  253. if (!dic) {
  254. return;
  255. }
  256. if (--this._usedCounter === 0) {
  257. dic.remove(this._cachedFontId);
  258. this.dispose();
  259. }
  260. }
  261. private _usedCounter = 1;
  262. static addLoader(fileExtension: string, plugin: IBitmapFontLoader) {
  263. let a = BitmapFontTexture.plugins.getOrAddWithFactory(fileExtension.toLocaleLowerCase(), () => new Array<IBitmapFontLoader>());
  264. a.push(plugin);
  265. }
  266. static plugins: StringDictionary<IBitmapFontLoader[]> = new StringDictionary<Array<IBitmapFontLoader>>();
  267. }
  268. /**
  269. * This class is a special kind of texture which generates on the fly characters of a given css style "fontName".
  270. * The generated texture will be updated when new characters will be retrieved using the getChar() method, but you have
  271. * to call the update() method for the texture to fetch these changes, you can harmlessly call update any time you want, if no
  272. * change were made, nothing will happen.
  273. * The Font Texture can be rendered in three modes: normal size, super sampled size (x2) or using Signed Distance Field rendering.
  274. * Signed Distance Field should be prefered because the texture can be rendered using AlphaTest instead of Transparency, which is way more faster. More about SDF here (http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf).
  275. * The only flaw of SDF is that the rendering quality may not be the best or the edges too sharp is the font thickness is too thin.
  276. */
  277. export class FontTexture extends BaseFontTexture {
  278. private _canvas: HTMLCanvasElement;
  279. private _context: CanvasRenderingContext2D;
  280. private _lineHeightSuper: number;
  281. private _xMargin: number;
  282. private _yMargin: number;
  283. private _offset: number;
  284. private _currentFreePosition: Vector2;
  285. private _curCharCount = 0;
  286. private _lastUpdateCharCount = -1;
  287. private _spaceWidthSuper;
  288. private _sdfCanvas: HTMLCanvasElement;
  289. private _sdfContext: CanvasRenderingContext2D;
  290. private _sdfScale: number;
  291. private _usedCounter = 1;
  292. get isDynamicFontTexture(): boolean {
  293. return true;
  294. }
  295. public static GetCachedFontTexture(scene: Scene, fontName: string, supersample: boolean = false, signedDistanceField: boolean = false): FontTexture {
  296. let dic = scene.getOrAddExternalDataWithFactory("FontTextureCache", () => new StringDictionary<FontTexture>());
  297. let lfn = fontName.toLocaleLowerCase() + (supersample ? "_+SS" : "_-SS") + (signedDistanceField ? "_+SDF" : "_-SDF");
  298. let ft = dic.get(lfn);
  299. if (ft) {
  300. ++ft._usedCounter;
  301. return ft;
  302. }
  303. ft = new FontTexture(null, fontName, scene, supersample ? 100 : 200, Texture.BILINEAR_SAMPLINGMODE, supersample, signedDistanceField);
  304. ft._cachedFontId = lfn;
  305. dic.add(lfn, ft);
  306. return ft;
  307. }
  308. public static ReleaseCachedFontTexture(scene: Scene, fontName: string, supersample: boolean = false, signedDistanceField: boolean = false) {
  309. let dic = scene.getExternalData<StringDictionary<FontTexture>>("FontTextureCache");
  310. if (!dic) {
  311. return;
  312. }
  313. let lfn = fontName.toLocaleLowerCase() + (supersample ? "_+SS" : "_-SS") + (signedDistanceField ? "_+SDF" : "_-SDF");
  314. var font = dic.get(lfn);
  315. if (--font._usedCounter === 0) {
  316. dic.remove(lfn);
  317. font.dispose();
  318. }
  319. }
  320. /**
  321. * Create a new instance of the FontTexture class
  322. * @param name the name of the texture
  323. * @param font the font to use, use the W3C CSS notation
  324. * @param scene the scene that owns the texture
  325. * @param maxCharCount the approximative maximum count of characters that could fit in the texture. This is an approximation because most of the fonts are proportional (each char has its own Width). The 'W' character's width is used to compute the size of the texture based on the given maxCharCount
  326. * @param samplingMode the texture sampling mode
  327. * @param superSample if true the FontTexture will be created with a font of a size twice bigger than the given one but all properties (lineHeight, charWidth, etc.) will be according to the original size. This is made to improve the text quality.
  328. */
  329. constructor(name: string, font: string, scene: Scene, maxCharCount = 200, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE, superSample: boolean = false, signedDistanceField: boolean = false) {
  330. super(null, scene, true, false, samplingMode);
  331. this.name = name;
  332. this.wrapU = Texture.CLAMP_ADDRESSMODE;
  333. this.wrapV = Texture.CLAMP_ADDRESSMODE;
  334. this._sdfScale = 8;
  335. this._signedDistanceField = signedDistanceField;
  336. this._superSample = false;
  337. // SDF will use supersample no matter what, the resolution is otherwise too poor to produce correct result
  338. if (superSample || signedDistanceField) {
  339. let sfont = this.getSuperSampleFont(font);
  340. if (sfont) {
  341. this._superSample = true;
  342. font = sfont;
  343. }
  344. }
  345. // First canvas creation to determine the size of the texture to create
  346. this._canvas = document.createElement("canvas");
  347. this._context = this._canvas.getContext("2d");
  348. this._context.font = font;
  349. this._context.fillStyle = "white";
  350. this._context.textBaseline = "top";
  351. var res = this.getFontHeight(font);
  352. this._lineHeightSuper = res.height; //+4;
  353. this._lineHeight = this._superSample ? (Math.ceil(this._lineHeightSuper / 2)) : this._lineHeightSuper;
  354. this._offset = res.offset - 1;
  355. this._xMargin = 1 + Math.ceil(this._lineHeightSuper / 15); // Right now this empiric formula seems to work...
  356. this._yMargin = this._xMargin;
  357. var maxCharWidth = this._context.measureText("W").width;
  358. this._spaceWidthSuper = this._context.measureText(" ").width;
  359. this._spaceWidth = this._superSample ? (this._spaceWidthSuper / 2) : this._spaceWidthSuper;
  360. // This is an approximate size, but should always be able to fit at least the maxCharCount
  361. var totalEstSurface = (this._lineHeightSuper + this._yMargin) * (maxCharWidth + this._xMargin) * maxCharCount;
  362. var edge = Math.sqrt(totalEstSurface);
  363. var textSize = Math.pow(2, Math.ceil(Math.log(edge) / Math.log(2)));
  364. // Create the texture that will store the font characters
  365. this._texture = scene.getEngine().createDynamicTexture(textSize, textSize, false, samplingMode);
  366. var textureSize = this.getSize();
  367. this.hasAlpha = this._signedDistanceField===false;
  368. // Recreate a new canvas with the final size: the one matching the texture (resizing the previous one doesn't work as one would expect...)
  369. this._canvas = document.createElement("canvas");
  370. this._canvas.width = textureSize.width;
  371. this._canvas.height = textureSize.height;
  372. this._context = this._canvas.getContext("2d");
  373. this._context.textBaseline = "top";
  374. this._context.font = font;
  375. this._context.fillStyle = "white";
  376. this._context.imageSmoothingEnabled = false;
  377. this._context.clearRect(0, 0, textureSize.width, textureSize.height);
  378. // Create a canvas for the signed distance field mode, we only have to store one char, the purpose is to render a char scaled _sdfScale times
  379. // into this 2D context, then get the bitmap data, create the sdf char and push the result in the _context (which hold the whole Font Texture content)
  380. // So you can see this context as an intermediate one, because it is.
  381. if (this._signedDistanceField) {
  382. let sdfC = document.createElement("canvas");
  383. let s = this._sdfScale;
  384. sdfC.width = maxCharWidth * s;
  385. sdfC.height = this._lineHeightSuper * s;
  386. let sdfCtx = sdfC.getContext("2d");
  387. sdfCtx.scale(s, s);
  388. sdfCtx.textBaseline = "top";
  389. sdfCtx.font = font;
  390. sdfCtx.fillStyle = "white";
  391. sdfCtx.imageSmoothingEnabled = false;
  392. this._sdfCanvas = sdfC;
  393. this._sdfContext = sdfCtx;
  394. }
  395. this._currentFreePosition = Vector2.Zero();
  396. // Add the basic ASCII based characters
  397. for (let i = 0x20; i < 0x7F; i++) {
  398. var c = String.fromCharCode(i);
  399. this.getChar(c);
  400. }
  401. this.update();
  402. }
  403. /**
  404. * Make sure the given char is present in the font map.
  405. * @param char the character to get or add
  406. * @return the CharInfo instance corresponding to the given character
  407. */
  408. public getChar(char: string): CharInfo {
  409. if (char.length !== 1) {
  410. return null;
  411. }
  412. var info = this._charInfos.get(char);
  413. if (info) {
  414. return info;
  415. }
  416. info = new CharInfo();
  417. var measure = this._context.measureText(char);
  418. var textureSize = this.getSize();
  419. // we reached the end of the current line?
  420. let width = Math.round(measure.width);
  421. if (this._currentFreePosition.x + width + this._xMargin > textureSize.width) {
  422. this._currentFreePosition.x = 0;
  423. this._currentFreePosition.y += this._lineHeightSuper + this._yMargin;
  424. // No more room?
  425. if (this._currentFreePosition.y > textureSize.height) {
  426. return this.getChar("!");
  427. }
  428. }
  429. // In sdf mode we render the character in an intermediate 2D context which scale the character this._sdfScale times (which is required to compute the sdf map accurately)
  430. if (this._signedDistanceField) {
  431. this._sdfContext.clearRect(0, 0, this._sdfCanvas.width, this._sdfCanvas.height);
  432. this._sdfContext.fillText(char, 0, -this._offset);
  433. let data = this._sdfContext.getImageData(0, 0, width*this._sdfScale, this._sdfCanvas.height);
  434. let res = this._computeSDFChar(data);
  435. this._context.putImageData(res, this._currentFreePosition.x, this._currentFreePosition.y);
  436. } else {
  437. // Draw the character in the HTML canvas
  438. this._context.fillText(char, this._currentFreePosition.x, this._currentFreePosition.y - this._offset);
  439. }
  440. // Fill the CharInfo object
  441. info.topLeftUV = new Vector2(this._currentFreePosition.x / textureSize.width, this._currentFreePosition.y / textureSize.height);
  442. info.bottomRightUV = new Vector2((this._currentFreePosition.x + width) / textureSize.width, info.topLeftUV.y + ((this._lineHeightSuper + 2) / textureSize.height));
  443. if (this._signedDistanceField) {
  444. let off = 1/textureSize.width;
  445. info.topLeftUV.addInPlace(new Vector2(off, off));
  446. info.bottomRightUV.addInPlace(new Vector2(off, off));
  447. }
  448. info.charWidth = this._superSample ? (width/2) : width;
  449. // Add the info structure
  450. this._charInfos.add(char, info);
  451. this._curCharCount++;
  452. // Set the next position
  453. this._currentFreePosition.x += width + this._xMargin;
  454. return info;
  455. }
  456. private _computeSDFChar(source: ImageData): ImageData {
  457. let scl = this._sdfScale;
  458. let sw = source.width;
  459. let sh = source.height;
  460. let dw = sw / scl;
  461. let dh = sh / scl;
  462. let roffx = 0;
  463. let roffy = 0;
  464. // We shouldn't look beyond half of the biggest between width and height
  465. let radius = scl;
  466. let br = radius - 1;
  467. let lookupSrc = (dx: number, dy: number, offX: number, offY: number, lookVis: boolean): boolean => {
  468. let sx = dx * scl;
  469. let sy = dy * scl;
  470. // Looking out of the area? return true to make the test going on
  471. if (((sx + offX) < 0) || ((sx + offX) >= sw) || ((sy + offY) < 0) || ((sy + offY) >= sh)) {
  472. return true;
  473. }
  474. // Get the pixel we want
  475. let val = source.data[(((sy + offY) * sw) + (sx + offX)) * 4];
  476. let res = (val > 0) === lookVis;
  477. if (!res) {
  478. roffx = offX;
  479. roffy = offY;
  480. }
  481. return res;
  482. }
  483. let lookupArea = (dx: number, dy: number, lookVis: boolean): number => {
  484. // Fast rejection test, if we have the same result in N, S, W, E at a distance which is the radius-1 then it means the data will be consistent in this area. That's because we've scale the rendering of the letter "radius" times, so a letter's pixel will be at least radius wide
  485. if (lookupSrc(dx, dy, 0, br, lookVis) &&
  486. lookupSrc(dx, dy, 0, -br, lookVis) &&
  487. lookupSrc(dx, dy, -br, 0, lookVis) &&
  488. lookupSrc(dx, dy, br, 0, lookVis)) {
  489. return 0;
  490. }
  491. for (let i = 1; i <= radius; i++) {
  492. // Quick test N, S, W, E
  493. if (!lookupSrc(dx, dy, 0, i, lookVis) || !lookupSrc(dx, dy, 0, -i, lookVis) || !lookupSrc(dx, dy, -i, 0, lookVis) || !lookupSrc(dx, dy, i, 0, lookVis)) {
  494. return i * i; // Squared Distance is simple to compute in this case
  495. }
  496. // Test the frame area (except the N, S, W, E spots) from the nearest point from the center to the further one
  497. for (let j = 1; j <= i; j++) {
  498. if (
  499. !lookupSrc(dx, dy, -j, i, lookVis) || !lookupSrc(dx, dy, j, i, lookVis) ||
  500. !lookupSrc(dx, dy, i, -j, lookVis) || !lookupSrc(dx, dy, i, j, lookVis) ||
  501. !lookupSrc(dx, dy, -j, -i, lookVis) || !lookupSrc(dx, dy, j, -i, lookVis) ||
  502. !lookupSrc(dx, dy, -i, -j, lookVis) || !lookupSrc(dx, dy, -i, j, lookVis)) {
  503. // We found the nearest texel having and opposite state, store the squared length
  504. let res = (i * i) + (j * j);
  505. let count = 1;
  506. // To improve quality we will sample the texels around this one, so it's 8 samples, we consider only the one having an opposite state, add them to the current res and will will compute the average at the end
  507. if (!lookupSrc(dx, dy, roffx - 1, roffy, lookVis)) {
  508. res += (roffx - 1) * (roffx - 1) + roffy * roffy;
  509. ++count;
  510. }
  511. if (!lookupSrc(dx, dy, roffx + 1, roffy, lookVis)) {
  512. res += (roffx + 1) * (roffx + 1) + roffy * roffy;
  513. ++count;
  514. }
  515. if (!lookupSrc(dx, dy, roffx, roffy - 1, lookVis)) {
  516. res += roffx * roffx + (roffy - 1) * (roffy - 1);
  517. ++count;
  518. }
  519. if (!lookupSrc(dx, dy, roffx, roffy + 1, lookVis)) {
  520. res += roffx * roffx + (roffy + 1) * (roffy + 1);
  521. ++count;
  522. }
  523. if (!lookupSrc(dx, dy, roffx - 1, roffy - 1, lookVis)) {
  524. res += (roffx - 1) * (roffx - 1) + (roffy - 1) * (roffy - 1);
  525. ++count;
  526. }
  527. if (!lookupSrc(dx, dy, roffx + 1, roffy + 1, lookVis)) {
  528. res += (roffx + 1) * (roffx + 1) + (roffy + 1) * (roffy + 1);
  529. ++count;
  530. }
  531. if (!lookupSrc(dx, dy, roffx + 1, roffy - 1, lookVis)) {
  532. res += (roffx + 1) * (roffx + 1) + (roffy - 1) * (roffy - 1);
  533. ++count;
  534. }
  535. if (!lookupSrc(dx, dy, roffx - 1, roffy + 1, lookVis)) {
  536. res += (roffx - 1) * (roffx - 1) + (roffy + 1) * (roffy + 1);
  537. ++count;
  538. }
  539. // Compute the average based on the accumulated distance
  540. return res / count;
  541. }
  542. }
  543. }
  544. return 0;
  545. }
  546. let tmp = new Array<number>(dw * dh);
  547. for (let y = 0; y < dh; y++) {
  548. for (let x = 0; x < dw; x++) {
  549. let curState = lookupSrc(x, y, 0, 0, true);
  550. let d = lookupArea(x, y, curState);
  551. if (d === 0) {
  552. d = radius * radius * 2;
  553. }
  554. tmp[(y * dw) + x] = curState ? d : -d;
  555. }
  556. }
  557. let res = this._context.createImageData(dw, dh);
  558. let size = dw * dh;
  559. for (let j = 0; j < size; j++) {
  560. let d = tmp[j];
  561. let sign = (d < 0) ? -1 : 1;
  562. d = Math.sqrt(Math.abs(d)) * sign;
  563. d *= 127.5 / radius;
  564. d += 127.5;
  565. if (d < 0) {
  566. d = 0;
  567. } else if (d > 255) {
  568. d = 255;
  569. }
  570. d += 0.5;
  571. res.data[j*4 + 0] = d;
  572. res.data[j*4 + 1] = d;
  573. res.data[j*4 + 2] = d;
  574. res.data[j*4 + 3] = 255;
  575. }
  576. return res;
  577. }
  578. private getSuperSampleFont(font: string): string {
  579. // Eternal thank to http://stackoverflow.com/a/10136041/802124
  580. let regex = /^\s*(?=(?:(?:[-a-z]+\s*){0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\s*){0,2}(small-caps))?)(?=(?:(?:[-a-z]+\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\1|\2|\3)\s*){0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\d]+(?:\%|in|[cem]m|ex|p[ctx]))(?:\s*\/\s*(normal|[.\d]+(?:\%|in|[cem]m|ex|p[ctx])))?\s*([-,\"\sa-z]+?)\s*$/;
  581. let res = font.toLocaleLowerCase().match(regex);
  582. if (res == null) {
  583. return null;
  584. }
  585. let size = parseInt(res[4]);
  586. res[4] = (size * 2).toString() + (res[4].match(/\D+/) || []).pop();
  587. let newFont = "";
  588. for (let j = 1; j < res.length; j++) {
  589. if (res[j] != null) {
  590. newFont += res[j] + " ";
  591. }
  592. }
  593. return newFont;
  594. }
  595. // More info here: https://videlais.com/2014/03/16/the-many-and-varied-problems-with-measuring-font-height-for-html5-canvas/
  596. private getFontHeight(font: string): {height: number, offset: number} {
  597. var fontDraw = document.createElement("canvas");
  598. fontDraw.width = 600;
  599. fontDraw.height = 600;
  600. var ctx = fontDraw.getContext('2d');
  601. ctx.fillRect(0, 0, fontDraw.width, fontDraw.height);
  602. ctx.textBaseline = 'top';
  603. ctx.fillStyle = 'white';
  604. ctx.font = font;
  605. ctx.fillText('jH|', 0, 0);
  606. var pixels = ctx.getImageData(0, 0, fontDraw.width, fontDraw.height).data;
  607. var start = -1;
  608. var end = -1;
  609. for (var row = 0; row < fontDraw.height; row++) {
  610. for (var column = 0; column < fontDraw.width; column++) {
  611. var index = (row * fontDraw.width + column) * 4;
  612. if (pixels[index] === 0) {
  613. if (column === fontDraw.width - 1 && start !== -1) {
  614. end = row;
  615. row = fontDraw.height;
  616. break;
  617. }
  618. continue;
  619. }
  620. else {
  621. if (start === -1) {
  622. start = row;
  623. }
  624. break;
  625. }
  626. }
  627. }
  628. return { height: (end - start)+1, offset: start-1}
  629. }
  630. public get canRescale(): boolean {
  631. return false;
  632. }
  633. public getContext(): CanvasRenderingContext2D {
  634. return this._context;
  635. }
  636. /**
  637. * Call this method when you've call getChar() at least one time, this will update the texture if needed.
  638. * Don't be afraid to call it, if no new character was added, this method simply does nothing.
  639. */
  640. public update(): void {
  641. // Update only if there's new char added since the previous update
  642. if (this._lastUpdateCharCount < this._curCharCount) {
  643. this.getScene().getEngine().updateDynamicTexture(this._texture, this._canvas, false, true);
  644. this._lastUpdateCharCount = this._curCharCount;
  645. }
  646. }
  647. // cloning should be prohibited, there's no point to duplicate this texture at all
  648. public clone(): FontTexture {
  649. return null;
  650. }
  651. /**
  652. * For FontTexture retrieved using GetCachedFontTexture, use this method when you transfer this object's lifetime to another party in order to share this resource.
  653. * When the other party is done with this object, decCachedFontTextureCounter must be called.
  654. */
  655. public incCachedFontTextureCounter() {
  656. ++this._usedCounter;
  657. }
  658. /**
  659. * Use this method only in conjunction with incCachedFontTextureCounter, call it when you no longer need to use this shared resource.
  660. */
  661. public decCachedFontTextureCounter() {
  662. let dic = this.getScene().getExternalData<StringDictionary<FontTexture>>("FontTextureCache");
  663. if (!dic) {
  664. return;
  665. }
  666. if (--this._usedCounter === 0) {
  667. dic.remove(this._cachedFontId);
  668. this.dispose();
  669. }
  670. }
  671. }
  672. /**
  673. * Orginial code from cocos2d-js, converted to TypeScript by Nockawa
  674. * Load the Text version of the BMFont format, no XML or binary supported, just plain old text
  675. */
  676. @BitmapFontLoaderPlugin("fnt", new BMFontLoaderTxt())
  677. class BMFontLoaderTxt implements IBitmapFontLoader {
  678. private static INFO_EXP = /info [^\r\n]*(\r\n|$)/gi;
  679. private static COMMON_EXP = /common [^\n]*(\n|$)/gi;
  680. private static PAGE_EXP = /page [^\n]*(\n|$)/gi;
  681. private static CHAR_EXP = /char [^\n]*(\n|$)/gi;
  682. private static KERNING_EXP = /kerning [^\n]*(\n|$)/gi;
  683. private static ITEM_EXP = /\w+=[^ \r\n]+/gi;
  684. private static INT_EXP = /^[\-]?\d+$/;
  685. private _parseStrToObj(str) {
  686. var arr = str.match(BMFontLoaderTxt.ITEM_EXP);
  687. if (!arr) {
  688. return null;
  689. }
  690. var obj = {};
  691. for (var i = 0, li = arr.length; i < li; i++) {
  692. var tempStr = arr[i];
  693. var index = tempStr.indexOf("=");
  694. var key = tempStr.substring(0, index);
  695. var value = tempStr.substring(index + 1);
  696. if (value.match(BMFontLoaderTxt.INT_EXP)) value = parseInt(value);
  697. else if (value[0] === '"') value = value.substring(1, value.length - 1);
  698. obj[key] = value;
  699. }
  700. return obj;
  701. }
  702. private _buildCharInfo(initialLine: string, obj: any, textureSize: Size, invertY: boolean, chars: StringDictionary<CharInfo>) {
  703. let char: string = null;
  704. let x: number = null;
  705. let y: number = null;
  706. let xadv: number = null;
  707. let width: number = null;
  708. let height: number = null;
  709. let ci = new CharInfo();
  710. for (let key in obj) {
  711. let value = obj[key];
  712. switch (key) {
  713. case "id":
  714. char = String.fromCharCode(value);
  715. break;
  716. case "x":
  717. x = value;
  718. break;
  719. case "y":
  720. y = value;
  721. break;
  722. case "width":
  723. width = value;
  724. break;
  725. case "height":
  726. height = value;
  727. break;
  728. case "xadvance":
  729. xadv = value;
  730. break;
  731. }
  732. }
  733. if (x != null && y != null && width != null && height != null && char != null) {
  734. if (xadv) {
  735. width = xadv;
  736. }
  737. if (invertY) {
  738. ci.topLeftUV = new Vector2(1 - (x / textureSize.width), 1 - (y / textureSize.height));
  739. ci.bottomRightUV = new Vector2(1 - ((x + width) / textureSize.width), 1 - ((y + height) / textureSize.height));
  740. } else {
  741. ci.topLeftUV = new Vector2(x / textureSize.width, y / textureSize.height);
  742. ci.bottomRightUV = new Vector2((x + width) / textureSize.width, (y + height) / textureSize.height);
  743. }
  744. ci.charWidth = width;
  745. chars.add(char, ci);
  746. } else {
  747. console.log("Error while parsing line " + initialLine);
  748. }
  749. }
  750. public loadFont(fontContent: any, scene: Scene, invertY: boolean): { bfi: BitmapFontInfo, errorMsg: string, errorCode: number } {
  751. let fontStr = <string>fontContent;
  752. let bfi = new BitmapFontInfo();
  753. let errorCode = 0;
  754. let errorMsg = "OK";
  755. //padding
  756. let info = fontStr.match(BMFontLoaderTxt.INFO_EXP);
  757. let infoObj = this._parseStrToObj(info[0]);
  758. if (!infoObj) {
  759. return null;
  760. }
  761. let paddingArr = infoObj["padding"].split(",");
  762. bfi.padding = new Vector4(parseInt(paddingArr[0]), parseInt(paddingArr[1]), parseInt(paddingArr[2]), parseInt(paddingArr[3]));
  763. //common
  764. var commonObj = this._parseStrToObj(fontStr.match(BMFontLoaderTxt.COMMON_EXP)[0]);
  765. bfi.lineHeight = commonObj["lineHeight"];
  766. bfi.textureSize = new Size(commonObj["scaleW"], commonObj["scaleH"]);
  767. var maxTextureSize = scene.getEngine()._gl.getParameter(0xd33);
  768. if (commonObj["scaleW"] > maxTextureSize.width || commonObj["scaleH"] > maxTextureSize.height) {
  769. errorMsg = "FontMap texture's size is bigger than what WebGL supports";
  770. errorCode = -1;
  771. } else {
  772. if (commonObj["pages"] !== 1) {
  773. errorMsg = "FontMap must contain one page only.";
  774. errorCode = -1;
  775. } else {
  776. //page
  777. let pageObj = this._parseStrToObj(fontStr.match(BMFontLoaderTxt.PAGE_EXP)[0]);
  778. if (pageObj["id"] !== 0) {
  779. errorMsg = "Only one page of ID 0 is supported";
  780. errorCode = -1;
  781. } else {
  782. bfi.textureFile = pageObj["file"];
  783. //char
  784. let charLines = fontStr.match(BMFontLoaderTxt.CHAR_EXP);
  785. for (let i = 0, li = charLines.length; i < li; i++) {
  786. let charObj = this._parseStrToObj(charLines[i]);
  787. this._buildCharInfo(charLines[i], charObj, bfi.textureSize, invertY, bfi.charDic);
  788. }
  789. //kerning
  790. var kerningLines = fontStr.match(BMFontLoaderTxt.KERNING_EXP);
  791. if (kerningLines) {
  792. for (let i = 0, li = kerningLines.length; i < li; i++) {
  793. let kerningObj = this._parseStrToObj(kerningLines[i]);
  794. bfi.kerningDic.add(((kerningObj["first"] << 16) | (kerningObj["second"] & 0xffff)).toString(), kerningObj["amount"]);
  795. }
  796. }
  797. }
  798. }
  799. }
  800. return { bfi: bfi, errorCode: errorCode, errorMsg: errorMsg };
  801. }
  802. };
  803. export function BitmapFontLoaderPlugin(fileExtension: string, plugin: IBitmapFontLoader): (target: Object) => void {
  804. return () => {
  805. BitmapFontTexture.addLoader(fileExtension, plugin);
  806. }
  807. }
  808. }