commits
tags
#include "torlinkc/ui/logo.hpp"
#include <cmath>
#include <optional>
#include <string>
#include <vector>
#include <ftxui/component/animation.hpp>
#include <ftxui/component/component_base.hpp>
using namespace ftxui;
namespace torlinkc::ui {
namespace {
// The original torlink block-art wordmark: a block font has no real
// upper/lowercase distinction, so this already reads as "TORLINK". Ported
// from ui/logo.ts::LOGO_LINES, with "C++" appended in the same half-block
// glyph style (C reuses the O/R glyphs' left stroke + open top/bottom caps;
// + uses the half-block seam trick -- a bottom-half cell stacked on a
// top-half cell lines up into a full-height crossbar at the row seam).
const std::vector<std::string> kLogoLines = {
" π ",
" βββ βββ βββ β β ββ β βββ βββ βββ βββ",
" β βββ βββ βββ β β ββ β β βββ βββ βββ",
};
// accent (#a78bfa) -> bright (#d8b4fe), matching ui/theme.ts::ACCENT_RAMP.
constexpr int kAccentR = 167, kAccentG = 139, kAccentB = 250;
constexpr int kBrightR = 216, kBrightG = 180, kBrightB = 254;
// The splash-screen sweep: a cosine-bell highlight travelling across the
// wordmark's columns. Same shape as ui/sheen.ts's progress-bar sheen, ported
// to glyph columns instead of cells (kept separate rather than shared, since
// the two operate in different units and sheen.ts is React/Ink-specific).
constexpr int kPeakR = 244, kPeakG = 239, kPeakB = 255; // #f4efff
constexpr float kSweepRadius = 4.5f; // bell half-width, in glyph columns
constexpr float kSweepGap = 8.0f; // dark columns between sweeps
constexpr float kSweepSpeed = 11.25f; // columns per second
constexpr float kSweepMax = 0.9f; // peak blend toward the highlight
float sweepPeriod() { return static_cast<float>(kLogoWidth) + kSweepRadius * 2.0f + kSweepGap; }
float sweepIntensity(float column, float center) {
const float d = std::fabs(column - center);
if (d >= kSweepRadius) return 0.0f;
return 0.5f * (1.0f + std::cos(static_cast<float>(M_PI) * d / kSweepRadius)) * kSweepMax;
}
std::vector<std::string> splitUtf8(const std::string& s) {
std::vector<std::string> out;
std::size_t i = 0;
while (i < s.size()) {
const unsigned char c = static_cast<unsigned char>(s[i]);
std::size_t len = 1;
if ((c & 0xE0) == 0xC0) len = 2;
else if ((c & 0xF0) == 0xE0) len = 3;
else if ((c & 0xF8) == 0xF0) len = 4;
len = std::min(len, s.size() - i);
out.push_back(s.substr(i, len));
i += len;
}
return out;
}
int lerpInt(int a, int b, float t) { return static_cast<int>(std::lround(a + (b - a) * t)); }
Color lerpAccent(float t) { return Color::RGB(lerpInt(kAccentR, kBrightR, t), lerpInt(kAccentG, kBrightG, t), lerpInt(kAccentB, kBrightB, t)); }
// Base accent->bright gradient at column fraction `t`, with the sweep
// highlight (if any) blended in at raw column `column`.
Color cellColor(float t, std::size_t column, std::optional<float> sweepCenter) {
const int baseR = lerpInt(kAccentR, kBrightR, t);
const int baseG = lerpInt(kAccentG, kBrightG, t);
const int baseB = lerpInt(kAccentB, kBrightB, t);
if (!sweepCenter) return Color::RGB(baseR, baseG, baseB);
const float intensity = sweepIntensity(static_cast<float>(column), *sweepCenter);
if (intensity <= 0.0f) return Color::RGB(baseR, baseG, baseB);
return Color::RGB(lerpInt(baseR, kPeakR, intensity), lerpInt(baseG, kPeakG, intensity), lerpInt(baseB, kPeakB, intensity));
}
Element renderLogoImpl(std::optional<float> sweepCenter) {
Elements rows;
for (const auto& line : kLogoLines) {
const auto glyphs = splitUtf8(line);
Elements cells;
cells.reserve(glyphs.size());
for (std::size_t i = 0; i < glyphs.size(); ++i) {
const float t = glyphs.size() <= 1 ? 0.0f : static_cast<float>(i) / static_cast<float>(glyphs.size() - 1);
cells.push_back(text(glyphs[i]) | color(cellColor(t, i, sweepCenter)));
}
rows.push_back(hbox(std::move(cells)));
}
return vbox(std::move(rows)) | bold;
}
class AnimatedLogo : public ComponentBase {
public:
explicit AnimatedLogo(std::function<bool()> isActive) : isActive_(std::move(isActive)) {}
Element Render() override {
const float center = std::fmod(elapsed_ * kSweepSpeed, sweepPeriod()) - kSweepRadius;
return renderLogoImpl(center);
}
void OnAnimation(animation::Params& params) override {
if (!isActive_()) return;
elapsed_ = std::fmod(elapsed_ + params.duration().count(), sweepPeriod() / kSweepSpeed);
animation::RequestAnimationFrame();
}
private:
std::function<bool()> isActive_;
float elapsed_ = 0.0f;
};
} // namespace
Element renderLogo() { return renderLogoImpl(std::nullopt); }
Element renderLogo(float sweepCenter) { return renderLogoImpl(sweepCenter); }
Component MakeAnimatedLogo(std::function<bool()> isActive) { return Make<AnimatedLogo>(std::move(isActive)); }
} // namespace torlinkc::ui