funfriend/src/fontman.cr

180 lines
5.3 KiB
Crystal

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