365 lines
11 KiB
C++
365 lines
11 KiB
C++
|
||
#include "fl/xypath_impls.h"
|
||
|
||
#include <math.h>
|
||
|
||
#include "fl/assert.h"
|
||
#include "fl/function.h"
|
||
#include "fl/lut.h"
|
||
#include "fl/map_range.h"
|
||
#include "fl/math_macros.h"
|
||
#include "fl/raster.h"
|
||
|
||
#include "fl/xypath_renderer.h"
|
||
|
||
namespace fl {
|
||
|
||
LinePath::LinePath(float x0, float y0, float x1, float y1) {
|
||
mParams = fl::make_shared<LinePathParams>();
|
||
params().x0 = x0;
|
||
params().y0 = y0;
|
||
params().x1 = x1;
|
||
params().y1 = y1;
|
||
}
|
||
|
||
vec2f LinePath::compute(float alpha) {
|
||
// α in [0,1] → (x,y) on the line
|
||
float x = params().x0 + alpha * (params().x1 - params().x0);
|
||
float y = params().y0 + alpha * (params().y1 - params().y0);
|
||
return {x, y};
|
||
}
|
||
|
||
void LinePath::set(float x0, float y0, float x1, float y1) {
|
||
params().x0 = x0;
|
||
params().y0 = y0;
|
||
params().x1 = x1;
|
||
params().y1 = y1;
|
||
}
|
||
|
||
void LinePath::set(const LinePathParams &p) { params() = p; }
|
||
|
||
vec2f CirclePath::compute(float alpha) {
|
||
// α in [0,1] → (x,y) on the unit circle [-1, 1]
|
||
float t = alpha * 2.0f * PI;
|
||
float x = cosf(t);
|
||
float y = sinf(t);
|
||
return vec2f(x, y);
|
||
}
|
||
|
||
CirclePath::CirclePath() {}
|
||
|
||
HeartPath::HeartPath() {}
|
||
|
||
vec2f HeartPath::compute(float alpha) {
|
||
// Parametric equation for a heart shape
|
||
// α in [0,1] → (x,y) on the heart curve
|
||
float t = alpha * 2.0f * PI;
|
||
|
||
// Heart formula based on a modified cardioid with improved aesthetics
|
||
float x = 16.0f * powf(sinf(t), 3);
|
||
|
||
// Modified y formula for a more balanced heart shape
|
||
// This creates a fuller bottom and more defined top curve
|
||
float y = -(13.0f * cosf(t) - 5.0f * cosf(2.0f * t) -
|
||
2.0f * cosf(3.0f * t) - cosf(4.0f * t));
|
||
|
||
// Scale to fit in [-1, 1] range
|
||
// The 16.0f divisor for x ensures x is in [-1, 1]
|
||
x /= 16.0f;
|
||
|
||
// Scale y to ensure it's in [-1, 1]
|
||
// The 16.0f divisor ensures proper scaling while maintaining proportions
|
||
y /= -16.0f;
|
||
|
||
// Apply a slight vertical stretch to fill the [-1, 1] range better
|
||
y *= 1.10f;
|
||
|
||
y += 0.17f; // Adjust y to fit within the range of [-1, 1]
|
||
|
||
return vec2f(x, y);
|
||
}
|
||
|
||
ArchimedeanSpiralPath::ArchimedeanSpiralPath(u8 turns, float radius)
|
||
: mTurns(turns), mRadius(radius) {}
|
||
|
||
vec2f ArchimedeanSpiralPath::compute(float alpha) {
|
||
// Parametric equation for an Archimedean spiral
|
||
// α in [0,1] → (x,y) on the spiral curve
|
||
|
||
// Calculate the angle based on the number of turns
|
||
float theta = alpha * 2.0f * PI * mTurns;
|
||
|
||
// Calculate the radius at this angle (grows linearly with angle)
|
||
// Scale by alpha to ensure we start at center and grow outward
|
||
float r = alpha * mRadius;
|
||
|
||
// Convert polar coordinates (r, theta) to Cartesian (x, y)
|
||
float x = r * cosf(theta);
|
||
float y = r * sinf(theta);
|
||
|
||
// Ensure the spiral fits within [-1, 1] range
|
||
// No additional scaling needed as we control the radius directly
|
||
|
||
return vec2f(x, y);
|
||
}
|
||
|
||
RosePath::RosePath(u8 n, u8 d) {
|
||
mParams = fl::make_shared<RosePathParams>();
|
||
params().n = n;
|
||
params().d = d;
|
||
}
|
||
|
||
vec2f RosePath::compute(float alpha) {
|
||
// Parametric equation for a rose curve (rhodonea)
|
||
// α in [0,1] → (x,y) on the rose curve
|
||
|
||
// Map alpha to the full range needed for the rose
|
||
// For a complete rose, we need to go through k*PI radians where k is:
|
||
// - k = n if n is odd and d is 1
|
||
// - k = 2n if n is even and d is 1
|
||
// - k = n*d if n and d are coprime
|
||
// For simplicity, we'll use 2*PI*n as a good approximation
|
||
float theta = alpha * 2.0f * PI * params().n;
|
||
|
||
// Calculate the radius using the rose formula: r = cos(n*θ/d)
|
||
// We use cosine for a rose that starts with a petal at theta=0
|
||
float r = cosf(params().n * theta / params().d);
|
||
|
||
// Scale to ensure the rose fits within [-1, 1] range
|
||
// The absolute value ensures we get the proper shape
|
||
r = fabsf(r);
|
||
|
||
// Convert polar coordinates (r, theta) to Cartesian (x, y)
|
||
float x = r * cosf(theta);
|
||
float y = r * sinf(theta);
|
||
|
||
return vec2f(x, y);
|
||
}
|
||
|
||
vec2f PhyllotaxisPath::compute(float alpha) {
|
||
// total number of points you want in the pattern
|
||
const float N = static_cast<float>(params().c);
|
||
|
||
// continuous “index” from 0…N
|
||
float n = alpha * N;
|
||
|
||
// use the golden angle in radians:
|
||
// π * (3 – √5) ≈ 2.399963229728653
|
||
constexpr float goldenAngle = PI * (3.0f - 1.6180339887498948f);
|
||
|
||
// normalized radius [0…1]: sqrt(n/N) gives uniform point density
|
||
float r = sqrtf(n / N);
|
||
|
||
// spiral angle
|
||
float theta = n * goldenAngle;
|
||
|
||
// polar → Cartesian
|
||
float x = r * cosf(theta);
|
||
float y = r * sinf(theta);
|
||
|
||
return vec2f{x, y};
|
||
}
|
||
|
||
vec2f GielisCurvePath::compute(float alpha) {
|
||
// 1) map alpha to angle θ ∈ [0 … 2π)
|
||
constexpr float kTwoPi = 6.283185307179586f;
|
||
float theta = alpha * kTwoPi;
|
||
|
||
// 2) superformula parameters (members of your path)
|
||
// a, b control the “shape scale” (often both = 1)
|
||
// m controls symmetry (integer number of lobes)
|
||
// n1,n2,n3 control curvature/sharpness
|
||
float a = params().a;
|
||
float b = params().b;
|
||
float m = params().m;
|
||
float n1 = params().n1;
|
||
float n2 = params().n2;
|
||
float n3 = params().n3;
|
||
|
||
// 3) compute radius from superformula
|
||
float t2 = m * theta / 4.0f;
|
||
float part1 = powf(fabsf(cosf(t2) / a), n2);
|
||
float part2 = powf(fabsf(sinf(t2) / b), n3);
|
||
float r = powf(part1 + part2, -1.0f / n1);
|
||
|
||
// 4) polar → Cartesian in unit circle
|
||
float x = r * cosf(theta);
|
||
float y = r * sinf(theta);
|
||
|
||
return vec2f{x, y};
|
||
}
|
||
|
||
const string CirclePath::name() const { return "CirclePath"; }
|
||
|
||
vec2f PointPath::compute(float alpha) {
|
||
FASTLED_UNUSED(alpha);
|
||
return mPoint;
|
||
}
|
||
|
||
const string PointPath::name() const { return "PointPath"; }
|
||
|
||
void PointPath::set(float x, float y) { set(vec2f(x, y)); }
|
||
|
||
void PointPath::set(vec2f p) { mPoint = p; }
|
||
|
||
PointPath::PointPath(float x, float y) : mPoint(x, y) {}
|
||
|
||
PointPath::PointPath(vec2f p) : mPoint(p) {}
|
||
|
||
const string LinePath::name() const { return "LinePath"; }
|
||
|
||
LinePathParams &LinePath::params() { return *mParams; }
|
||
|
||
const LinePathParams &LinePath::params() const { return *mParams; }
|
||
|
||
LinePath::LinePath(const LinePathParamsPtr ¶ms) : mParams(params) {}
|
||
|
||
const string HeartPath::name() const { return "HeartPath"; }
|
||
|
||
const string ArchimedeanSpiralPath::name() const {
|
||
return "ArchimedeanSpiralPath";
|
||
}
|
||
|
||
void ArchimedeanSpiralPath::setTurns(u8 turns) { mTurns = turns; }
|
||
|
||
void ArchimedeanSpiralPath::setRadius(float radius) { mRadius = radius; }
|
||
|
||
const string RosePath::name() const { return "RosePath"; }
|
||
|
||
void RosePath::setN(u8 n) { params().n = n; }
|
||
|
||
void RosePath::setD(u8 d) { params().d = d; }
|
||
|
||
RosePath::RosePath(const fl::shared_ptr<RosePathParams> &p) : mParams(p) {}
|
||
|
||
RosePathParams &RosePath::params() { return *mParams; }
|
||
|
||
const RosePathParams &RosePath::params() const { return *mParams; }
|
||
|
||
const string PhyllotaxisPath::name() const { return "PhyllotaxisPath"; }
|
||
|
||
PhyllotaxisPath::PhyllotaxisPath(const fl::shared_ptr<PhyllotaxisParams> &p)
|
||
: mParams(p) {}
|
||
|
||
PhyllotaxisParams &PhyllotaxisPath::params() { return *mParams; }
|
||
|
||
const PhyllotaxisParams &PhyllotaxisPath::params() const { return *mParams; }
|
||
|
||
GielisCurvePath::GielisCurvePath(const fl::shared_ptr<GielisCurveParams> &p)
|
||
: mParams(p) {}
|
||
|
||
const string GielisCurvePath::name() const { return "GielisCurvePath"; }
|
||
void GielisCurvePath::setA(float a) { params().a = a; }
|
||
void GielisCurvePath::setB(float b) { params().b = b; }
|
||
void GielisCurvePath::setM(float m) { params().m = m; }
|
||
void GielisCurvePath::setN1(float n1) { params().n1 = n1; }
|
||
void GielisCurvePath::setN2(float n2) { params().n2 = n2; }
|
||
void GielisCurvePath::setN3(float n3) { params().n3 = n3; }
|
||
GielisCurveParams &GielisCurvePath::params() { return *mParams; }
|
||
const GielisCurveParams &GielisCurvePath::params() const { return *mParams; }
|
||
|
||
CatmullRomPath::CatmullRomPath(const fl::shared_ptr<CatmullRomParams> &p) : mParams(p) {}
|
||
|
||
void CatmullRomPath::addPoint(vec2f p) { params().addPoint(p); }
|
||
|
||
void CatmullRomPath::addPoint(float x, float y) { params().addPoint(x, y); }
|
||
|
||
void CatmullRomPath::clear() { params().clear(); }
|
||
|
||
fl::size CatmullRomPath::size() const { return params().size(); }
|
||
|
||
CatmullRomParams &CatmullRomPath::params() { return *mParams; }
|
||
|
||
const CatmullRomParams &CatmullRomPath::params() const { return *mParams; }
|
||
|
||
vec2f CatmullRomPath::compute(float alpha) {
|
||
const auto &points = params().points;
|
||
|
||
// Need at least 2 points to define a path
|
||
if (points.size() < 2) {
|
||
// Return origin if not enough points
|
||
return vec2f(0.0f, 0.0f);
|
||
}
|
||
|
||
// If only 2 points, do linear interpolation
|
||
if (points.size() == 2) {
|
||
return vec2f(points[0].x + alpha * (points[1].x - points[0].x),
|
||
points[0].y + alpha * (points[1].y - points[0].y));
|
||
}
|
||
|
||
// For Catmull-Rom, we need 4 points to interpolate between the middle two
|
||
// Scale alpha to the number of segments
|
||
float scaledAlpha = alpha * (points.size() - 1);
|
||
|
||
// Determine which segment we're in
|
||
int segment = static_cast<int>(scaledAlpha);
|
||
|
||
// Clamp to valid range
|
||
if (segment >= static_cast<int>(points.size()) - 1) {
|
||
segment = points.size() - 2;
|
||
scaledAlpha = static_cast<float>(segment) + 1.0f;
|
||
}
|
||
|
||
// Get local alpha within this segment [0,1]
|
||
float t = scaledAlpha - static_cast<float>(segment);
|
||
|
||
// Get the four points needed for interpolation
|
||
vec2f p0, p1, p2, p3;
|
||
|
||
// Handle boundary cases
|
||
if (segment == 0) {
|
||
// For the first segment, duplicate the first point
|
||
p0 = points[0];
|
||
p1 = points[0];
|
||
p2 = points[1];
|
||
p3 = (points.size() > 2) ? points[2] : points[1];
|
||
} else if (segment == static_cast<int>(points.size()) - 2) {
|
||
// For the last segment, duplicate the last point
|
||
p0 = (segment > 0) ? points[segment - 1] : points[0];
|
||
p1 = points[segment];
|
||
p2 = points[segment + 1];
|
||
p3 = points[segment + 1];
|
||
} else {
|
||
// Normal case - we have points before and after
|
||
p0 = points[segment - 1];
|
||
p1 = points[segment];
|
||
p2 = points[segment + 1];
|
||
p3 = points[segment + 2];
|
||
}
|
||
|
||
// Perform Catmull-Rom interpolation
|
||
auto out = interpolate(p0, p1, p2, p3, t);
|
||
return out;
|
||
}
|
||
|
||
vec2f CatmullRomPath::interpolate(const vec2f &p0, const vec2f &p1,
|
||
const vec2f &p2, const vec2f &p3,
|
||
float t) const {
|
||
|
||
// Catmull-Rom interpolation formula
|
||
// Using alpha=0.5 for the "tension" parameter (standard Catmull-Rom)
|
||
float t2 = t * t;
|
||
float t3 = t2 * t;
|
||
|
||
// Coefficients for x and y
|
||
float a = -0.5f * p0.x + 1.5f * p1.x - 1.5f * p2.x + 0.5f * p3.x;
|
||
float b = p0.x - 2.5f * p1.x + 2.0f * p2.x - 0.5f * p3.x;
|
||
float c = -0.5f * p0.x + 0.5f * p2.x;
|
||
float d = p1.x;
|
||
|
||
float x = a * t3 + b * t2 + c * t + d;
|
||
|
||
a = -0.5f * p0.y + 1.5f * p1.y - 1.5f * p2.y + 0.5f * p3.y;
|
||
b = p0.y - 2.5f * p1.y + 2.0f * p2.y - 0.5f * p3.y;
|
||
c = -0.5f * p0.y + 0.5f * p2.y;
|
||
d = p1.y;
|
||
|
||
float y = a * t3 + b * t2 + c * t + d;
|
||
|
||
return vec2f(x, y);
|
||
}
|
||
|
||
const string CatmullRomPath::name() const { return "CatmullRomPath"; }
|
||
|
||
} // namespace fl
|