3dexperiments/src/engine/gui/font.js

467 lines
14 KiB
JavaScript

import Resource from '../resource'
import Screen from '../screen'
import { Mesh } from '../mesh'
import { Texture } from '../mesh/material'
import { Node2D } from './'
import { mat4 } from 'gl-matrix'
const PAD_TOP = 0
const PAD_LEFT = 1
const PAD_BOTTOM = 2
const PAD_RIGHT = 3
const DESIRED_PADDING = 8
const SPLITTER = ' '
const NUMBER_SEPARATOR = ','
const LINE_HEIGHT = 0.03
const SPACE_ASCII = 32
const NL_ASCII = 10
class Character {
constructor (id, xTexCoord, yTexCoord, xTexSize, yTexSize,
xOffset, yOffset, sizeX, sizeY, xAdvance) {
this.id = id
this.xTexCoord = xTexCoord
this.yTexCoord = yTexCoord
this.xTexSize = xTexSize
this.yTexSize = yTexSize
this.xOffset = xOffset
this.yOffset = yOffset
this.sizeX = sizeX
this.sizeY = sizeY
this.xMaxTexCoord = xTexSize + xTexCoord
this.yMaxTexCoord = yTexSize + yTexCoord
this.xAdvance = xAdvance
}
}
class Word {
constructor (fontSize) {
this.fontSize = fontSize
this.characters = []
this.width = 0
}
addCharacter (char) {
if (!char) return
this.characters.push(char)
this.width += char.xAdvance * this.fontSize
}
}
class Line {
constructor (spaceWidth, fontSize, maxLength) {
this.spaceSize = spaceWidth * fontSize
this.maxLength = maxLength
this.words = []
this.lineLength = 0
}
attemptToAddWord (word) {
let additionalLength = word.width
additionalLength += this.words.length ? this.spaceSize : 0
if (this.lineLength + additionalLength <= this.maxLength) {
this.words.push(word)
this.lineLength += additionalLength
return true
}
return false
}
}
class FontFile {
constructor (name) {
this.name = name
this.metadata = {}
}
getValue (vals, key) {
return parseInt(vals[key])
}
getValues (vals, key) {
let nums = vals[key].split(NUMBER_SEPARATOR)
let result = []
for (let i in nums) {
result.push(parseInt(nums[i]))
}
return result
}
// Parse a .fnt file
readValues (data) {
let lines = data.split('\n')
for (let i in lines) {
let line = lines[i]
let lineSplit = line.split(SPLITTER)
let lineValues = {}
for (let j in lineSplit) {
let valuePairs = lineSplit[j].split('=')
if (valuePairs.length === 2) {
lineValues[valuePairs[0]] = valuePairs[1]
}
}
if (lineSplit[0] === 'info') {
this.padding = this.getValues(lineValues, 'padding')
this.paddingWidth = this.padding[PAD_LEFT] + this.padding[PAD_RIGHT]
this.paddingHeight = this.padding[PAD_TOP] + this.padding[PAD_BOTTOM]
} else if (lineSplit[0] === 'common') {
let lineHeightPixels = this.getValue(lineValues, 'lineHeight') - this.paddingHeight
this.vertPerPixelSize = LINE_HEIGHT / lineHeightPixels
this.horizPixelSize = this.vertPerPixelSize / Screen.aspectRatio
this.scaleWidth = this.getValue(lineValues, 'scaleW')
} else if (lineSplit[0] === 'char') {
let c = this.loadCharacter(lineValues, this.scaleWidth)
if (c) this.metadata[c.id] = c
}
}
}
loadCharacter (values, imageSize) {
let id = this.getValue(values, 'id')
if (id === SPACE_ASCII) {
this.spaceWidth = (this.getValue(values, 'xadvance') - this.paddingWidth) * this.horizPixelSize
return null
}
let xTex = (this.getValue(values, 'x') + (this.padding[PAD_LEFT] - DESIRED_PADDING)) / imageSize
let yTex = (this.getValue(values, 'y') + (this.padding[PAD_TOP] - DESIRED_PADDING)) / imageSize
let width = this.getValue(values, 'width') - (this.paddingWidth - (2 * DESIRED_PADDING))
let height = this.getValue(values, 'height') - ((this.paddingHeight) - (2 * DESIRED_PADDING))
let quadWidth = width * this.horizPixelSize
let quadHeight = height * this.vertPerPixelSize
let xTexSize = width / imageSize
let yTexSize = height / imageSize
let xOff = (this.getValue(values, 'xoffset') + this.padding[PAD_LEFT] - DESIRED_PADDING) * this.horizPixelSize
let yOff = (this.getValue(values, 'yoffset') + (this.padding[PAD_TOP] - DESIRED_PADDING)) * this.vertPerPixelSize
let xAdvance = (this.getValue(values, 'xadvance') - this.paddingWidth) * this.horizPixelSize
return new Character(id, xTex, yTex, xTexSize, yTexSize, xOff, yOff, quadWidth, quadHeight, xAdvance)
}
getCharacter (ascii) {
return this.metadata[ascii]
}
static async fromFile (fontName) {
let load = await Resource.GET('/assets/fonts/' + fontName + '.fnt')
let file = new FontFile(fontName)
file.readValues(load)
return file
}
}
class Font {
constructor (name, metadata) {
this.name = name
this.metadata = metadata
this.texture = null
}
// Load font data from a .fnt file and create a Font object with it
static async fromFile (name) {
let meta = await FontFile.fromFile(name)
return new Font(name, meta)
}
// Load font texture
async loadTextures (gl) {
this.texture = await Texture.createTexture2D(gl, await Resource.loadImage('/assets/fonts/' + this.name + '.png'), false, gl.LINEAR)
}
// Create a renderable mesh for a text
createTextMesh (gl, text) {
let lines = this.createStructure(text)
let data = this.createQuadVertices(text, lines)
return Mesh.constructFromVerticesUVs(gl, data.vertices, data.textureCoords)
}
// Create a structure of lines, words and characters in order to generate the vertices properly
createStructure (text) {
let chars = text.asCharacters
let lines = []
let currentLine = new Line(this.metadata.spaceWidth, text.fontSize, text.lineLength)
let currentWord = new Word(text.fontSize)
for (let c in chars) {
let ascii = parseInt(chars[c])
if (ascii === SPACE_ASCII) {
let added = currentLine.attemptToAddWord(currentWord)
if (!added) {
lines.push(currentLine)
currentLine = new Line(this.metadata.spaceWidth, text.fontSize, text.lineLength)
currentLine.attemptToAddWord(currentWord)
}
currentWord = new Word(text.fontSize)
continue
}
if (ascii === NL_ASCII) {
let added = currentLine.attemptToAddWord(currentWord)
if (!added) {
lines.push(currentLine)
currentLine = new Line(this.metadata.spaceWidth, text.fontSize, text.lineLength)
currentLine.attemptToAddWord(currentWord)
}
lines.push(currentLine)
currentLine = new Line(this.metadata.spaceWidth, text.fontSize, text.lineLength)
currentWord = new Word(text.fontSize)
continue
}
let character = this.metadata.getCharacter(ascii)
currentWord.addCharacter(character)
}
this.completeStructure(lines, currentLine, currentWord, text)
return lines
}
// Add final word
completeStructure (lines, currentLine, currentWord, text) {
let added = currentLine.attemptToAddWord(currentWord)
if (!added) {
lines.push(currentLine)
currentLine = new Line(this.metadata.spaceWidth, text.fontSize, text.lineLength)
currentLine.attemptToAddWord(currentWord)
}
lines.push(currentLine)
return lines
}
// Create text vertices
createQuadVertices (text, lines) {
text.lines = lines.length
let cursorX = 0
let cursorY = 0
let vertices = []
let textureCoords = []
for (let i in lines) {
let line = lines[i]
if (text.centered) {
cursorX = (line.maxLength - line.lineLength) / 2
}
for (let j in line.words) {
let word = line.words[j]
for (let k in word.characters) {
let letter = word.characters[k]
this.addVerticesForCharacter(cursorX, cursorY, letter, text.fontSize, vertices)
this.addTexCoords(textureCoords, letter.xTexCoord, letter.yTexCoord, letter.xMaxTexCoord, letter.yMaxTexCoord)
cursorX += letter.xAdvance * text.fontSize
}
cursorX += this.metadata.spaceWidth * text.fontSize
}
cursorX = 0
cursorY += LINE_HEIGHT * text.fontSize
}
return { vertices, textureCoords }
}
// Create text vertex coordinates for a specific character
addVerticesForCharacter (cursorX, cursorY, character, fontSize, vertices) {
let x = cursorX + (character.xOffset * fontSize)
let y = cursorY + (character.yOffset * fontSize)
let maxX = x + (character.sizeX * fontSize)
let maxY = y + (character.sizeY * fontSize)
let properX = (2 * x) - 1
let properY = (-2 * y) + 1
let properMaxX = (2 * maxX) - 1
let properMaxY = (-2 * maxY) + 1
this.addVertices(vertices, properX, properY, properMaxX, properMaxY)
}
// Create text vertex coordinates
addVertices (vertices, x, y, maxX, maxY) {
vertices.push(x)
vertices.push(y)
vertices.push(x)
vertices.push(maxY)
vertices.push(maxX)
vertices.push(maxY)
vertices.push(maxX)
vertices.push(maxY)
vertices.push(maxX)
vertices.push(y)
vertices.push(x)
vertices.push(y)
}
// Create text texture coordinates
addTexCoords (texCoords, x, y, maxX, maxY) {
texCoords.push(x)
texCoords.push(y)
texCoords.push(x)
texCoords.push(maxY)
texCoords.push(maxX)
texCoords.push(maxY)
texCoords.push(maxX)
texCoords.push(maxY)
texCoords.push(maxX)
texCoords.push(y)
texCoords.push(x)
texCoords.push(y)
}
}
class GUIText extends Node2D {
constructor (text, font, fontSize, pos, size, centered = false) {
super(pos, size)
this.text = text
this.fontSize = fontSize
this.font = font
this.centered = centered
this.color = [0.0, 0.0, 0.0]
this.width = 0.45
this.edge = 0.1
this.outline = {
width: 0.0,
edge: 0.5,
color: [0.0, 0.0, 0.0],
offset: [0.0, 0.0]
}
}
// Do not scale the transform like we do with regular GUIs
updateTransform () {
let matrix = mat4.create()
mat4.translate(matrix, matrix, [this.pos[0], this.pos[2], 0.0])
if (this.rotation !== 0.0) {
mat4.rotate(matrix, matrix, this.rotation * Math.PI / 180, [0.0, 0.0, 1.0])
}
// Add parent's transform to this
if (this.parent) {
mat4.mul(matrix, this.parent.transform, matrix)
}
// Set the matrix
this.transform = matrix
// Update children's transforms
for (let i in this.children) {
let child = this.children[i]
if (!(child instanceof Node2D)) continue
child.updateTransform()
}
}
setText (str) {
this.text = str
this.mesh = null
}
createMesh (gl) {
this.mesh = this.font.createTextMesh(gl, this)
}
get lineLength () {
return this.size[2]
}
// Return all characters as their ascii code
get asCharacters () {
let chars = []
for (let i = 0; i < this.text.length; i++) {
chars.push(this.text.charCodeAt(i))
}
return chars
}
draw (gl, shader) {
// Let only the font shader be used to render text
if (shader.name !== 'font') return super.draw(gl, shader)
const transformLocation = shader.getUniformLocation(gl, 'uTransformation')
const cLocation = shader.getUniformLocation(gl, 'uColor')
const wLocation = shader.getUniformLocation(gl, 'uWidth')
const eLocation = shader.getUniformLocation(gl, 'uEdge')
const owLocation = shader.getUniformLocation(gl, 'uOutlineWidth')
const oeLocation = shader.getUniformLocation(gl, 'uOutlineEdge')
const ooLocation = shader.getUniformLocation(gl, 'uOutlineOffset')
const ocLocation = shader.getUniformLocation(gl, 'uOutlineColor')
gl.uniformMatrix4fv(transformLocation, false, this.transform)
gl.uniform1f(wLocation, this.width)
gl.uniform1f(eLocation, this.edge)
gl.uniform1f(owLocation, this.outline.width)
gl.uniform1f(oeLocation, this.outline.edge)
gl.uniform2fv(ooLocation, this.outline.offset)
gl.uniform3fv(cLocation, this.color)
gl.uniform3fv(ocLocation, this.outline.color)
if (!this.mesh) this.createMesh(gl)
this.mesh.prepare(gl, shader)
this.mesh.draw(gl, shader)
this.mesh.postdraw(gl, shader)
}
}
class FontRenderer {
discoverTextNodes (nodes) {
let textNodes = []
for (let i in nodes) {
let node = nodes[i]
if (!(node instanceof GUIText)) {
if (node.children.length) {
textNodes = textNodes.concat(this.discoverTextNodes(node.children))
}
continue
}
textNodes.push(node)
}
return textNodes
}
draw (gl, cam, nodes) {
// Discover all nodes in the array that are texts
let fontPairs = {}
let textNodes = this.discoverTextNodes(nodes)
for (let i in textNodes) {
let node = textNodes[i]
if (!this.fonts) {
this.fonts = {}
}
if (!this.fonts[node.font.name]) {
this.fonts[node.font.name] = node.font
}
if (fontPairs[node.font.name]) {
fontPairs[node.font.name].push(node)
} else {
fontPairs[node.font.name] = [node]
}
}
// Start rendering individual text arrays
this.shader.use(gl)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.disable(gl.DEPTH_TEST)
for (let i in fontPairs) {
let texts = fontPairs[i]
let font = this.fonts[i]
// Set font's map as the texture
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(font.texture.type, font.texture.id)
// Draw all texts
for (let j in texts) {
let text = texts[j]
text.draw(gl, this.shader)
}
}
gl.enable(gl.DEPTH_TEST)
gl.disable(gl.BLEND)
}
async initialize (game) {
this.shader = await game.shaders.createShaderFromFiles(game.gl, 'font', false)
}
}
export { FontRenderer, GUIText, Font }