├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── cli.js ├── dist ├── GPUText.d.ts ├── GPUText.js └── gputext.min.js ├── example ├── OpenSans-Regular-0.png ├── OpenSans-Regular.json ├── OpenSans-Regular.msdf.bin ├── gputext-webgl.js └── index.html ├── generate ├── build.hxml ├── charsets │ ├── ascii+greek.txt │ ├── ascii.txt │ ├── example.txt │ └── latin-1.txt ├── example.sh ├── generate.js ├── node_modules │ └── opentype.js ├── prebuilt │ └── msdfgen ├── source-fonts │ └── OpenSans │ │ └── OpenSans-Regular.ttf └── src │ ├── BinPacker.hx │ ├── Console.hx │ ├── GenerateOpentypeExterns.hx │ ├── Main.hx │ ├── haxe │ └── zip │ │ └── Compress.hx │ ├── opentype-externs.js │ └── opentype │ ├── BoundingBox.hx │ ├── Coverage.hx │ ├── FeatureList.hx │ ├── Font.hx │ ├── FontOptions.hx │ ├── Glyph.hx │ ├── GlyphOptions.hx │ ├── Layout.hx │ ├── LookupList.hx │ ├── Opentype.hx │ ├── Path.hx │ ├── ScriptList.hx │ ├── Substitution.hx │ └── Table.hx ├── package-lock.json ├── package.json ├── src └── GPUText.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "generate/msdfgen"] 2 | path = generate/lib/msdfgen 3 | url = https://github.com/Chlumsky/msdfgen.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 VALIS Genomics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPU Text 2 | 3 | Engine agnostic GPU text rendering 4 | 5 | [Demo](https://valis-software.github.io/GPUText/example) 6 | 7 | # Building /dist 8 | 9 | ``` 10 | npm install 11 | npm run build 12 | ``` 13 | 14 | ## GPU Text Font Generator 15 | ##### Usage 16 | ``` 17 | ['--charset'] : Path of file containing character set 18 | ['--charlist'] : List of characters 19 | ['--output-dir', '-o'] : Sets the path of the output font file. External resources will be saved in the same directory 20 | ['--technique'] : Font rendering technique, one of: msdf, sdf, bitmap 21 | ['--msdfgen'] : Path of msdfgen executable 22 | ['--size'] : Maximum dimension of a glyph in pixels 23 | ['--pxrange'] : Specifies the width of the range around the shape between the minimum and maximum representable signed distance in pixels 24 | ['--max-texture-size'] : Sets the maximum dimension of the texture atlas 25 | ['--bounds'] : Enables storing glyph bounding boxes in the font (default false) 26 | ['--binary'] : Saves the font in the binary format (experimental; default false) 27 | ['--external-textures'] : Store textures externally when saving in the binary format 28 | ['--help'] : Shows this help 29 | _ : Path of TrueType font file (.ttf) 30 | ``` 31 | 32 | ##### Example Font Generation 33 | Checkout this repo 34 | ``` 35 | > ./cli.js source-fonts/OpenSans/OpenSans-Regular.ttf --binary true 36 | ``` 37 | 38 | ##### Building 39 | The atlas tool depends on [msdfgen](https://github.com/Chlumsky/msdfgen), a command-line tool to generate MSDF distance fields for TrueType glyphs. 40 | Checkout the msdfgen submodule and build it (after installing msdfgen dependencies) 41 | ``` 42 | git submodule init 43 | cd msdfgen 44 | cmake . 45 | make 46 | ``` 47 | 48 | Then with haxe 4.0.0: 49 | ``` 50 | haxelib install build.hxml 51 | haxe build.hxml 52 | ``` 53 | 54 | ## Release Todos 55 | - prebuilt msdfgen for windows 56 | - Complex layout demo 57 | - Text of different fonts within a single layout 58 | - Document public methods 59 | - Manually generate signed distance atlas mipmaps (this improves quality at low font sizes) 60 | - Store mipmap level in alpha and use to offset uvs to precise pixel coords 61 | - Support 3D anti-aliasing 62 | - Create examples for other libraries 63 | - three.js -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./generate/generate'); -------------------------------------------------------------------------------- /dist/GPUText.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Provides text layout, vertex buffer generation and file parsing 3 | 4 | Dev notes: 5 | - Should have progressive layout where text can be appended to an existing layout 6 | **/ 7 | declare class GPUText { 8 | static layout(text: string, font: GPUTextFont, layoutOptions: { 9 | kerningEnabled?: boolean; 10 | ligaturesEnabled?: boolean; 11 | lineHeight?: number; 12 | glyphScale?: number; 13 | }): GlyphLayout; 14 | /** 15 | Generates OpenGL coordinates where y increases from bottom to top 16 | 17 | @! improve docs 18 | 19 | => float32, [p, p, u, u, u], triangles with CCW face winding 20 | **/ 21 | static generateVertexData(glyphLayout: GlyphLayout): { 22 | vertexArray: Float32Array; 23 | elementsPerVertex: number; 24 | vertexCount: number; 25 | vertexLayout: { 26 | position: { 27 | elements: number; 28 | elementSizeBytes: number; 29 | strideBytes: number; 30 | offsetBytes: number; 31 | }; 32 | uv: { 33 | elements: number; 34 | elementSizeBytes: number; 35 | strideBytes: number; 36 | offsetBytes: number; 37 | }; 38 | }; 39 | }; 40 | /** 41 | * Given buffer containing a binary GPUText file, parse it and generate a GPUTextFont object 42 | * @throws string on parse errors 43 | */ 44 | static parse(buffer: ArrayBuffer): GPUTextFont; 45 | } 46 | export interface GPUTextFont extends GPUTextFontBase { 47 | characters: { 48 | [character: string]: TextureAtlasCharacter | null; 49 | }; 50 | kerning: { 51 | [characterPair: string]: number; 52 | }; 53 | glyphBounds?: { 54 | [character: string]: { 55 | l: number; 56 | b: number; 57 | r: number; 58 | t: number; 59 | }; 60 | }; 61 | textures: Array>; 64 | } 65 | export interface GlyphLayout { 66 | font: GPUTextFont; 67 | sequence: Array<{ 68 | char: string; 69 | x: number; 70 | y: number; 71 | }>; 72 | bounds: { 73 | l: number; 74 | r: number; 75 | t: number; 76 | b: number; 77 | }; 78 | glyphScale: number; 79 | } 80 | export interface TextureAtlasGlyph { 81 | atlasRect: { 82 | x: number; 83 | y: number; 84 | w: number; 85 | h: number; 86 | }; 87 | atlasScale: number; 88 | offset: { 89 | x: number; 90 | y: number; 91 | }; 92 | } 93 | export interface TextureAtlasCharacter { 94 | advance: number; 95 | glyph?: TextureAtlasGlyph; 96 | } 97 | export interface ResourceReference { 98 | payloadBytes?: { 99 | start: number; 100 | length: number; 101 | }; 102 | localPath?: string; 103 | } 104 | declare type GPUTextFormat = 'TextureAtlasFontJson' | 'TextureAtlasFontBinary'; 105 | declare type GPUTextTechnique = 'msdf' | 'sdf' | 'bitmap'; 106 | interface GPUTextFontMetadata { 107 | family: string; 108 | subfamily: string; 109 | version: string; 110 | postScriptName: string; 111 | copyright: string; 112 | trademark: string; 113 | manufacturer: string; 114 | manufacturerURL: string; 115 | designerURL: string; 116 | license: string; 117 | licenseURL: string; 118 | height_funits: number; 119 | funitsPerEm: number; 120 | } 121 | interface GPUTextFontBase { 122 | format: GPUTextFormat; 123 | version: number; 124 | technique: GPUTextTechnique; 125 | textureSize: { 126 | w: number; 127 | h: number; 128 | }; 129 | ascender: number; 130 | descender: number; 131 | typoAscender: number; 132 | typoDescender: number; 133 | lowercaseHeight: number; 134 | metadata: GPUTextFontMetadata; 135 | fieldRange_px: number; 136 | } 137 | export default GPUText; 138 | -------------------------------------------------------------------------------- /dist/GPUText.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | /** 12 | Provides text layout, vertex buffer generation and file parsing 13 | 14 | Dev notes: 15 | - Should have progressive layout where text can be appended to an existing layout 16 | **/ 17 | var GPUText = /** @class */ (function () { 18 | function GPUText() { 19 | } 20 | // y increases from top-down (like HTML/DOM coordinates) 21 | // y = 0 is set to be the font's ascender: https://i.stack.imgur.com/yjbKI.png 22 | // https://stackoverflow.com/a/50047090/4038621 23 | GPUText.layout = function (text, font, layoutOptions) { 24 | var opts = __assign({ glyphScale: 1.0, kerningEnabled: true, ligaturesEnabled: true, lineHeight: 1.0 }, layoutOptions); 25 | // scale text-wrap container 26 | // @! let wrapWidth /= glyphScale; 27 | // pre-allocate for each character having a glyph 28 | var sequence = new Array(text.length); 29 | var sequenceIndex = 0; 30 | var bounds = { 31 | l: 0, r: 0, 32 | t: 0, b: 0, 33 | }; 34 | var x = 0; 35 | var y = 0; 36 | for (var c = 0; c < text.length; c++) { 37 | var char = text[c]; 38 | var charCode = text.charCodeAt(c); 39 | // @! layout 40 | switch (charCode) { 41 | case 0xA0: 42 | // space character that prevents an automatic line break at its position. In some formats, including HTML, it also prevents consecutive whitespace characters from collapsing into a single space. 43 | // @! todo 44 | case '\n'.charCodeAt(0): // newline 45 | y += opts.lineHeight; 46 | x = 0; 47 | continue; 48 | } 49 | if (opts.ligaturesEnabled) { 50 | // @! todo, replace char and charCode if sequence maps to a ligature 51 | } 52 | if (opts.kerningEnabled && c > 0) { 53 | var kerningKey = text[c - 1] + char; 54 | x += font.kerning[kerningKey] || 0.0; 55 | } 56 | var fontCharacter = font.characters[char]; 57 | if (fontCharacter == null) { 58 | console.warn("Font does not contain character for \"" + char + "\" (" + charCode + ")"); 59 | continue; 60 | } 61 | if (fontCharacter.glyph != null) { 62 | // character has a glyph 63 | // this corresponds top-left coordinate of the glyph, like hanging letters on a line 64 | sequence[sequenceIndex++] = { 65 | char: char, 66 | x: x, 67 | y: y 68 | }; 69 | // width of a character is considered to be its 'advance' 70 | // height of a character is considered to be the lineHeight 71 | bounds.r = Math.max(bounds.r, x + fontCharacter.advance); 72 | bounds.b = Math.max(bounds.b, y + opts.lineHeight); 73 | } 74 | // advance glyph position 75 | // @! layout 76 | x += fontCharacter.advance; 77 | } 78 | // trim empty entries 79 | if (sequence.length > sequenceIndex) { 80 | sequence.length = sequenceIndex; 81 | } 82 | return { 83 | font: font, 84 | sequence: sequence, 85 | bounds: bounds, 86 | glyphScale: opts.glyphScale, 87 | }; 88 | }; 89 | /** 90 | Generates OpenGL coordinates where y increases from bottom to top 91 | 92 | @! improve docs 93 | 94 | => float32, [p, p, u, u, u], triangles with CCW face winding 95 | **/ 96 | GPUText.generateVertexData = function (glyphLayout) { 97 | // memory layout details 98 | var elementSizeBytes = 4; // (float32) 99 | var positionElements = 2; 100 | var uvElements = 3; // uv.z = glyph.atlasScale 101 | var elementsPerVertex = positionElements + uvElements; 102 | var vertexSizeBytes = elementsPerVertex * elementSizeBytes; 103 | var characterVertexCount = 6; 104 | var vertexArray = new Float32Array(glyphLayout.sequence.length * characterVertexCount * elementsPerVertex); 105 | var characterOffset_vx = 0; // in terms of numbers of vertices rather than array elements 106 | for (var i = 0; i < glyphLayout.sequence.length; i++) { 107 | var item = glyphLayout.sequence[i]; 108 | var font = glyphLayout.font; 109 | var fontCharacter = font.characters[item.char]; 110 | // skip null-glyphs 111 | if (fontCharacter == null || fontCharacter.glyph == null) 112 | continue; 113 | var glyph = fontCharacter.glyph; 114 | // quad dimensions 115 | var px = item.x - glyph.offset.x; 116 | // y = 0 in the glyph corresponds to the baseline, which is font.ascender from the top of the glyph 117 | var py = -(item.y + font.ascender + glyph.offset.y); 118 | var w = glyph.atlasRect.w / glyph.atlasScale; // convert width to normalized font units 119 | var h = glyph.atlasRect.h / glyph.atlasScale; 120 | // uv 121 | // add half-text offset to map to texel centers 122 | var ux = (glyph.atlasRect.x + 0.5) / font.textureSize.w; 123 | var uy = (glyph.atlasRect.y + 0.5) / font.textureSize.h; 124 | var uw = (glyph.atlasRect.w - 1.0) / font.textureSize.w; 125 | var uh = (glyph.atlasRect.h - 1.0) / font.textureSize.h; 126 | // flip glyph uv y, this is different from flipping the glyph y _position_ 127 | uy = uy + uh; 128 | uh = -uh; 129 | // two-triangle quad with ccw face winding 130 | vertexArray.set([ 131 | px, py, ux, uy, glyph.atlasScale, 132 | px + w, py + h, ux + uw, uy + uh, glyph.atlasScale, 133 | px, py + h, ux, uy + uh, glyph.atlasScale, 134 | px, py, ux, uy, glyph.atlasScale, 135 | px + w, py, ux + uw, uy, glyph.atlasScale, 136 | px + w, py + h, ux + uw, uy + uh, glyph.atlasScale, 137 | ], characterOffset_vx * elementsPerVertex); 138 | // advance character quad in vertex array 139 | characterOffset_vx += characterVertexCount; 140 | } 141 | return { 142 | vertexArray: vertexArray, 143 | elementsPerVertex: elementsPerVertex, 144 | vertexCount: characterOffset_vx, 145 | vertexLayout: { 146 | position: { 147 | elements: positionElements, 148 | elementSizeBytes: elementSizeBytes, 149 | strideBytes: vertexSizeBytes, 150 | offsetBytes: 0, 151 | }, 152 | uv: { 153 | elements: uvElements, 154 | elementSizeBytes: elementSizeBytes, 155 | strideBytes: vertexSizeBytes, 156 | offsetBytes: positionElements * elementSizeBytes, 157 | } 158 | } 159 | }; 160 | }; 161 | /** 162 | * Given buffer containing a binary GPUText file, parse it and generate a GPUTextFont object 163 | * @throws string on parse errors 164 | */ 165 | GPUText.parse = function (buffer) { 166 | var dataView = new DataView(buffer); 167 | // read header string, expect utf-8 encoded 168 | // the end of the header string is marked by a null character 169 | var p = 0; 170 | // end of header is marked by first 0x00 byte 171 | for (; p < buffer.byteLength; p++) { 172 | var byte = dataView.getInt8(p); 173 | if (byte === 0) 174 | break; 175 | } 176 | var headerBytes = new Uint8Array(buffer, 0, p); 177 | var jsonHeader = decodeUTF8(headerBytes); 178 | // payload is starts from the first byte after the null character 179 | var payloadStart = p + 1; 180 | var littleEndian = true; 181 | var header = JSON.parse(jsonHeader); 182 | // initialize GPUTextFont object 183 | var gpuTextFont = { 184 | format: header.format, 185 | version: header.version, 186 | technique: header.technique, 187 | ascender: header.ascender, 188 | descender: header.descender, 189 | typoAscender: header.typoAscender, 190 | typoDescender: header.typoDescender, 191 | lowercaseHeight: header.lowercaseHeight, 192 | metadata: header.metadata, 193 | fieldRange_px: header.fieldRange_px, 194 | characters: {}, 195 | kerning: {}, 196 | glyphBounds: undefined, 197 | textures: [], 198 | textureSize: header.textureSize, 199 | }; 200 | // parse character data payload into GPUTextFont characters map 201 | var characterDataView = new DataView(buffer, payloadStart + header.characters.start, header.characters.length); 202 | var characterBlockLength_bytes = 4 + // advance: F32 203 | 2 * 4 + // atlasRect(x, y, w, h): UI16 204 | 4 + // atlasScale: F32 205 | 4 * 2; // offset(x, y): F32 206 | for (var i = 0; i < header.charList.length; i++) { 207 | var char = header.charList[i]; 208 | var b0 = i * characterBlockLength_bytes; 209 | var glyph = { 210 | atlasRect: { 211 | x: characterDataView.getUint16(b0 + 4, littleEndian), 212 | y: characterDataView.getUint16(b0 + 6, littleEndian), 213 | w: characterDataView.getUint16(b0 + 8, littleEndian), 214 | h: characterDataView.getUint16(b0 + 10, littleEndian), 215 | }, 216 | atlasScale: characterDataView.getFloat32(b0 + 12, littleEndian), 217 | offset: { 218 | x: characterDataView.getFloat32(b0 + 16, littleEndian), 219 | y: characterDataView.getFloat32(b0 + 20, littleEndian), 220 | } 221 | }; 222 | // A glyph with 0 size is considered to be a null-glyph 223 | var isNullGlyph = glyph.atlasRect.w === 0 || glyph.atlasRect.h === 0; 224 | var characterData = { 225 | advance: characterDataView.getFloat32(b0 + 0, littleEndian), 226 | glyph: isNullGlyph ? undefined : glyph 227 | }; 228 | gpuTextFont.characters[char] = characterData; 229 | } 230 | // kerning payload 231 | var kerningDataView = new DataView(buffer, payloadStart + header.kerning.start, header.kerning.length); 232 | var kerningLength_bytes = 4; 233 | for (var i = 0; i < header.kerningPairs.length; i++) { 234 | var pair = header.kerningPairs[i]; 235 | var kerning = kerningDataView.getFloat32(i * kerningLength_bytes, littleEndian); 236 | gpuTextFont.kerning[pair] = kerning; 237 | } 238 | // glyph bounds payload 239 | if (header.glyphBounds != null) { 240 | gpuTextFont.glyphBounds = {}; 241 | var glyphBoundsDataView = new DataView(buffer, payloadStart + header.glyphBounds.start, header.glyphBounds.length); 242 | var glyphBoundsBlockLength_bytes = 4 * 4; 243 | for (var i = 0; i < header.charList.length; i++) { 244 | var char = header.charList[i]; 245 | var b0 = i * glyphBoundsBlockLength_bytes; 246 | // t r b l 247 | var bounds = { 248 | t: glyphBoundsDataView.getFloat32(b0 + 0, littleEndian), 249 | r: glyphBoundsDataView.getFloat32(b0 + 4, littleEndian), 250 | b: glyphBoundsDataView.getFloat32(b0 + 8, littleEndian), 251 | l: glyphBoundsDataView.getFloat32(b0 + 12, littleEndian), 252 | }; 253 | gpuTextFont.glyphBounds[char] = bounds; 254 | } 255 | } 256 | // texture payload 257 | // textures may be in the payload or an external reference 258 | for (var p_1 = 0; p_1 < header.textures.length; p_1++) { 259 | var page = header.textures[p_1]; 260 | gpuTextFont.textures[p_1] = []; 261 | for (var m = 0; m < page.length; m++) { 262 | var mipmap = page[m]; 263 | if (mipmap.payloadBytes != null) { 264 | // convert payload's image bytes into a HTMLImageElement object 265 | var imageBufferView = new Uint8Array(buffer, payloadStart + mipmap.payloadBytes.start, mipmap.payloadBytes.length); 266 | var imageBlob = new Blob([imageBufferView], { type: "image/png" }); 267 | var image = new Image(); 268 | image.src = URL.createObjectURL(imageBlob); 269 | gpuTextFont.textures[p_1][m] = image; 270 | } 271 | else if (mipmap.localPath != null) { 272 | // payload contains no image bytes; the image is store externally, pass on the path 273 | gpuTextFont.textures[p_1][m] = { 274 | localPath: mipmap.localPath 275 | }; 276 | } 277 | } 278 | } 279 | return gpuTextFont; 280 | }; 281 | return GPUText; 282 | }()); 283 | // credits github user pascaldekloe 284 | // https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330 285 | function decodeUTF8(bytes) { 286 | var i = 0, s = ''; 287 | while (i < bytes.length) { 288 | var c = bytes[i++]; 289 | if (c > 127) { 290 | if (c > 191 && c < 224) { 291 | if (i >= bytes.length) 292 | throw new Error('UTF-8 decode: incomplete 2-byte sequence'); 293 | c = (c & 31) << 6 | bytes[i++] & 63; 294 | } 295 | else if (c > 223 && c < 240) { 296 | if (i + 1 >= bytes.length) 297 | throw new Error('UTF-8 decode: incomplete 3-byte sequence'); 298 | c = (c & 15) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63; 299 | } 300 | else if (c > 239 && c < 248) { 301 | if (i + 2 >= bytes.length) 302 | throw new Error('UTF-8 decode: incomplete 4-byte sequence'); 303 | c = (c & 7) << 18 | (bytes[i++] & 63) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63; 304 | } 305 | else 306 | throw new Error('UTF-8 decode: unknown multibyte start 0x' + c.toString(16) + ' at index ' + (i - 1)); 307 | } 308 | if (c <= 0xffff) 309 | s += String.fromCharCode(c); 310 | else if (c <= 0x10ffff) { 311 | c -= 0x10000; 312 | s += String.fromCharCode(c >> 10 | 0xd800); 313 | s += String.fromCharCode(c & 0x3FF | 0xdc00); 314 | } 315 | else 316 | throw new Error('UTF-8 decode: code point 0x' + c.toString(16) + ' exceeds UTF-16 reach'); 317 | } 318 | return s; 319 | } 320 | exports.default = GPUText; 321 | if (typeof window !== 'undefined') { 322 | // expose GPUText on the window object 323 | window.GPUText = GPUText; 324 | } 325 | -------------------------------------------------------------------------------- /dist/gputext.min.js: -------------------------------------------------------------------------------- 1 | "use strict";var __assign=this&&this.__assign||Object.assign||function(e){for(var t,a=1,r=arguments.length;al&&(n.length=l),{font:t,sequence:n,bounds:s,glyphScale:r.glyphScale}},e.generateVertexData=function(e){for(var t=new Float32Array(6*e.sequence.length*5),a=0,r=0;r 2 | 3 | 4 | 5 | MSDF 6 | 7 | 8 | 9 | 10 | 11 | 47 | 48 | 49 |

 50 | 	
 51 | 
 52 | 	
 67 | 
 68 | 	
 90 | 
 91 | 
 92 | 	
93 |
94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 | 102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 345 | 346 | -------------------------------------------------------------------------------- /generate/build.hxml: -------------------------------------------------------------------------------- 1 | # haxe 4.0.0 2 | -main Main 3 | -cp src 4 | 5 | -lib hxargs:3.0.2 6 | -lib format:3.4.0 7 | -lib pako:git:https://github.com/azrafe7/hxPako.git#8cf5ac46e79a3cce14d349e840916d3857aee45d 8 | 9 | -dce full 10 | -D analyzer-optimize 11 | 12 | # node.js target 13 | -lib hxnodejs:git:https://github.com/HaxeFoundation/hxnodejs.git#4b20c1cf371e09b4a774ed331f50cbde7b818a4f 14 | 15 | -js generate.js -------------------------------------------------------------------------------- /generate/charsets/ascii+greek.txt: -------------------------------------------------------------------------------- 1 | !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρςστυφχψω -------------------------------------------------------------------------------- /generate/charsets/ascii.txt: -------------------------------------------------------------------------------- 1 | !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ -------------------------------------------------------------------------------- /generate/charsets/example.txt: -------------------------------------------------------------------------------- 1 | !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ΔΘΞΣΩαβγεζηθλμνπστυφχω£©«»±·×²³¼½¾ -------------------------------------------------------------------------------- /generate/charsets/latin-1.txt: -------------------------------------------------------------------------------- 1 | !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ -------------------------------------------------------------------------------- /generate/example.sh: -------------------------------------------------------------------------------- 1 | node generate.js source-fonts/OpenSans/OpenSans-Regular.ttf --binary true --charset charsets/example.txt && 2 | cp OpenSans-Regular* ../example/ -------------------------------------------------------------------------------- /generate/prebuilt/msdfgen: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VALIS-software/GPUText/d0f73831247107a17171c8bbaa7c63a116f592d9/generate/prebuilt/msdfgen -------------------------------------------------------------------------------- /generate/source-fonts/OpenSans/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VALIS-software/GPUText/d0f73831247107a17171c8bbaa7c63a116f592d9/generate/source-fonts/OpenSans/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /generate/src/BinPacker.hx: -------------------------------------------------------------------------------- 1 | // Translated from https://github.com/jakesgordon/bin-packing/blob/master/js/packer.js 2 | @:structInit 3 | class Node { 4 | public var x: Float; 5 | public var y: Float; 6 | public var w: Float; 7 | public var h: Float; 8 | 9 | public var used = false; 10 | public var down: Node; 11 | public var right: Node; 12 | 13 | public function new(x: Float, y: Float, w: Float, h: Float) { 14 | this.x = x; 15 | this.y = y; 16 | this.w = w; 17 | this.h = h; 18 | } 19 | } 20 | 21 | class BinPacker { 22 | 23 | public static function fit(blocks: Array<{w: Float, h: Float}>, w: Float, h: Float) { 24 | var sortedBlocks = blocks.copy(); 25 | sortedBlocks.sort((a, b) -> { 26 | var am = Math.max(a.w, a.h); 27 | var bm = Math.max(b.w, b.h); 28 | am > bm ? 1 : -1; 29 | }); 30 | 31 | var fits = new Array>(); 32 | 33 | var root = new Node(0, 0, w, h); 34 | 35 | for (block in sortedBlocks) { 36 | var node = findNode(root, block.w, block.h); 37 | fits[blocks.indexOf(block)] = node != null ? splitNode(node, block.w, block.h) : null; 38 | } 39 | return fits; 40 | } 41 | 42 | static function findNode(parent: Node, w: Float, h: Float) { 43 | if (parent.used) { 44 | var right = findNode(parent.right, w, h); 45 | return right != null ? right : findNode(parent.down, w, h); 46 | } else if ((w <= parent.w) && (h <= parent.h)) { 47 | return parent; 48 | } 49 | return null; 50 | } 51 | 52 | static function splitNode(node: Node, w: Float, h: Float) { 53 | node.used = true; 54 | node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h }; 55 | node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h }; 56 | return node; 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /generate/src/Console.hx: -------------------------------------------------------------------------------- 1 | #if macro 2 | import haxe.macro.Format; 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | #end 6 | 7 | // For windows consoles we have to enable formatting through kernel32 calls 8 | @:cppFileCode(' 9 | #if defined(HX_WINDOWS) 10 | #include 11 | #endif 12 | ') 13 | @:nullSafety 14 | class Console { 15 | 16 | static public var formatMode = determineConsoleFormatMode(); 17 | 18 | static public var logPrefix = '> '; 19 | static public var warnPrefix = '> '; 20 | static public var errorPrefix = '> '; 21 | static public var successPrefix = '> '; 22 | static public var debugPrefix = '> '; 23 | 24 | // sometimes it's useful to intercept calls to print 25 | // return false to prevent default print behavior 26 | // (str: String, outputStream: ConsoleOutputStream) -> Bool 27 | static public var printIntercept: Null ConsoleOutputStream -> Bool> = null; 28 | 29 | static var argSeparator = ' '; 30 | static var unicodeCompatibilityMode:UnicodeCompatibilityMode = #if ((sys || nodejs) && !macro) Sys.systemName() == 'Windows' ? Windows : #end None; 31 | static var unicodeCompatibilityEnabled = false; 32 | 33 | macro static public function log(rest:Array){ 34 | rest = rest.map(removeMarkupMeta); 35 | return macro Console.printlnFormatted(Console.logPrefix + ${joinArgExprs(rest)}, Log); 36 | } 37 | 38 | macro static public function warn(rest:Array){ 39 | rest = rest.map(removeMarkupMeta); 40 | return macro Console.printlnFormatted(Console.warnPrefix + ${joinArgExprs(rest)}, Warn); 41 | } 42 | 43 | macro static public function error(rest:Array){ 44 | rest = rest.map(removeMarkupMeta); 45 | return macro Console.printlnFormatted(Console.errorPrefix + ${joinArgExprs(rest)}, Error); 46 | } 47 | 48 | macro static public function success(rest:Array){ 49 | rest = rest.map(removeMarkupMeta); 50 | return macro Console.printlnFormatted(Console.successPrefix + ${joinArgExprs(rest)}, Log); 51 | } 52 | 53 | macro static public function examine(rest:Array) { 54 | var printer = new haxe.macro.Printer(); 55 | var namedArgs = rest.map(function(e) { 56 | switch e.expr { 57 | case EConst(CInt(_) | CFloat(_)): 58 | return macro '' + $e + ''; 59 | case EConst(CString(_)): 60 | return macro '' + $e + ''; 61 | // print expression as a string as well as the expression value (in bold) 62 | default: 63 | var exprString = printer.printExpr(e); 64 | return macro '' + $v{exprString} + ': ' + Std.string(cast $e) + ''; 65 | } 66 | }); 67 | return macro Console.log(${joinArgExprs(namedArgs)}); 68 | } 69 | 70 | // Only generates log call if -debug build flag is supplied 71 | macro static public function debug(rest:Array){ 72 | if(!Context.getDefines().exists('debug')) return macro null; 73 | rest = rest.map(removeMarkupMeta); 74 | #if haxe4 75 | var pos = haxe.macro.PositionTools.toLocation(Context.currentPos()); 76 | var posString = '${pos.file}:${pos.range.start.line}'; 77 | #elseif haxe3 78 | var pos = Context.currentPos(); 79 | var posString: String = '$pos'; 80 | // strip #pos( characters x-y) 81 | var posPattern = ~/#pos\((.*)\)$/; 82 | if (posPattern.match(posString)) { 83 | posString = posPattern.matched(1); 84 | var charactersPattern = ~/:\s*characters\s*[\d-]+$/; 85 | if (charactersPattern.match(posString)) { 86 | posString = charactersPattern.matchedLeft(); 87 | } 88 | } 89 | #end 90 | return macro Console.printlnFormatted(Console.debugPrefix + '$posString: ' + ${joinArgExprs(rest)}, Debug); 91 | } 92 | 93 | static public inline function printlnFormatted(?s:String = '', outputStream:ConsoleOutputStream = Log){ 94 | return printFormatted(s + '\n', outputStream); 95 | } 96 | 97 | static public inline function println(s:String = '', outputStream:ConsoleOutputStream = Log){ 98 | return print(s + '\n', outputStream); 99 | } 100 | 101 | #if macro 102 | static function removeMarkupMeta(e: Expr) { 103 | return switch e.expr { 104 | case EMeta({name: ':markup'}, innerExpr): 105 | Format.format(innerExpr); 106 | default: 107 | e; 108 | } 109 | } 110 | #end 111 | 112 | static var formatTagPattern = ~/(\\)?<(\/)?([^><{}\s]*|{[^}<>]*})>/g; 113 | static function format(s: String, formatMode: ConsoleFormatMode) { 114 | s = s + '';// Add a reset all to the end to prevent overflowing formatting to subsequent lines 115 | 116 | var activeFormatFlagStack = new Array(); 117 | var groupedProceedingTags = new Array(); 118 | var browserFormatArguments = []; 119 | 120 | inline function addFlag(flag: FormatFlag, proceedingTags: Int) { 121 | activeFormatFlagStack.push(flag); 122 | groupedProceedingTags.push(proceedingTags); 123 | } 124 | 125 | inline function removeFlag(flag: FormatFlag) { 126 | var i = activeFormatFlagStack.indexOf(flag); 127 | if (i != -1) { 128 | var proceedingTags = groupedProceedingTags[i]; 129 | // remove n tags 130 | activeFormatFlagStack.splice(i - proceedingTags, proceedingTags + 1); 131 | groupedProceedingTags.splice(i - proceedingTags, proceedingTags + 1); 132 | } 133 | } 134 | 135 | inline function resetFlags() { 136 | activeFormatFlagStack = []; 137 | groupedProceedingTags = []; 138 | } 139 | 140 | var result = formatTagPattern.map(s, function(e) { 141 | var escaped = e.matched(1) != null; 142 | if (escaped) { 143 | return e.matched(0); 144 | } 145 | 146 | var open = e.matched(2) == null; 147 | var tags = e.matched(3).split(','); 148 | 149 | // handle and 150 | if (!open && tags.length == 1) { 151 | if (tags[0] == '') { 152 | // we've got a shorthand to close the last tag: 153 | var last = activeFormatFlagStack[activeFormatFlagStack.length - 1]; 154 | removeFlag(last); 155 | } else if (FormatFlag.fromString(tags[0]) == FormatFlag.RESET) { 156 | resetFlags(); 157 | } else { 158 | // handle 159 | var flag = FormatFlag.fromString(tags[0]); 160 | if (flag != null) { 161 | removeFlag(flag); 162 | } 163 | } 164 | } else { 165 | var proceedingTags = 0; 166 | for (tag in tags) { 167 | var flag = FormatFlag.fromString(tag); 168 | if (flag == null) return e.matched(0); // unhandled tag, don't treat as formatting 169 | if (open) { 170 | addFlag(flag, proceedingTags); 171 | proceedingTags++; 172 | } else { 173 | removeFlag(flag); 174 | } 175 | } 176 | } 177 | 178 | // since format flags are cumulative, we only need to add the last item if it's an open tag 179 | switch formatMode { 180 | #if (sys || nodejs) 181 | case AsciiTerminal: 182 | // since format flags are cumulative, we only need to add the last item if it's an open tag 183 | if (open) { 184 | if (activeFormatFlagStack.length > 0) { 185 | var lastFlagCount = groupedProceedingTags[groupedProceedingTags.length - 1] + 1; 186 | var asciiFormatString = ''; 187 | for (i in 0...lastFlagCount) { 188 | var idx = groupedProceedingTags.length - 1 - i; 189 | asciiFormatString += getAsciiFormat(activeFormatFlagStack[idx]); 190 | } 191 | return asciiFormatString; 192 | } else { 193 | return ''; 194 | } 195 | } else { 196 | return 197 | getAsciiFormat(FormatFlag.RESET) + 198 | activeFormatFlagStack.map(function(f) return getAsciiFormat(f)) 199 | .filter(function(s) return s != null) 200 | .join(''); 201 | } 202 | #end 203 | #if js 204 | case BrowserConsole: 205 | browserFormatArguments.push( 206 | activeFormatFlagStack.map(function(f) return getBrowserFormat(f)) 207 | .filter(function(s) return s != null) 208 | .join(';') 209 | ); 210 | return '%c'; 211 | #end 212 | case Disabled: 213 | return ''; 214 | } 215 | }); 216 | 217 | return { 218 | formatted: result, 219 | #if js 220 | browserFormatArguments: browserFormatArguments, 221 | #end 222 | } 223 | } 224 | 225 | static public function stripFormatting(s: String) { 226 | return format(s, Disabled).formatted; 227 | } 228 | 229 | /** 230 | # Parse formatted message and print to console 231 | - Apply formatting with HTML-like tags: `bold` 232 | - Tags are case-insensitive 233 | - A closing tag without a tag name can be used to close the last-open format tag `` so `bold` will also work 234 | - A double-closing tag like `` will clear all active formatting 235 | - Multiple tags can be combined with comma separation, `bold-italic` 236 | - Whitespace is not allowed in tags, so `` would be ignored and printed as-is 237 | - Tags can be escaped with a leading backslash: `\` would be printed as-is 238 | - Unknown tags are skipped and will not show up in the output 239 | - For browser targets, CSS fields and colors can be used, for example: `<{color: red; font-size: 20px}>Inline CSS` or `<#FF0000>Red Text`. These will have no affect on native consoles 240 | **/ 241 | #if (sys || nodejs) 242 | public 243 | #end 244 | static function printFormatted(s:String = '', outputStream:ConsoleOutputStream = Log){ 245 | var result = format(s, formatMode); 246 | 247 | // for browser consoles we need to call console.log with formatting arguments 248 | #if js 249 | if (formatMode == BrowserConsole) { 250 | #if (!no_console) 251 | var logArgs = [result.formatted].concat(result.browserFormatArguments); 252 | 253 | #if haxe4 254 | switch outputStream { 255 | case Log, Debug: js.Syntax.code('console.log.apply(console, {0})', logArgs); 256 | case Warn: js.Syntax.code('console.warn.apply(console, {0})', logArgs); 257 | case Error: js.Syntax.code('console.error.apply(console, {0})', logArgs); 258 | } 259 | #elseif haxe3 260 | switch outputStream { 261 | case Log, Debug: untyped __js__('console.log.apply(console, {0})', logArgs); 262 | case Warn: untyped __js__('console.warn.apply(console, {0})', logArgs); 263 | case Error: untyped __js__('console.error.apply(console, {0})', logArgs); 264 | } 265 | #end 266 | 267 | #end 268 | return; 269 | } 270 | #end 271 | 272 | // otherwise we can print with inline escape codes 273 | print(result.formatted, outputStream); 274 | } 275 | 276 | #if (sys || nodejs) 277 | public 278 | #end 279 | static function print(s:String = '', outputStream:ConsoleOutputStream = Log){ 280 | // if printIntercept is set then call it first 281 | // if it returns false then don't print to console 282 | if (printIntercept != null) { 283 | var allowDefaultPrint = printIntercept(s, outputStream); 284 | if (!allowDefaultPrint) { 285 | return; 286 | } 287 | } 288 | 289 | #if (!no_console) 290 | 291 | #if (sys || nodejs) 292 | 293 | // if we're running windows then enable unicode output while printing 294 | if (unicodeCompatibilityMode == Windows && !unicodeCompatibilityEnabled) { 295 | exec('chcp 65001'); 296 | unicodeCompatibilityEnabled = true; 297 | } 298 | 299 | switch outputStream { 300 | case Log, Debug: 301 | Sys.stdout().writeString(s); 302 | case Warn, Error: 303 | Sys.stderr().writeString(s); 304 | } 305 | 306 | #elseif js 307 | // browser log 308 | #if haxe4 309 | switch outputStream { 310 | case Log, Debug: js.Syntax.code('console.log({0})', s); 311 | case Warn: js.Syntax.code('console.warn({0})', s); 312 | case Error: js.Syntax.code('console.error({0})', s); 313 | } 314 | #elseif haxe3 315 | switch outputStream { 316 | case Log, Debug: untyped __js__('console.log({0})', s); 317 | case Warn: untyped __js__('console.warn({0})', s); 318 | case Error: untyped __js__('console.error({0})', s); 319 | } 320 | #end 321 | #end 322 | 323 | #end // #if (!no_console) 324 | } 325 | 326 | // returns empty string for unhandled format 327 | static function getAsciiFormat(flag:FormatFlag): String { 328 | // custom hex color 329 | if ((flag:String).charAt(0) == '#') { 330 | var hex = (flag:String).substr(1); 331 | var r = Std.parseInt('0x'+hex.substr(0, 2)), g = Std.parseInt('0x'+hex.substr(2, 2)), b = Std.parseInt('0x'+hex.substr(4, 2)); 332 | return '\033[38;5;' + rgbToAscii256(cast r, cast g, cast b) + 'm'; 333 | } 334 | 335 | // custom hex background 336 | if ((flag:String).substr(0, 3) == 'bg#') { 337 | var hex = (flag:String).substr(3); 338 | var r = Std.parseInt('0x'+hex.substr(0, 2)), g = Std.parseInt('0x'+hex.substr(2, 2)), b = Std.parseInt('0x'+hex.substr(4, 2)); 339 | return '\033[48;5;' + rgbToAscii256(cast r, cast g, cast b) + 'm'; 340 | } 341 | 342 | return switch (flag) { 343 | case RESET: '\033[m'; 344 | 345 | case BOLD: '\033[1m'; 346 | case DIM: '\033[2m'; 347 | case ITALIC: '\033[3m'; 348 | case UNDERLINE: '\033[4m'; 349 | case BLINK: '\033[5m'; 350 | case INVERT: '\033[7m'; 351 | case HIDDEN: '\033[8m'; 352 | 353 | case BLACK: '\033[38;5;' + ASCII_BLACK_CODE + 'm'; 354 | case RED: '\033[38;5;' + ASCII_RED_CODE + 'm'; 355 | case GREEN: '\033[38;5;' + ASCII_GREEN_CODE + 'm'; 356 | case YELLOW: '\033[38;5;' + ASCII_YELLOW_CODE + 'm'; 357 | case BLUE: '\033[38;5;' + ASCII_BLUE_CODE + 'm'; 358 | case MAGENTA: '\033[38;5;' + ASCII_MAGENTA_CODE + 'm'; 359 | case CYAN: '\033[38;5;' + ASCII_CYAN_CODE + 'm'; 360 | case WHITE: '\033[38;5;' + ASCII_WHITE_CODE + 'm'; 361 | case LIGHT_BLACK: '\033[38;5;' + ASCII_LIGHT_BLACK_CODE + 'm'; 362 | case LIGHT_RED: '\033[38;5;' + ASCII_LIGHT_RED_CODE + 'm'; 363 | case LIGHT_GREEN: '\033[38;5;' + ASCII_LIGHT_GREEN_CODE + 'm'; 364 | case LIGHT_YELLOW: '\033[38;5;' + ASCII_LIGHT_YELLOW_CODE + 'm'; 365 | case LIGHT_BLUE: '\033[38;5;' + ASCII_LIGHT_BLUE_CODE + 'm'; 366 | case LIGHT_MAGENTA: '\033[38;5;' + ASCII_LIGHT_MAGENTA_CODE + 'm'; 367 | case LIGHT_CYAN: '\033[38;5;' + ASCII_LIGHT_CYAN_CODE + 'm'; 368 | case LIGHT_WHITE: '\033[38;5;' + ASCII_LIGHT_WHITE_CODE + 'm'; 369 | 370 | case BG_BLACK: '\033[48;5;' + ASCII_BLACK_CODE + 'm'; 371 | case BG_RED: '\033[48;5;' + ASCII_RED_CODE + 'm'; 372 | case BG_GREEN: '\033[48;5;' + ASCII_GREEN_CODE + 'm'; 373 | case BG_YELLOW: '\033[48;5;' + ASCII_YELLOW_CODE + 'm'; 374 | case BG_BLUE: '\033[48;5;' + ASCII_BLUE_CODE + 'm'; 375 | case BG_MAGENTA: '\033[48;5;' + ASCII_MAGENTA_CODE + 'm'; 376 | case BG_CYAN: '\033[48;5;' + ASCII_CYAN_CODE + 'm'; 377 | case BG_WHITE: '\033[48;5;' + ASCII_WHITE_CODE + 'm'; 378 | case BG_LIGHT_BLACK: '\033[48;5;' + ASCII_LIGHT_BLACK_CODE + 'm'; 379 | case BG_LIGHT_RED: '\033[48;5;' + ASCII_LIGHT_RED_CODE + 'm'; 380 | case BG_LIGHT_GREEN: '\033[48;5;' + ASCII_LIGHT_GREEN_CODE + 'm'; 381 | case BG_LIGHT_YELLOW: '\033[48;5;' + ASCII_LIGHT_YELLOW_CODE + 'm'; 382 | case BG_LIGHT_BLUE: '\033[48;5;' + ASCII_LIGHT_BLUE_CODE + 'm'; 383 | case BG_LIGHT_MAGENTA: '\033[48;5;' + ASCII_LIGHT_MAGENTA_CODE + 'm'; 384 | case BG_LIGHT_CYAN: '\033[48;5;' + ASCII_LIGHT_CYAN_CODE + 'm'; 385 | case BG_LIGHT_WHITE: '\033[48;5;' + ASCII_LIGHT_WHITE_CODE + 'm'; 386 | // return empty string when ascii format flag is not known 387 | default: return ''; 388 | } 389 | } 390 | 391 | /* 392 | Find the best matching ascii color code for a given hex string 393 | - Ascii 256-color terminals support a subset of 24-bit colors 394 | - This includes 216 colors and 24 grayscale values 395 | - Assumes valid hex string 396 | */ 397 | static function rgbToAscii256(r:Int, g:Int, b:Int):Null { 398 | // Find the nearest value's index in the set 399 | // A metric like ciede2000 would be better, but this will do for now 400 | function nearIdx(c:Int, set:Array){ 401 | var delta = Math.POSITIVE_INFINITY; 402 | var index = -1; 403 | for (i in 0...set.length) { 404 | var d = Math.abs(c - set[i]); 405 | if (d < delta) { 406 | delta = d; 407 | index = i; 408 | } 409 | } 410 | return index; 411 | } 412 | 413 | inline function clamp(x:Int, min:Int, max:Int){ 414 | return Math.max(Math.min(x, max), min); 415 | } 416 | 417 | // Colors are index 16 to 231 inclusive = 216 colors 418 | // Steps are in spaces of 40 except for the first which is 95 419 | // (0x5f + 40 * (n - 1)) * (n > 0 ? 1 : 0) 420 | var colorSteps = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; 421 | var ir = nearIdx(r, colorSteps), ig = nearIdx(g, colorSteps), ib = nearIdx(b, colorSteps); 422 | var ier = Math.abs(r - colorSteps[ir]), ieg = Math.abs(g - colorSteps[ig]), ieb = Math.abs(b - colorSteps[ib]); 423 | var averageColorError = ier + ieg + ieb; 424 | 425 | // Gray scale values are 232 to 255 inclusive = 24 colors 426 | // Steps are in spaces of 10 427 | // 0x08 + 10 * n = c 428 | var jr = Math.round((r - 0x08) / 10), jg = Math.round((g - 0x08) / 10), jb = Math.round((b - 0x08) / 10); 429 | var jer = Math.abs(r - clamp((jr * 10 + 0x08), 0x08, 0xee)); 430 | var jeg = Math.abs(g - clamp((jg * 10 + 0x08), 0x08, 0xee)); 431 | var jeb = Math.abs(b - clamp((jb * 10 + 0x08), 0x08, 0xee)); 432 | var averageGrayError = jer + jeg + jeb; 433 | 434 | // If we hit an exact grayscale match then use that instead 435 | if (averageGrayError < averageColorError && r == g && g == b) { 436 | var grayIndex = jr + 232; 437 | return grayIndex; 438 | } else { 439 | var colorIndex = 16 + ir*36 + ig*6 + ib; 440 | return colorIndex; 441 | } 442 | } 443 | 444 | static function getBrowserFormat(flag:FormatFlag):Null { 445 | // custom hex color 446 | if ((flag:String).charAt(0) == '#') { 447 | return 'color: $flag'; 448 | } 449 | 450 | // custom hex background 451 | if ((flag:String).substr(0, 3) == 'bg#') { 452 | return 'background-color: ${(flag:String).substr(2)}'; 453 | } 454 | 455 | // inline CSS - browser consoles only 456 | if ((flag:String).charAt(0) == '{') { 457 | // return content as-is but remove enclosing braces 458 | return (flag:String).substr(1, (flag:String).length - 2); 459 | } 460 | 461 | return switch (flag) { 462 | case RESET: ''; 463 | 464 | case BOLD: 'font-weight: bold'; 465 | case ITALIC: 'font-style: italic'; 466 | case DIM: 'color: gray'; 467 | case UNDERLINE: 'text-decoration: underline'; 468 | case BLINK: 'text-decoration: blink'; // not supported 469 | case INVERT: '-webkit-filter: invert(100%); filter: invert(100%)'; // not supported 470 | case HIDDEN: 'visibility: hidden; color: white'; // not supported 471 | 472 | case BLACK: 'color: black'; 473 | case RED: 'color: red'; 474 | case GREEN: 'color: green'; 475 | case YELLOW: 'color: #f5ba00'; 476 | case BLUE: 'color: blue'; 477 | case MAGENTA: 'color: magenta'; 478 | case CYAN: 'color: cyan'; 479 | case WHITE: 'color: whiteSmoke'; 480 | 481 | case LIGHT_BLACK: 'color: gray'; 482 | case LIGHT_RED: 'color: salmon'; 483 | case LIGHT_GREEN: 'color: lightGreen'; 484 | case LIGHT_YELLOW: 'color: #ffed88'; 485 | case LIGHT_BLUE: 'color: lightBlue'; 486 | case LIGHT_MAGENTA: 'color: lightPink'; 487 | case LIGHT_CYAN: 'color: lightCyan'; 488 | case LIGHT_WHITE: 'color: white'; 489 | 490 | case BG_BLACK: 'background-color: black'; 491 | case BG_RED: 'background-color: red'; 492 | case BG_GREEN: 'background-color: green'; 493 | case BG_YELLOW: 'background-color: gold'; 494 | case BG_BLUE: 'background-color: blue'; 495 | case BG_MAGENTA: 'background-color: magenta'; 496 | case BG_CYAN: 'background-color: cyan'; 497 | case BG_WHITE: 'background-color: whiteSmoke'; 498 | case BG_LIGHT_BLACK: 'background-color: gray'; 499 | case BG_LIGHT_RED: 'background-color: salmon'; 500 | case BG_LIGHT_GREEN: 'background-color: lightGreen'; 501 | case BG_LIGHT_YELLOW: 'background-color: lightYellow'; 502 | case BG_LIGHT_BLUE: 'background-color: lightBlue'; 503 | case BG_LIGHT_MAGENTA: 'background-color: lightPink'; 504 | case BG_LIGHT_CYAN: 'background-color: lightCyan'; 505 | case BG_LIGHT_WHITE: 'background-color: white'; 506 | // return empty string for unknown format 507 | default: return ''; 508 | } 509 | } 510 | 511 | static function determineConsoleFormatMode():Console.ConsoleFormatMode { 512 | #if (!macro && !no_console) 513 | 514 | // browser console test 515 | #if js 516 | 517 | // if there's a window object, we're probably running in a browser 518 | var hasWindowObject = 519 | #if haxe4 520 | js.Syntax.typeof(js.Browser.window) != 'undefined'; 521 | #elseif haxe3 522 | untyped __js__('typeof window !== "undefined"'); 523 | #end 524 | 525 | if (hasWindowObject){ 526 | return BrowserConsole; 527 | } 528 | 529 | #end 530 | 531 | // native unix tput test 532 | #if (sys || nodejs) 533 | var tputColors = exec('tput colors'); 534 | if (tputColors.exit == 0) { 535 | var tputResult = Std.parseInt(tputColors.stdout); 536 | if (tputResult != null && tputResult > 2) { 537 | return AsciiTerminal; 538 | } 539 | } 540 | 541 | // try checking if we can enable colors in windows 542 | #if cpp 543 | var winconVTEnabled = false; 544 | 545 | untyped __cpp__(' 546 | #if defined(HX_WINDOWS) && defined(ENABLE_VIRTUAL_TERMINAL_PROCESSING) 547 | // Set output mode to handle virtual terminal sequences 548 | HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); 549 | DWORD dwMode = 0; 550 | if (hOut != INVALID_HANDLE_VALUE && GetConsoleMode(hOut, &dwMode)) 551 | { 552 | dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; 553 | {0} = SetConsoleMode(hOut, dwMode); 554 | } 555 | #endif 556 | ', winconVTEnabled); 557 | 558 | if (winconVTEnabled) { 559 | return AsciiTerminal; 560 | } 561 | 562 | #elseif neko 563 | if (Sys.systemName() == 'Windows') { 564 | // try enabling virtual terminal emulation via wincon.ndll 565 | var enableVTT:Void->Int = neko.Lib.load('wincon', 'enableVTT', 0); 566 | if (enableVTT() != 0) { 567 | // successfully enabled ascii escape codes in windows consoles 568 | return AsciiTerminal; 569 | } 570 | } 571 | #end // neko 572 | 573 | // detect specific TERM environments 574 | var termEnv = Sys.environment().get('TERM'); 575 | 576 | if (termEnv != null && ~/cygwin|xterm|vt100/.match(termEnv)) { 577 | return AsciiTerminal; 578 | } 579 | #end // (sys || nodejs) 580 | 581 | #end // (!macro && !no_console) 582 | 583 | return Disabled; 584 | } 585 | 586 | #if macro 587 | static function joinArgExprs(rest:Array):ExprOf { 588 | var msg:Expr = macro ''; 589 | for(i in 0...rest.length){ 590 | var e = rest[i]; 591 | msg = macro $msg + Std.string(cast $e); 592 | if (i != rest.length - 1){ 593 | msg = macro $msg + @:privateAccess Console.argSeparator; 594 | } 595 | } 596 | return msg; 597 | } 598 | #end 599 | 600 | #if (sys || nodejs) 601 | static function exec(cmd: String, ?args:Array) { 602 | #if (nodejs && !macro) 603 | //hxnodejs doesn't support sys.io.Process yet 604 | var p = js.node.ChildProcess.spawnSync(cmd, args != null ? args : [], {}); 605 | var stdout = (p.stdout:js.node.Buffer) == null ? '' : (p.stdout:js.node.Buffer).toString(); 606 | if (stdout == null) stdout = ''; 607 | return { 608 | exit: p.status, 609 | stdout: stdout 610 | } 611 | #else 612 | try { 613 | var p = new sys.io.Process(cmd, args); 614 | var exit = p.exitCode(); // assumed to block 615 | var stdout = p.stdout.readAll().toString(); 616 | p.close(); 617 | return { 618 | exit: exit, 619 | stdout: stdout, 620 | } 621 | } catch (e: Dynamic) { 622 | return { 623 | exit: 1, 624 | stdout: '' 625 | } 626 | } 627 | #end 628 | } 629 | #end 630 | 631 | } 632 | 633 | @:enum 634 | abstract ConsoleOutputStream(Int) { 635 | var Log = 0; 636 | var Warn = 1; 637 | var Error = 2; 638 | var Debug = 3; 639 | } 640 | 641 | @:enum 642 | abstract ConsoleFormatMode(Int) { 643 | #if (sys || nodejs) 644 | var AsciiTerminal = 0; 645 | #end 646 | #if js 647 | // Only enable browser console output on js targets 648 | var BrowserConsole = 1; 649 | #end 650 | var Disabled = 2; 651 | } 652 | 653 | @:enum 654 | abstract UnicodeCompatibilityMode(Int) { 655 | var None = 0; 656 | var Windows = 1; 657 | } 658 | 659 | @:enum 660 | abstract FormatFlag(String) to String { 661 | var RESET = 'reset'; 662 | var BOLD = 'bold'; 663 | var ITALIC = 'italic'; 664 | var DIM = 'dim'; 665 | var UNDERLINE = 'underline'; 666 | var BLINK = 'blink'; 667 | var INVERT = 'invert'; 668 | var HIDDEN = 'hidden'; 669 | var BLACK = 'black'; 670 | var RED = 'red'; 671 | var GREEN = 'green'; 672 | var YELLOW = 'yellow'; 673 | var BLUE = 'blue'; 674 | var MAGENTA = 'magenta'; 675 | var CYAN = 'cyan'; 676 | var WHITE = 'white'; 677 | var LIGHT_BLACK = 'light_black'; 678 | var LIGHT_RED = 'light_red'; 679 | var LIGHT_GREEN = 'light_green'; 680 | var LIGHT_YELLOW = 'light_yellow'; 681 | var LIGHT_BLUE = 'light_blue'; 682 | var LIGHT_MAGENTA = 'light_magenta'; 683 | var LIGHT_CYAN = 'light_cyan'; 684 | var LIGHT_WHITE = 'light_white'; 685 | var BG_BLACK = 'bg_black'; 686 | var BG_RED = 'bg_red'; 687 | var BG_GREEN = 'bg_green'; 688 | var BG_YELLOW = 'bg_yellow'; 689 | var BG_BLUE = 'bg_blue'; 690 | var BG_MAGENTA = 'bg_magenta'; 691 | var BG_CYAN = 'bg_cyan'; 692 | var BG_WHITE = 'bg_white'; 693 | var BG_LIGHT_BLACK = 'bg_light_black'; 694 | var BG_LIGHT_RED = 'bg_light_red'; 695 | var BG_LIGHT_GREEN = 'bg_light_green'; 696 | var BG_LIGHT_YELLOW = 'bg_light_yellow'; 697 | var BG_LIGHT_BLUE = 'bg_light_blue'; 698 | var BG_LIGHT_MAGENTA = 'bg_light_magenta'; 699 | var BG_LIGHT_CYAN = 'bg_light_cyan'; 700 | var BG_LIGHT_WHITE = 'bg_light_white'; 701 | 702 | @:from 703 | static public function fromString(str:String) { 704 | str = str.toLowerCase(); 705 | 706 | // normalize hex colors 707 | if (str.charAt(0) == '#' || str.substr(0, 3) == 'bg#') { 708 | var hIdx = str.indexOf('#'); 709 | var hex = str.substr(hIdx + 1); 710 | 711 | // expand shorthand hex 712 | if (hex.length == 3) { 713 | var a = hex.split(''); 714 | hex = [a[0], a[0], a[1], a[1], a[2], a[2]].join(''); 715 | } 716 | 717 | // validate hex 718 | if((~/[^0-9a-f]/i).match(hex) || hex.length < 6) { 719 | // hex contains a non-hexadecimal character or it's too short 720 | return untyped ''; // return empty flag, which has no formatting rules 721 | } 722 | 723 | var normalized = str.substring(0, hIdx) + '#' + hex; 724 | 725 | return untyped normalized; 726 | } 727 | 728 | // handle aliases 729 | return switch str { 730 | case '/': RESET; 731 | case '!': INVERT; 732 | case 'u': UNDERLINE; 733 | case 'b': BOLD; 734 | case 'i': ITALIC; 735 | case 'gray': LIGHT_BLACK; 736 | case 'bg_gray': BG_LIGHT_BLACK; 737 | default: cast str; 738 | } 739 | } 740 | } 741 | 742 | @:enum 743 | abstract AsciiColorCodes(Int){ 744 | var ASCII_BLACK_CODE = 0; 745 | var ASCII_RED_CODE = 1; 746 | var ASCII_GREEN_CODE = 2; 747 | var ASCII_YELLOW_CODE = 3; 748 | var ASCII_BLUE_CODE = 4; 749 | var ASCII_MAGENTA_CODE = 5; 750 | var ASCII_CYAN_CODE = 6; 751 | var ASCII_WHITE_CODE = 7; 752 | var ASCII_LIGHT_BLACK_CODE = 8; 753 | var ASCII_LIGHT_RED_CODE = 9; 754 | var ASCII_LIGHT_GREEN_CODE = 10; 755 | var ASCII_LIGHT_YELLOW_CODE = 11; 756 | var ASCII_LIGHT_BLUE_CODE = 12; 757 | var ASCII_LIGHT_MAGENTA_CODE = 13; 758 | var ASCII_LIGHT_CYAN_CODE = 14; 759 | var ASCII_LIGHT_WHITE_CODE = 15; 760 | } -------------------------------------------------------------------------------- /generate/src/GenerateOpentypeExterns.hx: -------------------------------------------------------------------------------- 1 | /* 2 | Generates OpenType.js externs in opentype/ 3 | 4 | Usage: haxe --run GenerateOpentypeExterns.hx 5 | */ 6 | 7 | import haxe.macro.Expr; 8 | 9 | using StringTools; 10 | 11 | class GenerateOpentypeExterns { 12 | 13 | static function main() { 14 | // see https://github.com/nodebox/opentype.js/blob/cbf824f27b7250f287a66ed06b13700e5dc59fe2/externs/opentype.js 15 | var jsExterns = sys.io.File.getContent('opentype-externs.js'); 16 | 17 | var modules = ClosureExternConverter.convert(jsExterns); 18 | 19 | function addTypeDef(name, type: ComplexType) { 20 | var opentypeModule = modules.get('opentype'); 21 | var fontOptionsDef: TypeDefinition = { 22 | pack: ['opentype'], 23 | name: name, 24 | pos: null, 25 | kind: TDAlias(type), 26 | fields: null 27 | } 28 | opentypeModule.set('opentype.$name', fontOptionsDef); 29 | } 30 | 31 | // manual fix-ups 32 | var opentypeDef = modules.get('opentype').get('opentype.Opentype'); 33 | var load = opentypeDef.fields.filter(f -> f.name == 'load')[0]; 34 | switch load.kind { 35 | case FFun({args: [_, cb = {name: callback}]}): 36 | cb.type = macro : String -> Font -> Void; 37 | default: 38 | } 39 | 40 | var fontDef = modules.get('opentype').get('opentype.Font'); 41 | fontDef.fields = (macro class X { 42 | var names: { 43 | fontFamily: {en: String}, 44 | fontSubfamily: {en: String}, 45 | fullName: {en: String}, 46 | postScriptName: {en: String}, 47 | designer: {en: String}, 48 | designerURL: {en: String}, 49 | manufacturer: {en: String}, 50 | manufacturerURL: {en: String}, 51 | license: {en: String}, 52 | licenseURL: {en: String}, 53 | version: {en: String}, 54 | description: {en: String}, 55 | copyright: {en: String}, 56 | trademark: {en: String}, 57 | }; 58 | var unitsPerEm: Int; 59 | var ascender: Float; 60 | var descender: Float; 61 | var createdTimestamp: Int; 62 | var tables:{ 63 | os2: { 64 | version: Int, 65 | xAvgCharWidth: Int, 66 | usWeightClass: Int, 67 | usWidthClass: Int, 68 | fsType: Int, 69 | ySubscriptXSize: Int, 70 | ySubscriptYSize: Int, 71 | ySubscriptXOffset: Int, 72 | ySubscriptYOffset: Int, 73 | ySuperscriptXSize: Int, 74 | ySuperscriptYSize: Int, 75 | ySuperscriptXOffset: Int, 76 | ySuperscriptYOffset: Int, 77 | yStrikeoutSize: Int, 78 | yStrikeoutPosition: Int, 79 | sFamilyClass: Int, 80 | bFamilyType: Int, 81 | bSerifStyle: Int, 82 | bWeight: Int, 83 | bProportion: Int, 84 | bContrast: Int, 85 | bStrokeVariation: Int, 86 | bArmStyle: Int, 87 | bLetterform: Int, 88 | bMidline: Int, 89 | bXHeight: Int, 90 | ulUnicodeRange1: Int, 91 | ulUnicodeRange2: Int, 92 | ulUnicodeRange3: Int, 93 | ulUnicodeRange4: Int, 94 | achVendID: String, 95 | fsSelection: Int, 96 | usFirstCharIndex: Int, 97 | usLastCharIndex: Int, 98 | sTypoAscender: Int, 99 | sTypoDescender: Int, 100 | sTypoLineGap: Int, 101 | usWinAscent: Int, 102 | usWinDescent: Int, 103 | ulCodePageRange1: Int, 104 | ulCodePageRange2: Int, 105 | sxHeight: Int, 106 | sCapHeight: Int, 107 | usDefaultChar: Int, 108 | usBreakChar: Int, 109 | usMaxContext: Int, 110 | } 111 | }; 112 | 113 | var supported: Bool; 114 | var glyphs: { 115 | function get(index: Int): Glyph; 116 | function push(index: Int, loader: Any): Void; 117 | var length: Int; 118 | }; 119 | 120 | // untyped fields 121 | var encoding: Any; 122 | var position: Any; 123 | var substitution: Substitution; 124 | var hinting: Any; 125 | }).fields.concat(fontDef.fields); 126 | 127 | // hardcoded types 128 | addTypeDef('FontOptions', macro : { 129 | empty: Bool, 130 | familyName: String, 131 | styleName: String, 132 | ?fullName: String, 133 | ?postScriptName: String, 134 | ?designer: String, 135 | ?designerURL: String, 136 | ?manufacturer: String, 137 | ?manufacturerURL: String, 138 | ?license: String, 139 | ?licenseURL: String, 140 | ?version: String, 141 | ?description: String, 142 | ?copyright: String, 143 | ?trademark: String, 144 | unitsPerEm: Float, 145 | ascender: Float, 146 | descender: Float, 147 | createdTimestamp: Float, 148 | ?weightClass: String, 149 | ?widthClass: String, 150 | ?fsSelection: String, 151 | }); 152 | addTypeDef('GlyphOptions', macro :{ 153 | name: String, 154 | unicode: Int, 155 | unicodes: Array, 156 | xMin: Float, 157 | yMin: Float, 158 | xMax: Float, 159 | yMax: Float, 160 | advanceWidth: Float, 161 | }); 162 | 163 | // save results to disk 164 | var hxPrinter = new haxe.macro.Printer(); 165 | 166 | for (modulePath in modules.keys()) { 167 | var module = modules.get(modulePath); 168 | 169 | sys.FileSystem.createDirectory(modulePath.split('.').join('/')); 170 | 171 | for (classPath in module.keys()) { 172 | 173 | var filePath = classPath.split('.').join('/') + '.hx'; 174 | var classDef = module.get(classPath); 175 | 176 | sys.io.File.saveContent(filePath, hxPrinter.printTypeDefinition(classDef, true)); 177 | trace('Saved "$filePath"'); 178 | } 179 | 180 | } 181 | 182 | } 183 | 184 | } 185 | 186 | 187 | /* 188 | Incomplete parser for closure compiler externs 189 | https://github.com/google/closure-compiler/wiki 190 | */ 191 | class ClosureExternConverter { 192 | 193 | static public function convert(externContent: String) { 194 | // module = map of classes 195 | var modules = new Map>(); 196 | 197 | function getModule(path: Array) { 198 | var pathStr = path.join('.'); 199 | var module = modules.get(pathStr); 200 | if (module == null) { 201 | throw 'Module "${pathStr}" has not been defined'; 202 | } 203 | return module; 204 | } 205 | 206 | // everything between /** */ 207 | var e = EReg.escape; 208 | var docPattern = new EReg('${ e('/**') }((.|\n)+?)(?=${ e('*/') })${ e('*/\n') }', 'm'); 209 | 210 | var modulePattern = ~/^\s*(var|let|const)\s+(\w+)/; 211 | var moduleFieldPattern = ~/^\s*([\w.]*)\b([a-z_]\w+)(\s*=\s*([^\n]*))?/; 212 | var classPattern = ~/^\s*([\w.]*)\b([A-Z_]\w+)\s*=\s*([^\n]*)/; 213 | var classFieldPattern = ~/^\s*([\w.]*)\b([A-Z_]\w+)\.prototype\.(\w+)(\s*=\s*([^\n]*))?/; 214 | 215 | var constructorMetaPattern = ~/^@constructor\b/m; 216 | var extendsMetaPattern = ~/^@extends\s+{?([^}\n]*)}?/m; 217 | 218 | var str = externContent; 219 | while (docPattern.match(str)) { 220 | var doc = cleanDoc(docPattern.matched(1)); 221 | 222 | str = docPattern.matchedRight(); 223 | var nextLineEnd = str.indexOf('\n'); 224 | var nextLine = str.substring(0, nextLineEnd); 225 | // skip line 226 | str = str.substr(nextLineEnd); 227 | 228 | if (modulePattern.match(nextLine)) { 229 | 230 | var modulePath = parseModulePath(modulePattern.matched(2)); 231 | var moduleName = modulePath[modulePath.length - 1]; 232 | // trace('Found module', modulePattern.matched(2), modulePath.join('.')); 233 | 234 | modules.set(modulePath.join('.'), new Map()); 235 | 236 | // create a module class for any static methods 237 | var className = toClassName(moduleName); 238 | var classPath = modulePath.concat([className]).join('.'); 239 | var classDef = macro class $className {}; 240 | classDef.meta = [{name: ':jsRequire', params: [{expr: EConst(CString(moduleName)), pos: null}], pos: null}]; 241 | classDef.isExtern = true; 242 | classDef.doc = doc; 243 | classDef.pack = modulePath; 244 | 245 | getModule(modulePath).set(classPath, classDef); 246 | 247 | } else if (classPattern.match(nextLine)) { 248 | 249 | var modulePath = parseModulePath(classPattern.matched(1)); 250 | var className = classPattern.matched(2); 251 | var expression = classPattern.matched(3); 252 | 253 | var classDef = macro class $className {}; 254 | 255 | if (constructorMetaPattern.match(doc)) { 256 | var ctorDef = parseFunction(expression, doc); 257 | ctorDef.name = 'new'; 258 | classDef.fields.push(ctorDef); 259 | } 260 | 261 | if (extendsMetaPattern.match(doc)) { 262 | var superPath = extendsMetaPattern.matched(1); 263 | var parts = superPath.split('.'); 264 | var superClass: TypePath = { 265 | pack: parts.slice(0, parts.length - 1), 266 | name: parts[parts.length - 1] 267 | } 268 | classDef.kind = TDClass(superClass); 269 | } 270 | 271 | classDef.isExtern = true; 272 | classDef.doc = doc; 273 | classDef.pack = modulePath; 274 | 275 | var classPath = modulePath.concat([className]).join('.'); 276 | 277 | getModule(modulePath).set(classPath, classDef); 278 | 279 | } else if (classFieldPattern.match(nextLine)) { 280 | var modulePath = parseModulePath(classFieldPattern.matched(1)); 281 | var className = classFieldPattern.matched(2); 282 | var fieldName = classFieldPattern.matched(3); 283 | var expression = classFieldPattern.matched(5); 284 | 285 | var classPath = modulePath.concat([className]).join('.'); 286 | 287 | // trace('field', classPath, fieldName); 288 | 289 | var classDef = getModule(modulePath).get(classPath); 290 | if (classDef == null) throw 'Class $classPath not defined'; 291 | 292 | // parse field 293 | var fieldDef = parseField(fieldName, expression, doc); 294 | 295 | classDef.fields.push(fieldDef); 296 | 297 | } else if (moduleFieldPattern.match(nextLine)) { 298 | var modulePath = parseModulePath(moduleFieldPattern.matched(1)); 299 | var className = toClassName(modulePath[modulePath.length - 1]); 300 | var classPath = modulePath.concat([className]).join('.'); 301 | var fieldName = moduleFieldPattern.matched(2); 302 | var expression = moduleFieldPattern.matched(3); 303 | 304 | // get module class 305 | var classDef = getModule(modulePath).get(classPath); 306 | 307 | var fieldDef = parseField(fieldName, expression, doc); 308 | fieldDef.access = [AStatic, APublic]; 309 | classDef.fields.push(fieldDef); 310 | 311 | } else if (StringTools.trim(nextLine) != '') { 312 | 313 | trace('Unknown line format "$nextLine"'); 314 | 315 | } 316 | } 317 | 318 | return modules; 319 | } 320 | 321 | static function parseType(typeStr: String): ComplexType { 322 | typeStr = StringTools.trim(typeStr); 323 | var builtIn = switch typeStr.toLowerCase() { 324 | case 'string': macro :String; 325 | case 'number': macro :Float; 326 | case 'boolean': macro :Bool; 327 | case 'array': macro :Array; 328 | case 'object': macro :haxe.DynamicAccess; 329 | case 'function': macro :Any; 330 | case 'type': macro :Any; 331 | 332 | // convert js type names into haxe type names 333 | // this list could be fully completed by iterating all items in js and finding their @:native metadata 334 | case 'canvasrenderingcontext2d': macro :js.html.CanvasRenderingContext2D; 335 | case 'arraybuffer': macro :js.html.ArrayBuffer; 336 | case 'svgpathelement': macro :js.html.svg.PathElement; 337 | 338 | default: null; 339 | } 340 | 341 | if (builtIn != null) return builtIn; 342 | 343 | var arrayPattern = ~/^(\[(.*)\]|(.*)\[\]|Array<(.*)>)$/; 344 | if (arrayPattern.match(typeStr)) { 345 | var innerTypeStr = 346 | arrayPattern.matched(2) != null ? arrayPattern.matched(2) : 347 | (arrayPattern.matched(3) != null ? arrayPattern.matched(3) : arrayPattern.matched(4)); 348 | var innerType = parseType(innerTypeStr); 349 | return macro :Array<$innerType>; 350 | } 351 | 352 | if (!~/^[\w.]+$/.match(typeStr)) { 353 | throw 'Unhandled type syntax: "$typeStr"'; 354 | } 355 | 356 | return TPath({pack: [], name: typeStr}); 357 | } 358 | 359 | static function parseFunction(functionDeclExpression: String, doc: String): Field { 360 | var functionDecl = ~/function\s*(\w+)?\s*\(([^)]*)\)/m; 361 | var paramMetaPattern = ~/^@param\s+{([^}]*)}(\s+\[?(\w+))?/mg; 362 | var returnMetaPattern = ~/^@return\s+{([^}]*)}/m; 363 | 364 | if (!functionDecl.match(functionDeclExpression)) { 365 | throw 'Unhandled function declaration'; 366 | } 367 | 368 | var funcName = functionDecl.matched(1); 369 | var argNames = functionDecl.matched(2) 370 | .split(',').map(s -> StringTools.trim(s)) 371 | .filter(s -> s != ''); 372 | var argTypes = new Map(); 373 | var returnType = macro :Void; 374 | 375 | // match function hints 376 | paramMetaPattern.map(doc, s -> { 377 | var typeStr = paramMetaPattern.matched(1).trim(); 378 | 379 | var optional = false; 380 | if (typeStr.charAt(typeStr.length - 1) == '=') { 381 | typeStr = typeStr.substr(0, typeStr.length - 1); 382 | optional = true; 383 | } 384 | 385 | var type = parseType(typeStr); 386 | var name = paramMetaPattern.matched(3); 387 | if (name == null) name = argNames[0]; 388 | 389 | argTypes.set(name, {t: type, opt: optional}); 390 | 391 | return s.matched(0); 392 | }); 393 | 394 | if (returnMetaPattern.match(doc)) { 395 | returnType = parseType(returnMetaPattern.matched(1)); 396 | } 397 | 398 | return { 399 | name: funcName, 400 | kind: FFun({ 401 | args: argNames.map(name -> { 402 | name: name, 403 | type: argTypes.exists(name) ? argTypes.get(name).t : macro :Any, 404 | meta: null, 405 | opt: argTypes.exists(name) ? argTypes.get(name).opt : false, 406 | value: null, 407 | }), 408 | expr: null, 409 | ret: returnType, 410 | }), 411 | pos: null 412 | } 413 | } 414 | 415 | static function parseField(fieldName: String, expression: String, doc: String) { 416 | var typeMetaPattern = ~/^@type\s+{([^}]*)}/m; 417 | 418 | // default to var $fieldName:Any 419 | var fieldDef = (macro class X { 420 | var $fieldName: Any; 421 | }).fields[0]; 422 | 423 | if (typeMetaPattern.match(doc)) { 424 | var type = parseType(typeMetaPattern.matched(1)); 425 | fieldDef = (macro class X { 426 | var $fieldName: $type; 427 | }).fields[0]; 428 | } else { 429 | fieldDef = parseFunction(expression, doc); 430 | fieldDef.name = fieldName; 431 | } 432 | 433 | fieldDef.doc = doc; 434 | 435 | return fieldDef; 436 | } 437 | 438 | static function parseModulePath(str: String) { 439 | return str.split('.').filter(s -> s != ''); 440 | } 441 | 442 | static function toClassName(str: String) { 443 | return str.charAt(0).toUpperCase() + str.substr(1); 444 | } 445 | 446 | static function cleanDoc(doc: String) { 447 | return doc.split('\n') 448 | .map(l -> StringTools.trim(l)) 449 | .map(l -> l.charAt(0) == '*' ? StringTools.trim(l.substr(1)) : l) 450 | .join('\n'); 451 | } 452 | 453 | } -------------------------------------------------------------------------------- /generate/src/Main.hx: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | # GPUText Font-atlas Generator 4 | 5 | Definitions 6 | character 7 | An entry in the character set, may not have an associated glyph (e.g. tab character) 8 | glyph 9 | A printable shape associated with a character 10 | 11 | Units 12 | su 13 | shape units, specific to msdfgen 14 | FUnits 15 | values directly stored in the truetype file 16 | fontHeight 17 | |font.ascender| + |font.descender| 18 | normalized units 19 | FUnits / fontHeight 20 | Normalized so that a value of 1 corresponds to the font's ascender and 0 the the descender 21 | 22 | References 23 | https://docs.microsoft.com/en-gb/typography/opentype/spec/ttch01 24 | http://chanae.walon.org/pub/ttf/ttf_glyphs.htm 25 | 26 | **/ 27 | 28 | using StringTools; 29 | using Lambda; 30 | 31 | @:enum abstract GPUTextFormat(String) { 32 | var TEXTURE_ATLAS_FONT_JSON = 'TextureAtlasFontJson'; 33 | var TEXTURE_ATLAS_FONT_BINARY = 'TextureAtlasFontBinary'; 34 | } 35 | 36 | @:enum abstract TextureFontTechnique(String) from String { 37 | var MSDF = 'msdf'; 38 | var SDF = 'sdf'; 39 | var BITMAP = 'bitmap'; 40 | } 41 | 42 | typedef ResourceReference = { 43 | // @! add format, i.e., png, or byte field descriptor 44 | 45 | // range of bytes within the file's binary payload 46 | ?payloadBytes: { 47 | start: Int, 48 | length: Int, 49 | }, 50 | 51 | // path relative to font file 52 | // an implementation should not allow resource paths to be in directories _above_ the font file 53 | ?localPath: String, 54 | } 55 | 56 | typedef TextureAtlasGlyph = { 57 | // location of glyph within the text atlas, in units of pixels 58 | atlasRect: {x: Int, y: Int, w: Int, h: Int}, 59 | atlasScale: Float, // (normalized font units) * atlasScale = (pixels in texture atlas) 60 | 61 | // the offset within the atlasRect in normalized font units 62 | offset: {x: Float, y: Float}, 63 | } 64 | 65 | typedef TextureAtlasCharacter = { 66 | // the distance from the glyph's x = 0 coordinate to the x = 0 coordinate of the next glyph, in normalized font units 67 | advance: Float, 68 | ?glyph: TextureAtlasGlyph, 69 | } 70 | 71 | typedef FontMetadata = { 72 | family: String, 73 | subfamily: String, 74 | version: String, 75 | postScriptName: String, 76 | 77 | copyright: String, 78 | trademark: String, 79 | manufacturer: String, 80 | manufacturerURL: String, 81 | designerURL: String, 82 | license: String, 83 | licenseURL: String, 84 | 85 | // original authoring height 86 | // this can be used to reproduce the unnormalized source values of the font 87 | height_funits: Float, 88 | funitsPerEm: Float, 89 | } 90 | 91 | typedef TextureAtlasFont = { 92 | format: GPUTextFormat, 93 | version: Int, 94 | 95 | technique: TextureFontTechnique, 96 | 97 | characters: haxe.DynamicAccess, 98 | kerning: haxe.DynamicAccess, 99 | 100 | textures: Array< 101 | // array of mipmap levels, where 0 = largest and primary texture (mipmaps may be omitted) 102 | Array 103 | >, 104 | 105 | textureSize: { 106 | w: Int, 107 | h: Int, 108 | }, 109 | 110 | // normalized font units 111 | ascender: Float, 112 | descender: Float, 113 | typoAscender: Float, 114 | typoDescender: Float, 115 | lowercaseHeight: Float, 116 | 117 | metadata: FontMetadata, 118 | 119 | fieldRange_px: Float, 120 | 121 | // glyph bounding boxes in normalized font units 122 | // not guaranteed to be included in the font file 123 | ?glyphBounds: haxe.DynamicAccess<{l: Float, b: Float, r: Float, t: Float}> 124 | } 125 | 126 | typedef TextureAtlasFontBinaryHeader = { 127 | format: GPUTextFormat, 128 | version: Int, 129 | 130 | technique: TextureFontTechnique, 131 | 132 | ascender: Float, 133 | descender: Float, 134 | typoAscender: Float, 135 | typoDescender: Float, 136 | lowercaseHeight: Float, 137 | 138 | metadata: FontMetadata, 139 | 140 | fieldRange_px: Float, 141 | 142 | charList: Array, 143 | kerningPairs: Array, 144 | 145 | // payload data 146 | textures: Array< 147 | // array of mipmap levels, where 0 = largest and primary texture (mipmaps may be omitted) 148 | Array 149 | >, 150 | 151 | textureSize: { 152 | w: Int, 153 | h: Int, 154 | }, 155 | 156 | characters: { 157 | start: Int, 158 | length: Int, 159 | }, 160 | kerning: { 161 | start: Int, 162 | length: Int, 163 | }, 164 | ?glyphBounds: { 165 | start: Int, 166 | length: Int, 167 | }, 168 | } 169 | 170 | enum DataType { 171 | Float; 172 | Int; 173 | UInt; 174 | } 175 | 176 | typedef BinaryDataField = {key: String, type: DataType, length_bytes: Int}; 177 | 178 | class Main { 179 | 180 | static var textureAtlasFontVersion = 1; 181 | 182 | static var technique:TextureFontTechnique = MSDF; 183 | 184 | static var msdfgenPath = 'prebuilt/msdfgen'; // search at runtime 185 | static var charsetPath: Null = null; 186 | static var localTmpDir = '__glyph-cache'; 187 | static var fontOutputDirectory = ''; 188 | 189 | static var sourceTtfPaths = new Array(); 190 | static var charList = null; 191 | static var size_px: Int = 32; 192 | static var fieldRange_px: Int = 2; 193 | static var maximumTextureSize = 4096; 194 | static var storeBounds = false; 195 | static var saveBinary = true; 196 | static var externalTextures = false; 197 | static var preserveTmp = false; 198 | 199 | static var whitespaceCharacters = [ 200 | ' ', '\t' 201 | ]; 202 | 203 | // not sure why msdfgen divides truetype's FUnits by 64 but it does 204 | static inline var suToFUnits = 64.0; 205 | 206 | static function main() { 207 | Console.errorPrefix = '> '; 208 | Console.warnPrefix = '> '; 209 | 210 | // search for msdfgen binary, relative to the program 211 | var msdfBinaryName = Sys.systemName() == 'Windows' ? 'msdfgen.exe' : 'msdfgen'; 212 | var msdfSearchDirectories = ['.', 'msdfgen', 'prebuilt']; 213 | var programDirectory = haxe.io.Path.directory(Sys.programPath()); 214 | for (dir in msdfSearchDirectories) { 215 | var path = haxe.io.Path.join([programDirectory, dir, msdfBinaryName]); 216 | if (sys.FileSystem.exists(path) && !sys.FileSystem.isDirectory(path)) { 217 | msdfgenPath = path; 218 | break; 219 | } 220 | } 221 | 222 | var showHelp = false; 223 | var argHandler = hxargs.Args.generate([ 224 | @doc('Path of TrueType font file (.ttf)') 225 | _ => (path: String) -> { 226 | // catch any common aliases for help 227 | if (path.charAt(0) == '-') { 228 | if (['-help', '-h', '-?'].indexOf(path) != -1) { 229 | showHelp = true; 230 | } else { 231 | throw 'Unrecognized argument "$path"'; 232 | } 233 | } 234 | // assume it's a ttf path 235 | sourceTtfPaths.push(path); 236 | }, 237 | 238 | @doc('Path of file containing character set') 239 | ['--charset'] => (path: String) -> charsetPath = path, 240 | 241 | @doc('List of characters') 242 | ['--charlist'] => (characters: String) -> charList = characters.split(''), 243 | 244 | @doc('Sets the path of the output font file. External resources will be saved in the same directory') 245 | ['--output-dir', '-o'] => (path: String) -> fontOutputDirectory = path, 246 | 247 | @doc('Font rendering technique, one of: msdf, sdf, bitmap') 248 | ['--technique'] => (name: String) -> technique = name, 249 | 250 | // texture atlas mode options 251 | @doc('Path of msdfgen executable') 252 | ['--msdfgen'] => (path: String) -> msdfgenPath = path, 253 | 254 | @doc('Maximum dimension of a glyph in pixels') 255 | ['--size'] => (glyphSize: Int) -> size_px = glyphSize, 256 | 257 | @doc('Specifies the width of the range around the shape between the minimum and maximum representable signed distance in pixels') 258 | ['--pxrange'] => (range: Int) -> fieldRange_px = range, 259 | 260 | @doc('Sets the maximum dimension of the texture atlas') 261 | ['--max-texture-size'] => (size: Int) -> maximumTextureSize = size, 262 | 263 | @doc('Enables storing glyph bounding boxes in the font (default false)') 264 | ['--bounds'] => (enabled: Bool) -> storeBounds = enabled, 265 | 266 | @doc('Saves the font in the binary format (default true)') 267 | ['--binary'] => (enabled: Bool) -> saveBinary = enabled, 268 | 269 | @doc('Store textures externally when saving in the binary format') 270 | ['--external-textures'] => (enabled: Bool) -> externalTextures = enabled, 271 | 272 | @doc('Preserves any temporary files generated (default false)') 273 | ['--preserve-tmp'] => () -> preserveTmp = true, 274 | 275 | // misc 276 | @doc('Shows this help') 277 | ['--help'] => () -> { 278 | showHelp = true; 279 | }, 280 | ]); 281 | 282 | function printUsage() { 283 | Console.printlnFormatted('Usage: [options]\n'); 284 | Console.print(argHandler.getDoc()); 285 | Console.println(''); 286 | Console.println(''); 287 | } 288 | 289 | try { 290 | argHandler.parse(Sys.args()); 291 | 292 | if (showHelp) { 293 | printUsage(); 294 | Sys.exit(0); 295 | return; 296 | } 297 | 298 | // validate args 299 | if (!sys.FileSystem.exists(msdfgenPath)) { 300 | throw 'msdfgen executable was not found at "$msdfgenPath" – ensure it is built'; 301 | } 302 | 303 | if (sourceTtfPaths.length == 0) { 304 | throw 'Path of source TrueType font file is required'; 305 | } 306 | 307 | for (ttfPath in sourceTtfPaths) { 308 | if (!sys.FileSystem.exists(ttfPath)) { 309 | throw 'Font file "$ttfPath" does not exist'; 310 | } 311 | } 312 | 313 | if (charList == null) { 314 | charList = if (charsetPath == null) { 315 | Console.warn("No charset supplied, using ASCII charset by default, add --charset {path-to-txt-file} to specify which characters the generated font should"); 316 | ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.split(''); 317 | } else sys.io.File.getContent(charsetPath).split(''); 318 | } 319 | 320 | switch technique { 321 | case MSDF: 322 | default: throw 'Font technique "$technique" is not implemented'; 323 | } 324 | 325 | } catch (e: Any) { 326 | Console.error(e); 327 | Console.println(''); 328 | printUsage(); 329 | Sys.exit(1); 330 | return; 331 | } 332 | 333 | for (ttfPath in sourceTtfPaths) { 334 | sys.FileSystem.createDirectory(localTmpDir); 335 | 336 | var font = opentype.Opentype.loadSync(ttfPath); 337 | // notes on metrics 338 | // https://glyphsapp.com/tutorials/vertical-metrics 339 | // https://silnrsi.github.io/FDBP/en-US/Line_Metrics.html 340 | 341 | // font.ascender = font.table.hhea.ascender 342 | // font.descender = font.table.hhea.descender 343 | 344 | var fontHeight = font.ascender - font.descender; 345 | var fontFileName = haxe.io.Path.withoutDirectory(haxe.io.Path.withoutExtension(ttfPath)); 346 | 347 | // filter all characters without glyphs into a separate list 348 | var glyphList = charList.filter(c -> { 349 | var g = font.charToGlyph(c); 350 | return font.hasChar(c) && untyped g.xMin != null && untyped g.name != '.notdef'; 351 | }); 352 | 353 | // liga = ligatures which you want to be on by default, but which can be deactivated by the user. 354 | // dlig = ligatures which you want to be off by default, but which can be activated by the user. 355 | // rlig = ligatures which you want to be on and which cannot be turned off. Or at least that's the theory. Unfortunately, this feature is not implemented 356 | // trace(font.substitution.getLigatures('liga', null, null)); 357 | // trace(font.substitution.getLigatures('dlig', null, null)); 358 | // trace(font.substitution.getLigatures('rlig', null, null)); 359 | 360 | function normalizeFUnits(fUnit: Float) return fUnit / fontHeight; 361 | 362 | Console.log('Generating glyphs for "$ttfPath"'); 363 | 364 | function imagePath(charCode:Int) return '$localTmpDir/$charCode-$size_px.bmp'; 365 | function metricsPath(charCode:Int) return '$localTmpDir/$charCode-$size_px-metrics.txt'; 366 | 367 | for (char in charList) { 368 | var charCode = char.charCodeAt(0); 369 | var e = Sys.command( 370 | '$msdfgenPath msdf -autoframe -font $ttfPath $charCode -size $size_px $size_px -printmetrics -pxrange $fieldRange_px -o "${imagePath(charCode)}"> "${metricsPath(charCode)}"' 371 | ); 372 | if (e != 0) { 373 | Console.error('$msdfgenPath exited with code $e'); 374 | Sys.exit(e); 375 | return; 376 | } 377 | } 378 | 379 | Console.log('Reading glyph metrics'); 380 | 381 | inline function norm(x: Float) return normalizeFUnits((x * suToFUnits)); 382 | 383 | var atlasCharacters = new haxe.DynamicAccess(); 384 | // initialize each character 385 | for (char in charList) { 386 | atlasCharacters.set(char, { 387 | advance: 1, 388 | glyph: { 389 | atlasScale: suToFUnits, 390 | atlasRect: null, 391 | offset: {x: 0, y: 0}, 392 | } 393 | }); 394 | } 395 | 396 | // parse the generated metric files and copy values into the atlas character map 397 | var glyphBounds = new haxe.DynamicAccess<{l: Float, b: Float, r: Float, t: Float}>(); 398 | for (char in charList) { 399 | var charCode = char.charCodeAt(0); 400 | var metricsFileContent = sys.io.File.getContent(metricsPath(charCode)); 401 | 402 | // parse metrics 403 | var atlasCharacter = atlasCharacters.get(char); 404 | 405 | var varPattern = ~/^\s*(\w+)\s*=([^\n]+)/; // name = a, b 406 | var str = metricsFileContent; 407 | while (varPattern.match(str)) { 408 | var name = varPattern.matched(1); 409 | var value = varPattern.matched(2).split(',').map(f -> Std.parseFloat(f)); 410 | 411 | // multiply all values by a conversion factor from msdfgen to recover font's original values in FUnits 412 | // divide all values by the number of FUnits that make up one side of the 'em' square to get normalized values 413 | 414 | switch name { 415 | case 'advance': 416 | atlasCharacter.advance = norm(value[0]); 417 | case 'scale': 418 | atlasCharacter.glyph.atlasScale = 1/norm(1/value[0]); 419 | case 'bounds': 420 | glyphBounds.set(char, {l: norm(value[0]), b: norm(value[1]), r: norm(value[2]), t: norm(value[3])}); 421 | case 'translate': 422 | atlasCharacter.glyph.offset = {x: norm(value[0]), y: norm(value[1])}; 423 | // case 'range': 424 | // atlasCharacter.glyph.fieldRange = norm(value[0]); 425 | } 426 | 427 | str = varPattern.matchedRight(); 428 | } 429 | } 430 | 431 | Console.log('Packing glyphs into texture'); 432 | 433 | // find nearest power-of-two texture atlas dimensions that encompasses all glyphs 434 | var blocks = [for (_ in glyphList) {w: size_px, h: size_px}]; 435 | var atlasW = ceilPot(size_px); 436 | var atlasH = ceilPot(size_px); 437 | var mode: Int = -1; 438 | var fitSucceeded = false; 439 | while (atlasW <= maximumTextureSize && atlasH <= maximumTextureSize) { 440 | var nodes = BinPacker.fit(cast blocks, atlasW, atlasH); 441 | if (nodes.indexOf(null) != -1) { 442 | // not all blocks could fit, double one of the dimensions 443 | if (mode == -1) atlasW = atlasW * 2; 444 | else atlasH = atlasH * 2; 445 | mode *= -1; 446 | } else { 447 | // copy results into the atlas characters 448 | for (i in 0...glyphList.length) { 449 | var char = glyphList[i]; 450 | var block = blocks[i]; 451 | var node = nodes[i]; 452 | atlasCharacters.get(char).glyph.atlasRect = { 453 | x: Math.floor(node.x), y: Math.floor(node.y), 454 | w: block.w, h: block.h, 455 | } 456 | } 457 | fitSucceeded = true; 458 | break; 459 | } 460 | } 461 | 462 | if (!fitSucceeded) { 463 | Console.error('Could not fit glyphs into ${maximumTextureSize}x${maximumTextureSize} texture - try a smaller character set or reduced glyph size (multi-atlas is not implemented)'); 464 | Sys.exit(1); 465 | } 466 | 467 | // delete .glyph fields from characters that have no glyph (like whitespace) 468 | for (char in charList) { 469 | var hasGlyph = glyphList.indexOf(char) != -1; 470 | if (!hasGlyph) { 471 | Reflect.deleteField(atlasCharacters.get(char), 'glyph'); 472 | } 473 | } 474 | 475 | // blit all glyphs into a png with no padding 476 | var channels = 3; 477 | var bytesPerChannel = 1; 478 | var mapRgbBytes = haxe.io.Bytes.ofData(new haxe.io.BytesData(channels * bytesPerChannel * atlasW * atlasH)); 479 | 480 | for (char in glyphList) { 481 | var charCode = char.charCodeAt(0); 482 | var input = sys.io.File.read(imagePath(charCode), true); 483 | var bmpData = new format.bmp.Reader(input).read(); 484 | var glyphHeader = bmpData.header; 485 | var glyphBGRA = format.bmp.Tools.extractBGRA(bmpData);//format.png.Tools.extract32(pngData, null, false); 486 | 487 | var rect = atlasCharacters.get(char).glyph.atlasRect; 488 | 489 | inline function getIndex(x: Int, y: Int, channels: Int, width: Int) { 490 | return (y * width + x) * channels; 491 | } 492 | 493 | for (x in 0...glyphHeader.width) { 494 | for (y in 0...glyphHeader.height) { 495 | var i = getIndex(x, y, 4, glyphHeader.width); 496 | var b = glyphBGRA.get(i + 0); 497 | var g = glyphBGRA.get(i + 1); 498 | var r = glyphBGRA.get(i + 2); 499 | //var a = glyphBGRA.get(i + 3); 500 | 501 | // blit to map 502 | var mx = x + Std.int(rect.x); 503 | var my = y + Std.int(rect.y); 504 | var mi = getIndex(mx, my, 3, atlasW); 505 | mapRgbBytes.set(mi + 0, b); 506 | mapRgbBytes.set(mi + 1, g); 507 | mapRgbBytes.set(mi + 2, r); 508 | } 509 | } 510 | } 511 | 512 | if (!preserveTmp) { 513 | Console.log('Deleting glyph cache'); 514 | var tmpFiles = sys.FileSystem.readDirectory(localTmpDir); 515 | for (name in tmpFiles) { 516 | try sys.FileSystem.deleteFile(haxe.io.Path.join([localTmpDir, name])) catch(e:Any) {} 517 | } 518 | sys.FileSystem.deleteDirectory(localTmpDir); 519 | } 520 | 521 | // build png atlas bytes 522 | var textureFileName = '$fontFileName-0.png'; 523 | var textureFilePath = haxe.io.Path.join([fontOutputDirectory, textureFileName]); 524 | 525 | var pngData = format.png.Tools.buildRGB(atlasW, atlasH, mapRgbBytes, 9); 526 | var pngOutput = new haxe.io.BytesOutput(); 527 | new format.png.Writer(pngOutput).write(pngData); 528 | var pngBytes = pngOutput.getBytes(); 529 | 530 | // create font descriptor 531 | // generate kerning map 532 | var kerningMap = new haxe.DynamicAccess(); 533 | for (first in charList) { 534 | for (second in charList) { 535 | var kerningAmount_fu = font.getKerningValue(font.charToGlyph(first), font.charToGlyph(second)); 536 | if (kerningAmount_fu != null && kerningAmount_fu != 0) { 537 | kerningMap.set(first + second, normalizeFUnits(kerningAmount_fu)); 538 | } 539 | } 540 | } 541 | 542 | function processFontNameField(field: Null<{?en: String}>): String { 543 | if (field == null) return null; 544 | if (field.en == null) return null; 545 | return field.en.trim(); 546 | } 547 | 548 | var jsonFont: TextureAtlasFont = { 549 | format: TEXTURE_ATLAS_FONT_JSON, 550 | version: textureAtlasFontVersion, 551 | technique: MSDF, 552 | characters: atlasCharacters, 553 | kerning: kerningMap, 554 | textures: [ 555 | [{localPath: textureFileName}] 556 | ], 557 | textureSize: { 558 | w: atlasW, 559 | h: atlasH 560 | }, 561 | ascender: font.ascender / fontHeight, 562 | descender: font.descender / fontHeight, 563 | typoAscender: font.tables.os2.sTypoAscender / fontHeight, 564 | typoDescender: font.tables.os2.sTypoDescender / fontHeight, 565 | lowercaseHeight: font.tables.os2.sxHeight / fontHeight, 566 | metadata: { 567 | family: processFontNameField(font.names.fontFamily), 568 | subfamily: processFontNameField(font.names.fontSubfamily), 569 | version: processFontNameField(font.names.version), 570 | postScriptName: processFontNameField(font.names.postScriptName), 571 | 572 | copyright: processFontNameField(font.names.copyright), 573 | trademark: processFontNameField(font.names.trademark), 574 | manufacturer: processFontNameField(font.names.manufacturer), 575 | manufacturerURL: processFontNameField(font.names.manufacturerURL), 576 | designerURL: processFontNameField(font.names.designerURL), 577 | license: processFontNameField(font.names.license), 578 | licenseURL: processFontNameField(font.names.licenseURL), 579 | 580 | height_funits: fontHeight, 581 | funitsPerEm: font.unitsPerEm 582 | }, 583 | glyphBounds: storeBounds ? glyphBounds : null, 584 | fieldRange_px: fieldRange_px, 585 | } 586 | 587 | // Output file writing 588 | 589 | if (fontOutputDirectory != '') sys.FileSystem.createDirectory(fontOutputDirectory); 590 | 591 | if (!saveBinary) { 592 | var fontJsonOutputPath = haxe.io.Path.join([fontOutputDirectory, fontFileName + '.json']); 593 | sys.io.File.saveContent(fontJsonOutputPath, haxe.Json.stringify(jsonFont, null, '\t')); 594 | Console.success('Saved "$fontJsonOutputPath"'); 595 | 596 | sys.io.File.saveBytes(textureFilePath, pngBytes); 597 | Console.success('Saved "$textureFilePath" (${atlasW}x${atlasH}, ${glyphList.length} glyphs)'); 598 | } else { 599 | // convert to binary format 600 | 601 | var header: TextureAtlasFontBinaryHeader = { 602 | format: TEXTURE_ATLAS_FONT_BINARY, 603 | version: textureAtlasFontVersion, 604 | technique: jsonFont.technique, 605 | ascender: jsonFont.ascender, 606 | descender: jsonFont.descender, 607 | typoAscender: jsonFont.typoAscender, 608 | typoDescender: jsonFont.typoDescender, 609 | lowercaseHeight: jsonFont.lowercaseHeight, 610 | metadata: jsonFont.metadata, 611 | fieldRange_px: jsonFont.fieldRange_px, 612 | textureSize: jsonFont.textureSize, 613 | 614 | charList: charList, 615 | kerningPairs: jsonFont.kerning.keys(), 616 | 617 | // payload data 618 | characters: null, 619 | kerning: null, 620 | glyphBounds: null, 621 | textures: null, 622 | }; 623 | 624 | // build payload 625 | var payload = new haxe.io.BytesOutput(); 626 | var payloadPos = 0; 627 | 628 | // character data payload 629 | var characterDataBytes = new haxe.io.BytesOutput(); 630 | var characterDataLength_bytes = 4 + (4 * 2) + (3 * 4); 631 | characterDataBytes.prepare(charList.length * characterDataLength_bytes); 632 | for (character in charList) { 633 | var characterData = atlasCharacters.get(character); 634 | 635 | characterDataBytes.writeFloat(characterData.advance); 636 | 637 | var glyph = characterData.glyph != null ? characterData.glyph : { 638 | atlasRect: {x: 0, y: 0, w: 0, h: 0}, 639 | atlasScale: 0.0, 640 | offset: {x: 0.0, y: 0.0}, 641 | }; 642 | 643 | characterDataBytes.writeUInt16(glyph.atlasRect.x); 644 | characterDataBytes.writeUInt16(glyph.atlasRect.y); 645 | characterDataBytes.writeUInt16(glyph.atlasRect.w); 646 | characterDataBytes.writeUInt16(glyph.atlasRect.h); 647 | characterDataBytes.writeFloat(glyph.atlasScale); 648 | characterDataBytes.writeFloat(glyph.offset.x); 649 | characterDataBytes.writeFloat(glyph.offset.y); 650 | } 651 | 652 | // write character payload 653 | payload.write(characterDataBytes.getBytes()); 654 | header.characters = { 655 | start: payloadPos, length: characterDataBytes.length 656 | } 657 | payloadPos = payload.length; 658 | 659 | // kerning payload 660 | var kerningBytes = new haxe.io.BytesOutput(); 661 | var kerningDataLength_bytes = 4; 662 | kerningBytes.prepare(kerningDataLength_bytes * jsonFont.kerning.keys().length); 663 | for (k in jsonFont.kerning.keys()) { 664 | kerningBytes.writeFloat(jsonFont.kerning.get(k)); 665 | } 666 | payload.write(kerningBytes.getBytes()); 667 | header.kerning = { 668 | start: payloadPos, length: kerningBytes.length 669 | } 670 | payloadPos = payload.length; 671 | 672 | // glyph bounds payload 673 | if (storeBounds) { 674 | var boundsBytes = new haxe.io.BytesOutput(); 675 | var boundsDataLength_bytes = 4 * 4; 676 | boundsBytes.prepare(boundsDataLength_bytes * glyphBounds.keys().length); 677 | for (character in charList) { 678 | var bounds = glyphBounds.get(character); 679 | if (bounds == null) { 680 | bounds = { l: 0, r: 0, t: 0, b: 0, } 681 | } 682 | boundsBytes.writeFloat(bounds.t); 683 | boundsBytes.writeFloat(bounds.r); 684 | boundsBytes.writeFloat(bounds.b); 685 | boundsBytes.writeFloat(bounds.l); 686 | } 687 | header.glyphBounds = { 688 | start: payloadPos, length: boundsBytes.length 689 | } 690 | payloadPos = payload.length; 691 | } 692 | 693 | // atlas textures png payload (or external file) 694 | if (externalTextures) { 695 | sys.io.File.saveBytes(textureFilePath, pngBytes); 696 | Console.success('Saved "$textureFilePath" (${atlasW}x${atlasH}, ${glyphList.length} glyphs)'); 697 | 698 | header.textures = [[ 699 | { 700 | {localPath: textureFileName} 701 | } 702 | ]]; 703 | } else { 704 | payload.write(pngBytes); 705 | header.textures = [[ 706 | { 707 | payloadBytes: { 708 | start: payloadPos, length: pngBytes.length 709 | } 710 | } 711 | ]]; 712 | payloadPos = payload.length; 713 | } 714 | 715 | var binaryFontOutput = new haxe.io.BytesOutput(); 716 | binaryFontOutput.writeString(haxe.Json.stringify(header)); 717 | binaryFontOutput.writeByte(0x00); 718 | binaryFontOutput.write(payload.getBytes()); 719 | 720 | var fontBinOutputPath = haxe.io.Path.join([fontOutputDirectory, fontFileName + '.' + technique + '.bin']); 721 | sys.io.File.saveBytes(fontBinOutputPath, binaryFontOutput.getBytes()); 722 | Console.success('Saved "$fontBinOutputPath"'); 723 | } 724 | } 725 | } 726 | 727 | static function ceilPot(x: Float) { 728 | return Std.int(Math.pow(2, Math.ceil(Math.log(x)/Math.log(2)))); 729 | } 730 | 731 | } -------------------------------------------------------------------------------- /generate/src/haxe/zip/Compress.hx: -------------------------------------------------------------------------------- 1 | package haxe.zip; 2 | 3 | class Compress { 4 | 5 | public static function run( s : haxe.io.Bytes, level : Int ) : haxe.io.Bytes { 6 | var result = pako.Pako.deflate(haxe.io.UInt8Array.fromBytes(s), {level: level}); 7 | return result.view.buffer; 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /generate/src/opentype-externs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Closure Compiler externs for opentype.js version. 3 | * @see http://opentype.js.org/ 4 | * @externs 5 | */ 6 | 7 | /** @const */ 8 | var opentype = {}; 9 | /** 10 | * A Font represents a loaded OpenType font file. 11 | * It contains a set of glyphs and methods to draw text on a drawing context, 12 | * or to get a path representing the text. 13 | * @param {FontOptions} 14 | * @constructor 15 | */ 16 | opentype.Font = function(options) {}; 17 | 18 | /** 19 | * Check if the font has a glyph for the given character. 20 | * @param {string} 21 | * @return {Boolean} 22 | */ 23 | opentype.Font.prototype.hasChar = function(c) {}; 24 | 25 | /** 26 | * Convert the given character to a single glyph index. 27 | * Note that this function assumes that there is a one-to-one mapping between 28 | * the given character and a glyph; for complex scripts this might not be the case. 29 | * @param {string} 30 | * @return {Number} 31 | */ 32 | opentype.Font.prototype.charToGlyphIndex = function(s) {}; 33 | 34 | /** 35 | * Convert the given character to a single Glyph object. 36 | * Note that this function assumes that there is a one-to-one mapping between 37 | * the given character and a glyph; for complex scripts this might not be the case. 38 | * @param {string} c 39 | * @return {opentype.Glyph} 40 | */ 41 | opentype.Font.prototype.charToGlyph = function(c) {}; 42 | 43 | /** 44 | * Convert the given text to a list of Glyph objects. 45 | * Note that there is no strict one-to-one mapping between characters and 46 | * glyphs, so the list of returned glyphs can be larger or smaller than the 47 | * length of the given string. 48 | * @param {string} s 49 | * @param {Object=} options 50 | * @return {opentype.Glyph[]} 51 | */ 52 | opentype.Font.prototype.stringToGlyphs = function(s, options) {}; 53 | 54 | /** 55 | * @param {string} 56 | * @return {Number} 57 | */ 58 | opentype.Font.prototype.nameToGlyphIndex = function(name) {}; 59 | 60 | /** 61 | * @param {string} 62 | * @return {opentype.Glyph} 63 | */ 64 | opentype.Font.prototype.nameToGlyph = function(name) {}; 65 | 66 | /** 67 | * @param {Number} 68 | * @return {String} 69 | */ 70 | opentype.Font.prototype.glyphIndexToName = function(gid) {}; 71 | 72 | /** 73 | * Retrieve the value of the kerning pair between the left glyph (or its index) 74 | * and the right glyph (or its index). If no kerning pair is found, return 0. 75 | * The kerning value gets added to the advance width when calculating the spacing 76 | * between glyphs. 77 | * @param {opentype.Glyph} leftGlyph 78 | * @param {opentype.Glyph} rightGlyph 79 | * @return {Number} 80 | */ 81 | opentype.Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) {}; 82 | 83 | /** 84 | * Helper function that invokes the given callback for each glyph in the given text. 85 | * The callback gets `(glyph, x, y, fontSize, options)`.* @param {string} text 86 | * @param {number} x - Horizontal position of the beginning of the text. 87 | * @param {number} y - Vertical position of the *baseline* of the text. 88 | * @param {number} fontSize - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 89 | * @param {Object} options 90 | * @param {Function} callback 91 | */ 92 | opentype.Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback) {}; 93 | 94 | /** 95 | * Create a Path object that represents the given text. 96 | * @param {string} text - The text to create. 97 | * @param {number} [x=0] - Horizontal position of the beginning of the text. 98 | * @param {number} [y=0] - Vertical position of the *baseline* of the text. 99 | * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 100 | * @param {Object=} options 101 | * @return {opentype.Path} 102 | */ 103 | opentype.Font.prototype.getPath = function(text, x, y, fontSize, options) {}; 104 | 105 | /** 106 | * Create an array of Path objects that represent the glyps of a given text. 107 | * @param {string} text - The text to create. 108 | * @param {number} [x=0] - Horizontal position of the beginning of the text. 109 | * @param {number} [y=0] - Vertical position of the *baseline* of the text. 110 | * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 111 | * @param {Object=} options 112 | * @return {opentype.Path[]} 113 | */ 114 | opentype.Font.prototype.getPaths = function(text, x, y, fontSize, options) {}; 115 | 116 | /** 117 | * Draw the text on the given drawing context. 118 | * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 119 | * @param {string} text - The text to create. 120 | * @param {number} [x=0] - Horizontal position of the beginning of the text. 121 | * @param {number} [y=0] - Vertical position of the *baseline* of the text. 122 | * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 123 | * @param {Object=} options 124 | */ 125 | opentype.Font.prototype.draw = function(ctx, text, x, y, fontSize, options) {}; 126 | 127 | /** 128 | * Draw the points of all glyphs in the text. 129 | * On-curve points will be drawn in blue, off-curve points will be drawn in red. 130 | * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 131 | * @param {string} text - The text to create. 132 | * @param {number} [x=0] - Horizontal position of the beginning of the text. 133 | * @param {number} [y=0] - Vertical position of the *baseline* of the text. 134 | * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 135 | * @param {Object=} options 136 | */ 137 | opentype.Font.prototype.drawPoints = function(ctx, text, x, y, fontSize, options) {}; 138 | 139 | /** 140 | * Draw lines indicating important font measurements for all glyphs in the text. 141 | * Black lines indicate the origin of the coordinate system (point 0,0). 142 | * Blue lines indicate the glyph bounding box. 143 | * Green line indicates the advance width of the glyph. 144 | * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 145 | * @param {string} text - The text to create. 146 | * @param {number} [x=0] - Horizontal position of the beginning of the text. 147 | * @param {number} [y=0] - Vertical position of the *baseline* of the text. 148 | * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 149 | * @param {Object=} options 150 | */ 151 | opentype.Font.prototype.drawMetrics = function(ctx, text, x, y, fontSize, options) {}; 152 | 153 | /** 154 | * @param {string} 155 | * @return {string} 156 | */ 157 | opentype.Font.prototype.getEnglishName = function(name) {}; 158 | 159 | /** 160 | * Validate 161 | */ 162 | opentype.Font.prototype.validate = function() {}; 163 | 164 | /** 165 | * Convert the font object to a SFNT data structure. 166 | * This structure contains all the necessary tables and metadata to create a binary OTF file. 167 | * @return {opentype.Table} 168 | */ 169 | opentype.Font.prototype.toTables = function() {}; 170 | /** 171 | * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead. 172 | */ 173 | opentype.Font.prototype.toBuffer = function() {}; 174 | /** 175 | * Converts a `opentype.Font` into an `ArrayBuffer` 176 | * @return {ArrayBuffer} 177 | */ 178 | opentype.Font.prototype.toArrayBuffer = function() {}; 179 | 180 | /** 181 | * Initiate a download of the OpenType font. 182 | * @param {string=} fileName 183 | */ 184 | opentype.Font.prototype.download = function(fileName) {}; 185 | 186 | // A Glyph is an individual mark that often corresponds to a character. 187 | // Some glyphs, such as ligatures, are a combination of many characters. 188 | // Glyphs are the basic building blocks of a font. 189 | // 190 | // The `Glyph` class contains utility methods for drawing the path and its points. 191 | /** 192 | * @param {GlyphOptions} 193 | * @constructor 194 | */ 195 | opentype.Glyph = function(options) {}; 196 | 197 | /** 198 | * @param {number} 199 | */ 200 | opentype.Glyph.prototype.addUnicode = function(unicode) {}; 201 | 202 | /** 203 | * Calculate the minimum bounding box for this glyph. 204 | * @return {opentype.BoundingBox} 205 | */ 206 | opentype.Glyph.prototype.getBoundingBox = function() {}; 207 | 208 | /** 209 | * Convert the glyph to a Path we can draw on a drawing context. 210 | * @param {number} [x=0] - Horizontal position of the beginning of the text. 211 | * @param {number} [y=0] - Vertical position of the *baseline* of the text. 212 | * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 213 | * @param {Object=} options - xScale, yScale to strech the glyph. 214 | * @return {opentype.Path} 215 | */ 216 | opentype.Glyph.prototype.getPath = function(x, y, fontSize, options) {}; 217 | 218 | /** 219 | * Split the glyph into contours. 220 | * This function is here for backwards compatibility, and to 221 | * provide raw access to the TrueType glyph outlines. 222 | * @return {Array} 223 | */ 224 | opentype.Glyph.prototype.getContours = function() {}; 225 | 226 | /** 227 | * Calculate the xMin/yMin/xMax/yMax/lsb/rsb for a Glyph. 228 | * @return {Object} 229 | */ 230 | opentype.Glyph.prototype.getMetrics = function() {}; 231 | 232 | /** 233 | * Draw the glyph on the given context. 234 | * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 235 | * @param {number} [x=0] - Horizontal position of the beginning of the text. 236 | * @param {number} [y=0] - Vertical position of the *baseline* of the text. 237 | * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 238 | * @param {Object=} options - xScale, yScale to strech the glyph. 239 | */ 240 | opentype.Glyph.prototype.draw = function(ctx, x, y, fontSize, options) {}; 241 | 242 | /** 243 | * Draw the points of the glyph. 244 | * On-curve points will be drawn in blue, off-curve points will be drawn in red. 245 | * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 246 | * @param {number} [x=0] - Horizontal position of the beginning of the text. 247 | * @param {number} [y=0] - Vertical position of the *baseline* of the text. 248 | * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 249 | */ 250 | opentype.Glyph.prototype.drawPoints = function(ctx, x, y, fontSize) {}; 251 | 252 | /** 253 | * Draw lines indicating important font measurements. 254 | * Black lines indicate the origin of the coordinate system (point 0,0). 255 | * Blue lines indicate the glyph bounding box. 256 | * Green line indicates the advance width of the glyph. 257 | * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 258 | * @param {number} [x=0] - Horizontal position of the beginning of the text. 259 | * @param {number} [y=0] - Vertical position of the *baseline* of the text. 260 | * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 261 | */ 262 | opentype.Glyph.prototype.drawMetrics = function(ctx, x, y, fontSize) {}; 263 | 264 | /** 265 | * A bézier path containing a set of path commands similar to a SVG path. 266 | * Paths can be drawn on a context using `draw`. 267 | * @constructor 268 | */ 269 | opentype.Path = function() {}; 270 | 271 | /** 272 | * @param {number} x 273 | * @param {number} y 274 | */ 275 | opentype.Path.prototype.moveTo = function(x, y) {}; 276 | 277 | /** 278 | * @param {number} x 279 | * @param {number} y 280 | */ 281 | opentype.Path.prototype.lineTo = function(x, y) {}; 282 | 283 | /** 284 | * Draws cubic curve 285 | * @param {number} x1 - x of control 1 286 | * @param {number} y1 - y of control 1 287 | * @param {number} x2 - x of control 2 288 | * @param {number} y2 - y of control 2 289 | * @param {number} x - x of path point 290 | * @param {number} y - y of path point 291 | */ 292 | opentype.Path.prototype.curveTo = function(x1, y1, x2, y2, x, y) {}; 293 | 294 | /** 295 | * Draws cubic curve 296 | * @param {number} x1 - x of control 1 297 | * @param {number} y1 - y of control 1 298 | * @param {number} x2 - x of control 2 299 | * @param {number} y2 - y of control 2 300 | * @param {number} x - x of path point 301 | * @param {number} y - y of path point 302 | */ 303 | opentype.Path.prototype.bezierCurveTo = function(x1, y1, x2, y2, x, y) {}; 304 | 305 | /** 306 | * Draws quadratic curve 307 | * @param {number} x1 - x of control 308 | * @param {number} y1 - y of control 309 | * @param {number} x - x of path point 310 | * @param {number} y - y of path point 311 | */ 312 | opentype.Path.prototype.quadTo = function(x1, y1, x, y) {}; 313 | 314 | /** 315 | * Draws quadratic curve 316 | * @param {number} x1 - x of control 317 | * @param {number} y1 - y of control 318 | * @param {number} x - x of path point 319 | * @param {number} y - y of path point 320 | */ 321 | opentype.Path.prototype.quadraticCurveTo = function(x1, y1, x, y) {}; 322 | 323 | /** 324 | * Close the path 325 | */ 326 | opentype.Path.prototype.close = function() {}; 327 | 328 | /** 329 | * Closes the path 330 | */ 331 | opentype.Path.prototype.closePath = function() {}; 332 | 333 | /** 334 | * Add the given path or list of commands to the commands of this path. 335 | * @param {Array} pathOrCommands - another opentype.Path, an opentype.BoundingBox, or an array of commands. 336 | */ 337 | opentype.Path.prototype.extend = function(pathOrCommands) {}; 338 | 339 | /** 340 | * Calculate the bounding box of the path. 341 | * @returns {opentype.BoundingBox} 342 | */ 343 | opentype.Path.prototype.getBoundingBox = function() {}; 344 | 345 | /** 346 | * @param {CanvasRenderingContext2D} ctx - A 2D drawing context. 347 | */ 348 | opentype.Path.prototype.draw = function(ctx) {}; 349 | 350 | /** 351 | * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values 352 | * @return {string} 353 | */ 354 | opentype.Path.prototype.toPathData = function(decimalPlaces) {}; 355 | 356 | /** 357 | * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values 358 | * @return {string} 359 | */ 360 | opentype.Path.prototype.toSVG = function(decimalPlaces) {}; 361 | 362 | /** 363 | * Convert the path to a DOM element. 364 | * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values 365 | * @return {SVGPathElement} 366 | */ 367 | opentype.Path.prototype.toDOMElement = function(decimalPlaces) {}; 368 | 369 | /** 370 | * @constructor 371 | */ 372 | opentype.Layout = function(font, tableName) {}; 373 | 374 | /** 375 | * Binary search an object by "tag" property 376 | * @param {Array} arr 377 | * @param {string} tag 378 | * @return {number} 379 | */ 380 | opentype.Layout.prototype.searchTag = function(arr, tag) {}; 381 | 382 | /** 383 | * Binary search in a list of numbers 384 | * @param {Array} arr 385 | * @param {number} value 386 | * @return {number} 387 | */ 388 | opentype.Layout.prototype.binSearch = function (arr, value) {}; 389 | 390 | /** 391 | * Get or create the Layout table (GSUB, GPOS etc). 392 | * @param {boolean} create - Whether to create a new one. 393 | * @return {Object} The GSUB or GPOS table. 394 | */ 395 | opentype.Layout.prototype.getTable = function(create) {}; 396 | 397 | /** 398 | * Returns all scripts in the substitution table. 399 | * @instance 400 | * @return {Array} 401 | */ 402 | opentype.Layout.prototype.getScriptNames = function() {}; 403 | 404 | /** 405 | * Returns all LangSysRecords in the given script. 406 | * @instance 407 | * @param {string} script - Use 'DFLT' for default script 408 | * @param {boolean} create - forces the creation of this script table if it doesn't exist. 409 | * @return {Array} Array on names 410 | */ 411 | opentype.Layout.prototype.getScriptTable = function(script, create) {}; 412 | 413 | /** 414 | * Returns a language system table 415 | * @instance 416 | * @param {string} script - Use 'DFLT' for default script 417 | * @param {string} language - Use 'dlft' for default language 418 | * @param {boolean} create - forces the creation of this langSysTable if it doesn't exist. 419 | * @return {Object} An object with tag and script properties. 420 | */ 421 | opentype.Layout.prototype.getLangSysTable = function(script, language, create) {}; 422 | 423 | /** 424 | * Get a specific feature table. 425 | * @instance 426 | * @param {string} script - Use 'DFLT' for default script 427 | * @param {string} language - Use 'dlft' for default language 428 | * @param {string} feature - One of the codes listed at https://www.microsoft.com/typography/OTSPEC/featurelist.htm 429 | * @param {boolean} create - forces the creation of the feature table if it doesn't exist. 430 | * @return {Object} 431 | */ 432 | opentype.Layout.prototype.getFeatureTable = function(script, language, feature, create) {}; 433 | 434 | /** 435 | * Get the lookup tables of a given type for a script/language/feature. 436 | * @instance 437 | * @param {string} [script='DFLT'] 438 | * @param {string} [language='dlft'] 439 | * @param {string} feature - 4-letter feature code 440 | * @param {number} lookupType - 1 to 8 441 | * @param {boolean} create - forces the creation of the lookup table if it doesn't exist, with no subtables. 442 | * @return {Object[]} 443 | */ 444 | opentype.Layout.prototype.getLookupTables = function(script, language, feature, lookupType, create) {}; 445 | 446 | /** 447 | * Returns the list of glyph indexes of a coverage table. 448 | * Format 1: the list is stored raw 449 | * Format 2: compact list as range records. 450 | * @instance 451 | * @param {Object} coverageTable 452 | * @return {Array} 453 | */ 454 | opentype.Layout.prototype.expandCoverage = function(coverageTable) {}; 455 | 456 | /** 457 | * @extends opentype.Layout 458 | * @constructor 459 | * @param {opentype.Font} 460 | */ 461 | opentype.Substitution = function(font) {}; 462 | 463 | /** 464 | * Create a default GSUB table. 465 | * @return {Object} gsub - The GSUB table. 466 | */ 467 | opentype.Substitution.prototype.createDefaultTable = function() {}; 468 | 469 | /** 470 | * List all single substitutions (lookup type 1) for a given script, language, and feature. 471 | * @param {string} script 472 | * @param {string} language 473 | * @param {string} feature - 4-character feature name ('aalt', 'salt', 'ss01'...) 474 | * @return {Array} substitutions - The list of substitutions. 475 | */ 476 | opentype.Substitution.prototype.getSingle = function(feature, script, language) {}; 477 | 478 | /** 479 | * List all alternates (lookup type 3) for a given script, language, and feature. 480 | * @param {string} feature - 4-character feature name ('aalt', 'salt'...) 481 | * @param {string} script 482 | * @param {string} language 483 | * @return {Array} alternates - The list of alternates 484 | */ 485 | opentype.Substitution.prototype.getAlternates = function(feature, script, language) {}; 486 | 487 | /** 488 | * List all ligatures (lookup type 4) for a given script, language, and feature. 489 | * The result is an array of ligature objects like { sub: [ids], by: id } 490 | * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) 491 | * @param {string} script 492 | * @param {string} language 493 | * @return {Array} ligatures - The list of ligatures. 494 | */ 495 | opentype.Substitution.prototype.getLigatures = function(feature, script, language) {}; 496 | 497 | /** 498 | * Add or modify a single substitution (lookup type 1) 499 | * Format 2, more flexible, is always used. 500 | * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) 501 | * @param {Object} substitution - { sub: id, delta: number } for format 1 or { sub: id, by: id } for format 2. 502 | * @param {string} [script='DFLT'] 503 | * @param {string} [language='dflt'] 504 | */ 505 | opentype.Substitution.prototype.addSingle = function(feature, substitution, script, language) {}; 506 | 507 | /** 508 | * Add or modify an alternate substitution (lookup type 1) 509 | * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) 510 | * @param {Object} substitution - { sub: id, by: [ids] } 511 | * @param {string} [script='DFLT'] 512 | * @param {string} [language='dflt'] 513 | */ 514 | opentype.Substitution.prototype.addAlternate = function(feature, substitution, script, language) {}; 515 | 516 | /** 517 | * Add a ligature (lookup type 4) 518 | * Ligatures with more components must be stored ahead of those with fewer components in order to be found 519 | * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) 520 | * @param {Object} ligature - { sub: [ids], by: id } 521 | * @param {string} [script='DFLT'] 522 | * @param {string} [language='dflt'] 523 | */ 524 | opentype.Substitution.prototype.addLigature = function(feature, ligature, script, language) {}; 525 | 526 | /** 527 | * List all feature data for a given script and language. 528 | * @param {string} feature - 4-letter feature name 529 | * @param {string} [script='DFLT'] 530 | * @param {string} [language='dflt'] 531 | * @return {[type]} [description] 532 | * @return {Array} substitutions - The list of substitutions. 533 | */ 534 | opentype.Substitution.prototype.getFeature = function(feature, script, language) {}; 535 | 536 | /** 537 | * Add a substitution to a feature for a given script and language. 538 | * @param {string} feature - 4-letter feature name 539 | * @param {Object} sub - the substitution to add (an Object like { sub: id or [ids], by: id or [ids] }) 540 | * @param {string} [script='DFLT'] 541 | * @param {string} [language='dflt'] 542 | */ 543 | opentype.Substitution.prototype.add = function(feature, sub, script, language) {}; 544 | 545 | /** 546 | * @param {string} tableName 547 | * @param {Array} fields 548 | * @param {Object} options 549 | * @constructor 550 | */ 551 | opentype.Table = function(tableName, fields, options) {}; 552 | 553 | /** 554 | * Encodes the table and returns an array of bytes 555 | * @return {Array} 556 | */ 557 | opentype.Table.prototype.encode = function() {}; 558 | 559 | /** 560 | * Get the size of the table. 561 | * @return {number} 562 | */ 563 | opentype.Table.prototype.sizeOf = function() {}; 564 | 565 | /** 566 | * @type {string} 567 | */ 568 | opentype.Table.prototype.tableName; 569 | 570 | /** 571 | * @type {Array} 572 | */ 573 | opentype.Table.prototype.fields; 574 | 575 | /** 576 | * @extends {opentype.Table} 577 | * @param {opentype.Table} coverageTable 578 | * @constructor 579 | */ 580 | opentype.Coverage = function(coverageTable) {}; 581 | 582 | /** 583 | * @extends {opentype.Table} 584 | * @param {opentype.Table} scriptListTable 585 | * @constructor 586 | */ 587 | opentype.ScriptList = function(scriptListTable) {}; 588 | 589 | /** 590 | * @extends {opentype.Table} 591 | * @param {opentype.Table} featureListTable 592 | * @constructor 593 | */ 594 | opentype.FeatureList = function(featureListTable) {}; 595 | 596 | /** 597 | * @extends {opentype.Table} 598 | * @param {opentype.Table} lookupListTable 599 | * @param {Object} subtableMakers 600 | * @constructor 601 | */ 602 | opentype.LookupList = function(lookupListTable, subtableMakers) {}; 603 | 604 | /** 605 | * @constructor 606 | */ 607 | opentype.BoundingBox = function() {}; 608 | 609 | /** 610 | * @param {string} url - The URL of the font to load. 611 | * @param {Function} callback - The callback. 612 | */ 613 | opentype.load = function(url, callback) {}; 614 | 615 | /** 616 | * @param {string} url - The URL of the font to load. 617 | * @return {opentype.Font} 618 | */ 619 | opentype.loadSync = function(url) {}; 620 | 621 | /** 622 | * @param {ArrayBuffer} 623 | * @return {opentype.Font} 624 | */ 625 | opentype.parse = function(buffer) {}; 626 | -------------------------------------------------------------------------------- /generate/src/opentype/BoundingBox.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | @constructor 5 | 6 | **/ 7 | extern class BoundingBox { 8 | function new():Void; 9 | } -------------------------------------------------------------------------------- /generate/src/opentype/Coverage.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | @extends {opentype.Table} 5 | @param {opentype.Table} coverageTable 6 | @constructor 7 | 8 | **/ 9 | extern class Coverage extends opentype.Table { 10 | function new(coverageTable:opentype.Table):Void; 11 | } -------------------------------------------------------------------------------- /generate/src/opentype/FeatureList.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | @extends {opentype.Table} 5 | @param {opentype.Table} featureListTable 6 | @constructor 7 | 8 | **/ 9 | extern class FeatureList extends opentype.Table { 10 | function new(featureListTable:opentype.Table):Void; 11 | } -------------------------------------------------------------------------------- /generate/src/opentype/Font.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | A Font represents a loaded OpenType font file. 5 | It contains a set of glyphs and methods to draw text on a drawing context, 6 | or to get a path representing the text. 7 | @param {FontOptions} 8 | @constructor 9 | 10 | **/ 11 | extern class Font { 12 | var names : { var fontFamily : { var en : String; }; var fontSubfamily : { var en : String; }; var fullName : { var en : String; }; var postScriptName : { var en : String; }; var designer : { var en : String; }; var designerURL : { var en : String; }; var manufacturer : { var en : String; }; var manufacturerURL : { var en : String; }; var license : { var en : String; }; var licenseURL : { var en : String; }; var version : { var en : String; }; var description : { var en : String; }; var copyright : { var en : String; }; var trademark : { var en : String; }; }; 13 | var unitsPerEm : Int; 14 | var ascender : Float; 15 | var descender : Float; 16 | var createdTimestamp : Int; 17 | var tables : { var os2 : { var version : Int; var xAvgCharWidth : Int; var usWeightClass : Int; var usWidthClass : Int; var fsType : Int; var ySubscriptXSize : Int; var ySubscriptYSize : Int; var ySubscriptXOffset : Int; var ySubscriptYOffset : Int; var ySuperscriptXSize : Int; var ySuperscriptYSize : Int; var ySuperscriptXOffset : Int; var ySuperscriptYOffset : Int; var yStrikeoutSize : Int; var yStrikeoutPosition : Int; var sFamilyClass : Int; var bFamilyType : Int; var bSerifStyle : Int; var bWeight : Int; var bProportion : Int; var bContrast : Int; var bStrokeVariation : Int; var bArmStyle : Int; var bLetterform : Int; var bMidline : Int; var bXHeight : Int; var ulUnicodeRange1 : Int; var ulUnicodeRange2 : Int; var ulUnicodeRange3 : Int; var ulUnicodeRange4 : Int; var achVendID : String; var fsSelection : Int; var usFirstCharIndex : Int; var usLastCharIndex : Int; var sTypoAscender : Int; var sTypoDescender : Int; var sTypoLineGap : Int; var usWinAscent : Int; var usWinDescent : Int; var ulCodePageRange1 : Int; var ulCodePageRange2 : Int; var sxHeight : Int; var sCapHeight : Int; var usDefaultChar : Int; var usBreakChar : Int; var usMaxContext : Int; }; }; 18 | var supported : Bool; 19 | var glyphs : { function get(index:Int):Glyph; function push(index:Int, loader:Any):Void; var length : Int; }; 20 | var encoding : Any; 21 | var position : Any; 22 | var substitution : Substitution; 23 | var hinting : Any; 24 | function new(options:FontOptions):Void; 25 | /** 26 | 27 | Check if the font has a glyph for the given character. 28 | @param {string} 29 | @return {Boolean} 30 | 31 | **/ 32 | function hasChar(c:String):Bool; 33 | /** 34 | 35 | Convert the given character to a single glyph index. 36 | Note that this function assumes that there is a one-to-one mapping between 37 | the given character and a glyph; for complex scripts this might not be the case. 38 | @param {string} 39 | @return {Number} 40 | 41 | **/ 42 | function charToGlyphIndex(s:String):Float; 43 | /** 44 | 45 | Convert the given character to a single Glyph object. 46 | Note that this function assumes that there is a one-to-one mapping between 47 | the given character and a glyph; for complex scripts this might not be the case. 48 | @param {string} c 49 | @return {opentype.Glyph} 50 | 51 | **/ 52 | function charToGlyph(c:String):opentype.Glyph; 53 | /** 54 | 55 | Convert the given text to a list of Glyph objects. 56 | Note that there is no strict one-to-one mapping between characters and 57 | glyphs, so the list of returned glyphs can be larger or smaller than the 58 | length of the given string. 59 | @param {string} s 60 | @param {Object=} options 61 | @return {opentype.Glyph[]} 62 | 63 | **/ 64 | function stringToGlyphs(s:String, ?options:haxe.DynamicAccess):Array; 65 | /** 66 | 67 | @param {string} 68 | @return {Number} 69 | 70 | **/ 71 | function nameToGlyphIndex(name:String):Float; 72 | /** 73 | 74 | @param {string} 75 | @return {opentype.Glyph} 76 | 77 | **/ 78 | function nameToGlyph(name:String):opentype.Glyph; 79 | /** 80 | 81 | @param {Number} 82 | @return {String} 83 | 84 | **/ 85 | function glyphIndexToName(gid:Float):String; 86 | /** 87 | 88 | Retrieve the value of the kerning pair between the left glyph (or its index) 89 | and the right glyph (or its index). If no kerning pair is found, return 0. 90 | The kerning value gets added to the advance width when calculating the spacing 91 | between glyphs. 92 | @param {opentype.Glyph} leftGlyph 93 | @param {opentype.Glyph} rightGlyph 94 | @return {Number} 95 | 96 | **/ 97 | function getKerningValue(leftGlyph:opentype.Glyph, rightGlyph:opentype.Glyph):Float; 98 | /** 99 | 100 | Helper function that invokes the given callback for each glyph in the given text. 101 | The callback gets `(glyph, x, y, fontSize, options)`.* @param {string} text 102 | @param {number} x - Horizontal position of the beginning of the text. 103 | @param {number} y - Vertical position of the *baseline* of the text. 104 | @param {number} fontSize - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 105 | @param {Object} options 106 | @param {Function} callback 107 | 108 | **/ 109 | function forEachGlyph(text:Any, x:Float, y:Float, fontSize:Float, options:haxe.DynamicAccess, callback:Any):Void; 110 | /** 111 | 112 | Create a Path object that represents the given text. 113 | @param {string} text - The text to create. 114 | @param {number} [x=0] - Horizontal position of the beginning of the text. 115 | @param {number} [y=0] - Vertical position of the *baseline* of the text. 116 | @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 117 | @param {Object=} options 118 | @return {opentype.Path} 119 | 120 | **/ 121 | function getPath(text:String, x:Float, y:Float, fontSize:Float, ?options:haxe.DynamicAccess):opentype.Path; 122 | /** 123 | 124 | Create an array of Path objects that represent the glyps of a given text. 125 | @param {string} text - The text to create. 126 | @param {number} [x=0] - Horizontal position of the beginning of the text. 127 | @param {number} [y=0] - Vertical position of the *baseline* of the text. 128 | @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 129 | @param {Object=} options 130 | @return {opentype.Path[]} 131 | 132 | **/ 133 | function getPaths(text:String, x:Float, y:Float, fontSize:Float, ?options:haxe.DynamicAccess):Array; 134 | /** 135 | 136 | Draw the text on the given drawing context. 137 | @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 138 | @param {string} text - The text to create. 139 | @param {number} [x=0] - Horizontal position of the beginning of the text. 140 | @param {number} [y=0] - Vertical position of the *baseline* of the text. 141 | @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 142 | @param {Object=} options 143 | 144 | **/ 145 | function draw(ctx:js.html.CanvasRenderingContext2D, text:String, x:Float, y:Float, fontSize:Float, ?options:haxe.DynamicAccess):Void; 146 | /** 147 | 148 | Draw the points of all glyphs in the text. 149 | On-curve points will be drawn in blue, off-curve points will be drawn in red. 150 | @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 151 | @param {string} text - The text to create. 152 | @param {number} [x=0] - Horizontal position of the beginning of the text. 153 | @param {number} [y=0] - Vertical position of the *baseline* of the text. 154 | @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 155 | @param {Object=} options 156 | 157 | **/ 158 | function drawPoints(ctx:js.html.CanvasRenderingContext2D, text:String, x:Float, y:Float, fontSize:Float, ?options:haxe.DynamicAccess):Void; 159 | /** 160 | 161 | Draw lines indicating important font measurements for all glyphs in the text. 162 | Black lines indicate the origin of the coordinate system (point 0,0). 163 | Blue lines indicate the glyph bounding box. 164 | Green line indicates the advance width of the glyph. 165 | @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 166 | @param {string} text - The text to create. 167 | @param {number} [x=0] - Horizontal position of the beginning of the text. 168 | @param {number} [y=0] - Vertical position of the *baseline* of the text. 169 | @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 170 | @param {Object=} options 171 | 172 | **/ 173 | function drawMetrics(ctx:js.html.CanvasRenderingContext2D, text:String, x:Float, y:Float, fontSize:Float, ?options:haxe.DynamicAccess):Void; 174 | /** 175 | 176 | @param {string} 177 | @return {string} 178 | 179 | **/ 180 | function getEnglishName(name:String):String; 181 | /** 182 | 183 | Validate 184 | 185 | **/ 186 | function validate():Void; 187 | /** 188 | 189 | Convert the font object to a SFNT data structure. 190 | This structure contains all the necessary tables and metadata to create a binary OTF file. 191 | @return {opentype.Table} 192 | 193 | **/ 194 | function toTables():opentype.Table; 195 | /** 196 | 197 | @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead. 198 | 199 | **/ 200 | function toBuffer():Void; 201 | /** 202 | 203 | Converts a `opentype.Font` into an `ArrayBuffer` 204 | @return {ArrayBuffer} 205 | 206 | **/ 207 | function toArrayBuffer():js.html.ArrayBuffer; 208 | /** 209 | 210 | Initiate a download of the OpenType font. 211 | @param {string=} fileName 212 | 213 | **/ 214 | function download(?fileName:String):Void; 215 | } -------------------------------------------------------------------------------- /generate/src/opentype/FontOptions.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | typedef FontOptions = { 3 | var empty : Bool; 4 | var familyName : String; 5 | var styleName : String; 6 | @:optional 7 | var fullName : String; 8 | @:optional 9 | var postScriptName : String; 10 | @:optional 11 | var designer : String; 12 | @:optional 13 | var designerURL : String; 14 | @:optional 15 | var manufacturer : String; 16 | @:optional 17 | var manufacturerURL : String; 18 | @:optional 19 | var license : String; 20 | @:optional 21 | var licenseURL : String; 22 | @:optional 23 | var version : String; 24 | @:optional 25 | var description : String; 26 | @:optional 27 | var copyright : String; 28 | @:optional 29 | var trademark : String; 30 | var unitsPerEm : Float; 31 | var ascender : Float; 32 | var descender : Float; 33 | var createdTimestamp : Float; 34 | @:optional 35 | var weightClass : String; 36 | @:optional 37 | var widthClass : String; 38 | @:optional 39 | var fsSelection : String; 40 | }; -------------------------------------------------------------------------------- /generate/src/opentype/Glyph.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | @param {GlyphOptions} 5 | @constructor 6 | 7 | **/ 8 | extern class Glyph { 9 | function new(options:GlyphOptions):Void; 10 | /** 11 | 12 | @param {number} 13 | 14 | **/ 15 | function addUnicode(unicode:Float):Void; 16 | /** 17 | 18 | Calculate the minimum bounding box for this glyph. 19 | @return {opentype.BoundingBox} 20 | 21 | **/ 22 | function getBoundingBox():opentype.BoundingBox; 23 | /** 24 | 25 | Convert the glyph to a Path we can draw on a drawing context. 26 | @param {number} [x=0] - Horizontal position of the beginning of the text. 27 | @param {number} [y=0] - Vertical position of the *baseline* of the text. 28 | @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 29 | @param {Object=} options - xScale, yScale to strech the glyph. 30 | @return {opentype.Path} 31 | 32 | **/ 33 | function getPath(x:Float, y:Float, fontSize:Float, ?options:haxe.DynamicAccess):opentype.Path; 34 | /** 35 | 36 | Split the glyph into contours. 37 | This function is here for backwards compatibility, and to 38 | provide raw access to the TrueType glyph outlines. 39 | @return {Array} 40 | 41 | **/ 42 | function getContours():Array; 43 | /** 44 | 45 | Calculate the xMin/yMin/xMax/yMax/lsb/rsb for a Glyph. 46 | @return {Object} 47 | 48 | **/ 49 | function getMetrics():haxe.DynamicAccess; 50 | /** 51 | 52 | Draw the glyph on the given context. 53 | @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 54 | @param {number} [x=0] - Horizontal position of the beginning of the text. 55 | @param {number} [y=0] - Vertical position of the *baseline* of the text. 56 | @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 57 | @param {Object=} options - xScale, yScale to strech the glyph. 58 | 59 | **/ 60 | function draw(ctx:js.html.CanvasRenderingContext2D, x:Float, y:Float, fontSize:Float, ?options:haxe.DynamicAccess):Void; 61 | /** 62 | 63 | Draw the points of the glyph. 64 | On-curve points will be drawn in blue, off-curve points will be drawn in red. 65 | @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 66 | @param {number} [x=0] - Horizontal position of the beginning of the text. 67 | @param {number} [y=0] - Vertical position of the *baseline* of the text. 68 | @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 69 | 70 | **/ 71 | function drawPoints(ctx:js.html.CanvasRenderingContext2D, x:Float, y:Float, fontSize:Float):Void; 72 | /** 73 | 74 | Draw lines indicating important font measurements. 75 | Black lines indicate the origin of the coordinate system (point 0,0). 76 | Blue lines indicate the glyph bounding box. 77 | Green line indicates the advance width of the glyph. 78 | @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. 79 | @param {number} [x=0] - Horizontal position of the beginning of the text. 80 | @param {number} [y=0] - Vertical position of the *baseline* of the text. 81 | @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. 82 | 83 | **/ 84 | function drawMetrics(ctx:js.html.CanvasRenderingContext2D, x:Float, y:Float, fontSize:Float):Void; 85 | } -------------------------------------------------------------------------------- /generate/src/opentype/GlyphOptions.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | typedef GlyphOptions = { 3 | var name : String; 4 | var unicode : Int; 5 | var unicodes : Array; 6 | var xMin : Float; 7 | var yMin : Float; 8 | var xMax : Float; 9 | var yMax : Float; 10 | var advanceWidth : Float; 11 | }; -------------------------------------------------------------------------------- /generate/src/opentype/Layout.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | @constructor 5 | 6 | **/ 7 | extern class Layout { 8 | function new(font:Any, tableName:Any):Void; 9 | /** 10 | 11 | Binary search an object by "tag" property 12 | @param {Array} arr 13 | @param {string} tag 14 | @return {number} 15 | 16 | **/ 17 | function searchTag(arr:Array, tag:String):Float; 18 | /** 19 | 20 | Binary search in a list of numbers 21 | @param {Array} arr 22 | @param {number} value 23 | @return {number} 24 | 25 | **/ 26 | function binSearch(arr:Array, value:Float):Float; 27 | /** 28 | 29 | Get or create the Layout table (GSUB, GPOS etc). 30 | @param {boolean} create - Whether to create a new one. 31 | @return {Object} The GSUB or GPOS table. 32 | 33 | **/ 34 | function getTable(create:Bool):haxe.DynamicAccess; 35 | /** 36 | 37 | Returns all scripts in the substitution table. 38 | @instance 39 | @return {Array} 40 | 41 | **/ 42 | function getScriptNames():Array; 43 | /** 44 | 45 | Returns all LangSysRecords in the given script. 46 | @instance 47 | @param {string} script - Use 'DFLT' for default script 48 | @param {boolean} create - forces the creation of this script table if it doesn't exist. 49 | @return {Array} Array on names 50 | 51 | **/ 52 | function getScriptTable(script:String, create:Bool):Array; 53 | /** 54 | 55 | Returns a language system table 56 | @instance 57 | @param {string} script - Use 'DFLT' for default script 58 | @param {string} language - Use 'dlft' for default language 59 | @param {boolean} create - forces the creation of this langSysTable if it doesn't exist. 60 | @return {Object} An object with tag and script properties. 61 | 62 | **/ 63 | function getLangSysTable(script:String, language:String, create:Bool):haxe.DynamicAccess; 64 | /** 65 | 66 | Get a specific feature table. 67 | @instance 68 | @param {string} script - Use 'DFLT' for default script 69 | @param {string} language - Use 'dlft' for default language 70 | @param {string} feature - One of the codes listed at https://www.microsoft.com/typography/OTSPEC/featurelist.htm 71 | @param {boolean} create - forces the creation of the feature table if it doesn't exist. 72 | @return {Object} 73 | 74 | **/ 75 | function getFeatureTable(script:String, language:String, feature:String, create:Bool):haxe.DynamicAccess; 76 | /** 77 | 78 | Get the lookup tables of a given type for a script/language/feature. 79 | @instance 80 | @param {string} [script='DFLT'] 81 | @param {string} [language='dlft'] 82 | @param {string} feature - 4-letter feature code 83 | @param {number} lookupType - 1 to 8 84 | @param {boolean} create - forces the creation of the lookup table if it doesn't exist, with no subtables. 85 | @return {Object[]} 86 | 87 | **/ 88 | function getLookupTables(script:String, language:String, feature:String, lookupType:Float, create:Bool):Array>; 89 | /** 90 | 91 | Returns the list of glyph indexes of a coverage table. 92 | Format 1: the list is stored raw 93 | Format 2: compact list as range records. 94 | @instance 95 | @param {Object} coverageTable 96 | @return {Array} 97 | 98 | **/ 99 | function expandCoverage(coverageTable:haxe.DynamicAccess):Array; 100 | } -------------------------------------------------------------------------------- /generate/src/opentype/LookupList.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | @extends {opentype.Table} 5 | @param {opentype.Table} lookupListTable 6 | @param {Object} subtableMakers 7 | @constructor 8 | 9 | **/ 10 | extern class LookupList extends opentype.Table { 11 | function new(lookupListTable:opentype.Table, subtableMakers:haxe.DynamicAccess):Void; 12 | } -------------------------------------------------------------------------------- /generate/src/opentype/Opentype.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | @const 4 | **/ 5 | @:jsRequire("opentype") extern class Opentype { 6 | /** 7 | 8 | @param {string} url - The URL of the font to load. 9 | @param {Function} callback - The callback. 10 | 11 | **/ 12 | static public function load(url:String, callback:String -> Font -> Void):Void; 13 | /** 14 | 15 | @param {string} url - The URL of the font to load. 16 | @return {opentype.Font} 17 | 18 | **/ 19 | static public function loadSync(url:String):opentype.Font; 20 | /** 21 | 22 | @param {ArrayBuffer} 23 | @return {opentype.Font} 24 | 25 | **/ 26 | static public function parse(buffer:js.html.ArrayBuffer):opentype.Font; 27 | } -------------------------------------------------------------------------------- /generate/src/opentype/Path.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | A bézier path containing a set of path commands similar to a SVG path. 5 | Paths can be drawn on a context using `draw`. 6 | @constructor 7 | 8 | **/ 9 | extern class Path { 10 | function new():Void; 11 | /** 12 | 13 | @param {number} x 14 | @param {number} y 15 | 16 | **/ 17 | function moveTo(x:Float, y:Float):Void; 18 | /** 19 | 20 | @param {number} x 21 | @param {number} y 22 | 23 | **/ 24 | function lineTo(x:Float, y:Float):Void; 25 | /** 26 | 27 | Draws cubic curve 28 | @param {number} x1 - x of control 1 29 | @param {number} y1 - y of control 1 30 | @param {number} x2 - x of control 2 31 | @param {number} y2 - y of control 2 32 | @param {number} x - x of path point 33 | @param {number} y - y of path point 34 | 35 | **/ 36 | function curveTo(x1:Float, y1:Float, x2:Float, y2:Float, x:Float, y:Float):Void; 37 | /** 38 | 39 | Draws cubic curve 40 | @param {number} x1 - x of control 1 41 | @param {number} y1 - y of control 1 42 | @param {number} x2 - x of control 2 43 | @param {number} y2 - y of control 2 44 | @param {number} x - x of path point 45 | @param {number} y - y of path point 46 | 47 | **/ 48 | function bezierCurveTo(x1:Float, y1:Float, x2:Float, y2:Float, x:Float, y:Float):Void; 49 | /** 50 | 51 | Draws quadratic curve 52 | @param {number} x1 - x of control 53 | @param {number} y1 - y of control 54 | @param {number} x - x of path point 55 | @param {number} y - y of path point 56 | 57 | **/ 58 | function quadTo(x1:Float, y1:Float, x:Float, y:Float):Void; 59 | /** 60 | 61 | Draws quadratic curve 62 | @param {number} x1 - x of control 63 | @param {number} y1 - y of control 64 | @param {number} x - x of path point 65 | @param {number} y - y of path point 66 | 67 | **/ 68 | function quadraticCurveTo(x1:Float, y1:Float, x:Float, y:Float):Void; 69 | /** 70 | 71 | Close the path 72 | 73 | **/ 74 | function close():Void; 75 | /** 76 | 77 | Closes the path 78 | 79 | **/ 80 | function closePath():Void; 81 | /** 82 | 83 | Add the given path or list of commands to the commands of this path. 84 | @param {Array} pathOrCommands - another opentype.Path, an opentype.BoundingBox, or an array of commands. 85 | 86 | **/ 87 | function extend(pathOrCommands:Array):Void; 88 | /** 89 | 90 | Calculate the bounding box of the path. 91 | @returns {opentype.BoundingBox} 92 | 93 | **/ 94 | function getBoundingBox():Void; 95 | /** 96 | 97 | @param {CanvasRenderingContext2D} ctx - A 2D drawing context. 98 | 99 | **/ 100 | function draw(ctx:js.html.CanvasRenderingContext2D):Void; 101 | /** 102 | 103 | @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values 104 | @return {string} 105 | 106 | **/ 107 | function toPathData(decimalPlaces:Float):String; 108 | /** 109 | 110 | @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values 111 | @return {string} 112 | 113 | **/ 114 | function toSVG(decimalPlaces:Float):String; 115 | /** 116 | 117 | Convert the path to a DOM element. 118 | @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values 119 | @return {SVGPathElement} 120 | 121 | **/ 122 | function toDOMElement(decimalPlaces:Float):js.html.svg.PathElement; 123 | } -------------------------------------------------------------------------------- /generate/src/opentype/ScriptList.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | @extends {opentype.Table} 5 | @param {opentype.Table} scriptListTable 6 | @constructor 7 | 8 | **/ 9 | extern class ScriptList extends opentype.Table { 10 | function new(scriptListTable:opentype.Table):Void; 11 | } -------------------------------------------------------------------------------- /generate/src/opentype/Substitution.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | @extends opentype.Layout 5 | @constructor 6 | @param {opentype.Font} 7 | 8 | **/ 9 | extern class Substitution extends opentype.Layout { 10 | function new(font:opentype.Font):Void; 11 | /** 12 | 13 | Create a default GSUB table. 14 | @return {Object} gsub - The GSUB table. 15 | 16 | **/ 17 | function createDefaultTable():haxe.DynamicAccess; 18 | /** 19 | 20 | List all single substitutions (lookup type 1) for a given script, language, and feature. 21 | @param {string} script 22 | @param {string} language 23 | @param {string} feature - 4-character feature name ('aalt', 'salt', 'ss01'...) 24 | @return {Array} substitutions - The list of substitutions. 25 | 26 | **/ 27 | function getSingle(feature:String, script:String, language:String):Array; 28 | /** 29 | 30 | List all alternates (lookup type 3) for a given script, language, and feature. 31 | @param {string} feature - 4-character feature name ('aalt', 'salt'...) 32 | @param {string} script 33 | @param {string} language 34 | @return {Array} alternates - The list of alternates 35 | 36 | **/ 37 | function getAlternates(feature:String, script:String, language:String):Array; 38 | /** 39 | 40 | List all ligatures (lookup type 4) for a given script, language, and feature. 41 | The result is an array of ligature objects like { sub: [ids], by: id } 42 | @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) 43 | @param {string} script 44 | @param {string} language 45 | @return {Array} ligatures - The list of ligatures. 46 | 47 | **/ 48 | function getLigatures(feature:String, script:String, language:String):Array; 49 | /** 50 | 51 | Add or modify a single substitution (lookup type 1) 52 | Format 2, more flexible, is always used. 53 | @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) 54 | @param {Object} substitution - { sub: id, delta: number } for format 1 or { sub: id, by: id } for format 2. 55 | @param {string} [script='DFLT'] 56 | @param {string} [language='dflt'] 57 | 58 | **/ 59 | function addSingle(feature:String, substitution:haxe.DynamicAccess, script:String, language:String):Void; 60 | /** 61 | 62 | Add or modify an alternate substitution (lookup type 1) 63 | @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) 64 | @param {Object} substitution - { sub: id, by: [ids] } 65 | @param {string} [script='DFLT'] 66 | @param {string} [language='dflt'] 67 | 68 | **/ 69 | function addAlternate(feature:String, substitution:haxe.DynamicAccess, script:String, language:String):Void; 70 | /** 71 | 72 | Add a ligature (lookup type 4) 73 | Ligatures with more components must be stored ahead of those with fewer components in order to be found 74 | @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) 75 | @param {Object} ligature - { sub: [ids], by: id } 76 | @param {string} [script='DFLT'] 77 | @param {string} [language='dflt'] 78 | 79 | **/ 80 | function addLigature(feature:String, ligature:haxe.DynamicAccess, script:String, language:String):Void; 81 | /** 82 | 83 | List all feature data for a given script and language. 84 | @param {string} feature - 4-letter feature name 85 | @param {string} [script='DFLT'] 86 | @param {string} [language='dflt'] 87 | @return {[type]} [description] 88 | @return {Array} substitutions - The list of substitutions. 89 | 90 | **/ 91 | function getFeature(feature:String, script:String, language:String):Array; 92 | /** 93 | 94 | Add a substitution to a feature for a given script and language. 95 | @param {string} feature - 4-letter feature name 96 | @param {Object} sub - the substitution to add (an Object like { sub: id or [ids], by: id or [ids] }) 97 | @param {string} [script='DFLT'] 98 | @param {string} [language='dflt'] 99 | 100 | **/ 101 | function add(feature:String, sub:haxe.DynamicAccess, script:String, language:String):Void; 102 | } -------------------------------------------------------------------------------- /generate/src/opentype/Table.hx: -------------------------------------------------------------------------------- 1 | package opentype; 2 | /** 3 | 4 | @param {string} tableName 5 | @param {Array} fields 6 | @param {Object} options 7 | @constructor 8 | 9 | **/ 10 | extern class Table { 11 | function new(tableName:String, fields:Array, options:haxe.DynamicAccess):Void; 12 | /** 13 | 14 | Encodes the table and returns an array of bytes 15 | @return {Array} 16 | 17 | **/ 18 | function encode():Array; 19 | /** 20 | 21 | Get the size of the table. 22 | @return {number} 23 | 24 | **/ 25 | function sizeOf():Float; 26 | /** 27 | 28 | @type {string} 29 | 30 | **/ 31 | var tableName : String; 32 | /** 33 | 34 | @type {Array} 35 | 36 | **/ 37 | var fields : Array; 38 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gputext", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "commander": { 8 | "version": "2.17.1", 9 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", 10 | "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", 11 | "dev": true 12 | }, 13 | "source-map": { 14 | "version": "0.6.1", 15 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 16 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 17 | "dev": true 18 | }, 19 | "typescript": { 20 | "version": "2.9.1", 21 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.1.tgz", 22 | "integrity": "sha512-h6pM2f/GDchCFlldnriOhs1QHuwbnmj6/v7499eMHqPeW4V2G0elua2eIc2nu8v2NdHV0Gm+tzX83Hr6nUFjQA==", 23 | "dev": true 24 | }, 25 | "uglify-js": { 26 | "version": "3.4.9", 27 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", 28 | "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", 29 | "dev": true, 30 | "requires": { 31 | "commander": "~2.17.1", 32 | "source-map": "~0.6.1" 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gputext", 3 | "version": "1.0.0", 4 | "description": "Lightweight engine-agnostic GPU text system (dependency free)", 5 | "main": "./dist/GPUText.js", 6 | "types": "./dist/GPUText.d.ts", 7 | "directories": { 8 | "example": "example" 9 | }, 10 | "bin": "./cli.js", 11 | "scripts": { 12 | "dist": "tsc && uglifyjs dist/GPUText.js --compress --mangle -o dist/GPUText.min.js", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/VALIS-software/GPUText.git" 18 | }, 19 | "keywords": [ 20 | "webgl", 21 | "text", 22 | "webgl text", 23 | "gpu text", 24 | "text rendering", 25 | "rendering" 26 | ], 27 | "author": "George Corney (haxiomic)", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/VALIS-software/GPUText/issues" 31 | }, 32 | "homepage": "https://github.com/VALIS-software/GPUText#readme", 33 | "devDependencies": { 34 | "typescript": "^2.9.1", 35 | "uglify-js": "^3.4.9" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/GPUText.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Provides text layout, vertex buffer generation and file parsing 3 | 4 | Dev notes: 5 | - Should have progressive layout where text can be appended to an existing layout 6 | **/ 7 | class GPUText { 8 | 9 | // y increases from top-down (like HTML/DOM coordinates) 10 | // y = 0 is set to be the font's ascender: https://i.stack.imgur.com/yjbKI.png 11 | // https://stackoverflow.com/a/50047090/4038621 12 | static layout( 13 | text: string, 14 | font: GPUTextFont, 15 | layoutOptions: { 16 | kerningEnabled?: boolean, 17 | ligaturesEnabled?: boolean, 18 | lineHeight?: number, 19 | 20 | glyphScale?: number, // scale of characters when wrapping text 21 | // doesn't affect the scale of the generated sequence or vertices 22 | } 23 | ): GlyphLayout { 24 | const opts = { 25 | glyphScale: 1.0, 26 | kerningEnabled: true, 27 | ligaturesEnabled: true, 28 | lineHeight: 1.0, 29 | ...layoutOptions 30 | } 31 | 32 | // scale text-wrap container 33 | // @! let wrapWidth /= glyphScale; 34 | 35 | // pre-allocate for each character having a glyph 36 | const sequence = new Array(text.length); 37 | let sequenceIndex = 0; 38 | 39 | const bounds = { 40 | l: 0, r: 0, 41 | t: 0, b: 0, 42 | } 43 | 44 | let x = 0; 45 | let y = 0; 46 | 47 | for (let c = 0; c < text.length; c++) { 48 | let char = text[c]; 49 | let charCode = text.charCodeAt(c); 50 | 51 | // @! layout 52 | switch (charCode) { 53 | case 0xA0: 54 | // space character that prevents an automatic line break at its position. In some formats, including HTML, it also prevents consecutive whitespace characters from collapsing into a single space. 55 | // @! todo 56 | case '\n'.charCodeAt(0): // newline 57 | y += opts.lineHeight; 58 | x = 0; 59 | continue; 60 | } 61 | 62 | if (opts.ligaturesEnabled) { 63 | // @! todo, replace char and charCode if sequence maps to a ligature 64 | } 65 | 66 | if (opts.kerningEnabled && c > 0) { 67 | let kerningKey = text[c - 1] + char; 68 | x += font.kerning[kerningKey] || 0.0; 69 | } 70 | 71 | const fontCharacter = font.characters[char]; 72 | 73 | if (fontCharacter == null) { 74 | console.warn(`Font does not contain character for "${char}" (${charCode})`); 75 | continue; 76 | } 77 | 78 | if (fontCharacter.glyph != null) { 79 | // character has a glyph 80 | 81 | // this corresponds top-left coordinate of the glyph, like hanging letters on a line 82 | sequence[sequenceIndex++] = { 83 | char: char, 84 | x: x, 85 | y: y 86 | }; 87 | 88 | // width of a character is considered to be its 'advance' 89 | // height of a character is considered to be the lineHeight 90 | bounds.r = Math.max(bounds.r, x + fontCharacter.advance); 91 | bounds.b = Math.max(bounds.b, y + opts.lineHeight); 92 | } 93 | 94 | // advance glyph position 95 | // @! layout 96 | x += fontCharacter.advance; 97 | } 98 | 99 | // trim empty entries 100 | if (sequence.length > sequenceIndex) { 101 | sequence.length = sequenceIndex; 102 | } 103 | 104 | return { 105 | font: font, 106 | sequence: sequence, 107 | bounds: bounds, 108 | glyphScale: opts.glyphScale, 109 | } 110 | } 111 | 112 | /** 113 | Generates OpenGL coordinates where y increases from bottom to top 114 | 115 | @! improve docs 116 | 117 | => float32, [p, p, u, u, u], triangles with CCW face winding 118 | **/ 119 | static generateVertexData(glyphLayout: GlyphLayout) { 120 | // memory layout details 121 | const elementSizeBytes = 4; // (float32) 122 | const positionElements = 2; 123 | const uvElements = 3; // uv.z = glyph.atlasScale 124 | const elementsPerVertex = positionElements + uvElements; 125 | const vertexSizeBytes = elementsPerVertex * elementSizeBytes; 126 | const characterVertexCount = 6; 127 | 128 | const vertexArray = new Float32Array(glyphLayout.sequence.length * characterVertexCount * elementsPerVertex); 129 | 130 | let characterOffset_vx = 0; // in terms of numbers of vertices rather than array elements 131 | 132 | for (let i = 0; i < glyphLayout.sequence.length; i++) { 133 | const item = glyphLayout.sequence[i]; 134 | const font = glyphLayout.font; 135 | const fontCharacter = font.characters[item.char]; 136 | 137 | // skip null-glyphs 138 | if (fontCharacter == null || fontCharacter.glyph == null) continue; 139 | 140 | const glyph = fontCharacter.glyph; 141 | 142 | // quad dimensions 143 | let px = item.x - glyph.offset.x; 144 | // y = 0 in the glyph corresponds to the baseline, which is font.ascender from the top of the glyph 145 | let py = -(item.y + font.ascender + glyph.offset.y); 146 | 147 | let w = glyph.atlasRect.w / glyph.atlasScale; // convert width to normalized font units 148 | let h = glyph.atlasRect.h / glyph.atlasScale; 149 | 150 | // uv 151 | // add half-text offset to map to texel centers 152 | let ux = (glyph.atlasRect.x + 0.5) / font.textureSize.w; 153 | let uy = (glyph.atlasRect.y + 0.5) / font.textureSize.h; 154 | let uw = (glyph.atlasRect.w - 1.0) / font.textureSize.w; 155 | let uh = (glyph.atlasRect.h - 1.0) / font.textureSize.h; 156 | // flip glyph uv y, this is different from flipping the glyph y _position_ 157 | uy = uy + uh; 158 | uh = -uh; 159 | // two-triangle quad with ccw face winding 160 | vertexArray.set([ 161 | px, py, ux, uy, glyph.atlasScale, // bottom left 162 | px + w, py + h, ux + uw, uy + uh, glyph.atlasScale, // top right 163 | px, py + h, ux, uy + uh, glyph.atlasScale, // top left 164 | 165 | px, py, ux, uy, glyph.atlasScale, // bottom left 166 | px + w, py, ux + uw, uy, glyph.atlasScale, // bottom right 167 | px + w, py + h, ux + uw, uy + uh, glyph.atlasScale, // top right 168 | ], characterOffset_vx * elementsPerVertex); 169 | 170 | // advance character quad in vertex array 171 | characterOffset_vx += characterVertexCount; 172 | } 173 | 174 | return { 175 | vertexArray: vertexArray, 176 | elementsPerVertex: elementsPerVertex, 177 | vertexCount: characterOffset_vx, 178 | vertexLayout: { 179 | position: { 180 | elements: positionElements, 181 | elementSizeBytes: elementSizeBytes, 182 | strideBytes: vertexSizeBytes, 183 | offsetBytes: 0, 184 | }, 185 | uv: { 186 | elements: uvElements, 187 | elementSizeBytes: elementSizeBytes, 188 | strideBytes: vertexSizeBytes, 189 | offsetBytes: positionElements * elementSizeBytes, 190 | } 191 | } 192 | } 193 | } 194 | 195 | /** 196 | * Given buffer containing a binary GPUText file, parse it and generate a GPUTextFont object 197 | * @throws string on parse errors 198 | */ 199 | static parse(buffer: ArrayBuffer): GPUTextFont { 200 | const dataView = new DataView(buffer); 201 | 202 | // read header string, expect utf-8 encoded 203 | // the end of the header string is marked by a null character 204 | let p = 0; 205 | 206 | // end of header is marked by first 0x00 byte 207 | for (; p < buffer.byteLength; p++) { 208 | let byte = dataView.getInt8(p); 209 | if (byte === 0) break; 210 | } 211 | 212 | let headerBytes = new Uint8Array(buffer, 0, p); 213 | let jsonHeader = decodeUTF8(headerBytes); 214 | 215 | // payload is starts from the first byte after the null character 216 | const payloadStart = p + 1; 217 | const littleEndian = true; 218 | 219 | const header: GPUTextFontHeader = JSON.parse(jsonHeader); 220 | 221 | // initialize GPUTextFont object 222 | let gpuTextFont: GPUTextFont = { 223 | format: header.format, 224 | version: header.version, 225 | technique: header.technique, 226 | 227 | ascender: header.ascender, 228 | descender: header.descender, 229 | typoAscender: header.typoAscender, 230 | typoDescender: header.typoDescender, 231 | lowercaseHeight: header.lowercaseHeight, 232 | metadata: header.metadata, 233 | fieldRange_px: header.fieldRange_px, 234 | 235 | characters: {}, 236 | kerning: {}, 237 | glyphBounds: undefined, 238 | 239 | textures: [], 240 | textureSize: header.textureSize, 241 | }; 242 | 243 | // parse character data payload into GPUTextFont characters map 244 | let characterDataView = new DataView(buffer, payloadStart + header.characters.start, header.characters.length); 245 | let characterBlockLength_bytes = 246 | 4 + // advance: F32 247 | 2 * 4 + // atlasRect(x, y, w, h): UI16 248 | 4 + // atlasScale: F32 249 | 4 * 2; // offset(x, y): F32 250 | for (let i = 0; i < header.charList.length; i++) { 251 | let char = header.charList[i]; 252 | let b0 = i * characterBlockLength_bytes; 253 | 254 | let glyph = { 255 | atlasRect: { 256 | x: characterDataView.getUint16(b0 + 4, littleEndian), 257 | y: characterDataView.getUint16(b0 + 6, littleEndian), 258 | w: characterDataView.getUint16(b0 + 8, littleEndian), 259 | h: characterDataView.getUint16(b0 + 10, littleEndian), 260 | }, 261 | atlasScale: characterDataView.getFloat32(b0 + 12, littleEndian), 262 | offset: { 263 | x: characterDataView.getFloat32(b0 + 16, littleEndian), 264 | y: characterDataView.getFloat32(b0 + 20, littleEndian), 265 | } 266 | } 267 | 268 | // A glyph with 0 size is considered to be a null-glyph 269 | let isNullGlyph = glyph.atlasRect.w === 0 || glyph.atlasRect.h === 0; 270 | 271 | let characterData: TextureAtlasCharacter = { 272 | advance: characterDataView.getFloat32(b0 + 0, littleEndian), 273 | glyph: isNullGlyph ? undefined : glyph 274 | } 275 | 276 | gpuTextFont.characters[char] = characterData; 277 | } 278 | 279 | // kerning payload 280 | let kerningDataView = new DataView(buffer, payloadStart + header.kerning.start, header.kerning.length); 281 | let kerningLength_bytes = 4; 282 | for (let i = 0; i < header.kerningPairs.length; i++) { 283 | let pair = header.kerningPairs[i]; 284 | let kerning = kerningDataView.getFloat32(i * kerningLength_bytes, littleEndian); 285 | gpuTextFont.kerning[pair] = kerning; 286 | } 287 | 288 | // glyph bounds payload 289 | if (header.glyphBounds != null) { 290 | gpuTextFont.glyphBounds = {}; 291 | 292 | let glyphBoundsDataView = new DataView(buffer, payloadStart + header.glyphBounds.start, header.glyphBounds.length); 293 | let glyphBoundsBlockLength_bytes = 4 * 4; 294 | for (let i = 0; i < header.charList.length; i++) { 295 | let char = header.charList[i]; 296 | let b0 = i * glyphBoundsBlockLength_bytes; 297 | // t r b l 298 | let bounds = { 299 | t: glyphBoundsDataView.getFloat32(b0 + 0, littleEndian), 300 | r: glyphBoundsDataView.getFloat32(b0 + 4, littleEndian), 301 | b: glyphBoundsDataView.getFloat32(b0 + 8, littleEndian), 302 | l: glyphBoundsDataView.getFloat32(b0 + 12, littleEndian), 303 | } 304 | 305 | gpuTextFont.glyphBounds[char] = bounds; 306 | } 307 | } 308 | 309 | // texture payload 310 | // textures may be in the payload or an external reference 311 | for (let p = 0; p < header.textures.length; p++) { 312 | let page = header.textures[p]; 313 | gpuTextFont.textures[p] = []; 314 | 315 | for (let m = 0; m < page.length; m++) { 316 | let mipmap = page[m]; 317 | 318 | if (mipmap.payloadBytes != null) { 319 | // convert payload's image bytes into a HTMLImageElement object 320 | let imageBufferView = new Uint8Array(buffer, payloadStart + mipmap.payloadBytes.start, mipmap.payloadBytes.length); 321 | let imageBlob = new Blob([imageBufferView], { type: "image/png" }); 322 | let image = new Image(); 323 | image.src = URL.createObjectURL(imageBlob); 324 | gpuTextFont.textures[p][m] = image; 325 | } else if (mipmap.localPath != null) { 326 | // payload contains no image bytes; the image is store externally, pass on the path 327 | gpuTextFont.textures[p][m] = { 328 | localPath: mipmap.localPath 329 | }; 330 | } 331 | } 332 | } 333 | 334 | return gpuTextFont; 335 | } 336 | 337 | } 338 | 339 | // credits github user pascaldekloe 340 | // https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330 341 | function decodeUTF8(bytes: Uint8Array) { 342 | let i = 0, s = ''; 343 | while (i < bytes.length) { 344 | let c = bytes[i++]; 345 | if (c > 127) { 346 | if (c > 191 && c < 224) { 347 | if (i >= bytes.length) 348 | throw new Error('UTF-8 decode: incomplete 2-byte sequence'); 349 | c = (c & 31) << 6 | bytes[i++] & 63; 350 | } else if (c > 223 && c < 240) { 351 | if (i + 1 >= bytes.length) 352 | throw new Error('UTF-8 decode: incomplete 3-byte sequence'); 353 | c = (c & 15) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63; 354 | } else if (c > 239 && c < 248) { 355 | if (i + 2 >= bytes.length) 356 | throw new Error('UTF-8 decode: incomplete 4-byte sequence'); 357 | c = (c & 7) << 18 | (bytes[i++] & 63) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63; 358 | } else throw new Error('UTF-8 decode: unknown multibyte start 0x' + c.toString(16) + ' at index ' + (i - 1)); 359 | } 360 | if (c <= 0xffff) s += String.fromCharCode(c); 361 | else if (c <= 0x10ffff) { 362 | c -= 0x10000; 363 | s += String.fromCharCode(c >> 10 | 0xd800) 364 | s += String.fromCharCode(c & 0x3FF | 0xdc00) 365 | } else throw new Error('UTF-8 decode: code point 0x' + c.toString(16) + ' exceeds UTF-16 reach'); 366 | } 367 | return s; 368 | } 369 | 370 | export interface GPUTextFont extends GPUTextFontBase { 371 | characters: { [character: string]: TextureAtlasCharacter | null }, 372 | kerning: { [characterPair: string]: number }, 373 | // glyph bounding boxes in normalized font units 374 | // not guaranteed to be included in the font file 375 | glyphBounds?: { [character: string]: { l: number, b: number, r: number, t: number } }, 376 | textures: Array>, 377 | } 378 | 379 | export interface GlyphLayout { 380 | font: GPUTextFont, 381 | sequence: Array<{ 382 | char: string, 383 | x: number, 384 | y: number 385 | }>, 386 | bounds: { l: number, r: number, t: number, b: number }, 387 | glyphScale: number, 388 | } 389 | 390 | export interface TextureAtlasGlyph { 391 | // location of glyph within the text atlas, in units of pixels 392 | atlasRect: { x: number, y: number, w: number, h: number }, 393 | atlasScale: number, // (normalized font units) * atlasScale = (pixels in texture atlas) 394 | 395 | // the offset within the atlasRect in normalized font units 396 | offset: { x: number, y: number }, 397 | } 398 | 399 | export interface TextureAtlasCharacter { 400 | // the distance from the glyph's x = 0 coordinate to the x = 0 coordinate of the next glyph, in normalized font units 401 | advance: number, 402 | glyph?: TextureAtlasGlyph, 403 | } 404 | 405 | export interface ResourceReference { 406 | // range of bytes within the file's binary payload 407 | payloadBytes?: { 408 | start: number, 409 | length: number 410 | }, 411 | 412 | // path relative to font file 413 | // an implementation should not allow resource paths to be in directories _above_ the font file 414 | localPath?: string, 415 | } 416 | 417 | type GPUTextFormat = 'TextureAtlasFontJson' | 'TextureAtlasFontBinary'; 418 | type GPUTextTechnique = 'msdf' | 'sdf' | 'bitmap'; 419 | 420 | interface GPUTextFontMetadata { 421 | family: string, 422 | subfamily: string, 423 | version: string, 424 | postScriptName: string, 425 | 426 | copyright: string, 427 | trademark: string, 428 | manufacturer: string, 429 | manufacturerURL: string, 430 | designerURL: string, 431 | license: string, 432 | licenseURL: string, 433 | 434 | // original authoring height 435 | // this can be used to reproduce the unnormalized source values of the font 436 | height_funits: number, 437 | funitsPerEm: number, 438 | } 439 | 440 | interface GPUTextFontBase { 441 | format: GPUTextFormat, 442 | version: number, 443 | 444 | technique: GPUTextTechnique, 445 | 446 | textureSize: { 447 | w: number, 448 | h: number, 449 | }, 450 | 451 | // the following are in normalized font units where (ascender - descender) = 1.0 452 | ascender: number, 453 | descender: number, 454 | typoAscender: number, 455 | typoDescender: number, 456 | lowercaseHeight: number, 457 | 458 | metadata: GPUTextFontMetadata, 459 | 460 | fieldRange_px: number, 461 | } 462 | 463 | // binary text file JSON header 464 | interface GPUTextFontHeader extends GPUTextFontBase { 465 | charList: Array, 466 | kerningPairs: Array, 467 | characters: { 468 | start: number, 469 | length: number, 470 | }, 471 | kerning: { 472 | start: number, 473 | length: number, 474 | }, 475 | glyphBounds?: { 476 | start: number, 477 | length: number, 478 | }, 479 | textures: Array>, 480 | } 481 | 482 | export default GPUText; 483 | 484 | if (typeof window !== 'undefined') { 485 | // expose GPUText on the window object 486 | (window as any).GPUText = GPUText; 487 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "declaration": true, 7 | "strict": true, 8 | }, 9 | "include": [ "./src/**/*" ] 10 | } --------------------------------------------------------------------------------