
下列代码是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压缩包