#pragma once #include "fibonacci_line_pattern.h" #include "page.h" #include "scroller_streams.h" #include #include #include #include #include #include struct AppState; // Full-screen page showing proclamation text scrolling vertically using the // Fibonacci line-length pattern, per-frequency color palettes, and crossfaded // solfeggio audio. Proclamation text is never rendered as human-readable type: // every code point is rasterized and permuted into a scrambled glyph tile by // the Fibonacci affine map (see scramble/fibonacci_scrambler.h), and words are // assembled from those tiles. ESC returns to the previous page (Home). class ScrollerPage : public Page { public: void onPush(AppState& state) override; void onResume(AppState& state) override; void onPause(AppState& state) override; void onPop(AppState& state) override; void handleEvent(const SDL_Event& event) override; void update(float dt) override; void render(SDL_Renderer* renderer) override; private: struct WordItem { SDL_Texture* texture = nullptr; std::string text; float width = 0; float height = 0; float x = 0; // relative to the line's left edge float y = 0; }; struct ScrollerLine { std::vector words; float width = 0; uint8_t r = 255; uint8_t g = 255; uint8_t b = 255; }; struct CachedWord { SDL_Texture* texture = nullptr; float width = 0; float height = 0; }; // A single glyph's scrambled tile -- [This implementation] rendering // decision: scrambling is per code point so any glyph in the proclamation // set is permuted exactly once per font size, then reused by every word // that contains it. // `surface` is the N x N (N = max(glyphW, glyphH)) RGBA8888 canvas // produced by the Fibonacci affine scrambler. glyphW x glyphH is the // size of the ORIGINAL, unscrambled glyph (the top-left W x H // sub-rectangle of the tile, matching the crop-window display // convention); word assembly blits exactly that crop. `advance` is the // glyph's unmodified TTF advance width, so per-glyph assembly keeps word // layout identical to plain text rendering. Every glyph surface carries // the font's full line height, so crops align on the shared baseline. struct CharEntry { SDL_Surface* surface = nullptr; float glyphW = 0; float glyphH = 0; float advance = 0; }; void buildStreams(AppState& state); void createFont(AppState& state); void createPatterns(); int computePeakWordCount(const std::vector& words) const; void destroyResources(); void rebuildWordTextures(); void prefillLines(); void addLineAt(float baseY); void advanceStream(); void switchAudioForCurrentStream(); void updateAutoVelocity(float dt); void ensureWordTexture(const std::string& word); void ensureCharTexture(const std::string& glyph); void clearCharTextures(); float measureWidth(const std::string& text) const; std::string audioFilePath(const std::string& filename) const; AppState* state_ = nullptr; std::vector streams_; std::vector fallbackPalette_; std::vector patterns_; int streamIndex_ = 0; bool prefilling_ = false; TTF_Font* font_ = nullptr; int fontPxSize_ = 32; float lineHeight_ = 0; float spaceWidth_ = 0; float screenW_ = 0; float screenH_ = 0; float maxLineWidth_ = 0; std::deque lines_; std::unordered_map wordTextures_; std::unordered_map charTextures_; float scrollOffset_ = 0; float velocity_ = -2750.0f; float velocityStep_ = 100.0f; float velocityDelay_ = 2.5f; int velocityState_ = 0; float velocityTimer_ = 0.0f; };