foxygit / Torlinkc Log in
commits tags

/src/ui/logo.cpp ยท 4.92 KB

raw
#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