module Funfriend::FontMan extend self private def quoted_space_split(string : String) accum = [] of String chars = [] of Char quoted = false string.each_char do |char| if !quoted && char == ' ' if (chars.count &.!= ' ') > 0 accum << chars.join end chars = [] of Char else if char == '"' quoted = !quoted end chars << char end end if (chars.count &.!= ' ') > 0 accum << chars.join end return accum end alias BMCommon = NamedTuple(line_height: Int32, base: Int32, scale_w: Int32, scale_h: Int32) alias BMChar = NamedTuple(id: Int32, x: Int32, y: Int32, width: Int32, height: Int32, xoffset: Int32, yoffset: Int32, xadvance: Int32, letter: Char) alias BMKerning = NamedTuple(first: Int32, second: Int32, amount: Int32) alias BMSheet = NamedTuple(common: BMCommon, chars: Array(BMChar), kernings: Array(BMKerning)) def parse_bm(data : String) : BMSheet common = nil chars = [] of BMChar kernings = [] of BMKerning data.each_line do |line| words = line.split key = words[0]? args_array = quoted_space_split(words[1..].join(" ")).map &.split('=', 2) args = Hash.zip(args_array.map &.[0], args_array.map &.[1]) case key when "common" common = { line_height: args["lineHeight"].to_i, base: args["base"].to_i, scale_w: args["scaleW"].to_i, scale_h: args["scaleH"].to_i } when "char" chars << { id: args["id"].to_i, x: args["x"].to_i, y: args["y"].to_i, width: args["width"].to_i, height: args["height"].to_i, xoffset: args["xoffset"].to_i, yoffset: args["yoffset"].to_i, xadvance: args["xadvance"].to_i, letter: args["letter"].lchop('"').rchop('"')[0], } when "kerning" kernings << { first: args["first"].to_i, second: args["second"].to_i, amount: args["amount"].to_i, } end end return { common: common.not_nil!, chars: chars, kernings: kernings, } end # quick shorthand def text_width(text : String, sheet : BMSheet) text.chars.reduce 0 do |width, char| bm_char = sheet[:chars].find! { |c| c[:letter] == char } width + bm_char[:xadvance] end end # TODO: kerning def position_text(text : String, sheet : BMSheet) positions = [] of NamedTuple(x: Int32, y: Int32, char: BMChar) x = 0 text.each_char do |char| bm_char = sheet[:chars].find! { |c| c[:letter] == char } positions << { x: x + bm_char[:xoffset], y: sheet[:common][:base] - bm_char[:height] - bm_char[:yoffset] + (sheet[:common][:line_height] - sheet[:common][:base]), char: bm_char, } x = x + bm_char[:xadvance] end return { width: x, height: sheet[:common][:line_height], positions: positions, } end def get_letter_crop(char : BMChar, sheet : BMSheet) x = (char[:x] / sheet[:common][:scale_w]).to_f32 y = (char[:y] / sheet[:common][:scale_h]).to_f32 w = (char[:width] / sheet[:common][:scale_w]).to_f32 h = (char[:height] / sheet[:common][:scale_h]).to_f32 return {x, y, w, h} end def get_text_mesh(text : String, sheet : BMSheet, offset_x : Int32, offset_y : Int32, width : Int32, height : Int32) vertices = [] of Float32 indices = [] of Int32 position_data = FontMan.position_text(text, sheet) i = 0 position_data[:positions].each do |letter| char = letter[:char] x, y, w, h = get_letter_crop(char, sheet) pos_x = ((letter[:x] + offset_x) / width ).to_f32 * 2 - 1 pos_w = (char[:width] / width ).to_f32 * 2 pos_y = ((letter[:y] + offset_y) / height).to_f32 * 2 - 1 pos_h = (char[:height] / height).to_f32 * 2 vertices += [ # ------ positions ------- texture coordinates pos_x + pos_w, pos_y + pos_h, 0.0f32, x + w, y, # top right pos_x + pos_w, pos_y, 0.0f32, x + w, y + h, # bottom right pos_x, pos_y, 0.0f32, x, y + h, # bottom left pos_x, pos_y + pos_h, 0.0f32, x, y, # top left ] # Indices used for indexed draw calls. Each is used as an index into the four vertices. indices += [ 0, 1, 3, # first triangle 1, 2, 3 # second triangle ].map &.+ (i * 4) i = i + 1 end vertex_buffer = Buffer.new element_buffer = Buffer.new vertex_array = VertexArray.new vertex_array.bind do vertex_buffer.bind(Buffer::Target::Array) do |buffer, target| GL.buffer_data_array(target, vertices, Buffer::UsageHint::StaticDraw) vertex_array.define_attributes do |va| va.attribute(3, DataType::Float, false) va.attribute(2, DataType::Float, false) end end # bind the element buffer to GL_ELEMENT_ARRAY_BUFFER element_buffer.bind(Buffer::Target::ElementArray) # Send index data to the GPU. GL.buffer_data_array(Buffer::Target::ElementArray, indices, Buffer::UsageHint::StaticDraw) end # unbind the element buffer. element_buffer.unbind return {vertex_array, vertex_buffer} end end