view src/semicongine/text.nim @ 363:451b7ccfe722

improve 2D collision, add some vector functionality, allow shaders/pipelines to be ordered for deterministic rendering order
author Sam <sam@basx.dev>
date Sun, 01 Oct 2023 20:53:35 +0700
parents 61c5d5fe9d93
children eef6cc3e1104
line wrap: on
line source

import std/tables
import std/unicode
import std/strformat

import ./core
import ./mesh
import ./vulkan/shader

const SHADER_ATTRIB_PREFIX = "semicon_text_"
type
  TextAlignment = enum
    Left
    Center
    Right
  Textbox* = object
    maxLen*: int
    text: seq[Rune]
    dirty: bool
    alignment*: TextAlignment
    font*: Font
    mesh*: Mesh

const
  TRANSFORM_ATTRIB = "transform"
  POSITION_ATTRIB = SHADER_ATTRIB_PREFIX & "position"
  UV_ATTRIB = SHADER_ATTRIB_PREFIX & "uv"
  TEXT_MATERIAL* = "default-text"
  TEXT_SHADER* = createShaderConfiguration(
    inputs=[
      attr[Mat4](TRANSFORM_ATTRIB, memoryPerformanceHint=PreferFastWrite, perInstance=true),
      attr[Vec3f](POSITION_ATTRIB, memoryPerformanceHint=PreferFastWrite),
      attr[Vec2f](UV_ATTRIB, memoryPerformanceHint=PreferFastWrite),
    ],
    intermediates=[attr[Vec2f]("uvFrag")],
    outputs=[attr[Vec4f]("color")],
    samplers=[attr[Texture]("fontAtlas")],
    vertexCode= &"""gl_Position = vec4({POSITION_ATTRIB}, 1.0) * {TRANSFORM_ATTRIB}; uvFrag = {UV_ATTRIB};""",
    fragmentCode= &"""color = texture(fontAtlas, uvFrag);""",
  )

proc updateMesh(textbox: var Textbox) =

  # pre-calculate text-width
  var width = 0'f32
  for i in 0 ..< min(textbox.text.len, textbox.maxLen):
    width += textbox.font.glyphs[textbox.text[i]].advance
    if i < textbox.text.len - 1:
      width += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])]

  let centerX = width / 2
  let centerY = textbox.font.maxHeight / 2

  var offsetX = 0'f32
  for i in 0 ..< textbox.maxLen:
    let vertexOffset = i * 4
    if i < textbox.text.len:
      let
        glyph = textbox.font.glyphs[textbox.text[i]]
        left = offsetX + glyph.leftOffset
        right = offsetX + glyph.leftOffset + glyph.dimension.x
        top = glyph.topOffset
        bottom = glyph.topOffset + glyph.dimension.y

      textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f(left - centerX, bottom + centerY)
      textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f(left - centerX, top + centerY)
      textbox.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f(right - centerX, top + centerY)
      textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f(right - centerX, bottom + centerY)

      textbox.mesh[UV_ATTRIB, vertexOffset + 0] = glyph.uvs[0]
      textbox.mesh[UV_ATTRIB, vertexOffset + 1] = glyph.uvs[1]
      textbox.mesh[UV_ATTRIB, vertexOffset + 2] = glyph.uvs[2]
      textbox.mesh[UV_ATTRIB, vertexOffset + 3] = glyph.uvs[3]

      offsetX += glyph.advance
      if i < textbox.text.len - 1:
        offsetX += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])]
    else:
      textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f()
      textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f()
      textbox.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f()
      textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f()


func text*(textbox: Textbox): seq[Rune] =
  textbox.text

proc `text=`*(textbox: var Textbox, text: seq[Rune]) =
  textbox.text = text
  textbox.updateMesh()
proc `text=`*(textbox: var Textbox, text: string) =
  `text=`(textbox, text.toRunes)

proc initTextbox*(maxLen: int, font: Font, text="".toRunes): Textbox =
  var
    positions = newSeq[Vec3f](int(maxLen * 4))
    indices: seq[array[3, uint16]]
    uvs = newSeq[Vec2f](int(maxLen * 4))
  for i in 0 ..< maxLen:
    let offset = i * 4
    indices.add [
      [uint16(offset + 0), uint16(offset + 1), uint16(offset + 2)],
      [uint16(offset + 2), uint16(offset + 3), uint16(offset + 0)],
    ]

  result = Textbox(maxLen: maxLen, text: text, font: font, dirty: true)
  result.mesh = newMesh(positions = positions, indices = indices, uvs = uvs)
  result.mesh[].renameAttribute("position", POSITION_ATTRIB)
  result.mesh[].renameAttribute("uv", UV_ATTRIB)
  result.mesh.materials = @[Material(
    name: TEXT_MATERIAL,
    textures: {"fontAtlas": font.fontAtlas}.toTable,
  )]

  result.updateMesh()

proc initTextbox*(maxLen: int, font: Font, text=""): Textbox =
  initTextbox(maxLen=maxLen, font=font, text=text.toRunes)