467 lines
14 KiB
JavaScript
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 }
|