3 Commits
Author SHA1 Message Date
NeilClack 43cd93bbd7 Switch from window-size to render-output-size 2026-08-25 11:30:20 -05:00
NeilClack 92ba9f39fb Switch from window-size to render-output-size 2026-08-25 11:28:02 -05:00
NeilClack 400c6a782b Scroller: use full window height on rotated/tall displays
build-dev / nix-build (push) Successful in 41s
Override the LETTERBOX logical presentation while the scroller is
active so it renders into the full physical window.  On a 1920x6480
rotated display the scroller now scrolls through all 6480 pixels of
vertical space instead of being confined to the 1920x1080 letterbox
region.  Text scale is unchanged (same font size, same pixel
rasterization); more lines are simply visible at once.

- onPush: save current logical presentation, switch to DISABLED with
  real window dimensions
- onPop: restore the saved LETTERBOX presentation
- resize handler in main.cpp: skip re-apply while scroller is active
  (it owns its own presentation and restores on pop)
2026-08-25 10:55:53 -05:00
3 changed files with 638 additions and 696 deletions
+6 -1
View File
@@ -7,6 +7,7 @@
#include "logical_size.h"
#include "logger/logger.h"
#include "provisioning.h"
#include "scroller_page.h"
namespace {
// How long audio fades out before the app tears down on quit. Only applied
@@ -201,12 +202,16 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
std::to_string(event->window.data1) + "x" +
std::to_string(event->window.data2));
// Logical presentation persists across resizes; re-apply defensively
// in case backend state was reset underneath us.
// in case backend state was reset underneath us. Skip while the
// scroller is active — it owns its own presentation (NONE, full
// window) and restores LETTERBOX in onPop.
if (!dynamic_cast<ScrollerPage *>(state.pages.current())) {
SDL_SetRenderLogicalPresentation(state.renderer,
radiantwave::kDesignWidth,
radiantwave::kDesignHeight,
SDL_LOGICAL_PRESENTATION_LETTERBOX);
}
}
if (!state.keybinds.handleEvent(*event)) {
state.pages.handleEvent(*event);
+203 -270
View File
@@ -2,27 +2,26 @@
#include "app_state.h"
#include "audio/audio_downloader.h"
#include "logical_size.h"
#include "proclamation_options_page.h"
#include "scramble/fibonacci_scrambler.h"
#include "utf8_utils.h"
#include "logical_size.h"
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cstdint>
#include <filesystem>
#include <vector>
using radiantwave::FibonacciLinePattern;
using radiantwave::scroller::ProclamationStream;
namespace
{
namespace {
constexpr float kFadeSeconds = 5.0f;
constexpr float kVelocityMin = -3500.0f;
@@ -30,17 +29,41 @@ namespace
} // anonymous namespace
void ScrollerPage::onPush(AppState &state)
{
void ScrollerPage::onPush(AppState &state) {
state_ = &state;
int winW = 0;
int winH = 0;
radiantwave::getLayoutSize(state.renderer, &winW, &winH);
screenW_ = static_cast<float>(winW);
screenH_ = static_cast<float>(winH);
// Lines are capped at the full screen width; the widest Fibonacci lines are
// sized to fill it with whole words (peak word count derived from the
// Save the current logical presentation (LETTERBOX at design resolution)
// and switch to NONE so we render into the full physical window. This
// lets the scroller scroll the entire vertical extent of tall/rotated
// displays while keeping text at the same pixel scale. onPop restores
// the original presentation for other pages.
SDL_GetRenderLogicalPresentation(state.renderer, &savedLogicalW_,
&savedLogicalH_, &savedMode_);
// Use the renderer's actual output size in pixels, not the window size in
// points. SDL_GetWindowSize() returns screen coordinates, which can be
// smaller than the real drawable if the display has a content scale
// applied (common on some monitors/compositors) — that mismatch caused
// this page to lay out and center within a sub-region of the screen
// instead of the full physical canvas. SDL_GetRenderOutputSize() gives us
// the true pixel dimensions of whatever we're actually rendering into.
int windowW = 0;
int windowH = 0;
SDL_GetRenderOutputSize(state.renderer, &windowW, &windowH);
if (windowW <= 0) {
windowW = radiantwave::kDesignWidth;
}
if (windowH <= 0) {
windowH = radiantwave::kDesignHeight;
}
SDL_SetRenderLogicalPresentation(state.renderer, windowW, windowH,
SDL_LOGICAL_PRESENTATION_DISABLED);
screenW_ = static_cast<float>(windowW);
screenH_ = static_cast<float>(windowH);
// Lines are capped at the full screen width; the widest Fibonacci lines
// are sized to fill it with whole words (peak word count derived from the
// screen), independent of resolution.
maxLineWidth_ = screenW_;
@@ -58,63 +81,59 @@ void ScrollerPage::onPush(AppState &state)
// plays. A frequency file that is not on disk yet falls back to the manual
// track rather than playing nothing.
std::string track = state.config.get().audio.current_track;
if (state.config.get().audio.dynamic_audio && !streams_.empty())
{
if (state.config.get().audio.dynamic_audio && !streams_.empty()) {
const ProclamationStream &stream = streams_[streamIndex_];
if (stream.solfeggio_frequency_id != 0 && !stream.audioFilename.empty() &&
std::filesystem::exists(audioFilePath(stream.audioFilename)))
{
if (stream.solfeggio_frequency_id != 0 &&
!stream.audioFilename.empty() &&
std::filesystem::exists(audioFilePath(stream.audioFilename))) {
track = stream.audioFilename;
}
}
if (std::filesystem::exists(audioFilePath(track)))
{
if (std::filesystem::exists(audioFilePath(track))) {
state.audio.playTrackFadeIn(track, kFadeSeconds);
}
state.keybinds.bind(SDLK_ESCAPE, "scroller_exit", [this]()
{
state.keybinds.bind(SDLK_ESCAPE, "scroller_exit", [this]() {
if (state_) {
state_->pages.pop(*state_);
} });
}
});
}
void ScrollerPage::onResume(AppState &state)
{
void ScrollerPage::onResume(AppState &state) {
state_ = &state;
createFont(state);
createPatterns();
rebuildWordTextures();
state.keybinds.bind(SDLK_ESCAPE, "scroller_exit", [this]()
{
state.keybinds.bind(SDLK_ESCAPE, "scroller_exit", [this]() {
if (state_) {
state_->pages.pop(*state_);
} });
}
});
}
void ScrollerPage::onPause(AppState &state)
{
void ScrollerPage::onPause(AppState &state) {
state.keybinds.unbindAll();
destroyResources();
}
void ScrollerPage::onPop(AppState &state)
{
void ScrollerPage::onPop(AppState &state) {
// Restore the original logical presentation so other pages (Home,
// Settings, etc.) render in the design-resolution LETTERBOX space.
SDL_SetRenderLogicalPresentation(state.renderer, savedLogicalW_,
savedLogicalH_, savedMode_);
state.audio.stopTrackFadeOut(kFadeSeconds);
state.keybinds.unbindAll();
destroyResources();
TTF_Quit();
}
void ScrollerPage::handleEvent(const SDL_Event &)
{
}
void ScrollerPage::handleEvent(const SDL_Event &) {}
void ScrollerPage::update(float dt)
{
if (state_)
{
void ScrollerPage::update(float dt) {
if (state_) {
state_->audio.update(dt);
}
@@ -122,69 +141,55 @@ void ScrollerPage::update(float dt)
scrollOffset_ += velocity_ * dt;
float move = std::floor(scrollOffset_);
if (move != 0.0f)
{
if (move != 0.0f) {
scrollOffset_ -= move;
for (auto &line : lines_)
{
for (auto &w : line.words)
{
for (auto &line : lines_) {
for (auto &w : line.words) {
w.y += move;
}
}
}
if (lines_.empty())
{
if (lines_.empty()) {
return;
}
if (velocity_ > 0.0f)
{
if (velocity_ > 0.0f) {
// Lines move downward. Recycle every line that scrolled fully off the
// bottom this frame (movement is continuous, so one frame can cover
// several line heights) and add a replacement just above the top.
while (!lines_.empty() && lines_.back().words[0].y > screenH_)
{
while (!lines_.empty() && lines_.back().words[0].y > screenH_) {
float baseY = lines_.front().words[0].y - lineHeight_;
lines_.pop_back();
if (!addLineAt(baseY))
{
if (!addLineAt(baseY)) {
break;
}
}
}
else if (velocity_ < 0.0f)
{
} else if (velocity_ < 0.0f) {
// Lines move upward. Recycle every line that scrolled fully off the top
// this frame and add a replacement just below the bottom.
while (!lines_.empty() &&
lines_.front().words[0].y + lines_.front().words[0].height < 0.0f)
{
lines_.front().words[0].y + lines_.front().words[0].height <
0.0f) {
float baseY = lines_.back().words[0].y + lineHeight_;
lines_.pop_front();
if (!addLineAt(baseY))
{
if (!addLineAt(baseY)) {
break;
}
}
}
}
void ScrollerPage::render(SDL_Renderer *renderer)
{
void ScrollerPage::render(SDL_Renderer *renderer) {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
for (const auto &line : lines_)
{
if (line.words.empty())
{
for (const auto &line : lines_) {
if (line.words.empty()) {
continue;
}
float startX = std::floor((screenW_ - line.width) / 2.0f);
for (const auto &w : line.words)
{
for (const auto &w : line.words) {
SDL_SetTextureColorMod(w.texture, line.r, line.g, line.b);
SDL_FRect dst{startX + w.x, w.y, w.width, w.height};
SDL_RenderTexture(renderer, w.texture, nullptr, &dst);
@@ -192,53 +197,46 @@ void ScrollerPage::render(SDL_Renderer *renderer)
}
}
void ScrollerPage::buildStreams(AppState &state)
{
void ScrollerPage::buildStreams(AppState &state) {
streams_.clear();
fallbackPalette_.clear();
lines_.clear();
streamIndex_ = 0;
auto proclamations = state.db.getActiveProclamations();
if (proclamations.empty())
{
if (proclamations.empty()) {
return;
}
for (const auto &c : state.db.getPaletteForFrequencyId(5))
{ // 528 Hz palette
for (const auto &c :
state.db.getPaletteForFrequencyId(5)) { // 528 Hz palette
fallbackPalette_.push_back({c.r, c.g, c.b, c.a});
}
std::unordered_map<int, std::vector<radiantwave::scroller::StreamColor>> palettesByFreqId;
for (const auto &rec : proclamations)
{
std::unordered_map<int, std::vector<radiantwave::scroller::StreamColor>>
palettesByFreqId;
for (const auto &rec : proclamations) {
int freqId = rec.solfeggio_frequency_id;
if (palettesByFreqId.count(freqId) != 0)
{
if (palettesByFreqId.count(freqId) != 0) {
continue;
}
std::vector<radiantwave::scroller::StreamColor> colors;
for (const auto &c : state.db.getPaletteForFrequencyId(freqId))
{
for (const auto &c : state.db.getPaletteForFrequencyId(freqId)) {
colors.push_back({c.r, c.g, c.b, c.a});
}
palettesByFreqId[freqId] = std::move(colors);
}
std::unordered_map<int, radiantwave::db::AudioFileRecord> audioByFreqId;
for (const auto &af : state.db.getAudioFiles())
{
for (const auto &af : state.db.getAudioFiles()) {
int freqId = af.solfeggio_frequency_id;
// Self-heal for databases seeded from a legacy manifest that left the
// frequency id at 0 for every row: recover the id from the filename.
if (freqId == 0)
{
if (freqId == 0) {
freqId = radiantwave::audio::frequencyIdForHz(
radiantwave::audio::hzFromFilename(af.filename));
}
if (freqId != 0)
{
if (freqId != 0) {
audioByFreqId[freqId] = af;
}
}
@@ -248,10 +246,8 @@ void ScrollerPage::buildStreams(AppState &state)
DEFAULT_PROPLEAMATION_ID);
}
void ScrollerPage::createFont(AppState &state)
{
if (font_)
{
void ScrollerPage::createFont(AppState &state) {
if (font_) {
TTF_CloseFont(font_);
font_ = nullptr;
}
@@ -262,46 +258,41 @@ void ScrollerPage::createFont(AppState &state)
clearCharTextures();
fontPxSize_ = state.config.get().proclamation.font_size;
if (fontPxSize_ <= 0)
{
if (fontPxSize_ <= 0) {
fontPxSize_ = 32;
}
font_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", fontPxSize_);
if (!font_)
{
if (!font_) {
return;
}
int w = 0;
int h = 0;
TTF_GetStringSize(font_, "Mg", 0, &w, &h);
lineHeight_ = h > 0 ? static_cast<float>(h) : static_cast<float>(fontPxSize_);
lineHeight_ =
h > 0 ? static_cast<float>(h) : static_cast<float>(fontPxSize_);
spaceWidth_ = measureWidth(" ");
if (spaceWidth_ <= 0.0f)
{
if (spaceWidth_ <= 0.0f) {
spaceWidth_ = static_cast<float>(fontPxSize_) * 0.5f;
}
}
void ScrollerPage::createPatterns()
{
void ScrollerPage::createPatterns() {
patterns_.clear();
patterns_.reserve(streams_.size());
for (size_t i = 0; i < streams_.size(); ++i)
{
for (size_t i = 0; i < streams_.size(); ++i) {
int peak = computePeakWordCount(streams_[i].words);
patterns_.emplace_back(peak, maxLineWidth_, spaceWidth_,
[this](const std::string &word)
{ return measureWidth(word); });
patterns_.emplace_back(
peak, maxLineWidth_, spaceWidth_,
[this](const std::string &word) { return measureWidth(word); });
}
}
int ScrollerPage::computePeakWordCount(const std::vector<std::string> &words) const
{
if (words.empty() || screenW_ <= 0.0f)
{
int ScrollerPage::computePeakWordCount(
const std::vector<std::string> &words) const {
if (words.empty() || screenW_ <= 0.0f) {
return 1;
}
@@ -309,14 +300,12 @@ int ScrollerPage::computePeakWordCount(const std::vector<std::string> &words) co
// spacing. The peak is the number of whole words that fit across the
// screen, so the widest Fibonacci lines fill the width at natural spacing.
double total = 0.0;
for (const auto &w : words)
{
for (const auto &w : words) {
total += measureWidth(w);
}
double n = static_cast<double>(words.size());
double perWord = (total + (n - 1.0) * spaceWidth_) / n;
if (perWord <= 0.0)
{
if (perWord <= 0.0) {
perWord = 1.0;
}
@@ -324,45 +313,36 @@ int ScrollerPage::computePeakWordCount(const std::vector<std::string> &words) co
return fit < 1 ? 1 : fit;
}
void ScrollerPage::destroyResources()
{
void ScrollerPage::destroyResources() {
clearCharTextures();
for (auto &entry : wordTextures_)
{
if (entry.second.texture)
{
for (auto &entry : wordTextures_) {
if (entry.second.texture) {
SDL_DestroyTexture(entry.second.texture);
}
}
wordTextures_.clear();
if (font_)
{
if (font_) {
TTF_CloseFont(font_);
font_ = nullptr;
}
}
void ScrollerPage::rebuildWordTextures()
{
void ScrollerPage::rebuildWordTextures() {
wordTextures_.clear();
for (auto &line : lines_)
{
for (auto &line : lines_) {
float width = 0.0f;
for (auto &item : line.words)
{
for (auto &item : line.words) {
ensureWordTexture(item.text);
const CachedWord &cached = wordTextures_[item.text];
if (!cached.texture)
{
if (!cached.texture) {
continue;
}
item.texture = cached.texture;
item.width = cached.width;
item.height = cached.height;
if (width > 0.0f)
{
if (width > 0.0f) {
width += spaceWidth_;
}
item.x = width;
@@ -372,31 +352,25 @@ void ScrollerPage::rebuildWordTextures()
}
}
void ScrollerPage::prefillLines()
{
if (streams_.empty() || lineHeight_ <= 0.0f)
{
void ScrollerPage::prefillLines() {
if (streams_.empty() || lineHeight_ <= 0.0f) {
return;
}
int n = static_cast<int>(std::ceil(screenH_ / lineHeight_)) + 2;
for (int i = 0; i < n; ++i)
{
for (int i = 0; i < n; ++i) {
float baseY = -lineHeight_ + static_cast<float>(i) * lineHeight_;
addLineAt(baseY);
}
}
bool ScrollerPage::addLineAt(float baseY)
{
if (streams_.empty() || !font_)
{
bool ScrollerPage::addLineAt(float baseY) {
if (streams_.empty() || !font_) {
return false;
}
ProclamationStream &stream = streams_[streamIndex_];
if (stream.words.empty())
{
if (stream.words.empty()) {
advanceStream();
return false;
}
@@ -404,7 +378,8 @@ bool ScrollerPage::addLineAt(float baseY)
FibonacciLinePattern &pattern = patterns_[streamIndex_];
// Capture palette state before any stream advance.
std::vector<radiantwave::scroller::StreamColor> paletteCopy = stream.palette;
std::vector<radiantwave::scroller::StreamColor> paletteCopy =
stream.palette;
int lineColorIndex = stream.colorIndex;
stream.colorIndex++;
@@ -414,17 +389,15 @@ bool ScrollerPage::addLineAt(float baseY)
stream.wordsPlaced += placed;
// First word of a fresh pass is the "start" of the proclamation; print to
// stdout once so the console shows the current title while it plays.
if (stream.wordsPlaced == placed)
{
if (stream.wordsPlaced == placed) {
std::printf("Playing %s\n", stream.name.c_str());
}
bool advanced = false;
while (!advanced && !stream.words.empty() && stream.wordsPlaced >= static_cast<int>(stream.words.size()))
{
while (!advanced && !stream.words.empty() &&
stream.wordsPlaced >= static_cast<int>(stream.words.size())) {
stream.wordsPlaced -= static_cast<int>(stream.words.size());
stream.completed++;
if (stream.completed >= stream.repeat)
{
if (stream.completed >= stream.repeat) {
stream.completed = 0;
advanceStream();
advanced = true;
@@ -433,12 +406,10 @@ bool ScrollerPage::addLineAt(float baseY)
ScrollerLine line;
float width = 0.0f;
for (const auto &text : wordStrs)
{
for (const auto &text : wordStrs) {
ensureWordTexture(text);
const CachedWord &cached = wordTextures_[text];
if (!cached.texture)
{
if (!cached.texture) {
continue;
}
WordItem item;
@@ -447,8 +418,7 @@ bool ScrollerPage::addLineAt(float baseY)
item.width = cached.width;
item.height = cached.height;
item.y = baseY;
if (width > 0.0f)
{
if (width > 0.0f) {
width += spaceWidth_;
}
item.x = width;
@@ -456,77 +426,63 @@ bool ScrollerPage::addLineAt(float baseY)
line.words.push_back(item);
}
if (line.words.empty())
{
if (line.words.empty()) {
return false;
}
line.width = width;
if (!paletteCopy.empty())
{
const auto &c = paletteCopy[lineColorIndex % static_cast<int>(paletteCopy.size())];
if (!paletteCopy.empty()) {
const auto &c =
paletteCopy[lineColorIndex % static_cast<int>(paletteCopy.size())];
line.r = c.r;
line.g = c.g;
line.b = c.b;
}
if (!lines_.empty() && baseY < lines_.front().words[0].y)
{
if (!lines_.empty() && baseY < lines_.front().words[0].y) {
lines_.push_front(std::move(line));
}
else
{
} else {
lines_.push_back(std::move(line));
}
return true;
}
void ScrollerPage::advanceStream()
{
if (streams_.empty())
{
void ScrollerPage::advanceStream() {
if (streams_.empty()) {
return;
}
streamIndex_ = (streamIndex_ + 1) % static_cast<int>(streams_.size());
// Diagnostic: report the palette the incoming stream will render so the
// console shows whether colors should change when this stream starts.
const ProclamationStream &stream = streams_[streamIndex_];
if (!stream.palette.empty())
{
if (!stream.palette.empty()) {
const auto &c = stream.palette[0];
std::printf("[Scroller] -> %s freqId=%d palette[0]=#%02X%02X%02X\n",
stream.name.c_str(), stream.solfeggio_frequency_id,
c.r, c.g, c.b);
}
else
{
stream.name.c_str(), stream.solfeggio_frequency_id, c.r,
c.g, c.b);
} else {
std::printf("[Scroller] -> %s freqId=%d palette=EMPTY\n",
stream.name.c_str(), stream.solfeggio_frequency_id);
}
switchAudioForCurrentStream();
}
void ScrollerPage::switchAudioForCurrentStream()
{
if (prefilling_ || streams_.empty() || !state_)
{
void ScrollerPage::switchAudioForCurrentStream() {
if (prefilling_ || streams_.empty() || !state_) {
return;
}
// Dynamic audio only: with a fixed manual track selected the audio must
// never switch as proclamations change.
if (!state_->config.get().audio.dynamic_audio)
{
if (!state_->config.get().audio.dynamic_audio) {
return;
}
const ProclamationStream &stream = streams_[streamIndex_];
if (stream.solfeggio_frequency_id == 0 || stream.audioFilename.empty())
{
if (stream.solfeggio_frequency_id == 0 || stream.audioFilename.empty()) {
return; // default stream: never touch audio
}
if (!std::filesystem::exists(audioFilePath(stream.audioFilename)))
{
if (!std::filesystem::exists(audioFilePath(stream.audioFilename))) {
return; // keep current track; retried on the next activation
}
// Crossfades over kFadeSeconds; a no-op when the requested track is the
@@ -534,14 +490,11 @@ void ScrollerPage::switchAudioForCurrentStream()
state_->audio.playTrackCrossfade(stream.audioFilename, kFadeSeconds);
}
void ScrollerPage::updateAutoVelocity(float dt)
{
switch (velocityState_)
{
void ScrollerPage::updateAutoVelocity(float dt) {
switch (velocityState_) {
case 0: // ramping to the negative extreme
velocity_ -= velocityStep_ * dt;
if (velocity_ <= kVelocityMin)
{
if (velocity_ <= kVelocityMin) {
velocity_ = kVelocityMin;
velocityTimer_ = velocityDelay_;
velocityState_ = 1;
@@ -549,15 +502,13 @@ void ScrollerPage::updateAutoVelocity(float dt)
break;
case 1: // holding at the negative extreme
velocityTimer_ -= dt;
if (velocityTimer_ <= 0.0f)
{
if (velocityTimer_ <= 0.0f) {
velocityState_ = 2;
}
break;
case 2: // ramping to the positive extreme
velocity_ += velocityStep_ * dt;
if (velocity_ >= kVelocityMax)
{
if (velocity_ >= kVelocityMax) {
velocity_ = kVelocityMax;
velocityTimer_ = velocityDelay_;
velocityState_ = 3;
@@ -565,8 +516,7 @@ void ScrollerPage::updateAutoVelocity(float dt)
break;
case 3: // holding at the positive extreme
velocityTimer_ -= dt;
if (velocityTimer_ <= 0.0f)
{
if (velocityTimer_ <= 0.0f) {
velocityState_ = 0;
}
break;
@@ -576,10 +526,8 @@ void ScrollerPage::updateAutoVelocity(float dt)
}
}
void ScrollerPage::ensureWordTexture(const std::string &word)
{
if (wordTextures_.count(word) != 0)
{
void ScrollerPage::ensureWordTexture(const std::string &word) {
if (wordTextures_.count(word) != 0) {
return;
}
@@ -602,69 +550,64 @@ void ScrollerPage::ensureWordTexture(const std::string &word)
// breaking, centering, and the Fibonacci line pattern are unchanged; only
// the glyph pixels move.
CachedWord cached;
if (font_ && state_)
{
if (font_ && state_) {
std::vector<uint32_t> codePoints;
if (radiantwave::splitUtf8(word, codePoints) && !codePoints.empty())
{
if (radiantwave::splitUtf8(word, codePoints) && !codePoints.empty()) {
std::vector<CharEntry *> glyphs;
float totalWidth = 0.0f;
float glyphHeight = 0.0f;
for (uint32_t cp : codePoints)
{
for (uint32_t cp : codePoints) {
const std::string glyphText = radiantwave::encodeUtf8(cp);
if (glyphText.empty())
{
if (glyphText.empty()) {
continue;
}
ensureCharTexture(glyphText);
CharEntry &entry = charTextures_[glyphText];
if (!entry.surface)
{
if (!entry.surface) {
continue;
}
glyphs.push_back(&entry);
totalWidth += entry.advance;
if (entry.glyphH > glyphHeight)
{
if (entry.glyphH > glyphHeight) {
glyphHeight = entry.glyphH;
}
}
if (!glyphs.empty())
{
const int wordW = std::max(1, static_cast<int>(std::lround(totalWidth)));
const int wordH = std::max(1, static_cast<int>(std::lround(glyphHeight)));
SDL_Surface *wordSurface = SDL_CreateSurface(wordW, wordH, SDL_PIXELFORMAT_RGBA8888);
if (wordSurface)
{
if (!glyphs.empty()) {
const int wordW =
std::max(1, static_cast<int>(std::lround(totalWidth)));
const int wordH =
std::max(1, static_cast<int>(std::lround(glyphHeight)));
SDL_Surface *wordSurface =
SDL_CreateSurface(wordW, wordH, SDL_PIXELFORMAT_RGBA8888);
if (wordSurface) {
// Every glyph surface carries the font's full line height
// (probe-verified), so y = 0 keeps every crop on the shared
// baseline and per-word textures stay line-height tall.
float x = 0.0f;
for (const CharEntry *entry : glyphs)
{
for (const CharEntry *entry : glyphs) {
SDL_Rect srcRect{0, 0, static_cast<int>(entry->glyphW),
static_cast<int>(entry->glyphH)};
SDL_Rect dstRect{static_cast<int>(std::lround(x)), 0,
static_cast<int>(entry->glyphW),
static_cast<int>(entry->glyphH)};
SDL_BlitSurface(entry->surface, &srcRect, wordSurface, &dstRect);
SDL_BlitSurface(entry->surface, &srcRect, wordSurface,
&dstRect);
x += entry->advance;
}
cached.texture = SDL_CreateTextureFromSurface(state_->renderer, wordSurface);
cached.texture = SDL_CreateTextureFromSurface(
state_->renderer, wordSurface);
// The scroller steps glyphs in whole pixels, so bilinear
// filtering can only smear them: nearest-neighbor keeps
// every frame pixel-sharp.
if (cached.texture)
{
SDL_SetTextureScaleMode(cached.texture, SDL_SCALEMODE_NEAREST);
if (cached.texture) {
SDL_SetTextureScaleMode(cached.texture,
SDL_SCALEMODE_NEAREST);
}
cached.width = static_cast<float>(wordSurface->w);
cached.height = static_cast<float>(wordSurface->h);
SDL_DestroySurface(wordSurface);
if (!cached.texture)
{
if (!cached.texture) {
cached.width = 0.0f;
cached.height = 0.0f;
}
@@ -675,10 +618,8 @@ void ScrollerPage::ensureWordTexture(const std::string &word)
wordTextures_[word] = cached;
}
void ScrollerPage::ensureCharTexture(const std::string &glyph)
{
if (charTextures_.count(glyph) != 0)
{
void ScrollerPage::ensureCharTexture(const std::string &glyph) {
if (charTextures_.count(glyph) != 0) {
return;
}
@@ -687,25 +628,23 @@ void ScrollerPage::ensureCharTexture(const std::string &glyph)
// font size; createFont() clears the atlas whenever the font or its size
// changes, and pause/pop clear it as part of destroyResources().
CharEntry entry;
if (font_ && state_)
{
if (font_ && state_) {
SDL_Surface *surface = TTF_RenderText_Blended(font_, glyph.c_str(), 0,
{255, 255, 255, 255});
if (surface)
{
if (surface) {
const int gW = surface->w;
const int gH = surface->h;
const int n = radiantwave::scramble::canvasSize(gW, gH);
if (n > 0)
{
if (n > 0) {
// The scrambler needs tightly packed RGBA8888; a TTF surface's
// pitch can exceed gW * 4, so copy row by row.
std::vector<uint8_t> source(static_cast<size_t>(gW) * gH * 4);
const uint8_t *pixels = static_cast<const uint8_t *>(surface->pixels);
for (int y = 0; y < gH; ++y)
{
const uint8_t *pixels =
static_cast<const uint8_t *>(surface->pixels);
for (int y = 0; y < gH; ++y) {
std::memcpy(source.data() + static_cast<size_t>(y) * gW * 4,
pixels + static_cast<size_t>(y) * surface->pitch,
pixels +
static_cast<size_t>(y) * surface->pitch,
static_cast<size_t>(gW) * 4);
}
@@ -714,25 +653,24 @@ void ScrollerPage::ensureCharTexture(const std::string &glyph)
// The fixed seed makes every device render the same glyph as
// the same scrambled pattern, with no key to persist or ship.
radiantwave::scramble::ScrambleConfig config;
if (radiantwave::scramble::scramble(source.data(), scrambled.data(),
gW, gH, config))
{
SDL_Surface *tile = SDL_CreateSurface(n, n, SDL_PIXELFORMAT_RGBA8888);
if (tile)
{
if (radiantwave::scramble::scramble(
source.data(), scrambled.data(), gW, gH, config)) {
SDL_Surface *tile =
SDL_CreateSurface(n, n, SDL_PIXELFORMAT_RGBA8888);
if (tile) {
uint8_t *dst = static_cast<uint8_t *>(tile->pixels);
for (int y = 0; y < n; ++y)
{
std::memcpy(dst + static_cast<size_t>(y) * tile->pitch,
scrambled.data() + static_cast<size_t>(y) * n * 4,
for (int y = 0; y < n; ++y) {
std::memcpy(dst + static_cast<size_t>(y) *
tile->pitch,
scrambled.data() +
static_cast<size_t>(y) * n * 4,
static_cast<size_t>(n) * 4);
}
entry.surface = tile;
entry.glyphW = static_cast<float>(gW);
entry.glyphH = static_cast<float>(gH);
entry.advance = measureWidth(glyph);
if (entry.advance <= 0.0f)
{
if (entry.advance <= 0.0f) {
entry.advance = entry.glyphW;
}
}
@@ -744,22 +682,17 @@ void ScrollerPage::ensureCharTexture(const std::string &glyph)
charTextures_[glyph] = entry;
}
void ScrollerPage::clearCharTextures()
{
for (auto &entry : charTextures_)
{
if (entry.second.surface)
{
void ScrollerPage::clearCharTextures() {
for (auto &entry : charTextures_) {
if (entry.second.surface) {
SDL_DestroySurface(entry.second.surface);
}
}
charTextures_.clear();
}
float ScrollerPage::measureWidth(const std::string &text) const
{
if (!font_)
{
float ScrollerPage::measureWidth(const std::string &text) const {
if (!font_) {
return 0.0f;
}
int w = 0;
@@ -768,9 +701,9 @@ float ScrollerPage::measureWidth(const std::string &text) const
return static_cast<float>(w);
}
std::string ScrollerPage::audioFilePath(const std::string &filename) const
{
std::string path1 = std::string(std::getenv("HOME") ? std::getenv("HOME") : "/tmp") +
std::string ScrollerPage::audioFilePath(const std::string &filename) const {
std::string path1 =
std::string(std::getenv("HOME") ? std::getenv("HOME") : "/tmp") +
"/.local/radiantwave/audio/" + filename;
std::string path2 = "assets/audio/" + filename;
return std::filesystem::exists(path1) ? path1 : path2;
+4
View File
@@ -115,4 +115,8 @@ private:
float velocityDelay_ = 2.5f;
int velocityState_ = 0;
float velocityTimer_ = 0.0f;
int savedLogicalW_ = 0;
int savedLogicalH_ = 0;
SDL_RendererLogicalPresentation savedMode_ = SDL_LOGICAL_PRESENTATION_DISABLED;
};