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 := "SpaceMonoNerdFont-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, // é 10003, // checkmark } 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 }