20 std::lock_guard<std::mutex> lock(
ttf_mutex);
29 std::lock_guard<std::mutex> lock(
ttf_mutex);
40 const auto begin = value.find_first_not_of(
" \t\r\n");
41 if (begin == std::string::npos) {
44 const auto end = value.find_last_not_of(
" \t\r\n");
45 return value.substr(begin, end - begin + 1);
50 const std::string trimmed =
trim_copy(value);
51 const char *begin = trimmed.data();
52 const char *end = trimmed.data() + trimmed.size();
53 const std::from_chars_result result = std::from_chars(begin, end, parsed, base);
54 if (result.ec != std::errc{} || result.ptr != end || parsed < 0 || parsed > 255) {
57 return static_cast<Uint8
>(parsed);
61 const std::string value =
trim_copy(spec);
66 if (value.front() ==
'#') {
67 if (value.size() != 7) {
76 return SDL_Color{*r, *g, *b, 255};
79 const std::size_t first = value.find(
',');
80 if (first == std::string::npos) {
83 const std::size_t second = value.find(
',', first + 1);
84 if (second == std::string::npos || value.find(
',', second + 1) != std::string::npos) {
94 return SDL_Color{*r, *g, *b, 255};
98 return [base_color](
int level) {
99 constexpr int max_level = 7;
100 const float t = std::clamp(
static_cast<float>(level) /
static_cast<float>(max_level), 0.0f, 1.0f);
101 const Uint8 r =
static_cast<Uint8
>(std::lerp(0.0f,
static_cast<float>(base_color.r), std::pow(t, 1.0f)));
102 const Uint8 g =
static_cast<Uint8
>(std::lerp(0.0f,
static_cast<float>(base_color.g), std::pow(t, 1.0f)));
103 const Uint8 b =
static_cast<Uint8
>(std::lerp(0.0f,
static_cast<float>(base_color.b), std::pow(t, 1.0f)));
104 const Uint8 a =
static_cast<Uint8
>(std::lerp(95.0f, 235.0f, t));
105 return SDL_Color{r, g, b, a};
110 return [base_color]() {
111 return SDL_Color{base_color.r, base_color.g, base_color.b, 255};
116 void Rain::SurfaceDeleter::operator()(SDL_Surface *
surface)
const {
122 void Rain::TtfDeleter::operator()(TTF_Font *font)
const {
123 if (font !=
nullptr) {
129 constexpr int max_level = 7;
130 const float t = std::clamp(
static_cast<float>(level) /
static_cast<float>(max_level), 0.0f, 1.0f);
131 const Uint8 r =
static_cast<Uint8
>(std::lerp(0.0f, 48.0f, std::pow(t, 2.9f)));
132 const Uint8 g =
static_cast<Uint8
>(std::lerp(42.0f, 205.0f, std::pow(t, 0.82f)));
133 const Uint8 b =
static_cast<Uint8
>(std::lerp(0.0f, 24.0f, std::pow(t, 3.1f)));
134 const Uint8 a =
static_cast<Uint8
>(std::lerp(95.0f, 235.0f, t));
135 return SDL_Color{r, g, b, a};
139 return SDL_Color{208, 255, 200, 255};
144 0xFF66, 0xFF67, 0xFF68, 0xFF69, 0xFF6A, 0xFF6B, 0xFF6C, 0xFF6D,
145 0xFF6E, 0xFF6F, 0xFF71, 0xFF72, 0xFF73, 0xFF74, 0xFF75, 0xFF76,
146 0xFF77, 0xFF78, 0xFF79, 0xFF7A, 0xFF7B, 0xFF7C, 0xFF7D, 0xFF7E,
147 0xFF7F, 0xFF80, 0xFF81, 0xFF82, 0xFF83, 0xFF84, 0xFF85, 0xFF86,
148 0xFF87, 0xFF88, 0xFF89, 0xFF8A, 0xFF8B, 0xFF8C, 0xFF8D, 0xFF8E,
149 0xFF8F, 0xFF90, 0xFF91, 0xFF92, 0xFF93, 0xFF94, 0xFF95, 0xFF96,
150 0xFF97, 0xFF98, 0xFF99, 0xFF9A, 0xFF9B, 0xFF9C, 0xFF9D};
159 config.
font_path = asset_root +
"/data/NotoSansCJK-Bold.ttc";
167 :
Rain(std::move(rain_config), SurfaceMode::window_driven) {
172 :
Rain(std::move(rain_config), SurfaceMode::explicit_default) {}
175 : config(std::move(rain_config)), rng(std::random_device{}()) {
177 throw mxvk::Exception(
"Matrix rain requires a font_path");
180 throw mxvk::Exception(
"Matrix rain requires at least one symbol");
183 config.level_color = &Rain::matrix_color;
186 config.head_color = &Rain::head_color;
188 if (!config.
color.empty()) {
189 const std::optional<SDL_Color> parsed_color = parse_color_spec(config.color);
191 throw mxvk::Exception(
"Invalid matrix rain color: " + config.color +
" (expected #RRGGBB or R,G,B)");
193 config.
level_color = make_tinted_level_color(*parsed_color);
194 config.
head_color = make_tinted_head_color(*parsed_color);
196 if (mode == SurfaceMode::explicit_default && config.surface_width <= 0 && config.surface_height <= 0) {
197 config.surface_width = default_surface_width;
198 config.surface_height = default_surface_height;
201 throw mxvk::Exception(
"Failed to initialize SDL_ttf: " + std::string(SDL_GetError()));
205 font.reset(TTF_OpenFont(config.font_path.c_str(), config.font_size));
208 ttf_acquired =
false;
209 throw mxvk::Exception(
"Failed to load matrix font: " + config.font_path +
": " + std::string(SDL_GetError()));
212 TTF_SetFontHinting(font.get(), TTF_HINTING_LIGHT);
214 if (config.surface_width > 0 && config.surface_height > 0) {
215 resize(config.surface_width, config.surface_height);
217 last_frame = std::chrono::steady_clock::now();
223 Rain::Rain(
const std::string &asset_root_path,
const bool binary_glyph_mode)
237 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_SPACE) {
244 rebuild_for_extent(window);
252 canvas.reset(SDL_CreateSurface(
width,
height, SDL_PIXELFORMAT_RGBA32));
253 if (canvas ==
nullptr) {
254 throw mxvk::Exception(
"Failed to create matrix canvas: " + std::string(SDL_GetError()));
258 columns = std::max(1, (
width + cell_w - 1) / cell_w);
259 rows = std::max(1, (
height + cell_h - 1) / cell_h);
260 streams.assign(columns, {});
265 dt = std::clamp(dt, 0.0f, 1.0f / 15.0f);
266 if (canvas ==
nullptr || glyphs.empty()) {
273 if (rain_sprite ==
nullptr || canvas ==
nullptr) {
276 rain_sprite->updateTexture(canvas->pixels, canvas->w, canvas->h, canvas->pitch);
280 if (canvas ==
nullptr) {
283 render(canvas->w, canvas->h);
288 if (rain_sprite ==
nullptr || canvas ==
nullptr) {
295 opacity = std::clamp(value, 0.0f, 1.0f);
302 last_frame = std::chrono::steady_clock::now();
306 const auto now = std::chrono::steady_clock::now();
307 float dt = std::chrono::duration<float>(now - last_frame).count();
315 const auto now = std::chrono::steady_clock::now();
316 float dt = std::chrono::duration<float>(now - last_frame).count();
320 render(render_width, render_height);
336 return canvas !=
nullptr ? canvas->pixels :
nullptr;
340 return canvas !=
nullptr ? canvas->w : 0;
344 return canvas !=
nullptr ? canvas->h : 0;
348 return canvas !=
nullptr ? canvas->pitch : 0;
351 void Rain::load_glyphs() {
352 const std::vector<Uint32> &symbols = config.
symbols;
356 glyphs.reserve(symbols.size());
357 for (
const Uint32 codepoint : symbols) {
359 glyph.codepoint = codepoint;
361 for (
int level = 0; level <= config.
glyph_levels; ++level) {
362 SDL_Surface *rendered = TTF_RenderGlyph_Blended(font.get(), codepoint, config.
level_color(level));
363 if (rendered ==
nullptr) {
366 SurfacePtr converted(SDL_ConvertSurface(rendered, SDL_PIXELFORMAT_RGBA32));
367 SDL_DestroySurface(rendered);
368 if (converted !=
nullptr) {
369 SDL_SetSurfaceBlendMode(converted.get(), SDL_BLENDMODE_BLEND);
370 max_glyph_w = std::max(max_glyph_w, converted->w);
371 max_glyph_h = std::max(max_glyph_h, converted->h);
372 glyph.levels.emplace_back(std::move(converted));
375 SDL_Surface *rendered_head = TTF_RenderGlyph_Blended(font.get(), codepoint, config.head_color());
376 if (rendered_head !=
nullptr) {
377 SurfacePtr converted(SDL_ConvertSurface(rendered_head, SDL_PIXELFORMAT_RGBA32));
378 SDL_DestroySurface(rendered_head);
379 if (converted !=
nullptr) {
380 SDL_SetSurfaceBlendMode(converted.get(), SDL_BLENDMODE_BLEND);
381 max_glyph_w = std::max(max_glyph_w, converted->w);
382 max_glyph_h = std::max(max_glyph_h, converted->h);
383 glyph.levels.emplace_back(std::move(converted));
386 if (glyph.levels.size() ==
static_cast<std::size_t
>(config.glyph_levels + 2)) {
387 glyphs.emplace_back(std::move(glyph));
391 if (glyphs.empty()) {
392 throw mxvk::Exception(
"Matrix demo could not render any glyphs from the bundled font");
395 cell_w = std::max(config.min_cell_width, max_glyph_w + config.cell_padding_width);
396 cell_h = std::max(config.min_cell_height, max_glyph_h + config.cell_padding_height);
399 void Rain::rebuild_for_extent(mxvk::VK_Window &window) {
401 const int window_width =
static_cast<int>(extent.width);
402 const int window_height =
static_cast<int>(extent.height);
403 if (window_width <= 0 || window_height <= 0) {
407 const int target_width = (config.surface_width > 0)
408 ? config.surface_width
409 : std::max(1, static_cast<int>(std::lround(static_cast<float>(window_width) * config.surface_scale)));
410 const int target_height = (config.surface_height > 0)
411 ? config.surface_height
412 : std::max(1,
static_cast<int>(std::lround(
static_cast<float>(window_height) * config.surface_scale)));
414 const bool resized = (canvas ==
nullptr || canvas->w != target_width || canvas->h != target_height);
416 resize(target_width, target_height);
419 if (rain_sprite ==
nullptr) {
423 if (resized || streams.empty()) {
424 columns = std::max(1, (target_width + cell_w - 1) / cell_w);
425 rows = std::max(1, (target_height + cell_h - 1) / cell_h);
426 streams.assign(columns, {});
431 void Rain::randomize_streams() {
432 if (streams.empty() || glyphs.empty()) {
436 std::uniform_real_distribution<float> head_dist(-
static_cast<float>(rows) * config.initial_head_margin, 0.0f);
437 std::uniform_real_distribution<float> speed_dist(config.initial_speed_min, config.initial_speed_max);
438 std::uniform_int_distribution<int> length_dist(config.initial_length_min, config.initial_length_max);
439 std::uniform_int_distribution<int> glyph_dist(0,
static_cast<int>(glyphs.size() - 1));
440 std::uniform_int_distribution<int> shimmer_dist(0, 1200);
442 for (Stream &stream : streams) {
443 stream.head = head_dist(rng);
444 stream.speed = speed_dist(rng);
445 stream.length = std::min(std::max(config.initial_min_tail_rows, rows), length_dist(rng));
446 stream.glyphOffset = glyph_dist(rng);
447 stream.shimmer = shimmer_dist(rng);
451 void Rain::update_rain(
float dt) {
454 for (
int column = 0; column < columns; ++column) {
455 Stream &stream = streams[column];
456 stream.head += stream.speed * dt;
457 if (stream.head -
static_cast<float>(stream.length) >
static_cast<float>(rows + config.overscan_rows)) {
458 reset_stream(stream);
460 draw_stream(column, stream);
465 void Rain::reset_stream(Stream &stream) {
466 std::uniform_real_distribution<float> head_dist(-
static_cast<float>(rows) * config.reset_head_margin, -1.0f);
467 std::uniform_real_distribution<float> speed_dist(config.reset_speed_min, config.reset_speed_max);
468 std::uniform_int_distribution<int> length_dist(config.reset_length_min, config.reset_length_max);
469 std::uniform_int_distribution<int> glyph_dist(0,
static_cast<int>(glyphs.size() - 1));
470 std::uniform_int_distribution<int> shimmer_dist(0, 1200);
471 stream.head = head_dist(rng);
472 stream.speed = speed_dist(rng);
473 stream.length = std::min(std::max(config.reset_min_tail_rows, rows + rows / 4), length_dist(rng));
474 stream.glyphOffset = glyph_dist(rng);
475 stream.shimmer = shimmer_dist(rng);
478 void Rain::fade_canvas(
float dt) {
479 const float fade = std::pow(config.fade_base, dt * config.fade_exponent);
480 const float opacity_scale = opacity;
481 if (!SDL_LockSurface(canvas.get())) {
484 auto *
pixels =
static_cast<Uint8 *
>(canvas->pixels);
485 for (
int y = 0; y < canvas->h; ++y) {
486 Uint8 *row =
pixels + y * canvas->pitch;
487 for (
int x = 0; x < canvas->w; ++x) {
488 Uint8 *p = row + x * 4;
489 p[0] =
static_cast<Uint8
>(
static_cast<float>(p[0]) * fade * config.fade_red_scale);
490 p[1] =
static_cast<Uint8
>(
static_cast<float>(p[1]) * fade * config.fade_green_scale);
491 p[2] =
static_cast<Uint8
>(
static_cast<float>(p[2]) * fade * config.fade_blue_scale);
492 p[3] =
static_cast<Uint8
>(
static_cast<float>(p[3]) * fade * opacity_scale);
495 SDL_UnlockSurface(canvas.get());
498 void Rain::draw_stream(
int column,
const Stream &stream) {
499 const int head_row =
static_cast<int>(std::floor(stream.head));
500 const int x = column * cell_w;
501 for (
int tail = stream.length; tail >= 0; --tail) {
502 const int row = head_row - tail;
503 if (row < -1 || row >= rows) {
506 const float age =
static_cast<float>(tail) /
static_cast<float>(std::max(1, stream.length));
507 int level = config.glyph_levels -
static_cast<int>(std::round(age *
static_cast<float>(config.glyph_levels + 1)));
509 level = config.glyph_levels + 1;
510 }
else if (tail <= config.near_head_tail_threshold) {
511 level = config.glyph_levels;
513 level = std::clamp(level, 0, config.glyph_levels + 1);
514 const int glyph_index = (stream.glyphOffset + row * 17 + column * 11 + stream.shimmer + frame_counter / 3 + tail * 5) %
515 static_cast<int>(glyphs.size());
516 const Glyph &glyph = glyphs[glyph_index < 0 ? glyph_index + static_cast<int>(glyphs.size()) : glyph_index];
517 if (glyph.levels.empty()) {
520 const SDL_Rect clear_rect{x, row * cell_h, cell_w, cell_h};
521 SDL_FillSurfaceRect(canvas.get(), &clear_rect, SDL_MapSurfaceRGBA(canvas.get(), 0, 0, 0, 0));
522 SDL_Surface *
surface = glyph.levels[level].get();
523 Uint8 previous_alpha = 255;
524 SDL_GetSurfaceAlphaMod(
surface, &previous_alpha);
525 const Uint8 opacity_alpha =
static_cast<Uint8
>(std::lround(std::clamp(opacity, 0.0f, 1.0f) * 255.0f));
527 const int glyph_x = x + (cell_w - glyph.levels[level]->w) / 2;
528 const int glyph_y = row * cell_h;
529 const int glow_alpha =
static_cast<int>(
530 std::lround(((tail == 0)
531 ? config.head_glow_alpha
532 : (tail <= config.near_head_tail_threshold ? config.near_head_glow_alpha
533 : config.trail_glow_alpha)) *
536 SDL_SetSurfaceAlphaMod(
surface,
static_cast<Uint8
>((glow_alpha * opacity_alpha) / 255));
537 SDL_Rect glow_dst{glyph_x - 1, glyph_y, glyph.levels[level]->w, glyph.levels[level]->h};
538 SDL_BlitSurface(
surface,
nullptr, canvas.get(), &glow_dst);
539 glow_dst.x = glyph_x + 1;
540 glow_dst.y = glyph_y;
541 SDL_BlitSurface(
surface,
nullptr, canvas.get(), &glow_dst);
542 glow_dst.x = glyph_x;
543 glow_dst.y = glyph_y + 1;
544 SDL_BlitSurface(
surface,
nullptr, canvas.get(), &glow_dst);
546 SDL_SetSurfaceAlphaMod(
surface,
static_cast<Uint8
>((
static_cast<int>(previous_alpha) * opacity_alpha) / 255));
547 SDL_Rect dst{glyph_x, glyph_y, glyph.levels[level]->w, glyph.levels[level]->h};
548 SDL_BlitSurface(
surface,
nullptr, canvas.get(), &dst);
549 SDL_SetSurfaceAlphaMod(
surface, previous_alpha);
553 void Rain::clear_canvas() {
554 if (canvas !=
nullptr) {
555 SDL_FillSurfaceRect(canvas.get(),
nullptr, SDL_MapSurfaceRGBA(canvas.get(), 0, 0, 0, 0));