一个用 pygame 实现的弹幕式数据可视化动画框架

一个用pygame实现的弹幕式数据可视化动画框架

  • 动画代码
  • 爬虫代码
  • 数据

需要 Python 环境+ Pygame 库,点击即可运行。效果见B站视频: 中国古代历史人物生卒时间可视化。

动画代码

from msilib.schema import Font
import pygame,sys,random,datetime,os,json
from time import sleep,time
from random import randint
from pygame.locals import *#引用后可以直接输入按键名
from heapq import heappop,heappush,heapreplace
from math import inf,ceil,sqrt
from win32.win32api import GetSystemMetrics
import pyautogui

soundDir="D:\\Music\\"
songs=[
    "杨洪基 - 滚滚长江东逝水.mp3",
    "朱桦 - 意中人.mp3",
]

backgroundColor=(250,250,250)
color_WHITE=(255,255,255)
color_RED=(255,0,0)
color_GRAY=(200,200,200)
color_DarkGRAY=(100,100,100)
windowSize=[int(1*GetSystemMetrics(0)),int(1*GetSystemMetrics(1))]
topMargin,bottomMargin=[min(windowSize)//18,max(windowSize)//60]
mainHeight=windowSize[1]-topMargin-bottomMargin
midXLine=windowSize[0]//2

textSize=max(windowSize)//60
titleTextSize=2*textSize
titleTopMargin=0
FPS=30#60
animationTime=FPS#创建新矩形、消除旧矩形的时间都是1秒,

FPSCLOCK=pygame.time.Clock()
BACKGROUND=pygame.display.set_mode(windowSize)
SCREEN=pygame.display.set_mode(windowSize)

def MinkowskiDis(v1:list[int],v2:list[int])->int:
    return sum(abs(v1[i]-v2[i]) for i in range(len(v1)))

def loadHistoryData():#peopleData=[人名,出生时间,死亡时间,时间是否精确,浏览次数,备注]
    with open("全部历史人物生卒时间_含热度_备注_json版.txt",'r',encoding='utf-8') as f:
        peopleData=json.load(f)
        print(f"从文件中加载完毕,{
     len(peopleData)}项")
        f.close()
    allNum=sum(len(i[0].split(",")) for i in peopleData)
    print(f"文件读到的总人数:{
     allNum}")
    return peopleData

class HistoryVisualization:
    def __init__(self,peopleData=[]):
        self.pauseState=True
        self.timeWindowWidth=110#当前页面宽度对应的时间
        self.timeGridWidth=10
        self.curLeftTime=-2070-self.timeWindowWidth
        self.timeStep0=0.51
        self.timeStep=self.timeStep0
        if len(peopleData):
            self.peopleData=peopleData
        else:
            self.peopleData=loadHistoryData()#[人名,出生年份,去世年份,时间是否可信,浏览次数,备注]
        self.historyDataInitial(isNew=(len(peopleData)==0))
        self.curPlayState=0#共有4个状态,分别是标题、正式排序、收缩总结、结尾
        self.stateTime=[2.9*FPS,180*FPS,10*FPS,inf]
        self.marginRate=0.5 # 条目之间的排布,条目过多时要压缩
        self.defaultRowNum=20
        self.rectRowNum=self.rectRowNum0=20
        self.rowsFreeTime=[self.curLeftTime]*self.rectRowNum
        self.rowsFreeTimeHeap=[[self.curLeftTime,i] for i in range(self.rectRowNum)]
        self.rectHeight=self.targetRectHeight=mainHeight/((1+self.marginRate)*self.rectRowNum+self.marginRate)

    def historyDataInitial(self,isNew=True):
        self.startYear=min(-2070,min(i[1] for i in self.peopleData)-40)
        self.endYear=max(1910,max(i[2] for i in self.peopleData)+40)
        self.curLeftTime=min(self.curLeftTime,self.startYear)
        self.dataIdx=0#遍历到的索引
        self.hp=[]#按开始时间入堆,按结束时间出堆
        tp=[]
        if isNew:#未经处理
            self.peopleData+=tp+[["空心表示时间不确定,仅供参考",self.startYear+self.timeWindowWidth//2-20,self.startYear+self.timeWindowWidth//2+20,0],['今',2024,2025,1]]
            self.peopleData.sort(key=lambda x:(x[1],x[2]))
        n=len(self.peopleData)
        self.dataRowIdx=[0]*n#每个数据标签都在一个固定的行中
        importantPeopleDc=set(["黄帝","炎帝","大禹","老子","孔子","屈原","嬴政","秦始皇","刘彻","卫青","霍去病","诸葛亮","李世民","武则天","李白","杜甫","苏轼","辛弃疾","朱元璋","王阳明","孙中山","毛泽东",'今'])
        self.peopleDataColors=[[randint(0,255) for j in range(3)] for i in range(n)]
        for i in range(n):
            if any(name in importantPeopleDc for name in self.peopleData[i][0].split(",")):
                self.peopleDataColors[i]=color_RED
            else:
                while min(MinkowskiDis(self.peopleDataColors[i],backgroundColor),MinkowskiDis(self.peopleDataColors[i],color_RED))<60:
                    self.peopleDataColors[i]=[randint(0,255) for j in range(3)]
        
        #计算人名、称号长度
        self.dataNameLen=[0]*n#统计长度方便右对齐,可能有()、数字
        for i in range(n):
            sm=0
            for c in self.peopleData[i][0]:
                if c in "+-":
                    sm+=0.6
                elif c in "()" or c.isdigit():
                    sm+=0.5
                else:
                    sm+=1
            self.dataNameLen[i]=sm

        #时代信息
        self.eraData=[['夏朝', -2070, -1600], ['商朝', -1600, -1046], ['西周', -1046, -771],['春秋',-770,-476],['战国',-475,-221], ['秦朝', -221, -207], ['西汉', -206, 8], ['新朝', 8, 23],['玄汉',23,25], ['东汉', 25,220],['三国',220,265] ,['西晋', 265,316],['东晋',317,420],['南北朝',420,581], ['隋朝', 581,618], ['唐朝', 618,690], ['武周', 690,705], ['唐朝', 705,907],['五代十国',907,960], ['北宋', 960,1127],['南宋',1127,1279], ['元朝', 1279,1368], ['明朝', 1368,1644], ['清朝', 1644,1911]]
        self.eraLeftIdx=0
        self.eraRightIdx=0
        n=len(self.eraData)
        self.eraDataColors=[[randint(0,255) for j in range(3)] for i in range(n)]
        for i in range(n):
            while MinkowskiDis(self.eraDataColors[i],backgroundColor)<60:
                self.eraDataColors[i]=[randint(0,255) for j in range(3)]
        
        idx=self.eraData.index(['武周', 690,705])#唐朝颜色统一
        if idx!=-1:
            self.eraDataColors[idx]=self.eraDataColors[idx-1]
            self.eraDataColors[idx+1]=self.eraDataColors[idx-1]

    def plotText(self,pos,label,color=[0,0,0],textSize=30,fontName='simhei'):
        """绘制文本,不支持传入Font"""
        FONT=pygame.font.Font(pygame.font.match_font(fontName),int(textSize))
        textInfo=FONT.render(label,True,color)
        SCREEN.blit(textInfo,pos)

    def getXPosFromTime(self,t):
        return windowSize[0]*(t-self.curLeftTime)/self.timeWindowWidth

    def plotBackground(self):
        """
        静态背景,预想中只用绘制一次
        """
        SCREEN.fill(backgroundColor)
        if self.curPlayState<2:
            FONT=pygame.font.Font(pygame.font.match_font('simhei'),titleTextSize)
            textInfo=FONT.render('时代:',True,color_DarkGRAY)
            SCREEN.blit(textInfo,(0,titleTopMargin))
        #左下角
        if self.curPlayState<3:
            FONT=pygame.font.Font(pygame.font.match_font('simhei'),textSize)
            textInfo=FONT.render('公元',True,color_DarkGRAY)
            SCREEN.blit(textInfo,(0,windowSize[1]-textSize))
        
    def plotScreenBackground(self):
        """
        运动的背景,背景线等
        """
        FONT=pygame.font.Font(pygame.font.match_font('simhei'),textSize)
        t=self.curLeftTime%self.timeGridWidth
        startTime=int(self.curLef

你可能感兴趣的:(python,pygame,数据可视化)