1818#from scipy.spatial import cKDTree
1919from PyQt5 import QtCore
2020from numba import njit
21+ from PyQt5 .QtCore import QEvent , QObject
2122#import time
2223
23-
24- class PatternVisualizer3D :
24+ class PatternVisualizer3D (QObject ):
2525 def __init__ (self , visualize_piano = False , pos_type = "Fibonacci" , orientation = "up" ):
26+ super ().__init__ () # 初始化 QObject
2627 self .orientation = orientation
2728 self .data_height = 300
2829 self .pos_type = pos_type
2930 self .total_center = (0 , 0 , self .data_height // 2 )
3031 self .visualize_piano = visualize_piano
3132 self .working = True
33+ self .theme_index = 0
34+ self .fig_themes_rgba = [(0. ,0. ,60 / 255 ,1. ), (0. ,0. ,0. ,1. ), (1. ,1. ,1. ,1. ), (232 / 255 ,212 / 255 ,114 / 255 ,1. )]
35+ self .data_themes_rgb = [(229 / 255 ,248 / 255 ,1. ), (1. ,1. ,1. ), (0. ,0. ,0. ), (184 / 255 , 34 / 255 , 20 / 255 )]
3236 self ._initialize_plot ()
3337 self .position_list = self ._generate_positions (120 , self .total_center [0 ], self .total_center [1 ], 2 , 36 , pos_type = self .pos_type )
3438 self ._initialize_data ()
@@ -43,14 +47,20 @@ def _initialize_plot(self):
4347 self .target_elev = 30
4448 self .azim_angle = 30
4549 self .target_azim_speed = 1
46- self .fig = plt .figure (facecolor = ( 0 , 0 , 60 / 255 , 1 ), figsize = (7 , 7 ))
50+ self .fig = plt .figure (facecolor = self . fig_themes_rgba [ 0 ], figsize = (8 , 6 ))
4751 self .fig .canvas .manager .window .setWindowTitle ("🎼Musical Bubble Column!🎹" )
4852 self .toolbar = self .fig .canvas .manager .toolbar
4953 self .toolbar .hide ()
5054 new_icon = QtGui .QIcon (PATH_TO_ICON )
51- fig = plt .gcf ()
52- fig .canvas .manager .window .setWindowIcon (QtGui .QIcon (new_icon ))
55+ self .mouse_pressing = False
56+ self .mouse_controling_slider = False
57+ self .fig .canvas .manager .window .setWindowIcon (QtGui .QIcon (new_icon ))
5358 self .fig .canvas .manager .window .setWindowFlags (QtCore .Qt .WindowStaysOnTopHint ) # 设置窗口置顶
59+ self .fig .canvas .manager .window .installEventFilter (self ) # 安装事件过滤器
60+ self .fig .canvas .mpl_connect ('motion_notify_event' , self .on_mouse_move ) # 连接鼠标移动事件
61+ self .fig .canvas .mpl_connect ('button_press_event' , self .on_mouse_click ) # 连接鼠标点击事件
62+ self .fig .canvas .mpl_connect ('button_press_event' , self .on_mouse_press )
63+ self .fig .canvas .mpl_connect ('button_release_event' , self .on_mouse_release )
5464 if self .visualize_piano :
5565 if self .orientation == "down" :
5666 gs = GridSpec (2 , 1 , height_ratios = [1 , 30 ])
@@ -66,13 +76,14 @@ def _initialize_plot(self):
6676 self .ax .view_init (elev = self .elev , azim = self .azim_angle )
6777 plt .subplots_adjust (left = 0 , right = 1 , top = 1 , bottom = 0 )
6878 self ._hide_axes ()
69- self .ax .set_facecolor ((0 , 0 , 60 / 255 , 1 ))
79+ self .data_color = self .data_themes_rgb [0 ]
80+ self .ax .set_facecolor (self .fig_themes_rgba [0 ])
7081 self .ax .set_box_aspect ([1 , 1 , 3 ])
7182 self .elev_slider = plt .axes ([0.9 , 0.1 , 0.03 , 0.8 ], facecolor = 'none' ) # 创建滑条位置并设置颜色
72- self .elev_slider = plt .Slider (self .elev_slider , '' , 0 , 90 , orientation = 'vertical' , valinit = self .elev , color = (1 ,1 ,1 ,0.0 ), initcolor = "none" , track_color = (1 ,1 ,1 ,0.1 ), handle_style = {'facecolor' : 'none' , 'edgecolor' : '1 ' , 'size' : 10 }) # 初始化滑条并设置颜色
83+ self .elev_slider = plt .Slider (self .elev_slider , '' , 0 , 90 , orientation = 'vertical' , valinit = self .elev , color = (1 ,1 ,1 ,0.0 ), initcolor = "none" , track_color = (1 ,1 ,1 ,0.1 ), handle_style = {'facecolor' : 'none' , 'edgecolor' : '0.6 ' , 'size' : 10 }) # 初始化滑条并设置颜色
7384 self .elev_slider .on_changed (self .update_elev ) # 绑定滑条变化事件
7485 self .azim_slider = plt .axes ([0.2 , 0.01 if self .orientation == "down" else 0.1 , 0.6 , 0.03 ], facecolor = 'none' ) # 创建滑条位置并设置颜色
75- self .azim_slider = plt .Slider (self .azim_slider , '' , - 5 , 5 , orientation = 'horizontal' , valinit = self .target_azim_speed , color = (1 ,1 ,1 ,0.0 ), initcolor = "none" , track_color = (1 ,1 ,1 ,0.1 ), handle_style = {'facecolor' : 'none' , 'edgecolor' : '1 ' , 'size' : 10 }) # 初始化滑条并设置颜色
86+ self .azim_slider = plt .Slider (self .azim_slider , '' , - 5 , 5 , orientation = 'horizontal' , valinit = self .target_azim_speed , color = (1 ,1 ,1 ,0.0 ), initcolor = "none" , track_color = (1 ,1 ,1 ,0.1 ), handle_style = {'facecolor' : 'none' , 'edgecolor' : '0.6 ' , 'size' : 10 }) # 初始化滑条并设置颜色
7687 self .azim_slider .on_changed (self .update_azim ) # 绑定滑条变化事件
7788 self .fig .canvas .mpl_connect ('close_event' , self .handle_close )
7889 if self .visualize_piano :
@@ -94,6 +105,15 @@ def _initialize_data(self):
94105 self .position_index = {pos : idx for idx , pos in enumerate (self .position_list )}
95106 #self.position_tree = cKDTree(self.position_list) # 创建KD树
96107 self .opacity_dict = self ._calculate_opacity ()
108+ self .defalt_zlim = (0 , self .data_height + 2 )
109+ self .defalt_xlim = (- max_size // 2 , max_size // 2 )
110+ self .defalt_ylim = (- max_size // 2 , max_size // 2 )
111+ self .target_zlim = self .defalt_zlim
112+ self .target_xlim = self .defalt_xlim
113+ self .target_ylim = self .defalt_ylim
114+ self .zlim = self .defalt_zlim
115+ self .xlim = self .defalt_xlim
116+ self .ylim = self .defalt_ylim
97117
98118 def _generate_positions (self , num_positions , center_x , center_y , inner_radius , outer_radius , pos_type = "Fibonacci" ):
99119 positions = []
@@ -125,7 +145,6 @@ def _generate_positions(self, num_positions, center_x, center_y, inner_radius, o
125145 positions .append ((x , y ))
126146 outer_radius += 1
127147
128- self ._update_axis_limits (positions )
129148 # 计算偏移量
130149 min_x = min (pos [0 ] for pos in positions )
131150 min_y = min (pos [1 ] for pos in positions )
@@ -134,7 +153,7 @@ def _generate_positions(self, num_positions, center_x, center_y, inner_radius, o
134153 positions = [(x + self .offset [0 ], y + self .offset [1 ]) for x , y in positions ]
135154 return positions
136155
137- def update_pattern (self , new_pattern , volumes , average_volume , radius = 5 ):
156+ def update_pattern (self , new_pattern , volumes , average_volume ): # , radius=5
138157 # 检查绘图窗口是否仍然打开
139158 if not plt .fignum_exists (self .fig .number ):
140159 self ._initialize_plot () # 重新初始化绘图窗口
@@ -149,18 +168,29 @@ def update_pattern(self, new_pattern, volumes, average_volume, radius=5):
149168 # dist, index = self.position_tree.query(adjusted_coord, distance_upper_bound=radius)
150169 # if dist != float('inf'): # 如果找到在半径范围内的点
151170 # bit_array[index] = 1
152- else :
153- raise ValueError ("new_pattern must be either a bytes object or a list of (x, y) tuples." )
171+ # else:
172+ # raise ValueError("new_pattern must be either a bytes object or a list of (x, y) tuples.")
154173
155174 # 重置最后一层的pattern_data 和 pattern_data_thickness,淘汰边缘的旧数据
156175 self .pattern_data [- 1 if self .orientation == "down" else 0 , :, :] = 0
157176 self .pattern_data_thickness [- 1 if self .orientation == "down" else 0 , :, :] = 0
158177 self .azim_angle = (self .azim_angle - self .target_azim_speed ) % 360
159178 self .elev = self .elev + (self .target_elev - self .elev ) * 0.1
179+
160180 self ._update_data_layer (bit_array , volumes , average_volume )
161181
162182 self .ax .cla ()
163- self .ax .set_zlim (0 , self .data_height + 2 )
183+ if self .mouse_controling_slider :
184+ self .target_xlim = (self .defalt_xlim [0 ]* 1.5 , self .defalt_xlim [1 ]* 1.5 )
185+ self .target_ylim = (self .defalt_ylim [0 ]* 1.5 , self .defalt_ylim [1 ]* 1.5 )
186+ # 平滑过渡到目标限制
187+ self .zlim = tuple (np .array (self .zlim ) + (np .array (self .target_zlim ) - np .array (self .zlim )) * 0.1 )
188+ self .xlim = tuple (np .array (self .xlim ) + (np .array (self .target_xlim ) - np .array (self .xlim )) * 0.1 )
189+ self .ylim = tuple (np .array (self .ylim ) + (np .array (self .target_ylim ) - np .array (self .ylim )) * 0.1 )
190+ self .ax .set_xlim (self .xlim )
191+ self .ax .set_ylim (self .ylim )
192+ self .ax .set_zlim (self .zlim )
193+
164194 self ._hide_axes ()
165195 self ._draw_pattern ()
166196 if self .visualize_piano :
@@ -244,7 +274,7 @@ def _draw_pattern(self):
244274 else :
245275 all_opacity = step1_all_opacity
246276
247- self .ax .scatter (all_x , all_y , all_z , c = [( 229 / 255 , 248 / 255 , 1 , op ) for op in all_opacity ], marker = 'o' , s = all_sizes )
277+ self .ax .scatter (all_x , all_y , all_z , c = [self . data_color + ( op , ) for op in all_opacity ], marker = 'o' , s = all_sizes )
248278
249279 # 绘制最后一层
250280 #x, y, z = np.nonzero(np.atleast_3d(self.pattern_data[0 if self.orientation=="down" else self.data_height-1]))
@@ -255,6 +285,58 @@ def handle_close(self, event):
255285 plt .close (self .fig )
256286 self .working = False
257287
288+ def eventFilter (self , source , event ):
289+ if event .type () == QEvent .Leave : # 检测鼠标离开窗口
290+ #print("Mouse has left the window!")
291+ self .on_mouse_leave ()
292+ return super ().eventFilter (source , event )
293+
294+ def on_mouse_leave (self ):
295+ # 处理鼠标离开窗口时的逻辑
296+ self .target_xlim = self .defalt_xlim
297+ self .target_ylim = self .defalt_ylim
298+ self .target_zlim = self .defalt_zlim
299+
300+ def on_mouse_move (self , event ):
301+ # 检查鼠标是否在绘图区域内
302+ if event .inaxes :
303+ # 检查鼠标是否在 elev_slider 上
304+ if self .elev_slider .ax .contains (event )[0 ] or self .azim_slider .ax .contains (event )[0 ]:
305+ self .target_xlim = (self .defalt_xlim [0 ]* 1.5 , self .defalt_xlim [1 ]* 1.5 )
306+ self .target_ylim = (self .defalt_ylim [0 ]* 1.5 , self .defalt_ylim [1 ]* 1.5 )
307+ if self .mouse_pressing :
308+ self .mouse_controling_slider = True
309+ # 检查鼠标是否在主体范围内
310+ elif (abs (event .xdata )< 0.06 ) and (abs (event .ydata )< 0.08 ):
311+ self .target_xlim = (self .defalt_xlim [0 ]* 0.6 , self .defalt_xlim [1 ]* 0.6 )
312+ self .target_ylim = (self .defalt_ylim [0 ]* 0.6 , self .defalt_ylim [1 ]* 0.6 )
313+ self .target_zlim = (self .defalt_zlim [0 ] * 0.6 , self .defalt_zlim [1 ] * 0.6 )
314+ else :
315+ self .target_xlim = self .defalt_xlim
316+ self .target_ylim = self .defalt_ylim
317+ self .target_zlim = self .defalt_zlim
318+ else :
319+ self .target_xlim = self .defalt_xlim
320+ self .target_ylim = self .defalt_ylim
321+ self .target_zlim = self .defalt_zlim
322+
323+ def on_mouse_click (self , event ):
324+ if event .dblclick :
325+ self ._change_theme ()
326+ print (f"Double-click detected at { event .x } , { event .y } " )
327+ else :
328+ print (f"Single click at { event .x } , { event .y } " )
329+
330+ def on_mouse_press (self , event ):
331+ self .mouse_pressing = True
332+
333+ def on_mouse_release (self , event ):
334+ self .mouse_pressing = False
335+ if self .mouse_controling_slider :
336+ self .target_xlim = self .defalt_xlim
337+ self .target_ylim = self .defalt_ylim
338+ self .mouse_controling_slider = False
339+
258340 def update_elev (self , val ):
259341 self .target_elev = val
260342
@@ -264,12 +346,6 @@ def update_azim(self, val):
264346 def update_view_angle (self ):
265347 self .ax .view_init (elev = self .elev , azim = self .azim_angle )
266348
267- def _update_axis_limits (self , positions ):
268- min_x , max_x = min (positions , key = lambda pos : pos [0 ])[0 ], max (positions , key = lambda pos : pos [0 ])[0 ]
269- min_y , max_y = min (positions , key = lambda pos : pos [1 ])[1 ], max (positions , key = lambda pos : pos [1 ])[1 ]
270- self .ax_xlim_min , self .ax_xlim_max = min_x , max_x
271- self .ax_ylim_min , self .ax_ylim_max = min_y , max_y
272-
273349 def _update_piano_keys (self , bit_array , volumes ):
274350 for i , key in enumerate (self .piano_keys ):
275351 if bit_array [i ]:
@@ -294,6 +370,14 @@ def _hide_axes(self):
294370 axis .line .set_visible (False ) # 隐藏坐标轴线
295371 axis .set_ticks ([]) # 隐藏刻度线
296372
373+ def _change_theme (self ):
374+ self .theme_index = (self .theme_index + 1 ) % len (self .fig_themes_rgba ) # 循环到下一个颜色
375+ self .fig .set_facecolor (self .fig_themes_rgba [self .theme_index ]) # 设置新的 facecolor
376+ self .ax .set_facecolor (self .fig_themes_rgba [self .theme_index ])
377+ # 更新数据点颜色
378+ self .data_color = self .data_themes_rgb [self .theme_index ] # 更新数据颜色
379+
380+
297381@njit
298382def add_pattern (bit_array , volumes , average_volume , position_list , final_volume , final_volume_index , scaler , thickness_list , pattern_data , pattern_data_thickness , orientation ):
299383 variances = []
@@ -407,7 +491,7 @@ def map_note_to_range(note):
407491 zero_pattern_interval = 2
408492 update_count = 0
409493 process_midi_thread_bool = True
410-
494+
411495 def process_midi ():
412496 nonlocal new_pattern , update_count , volumes , process_midi_thread_bool
413497 for msg in midi_iterator :
@@ -462,7 +546,7 @@ def choose_midi_file(app):
462546 # 设置全局样式表
463547 app .setStyleSheet ("""
464548 QFileDialog {
465- background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 #ffffff, stop:1 #e0e0e0) ;
549+ background-color: #ffffff;
466550 color: #000000;
467551 border-radius: 15px;
468552 }
@@ -501,6 +585,15 @@ def choose_midi_file(app):
501585 midi_file_path = None
502586 return midi_file_path
503587
588+ class RoundedProgressDialog (QProgressDialog ):
589+ def paintEvent (self , event ):
590+ painter = QtGui .QPainter (self )
591+ painter .setRenderHint (QtGui .QPainter .Antialiasing ) # 开启抗锯齿
592+ rounded_rect = QtCore .QRectF (self .rect ()).adjusted (1 , 1 , - 1 , - 1 )
593+ painter .setBrush (QtGui .QBrush (QtGui .QColor ("#f0f0f0" ))) # 设置背景颜色
594+ painter .setPen (QtCore .Qt .NoPen ) # 无边框线
595+ painter .drawRoundedRect (rounded_rect , 15 , 15 ) # 圆角大小为 15px
596+
504597
505598if __name__ == "__main__" :
506599 pygame .init ()
@@ -509,24 +602,21 @@ def choose_midi_file(app):
509602 visualizer = None
510603 base_path = os_path .dirname (os_path .abspath (__file__ ))
511604 PATH_TO_ICON = os_path .join (base_path , "icon.png" )
605+
512606 while True :
513607 midi_file_path = choose_midi_file (app ) # 传递 app 实例
514608
515609 if midi_file_path :
516610 # 使用 QProgressDialog 作为加载提示框
517611 if not visualizer :
518- loading_msg = QProgressDialog ("正在预编译糟糕的函数..." , None , 0 , 0 ) # 创建进度对话框
612+ loading_msg = RoundedProgressDialog ("正在预编译糟糕的函数..." , None , 0 , 0 ) # 使用自定义的带圆角的进度对话框
519613 loading_msg .setWindowTitle ("Musical Bubble Column!" )
520614 loading_msg .setCancelButton (None ) # 不显示取消按钮
521- loading_msg .setWindowFlags (QtCore .Qt .FramelessWindowHint | QtCore .Qt .WindowStaysOnTopHint ) # 设置窗口置顶
615+ loading_msg .setWindowFlags (QtCore .Qt .FramelessWindowHint | QtCore .Qt .WindowStaysOnTopHint ) # 设置无边框和置顶
616+ loading_msg .setAttribute (QtCore .Qt .WA_TranslucentBackground ) # 允许背景透明
522617 loading_msg .setMinimumSize (600 , 100 ) # 设置最小大小
618+
523619 loading_msg .setStyleSheet ("""
524- QProgressDialog {
525- background-color: #f0f0f0; /* 背景颜色 */
526- color: #333; /* 字体颜色 */
527- font-size: 32px; /* 字体大小 */
528- font-family: 'Arial'; /* 字体类型 */
529- }
530620 QProgressBar {
531621 text-align: center; /* 文本居中 */
532622 background-color: #e0e0e0; /* 进度条背景颜色 */
@@ -540,11 +630,12 @@ def choose_midi_file(app):
540630 """ )
541631 loading_msg .setWindowIcon (QtGui .QIcon (PATH_TO_ICON )) # 使用相对路径设置图标
542632 loading_msg .show () # 显示提示框
543-
544- # 设置提示框位置在屏幕下方
633+
545634 screen_geometry = app .primaryScreen ().geometry ()
546- loading_msg .move (screen_geometry .x () + (screen_geometry .width () - loading_msg .width ()) // 2 , (screen_geometry .y () + screen_geometry .height ()) // 8 ) # 调整位置
547-
635+ loading_msg .move (
636+ screen_geometry .x () + (screen_geometry .width () - loading_msg .width ()) // 2 ,
637+ (screen_geometry .y () + screen_geometry .height ()) // 8
638+ )
548639 QApplication .processEvents ()
549640
550641 if not visualizer :
0 commit comments