890 lines
26 KiB
Go
890 lines
26 KiB
Go
package fontManager
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"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(cfg *db.AppConfig) 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),
|
||
}
|
||
|
||
fontPath := filepath.Join(cfg.AssetsDir, "fonts")
|
||
if err := fm.loadFonts(fontPath, cfg); 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, cfg *db.AppConfig) 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)
|
||
baseFontSize := cfg.StandardFontSize
|
||
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)
|
||
// }
|
||
|
||
// Standard ASCII (A-Z, a-z, 0-9) + punctuation
|
||
chars := []rune{
|
||
// Numbers 0-9
|
||
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, // 0-9
|
||
|
||
// Uppercase A-Z
|
||
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, // A-O
|
||
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, // P-Z
|
||
|
||
// Lowercase a-z
|
||
97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, // a-m
|
||
110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, // n-z
|
||
|
||
// Basic punctuation (ASCII 32-47, 58-64, 91-96, 123-126)
|
||
32, // Space
|
||
33, // !
|
||
34, // "
|
||
35, // #
|
||
36, // $
|
||
37, // %
|
||
38, // &
|
||
39, // '
|
||
40, // (
|
||
41, // )
|
||
42, // *
|
||
43, // +
|
||
44, // ,
|
||
45, // -
|
||
46, // .
|
||
47, // /
|
||
58, // :
|
||
59, // ;
|
||
60, // <
|
||
61, // =
|
||
62, // >
|
||
63, // ?
|
||
64, // @
|
||
91, // [
|
||
92, // \
|
||
93, // ]
|
||
94, // ^
|
||
95, // _
|
||
96, // `
|
||
123, // {
|
||
124, // |
|
||
125, // }
|
||
126, // ~
|
||
|
||
// Common Unicode punctuation
|
||
161, // ¡
|
||
191, // ¿
|
||
8211, // –
|
||
8212, // —
|
||
8216, // ‘
|
||
8217, // ’
|
||
8220, // “
|
||
8221, // ”
|
||
8226, // •
|
||
8230, // …
|
||
8240, // ‰
|
||
8364, // €
|
||
162, // ¢
|
||
163, // £
|
||
165, // ¥
|
||
169, // ©
|
||
174, // ®
|
||
8482, // ™
|
||
232, // è
|
||
233, // é
|
||
}
|
||
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
|
||
}
|