830 lines
25 KiB
Go
830 lines
25 KiB
Go
package fontManager
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/go-gl/gl/v3.3-core/gl"
|
|
"github.com/go-gl/mathgl/mgl32"
|
|
"github.com/veandco/go-sdl2/sdl"
|
|
"github.com/veandco/go-sdl2/ttf"
|
|
"rhemawavetech.com/rhemawave/internal/db"
|
|
"rhemawavetech.com/rhemawave/internal/graphics"
|
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
|
)
|
|
|
|
// FontManager handles the loading and management of fonts.
|
|
type FontManager struct {
|
|
Fonts map[string]*FontEntry
|
|
|
|
// Reusable GPU objects for string texture rendering
|
|
stringFBO uint32
|
|
stringVAO uint32
|
|
stringVBO uint32
|
|
}
|
|
|
|
var (
|
|
fontManagerInstance *FontManager
|
|
fontManagerOnce sync.Once
|
|
initErr error // Package-level variable to capture error from within sync.Once
|
|
)
|
|
|
|
// InitFontManager initializes the FontManager singleton.
|
|
// It must be called once at startup after ShaderManager has been initialized.
|
|
func InitFontManager() error {
|
|
fontManagerOnce.Do(func() {
|
|
|
|
// Dependency check: Ensure ShaderManager is ready
|
|
sm := shaderManager.Get()
|
|
if sm == nil {
|
|
initErr = fmt.Errorf("cannot initialize FontManager because ShaderManager is not initialized")
|
|
return // Exit the anonymous function
|
|
}
|
|
|
|
fm := &FontManager{
|
|
Fonts: make(map[string]*FontEntry),
|
|
}
|
|
|
|
assetsDir, err := db.GetConfigValue("assets_dir")
|
|
if err != nil {
|
|
initErr = fmt.Errorf("could not get assets_dir from config: %w", err)
|
|
return
|
|
}
|
|
fontPath := filepath.Join(assetsDir, "fonts")
|
|
if err := fm.loadFonts(fontPath); err != nil {
|
|
initErr = fmt.Errorf("could not load fonts from '%s': %w", fontPath, err)
|
|
return
|
|
}
|
|
|
|
// --- NEW: Create the scrambled font after loading normal fonts ---
|
|
// We will use "DejaVuSans" as the base. If you use a different default font,
|
|
// change this string.
|
|
baseFontForScramble := "Roboto-Regular"
|
|
if _, ok := fm.Fonts[baseFontForScramble]; ok {
|
|
if err := fm.CreateScrambledFont(baseFontForScramble, "Scrambled"); err != nil {
|
|
// This is not a fatal error for the whole application, so we just log it.
|
|
}
|
|
} else {
|
|
}
|
|
// --- END NEW ---
|
|
|
|
// Create reusable GPU objects for string texture rendering
|
|
gl.GenFramebuffers(1, &fm.stringFBO)
|
|
gl.GenVertexArrays(1, &fm.stringVAO)
|
|
gl.GenBuffers(1, &fm.stringVBO)
|
|
|
|
// Set up VAO attribute layout once
|
|
gl.BindVertexArray(fm.stringVAO)
|
|
gl.BindBuffer(gl.ARRAY_BUFFER, fm.stringVBO)
|
|
gl.BufferData(gl.ARRAY_BUFFER, 6*4*4, nil, gl.DYNAMIC_DRAW) // 6 vertices * 4 floats * 4 bytes
|
|
gl.EnableVertexAttribArray(0)
|
|
gl.VertexAttribPointerWithOffset(0, 2, gl.FLOAT, false, 4*4, 0)
|
|
gl.EnableVertexAttribArray(1)
|
|
gl.VertexAttribPointerWithOffset(1, 2, gl.FLOAT, false, 4*4, 2*4)
|
|
gl.BindVertexArray(0)
|
|
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
|
|
|
fontManagerInstance = fm
|
|
})
|
|
|
|
// Return the error that might have been captured inside the sync.Once block.
|
|
return initErr
|
|
}
|
|
|
|
// Get returns the singleton instance of the FontManager.
|
|
// It will panic if InitFontManager has not been called successfully first.
|
|
func Get() *FontManager {
|
|
if fontManagerInstance == nil {
|
|
panic("FontManager has not been initialized or initialization failed. Call InitFontManager at application startup.")
|
|
}
|
|
return fontManagerInstance
|
|
}
|
|
|
|
// FontEntry combines related Font information together
|
|
type FontEntry struct {
|
|
Name string
|
|
Font *ttf.Font
|
|
Map *FontMap
|
|
}
|
|
|
|
// FontMap contains the character map for a specific font
|
|
type FontMap struct {
|
|
FontMapTextureID uint32 // Memory address of the FontMapTexture within the GPU
|
|
Width int32 // The width of the font map texture
|
|
Height int32 // The height of the font map FontMap Texture
|
|
Glyphs map[rune]Glyph // A map of runes and their locations within the Fontmap
|
|
padding int32
|
|
}
|
|
|
|
// TempGlyph is a temporary struct used only during font map creation.
|
|
type TempGlyph struct {
|
|
Rune rune
|
|
Advance int
|
|
surface *sdl.Surface
|
|
}
|
|
|
|
// Glyph contains the rendering information for a single character
|
|
type Glyph struct {
|
|
Rune rune // The character this glyph represents.
|
|
SrcRect sdl.Rect // The source rectangle in the texture atlas.
|
|
Advance int // How far to advance the cursor after drawing this glyph.
|
|
TextureID uint32 // The ID of the texture atlas this glyph belongs to.
|
|
}
|
|
|
|
// loadFonts scans the fonts directory, loads all .ttf/.otf files, and creates a font map for each.
|
|
func (fm *FontManager) loadFonts(fontDir string) error {
|
|
files, err := os.ReadDir(fontDir)
|
|
if err != nil {
|
|
return fmt.Errorf("could not read font directory '%s': %w", fontDir, err)
|
|
}
|
|
|
|
for _, file := range files {
|
|
if file.IsDir() {
|
|
continue
|
|
}
|
|
fileName := file.Name()
|
|
if strings.HasSuffix(strings.ToLower(fileName), ".ttf") || strings.HasSuffix(strings.ToLower(fileName), ".otf") {
|
|
fontPath := filepath.Join(fontDir, fileName)
|
|
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
|
|
if err != nil {
|
|
return fmt.Errorf("could not get standard_font_size from config: %w", err)
|
|
}
|
|
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
|
|
if err != nil {
|
|
return fmt.Errorf("could not convert init_font_size to int: %w", err)
|
|
}
|
|
font, err := ttf.OpenFont(fontPath, baseFontSize) // Using a higher resolution for better quality
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
fontName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
|
fm.Fonts[fontName] = &FontEntry{
|
|
Name: fontName,
|
|
Font: font,
|
|
Map: nil,
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, fontEntry := range fm.Fonts {
|
|
if err := fm.createFontMap(fontEntry, false, nil); err != nil {
|
|
return fmt.Errorf("unable to create Font Map for font: %s : %v", fontEntry.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateScrambledFont generates a new font entry by scrambling the glyphs of a base font.
|
|
func (fm *FontManager) CreateScrambledFont(baseFontName string, newFontName string) error {
|
|
baseFont, ok := fm.Fonts[baseFontName]
|
|
if !ok {
|
|
return fmt.Errorf("base font '%s' not found", baseFontName)
|
|
}
|
|
|
|
scrambledFontEntry := &FontEntry{
|
|
Name: newFontName,
|
|
Font: baseFont.Font, // It uses the same underlying TTF font object for rendering
|
|
Map: nil,
|
|
}
|
|
|
|
if err := fm.createFontMap(scrambledFontEntry, true, baseFont); err != nil {
|
|
return fmt.Errorf("unable to create scrambled font map: %w", err)
|
|
}
|
|
|
|
fm.Fonts[newFontName] = scrambledFontEntry
|
|
return nil
|
|
}
|
|
|
|
// ATTENTION: TED
|
|
//
|
|
// # Deterministic Fibonacci-Based Visual Pattern Generation for Character Data
|
|
//
|
|
// This method transforms rendered text characters into structured visual patterns that produce
|
|
// character-specific spatial frequency signatures while maintaining perfect reversibility.
|
|
//
|
|
// The technology addresses a fundamental limitation in subliminal text display: conventional
|
|
// approaches either use unmodified text (consciously readable, defeating the subliminal purpose)
|
|
// or simple blurring/masking (destroys information content and character-specific structure).
|
|
// Our method preserves complete character information in a transformed visual state that remains
|
|
// illegible to conscious observation while generating structured spatial patterns.
|
|
//
|
|
// The approach treats each rendered character as a two-dimensional grid and applies deterministic
|
|
// geometric transformations parameterized by Fibonacci sequence values. The character's Unicode
|
|
// value seeds all transformation parameters, ensuring identical characters produce identical
|
|
// patterns while different characters generate distinct spatial signatures. Multiple transformation
|
|
// stages compound to create high-entropy visual output with underlying mathematical structure.
|
|
//
|
|
// Key differentiators from existing solutions:
|
|
// - Operates on rendered visual data rather than underlying text data or encrypted values
|
|
// - Generates character-specific spatial frequency patterns rather than uniform noise
|
|
// - Uses Fibonacci-sequence-derived parameters to create mathematically structured transformations
|
|
// - Maintains pixel-perfect reversibility despite high visual entropy
|
|
// - Produces deterministic, reproducible patterns for each character
|
|
//
|
|
// Within our Rhema Wave generation system, these transformed characters create spatial interference
|
|
// patterns designed for subconscious processing. Each character contributes a unique frequency
|
|
// signature to the overall visual field, with Fibonacci-based geometry intended to create harmonic
|
|
// relationships between character patterns. The result is a consciously illegible display that
|
|
// maintains structured information content in the spatial domain, forming a component of the
|
|
// broader Rhema Wave output.
|
|
func (fm *FontManager) scrambleSurface(surface *sdl.Surface, r rune) error {
|
|
if surface.W < 8 || surface.H < 8 {
|
|
return nil
|
|
}
|
|
if err := surface.Lock(); err != nil {
|
|
return fmt.Errorf("could not lock surface for scrambling: %w", err)
|
|
}
|
|
defer surface.Unlock()
|
|
|
|
W := int(surface.W)
|
|
H := int(surface.H)
|
|
|
|
// Use much finer grid for complete illegibility
|
|
// Fibonacci number determines grid size: 5, 8, or 13
|
|
fib := []int{5, 8, 13}
|
|
fibIndex := int(r) % len(fib)
|
|
gridSize := fib[fibIndex]
|
|
|
|
// Ensure we have at least 8x8 grid for strong scrambling
|
|
if gridSize < 8 {
|
|
gridSize = 8
|
|
}
|
|
|
|
blockW := W / gridSize
|
|
blockH := H / gridSize
|
|
|
|
if blockW == 0 || blockH == 0 {
|
|
// Surface too small, fall back to 4x4
|
|
gridSize = 4
|
|
blockW = W / gridSize
|
|
blockH = H / gridSize
|
|
if blockW == 0 || blockH == 0 {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
numBlocks := gridSize * gridSize
|
|
|
|
// Create complete permutation using Fibonacci linear congruential generator
|
|
blockIndices := make([]int, numBlocks)
|
|
|
|
// Use Fibonacci numbers for the LCG parameters
|
|
// F(7)=13, F(8)=21, F(9)=34, F(10)=55, F(11)=89
|
|
fibSeq := []int{13, 21, 34, 55, 89, 144}
|
|
multiplier := fibSeq[int(r)%len(fibSeq)]
|
|
increment := fibSeq[(int(r)+1)%len(fibSeq)]
|
|
|
|
// Generate pseudo-random permutation
|
|
for i := 0; i < numBlocks; i++ {
|
|
blockIndices[i] = (multiplier*i + increment*int(r)) % numBlocks
|
|
}
|
|
|
|
// Additional scrambling: apply multiple Fibonacci-based transformations
|
|
numTransforms := 3 + (int(r) % 5) // 3-7 transformations
|
|
|
|
for t := 0; t < numTransforms; t++ {
|
|
transformType := (int(r) + t*fibSeq[t%len(fibSeq)]) % 5
|
|
|
|
switch transformType {
|
|
case 0: // Reverse sections
|
|
sectionSize := gridSize / 2
|
|
for i := 0; i < numBlocks; i += sectionSize {
|
|
end := i + sectionSize
|
|
if end > numBlocks {
|
|
end = numBlocks
|
|
}
|
|
reverseSlice(blockIndices[i:end])
|
|
}
|
|
|
|
case 1: // Spiral rotation
|
|
// Rotate blocks in a spiral pattern
|
|
for layer := 0; layer < gridSize/2; layer++ {
|
|
rotateLayer(blockIndices, gridSize, layer)
|
|
}
|
|
|
|
case 2: // Fibonacci walk swap
|
|
step := fib[t%len(fib)]
|
|
for i := 0; i < numBlocks-step; i += step {
|
|
blockIndices[i], blockIndices[i+step] = blockIndices[i+step], blockIndices[i]
|
|
}
|
|
|
|
case 3: // Checkerboard transpose
|
|
for i := 0; i < gridSize; i++ {
|
|
for j := 0; j < gridSize; j++ {
|
|
if (i+j)%2 == 0 {
|
|
idx1 := i*gridSize + j
|
|
idx2 := j*gridSize + i
|
|
if idx2 < numBlocks {
|
|
blockIndices[idx1], blockIndices[idx2] = blockIndices[idx2], blockIndices[idx1]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
case 4: // Random-walk permutation
|
|
walkLen := fib[(t+1)%len(fib)]
|
|
pos := int(r+rune(t)) % numBlocks
|
|
for w := 0; w < walkLen && pos < numBlocks-1; w++ {
|
|
nextPos := (pos + fib[w%len(fib)]) % numBlocks
|
|
blockIndices[pos], blockIndices[nextPos] = blockIndices[nextPos], blockIndices[pos]
|
|
pos = nextPos
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy pixels to perform the scrambling
|
|
pixels := surface.Pixels()
|
|
bytesPerPixel := int(surface.Format.BytesPerPixel)
|
|
pitch := int(surface.Pitch)
|
|
originalPixels := make([]byte, len(pixels))
|
|
copy(originalPixels, pixels)
|
|
|
|
// Rearrange all blocks
|
|
for dstIdx := 0; dstIdx < numBlocks; dstIdx++ {
|
|
srcIdx := blockIndices[dstIdx]
|
|
|
|
srcCol := srcIdx % gridSize
|
|
srcRow := srcIdx / gridSize
|
|
dstCol := dstIdx % gridSize
|
|
dstRow := dstIdx / gridSize
|
|
|
|
srcX := srcCol * blockW
|
|
srcY := srcRow * blockH
|
|
dstX := dstCol * blockW
|
|
dstY := dstRow * blockH
|
|
|
|
// Copy block pixel by pixel
|
|
for y := 0; y < blockH; y++ {
|
|
if srcY+y >= H || dstY+y >= H {
|
|
continue
|
|
}
|
|
|
|
srcOffset := (srcY+y)*pitch + srcX*bytesPerPixel
|
|
dstOffset := (dstY+y)*pitch + dstX*bytesPerPixel
|
|
|
|
rowLen := blockW * bytesPerPixel
|
|
if srcX+blockW > W {
|
|
rowLen = (W - srcX) * bytesPerPixel
|
|
}
|
|
if dstX+blockW > W {
|
|
maxLen := (W - dstX) * bytesPerPixel
|
|
if maxLen < rowLen {
|
|
rowLen = maxLen
|
|
}
|
|
}
|
|
|
|
if srcOffset+rowLen <= len(originalPixels) && dstOffset+rowLen <= len(pixels) {
|
|
copy(pixels[dstOffset:dstOffset+rowLen], originalPixels[srcOffset:srcOffset+rowLen])
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper function to reverse a slice
|
|
func reverseSlice(arr []int) {
|
|
for i, j := 0, len(arr)-1; i < j; i, j = i+1, j-1 {
|
|
arr[i], arr[j] = arr[j], arr[i]
|
|
}
|
|
}
|
|
|
|
// Helper function to rotate a layer in spiral pattern
|
|
func rotateLayer(arr []int, gridSize, layer int) {
|
|
if layer >= gridSize/2 {
|
|
return
|
|
}
|
|
|
|
// Extract the layer elements
|
|
var temp []int
|
|
|
|
// Top edge
|
|
for i := layer; i < gridSize-layer; i++ {
|
|
temp = append(temp, arr[layer*gridSize+i])
|
|
}
|
|
// Right edge
|
|
for i := layer + 1; i < gridSize-layer; i++ {
|
|
temp = append(temp, arr[i*gridSize+(gridSize-layer-1)])
|
|
}
|
|
// Bottom edge
|
|
if gridSize-layer-1 > layer {
|
|
for i := gridSize - layer - 2; i >= layer; i-- {
|
|
temp = append(temp, arr[(gridSize-layer-1)*gridSize+i])
|
|
}
|
|
}
|
|
// Left edge
|
|
if gridSize-layer-1 > layer {
|
|
for i := gridSize - layer - 2; i > layer; i-- {
|
|
temp = append(temp, arr[i*gridSize+layer])
|
|
}
|
|
}
|
|
|
|
// Rotate the temp array by 1 position
|
|
if len(temp) > 0 {
|
|
last := temp[len(temp)-1]
|
|
copy(temp[1:], temp[0:len(temp)-1])
|
|
temp[0] = last
|
|
}
|
|
|
|
// Put back the rotated elements
|
|
idx := 0
|
|
// Top edge
|
|
for i := layer; i < gridSize-layer && idx < len(temp); i++ {
|
|
arr[layer*gridSize+i] = temp[idx]
|
|
idx++
|
|
}
|
|
// Right edge
|
|
for i := layer + 1; i < gridSize-layer && idx < len(temp); i++ {
|
|
arr[i*gridSize+(gridSize-layer-1)] = temp[idx]
|
|
idx++
|
|
}
|
|
// Bottom edge
|
|
if gridSize-layer-1 > layer {
|
|
for i := gridSize - layer - 2; i >= layer && idx < len(temp); i-- {
|
|
arr[(gridSize-layer-1)*gridSize+i] = temp[idx]
|
|
idx++
|
|
}
|
|
}
|
|
// Left edge
|
|
if gridSize-layer-1 > layer {
|
|
for i := gridSize - layer - 2; i > layer && idx < len(temp); i-- {
|
|
arr[i*gridSize+layer] = temp[idx]
|
|
idx++
|
|
}
|
|
}
|
|
}
|
|
|
|
// createFontMap generates a texture atlas for a given font entry.
|
|
func (fm *FontManager) createFontMap(font *FontEntry, scramble bool, baseFont *FontEntry) error {
|
|
var chars []rune
|
|
for i := rune(32); i <= 126; i++ { // Common ASCII characters
|
|
chars = append(chars, i)
|
|
}
|
|
|
|
tempGlyphs := make([]TempGlyph, 0, len(chars))
|
|
var totalWidth, maxHeight int32
|
|
padding := int32(2) // 2 pixels of padding between glyphs
|
|
|
|
// If scrambling, use the base font's metrics but our new entry's name
|
|
fontToRenderFrom := font
|
|
if scramble && baseFont != nil {
|
|
fontToRenderFrom = baseFont
|
|
}
|
|
|
|
for _, char := range chars {
|
|
tempGlyph, err := fm.createTempGlyph(fontToRenderFrom, char)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// --- NEW: Scramble the surface if requested ---
|
|
if scramble {
|
|
if err := fm.scrambleSurface(tempGlyph.surface, char); err != nil {
|
|
// We can continue, it will just use the unscrambled glyph
|
|
}
|
|
}
|
|
// --- END NEW ---
|
|
|
|
tempGlyphs = append(tempGlyphs, tempGlyph)
|
|
totalWidth += tempGlyph.surface.W + padding
|
|
if tempGlyph.surface.H > maxHeight {
|
|
maxHeight = tempGlyph.surface.H
|
|
}
|
|
}
|
|
|
|
atlasSurface, err := sdl.CreateRGBSurface(0, totalWidth, maxHeight, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create atlas surface: %w", err)
|
|
}
|
|
defer atlasSurface.Free()
|
|
|
|
fontMap := &FontMap{
|
|
Glyphs: make(map[rune]Glyph),
|
|
padding: padding,
|
|
}
|
|
|
|
var currentX int32 = 0
|
|
for _, tempGlyph := range tempGlyphs {
|
|
dstRect := sdl.Rect{X: currentX, Y: 0, W: tempGlyph.surface.W, H: tempGlyph.surface.H}
|
|
if err := tempGlyph.surface.Blit(nil, atlasSurface, &dstRect); err != nil {
|
|
tempGlyph.surface.Free()
|
|
continue
|
|
}
|
|
|
|
fontMap.Glyphs[tempGlyph.Rune] = Glyph{
|
|
Rune: tempGlyph.Rune,
|
|
Advance: tempGlyph.Advance,
|
|
SrcRect: dstRect,
|
|
}
|
|
|
|
currentX += tempGlyph.surface.W + padding
|
|
tempGlyph.surface.Free()
|
|
}
|
|
|
|
textureID, err := graphics.CreateTextureFromSurface(atlasSurface)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create texture from atlas surface: %w", err)
|
|
}
|
|
|
|
fontMap.FontMapTextureID = textureID
|
|
fontMap.Width = atlasSurface.W
|
|
fontMap.Height = atlasSurface.H
|
|
|
|
for r, g := range fontMap.Glyphs {
|
|
g.TextureID = textureID
|
|
fontMap.Glyphs[r] = g
|
|
}
|
|
|
|
font.Map = fontMap
|
|
return nil
|
|
}
|
|
|
|
// createTempGlyph renders a single character to a temporary SDL surface.
|
|
func (fm *FontManager) createTempGlyph(font *FontEntry, char rune) (TempGlyph, error) {
|
|
runeSurface, err := font.Font.RenderGlyphBlended(char, sdl.Color{R: 255, G: 255, B: 255, A: 255})
|
|
if err != nil {
|
|
return TempGlyph{}, fmt.Errorf("unable to create glyph for char '%c': %w", char, err)
|
|
}
|
|
|
|
metrics, err := font.Font.GlyphMetrics(char)
|
|
if err != nil {
|
|
runeSurface.Free()
|
|
return TempGlyph{}, fmt.Errorf("unable to fetch glyph metrics for char '%c': %w", char, err)
|
|
}
|
|
|
|
return TempGlyph{
|
|
Rune: char,
|
|
Advance: metrics.Advance,
|
|
surface: runeSurface,
|
|
}, nil
|
|
}
|
|
|
|
// GetFont retrieves a loaded font by its name.
|
|
func (fm *FontManager) GetFont(name string) (*FontEntry, bool) {
|
|
fontEntry, ok := fm.Fonts[name]
|
|
return fontEntry, ok
|
|
}
|
|
|
|
// GetGlyph retrieves the rendering data for a specific character from a font's map.
|
|
func (fm *FontManager) GetGlyph(fontName string, r rune) (Glyph, bool) {
|
|
fontEntry, ok := fm.Fonts[fontName]
|
|
if !ok || fontEntry.Map == nil {
|
|
return Glyph{}, false
|
|
}
|
|
glyph, ok := fontEntry.Map.Glyphs[r]
|
|
return glyph, ok
|
|
}
|
|
|
|
// GetFontNames returns a slice of all loaded font names.
|
|
func (fm *FontManager) GetFontNames() []string {
|
|
names := make([]string, 0, len(fm.Fonts))
|
|
for name := range fm.Fonts {
|
|
names = append(names, name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// Close frees all loaded font resources, including the generated textures.
|
|
func (fm *FontManager) Close() {
|
|
for _, fontEntry := range fm.Fonts {
|
|
if fontEntry.Map != nil && fontEntry.Map.FontMapTextureID != 0 {
|
|
gl.DeleteTextures(1, &fontEntry.Map.FontMapTextureID)
|
|
}
|
|
if fontEntry.Font != nil {
|
|
fontEntry.Font.Close()
|
|
}
|
|
}
|
|
|
|
if fm.stringFBO != 0 {
|
|
gl.DeleteFramebuffers(1, &fm.stringFBO)
|
|
}
|
|
if fm.stringVAO != 0 {
|
|
gl.DeleteVertexArrays(1, &fm.stringVAO)
|
|
}
|
|
if fm.stringVBO != 0 {
|
|
gl.DeleteBuffers(1, &fm.stringVBO)
|
|
}
|
|
}
|
|
|
|
func (fm *FontManager) CreateStringTexture(fontName string, text string, shader *shaderManager.Shader) (textureID uint32, width int32, height int32, err error) {
|
|
shaderID := shader.ProgramID
|
|
if fontName == "" || text == "" {
|
|
return 0, 0, 0, fmt.Errorf("font name and text cannot be blank")
|
|
}
|
|
|
|
selectedFont, ok := fm.Fonts[fontName]
|
|
if !ok || selectedFont.Map == nil {
|
|
return 0, 0, 0, fmt.Errorf("font '%s' not found or its font map is not created", fontName)
|
|
}
|
|
|
|
var totalWidth, maxHeight int32
|
|
glyphsToRender := make([]Glyph, 0, len(text))
|
|
|
|
for _, r := range text {
|
|
g, ok := selectedFont.Map.Glyphs[r]
|
|
if !ok {
|
|
g = selectedFont.Map.Glyphs['?'] // Fallback
|
|
}
|
|
totalWidth += g.SrcRect.W + selectedFont.Map.padding
|
|
if g.SrcRect.H > maxHeight {
|
|
maxHeight = g.SrcRect.H
|
|
}
|
|
glyphsToRender = append(glyphsToRender, g)
|
|
}
|
|
|
|
if totalWidth <= 0 || maxHeight <= 0 {
|
|
return 0, 0, 0, fmt.Errorf("calculated texture dimensions are invalid: %dx%d", totalWidth, maxHeight)
|
|
}
|
|
|
|
var lastFBO int32
|
|
gl.GetIntegerv(gl.FRAMEBUFFER_BINDING, &lastFBO)
|
|
var lastViewport [4]int32
|
|
gl.GetIntegerv(gl.VIEWPORT, &lastViewport[0])
|
|
|
|
gl.BindFramebuffer(gl.FRAMEBUFFER, fm.stringFBO)
|
|
|
|
var newTextureID uint32
|
|
gl.GenTextures(1, &newTextureID)
|
|
gl.BindTexture(gl.TEXTURE_2D, newTextureID)
|
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
|
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
|
|
|
|
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, totalWidth, maxHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
|
|
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newTextureID, 0)
|
|
|
|
if gl.CheckFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE {
|
|
gl.BindFramebuffer(gl.FRAMEBUFFER, uint32(lastFBO))
|
|
gl.DeleteTextures(1, &newTextureID)
|
|
return 0, 0, 0, fmt.Errorf("framebuffer is not complete")
|
|
}
|
|
|
|
gl.Viewport(0, 0, totalWidth, maxHeight)
|
|
gl.ClearColor(0.0, 0.0, 0.0, 0.0)
|
|
gl.Clear(gl.COLOR_BUFFER_BIT)
|
|
gl.UseProgram(shaderID)
|
|
|
|
projection := mgl32.Ortho(0, float32(totalWidth), 0, float32(maxHeight), -1, 1)
|
|
gl.UniformMatrix4fv(shader.LocMVPMatrix, 1, false, &projection[0])
|
|
|
|
if shader.LocTextColor != -1 {
|
|
gl.Uniform4f(shader.LocTextColor, 1.0, 1.0, 1.0, 1.0)
|
|
}
|
|
|
|
gl.ActiveTexture(gl.TEXTURE0)
|
|
gl.Uniform1i(shader.LocTexture, 0)
|
|
gl.Enable(gl.BLEND)
|
|
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
|
gl.BindTexture(gl.TEXTURE_2D, selectedFont.Map.FontMapTextureID)
|
|
|
|
gl.BindVertexArray(fm.stringVAO)
|
|
gl.BindBuffer(gl.ARRAY_BUFFER, fm.stringVBO)
|
|
|
|
var currentX float32
|
|
atlasW, atlasH := float32(selectedFont.Map.Width), float32(selectedFont.Map.Height)
|
|
|
|
for _, glyph := range glyphsToRender {
|
|
xPos := currentX
|
|
yPos := float32(0)
|
|
w, h := float32(glyph.SrcRect.W), float32(glyph.SrcRect.H)
|
|
u0 := float32(glyph.SrcRect.X) / atlasW
|
|
v0 := float32(glyph.SrcRect.Y) / atlasH
|
|
u1 := u0 + w/atlasW
|
|
v1 := v0 + h/atlasH
|
|
vertices := []float32{
|
|
xPos, yPos + h, u0, v0,
|
|
xPos, yPos, u0, v1,
|
|
xPos + w, yPos, u1, v1,
|
|
xPos, yPos + h, u0, v0,
|
|
xPos + w, yPos, u1, v1,
|
|
xPos + w, yPos + h, u1, v0,
|
|
}
|
|
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(vertices)*4, gl.Ptr(vertices))
|
|
gl.DrawArrays(gl.TRIANGLES, 0, 6)
|
|
currentX += float32(glyph.Advance)
|
|
}
|
|
|
|
gl.BindVertexArray(0)
|
|
gl.BindFramebuffer(gl.FRAMEBUFFER, uint32(lastFBO))
|
|
gl.Viewport(lastViewport[0], lastViewport[1], lastViewport[2], lastViewport[3])
|
|
gl.Disable(gl.BLEND)
|
|
|
|
return newTextureID, totalWidth, maxHeight, nil
|
|
}
|
|
|
|
// NewCreateStringTexture does the same thing as CreateStringTexture, except that it returns the width in pixels used by the glyphs
|
|
// in the texture, rather than the width of the texture itself. This can be used for trimming.
|
|
func (fm *FontManager) NewCreateStringTexture(fontName string, text string, shader *shaderManager.Shader) (textureID uint32, width float32, height int32, err error) {
|
|
shaderID := shader.ProgramID
|
|
if fontName == "" || text == "" {
|
|
return 0, 0, 0, fmt.Errorf("font name and text cannot be blank")
|
|
}
|
|
|
|
selectedFont, ok := fm.Fonts[fontName]
|
|
if !ok || selectedFont.Map == nil {
|
|
return 0, 0, 0, fmt.Errorf("font '%s' not found or its font map is not created", fontName)
|
|
}
|
|
|
|
var totalWidth, maxHeight int32
|
|
glyphsToRender := make([]Glyph, 0, len(text))
|
|
|
|
for _, r := range text {
|
|
g, ok := selectedFont.Map.Glyphs[r]
|
|
if !ok {
|
|
g = selectedFont.Map.Glyphs['?'] // Fallback
|
|
}
|
|
totalWidth += g.SrcRect.W + selectedFont.Map.padding
|
|
if g.SrcRect.H > maxHeight {
|
|
maxHeight = g.SrcRect.H
|
|
}
|
|
glyphsToRender = append(glyphsToRender, g)
|
|
}
|
|
|
|
if totalWidth <= 0 || maxHeight <= 0 {
|
|
return 0, 0, 0, fmt.Errorf("calculated texture dimensions are invalid: %dx%d", totalWidth, maxHeight)
|
|
}
|
|
|
|
var lastFBO int32
|
|
gl.GetIntegerv(gl.FRAMEBUFFER_BINDING, &lastFBO)
|
|
var lastViewport [4]int32
|
|
gl.GetIntegerv(gl.VIEWPORT, &lastViewport[0])
|
|
|
|
gl.BindFramebuffer(gl.FRAMEBUFFER, fm.stringFBO)
|
|
|
|
var newTextureID uint32
|
|
gl.GenTextures(1, &newTextureID)
|
|
gl.BindTexture(gl.TEXTURE_2D, newTextureID)
|
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
|
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
|
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
|
|
|
|
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, totalWidth, maxHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
|
|
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newTextureID, 0)
|
|
|
|
if gl.CheckFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE {
|
|
gl.BindFramebuffer(gl.FRAMEBUFFER, uint32(lastFBO))
|
|
gl.DeleteTextures(1, &newTextureID)
|
|
return 0, 0, 0, fmt.Errorf("framebuffer is not complete")
|
|
}
|
|
|
|
gl.Viewport(0, 0, totalWidth, maxHeight)
|
|
gl.ClearColor(0.0, 0.0, 0.0, 0.0)
|
|
gl.Clear(gl.COLOR_BUFFER_BIT)
|
|
gl.UseProgram(shaderID)
|
|
|
|
projection := mgl32.Ortho(0, float32(totalWidth), 0, float32(maxHeight), -1, 1)
|
|
gl.UniformMatrix4fv(shader.LocMVPMatrix, 1, false, &projection[0])
|
|
|
|
if shader.LocTextColor != -1 {
|
|
gl.Uniform4f(shader.LocTextColor, 1.0, 1.0, 1.0, 1.0)
|
|
}
|
|
|
|
gl.ActiveTexture(gl.TEXTURE0)
|
|
gl.Uniform1i(shader.LocTexture, 0)
|
|
gl.Enable(gl.BLEND)
|
|
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
|
gl.BindTexture(gl.TEXTURE_2D, selectedFont.Map.FontMapTextureID)
|
|
|
|
gl.BindVertexArray(fm.stringVAO)
|
|
gl.BindBuffer(gl.ARRAY_BUFFER, fm.stringVBO)
|
|
|
|
var currentX float32
|
|
atlasW, atlasH := float32(selectedFont.Map.Width), float32(selectedFont.Map.Height)
|
|
|
|
for _, glyph := range glyphsToRender {
|
|
xPos := currentX
|
|
yPos := float32(0)
|
|
w, h := float32(glyph.SrcRect.W), float32(glyph.SrcRect.H)
|
|
u0 := float32(glyph.SrcRect.X) / atlasW
|
|
v0 := float32(glyph.SrcRect.Y) / atlasH
|
|
u1 := u0 + w/atlasW
|
|
v1 := v0 + h/atlasH
|
|
vertices := []float32{
|
|
xPos, yPos + h, u0, v0,
|
|
xPos, yPos, u0, v1,
|
|
xPos + w, yPos, u1, v1,
|
|
xPos, yPos + h, u0, v0,
|
|
xPos + w, yPos, u1, v1,
|
|
xPos + w, yPos + h, u1, v0,
|
|
}
|
|
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(vertices)*4, gl.Ptr(vertices))
|
|
gl.DrawArrays(gl.TRIANGLES, 0, 6)
|
|
currentX += float32(glyph.Advance)
|
|
}
|
|
|
|
usedWidth := float32(currentX)
|
|
|
|
gl.BindVertexArray(0)
|
|
gl.BindFramebuffer(gl.FRAMEBUFFER, uint32(lastFBO))
|
|
gl.Viewport(lastViewport[0], lastViewport[1], lastViewport[2], lastViewport[3])
|
|
gl.Disable(gl.BLEND)
|
|
|
|
return newTextureID, usedWidth, maxHeight, nil
|
|
}
|