PyQt5 实现打字机效果

22 阅读4分钟

效果就是这样子

gif 麻烦,就截个图放这吧 image.png

代码

import sys, random
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QPainter, QFont, QColor, QPen, QFontMetrics

class TypewriterAnimation(QWidget):
    def __init__(self):
        super().__init__()
        
        # 窗口设置
        self.setWindowTitle("打字机动画效果")
        self.setGeometry(100, 100, 800, 600)
        self.setStyleSheet("background-color: #1a1a1a;")
        
        # 文字内容
        self.text_lines = [
            "欢迎来到打字机动画世界",
            "每个字符都会从天而降",
            "动画结束后会自动重播",
            "PyQt5 让一切变得简单"
        ]
        
        # 字体设置
        self.font = QFont("微软雅黑", 24, QFont.Bold)
        self.font_metrics = QFontMetrics(self.font)  # 用于获取字体度量信息
        
        # 动画参数
        self.char_data = []  # 存储每个字符的信息
        self.animation_speed = 3  # 下落速度
        self.char_delay = 100  # 字符出现间隔(毫秒)
        self.gravity = 0.5  # 重力加速度
        
        # 文字间距设置(可调整)
        self.char_spacing = 1.5  # 字符间距系数(1.0表示正常间距,大于1.0表示增加间距)
        
        # 当前状态
        self.current_line = 0
        self.current_char = 0
        self.animation_complete = False
        self.replay_delay = 2000  # 重播延迟(毫秒)
        
        # 定时器
        self.char_timer = QTimer(self)
        self.char_timer.timeout.connect(self.add_next_char)
        self.char_timer.start(self.char_delay)
        
        self.animation_timer = QTimer(self)
        self.animation_timer.timeout.connect(self.update_animation)
        self.animation_timer.start(16)  # 约60 FPS
        
        self.replay_timer = QTimer(self)
        self.replay_timer.timeout.connect(self.reset_animation)
        
    def add_next_char(self):
        """添加下一个字符到动画中"""
        if self.current_line < len(self.text_lines):
            line = self.text_lines[self.current_line]
            
            if self.current_char < len(line):
                char = line[self.current_char]
                
                # 添加字符数据
                char_info = {
                    'char': char,
                    'line_index': self.current_line,  # 记录行号
                    'char_in_line': self.current_char,  # 记录字符在行中的位置
                    'y': -50,  # 从屏幕上方开始
                    'velocity': 0,
                    'color': QColor(
                        random.randint(100, 255),
                        random.randint(100, 255),
                        random.randint(100, 255)
                    ),
                    'rotation': random.uniform(-30, 30),  # 随机旋转角度
                    'rotation_speed': random.uniform(-5, 5),  # 旋转速度
                    'scale': 0.1,  # 初始缩放
                    'scale_speed': 0.05,  # 缩放速度
                    'settled': False,  # 是否已稳定
                    'settle_progress': 0.0  # 稳定进度(0-1)
                }
                
                self.char_data.append(char_info)
                self.current_char += 1
            else:
                # 当前行完成,移到下一行
                self.current_line += 1
                self.current_char = 0
                
                if self.current_line >= len(self.text_lines):
                    # 所有文字完成,停止添加字符
                    self.char_timer.stop()
                    self.animation_complete = True
                    # 设置重播定时器
                    self.replay_timer.start(self.replay_delay)
    
    def update_animation(self):
        """更新动画状态"""
        for char_info in self.char_data:
            # 更新位置(重力效果)
            if not char_info['settled']:
                target_y = self.get_char_target_y(char_info)
                if char_info['y'] < target_y:
                    char_info['velocity'] += self.gravity
                    char_info['y'] += char_info['velocity']
                    
                    # 如果超过目标位置,回弹
                    if char_info['y'] > target_y:
                        char_info['y'] = target_y
                        char_info['velocity'] *= -0.5  # 弹性系数
                        
                        # 如果速度很小,开始稳定过程
                        if abs(char_info['velocity']) < 0.5:
                            char_info['velocity'] = 0
                            char_info['settled'] = True
                else:
                    char_info['y'] = target_y
                    char_info['settled'] = True
            
            # 如果已稳定,开始恢复到正常状态
            if char_info['settled'] and char_info['settle_progress'] < 1.0:
                char_info['settle_progress'] += 0.05  # 稳定进度增加速度
                if char_info['settle_progress'] > 1.0:
                    char_info['settle_progress'] = 1.0
                
                # 逐渐恢复旋转角度
                target_rotation = 0
                char_info['rotation'] = char_info['rotation'] * (1 - char_info['settle_progress']) + \
                                       target_rotation * char_info['settle_progress']
                
                # 逐渐恢复缩放
                target_scale = 1.0
                char_info['scale'] = char_info['scale'] * (1 - char_info['settle_progress']) + \
                                   target_scale * char_info['settle_progress']
            else:
                # 未稳定时的动画
                if not char_info['settled']:
                    # 更新旋转
                    if abs(char_info['rotation_speed']) > 0.1:
                        char_info['rotation'] += char_info['rotation_speed']
                        char_info['rotation_speed'] *= 0.95  # 旋转衰减
                    
                    # 更新缩放
                    if char_info['scale'] < 1.0:
                        char_info['scale'] += char_info['scale_speed']
                        if char_info['scale'] > 1.0:
                            char_info['scale'] = 1.0
        
        self.update()  # 触发重绘
    
    def get_char_target_y(self, char_info):
        """获取字符的目标Y坐标"""
        return 150 + char_info['line_index'] * 80
    
    def get_char_x(self, char_info):
        """获取字符的X坐标(水平居中)"""
        line = self.text_lines[char_info['line_index']]
        
        # 计算整行文本的总宽度(考虑间距)
        total_width = 0
        for i, c in enumerate(line):
            char_width = self.font_metrics.width(c)
            if i < len(line) - 1:  # 不是最后一个字符
                total_width += char_width * self.char_spacing
            else:  # 最后一个字符不需要额外间距
                total_width += char_width
        
        # 计算起始位置(居中)
        start_x = (self.width() - total_width) // 2
        
        # 计算当前字符的位置
        x = start_x
        for i in range(char_info['char_in_line']):
            char_width = self.font_metrics.width(line[i])
            x += char_width * self.char_spacing
        
        return x
    
    def resizeEvent(self, event):
        """窗口大小改变时重新计算字符位置"""
        super().resizeEvent(event)
        # 窗口大小改变时,所有字符的位置需要重新计算
        self.update()
    
    def reset_animation(self):
        """重置动画"""
        self.replay_timer.stop()
        self.char_data.clear()
        self.current_line = 0
        self.current_char = 0
        self.animation_complete = False
        self.char_timer.start(self.char_delay)
    
    def paintEvent(self, event):
        """绘制所有字符"""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        
        # 绘制每个字符
        for char_info in self.char_data:
            painter.save()
            
            # 设置字体
            painter.setFont(self.font)
            
            # 设置颜色
            painter.setPen(QPen(char_info['color'], 2))
            
            # 获取字符位置(动态计算,确保窗口大小变化时居中)
            x = self.get_char_x(char_info)
            y = char_info['y']
            
            # 移动到字符位置
            painter.translate(x, y)
            
            # 应用旋转
            painter.rotate(char_info['rotation'])
            
            # 应用缩放
            painter.scale(char_info['scale'], char_info['scale'])
            
            # 绘制字符(居中绘制)
            font_metrics = painter.fontMetrics()
            char_width = font_metrics.width(char_info['char'])
            char_height = font_metrics.height()
            
            painter.drawText(
                -char_width // 2,
                char_height // 2,
                char_info['char']
            )
            
            # 绘制阴影效果(仅在下落过程中)
            if not char_info['settled']:
                painter.setPen(QPen(QColor(255, 255, 255, 30), 1))
                painter.drawText(
                    -char_width // 2 + 2,
                    char_height // 2 + 2,
                    char_info['char']
                )
            
            painter.restore()
        
        # 绘制进度指示器
        # if not self.animation_complete:
        #     painter.setPen(QPen(QColor(100, 100, 100), 2))
        #     painter.setFont(QFont("Arial", 10))
        #     progress_text = f"进度: {self.current_line + 1}/{len(self.text_lines)} 行"
        #     painter.drawText(10, self.height() - 10, progress_text)
        # else:
        #     painter.setPen(QPen(QColor(255, 255, 0), 2))
        #     painter.setFont(QFont("Arial", 12))
        #     painter.drawText(
        #         self.width() // 2 - 50,
        #         self.height() - 20,
        #         "即将重播..."
        #     )
        
        # 显示当前间距设置
        # painter.setPen(QPen(QColor(150, 150, 150), 1))
        # painter.setFont(QFont("Arial", 10))
        # spacing_text = f"字符间距: {self.char_spacing:.1f}"
        # painter.drawText(self.width() - 120, 20, spacing_text)

def main():
    app = QApplication(sys.argv)
    
    # 创建动画窗口
    animation = TypewriterAnimation()
    animation.show()
    
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()