Qt画一个太极

0.前言

之前看到一个有意思的太极动画,原图效果如下:

虽然看起来是 3D 的,但是观察角度并没有变,而且图形也只有两个平面在旋转,除了一些光晕的效果,完全可以用 QPainter 画一个类似的。

本文代码效果及链接如下:

github 链接(TaiJi类): https://github.com/gongjianbo/EasyQPainter

1.实现思路

观察原图可以发现,两个平面相交于一条线,那么我们可以以这条线将两个平面都分割为成表面和底面两部分(其实就是 z 值低于这条线的就会被遮挡),底面先绘制,表面后绘制。

有了大致的思路后,首先要解决的就是坐标变换。常用的有 QTransform,QMatrix 等方式,本文使用的是 QTransform + QPainter 对 z 轴和 x 轴进行旋转。旋转之后,clip 当前的矩形区域,这样就只会在被分割的部分进行绘制。

参照左侧示意图。旋转的时候,先 z 轴转 15 度,这就是红色平面最终的裁剪角度,这也是两个平面相交线的角度。然后 clip 红色底部的半个区域进行填充,后续的绘制是会遮盖在这上面的。再对 x 轴旋转 50 度,即绿色平面仰起来 50 度。然后 clip 绿色顶部的半个区域填充。两个底部区域填充完后,再同样的操作对表面进行填充。

在实现中还遇到两个小问题:

一是平面相交线处会有虚线,可能是 clip 和 fill 时边界处抗锯齿导致颜色和填充色不一致,所以我把 clip 高度加了一像素,以遮盖异常的颜色。

二是外面的两个小球,因为是当作球而不是平面圆处理,所以我是计算好中心点后填充渐变色使看起来像个圆球。在判断层级顺序的时候,不需要像大圆那样 clip 成两部分,而是和大圆小孔的圆心进行比较。如示意图白色的球,当球心 y 值小于灰色小孔圆心 y 值时,表示在表层,应该后绘制,否则应该先绘制(这里注意 Qt 窗口坐标是左上角为起始点,右下角为正方向)。

2.主要代码

#pragma once
#include <QWidget>
#include <QTimer>

//太极
class TaiJi : public QWidget
{
    Q_OBJECT
public:
    explicit TaiJi(QWidget *parent = nullptr);

protected:
    //显示时才启动定时动画
    void showEvent(QShowEvent *event) override;
    void hideEvent(QHideEvent *event) override;
    //绘制
    void paintEvent(QPaintEvent *event) override;
    //逻辑验证
    void taijiTest();
    //太极绘制
    void taijiPaint();

private:
    //定时动画
    QTimer timer;
    //旋转角度[0-360]
    int offset{0};
};

#include "TaiJi.h"
#include <QPaintEvent>
#include <QPainter>
#include <QPainterPath>
#include <QGradient>
#include <QBrush>
#include <QTransform>

TaiJi::TaiJi(QWidget *parent)
    : QWidget(parent)
{
    connect(&timer, &QTimer::timeout, this, [this]
    {
        offset += 1;
        offset %= 360;
        update();
    });
}

void TaiJi::showEvent(QShowEvent *event)
{
    timer.start(50);
    QWidget::showEvent(event);
}

void TaiJi::hideEvent(QHideEvent *event)
{
    timer.stop();
    QWidget::hideEvent(event);
}

void TaiJi::paintEvent(QPaintEvent *event)
{
    event->accept();
    {
        QPainter painter(this);
        //黑色背景
        painter.fillRect(this->rect(), Qt::black);
    }
    taijiTest();
    taijiPaint();
}

void TaiJi::taijiTest()
{
    //此函数主要是验证旋转和图层遮挡的逻辑
    QPainter painter(this);
    //painter.fillRect(this->rect(), Qt::black);
    painter.setRenderHint(QPainter::Antialiasing);

    //两像素的画笔避免抗锯齿或者变换后框线看不清
    QPen pen;
    pen.setWidth(2);

    int radius = 150;
    //圆形,后面旋转z轴和y轴使两个平面相交
    QPainterPath ellipse_path;
    ellipse_path.addEllipse(QPointF(0, 0), radius, radius);

    //在圆形基础上加了十字线,用于观察旋转方向
    QPainterPath line_path = ellipse_path;
    line_path.moveTo(0, radius);
    line_path.lineTo(0, -radius);
    line_path.closeSubpath();
    line_path.moveTo(radius, 0);
    line_path.lineTo(-radius, 0);
    line_path.closeSubpath();

    //QTransform是一个二维变换类,可以和QPainter搭配使用
    QTransform trans;
    //中心移动到窗口中心偏左
    trans.translate(width() / 2 - radius - 10, height() / 2);
    //z轴旋转15度,效果是平面上右转了15度
    //后面的变换也是在此基础上,所以两个平面相交的部分是右倾的
    trans.rotate(15, Qt::ZAxis);
    //这里设置trans后,接下来的clip裁剪区域也是旋转了15度的
    painter.setTransform(trans);
    //在初次变换的基础上,z轴随时间偏移,产生旋转动画效果
    QTransform ztrans = trans;
    ztrans.rotate(offset, Qt::ZAxis);
    //开始画第一个面
    pen.setColor(Qt::red);
    painter.setPen(pen);
    {
        //save是为了clip不污染后面的操作
        painter.save();
        //clip顶部的矩形区域(底部是被另一个面遮盖的)
        painter.setClipRect(QRect(-radius, -radius, radius * 2, radius));
        painter.setTransform(ztrans);
        //填充这个区域会受clip的影响
        painter.fillPath(ellipse_path, QColor(255, 0, 0, 100));
        painter.restore();
    }
    //在没有clip的情况下绘制框线,以进行观察
    painter.setTransform(ztrans);
    painter.drawPath(line_path);

    //在z轴旋转15度的基础上,x轴再旋转50度,即顶部往里翻转了
    trans.rotate(50, Qt::XAxis);
    painter.setTransform(trans);
    //旋转的角度需要反过来
    QTransform xtrans = trans;
    xtrans.rotate(-offset, Qt::ZAxis);
    //开始画第二个面,逻辑同第一个面
    pen.setColor(Qt::cyan);
    painter.setPen(pen);
    {
        painter.save();
        painter.setClipRect(QRect(-radius, 0, radius * 2, radius));
        painter.setTransform(xtrans);
        painter.fillPath(ellipse_path, QColor(0, 255, 0, 100));
        painter.restore();
    }
    painter.setTransform(xtrans);
    painter.drawPath(line_path);
}

void TaiJi::taijiPaint()
{
    QPainter painter(this);
    //painter.fillRect(this->rect(), Qt::black);
    painter.setRenderHint(QPainter::Antialiasing);

    QPen pen;
    //大圆半径
    int radius = 150;
    //小孔半径
    int sub_radius = radius / 5;
    //玉外面的球-圆心
    QPointF out_point(0, radius / 2);
    //玉小孔的圆心
    QPointF in_point(0, -radius / 2);
    //z轴旋转面的颜色
    QColor z_color(100, 100, 100);
    //x轴旋转面的颜色
    QColor x_color(200, 200, 200);

    //圆形,后面旋转z轴和y轴使两个平面相交
    QPainterPath ellipse_path;
    ellipse_path.addEllipse(QPointF(0, 0), radius, radius);

    //z轴旋转面的玉路径
    QPainterPath z_path;
    //奇偶填充,这样填充会把小孔留出空白
    z_path.setFillRule(Qt::OddEvenFill);
    z_path.moveTo(0, -radius);
    //一个大圆弧
    z_path.arcTo(QRectF(-radius, -radius, radius * 2, radius * 2), 90, 180);
    //两个小圆弧
    z_path.arcTo(QRectF(-radius / 2, 0, radius, radius), 270, -180);
    z_path.arcTo(QRectF(-radius / 2, -radius, radius, radius), 270, 180);
    //小孔
    z_path.addEllipse(in_point, sub_radius, sub_radius);
    z_path.closeSubpath();

    //x轴旋转面的玉路径,做两个是因为旋转方向相反,绘制取反后绘制的效果不大好
    QPainterPath x_path;
    x_path.setFillRule(Qt::OddEvenFill);
    x_path.moveTo(0, radius);
    x_path.arcTo(QRectF(-radius, -radius, radius * 2, radius * 2), 270, 180);
    x_path.arcTo(QRectF(-radius / 2, -radius, radius, radius), 90, 180);
    x_path.arcTo(QRectF(-radius / 2, 0, radius, radius), 90, -180);
    x_path.addEllipse(in_point, sub_radius, sub_radius);
    x_path.closeSubpath();

    //QTransform是一个二维变换类,可以和QPainter搭配使用
    QTransform trans;
    //中心移动到窗口中心偏右
    trans.translate(width() / 2 + radius + 10, height() / 2);
    //z轴旋转15度,效果是平面上右转了15度
    //后面的变换也是在此基础上,所以两个平面相交的部分是右倾的
    trans.rotate(15, Qt::ZAxis);
    //在初次变换的基础上,z轴随时间偏移,产生旋转动画效果
    QTransform ztrans = trans;
    ztrans.rotate(offset, Qt::ZAxis);
    //在z轴旋转15度的基础上,x轴再旋转50度,即顶部往里翻转了
    trans.rotate(50, Qt::XAxis);
    QTransform xtrans = trans;
    xtrans.rotate(-offset, Qt::ZAxis);

    //通过变换获取到小球和小孔圆心对应窗口实际的坐标
    QPointF z_out = ztrans.map(out_point);
    QPointF z_in = ztrans.map(in_point);
    QPainterPath z_ptpath;
    z_ptpath.addEllipse(z_out, sub_radius, sub_radius);
    QPointF x_out = xtrans.map(out_point);
    //QPointF x_in=xtrans.map(in_point);
    QPainterPath x_ptpath;
    x_ptpath.addEllipse(x_out, sub_radius, sub_radius);

    //两个小球的渐变填充,是看起来有点立体感
    QRadialGradient x_gradient(x_out, sub_radius);
    x_gradient.setColorAt(0, QColor(250, 250, 250));
    x_gradient.setColorAt(1, QColor(200, 200, 200));
    QRadialGradient z_gradient(z_out, sub_radius);
    z_gradient.setColorAt(0, QColor(150, 150, 150));
    z_gradient.setColorAt(1, QColor(100, 100, 100));

    //先绘制底层,即被遮盖的区域(相当于z值权重更低)
    {
        //绘制x轴旋转的小球,y小于另一个玉的小孔圆心y,表示当前被遮挡
        if (x_out.y() < z_in.y())
        {
            pen.setColor(Qt::red);
            painter.setPen(pen);
            painter.fillPath(x_ptpath, x_gradient);
        }

        painter.save();
        QTransform trans;
        trans.translate(width() / 2 + radius + 10, height() / 2);
        //z轴旋转15度,效果是平面上右转了15度
        trans.rotate(15, Qt::ZAxis);
        painter.setTransform(trans);
        {
            //save是为了clip不污染后面的操作
            painter.save();
            //clip顶部的矩形区域(底部是被另一个面遮盖的)
            painter.setClipRect(QRect(-radius, 0, radius * 2, radius));
            painter.setTransform(ztrans);
            painter.fillPath(z_path, z_color);
            painter.restore();
        }
        //在z轴旋转15度的基础上,x轴再旋转50度,即顶部往里翻转了
        trans.rotate(50, Qt::XAxis);
        painter.setTransform(trans);
        {
            //save是为了clip不污染后面的操作
            painter.save();
            //clip底部的矩形区域(顶部是被另一个面遮盖的)
            painter.setClipRect(QRect(-radius, -radius, radius * 2, radius));
            painter.setTransform(xtrans);
            painter.fillPath(x_path, x_color);
            painter.restore();
        }
        painter.restore();

        //绘制z轴旋转的小球
        pen.setColor(Qt::red);
        painter.setPen(pen);
        painter.fillPath(z_ptpath, z_gradient);
    }

    //绘制表层,逻辑同绘制底层
    {
        painter.save();
        QTransform trans;
        trans.translate(width() / 2 + radius + 10, height() / 2);
        trans.rotate(15, Qt::ZAxis);
        painter.setTransform(trans);
        {
            painter.save();
            //高度+2是为了遮盖两个平面相交部分clip+抗锯齿导致的虚线
            painter.setClipRect(QRect(-radius, -radius - 1, radius * 2, radius + 2));
            painter.setTransform(ztrans);
            painter.fillPath(z_path, z_color);
            painter.restore();
        }

        trans.rotate(50, Qt::XAxis);
        painter.setTransform(trans);
        {
            painter.save();
            painter.setClipRect(QRect(-radius, -1, radius * 2, radius + 2));
            painter.setTransform(xtrans);
            painter.fillPath(x_path, x_color);
            painter.restore();
        }
        painter.restore();

        //绘制x轴旋转的小球,y大于另一个玉的小孔圆心y,表示在表层
        if (x_out.y() >= z_in.y())
        {
            pen.setColor(Qt::red);
            painter.setPen(pen);
            painter.fillPath(x_ptpath, x_gradient);
        }
    }

    //小球定位测试
    //painter.drawEllipse(z_in,10,10);
    //painter.drawEllipse(z_out,10,10);
    //painter.drawEllipse(x_in,10,10);
    //painter.drawEllipse(x_out,10,10);
}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚建波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值