Lumiera
The new emerging NLE for GNU/Linux

commons.hpp

/*
  commons.hpp  -  common definitions and utils for the video output demo code

   Copyright (C)
     2025,            Benny Lyons <benny.lyons@gmx.net>
     2025,            Hermann Vosseler <Ichthyostega@web.de>

  This program is free software; you can redistribute it and/or modify it
  under the terms of the GNU GPL version 2+ See the LICENSE file for details.

* ************************************************************************/

#ifndef COMMONS_H
#define COMMONS_H


#include <cmath>
#include <cstdint>
#include <cstddef>
#include <iostream>
#include <algorithm>
#include <string>
#include <array>
#include <set>

using std::byte;
using uint = unsigned int;
using FrameRate = uint;


inline void
__FAIL (std::string msg)
{
  std::cerr << "FAIL: " << msg << std::endl;
  std::abort();
}

/** shortcut for set value containment test (C++17) */
template <typename T, class CMP, class ALO>
inline bool
contains (std::set<T,CMP,ALO> const& set, T const& val)
{
  return set.end() != set.find (val);
}

/** build an value array holding the given elements inline */
template<typename...TS>
auto
asArray (TS&& ... data)
{
  using TT = std::common_type_t<TS...>;
  return std::array<TT, sizeof...(TS)>{data...};
}




/* == simplistic 2D vector math == */

struct Vec2
  {
    int x{0};
    int y{0};

    Vec2
    operator-()  const
      {
        return {-x, -y};
      }

    friend Vec2
    operator+ (Vec2 const& u, Vec2 const& v)
      {
        return {u.x+v.x, u.y+v.y};
      }
    friend Vec2
    operator- (Vec2 const& u, Vec2 const& v)
      {
        return {u.x-v.x, u.y-v.y};
      }

    friend int
    dot (Vec2 const& u, Vec2 const& v)
      {
        return u.x*v.x + u.y*v.y;
      }

    Vec2&
    operator+= (Vec2 const& o)
      {
        x += o.x;
        y += o.y;
        return *this;
      }

    int
    norm()  const
      {
        return dot (*this,*this);
      }

    friend int
    dist (Vec2 const& u, Vec2 const& v)
      {
        float normDiff = (v - u).norm();
        return int(std::sqrt (normDiff));
      }
  };


/* == 8bit colour triplets == */

using Trip = std::array<byte,3>;

constexpr inline byte
bClamp (int val)
{
  return byte(std::clamp (val, 0,0xFF));
}

constexpr inline Trip
trip (int c1, int c2, int c3)
{
  return Trip{{bClamp(c1)
              ,bClamp(c2)
              ,bClamp(c3)
             }};
}

constexpr inline Trip
gray (int lum =0)
{
  return trip (lum,lum,lum);
}

template<uint idx>
constexpr inline int
c (Trip const& t)
{
  static_assert (idx < t.size());
  return int(t[idx]);
}

constexpr inline Trip
operator* (double fac, Trip const& t)
{
  return trip (fac * c<0>(t)
              ,fac * c<1>(t)
              ,fac * c<2>(t)
              );
}

constexpr inline Trip
operator+ (Trip const& t1, Trip const& t2)
{
  return trip (c<0>(t1) + c<0>(t2)
              ,c<1>(t1) + c<1>(t2)
              ,c<2>(t1) + c<2>(t2)
              );
}

constexpr inline Trip
operator- (Trip const& t1, Trip const& t2)
{
  return trip (c<0>(t1) - c<0>(t2)
              ,c<1>(t1) - c<1>(t2)
              ,c<2>(t1) - c<2>(t2)
              );
}

constexpr inline void
operator += (Trip& c, Trip const& o)
{
  c = c + o;
}


/** closest prime to (2^32 * goldenRatio) % 2^32 */
const uint32_t KNUTH_MAGIC{0x9e3779b1};

/**
 * A cheap source of random bits, based on repeated mixing.
 * @warning fast but not high quality...
 */
inline uint32_t
noise()
{
  static uint32_t state{0x55555555};
  return state ^= KNUTH_MAGIC
                + (state<<6)
                + (state>>2)
                ;
}

/**
 * Manipulate a colour value to add a decay step towards a target colour.
 * When applying this repeatedly to the same colour, it will approach
 * the target colour asymptotically, by an exponential function,
 * because ∂/∂x e^kx ≡ k · e^kx
 * @param feedback controls the strength of the effect
 * @note adding 4 bit of random dither to each channel to avoid banding
 */
inline void
decay (Trip& col, double feedback, Trip const& target)
{
  auto randBits = noise();
  auto dither = [&]{ return 0.07 * (-7.5 + (0xF & (randBits >>= 4))); };
  col = trip (c<0>(col) + feedback * (c<0>(target) - c<0>(col)) + dither()
             ,c<1>(col) + feedback * (c<1>(target) - c<1>(col)) + dither()
             ,c<2>(col) + feedback * (c<2>(target) - c<2>(col)) + dither()
             );
}


#endif /*COMMONS_H*/

image-generator.hpp

/*
  image-generator.cpp  -  generate animated test video frames

   Copyright (C)
     2025,            Hermann Vosseler <Ichthyostega@web.de>

  This program is free software; you can redistribute it and/or modify it
  under the terms of the GNU GPL version 2+ See the LICENSE file for details.

* ************************************************************************/

#ifndef IMAGE_GENERATOR_H
#define IMAGE_GENERATOR_H

#include "commons.hpp"

#include <cstddef>
#include <array>


using uint = unsigned int;
using std::byte;

template<uint W, uint H>
class ImageGenerator
  {

  public:
    using Row = std::array<Trip, W>;
    using Img = std::array<Row,  H>;

    static_assert (sizeof(Img) == W * H * 3);

    using PackedRGB = std::array<Trip, W*H>;


    ImageGenerator(uint fps)
      : fps_{fps}
      , frameNr_{0}
      , img_{Row{Trip{}}}
      { };


    PackedRGB const&
    current()  const
      {
        return reinterpret_cast<PackedRGB const&> (img_);
      }

    /**
     * generate the next frame of the animation.
     * @return reference to the buffer with RGB888 data.
     */
    PackedRGB const&
    buildNext()
      {
        if (frameNr_ == 0)
          initGen();
        animatePos();
        attenuate();
        drawBall();
        ++frameNr_;
        return current();
      }

    uint getFrameNr()  const { return frameNr_; }
    uint getFps()      const { return fps_;     }

  private:
    uint fps_;
    uint frameNr_;
    Img img_;

    void initGen();
    void animatePos();
    void attenuate();
    void drawBall();

    constexpr static auto BLACK = gray (0);
    constexpr static auto WHITE = gray (0xFF);
    constexpr static auto GRAY1 = 0.25 * WHITE;
    constexpr static auto GRAY2 = 0.50 * WHITE;
    constexpr static auto GRAY3 = 0.75 * WHITE;
    constexpr static auto LT_YELLOW = trip (0xFF, 0xFF, 0xE0);
    constexpr static auto DARK_BLUE = trip (0x10, 0   , 0x50);

    constexpr static auto BALL = std::array{BLACK, GRAY3, WHITE, GRAY3, BLACK
                                           ,GRAY3, WHITE, WHITE, WHITE, GRAY3
                                           ,WHITE, WHITE, WHITE, WHITE, WHITE
                                           ,GRAY3, WHITE, WHITE, WHITE, GRAY3
                                           ,BLACK, GRAY3, WHITE, GRAY3, BLACK
                                           };
    constexpr static int BALL_SIZ = std::ceil (std::sqrt (BALL.size()));

    Vec2 p1_, p2_, v1_, v2_;
    double a1_{0}, a2_{0};

    void maybeBounce (Vec2&, Vec2&);
  };


  template<uint W, uint H>
  void
  ImageGenerator<W,H>::initGen()
    {
      std::srand (std::time (nullptr));
      p1_ = {0,0};
      v1_ = { 1 + rand() % 5,  1 + rand() % 5};
      p2_ = {rand() % int(W), rand() % int(H)};
      v2_ = {-1,-1};

      // initially fill background with colour bar pattern
      byte ON {0xE0};
      byte OFF{0};

      // classic NTSC colour bars  --R---G---B--
      std::array<Trip, 7> bars = {{{ ON, ON, ON}
                                  ,{ ON, ON,OFF}
                                  ,{OFF, ON, ON}
                                  ,{OFF, ON,OFF}
                                  ,{ ON,OFF, ON}
                                  ,{ ON,OFF,OFF}
                                  ,{OFF,OFF, ON}
                                 }};
      // create a colour strip pattern in the first row...
      for (uint x = 0; x < W; ++x)
        {  //  quantise into 7 columns
          uint col = x  * 7/W;
          img_[0][x] = bars[col];
        }
       // fill remaining rows alternating
      for (uint y = 1; y < H; ++y)
        if ((y/25) % 2 == 0)
          img_[y] = img_[0];
    }

  template<uint W, uint H>
  void
  ImageGenerator<W,H>::animatePos()
    {
      p1_ += v1_;
      maybeBounce (p1_, v1_);
      p2_ += v2_;
      maybeBounce (p2_, v2_);

      auto fade = [this](double& val, double target, uint dur)
                        {
                          if (val == target)
                            return;
                          val = std::min (target, val + target/(dur*fps_));
                        };
      fade (a1_, 0.05, 9);
      fade (a2_, 0.1,  2);
    }

  template<uint W, uint H>
  void
  ImageGenerator<W,H>::attenuate()
    {
      double dim{H*W};
      for (int row=0; row < int(H); ++row)
        for (int col=0; col < int(W); ++col)
          {
            Vec2 point{col,row};
            auto vicinity = [&](Vec2& p, int s)
                              {
                                int range = (p - point).norm();
                                return 1.0 /(1 + s/dim * range);
                              };
            Trip& px{img_[row][col]};

            decay (px, a1_,      (1.0 * vicinity (p2_, 1)) * DARK_BLUE);
            decay (px, a2_, px + (0.5 * vicinity (p1_,13)) * LT_YELLOW);
          }
    }

  template<uint W, uint H>
  void
  ImageGenerator<W,H>::drawBall()
    {
      for (uint row=0; row < BALL_SIZ; ++row)
        for (uint col=0; col < BALL_SIZ; ++col)
          img_[p1_.y+row][p1_.x+col] += BALL[row*BALL_SIZ + col];
    }

  template<uint W, uint H>
  void
  ImageGenerator<W,H>::maybeBounce (Vec2& p, Vec2& v)
    {
      auto randomise = [](int& val)
                          {
                            int offset{rand() % 7 - 3};
                            val += offset;
                            if (abs(val > 3) and val*offset > 0)
                              val /= 2; // damp excess outward trend
                          };
      int limX = W-BALL_SIZ;
      int limY = H-BALL_SIZ;

      if (p.x <= 0)
        {
          p.x *= -1;
          v.x *= -1;
          randomise (v.y);
        }
      else
      if (p.x >= limX)
        {
          p.x  = limX - (p.x-limX);
          v.x *= -1;
          randomise (v.y);
        }
      if (p.y <= 0)
        {
          p.y *= -1;
          v.y *= -1;
          randomise (v.x);
        }
      else
      if (p.y >= limY)
        {
          p.y  = limY - (p.y-limY);
          v.y *= -1;
          randomise (v.x);
        }
    }


#endif /*IMAGE_GENERATOR_H*/