贝塞尔曲线动画C++简单实践

article/2025/10/18 10:59:54

目录

        • 贝塞尔曲线简介
          • 一阶贝塞尔
          • 二阶贝塞尔
          • 三阶贝塞尔
          • N阶贝塞尔曲线
        • 贝塞尔曲线在动画中的应用
        • 实践
          • 求曲线散点坐标
          • 将曲线应用到动画
          • 动画框架
            • cmd动画
            • 窗口动画
        • 完整代码
          • 示例代码
          • 核心类代码
            • BezierCurve
            • Animator
            • Console
        • 参考资料

贝塞尔曲线简介

由于用计算机画图大部分时间是操作鼠标来掌握线条的路径,与手绘的感觉和效果有很大的差别。即使是一位精明的画师能轻松绘出各种图形,拿到鼠标想随心所欲的画图也不是一件容易的事。这一点是计算机万万不能代替手工的工作,所以人们只能颇感无奈。使用贝塞尔工具画图很大程度上弥补了这一缺憾。贝塞尔曲线是计算机图形图像造型的基本工具,是图形造型运用得最多的基本线条之一。它通过控制曲线上的四个点(起始点、终止点以及两个相互分离的中间点)来创造、编辑图形。
除此之外,贝塞尔曲线还经常用来做动画,让动画过渡更平滑。本文则记录如何使用贝塞尔曲线定制平滑的动画效果,并使用C++编写了cmd动画和窗口动画示例代码。

一阶贝塞尔

一阶贝塞尔曲线
设定图中运动的点为Ptt为运动时间,t∈(0,1),可得如下公式:
公式1

二阶贝塞尔

二阶贝塞尔曲线
在二阶贝塞尔曲线中,已知三点恒定(P0,P1,P2),设定在P0 P1中的点为Pa,在P1 P2中的点为PbPtPa Pb上的点,这三点都在相同时间t内做匀速运动。

由公式(1)可知
公式2,3,4
将公式(2)(3)代入公式(4)中,可得
公式5

三阶贝塞尔

三阶贝塞尔曲线
同理,根据以上的推导过程可得
公式6
由此可以推导
公式7

N阶贝塞尔曲线

四阶贝塞尔曲线:
四阶贝塞尔曲线
五阶贝塞尔曲线:
五阶贝塞尔曲线
N阶贝塞尔曲线公式:
N阶贝塞尔曲线公式

贝塞尔曲线在动画中的应用

  • 贝赛尔曲线广泛应用于绘图软件中,例如Adobe PhotoShop、Adobe Flash。
  • Android可以通过自定义的view来实现贝塞尔曲线
  • ios则可以使用UIBezierPath类来生成贝塞尔曲线
  • 前端,canvas bezierCurveTo,css animation-timing-function: cubic-bezier(x,x,x,x}都有关于贝赛尔曲线的一些应用。

贝塞尔曲线在动画中的应用一般是三阶贝塞尔曲线,通过两个控制点来描述一般的动画曲线。通常以动画完成度为y轴,时间为x轴,然后将时间带入动画曲线求得对应的动画完成度

但是上述公式描述的是点与点关系,想要分解为x,y坐标的关系,则需要继续推导,以三阶为例:贝塞尔曲线x与y坐标的关系
想要直观的感受曲线的效果可以前往: cubic-bezier
得到xy坐标关系后即可写代码进行实践了。

实践

求曲线散点坐标

由上面推导的曲线点的坐标和时间的关系可得:设曲线起点为(0,0),终点为(1,1),则t时刻点的位置仅与两个控制点P1 P2有关。先定义表示一个点的结构体:

  struct PointF{PointF() : x(0), y(0) {}PointF(double x, double y) : x(x), y(y) {}double x;double y;};

然后传入两个点的坐标进行初始化

  void init(double x1, double y1, double x2, double y2){pc.x = 3.0 * x1;pb.x = 3.0 * (x2 - x1) - pc.x;pa.x = 1.0 - pc.x - pb.x;pc.y = 3.0 * y1;pb.y = 3.0 * (y2 - y1) - pc.y;pa.y = 1.0 - pc.y - pb.y;}

计算t时刻的xy坐标

  double calcX(double t){return ((pa.x * t + pb.x) * t + pc.x) * t;}double calcY(double t){return ((pa.y * t + pb.y) * t + pc.y) * t;}

根据给定的采样计算出[0,1]时刻的n个曲线上的点坐标,即可绘制出曲线图。

  for (int i = 0; i < size; ++i) { // size即为采样个数,然后计算出对应采样时刻的曲线坐标sample_[i] = PointF(calcX(i * 1.0 / size), calcY(i * 1.0 / size));}

得到曲线坐标后可绘制曲线图看下:
贝塞尔曲线图
其中蓝色的曲线是贝塞尔曲线,绿色和红色的曲线分别表示贝塞尔曲线上的y坐标和x坐标与t的关系,即y随时间先慢,再快,最后慢。x随时间先快,再变慢,最后变快。

将曲线应用到动画

得到曲线散点坐标后,该怎么将其应用到动画呢?
因为我们已经设了x坐标在[0,1]之间,而动画一般就是分为动画完成度和时间的关系,而我们设动画的时间也在[0,1],那么就可以给定动画的时刻t,然后通过曲线散点坐标求得对应的动画完成度。
即通过x坐标求y坐标,因为我们只有散点坐标,时刻t不一定跟已有点的x坐标相同,因此需要找到最接近的时刻t的两个点进行插值,即可求得近似的y坐标,也即动画完成度。
废话不多说,直接上代码,使用二分法查找最近的两点并插值求y:

double GetYAtX(double x)
{int head = 0;int tail = size - 1;int center;while (head <= tail) {center = (head + tail) / 2;if (sample_[center].x < x) {head = center + 1;} else if (sample_[center].x > x) {tail = center - 1;} else {break;}}if (head < size - 1) {double x0 = sample_[head].x;double x1 = sample_[head + 1].x;double y0 = sample_[head].y;double y1 = sample_[head + 1].y;return (x - x0) / (x1 - x0) * (y1 - y0) + y0; // 线性插值计算} else {return 1;}
}
动画框架

现在时刻t的动画完成度能求了,接下来就是实验一下动画效果了,顺便封装一个简单的动画框架,这样就能方便的进行各种动画效果。

  1. 首先封装一下贝塞尔曲线的求值,BezierCurve
  2. 再封装做动画的类,Animator

封装完后即可开始使用

cmd动画

先来个cmd的动画试试,其中封装了个Console
示例代码:

int main()
{std::getc(stdin);std::thread thread([]() {draw::Rectangle r(10, 2);int fps = 70;int64_t interval = 1000 / fps;Animator animator(0, 150, 1, [&](int v) { r.SetPos(40 + v, 10);r.Draw();}, EasingCurve::Type::InOutBezier);auto last_time = std::chrono::steady_clock::now();bool forward = true;while (true) {while (!animator.Step(1.0 / fps, forward)) {auto now = std::chrono::steady_clock::now();auto delay = (interval - std::chrono::duration_cast<std::chrono::milliseconds>((now - last_time)).count());std::this_thread::sleep_for(std::chrono::milliseconds(delay > 0 ? delay : 1)); // 最低1ms免得卡last_time = std::chrono::steady_clock::now();}forward = !forward;}});thread.join();std::cout << std::endl;
}

分别实验了根据两种曲线进行动画的效果,通过cmd动画和网站上动画的对比,可以看出还是很接近的。

  • InOutBezier: 参数(.63, 0, .37, 1)
    曲线图:
    InOutBezier
    效果图:
    InOutBezier

  • InOutBounceBezier:参数(.56, -0.48, .46, 1.5)
    贝塞尔曲线:
    在这里插入图片描述

    效果图:
    InOutBounceBezier

窗口动画
  • InOutBezier:参数(.63, 0, .37, 1)
    曲线图:
    InOutBezier
    效果图:
    gui-inoutbezier

  • InOutBounceBezier:参数(.56, -0.48, .46, 1.5)
    贝塞尔曲线:
    在这里插入图片描述
    效果图:
    在这里插入图片描述

完整代码

示例代码

贝塞尔动画(csdn下载)
贝塞尔动画(github)

核心类代码
BezierCurve
#pragma oncestruct BezierEase
{BezierEase(double x1, double y1, double x2, double y2){init(x1, y1, x2, y2);for (int i = 0; i < size; ++i) {sample_[i] = PointF(calcX(i * 1.0 / size), calcY(i * 1.0 / size));}}double value(double t){return GetYAtX(t);}double GetYAtX(double x){int head = 0;int tail = size - 1;int center;while (head <= tail) {center = (head + tail) / 2;if (sample_[center].x < x) {head = center + 1;} else if (sample_[center].x > x) {tail = center - 1;} else {break;}}if (head < size - 1) {double x0 = sample_[head].x;double x1 = sample_[head + 1].x;double y0 = sample_[head].y;double y1 = sample_[head + 1].y;return (x - x0) / (x1 - x0) * (y1 - y0) + y0; // 线性插值计算} else {return 1;}}
private:struct PointF{PointF() : x(0), y(0) {}PointF(double x, double y) : x(x), y(y) {}double x;double y;};double calcX(double t){return ((pa.x * t + pb.x) * t + pc.x) * t;}double calcY(double t){return ((pa.y * t + pb.y) * t + pc.y) * t;}void init(double x1, double y1, double x2, double y2){pc.x = 3.0 * x1;pb.x = 3.0 * (x2 - x1) - pc.x;pa.x = 1.0 - pc.x - pb.x;pc.y = 3.0 * y1;pb.y = 3.0 * (y2 - y1) - pc.y;pa.y = 1.0 - pc.y - pb.y;}PointF pa;PointF pb;PointF pc;const static int size = 1001;PointF sample_[size];
};
Animator
#pragma once#include "bezier_curve.h"class EasingCurve
{
public:enum Type{InOutBezier, InOutBounceBezier};EasingCurve(Type t){switch (t) {case InOutBounceBezier:curve_.reset(new BezierEase(.56, -0.48, .46, 1.5));break;case InOutBezier:curve_.reset(new BezierEase(.63, 0, .37, 1));default:break;}}// [0 - 1]double valueForProgress(double t){t = max(0, min(t, 1));return curve_->value(t);}
private:std::unique_ptr<BezierEase> curve_;
};template<typename T = int>
class Animator_
{
public:Animator_(T from, T to, double duration/*单位秒*/, std::function<void(T)> fn = nullptr, EasingCurve::Type type = EasingCurve::InOutBezier): ec_(type), from_(from), to_(to), duration_(duration), fn_(fn){}Animator_() : ec_(EasingCurve::InOutBezier) {}bool Step(double interval/*单位秒*/, bool forward = true){assert(interval > 0);double time_now = current_time_ + (forward ? interval : (-interval));bool isFinished = forward ? time_now >= duration_ : time_now <= 0;time_now = forward ? min(time_now, duration_) : max(time_now, 0);T val = duration_ == 0 ? (forward ? to_ : from_) : from_ + T((to_ - from_) *ec_.valueForProgress(time_now / duration_));current_time_ = time_now;if (fn_) fn_(val);return isFinished;}
public:void SetRange(T from_val, T to_value) { from_ = from_val; to_ = to_value; }void SetDuration(double val) { duration_ = val; }void SetFn(std::function<void(T)> val) { fn_ = val; }void ResetCurrentTime() { current_time_ = 0; }void SetCurrentTime(double current_time) { current_time_ = current_time; }void Complete(bool state_forward) { SetCurrentTime(state_forward ? duration_ : 0); }
private:T from_ = {};T to_ = {};double duration_ = 0;double current_time_ = 0;	//当前时间std::function<void(T)> fn_ = nullptr;EasingCurve ec_;
};using Animator = Animator_<>;
Console
#pragma once
#include <iostream>class Console
{
private:struct Cursor{public:// up down是y变化,x不变void up(int n = 1){std::cout << "\033[" << n << "A";}void down(int n = 1){std::cout << "\033[" << n << "B";}void right(int n = 1){std::cout << "\033[" << n << "C";}void left(int n = 1){std::cout << "\033[" << n << "D";}void save(){ // 保存当前位置std::cout << "\0337";}void restore(){ // 回到保存的位置std::cout << "\0338";}void nextLine(int n = 1){ // 光标以当前位置为起始位置,往下到第n行开头std::cout << "\033[" << n << "E";}void previousLine(int n = 1){ // 光标以当前位置为起始位置,网上到第n行开头std::cout << "\033[" << n << "F";}void move(int y = 0, int x = 0){ // 以当前屏幕为原点(0,0)移动光标std::cout << "\033[" << y << ";" << x << "H";}void enableBlinking(bool enable = false){std::cout << "\033[?12" << (enable ? "h" : "l");}void hideCursor(bool hide = true){std::cout << "\033[?25" << (hide ? "l" : "h");}};
public:Console(){const auto h_out = GetStdHandle(STD_OUTPUT_HANDLE);DWORD mode;GetConsoleMode(h_out, &mode);SetConsoleMode(h_out, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);}enum Color{Black = 0,Red,Green,Yellow,Blue,Magenta,Cyan,White};void underline(){setTextAttr(4);}void noUnderline(){setTextAttr(24);}void bright(){ /* 设置前景颜色变亮 */setTextAttr(1);}void normal(){setTextAttr(0);}void negative(){ /* 前景色和背景色交换 */setTextAttr(7);}void positive(){ /* 将前景色和背景色恢复正常 */setTextAttr(27);}void setForeColor(Color color, bool bright = false){setTextAttr(bright ? 90 + color : 30 + color);}void setBackColor(Color color, bool bright = false){setTextAttr(bright ? 100 + color : 40 + color);}void setScrollRegion(int top, int bottom){std::cout << "\033[" << top << ";" << bottom << "r";}void clearCurLine(){std::cout << "\033[K";}void setTitle(const std::string &title){std::cout << "\033]2;" << title << "\x07";}int width(){auto info = getScreenBufferInfo();return info.srWindow.Right - info.srWindow.Left + 1;}int height(){auto info = getScreenBufferInfo();return info.srWindow.Bottom - info.srWindow.Top + 1;}
public:Cursor cursor;private:void setTextAttr(int n){std::cout << "\033[" << n << "m";}CONSOLE_SCREEN_BUFFER_INFO getScreenBufferInfo(){CONSOLE_SCREEN_BUFFER_INFO info;GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);return info;}
};

参考资料

  1. 从零开始学图形学:10分钟看懂贝塞尔曲线
  2. 如何理解并应用贝塞尔曲线
  3. JS模拟CSS3动画-贝塞尔曲线
  4. 求高手解答 贝塞尔曲线问题
  5. cubic-bezier:贝塞尔曲线在线预览网站

http://chatgpt.dhexx.cn/article/aI02FZwj.shtml

相关文章

贝塞尔曲线及实践案例

文章目录 1. 前言2. 介绍2.1 一阶贝济埃曲线2.2 二阶贝塞尔曲线2.3 三阶贝塞尔曲线 3. 一、二、三阶贝塞尔曲线实现4. 案例5. 后记 1. 前言 贝塞尔曲线(Bzier curve)&#xff0c;又称贝兹曲线或贝济埃曲线&#xff0c;是应用于二维图形应用程序的数学曲线。一般的矢量图形软件…

贝塞尔曲线工具类

贝塞尔曲线工具类 先上一张效果图看效果 贝塞尔曲线 用于计算N阶贝塞尔曲线上的点&#xff0c;根据传入控制点的个数判定阶数N 贝塞尔曲线计算公式: 工具类源码 public class BezierUtils {/*** 获取二项式系数** param l 行(杨辉三角)* param c 列(杨辉三角)* return 系…

贝塞尔曲线打断生成两个贝塞尔曲线

问题&#xff1a;如何将一个三阶贝塞尔曲线打断生成两个三阶贝塞尔曲线&#xff0c;生成的两条贝塞尔曲线与原来的贝塞尔曲线重合&#xff1f; 输入&#xff1a;一条贝塞尔曲线的四个控制点P1,C1,C2,P2,和一个打断点E(E在曲线上) 输出&#xff1a;两条贝塞尔曲线: P1,F,I,E E,J…

html5贝塞尔曲线,Canvas学习:贝塞尔曲线

在绘制圆和圆弧一节中,了解到在Canvas中可以使用arc()和arcTo()绘制制圆或弧线,但很多时候,仅这两个方法还不能满足我们实际的需求,特别是绘制复杂的曲线。不过值得庆幸的是,在Canvas中还提供了其他的方法可以帮助我们绘制复杂的曲线。那就是我们今天要说的贝塞尔曲线,在…

贝塞尔曲线(Bezier Curve)

有兴趣的建议看这篇&#xff0c;比较新&#xff1a;https://zhuanlan.zhihu.com/p/366678047 曲线和曲面 在生活中存在着各种各样光滑的曲线或曲面&#xff0c;例如汽车的表面&#xff0c;钢珠球等。 在建模的时候&#xff0c;我们通常使用很多的小三角形来逼近这样的曲面&am…

java贝塞尔曲线_贝塞尔曲线学习

贝塞尔曲线学习 1.贝塞尔曲线 以下公式中: B(t)为t时间下 点的坐标; P0为起点,Pn为终点,Pi为控制点 一阶贝塞尔曲线(线段): 一阶贝塞尔曲线公式 一阶贝塞尔曲线演示 意义:由 P0 至 P1 的连续点, 描述的一条线段 二阶贝塞尔曲线(抛物线): 二阶贝塞尔曲线公式 二阶贝塞尔曲…

贝塞尔曲线

一&#xff1a;简介 1962年&#xff0c;法国工程师贝塞尔发表&#xff0c;他运用贝塞尔曲线来为汽车的主体进行设计。 贝塞尔曲线是最基本的曲线&#xff0c;一般用在计算机 图形学和 图像处理。贝塞尔曲线可以用来创建平滑的曲线的道路、 弯曲的路径就像 祖玛游戏、 弯曲型的…

【Unity3d游戏开发】游戏中的贝塞尔曲线以及其在Unity中的实现

RT&#xff0c;马三最近在参与一款足球游戏的开发&#xff0c;其中涉及到足球的各种运动轨迹和路径&#xff0c;比如射门的轨迹&#xff0c;高吊球&#xff0c;香蕉球的轨迹。最早的版本中马三是使用物理引擎加力的方式实现的足球各种运动&#xff0c;后来的版本中使用了根据物…

Bezier Curve贝塞尔曲线概念

一、贝塞尔曲线的概念 对于两点之间的连线&#xff0c;我们可以用直线进行连接效果如下&#xff1a; 其中的每一个F点都在AB连接的线段上。 这就是一阶贝塞尔曲线。 如果我们加入一个控制点C&#xff0c;那么做图如下&#xff1a; 1、连接AC,BC&#xff1b; 2、在AC,B…

贝塞尔(贝兹尔)曲线介绍

2019独角兽企业重金招聘Python工程师标准>>> 贝塞尔(贝兹尔)曲线介绍 什么是贝塞尔曲线&#xff1f; “贝赛尔曲线”是由法国数学家Pierre Bzier所发明&#xff0c;由此为计算机矢量图形学奠定了基础。它的主要意义在于无论是直线或曲线都能在数学上予以描述。贝塞尔…

【ARM】-栈帧

ARM 栈帧 本系列均以 corter-A7(armv7-a) 为例 在 ARM 中&#xff0c;通常为满减栈&#xff08;Full Descending FD&#xff09;, 也就是说&#xff0c;堆栈指针指向堆栈内存中最后一个填充的位置&#xff0c;并且随着每个新数据项被压入堆栈而递减。 栈的本质 要理解栈的本…

JVM 栈和栈帧

tag: jvm,stack,stack frame,栈,栈帧 原文&#xff1a;JVM Stacks and Stack Frames 翻译&#xff1a;陈同学 欢迎访问陈同学博客原文&#xff0c;文章可读性更佳 前情提要 对于没有深度递归的函数来说&#xff0c;无需担心上篇文章中的算法。当知道正在处理数据集有限时&am…

C语言的函数栈帧

⭐️前面的话⭐️ &#x1f4d2;博客主页&#xff1a;未见花闻的博客主页 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4cc;本文由未见花闻原创&#xff0c;CSDN首发&#xff01; ✉️坚持和努力一定能换来诗与远方&#xff01; &…

【C语言】函数栈帧的创建和销毁(1)

前言&#xff1a; ❓ 在我们前期学习C语言时&#xff0c;你是否曾产生过很多困惑&#xff1f; &#x1f4ad; 比如&#xff1a;局部变量是怎么创建出来的&#xff1f;因为局部变量的值是随机值&#xff0c;我们建议将它初始化&#xff0c;那么为什么局部变量的值是随机值&am…

函数栈帧(详解版)

文章目录 前言1.浅谈C语言内存1.1 内存分配1.2 栈1.3 寄存器 2. 为main()函数开辟栈帧3.变量的初始化及函数调用4.ADD函数4.1 ADD函数的创建4.2 ADD函数使用与销毁 5.总结 前言 前期学习的时候&#xff0c;我们可能有很多困感? 比如: ●局部变量是怎么创建的?I为什么局部变量…

也谈栈和栈帧

&#xfeff;&#xfeff; 一个码农要是没遇见过coredump&#xff0c;那他就是神仙了。core file(coredump的转储文件)中保存的最重要内容之一&#xff0c;就是函数的call trace。还原这部分内容 (栈回溯) &#xff0c;并与原代码对应上&#xff0c;尽快找出程序崩溃的位置和…

(栈帧和函数调用一)栈帧,函数调用与栈的关系

&#xff08;栈帧和函数调用一&#xff09;栈帧&#xff0c;函数调用与栈的关系 一&#xff0c;栈帧的介绍二&#xff0c;函数调用与栈的关系三&#xff0c;汇编演示四&#xff0c;总结 在计算机科学中&#xff0c;栈是一个特殊的容器&#xff0c;用户可以将数据压入栈中&#…

理解栈帧和栈的运行原理

栈中的数据都是以栈帧&#xff08;Stack Frame&#xff09;的格式存在&#xff0c;栈帧是一个内存区块&#xff0c;是一个数据集&#xff0c;是一个有关方法 (Method) 和运行期数据的数据集&#xff0c;当一个方法A被调用时就产生了一个栈帧 F1&#xff0c;并被压入到栈中&…

函数栈帧的形成与释放

✅作者简介&#xff1a;嵌入式入坑者&#xff0c;与大家一起加油&#xff0c;希望文章能够帮助各位&#xff01;&#xff01;&#xff01;&#xff01; &#x1f4c3;个人主页&#xff1a;rivencode的个人主页 &#x1f525;系列专栏&#xff1a;玩转C语言 &#x1f4ac;推荐一…

【函数栈帧的创建和销毁】(超详细图解)

想必大家在学完C语言函数章节之后&#xff0c;是否有这样的困惑&#xff1a; 局部变量是怎么创建的 &#xff1f; 为什么局部变量的值是随机值 &#xff1f; 函数是怎么传参的&#xff1f;传参的顺序又是什么样的 &#xff1f; 形参和实参是什么关系 &#xff1f; 函数调用…