first commit
This commit is contained in:
54
modern-qt/utility/animation/animatable.cc
Normal file
54
modern-qt/utility/animation/animatable.cc
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "animatable.hh"
|
||||
#include <qdebug.h>
|
||||
using namespace creeper;
|
||||
|
||||
#include <qtimer.h>
|
||||
using qwidget = QWidget;
|
||||
using qtimer = QTimer;
|
||||
|
||||
struct Animatable::Impl {
|
||||
|
||||
qwidget& component;
|
||||
qtimer scheduler;
|
||||
|
||||
std::vector<std::unique_ptr<ITransitionTask>> transition_tasks;
|
||||
|
||||
explicit Impl(auto& component, int hz = 90) noexcept
|
||||
: component { component } {
|
||||
scheduler.connect(&scheduler, &qtimer::timeout, [this] { update(); });
|
||||
scheduler.setInterval(1'000 / hz);
|
||||
}
|
||||
|
||||
auto set_frame_rate(int hz) noexcept -> void { scheduler.setInterval(1'000 / hz); }
|
||||
|
||||
auto push_transition_task(std::unique_ptr<ITransitionTask> task) noexcept -> void {
|
||||
transition_tasks.push_back(std::move(task));
|
||||
if (!scheduler.isActive()) scheduler.start();
|
||||
}
|
||||
|
||||
auto update() noexcept -> void {
|
||||
|
||||
const auto [first, last] = std::ranges::remove_if(transition_tasks,
|
||||
[](const std::unique_ptr<ITransitionTask>& task) { return !task->update(); });
|
||||
|
||||
component.update();
|
||||
|
||||
transition_tasks.erase(first, last);
|
||||
|
||||
if (transition_tasks.empty()) {
|
||||
scheduler.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Animatable::Animatable(QWidget& component) noexcept
|
||||
: pimpl { std::make_unique<Impl>(component) } { }
|
||||
|
||||
Animatable::~Animatable() = default;
|
||||
|
||||
auto Animatable::set_frame_rate(int hz) noexcept -> void {
|
||||
pimpl->set_frame_rate(hz); //
|
||||
}
|
||||
auto Animatable::push_transition_task(std::unique_ptr<ITransitionTask> task) noexcept -> void {
|
||||
pimpl->push_transition_task(std::move(task));
|
||||
}
|
||||
27
modern-qt/utility/animation/animatable.hh
Normal file
27
modern-qt/utility/animation/animatable.hh
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include <qwidget.h>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
/// @note
|
||||
/// Ends after the calculation is completed or the controller call ends
|
||||
struct ITransitionTask {
|
||||
virtual ~ITransitionTask() noexcept = default;
|
||||
virtual auto update() noexcept -> bool = 0;
|
||||
};
|
||||
|
||||
class Animatable {
|
||||
CREEPER_PIMPL_DEFINITION(Animatable)
|
||||
|
||||
public:
|
||||
explicit Animatable(QWidget& widget) noexcept;
|
||||
|
||||
auto set_frame_rate(int hz) noexcept -> void;
|
||||
auto get_frame_rate() const noexcept -> int;
|
||||
|
||||
auto push_transition_task(std::unique_ptr<ITransitionTask> task) noexcept -> void;
|
||||
};
|
||||
|
||||
}
|
||||
95
modern-qt/utility/animation/math.hh
Normal file
95
modern-qt/utility/animation/math.hh
Normal file
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include <eigen3/Eigen/Dense>
|
||||
#include <qcolor.h>
|
||||
#include <qrect.h>
|
||||
|
||||
namespace creeper::animate {
|
||||
|
||||
template <typename T>
|
||||
constexpr auto zero() noexcept {
|
||||
if constexpr (std::is_arithmetic_v<T>) {
|
||||
return T { 0 };
|
||||
} else if constexpr (requires { T::Zero(); }) {
|
||||
return T::Zero();
|
||||
} else {
|
||||
static_assert(sizeof(T) == 0, "zero() not implemented for this type");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr auto magnitude(const T& error) noexcept {
|
||||
if constexpr (std::is_arithmetic_v<T>) {
|
||||
return std::abs(error);
|
||||
} else if constexpr (requires { error.norm(); }) {
|
||||
return std::abs(error.norm());
|
||||
} else {
|
||||
static_assert(sizeof(T) == 0, "magnitude() not implemented for this type");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr auto normalize(const T& error) noexcept {
|
||||
if constexpr (std::is_arithmetic_v<T>) {
|
||||
return error;
|
||||
} else if constexpr (requires { error.norm(); }) {
|
||||
return error.norm();
|
||||
} else {
|
||||
static_assert(sizeof(T) == 0, "magnitude() not implemented for this type");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr auto interpolate(const T& start, const T& end, double t) noexcept -> T {
|
||||
|
||||
const auto clamped_t = std::clamp(t, 0., 1.);
|
||||
|
||||
if constexpr (std::is_arithmetic_v<T>) {
|
||||
return static_cast<T>(start + (end - start) * clamped_t);
|
||||
} else if constexpr ( //
|
||||
requires(const T& a, const T& b, const double f) {
|
||||
{ a - b } -> std::convertible_to<T>;
|
||||
{ a * f } -> std::convertible_to<T>;
|
||||
{ a + b } -> std::convertible_to<T>;
|
||||
}) {
|
||||
return start + (end - start) * clamped_t;
|
||||
} else {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"interpolate() requires T to be an arithmetic type or define +, -, and scalar "
|
||||
"multiplication.");
|
||||
}
|
||||
}
|
||||
|
||||
constexpr auto interpolate(const QRectF& start, const QRectF& end, double position) -> QRectF {
|
||||
position = qBound(0.0, position, 1.0);
|
||||
auto _1 = start.left() + (end.left() - start.left()) * position;
|
||||
auto _2 = start.top() + (end.top() - start.top()) * position;
|
||||
auto _3 = start.width() + (end.width() - start.width()) * position;
|
||||
auto _4 = start.height() + (end.height() - start.height()) * position;
|
||||
return { _1, _2, _3, _4 };
|
||||
}
|
||||
}
|
||||
|
||||
namespace creeper {
|
||||
|
||||
constexpr auto from_color(const QColor& color) -> Eigen::Vector4d {
|
||||
return Eigen::Vector4d(color.red(), color.green(), color.blue(), color.alpha());
|
||||
}
|
||||
constexpr auto from_vector4(const Eigen::Vector4d& vector) -> QColor {
|
||||
return QColor(vector[0], vector[1], vector[2], vector[3]);
|
||||
}
|
||||
|
||||
constexpr auto extract_rect(const QRectF& rect, double w_weight, double h_weight) -> QRectF {
|
||||
double rw, rh;
|
||||
if (rect.width() * h_weight > rect.height() * w_weight) {
|
||||
rh = rect.height();
|
||||
rw = rh * w_weight / h_weight;
|
||||
} else {
|
||||
rw = rect.width();
|
||||
rh = rw * h_weight / w_weight;
|
||||
}
|
||||
const auto center = rect.center();
|
||||
return QRectF(center.x() - rw / 2, center.y() - rh / 2, rw, rh);
|
||||
}
|
||||
|
||||
}
|
||||
12
modern-qt/utility/animation/state/accessor.hh
Normal file
12
modern-qt/utility/animation/state/accessor.hh
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
namespace creeper {
|
||||
|
||||
struct NormalAccessor {
|
||||
auto get_value(this auto const& self) { return self.value; }
|
||||
auto set_value(this auto& self, auto const& t) { self.value = t; }
|
||||
auto get_target(this auto const& self) { return self.target; }
|
||||
auto set_target(this auto& self, auto const& t) { self.target = t; }
|
||||
};
|
||||
|
||||
}
|
||||
75
modern-qt/utility/animation/state/linear.hh
Normal file
75
modern-qt/utility/animation/state/linear.hh
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/animation/math.hh"
|
||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <typename T>
|
||||
struct LinearState : public NormalAccessor {
|
||||
using ValueT = T;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
T value = animate::zero<T>();
|
||||
T target = animate::zero<T>();
|
||||
|
||||
struct {
|
||||
double speed = 1.0;
|
||||
double epsilon = 1e-2;
|
||||
} config;
|
||||
|
||||
struct {
|
||||
TimePoint last_timestamp;
|
||||
} details;
|
||||
|
||||
auto set_target(T new_target) noexcept -> void {
|
||||
target = new_target;
|
||||
|
||||
const auto current_time = Clock::now();
|
||||
using namespace std::chrono_literals;
|
||||
const auto threshold = 16ms;
|
||||
|
||||
const auto elapsed_time = current_time - details.last_timestamp;
|
||||
|
||||
if (elapsed_time > threshold) {
|
||||
details.last_timestamp = current_time;
|
||||
}
|
||||
}
|
||||
|
||||
auto update() noexcept -> bool {
|
||||
const auto now = Clock::now();
|
||||
const auto duration = now - details.last_timestamp;
|
||||
const auto dt = std::chrono::duration<double>(duration).count();
|
||||
|
||||
if (dt <= 0.0) {
|
||||
details.last_timestamp = now;
|
||||
return animate::magnitude(target - value) > config.epsilon;
|
||||
}
|
||||
|
||||
const auto delta = target - value;
|
||||
const auto dist = animate::magnitude(delta);
|
||||
|
||||
if (dist <= config.epsilon) {
|
||||
value = target;
|
||||
details.last_timestamp = now;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto direction = animate::normalize(delta);
|
||||
const auto step = config.speed * dt;
|
||||
|
||||
if (step >= dist) {
|
||||
value = target;
|
||||
} else {
|
||||
value += direction * step;
|
||||
}
|
||||
|
||||
details.last_timestamp = now;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
94
modern-qt/utility/animation/state/pid.hh
Normal file
94
modern-qt/utility/animation/state/pid.hh
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/animation/math.hh"
|
||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <typename T>
|
||||
struct PidState : public NormalAccessor {
|
||||
using ValueT = T;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
T value = animate::zero<T>();
|
||||
T target = animate::zero<T>();
|
||||
|
||||
struct {
|
||||
double kp = 1.0;
|
||||
double ki = 0.0;
|
||||
double kd = 0.1;
|
||||
double epsilon = 1e-3;
|
||||
} config;
|
||||
|
||||
struct {
|
||||
T integral_error = animate::zero<T>();
|
||||
T last_error = animate::zero<T>();
|
||||
TimePoint last_timestamp;
|
||||
} details;
|
||||
|
||||
auto set_target(T new_target) noexcept -> void {
|
||||
target = new_target;
|
||||
|
||||
const auto current_time = Clock::now();
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
const auto threshold = 16ms;
|
||||
|
||||
const auto elapsed_time = current_time - details.last_timestamp;
|
||||
|
||||
if (elapsed_time > threshold) {
|
||||
details.last_error = target - value;
|
||||
details.last_timestamp = current_time;
|
||||
}
|
||||
}
|
||||
|
||||
auto update() noexcept -> bool {
|
||||
|
||||
const auto kp = config.kp;
|
||||
const auto ki = config.ki;
|
||||
const auto kd = config.kd;
|
||||
|
||||
const auto now = Clock::now();
|
||||
const auto duration = now - details.last_timestamp;
|
||||
|
||||
const auto dt = std::chrono::duration<double>(duration).count();
|
||||
|
||||
if (dt <= 0.0) {
|
||||
details.last_timestamp = now;
|
||||
return animate::magnitude(target - value) > config.epsilon;
|
||||
}
|
||||
|
||||
const auto current_error = target - value;
|
||||
|
||||
if (animate::magnitude(current_error) <= config.epsilon
|
||||
&& animate::magnitude(details.last_error) <= config.epsilon) {
|
||||
value = target;
|
||||
details.integral_error = animate::zero<T>();
|
||||
details.last_error = animate::zero<T>();
|
||||
details.last_timestamp = now;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto proportional_term = kp * current_error;
|
||||
|
||||
details.integral_error += current_error * dt;
|
||||
const auto integral_term = ki * details.integral_error;
|
||||
|
||||
const auto derivative_error = (current_error - details.last_error) / dt;
|
||||
const auto derivative_term = kd * derivative_error;
|
||||
|
||||
const auto output = proportional_term + integral_term + derivative_term;
|
||||
|
||||
value += output * dt;
|
||||
|
||||
details.last_error = current_error;
|
||||
details.last_timestamp = now;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
72
modern-qt/utility/animation/state/spring.hh
Normal file
72
modern-qt/utility/animation/state/spring.hh
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/animation/math.hh"
|
||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <typename T>
|
||||
struct SpringState : public NormalAccessor {
|
||||
using ValueT = T;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
T value;
|
||||
T target;
|
||||
|
||||
T velocity = animate::zero<T>();
|
||||
|
||||
TimePoint last_timestamp = Clock::now();
|
||||
|
||||
struct {
|
||||
double k = 1.0;
|
||||
double d = 0.1;
|
||||
double epsilon = 1e-1;
|
||||
} config;
|
||||
|
||||
auto set_target(T new_target) noexcept -> void {
|
||||
target = new_target;
|
||||
|
||||
const auto current_time = Clock::now();
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
const auto threshold = 16ms;
|
||||
|
||||
const auto elapsed_time = current_time - last_timestamp;
|
||||
|
||||
if (elapsed_time > threshold) {
|
||||
const auto error = target - value;
|
||||
velocity = animate::zero<T>();
|
||||
last_timestamp = current_time;
|
||||
}
|
||||
}
|
||||
|
||||
auto update() noexcept -> bool {
|
||||
const auto now = Clock::now();
|
||||
const auto duration = now - last_timestamp;
|
||||
const double dt = std::chrono::duration<double>(duration).count();
|
||||
|
||||
if (dt <= 0.0) {
|
||||
last_timestamp = now;
|
||||
return std::abs(animate::magnitude(target - value)) > config.epsilon;
|
||||
}
|
||||
|
||||
const auto error = value - target;
|
||||
const auto a_force = -config.k * error;
|
||||
const auto a_damping = -config.d * velocity;
|
||||
const auto a_total = a_force + a_damping;
|
||||
|
||||
velocity += a_total * dt;
|
||||
value += velocity * dt;
|
||||
|
||||
last_timestamp = now;
|
||||
|
||||
const bool done =
|
||||
animate::magnitude(error) < config.epsilon && std::abs(velocity) < config.epsilon;
|
||||
|
||||
if (done) velocity = animate::zero<T>();
|
||||
return !done;
|
||||
}
|
||||
};
|
||||
}
|
||||
86
modern-qt/utility/animation/transition.hh
Normal file
86
modern-qt/utility/animation/transition.hh
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
#include "animatable.hh"
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <class T>
|
||||
concept transition_state_trait = requires(T& t) {
|
||||
typename T::ValueT;
|
||||
{ t.get_value() } -> std::same_as<typename T::ValueT>;
|
||||
{ t.get_target() } -> std::same_as<typename T::ValueT>;
|
||||
|
||||
{ t.set_value(std::declval<typename T::ValueT>()) };
|
||||
{ t.set_target(std::declval<typename T::ValueT>()) };
|
||||
|
||||
{ t.update() } -> std::same_as<bool>;
|
||||
};
|
||||
|
||||
// Functor like lambda
|
||||
template <transition_state_trait State>
|
||||
struct TransitionTask : public ITransitionTask {
|
||||
public:
|
||||
explicit TransitionTask(std::shared_ptr<State> state, std::shared_ptr<bool> token) noexcept
|
||||
: state { std::move(state) }
|
||||
, running { std::move(token) } { }
|
||||
|
||||
~TransitionTask() override = default;
|
||||
|
||||
auto update() noexcept -> bool override {
|
||||
return *running && state->update(); //
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<State> state;
|
||||
std::shared_ptr<bool> running;
|
||||
};
|
||||
|
||||
template <transition_state_trait State>
|
||||
struct TransitionValue {
|
||||
public:
|
||||
using T = State::ValueT;
|
||||
|
||||
explicit TransitionValue(Animatable& animatable, std::shared_ptr<State> state) noexcept
|
||||
: animatable { animatable }
|
||||
, state { std::move(state) } { }
|
||||
|
||||
auto get_state() const noexcept -> const State& { return *state; }
|
||||
|
||||
auto get_value() const noexcept { return state->get_value(); }
|
||||
|
||||
auto get_target() const noexcept { return state->get_target(); }
|
||||
|
||||
operator T() const noexcept { return state->get_value(); }
|
||||
|
||||
auto transition_to(const T& to) noexcept -> void {
|
||||
// Update target of state
|
||||
state->set_target(to);
|
||||
|
||||
// Clear last transition task
|
||||
if (running) {
|
||||
*running = false;
|
||||
}
|
||||
running = std::make_shared<bool>(true);
|
||||
|
||||
// Push new transition task
|
||||
auto task = std::make_unique<TransitionTask<State>>(state, running);
|
||||
animatable.push_transition_task(std::move(task));
|
||||
}
|
||||
auto snap_to(T to) noexcept -> void {
|
||||
state->set_value(std::move(to));
|
||||
state->set_target(std::move(to));
|
||||
if (running) *running = false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<State> state;
|
||||
std::shared_ptr<bool> running;
|
||||
|
||||
Animatable& animatable;
|
||||
};
|
||||
|
||||
template <transition_state_trait State>
|
||||
inline auto make_transition(Animatable& core, std::shared_ptr<State> state) {
|
||||
return std::make_unique<TransitionValue<State>>(core, state);
|
||||
}
|
||||
|
||||
}
|
||||
71
modern-qt/utility/animation/water-ripple.hh
Normal file
71
modern-qt/utility/animation/water-ripple.hh
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
||||
#include "modern-qt/utility/animation/transition.hh"
|
||||
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPointF>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
struct WaterRippleState : public NormalAccessor {
|
||||
using ValueT = double;
|
||||
|
||||
QPointF origin;
|
||||
double value = 0.0;
|
||||
double target = 0.0;
|
||||
double speed = 1.0;
|
||||
|
||||
auto update() noexcept -> bool {
|
||||
value += speed;
|
||||
return value < target;
|
||||
}
|
||||
};
|
||||
|
||||
class WaterRippleRenderer {
|
||||
public:
|
||||
explicit WaterRippleRenderer(Animatable& core, double speed)
|
||||
: animatable { core }
|
||||
, speed { speed } { }
|
||||
|
||||
auto clicked(const QPointF& origin, double max_distance) noexcept -> void {
|
||||
auto state = std::make_shared<WaterRippleState>();
|
||||
state->origin = origin;
|
||||
state->speed = speed;
|
||||
state->target = max_distance;
|
||||
|
||||
auto ripple = make_transition(animatable, state);
|
||||
ripple->transition_to(max_distance);
|
||||
ripples.push_back(std::move(ripple));
|
||||
}
|
||||
|
||||
auto renderer(const QPainterPath& clip_path, const QColor& water_color) noexcept {
|
||||
return [&, this](QPainter& painter) {
|
||||
std::erase_if(ripples, [&](const auto& ripple) {
|
||||
const auto& state = ripple->get_state();
|
||||
const auto opacity = 1.0 - state.value / state.target;
|
||||
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setClipPath(clip_path);
|
||||
painter.setOpacity(opacity);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(water_color);
|
||||
painter.drawEllipse(state.origin, state.value, state.value);
|
||||
painter.setOpacity(1.0);
|
||||
|
||||
return state.value >= state.target;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<TransitionValue<WaterRippleState>>> ripples;
|
||||
Animatable& animatable;
|
||||
double speed;
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user