Files
radiantwave/src/scroller_page.h
T
2026-08-07 14:51:41 -05:00

119 lines
4.0 KiB
C++

#pragma once
#include "fibonacci_line_pattern.h"
#include "page.h"
#include "scroller_streams.h"
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <deque>
#include <string>
#include <unordered_map>
#include <vector>
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<WordItem> 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<std::string>& 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<radiantwave::scroller::ProclamationStream> streams_;
std::vector<radiantwave::scroller::StreamColor> fallbackPalette_;
std::vector<radiantwave::FibonacciLinePattern> 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<ScrollerLine> lines_;
std::unordered_map<std::string, CachedWord> wordTextures_;
std::unordered_map<std::string, CharEntry> charTextures_;
float scrollOffset_ = 0;
float velocity_ = -2750.0f;
float velocityStep_ = 100.0f;
float velocityDelay_ = 2.5f;
int velocityState_ = 0;
float velocityTimer_ = 0.0f;
};