Optimize proclamation loading
This commit is contained in:
+177
-223
@@ -1,59 +1,3 @@
|
|||||||
// ATTENTION: TED
|
|
||||||
//
|
|
||||||
// # Integrated Multi-Domain Rhema Wave Generation System
|
|
||||||
//
|
|
||||||
// This module implements the complete Rhema Wave generation system by synchronizing multiple
|
|
||||||
// subsystems that operate simultaneously across spatial, temporal, and chromatic
|
|
||||||
// domains to produce a unified frequency-structured visual output for subliminal proclamation display.
|
|
||||||
//
|
|
||||||
// The technology addresses the fundamental challenge of presenting textual proclamation content
|
|
||||||
// in a format optimized for subconscious processing rather than conscious reading. Conventional
|
|
||||||
// subliminal text systems use isolated techniques (simple masking, single-frequency flashing, or
|
|
||||||
// uniform scrolling) that operate in only one domain. Our system integrates four mathematically-
|
|
||||||
// synchronized subsystems, each generating Fibonacci-based patterns in different domains, creating
|
|
||||||
// a compound frequency signature designed to enhance subconscious absorption while maintaining
|
|
||||||
// conscious illegibility.
|
|
||||||
//
|
|
||||||
// The system architecture integrates:
|
|
||||||
//
|
|
||||||
// 1. Spatial Character Transformation - Each rendered character undergoes Fibonacci-based
|
|
||||||
// block scrambling (described separately), creating local spatial frequency patterns at
|
|
||||||
// the character level.
|
|
||||||
//
|
|
||||||
// 2. Spatial Line Distribution - Text lines contain varying word counts following an oscillating
|
|
||||||
// Fibonacci sequence (1, 1, 2, 3, 5, 8, 13... then reversing), creating a macro-level
|
|
||||||
// two-dimensional wave pattern in the display geometry and ensuring balanced temporal
|
|
||||||
// exposure of proclamation content segments.
|
|
||||||
//
|
|
||||||
// 3. Temporal Color Modulation - Sequential words receive colors from an intent-based palette,
|
|
||||||
// with color duration determined by an oscillating Fibonacci sequence, creating rhythmic
|
|
||||||
// chromatic variation synchronized with natural mathematical progression.
|
|
||||||
//
|
|
||||||
// 4. Temporal Velocity Modulation - Display scroll velocity oscillates between negative and
|
|
||||||
// positive extremes with acceleration/deceleration rates and hold periods, creating a
|
|
||||||
// "breathing" temporal rhythm for the entire visual field.
|
|
||||||
//
|
|
||||||
// The key innovation lies in the temporal and mathematical synchronization of these subsystems.
|
|
||||||
// All four operate on Fibonacci-derived parameters, creating harmonic relationships between the
|
|
||||||
// spatial frequencies (character scrambling, line wave patterns), chromatic frequencies (color
|
|
||||||
// duration oscillation), and temporal frequencies (scroll velocity modulation). This multi-domain
|
|
||||||
// integration produces what we term the "Rhema Wave" - a compound frequency signature where
|
|
||||||
// spatial, temporal, and chromatic patterns reinforce each other through mathematical harmony.
|
|
||||||
//
|
|
||||||
// Key differentiators from existing solutions:
|
|
||||||
// - Integrates four distinct Fibonacci-based frequency generators in a single system
|
|
||||||
// - Synchronizes pattern generation across spatial, temporal, and chromatic domains
|
|
||||||
// - Creates compound frequency signatures rather than isolated single-domain effects
|
|
||||||
// - Maintains character-specific and content-specific mathematical structure throughout
|
|
||||||
// - Uses bidirectional oscillation across multiple subsystems for rhythmic coherence
|
|
||||||
// - Combines macro-level (line patterns, velocity) and micro-level (character, color) modulation
|
|
||||||
//
|
|
||||||
// The system loads proclamation text content, processes it through all four subsystems simultaneously,
|
|
||||||
// and generates a continuously scrolling display where every element - from individual character
|
|
||||||
// pixels to overall scroll rhythm - contributes to the unified Rhema Wave output. The mathematical
|
|
||||||
// relationships between subsystems create interference patterns and harmonic resonances designed
|
|
||||||
// to enhance subconscious processing of the proclamation content while maintaining conscious
|
|
||||||
// illegibility through the character scrambling subsystem.
|
|
||||||
package page
|
package page
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -77,7 +21,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
defaultProclamationID string = "f3d42354-3197-45e4-9b08-be447f14386d"
|
defaultProclamationID string = "f3d42354-3197-45e4-9b08-be447f14386d"
|
||||||
proclamationPlayCount int = 10
|
proclamationPlayCount int = 10
|
||||||
defaultProclamationPlayCount int = 10
|
defaultProclamationPlayCount int = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type Line struct {
|
type Line struct {
|
||||||
@@ -85,6 +29,15 @@ type Line struct {
|
|||||||
width int32
|
width int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// proclamationStream tracks one proclamation and how many full loops it should play.
|
||||||
|
type proclamationStream struct {
|
||||||
|
name string
|
||||||
|
words []string
|
||||||
|
repeat int
|
||||||
|
completed int
|
||||||
|
wordIndex int
|
||||||
|
}
|
||||||
|
|
||||||
// LineDeque is a double-ended queue for Line values backed by a circular buffer.
|
// LineDeque is a double-ended queue for Line values backed by a circular buffer.
|
||||||
// It avoids O(n) prepend and prevents capacity leaks from head-of-slice removal.
|
// It avoids O(n) prepend and prevents capacity leaks from head-of-slice removal.
|
||||||
type LineDeque struct {
|
type LineDeque struct {
|
||||||
@@ -171,8 +124,9 @@ func (d *LineDeque) Clear() {
|
|||||||
type ScrollerPage struct {
|
type ScrollerPage struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
words []string
|
streams []proclamationStream
|
||||||
wordIndex int
|
streamIndex int
|
||||||
|
|
||||||
lines *LineDeque
|
lines *LineDeque
|
||||||
lineHeight int32
|
lineHeight int32
|
||||||
shader *shaderManager.Shader
|
shader *shaderManager.Shader
|
||||||
@@ -185,6 +139,9 @@ type ScrollerPage struct {
|
|||||||
scrollY float32
|
scrollY float32
|
||||||
fontScale float32
|
fontScale float32
|
||||||
|
|
||||||
|
// Reuse one GL texture per unique word.
|
||||||
|
wordTextureCache map[string]StringItem
|
||||||
|
|
||||||
increment float32
|
increment float32
|
||||||
velocity float32
|
velocity float32
|
||||||
|
|
||||||
@@ -227,13 +184,14 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
|||||||
logger.Info("Shader Program ID: %d", textShader.ProgramID)
|
logger.Info("Shader Program ID: %d", textShader.ProgramID)
|
||||||
logger.Info("p.shader Program ID: %d", p.shader.ProgramID)
|
logger.Info("p.shader Program ID: %d", p.shader.ProgramID)
|
||||||
|
|
||||||
// Load proclamation words from database
|
p.wordTextureCache = make(map[string]StringItem, 256)
|
||||||
|
|
||||||
|
// Load proclamation streams from database
|
||||||
if err := p.loadProclamations(); err != nil {
|
if err := p.loadProclamations(); err != nil {
|
||||||
logger.Error("Error loading proclamations: %v", err)
|
logger.Error("Error loading proclamations: %v", err)
|
||||||
p.Context.NavigateTo(PROCLAMATIONS)
|
p.Context.NavigateTo(PROCLAMATIONS)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
p.wordIndex = 0
|
|
||||||
|
|
||||||
// Get line pattern from database
|
// Get line pattern from database
|
||||||
var err error
|
var err error
|
||||||
@@ -256,25 +214,19 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
|||||||
}
|
}
|
||||||
p.colorPattern = colorPattern.NewColorPattern(baseColors)
|
p.colorPattern = colorPattern.NewColorPattern(baseColors)
|
||||||
|
|
||||||
// Get font sizes from database (optimize: load once, cache as int32)
|
|
||||||
// Match the working page's logic exactly:
|
|
||||||
p.fontScale = float32(32) / float32(p.Context.CfgPtr.StandardFontSize)
|
|
||||||
|
|
||||||
// Font setup
|
// Font setup
|
||||||
nLines := int(float32(p.ScreenHeight) / 32.0) // 32 is your target visual size
|
p.fontScale = float32(32) / float32(p.Context.CfgPtr.StandardFontSize)
|
||||||
|
nLines := int(float32(p.ScreenHeight) / 32.0)
|
||||||
p.lines = NewLineDeque(nLines * 2)
|
p.lines = NewLineDeque(nLines * 2)
|
||||||
|
|
||||||
// Calculate space width using the SCALE ratio, not the font size int
|
// Calculate space width using the scale ratio.
|
||||||
spaceGlyph, ok := fm.GetGlyph(font.Name, ' ')
|
spaceGlyph, ok := fm.GetGlyph(font.Name, ' ')
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("failed to get space glyph")
|
return fmt.Errorf("failed to get space glyph")
|
||||||
}
|
}
|
||||||
p.spaceWidth = int32(float32(spaceGlyph.Advance) * p.fontScale)
|
p.spaceWidth = int32(float32(spaceGlyph.Advance) * p.fontScale)
|
||||||
|
|
||||||
// Get line height using the SCALE ratio
|
// Get line height using the same scale ratio.
|
||||||
if fm == nil {
|
|
||||||
logger.Error("FontManager somehow ended up nil...")
|
|
||||||
}
|
|
||||||
tmpStrID, _, H, err := fm.NewCreateStringTexture(font.Name, "Mg", p.shader)
|
tmpStrID, _, H, err := fm.NewCreateStringTexture(font.Name, "Mg", p.shader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create sample texture: %w", err)
|
return fmt.Errorf("failed to create sample texture: %w", err)
|
||||||
@@ -290,7 +242,7 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create initial lines
|
// Create initial lines
|
||||||
for i := 0; i < nLines; i++ {
|
for i := range nLines {
|
||||||
if err := p.createLine(i, true); err != nil {
|
if err := p.createLine(i, true); err != nil {
|
||||||
return fmt.Errorf("failed to create initial line %d: %w", i, err)
|
return fmt.Errorf("failed to create initial line %d: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -298,92 +250,154 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
|||||||
|
|
||||||
// Audio setup
|
// Audio setup
|
||||||
audioDeviceName := p.Context.CfgPtr.SelectedAudioDevice
|
audioDeviceName := p.Context.CfgPtr.SelectedAudioDevice
|
||||||
|
|
||||||
// Switch audio devices if needed
|
|
||||||
if mixer.CurrentDevice() != audioDeviceName {
|
if mixer.CurrentDevice() != audioDeviceName {
|
||||||
if err := mixer.SwitchDevice(audioDeviceName); err != nil {
|
if err := mixer.SwitchDevice(audioDeviceName); err != nil {
|
||||||
} else {
|
logger.Error("failed to switch audio device: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start audio playback
|
|
||||||
|
|
||||||
// selectedAudio := p.Context.CfgPtr.SelectedAudio
|
|
||||||
|
|
||||||
assetsDir := p.Context.CfgPtr.AssetsDir
|
assetsDir := p.Context.CfgPtr.AssetsDir
|
||||||
audioFile := filepath.Join(assetsDir, "audio/audio.wav")
|
audioFile := filepath.Join(assetsDir, "audio/audio.wav")
|
||||||
logger.Info("Attempting to play %s", audioFile)
|
logger.Info("Attempting to play %s", audioFile)
|
||||||
if err := mixer.Play(audioFile, true, p.Context.CfgPtr); err != nil {
|
if err := mixer.Play(audioFile, true, p.Context.CfgPtr); err != nil {
|
||||||
logger.Error("Mixer returned an error: %v", err) // ADD THIS
|
logger.Error("Mixer returned an error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentDevice := mixer.CurrentDevice()
|
currentDevice := mixer.CurrentDevice()
|
||||||
if currentDevice == "" {
|
if currentDevice == "" {
|
||||||
currentDevice = "default"
|
currentDevice = "default"
|
||||||
}
|
}
|
||||||
|
_ = currentDevice
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadProclamations loads selected proclamations from the database
|
// loadProclamations loads selected proclamations as stream units (not one giant word list).
|
||||||
func (p *ScrollerPage) loadProclamations() error {
|
func (p *ScrollerPage) loadProclamations() error {
|
||||||
p.words = []string{}
|
p.streams = p.streams[:0]
|
||||||
|
|
||||||
// Get all selected proclamations from database
|
var selected []db.Proclamation
|
||||||
var proclamations []db.Proclamation
|
if err := p.Context.DbPtr.Where("selected = ?", true).Find(&selected).Error; err != nil {
|
||||||
err := p.Context.DbPtr.Where("selected = ?", true).Find(&proclamations).Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultProclamation db.Proclamation
|
var defaultProclamation db.Proclamation
|
||||||
err = p.Context.DbPtr.Where("ID = ?", defaultProclamationID).Find(&defaultProclamation).Error
|
if err := p.Context.DbPtr.Where("ID = ?", defaultProclamationID).First(&defaultProclamation).Error; err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build word list from selected, available proclamations
|
|
||||||
defaultWords := strings.Fields(defaultProclamation.Content)
|
defaultWords := strings.Fields(defaultProclamation.Content)
|
||||||
|
|
||||||
for _, proclamation := range proclamations {
|
// Build interleaved stream:
|
||||||
words := strings.Fields(proclamation.Content)
|
// each selected proclamation repeats N times, then default repeats once.
|
||||||
|
for _, proclamation := range selected {
|
||||||
// Pre-allocate: 10x proclamation words + 1x default words
|
// Prevent default proclamation from being treated as a normal selected proclamation.
|
||||||
batch := make([]string, 0, len(words)*10+len(defaultWords))
|
if proclamation.ID == defaultProclamationID {
|
||||||
for range 10 {
|
continue
|
||||||
logger.Debug("Adding %s", proclamation.Name)
|
|
||||||
batch = append(batch, words...)
|
|
||||||
}
|
}
|
||||||
logger.Debug("Adding %s", defaultProclamation.Name)
|
|
||||||
batch = append(batch, defaultWords...)
|
|
||||||
|
|
||||||
p.words = append(p.words, batch...)
|
words := strings.Fields(proclamation.Content)
|
||||||
|
if len(words) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
p.streams = append(p.streams, proclamationStream{
|
||||||
|
name: proclamation.Name,
|
||||||
|
words: words,
|
||||||
|
repeat: proclamationPlayCount, // 10
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(defaultWords) > 0 {
|
||||||
|
p.streams = append(p.streams, proclamationStream{
|
||||||
|
name: defaultProclamation.Name,
|
||||||
|
words: defaultWords,
|
||||||
|
repeat: 1, // play once between selected proclamations
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(p.words) == 0 {
|
// Fallback: if no non-default selected proclamations, at least play default.
|
||||||
return fmt.Errorf("Failed to load proclamations, no proclamations selected!")
|
if len(p.streams) == 0 && len(defaultWords) > 0 {
|
||||||
|
p.streams = append(p.streams, proclamationStream{
|
||||||
|
name: defaultProclamation.Name,
|
||||||
|
words: defaultWords,
|
||||||
|
repeat: 1,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(p.streams) == 0 {
|
||||||
|
return fmt.Errorf("failed to load proclamations: no proclamation words available")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.streamIndex = 0
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createLine is a helper to generate a new line at a specific index
|
// nextLineWords gets words for the current line and advances proclamation stream when needed.
|
||||||
// isInitial determines if this is during initialization (positions from top)
|
func (p *ScrollerPage) nextLineWords() []string {
|
||||||
// or dynamic creation (positions relative to existing lines)
|
if len(p.streams) == 0 {
|
||||||
func (p *ScrollerPage) createLine(index int, isInitial bool) error {
|
return nil
|
||||||
words := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx)
|
}
|
||||||
|
|
||||||
|
s := &p.streams[p.streamIndex]
|
||||||
|
if len(s.words) == 0 {
|
||||||
|
p.streamIndex = (p.streamIndex + 1) % len(p.streams)
|
||||||
|
return p.nextLineWords()
|
||||||
|
}
|
||||||
|
|
||||||
|
prevIdx := s.wordIndex
|
||||||
|
words := p.activePattern.GetNextLine(s.words, &s.wordIndex, p.lineCtx)
|
||||||
|
|
||||||
|
// If word index wraps, one full proclamation pass completed.
|
||||||
|
if s.wordIndex < prevIdx {
|
||||||
|
s.completed++
|
||||||
|
targetRepeats := s.repeat
|
||||||
|
if targetRepeats <= 0 {
|
||||||
|
targetRepeats = 1
|
||||||
|
}
|
||||||
|
logger.Debug("Now playing proclamation: %s", s.name)
|
||||||
|
if s.completed >= targetRepeats {
|
||||||
|
logger.Debug("Advancing proclamation stream: %s", s.name)
|
||||||
|
s.completed = 0
|
||||||
|
s.wordIndex = 0
|
||||||
|
p.streamIndex = (p.streamIndex + 1) % len(p.streams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCachedWord returns a reusable textured StringItem for a word.
|
||||||
|
func (p *ScrollerPage) getCachedWord(word string) (StringItem, error) {
|
||||||
|
if cached, ok := p.wordTextureCache[word]; ok {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
base, err := NewStringItem(word, p.font, sdl.Color{R: 255, G: 255, B: 255, A: 255})
|
||||||
|
if err != nil {
|
||||||
|
return StringItem{}, fmt.Errorf("unable to create cached string item for word '%s': %w", word, err)
|
||||||
|
}
|
||||||
|
base.Scale(p.fontScale)
|
||||||
|
|
||||||
|
p.wordTextureCache[word] = base
|
||||||
|
return base, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildLineFromWords builds a line using cached texture IDs and per-instance color/position.
|
||||||
|
func (p *ScrollerPage) buildLineFromWords(words []string) (Line, error) {
|
||||||
line := Line{words: []StringItem{}}
|
line := Line{words: []StringItem{}}
|
||||||
|
|
||||||
for _, word := range words {
|
for _, word := range words {
|
||||||
if word == "" {
|
if word == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item, err := NewStringItem(word, p.font, p.colorPattern.Color())
|
|
||||||
|
cached, err := p.getCachedWord(word)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create string item for word '%s': %w", word, err)
|
return Line{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Scale(p.fontScale)
|
item := cached
|
||||||
|
item.Color = p.colorPattern.Color()
|
||||||
|
|
||||||
if len(line.words) > 0 {
|
if len(line.words) > 0 {
|
||||||
line.width += p.spaceWidth
|
line.width += p.spaceWidth
|
||||||
@@ -392,18 +406,12 @@ func (p *ScrollerPage) createLine(index int, isInitial bool) error {
|
|||||||
line.width += item.W
|
line.width += item.W
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate horizontal position (centered)
|
return line, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// positionLine sets X/Y for all words in a line.
|
||||||
|
func (p *ScrollerPage) positionLine(line *Line, baseY int32) {
|
||||||
startX := p.ScreenCenterX - (line.width / 2)
|
startX := p.ScreenCenterX - (line.width / 2)
|
||||||
|
|
||||||
// Calculate vertical position
|
|
||||||
var baseY int32
|
|
||||||
if isInitial {
|
|
||||||
baseY = int32(p.scrollY) - int32(p.lineHeight)*int32(index)
|
|
||||||
} else {
|
|
||||||
baseY = 0 // Will be set by caller based on context
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position all words in the line
|
|
||||||
for j := range line.words {
|
for j := range line.words {
|
||||||
if j == 0 {
|
if j == 0 {
|
||||||
line.words[j].X = startX
|
line.words[j].X = startX
|
||||||
@@ -412,6 +420,25 @@ func (p *ScrollerPage) createLine(index int, isInitial bool) error {
|
|||||||
}
|
}
|
||||||
line.words[j].Y = baseY
|
line.words[j].Y = baseY
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createLine is a helper to generate a new line at a specific index.
|
||||||
|
// isInitial determines if this is during initialization (positions from top)
|
||||||
|
// or dynamic creation (positions relative to existing lines).
|
||||||
|
func (p *ScrollerPage) createLine(index int, isInitial bool) error {
|
||||||
|
words := p.nextLineWords()
|
||||||
|
line, err := p.buildLineFromWords(words)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseY int32
|
||||||
|
if isInitial {
|
||||||
|
baseY = int32(p.scrollY) - int32(p.lineHeight)*int32(index)
|
||||||
|
} else {
|
||||||
|
baseY = 0
|
||||||
|
}
|
||||||
|
p.positionLine(&line, baseY)
|
||||||
|
|
||||||
if isInitial {
|
if isInitial {
|
||||||
p.lines.PushBack(line)
|
p.lines.PushBack(line)
|
||||||
@@ -463,10 +490,7 @@ func (p *ScrollerPage) Update(dt float32) error {
|
|||||||
if p.velocity > 0 {
|
if p.velocity > 0 {
|
||||||
// Moving up: replace lines that go off top
|
// Moving up: replace lines that go off top
|
||||||
for p.lines.Len() > 0 && len(p.lines.Front().words) > 0 && p.lines.Front().words[0].Y > p.ScreenHeight {
|
for p.lines.Len() > 0 && len(p.lines.Front().words) > 0 && p.lines.Front().words[0].Y > p.ScreenHeight {
|
||||||
old := p.lines.PopFront()
|
_ = p.lines.PopFront() // no texture delete; textures are shared via cache
|
||||||
for _, word := range old.words {
|
|
||||||
gl.DeleteTextures(1, &word.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.addLineAtBottom(); err != nil {
|
if err := p.addLineAtBottom(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -480,10 +504,7 @@ func (p *ScrollerPage) Update(dt float32) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
old := p.lines.PopBack()
|
_ = p.lines.PopBack() // no texture delete; textures are shared via cache
|
||||||
for _, word := range old.words {
|
|
||||||
gl.DeleteTextures(1, &word.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.addLineAtTop(); err != nil {
|
if err := p.addLineAtTop(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -494,32 +515,14 @@ func (p *ScrollerPage) Update(dt float32) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addLineAtBottom creates and adds a new line at the bottom of the display
|
// addLineAtBottom creates and adds a new line at the bottom of the display.
|
||||||
func (p *ScrollerPage) addLineAtBottom() error {
|
func (p *ScrollerPage) addLineAtBottom() error {
|
||||||
newWords := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx)
|
newWords := p.nextLineWords()
|
||||||
line := Line{words: []StringItem{}}
|
line, err := p.buildLineFromWords(newWords)
|
||||||
|
if err != nil {
|
||||||
for _, word := range newWords {
|
return err
|
||||||
if word == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
item, err := NewStringItem(word, p.font, p.colorPattern.Color())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create string item for word '%s': %w", word, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
item.Scale(p.fontScale)
|
|
||||||
|
|
||||||
if len(line.words) > 0 {
|
|
||||||
line.width += p.spaceWidth
|
|
||||||
}
|
|
||||||
line.words = append(line.words, item)
|
|
||||||
line.width += item.W
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startX := p.ScreenCenterX - (line.width / 2)
|
|
||||||
|
|
||||||
// Position relative to last existing line
|
|
||||||
var baseY int32
|
var baseY int32
|
||||||
if p.lines.Len() > 0 && len(p.lines.Back().words) > 0 {
|
if p.lines.Len() > 0 && len(p.lines.Back().words) > 0 {
|
||||||
baseY = p.lines.Back().words[0].Y - int32(p.lineHeight)
|
baseY = p.lines.Back().words[0].Y - int32(p.lineHeight)
|
||||||
@@ -527,45 +530,19 @@ func (p *ScrollerPage) addLineAtBottom() error {
|
|||||||
baseY = -int32(p.lineHeight)
|
baseY = -int32(p.lineHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
for j := range line.words {
|
p.positionLine(&line, baseY)
|
||||||
if j == 0 {
|
|
||||||
line.words[j].X = startX
|
|
||||||
} else {
|
|
||||||
line.words[j].X = line.words[j-1].X + line.words[j-1].W + p.spaceWidth
|
|
||||||
}
|
|
||||||
line.words[j].Y = baseY
|
|
||||||
}
|
|
||||||
|
|
||||||
p.lines.PushBack(line)
|
p.lines.PushBack(line)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addLineAtTop creates and adds a new line at the top of the display
|
// addLineAtTop creates and adds a new line at the top of the display.
|
||||||
func (p *ScrollerPage) addLineAtTop() error {
|
func (p *ScrollerPage) addLineAtTop() error {
|
||||||
newWords := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx)
|
newWords := p.nextLineWords()
|
||||||
line := Line{words: []StringItem{}}
|
line, err := p.buildLineFromWords(newWords)
|
||||||
|
if err != nil {
|
||||||
for _, word := range newWords {
|
return err
|
||||||
if word == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
item, err := NewStringItem(word, p.font, p.colorPattern.Color())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create string item for word '%s': %w", word, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
item.Scale(p.fontScale)
|
|
||||||
|
|
||||||
if len(line.words) > 0 {
|
|
||||||
line.width += p.spaceWidth
|
|
||||||
}
|
|
||||||
line.words = append(line.words, item)
|
|
||||||
line.width += item.W
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startX := p.ScreenCenterX - (line.width / 2)
|
|
||||||
|
|
||||||
// Position relative to first existing line
|
|
||||||
var baseY int32
|
var baseY int32
|
||||||
if p.lines.Len() > 0 && len(p.lines.Front().words) > 0 {
|
if p.lines.Len() > 0 && len(p.lines.Front().words) > 0 {
|
||||||
baseY = p.lines.Front().words[0].Y + int32(p.lineHeight)
|
baseY = p.lines.Front().words[0].Y + int32(p.lineHeight)
|
||||||
@@ -573,15 +550,7 @@ func (p *ScrollerPage) addLineAtTop() error {
|
|||||||
baseY = p.ScreenHeight
|
baseY = p.ScreenHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
for j := range line.words {
|
p.positionLine(&line, baseY)
|
||||||
if j == 0 {
|
|
||||||
line.words[j].X = startX
|
|
||||||
} else {
|
|
||||||
line.words[j].X = line.words[j-1].X + line.words[j-1].W + p.spaceWidth
|
|
||||||
}
|
|
||||||
line.words[j].Y = baseY
|
|
||||||
}
|
|
||||||
|
|
||||||
p.lines.PushFront(line)
|
p.lines.PushFront(line)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -608,40 +577,25 @@ func (p *ScrollerPage) Destroy() error {
|
|||||||
|
|
||||||
mixer.Stop(1000, p.Context.CfgPtr)
|
mixer.Stop(1000, p.Context.CfgPtr)
|
||||||
|
|
||||||
// GL cleanup — delete all text textures
|
// Delete each cached word texture exactly once.
|
||||||
if p.lines != nil {
|
for k, item := range p.wordTextureCache {
|
||||||
for i := 0; i < p.lines.Len(); i++ {
|
if item.ID != 0 {
|
||||||
line := p.lines.Get(i)
|
id := item.ID
|
||||||
for j := range line.words {
|
gl.DeleteTextures(1, &id)
|
||||||
if id := line.words[j].ID; id != 0 {
|
|
||||||
gl.DeleteTextures(1, &line.words[j].ID)
|
|
||||||
line.words[j].ID = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
delete(p.wordTextureCache, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
gl.Flush()
|
gl.Flush()
|
||||||
|
|
||||||
// Drop references for GC safely
|
// Drop references for GC safely
|
||||||
if p.lines != nil {
|
p.lines = nil
|
||||||
p.lines = nil
|
p.streams = nil
|
||||||
}
|
p.lineCtx = nil
|
||||||
if p.words != nil {
|
p.font = nil
|
||||||
p.words = nil
|
p.shader = nil
|
||||||
}
|
p.colorPattern = nil
|
||||||
if p.lineCtx != nil {
|
p.wordTextureCache = nil
|
||||||
p.lineCtx = nil
|
|
||||||
}
|
|
||||||
if p.font != nil {
|
|
||||||
p.font = nil
|
|
||||||
}
|
|
||||||
if p.shader != nil {
|
|
||||||
p.shader = nil
|
|
||||||
}
|
|
||||||
if p.colorPattern != nil {
|
|
||||||
p.colorPattern = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -662,7 +616,7 @@ func (p *ScrollerPage) SetAutoVelocity(enabled bool, floor, ceiling, step, delay
|
|||||||
p.velocity = p.velocityFloor
|
p.velocity = p.velocityFloor
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateAutoVelocity handles automatic velocity changes
|
// updateAutoVelocity handles automatic velocity changes.
|
||||||
func (p *ScrollerPage) updateAutoVelocity(dt float32) {
|
func (p *ScrollerPage) updateAutoVelocity(dt float32) {
|
||||||
if !p.autoVelocity {
|
if !p.autoVelocity {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user