Skip to content

Commit e819b2e

Browse files
authored
Update musicalbubblecolumn.py
优化交互 鼠标双击,主题色切换功能
1 parent 2f15655 commit e819b2e

File tree

1 file changed

+125
-34
lines changed

1 file changed

+125
-34
lines changed

script/musicalbubblecolumn.py

Lines changed: 125 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,21 @@
1818
#from scipy.spatial import cKDTree
1919
from PyQt5 import QtCore
2020
from 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
298382
def 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

505598
if __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

Comments
 (0)