有趣滑动变脸v1+升级版(python)

2025年5月24日 999+浏览

下列代码是v1版本:实现了基础的滑动变脸(v1版本,很少改动),

v2版本:实现了更强的视觉视觉跟随效果及表情变化和捕捉范围(代码文件在文章末尾)

# -*- coding: utf-8 -*-
# 依赖库:PySide6
# 安装:pip install PySide6 -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple

import sys, math
from PySide6.QtCore import Qt, QTimer, QPointF, QRectF
from PySide6.QtGui import (
    QPainter, QBrush, QPen, QColor, QLinearGradient, QRadialGradient,
    QPainterPath, QFont
)
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QPushButton, QVBoxLayout,
    QHBoxLayout, QLabel, QStackedWidget
)

def drawRoundedRectDifferentRadii(painter, rect: QRectF,
                                  tl_radius, tr_radius, br_radius, bl_radius):
    """
    绘制一个具有不同四角圆角的矩形
    """
    path = QPainterPath()

    # 从左上角开始绘制
    path.moveTo(rect.left() + tl_radius, rect.top())
    # 绘制上边到右上角
    path.lineTo(rect.right() - tr_radius, rect.top())
    # 右上角的圆弧
    path.quadTo(rect.right(), rect.top(), rect.right(), rect.top() + tr_radius)

    # 绘制右边到右下角
    path.lineTo(rect.right(), rect.bottom() - br_radius)
    # 右下角的圆弧
    path.quadTo(rect.right(), rect.bottom(), rect.right() - br_radius, rect.bottom())

    # 绘制下边到左下角
    path.lineTo(rect.left() + bl_radius, rect.bottom())
    # 左下角的圆弧
    path.quadTo(rect.left(), rect.bottom(), rect.left(), rect.bottom() - bl_radius)

    # 绘制左边到左上角
    path.lineTo(rect.left(), rect.top() + tl_radius)
    # 左上角的圆弧
    path.quadTo(rect.left(), rect.top(), rect.left() + tl_radius, rect.top())

    path.closeSubpath()
    painter.drawPath(path)


class FaceWidget(QWidget):
    """
    一个动态脸部控件,用于展示不同表情。
    """
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMinimumSize(220, 220)

        # 当前状态(动画插值用)
        self.current_happiness = 0.9
        self.current_derp = 1.0
        self.current_px = 0.5
        self.current_py = 0.5

        # 目标状态
        self.target_happiness = 0.9
        self.target_derp = 1.0
        self.target_px = 0.5
        self.target_py = 0.5

        # 定时器做动画插值
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.animateStep)
        self.timer.start(16)

        self.setMouseTracking(True)

    def setTargetExpression(self, happiness, derp, px, py):
        self.target_happiness = happiness
        self.target_derp = derp
        self.target_px = px
        self.target_py = py

    def setExpressionInstant(self, happiness, derp, px, py):
        # 直接设定,不动画
        self.current_happiness = self.target_happiness = happiness
        self.current_derp = self.target_derp = derp
        self.current_px = self.target_px = px
        self.current_py = self.target_py = py
        self.update()

    def animateStep(self):
        def smooth_update(cur, tgt, speed=0.1):
            if abs(tgt - cur) < 0.001:
                return tgt
            return cur + (tgt - cur) * speed

        self.current_happiness = smooth_update(self.current_happiness, self.target_happiness)
        self.current_derp      = smooth_update(self.current_derp, self.target_derp)
        self.current_px        = smooth_update(self.current_px, self.target_px)
        self.current_py        = smooth_update(self.current_py, self.target_py)
        self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, True)

        w = self.width()
        h = self.height()
        size = min(w, h)
        face_rect = QRectF((w - size)/2, (h - size)/2, size, size)

        # (1) 脸部底色 (径向渐变)
        grad = QRadialGradient(face_rect.center(), size*0.5)
        grad.setColorAt(0, QColor("#f7e0b2"))
        grad.setColorAt(1, QColor("#eb5"))
        painter.setBrush(QBrush(grad))
        painter.setPen(QPen(Qt.black, 2))
        painter.drawEllipse(face_rect)

        # (2) 顶部渐变叠加,透明度 = (1 - happiness)
        overlay = QLinearGradient(face_rect.topLeft(), face_rect.bottomLeft())
        overlay.setColorAt(0, QColor("#5a8"))
        overlay.setColorAt(1, QColor(85,170,136,0))
        painter.save()
        painter.setOpacity(1 - self.current_happiness)
        painter.setBrush(QBrush(overlay))
        painter.setPen(Qt.NoPen)
        painter.drawEllipse(face_rect)
        painter.restore()

        face_x, face_y = face_rect.x(), face_rect.y()
        face_w, face_h = face_rect.width(), face_rect.height()

        # (3) 腮红
        blush_w = 0.20 * face_w
        blush_h = 0.10 * face_h
        blush_top = face_y + 0.45*face_h + self.current_py*0.10*face_h
        blush_color = QColor(255, 100, 100, int((self.current_happiness**2*0.9 + 0.1)*255))
        painter.setBrush(QBrush(blush_color))
        painter.setPen(QPen(blush_color, 3))

        # 左腮红
        blush_left_x = face_x + 0.07*face_w + self.current_px*0.02*face_w
        painter.drawEllipse(QRectF(blush_left_x, blush_top, blush_w, blush_h))

        # 右腮红
        blush_right_x = face_x + face_w - (0.09*face_w - self.current_px*0.02*face_w) - blush_w
        painter.drawEllipse(QRectF(blush_right_x, blush_top, blush_w, blush_h))

        # (4) 眼睛
        # 尺寸: 26% - happiness*2%
        eye_size = 0.26*face_w - self.current_happiness*0.02*face_w
        eye_top = face_y + 0.25*face_h + self.current_py*0.10*face_h
        # 左眼
        left_eye_x = face_x + 0.18*face_w + self.current_px*0.04*face_w
        left_eye_rect = QRectF(left_eye_x, eye_top, eye_size, eye_size)
        # 右眼
        right_eye_x = face_x + face_w - (0.22*face_w - self.current_px*0.04*face_w) - eye_size
        right_eye_rect = QRectF(right_eye_x, eye_top, eye_size, eye_size)

        painter.setBrush(Qt.white)
        painter.setPen(QPen(Qt.black, 2))
        painter.drawEllipse(left_eye_rect)
        painter.drawEllipse(right_eye_rect)

        # (5) 瞳孔
        pupil_scale = 0.55 - self.current_happiness*0.10
        pupil_d = pupil_scale * eye_size
        pupil_radius = pupil_d/2
        # 根据公式进行偏移
        offset_factor = 0.3
        offset_x = ((self.current_px + self.current_derp*0.5) - 0.5)*eye_size*offset_factor
        offset_y = ((self.current_py + self.current_derp*0.5) - 0.5)*eye_size*offset_factor

        painter.setBrush(QColor("#421"))
        painter.setPen(Qt.NoPen)
        # 左眼瞳孔中心
        lc = left_eye_rect.center()
        painter.drawEllipse(QPointF(lc.x()+offset_x, lc.y()+offset_y), pupil_radius, pupil_radius)
        # 右眼瞳孔中心
        rc = right_eye_rect.center()
        painter.drawEllipse(QPointF(rc.x()+offset_x, rc.y()+offset_y), pupil_radius, pupil_radius)

        # (6) 嘴巴
        # width: calc(51% - happiness*2%), height: calc(26% - happiness*2%)
        mouth_w = 0.51*face_w - self.current_happiness*0.02*face_w
        mouth_h = 0.26*face_h - self.current_happiness*0.02*face_h
        mouth_x = face_x + 0.475*face_w + self.current_px*0.05*face_w - mouth_w/2
        mouth_y = face_y + 0.575*face_h + self.current_py*0.05*face_h

        mouth_rect = QRectF(mouth_x, mouth_y, mouth_w, mouth_h)

        # 圆角半径
        em = mouth_h * 0.07
        tl = tr = max(0, (1 - self.current_happiness)*5*em)
        br = bl = max(0, self.current_happiness*16*em)

        # 填充 #a33 + 3px 边框 #962d2d
        painter.setBrush(QColor("#a33"))
        painter.setPen(QPen(QColor("#962d2d"), 3))
        drawRoundedRectDifferentRadii(painter, mouth_rect, tl, tr, br, bl)


# -------------------------
# 页面基类
# -------------------------
class BasePage(QWidget):
    def __init__(self, title_text, subtitle_text):
        super().__init__()
        layout = QVBoxLayout(self)
        layout.setContentsMargins(40, 20, 40, 20)
        layout.setSpacing(10)

        # 标题
        self.titleLabel = QLabel(title_text)
        self.titleLabel.setAlignment(Qt.AlignCenter)
        font = QFont()
        font.setPointSize(20)
        self.titleLabel.setFont(font)
        layout.addWidget(self.titleLabel)

        # 副标题
        self.subtitleLabel = QLabel(subtitle_text)
        self.subtitleLabel.setAlignment(Qt.AlignCenter)
        font2 = QFont()
        font2.setPointSize(14)
        self.subtitleLabel.setFont(font2)
        layout.addWidget(self.subtitleLabel)

        # 动态脸部
        self.face_widget = FaceWidget()
        layout.addWidget(self.face_widget, stretch=1)

        # 按钮区
        self.buttonLayout = QHBoxLayout()
        layout.addLayout(self.buttonLayout)

# 确认页
class ConfirmPage(BasePage):
    def __init__(self):
        super().__init__(
            "听说你刚才拉屎没擦屁股",
            "真的假的??"
        )
        self.btnAccept = QPushButton("假的,我根本没去上厕所")
        self.btnReject = QPushButton("真的,不过已风干")
        self.buttonLayout.addWidget(self.btnAccept)
        self.buttonLayout.addWidget(self.btnReject)
        self.setMouseTracking(True)

    def mouseMoveEvent(self, event):
        pos = event.position()
        x, y = pos.x(), pos.y()

        # 按钮中心
        ag = self.btnAccept.geometry()
        rg = self.btnReject.geometry()
        ac = QPointF(ag.x() + ag.width()/2, ag.y() + ag.height()/2)
        rc = QPointF(rg.x() + rg.width()/2, rg.y() + rg.height()/2)

        distA = math.hypot(x - ac.x(), y - ac.y())
        distR = math.hypot(x - rc.x(), y - rc.y())
        total = distA + distR
        if total < 1e-6:
            ratio = 0.5
        else:
            ratio = distR / total

        happiness = ratio**0.75
        derp = 0.0
        w, h = self.width(), self.height()
        px = x / w
        py = y / h
        self.face_widget.setTargetExpression(happiness, derp, px, py)
        super().mouseMoveEvent(event)

    def leaveEvent(self, event):
        # 鼠标离开时恢复默认
        self.face_widget.setTargetExpression(0.9, 1.0, 0.5, 0.5)
        super().leaveEvent(event)

# 接受页
class AcceptedPage(BasePage):
    def __init__(self):
        super().__init__(
            "太好了,我们还是朋友!",
            "错怪你了,对不起 o(╥﹏╥)o"
        )
        self.btnReturn = QPushButton("乖乖")
        self.buttonLayout.addWidget(self.btnReturn)
        # 固定表情
        self.face_widget.setExpressionInstant(1.0, 0.0, 0.5, 0.5)

# 拒绝页
class RejectedPage(BasePage):
    def __init__(self):
        super().__init__(
            "┌(。Д。)┐ 好吧",
            "我尊重你的爱好,但是你能不能先从我的腿上挪开?!"
        )
        self.btnReturn = QPushButton("再见")
        self.buttonLayout.addWidget(self.btnReturn)
        # 固定表情
        self.face_widget.setExpressionInstant(0.2, 0.0, 0.5, 0.5)

# 主窗口 + 页面切换
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("滑动变脸")
        self.resize(700, 500)
        self.stack = QStackedWidget()
        self.setCentralWidget(self.stack)

        self.confirmPage = ConfirmPage()
        self.acceptedPage = AcceptedPage()
        self.rejectedPage = RejectedPage()

        self.stack.addWidget(self.confirmPage)
        self.stack.addWidget(self.acceptedPage)
        self.stack.addWidget(self.rejectedPage)

        # 按钮事件
        self.confirmPage.btnAccept.clicked.connect(lambda: self.stack.setCurrentWidget(self.acceptedPage))
        self.confirmPage.btnReject.clicked.connect(lambda: self.stack.setCurrentWidget(self.rejectedPage))
        self.acceptedPage.btnReturn.clicked.connect(lambda: self.stack.setCurrentWidget(self.confirmPage))
        self.rejectedPage.btnReturn.clicked.connect(lambda: self.stack.setCurrentWidget(self.confirmPage))

if __name__ == "__main__":
    # Ensure only one QApplication instance is created
    if not QApplication.instance():
        app = QApplication(sys.argv)
    else:
        app = QApplication.instance()  # Use the existing instance

    window = MainWindow()
    window.show()
    sys.exit(app.exec())

下载v1的python源代码文件及v2升级版:滑动变脸v1 python压缩包