package kha; import haxe.ds.Vector; import haxe.io.Bytes; import kha.graphics2.truetype.StbTruetype; import kha.graphics4.TextureFormat; import kha.graphics4.Usage; class AlignedQuad { public function new() {} // top-left public var x0: Float; public var y0: Float; public var s0: Float; public var t0: Float; // bottom-right public var x1: Float; public var y1: Float; public var s1: Float; public var t1: Float; public var xadvance: Float; } class KravurImage { var mySize: Float; var chars: Vector; var texture: Image; public var width: Int; public var height: Int; var baseline: Float; public static var charBlocks: Array; public function new(size: Int, ascent: Int, descent: Int, lineGap: Int, width: Int, height: Int, chars: Vector, pixels: Blob) { mySize = size; this.width = width; this.height = height; this.chars = chars; baseline = ascent; for (char in chars) { char.yoff += baseline; } texture = Image.create(width, height, TextureFormat.L8); var bytes = texture.lock(); var pos: Int = 0; for (y in 0...height) for (x in 0...width) { bytes.set(pos, pixels.readU8(pos)); ++pos; } texture.unlock(); } public function getTexture(): Image { return texture; } public function getBakedQuad(q: AlignedQuad, char_index: Int, xpos: Float, ypos: Float): AlignedQuad { if (char_index >= chars.length) return null; var ipw: Float = 1.0 / width; var iph: Float = 1.0 / height; var b = chars[char_index]; if (b == null) return null; var round_x: Int = Math.round(xpos + b.xoff); var round_y: Int = Math.round(ypos + b.yoff); q.x0 = round_x; q.y0 = round_y; q.x1 = round_x + b.x1 - b.x0; q.y1 = round_y + b.y1 - b.y0; q.s0 = b.x0 * ipw; q.t0 = b.y0 * iph; q.s1 = b.x1 * ipw; q.t1 = b.y1 * iph; q.xadvance = b.xadvance; return q; } function getCharWidth(charIndex: Int): Float { if (chars.length == 0) return 0; var offset = charBlocks[0]; if (charIndex < offset) return chars[0].xadvance; for (i in 1...Std.int(charBlocks.length / 2)) { var prevEnd = charBlocks[i * 2 - 1]; var start = charBlocks[i * 2]; if (charIndex > start - 1) offset += start - 1 - prevEnd; } if (charIndex - offset >= chars.length) return chars[0].xadvance; return chars[charIndex - offset].xadvance; } public function getHeight(): Float { return mySize; } public function stringWidth(str: String): Float { var width: Float = 0; for (c in 0...str.length) { width += getCharWidth(str.charCodeAt(c)); } return width; } public function charactersWidth(characters: Array, start: Int, length: Int): Float { var width: Float = 0; for (i in start...start + length) { width += getCharWidth(characters[i]); } return width; } public function getBaselinePosition(): Float { return baseline; } } class Kravur implements Resource { // Do not put static data in Kravur because it is overwritten // when is used - but it's still used by the overwriting class. var oldGlyphs: Array; var blob: Blob; var images: Map = new Map(); var fontIndex: Int; public function new(blob: Blob, fontIndex: Int = 0) { this.blob = blob; this.fontIndex = fontIndex; } public static function fromBytes(bytes: Bytes, fontIndex: Int = 0): Kravur { return new Kravur(Blob.fromBytes(bytes), fontIndex); } public function _get(fontSize: Int): KravurImage { var glyphs = kha.graphics2.Graphics.fontGlyphs; if (glyphs != oldGlyphs) { oldGlyphs = glyphs; // save first/last chars of sequences KravurImage.charBlocks = [glyphs[0]]; var nextChar = KravurImage.charBlocks[0] + 1; for (i in 1...glyphs.length) { if (glyphs[i] != nextChar) { KravurImage.charBlocks.push(glyphs[i - 1]); KravurImage.charBlocks.push(glyphs[i]); nextChar = glyphs[i] + 1; } else nextChar++; } KravurImage.charBlocks.push(glyphs[glyphs.length - 1]); } var imageIndex = fontIndex * 10000000 + fontSize * 10000 + glyphs.length; if (!images.exists(imageIndex)) { var width: Int = 64; var height: Int = 32; var baked = new Vector(glyphs.length); for (i in 0...baked.length) { baked[i] = new Stbtt_bakedchar(); } var pixels: Blob = null; var offset = StbTruetype.stbtt_GetFontOffsetForIndex(blob, fontIndex); if (offset == -1) { offset = StbTruetype.stbtt_GetFontOffsetForIndex(blob, 0); } var status: Int = -1; while (status <= 0) { if (height < width) height *= 2; else width *= 2; pixels = Blob.alloc(width * height); status = StbTruetype.stbtt_BakeFontBitmap(blob, offset, fontSize, pixels, width, height, glyphs, baked); } // TODO: Scale pixels down if they exceed the supported texture size var info = new Stbtt_fontinfo(); StbTruetype.stbtt_InitFont(info, blob, offset); var metrics = StbTruetype.stbtt_GetFontVMetrics(info); var scale = StbTruetype.stbtt_ScaleForPixelHeight(info, fontSize); var ascent = Math.round(metrics.ascent * scale); // equals baseline var descent = Math.round(metrics.descent * scale); var lineGap = Math.round(metrics.lineGap * scale); var image = new KravurImage(Std.int(fontSize), ascent, descent, lineGap, width, height, baked, pixels); images[imageIndex] = image; return image; } return images[imageIndex]; } public function height(fontSize: Int): Float { return _get(fontSize).getHeight(); } public function width(fontSize: Int, str: String): Float { return _get(fontSize).stringWidth(str); } public function widthOfCharacters(fontSize: Int, characters: Array, start: Int, length: Int): Float { return _get(fontSize).charactersWidth(characters, start, length); } public function baseline(fontSize: Int): Float { return _get(fontSize).getBaselinePosition(); } public function setFontIndex(fontIndex: Int): Void { this.fontIndex = fontIndex; } public function unload(): Void { blob = null; images = null; } }