messageflight 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- message_flight/__init__.py +0 -0
- message_flight/autostart.py +41 -0
- message_flight/config.py +457 -0
- message_flight/demo_notifications.py +12 -0
- message_flight/flight_widget.py +470 -0
- message_flight/i18n.py +370 -0
- message_flight/notification_queue.py +72 -0
- message_flight/notification_worker.py +98 -0
- message_flight/plane_banner.py +359 -0
- message_flight/plane_presets/__init__.py +26 -0
- message_flight/plane_presets/airplane.py +103 -0
- message_flight/plane_presets/base.py +35 -0
- message_flight/plane_presets/bird.py +87 -0
- message_flight/plane_presets/rocket.py +94 -0
- message_flight/plane_presets/ufo.py +73 -0
- message_flight/preset_editor.py +345 -0
- message_flight/settings_dialog.py +266 -0
- message_flight/tray_app.py +280 -0
- message_flight/tts.py +298 -0
- message_flight/tts_manager.py +98 -0
- messageflight-0.2.4.dist-info/METADATA +94 -0
- messageflight-0.2.4.dist-info/RECORD +24 -0
- messageflight-0.2.4.dist-info/WHEEL +4 -0
- messageflight-0.2.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Custom QWidget that draws a plane with a notification banner."""
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from PyQt6.QtCore import QRect, Qt, QTimer, pyqtProperty # type: ignore[attr-defined]
|
|
5
|
+
from PyQt6.QtGui import QColor, QFont, QFontMetrics, QPainter, QPainterPath, QPixmap
|
|
6
|
+
from PyQt6.QtWidgets import QWidget
|
|
7
|
+
|
|
8
|
+
from message_flight.plane_presets import get_preset
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlaneBanner(QWidget):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
parent=None,
|
|
15
|
+
*,
|
|
16
|
+
plane_color: str = "#FF69B4",
|
|
17
|
+
wing_color: str = "#FF1493",
|
|
18
|
+
accent_color: str = "#FFFFFF",
|
|
19
|
+
decor_color: str = "#FF69B4",
|
|
20
|
+
banner_color: str = "#FFB6C1",
|
|
21
|
+
text_color: str = "#FFFFFF",
|
|
22
|
+
thruster_outer_color: str = "#FFA500",
|
|
23
|
+
thruster_middle_color: str = "#FF4500",
|
|
24
|
+
thruster_inner_color: str = "#FFFF00",
|
|
25
|
+
):
|
|
26
|
+
super().__init__(parent)
|
|
27
|
+
self._banner_width = 280
|
|
28
|
+
self._banner_height = 50
|
|
29
|
+
self._text = ""
|
|
30
|
+
self._text_color = QColor(text_color)
|
|
31
|
+
self._preset = get_preset("airplane")
|
|
32
|
+
self._params = self._preset.get_default_params()
|
|
33
|
+
# Apply any color overrides from constructor
|
|
34
|
+
for attr_name in (
|
|
35
|
+
"plane_color", "wing_color", "accent_color", "decor_color",
|
|
36
|
+
"banner_color", "thruster_outer_color", "thruster_middle_color",
|
|
37
|
+
"thruster_inner_color",
|
|
38
|
+
):
|
|
39
|
+
val = locals()[attr_name]
|
|
40
|
+
if hasattr(self._params, attr_name):
|
|
41
|
+
setattr(self._params, attr_name, val)
|
|
42
|
+
self._plane_offset = 0.0
|
|
43
|
+
self._facing_direction = 1 # 1 = 朝右, -1 = 朝左
|
|
44
|
+
self.setFixedSize(self._banner_width + 80, 80)
|
|
45
|
+
|
|
46
|
+
# 交互状态
|
|
47
|
+
self._click_feedback_text = ""
|
|
48
|
+
self._click_feedback_timer = None
|
|
49
|
+
self._dragging = False
|
|
50
|
+
self._drag_start_pos = None
|
|
51
|
+
self._drag_start_global = None
|
|
52
|
+
|
|
53
|
+
def _ensure_feedback_timer(self):
|
|
54
|
+
if self._click_feedback_timer is None:
|
|
55
|
+
try:
|
|
56
|
+
self._click_feedback_timer = QTimer(self)
|
|
57
|
+
self._click_feedback_timer.setSingleShot(True)
|
|
58
|
+
self._click_feedback_timer.timeout.connect(self._hide_click_feedback)
|
|
59
|
+
except RuntimeError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def _hide_click_feedback(self):
|
|
63
|
+
self._click_feedback_text = ""
|
|
64
|
+
self.update()
|
|
65
|
+
|
|
66
|
+
def mousePressEvent(self, event):
|
|
67
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
68
|
+
self._dragging = True
|
|
69
|
+
self._drag_start_pos = event.pos()
|
|
70
|
+
self._drag_start_global = event.globalPosition().toPoint()
|
|
71
|
+
# 显示点击反馈
|
|
72
|
+
self._click_feedback_text = "✈️ 收到!"
|
|
73
|
+
self._ensure_feedback_timer()
|
|
74
|
+
if self._click_feedback_timer is not None:
|
|
75
|
+
self._click_feedback_timer.start(1500)
|
|
76
|
+
self.update()
|
|
77
|
+
# 通知父窗口暂停飞行动画
|
|
78
|
+
parent = self.parent()
|
|
79
|
+
if parent and hasattr(parent, "set_paused"):
|
|
80
|
+
parent.set_paused(True)
|
|
81
|
+
event.accept()
|
|
82
|
+
|
|
83
|
+
def mouseMoveEvent(self, event):
|
|
84
|
+
if self._dragging and self.parent():
|
|
85
|
+
delta = event.globalPosition().toPoint() - self._drag_start_global
|
|
86
|
+
new_pos = self.pos() + delta
|
|
87
|
+
# 限制在屏幕范围内
|
|
88
|
+
new_pos.setX(max(-self.width() + 50, min(new_pos.x(), self.parent().width() - 50)))
|
|
89
|
+
new_pos.setY(max(-self.height() + 50, min(new_pos.y(), self.parent().height() - 50)))
|
|
90
|
+
self.move(new_pos)
|
|
91
|
+
self._drag_start_global = event.globalPosition().toPoint()
|
|
92
|
+
event.accept()
|
|
93
|
+
|
|
94
|
+
def mouseReleaseEvent(self, event):
|
|
95
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
96
|
+
self._dragging = False
|
|
97
|
+
self._drag_start_pos = None
|
|
98
|
+
self._drag_start_global = None
|
|
99
|
+
# 通知父窗口恢复飞行动画
|
|
100
|
+
parent = self.parent()
|
|
101
|
+
if parent and hasattr(parent, "set_paused"):
|
|
102
|
+
parent.set_paused(False)
|
|
103
|
+
event.accept()
|
|
104
|
+
|
|
105
|
+
def is_dragging(self) -> bool:
|
|
106
|
+
return self._dragging
|
|
107
|
+
|
|
108
|
+
def _generate_plane_cache(self) -> None:
|
|
109
|
+
"""Render the plane preset to an off-screen pixmap for fast blitting."""
|
|
110
|
+
size = 100
|
|
111
|
+
self._plane_cache = QPixmap(size, size)
|
|
112
|
+
self._plane_cache.fill(Qt.GlobalColor.transparent)
|
|
113
|
+
painter = QPainter(self._plane_cache)
|
|
114
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
115
|
+
# Translate by (20,20) so negative coordinates (e.g. bird wings) don't get clipped
|
|
116
|
+
painter.translate(20, 20)
|
|
117
|
+
rotation = getattr(self._params, 'rotation', 0.0)
|
|
118
|
+
if rotation != 0.0:
|
|
119
|
+
painter.translate(35, 40)
|
|
120
|
+
painter.rotate(rotation)
|
|
121
|
+
painter.translate(-35, -40)
|
|
122
|
+
self._preset.draw(painter, self._params)
|
|
123
|
+
painter.end()
|
|
124
|
+
|
|
125
|
+
def _recalculate_size(self) -> None:
|
|
126
|
+
"""Recalculate widget size based on banner width and mount point offset."""
|
|
127
|
+
attach_x = getattr(self._params, 'banner_attach_x', 0)
|
|
128
|
+
extra = max(0, -2 * attach_x, attach_x - 80)
|
|
129
|
+
self.setFixedSize(self._banner_width + 100 + extra, 100)
|
|
130
|
+
|
|
131
|
+
def set_text(self, text: str):
|
|
132
|
+
self._text = text
|
|
133
|
+
fm = QFontMetrics(QFont("Microsoft YaHei", 12, QFont.Weight.Bold))
|
|
134
|
+
tw = fm.horizontalAdvance(text) + 40
|
|
135
|
+
self._banner_width = max(200, tw)
|
|
136
|
+
self._recalculate_size()
|
|
137
|
+
self.update()
|
|
138
|
+
|
|
139
|
+
def _plane_rect(self) -> QRect:
|
|
140
|
+
"""Return the bounding rect of the plane area (approximate)."""
|
|
141
|
+
float_y = int(self._plane_offset * 6)
|
|
142
|
+
attach_x = getattr(self._params, 'banner_attach_x', 0)
|
|
143
|
+
extra_left = max(0, -attach_x)
|
|
144
|
+
if self._facing_direction == 1:
|
|
145
|
+
plane_x = extra_left + self._banner_width + 10
|
|
146
|
+
else:
|
|
147
|
+
plane_x = extra_left
|
|
148
|
+
return QRect(plane_x, 35 + float_y, 100, 100)
|
|
149
|
+
|
|
150
|
+
def _banner_rect(self) -> QRect:
|
|
151
|
+
"""Return the bounding rect of the banner area."""
|
|
152
|
+
float_y = int(self._plane_offset * 6)
|
|
153
|
+
attach_x = getattr(self._params, 'banner_attach_x', 0)
|
|
154
|
+
extra_left = max(0, -attach_x)
|
|
155
|
+
if self._facing_direction == 1:
|
|
156
|
+
plane_x = extra_left + self._banner_width + 10
|
|
157
|
+
mount_x = plane_x + attach_x
|
|
158
|
+
bx = mount_x - self._banner_width - 10
|
|
159
|
+
else:
|
|
160
|
+
plane_x = extra_left
|
|
161
|
+
mount_x = plane_x + 100 - attach_x
|
|
162
|
+
bx = mount_x + 10
|
|
163
|
+
by = 35 + float_y + getattr(self._params, 'banner_attach_y', 35) - self._banner_height // 2
|
|
164
|
+
return QRect(bx, by, self._banner_width + 20, self._banner_height + 20)
|
|
165
|
+
|
|
166
|
+
def set_facing_direction(self, direction: int) -> None:
|
|
167
|
+
"""Set the facing direction (1 = right, -1 = left) and trigger repaint."""
|
|
168
|
+
self._facing_direction = direction
|
|
169
|
+
self.update()
|
|
170
|
+
|
|
171
|
+
def _get_color(self, name: str) -> QColor:
|
|
172
|
+
"""Return a QColor for the given attribute name from _params."""
|
|
173
|
+
value = getattr(self._params, name, "#FFFFFF")
|
|
174
|
+
return QColor(value)
|
|
175
|
+
|
|
176
|
+
def get_plane_offset(self):
|
|
177
|
+
return self._plane_offset
|
|
178
|
+
|
|
179
|
+
def set_plane_offset(self, val: float):
|
|
180
|
+
old_rect = self._plane_rect()
|
|
181
|
+
self._plane_offset = val
|
|
182
|
+
new_rect = self._plane_rect()
|
|
183
|
+
self.update(old_rect.united(new_rect))
|
|
184
|
+
|
|
185
|
+
plane_offset = pyqtProperty(float, get_plane_offset, set_plane_offset)
|
|
186
|
+
|
|
187
|
+
def update_colors(
|
|
188
|
+
self,
|
|
189
|
+
*,
|
|
190
|
+
plane_color: Optional[str] = None,
|
|
191
|
+
wing_color: Optional[str] = None,
|
|
192
|
+
accent_color: Optional[str] = None,
|
|
193
|
+
decor_color: Optional[str] = None,
|
|
194
|
+
banner_color: Optional[str] = None,
|
|
195
|
+
text_color: Optional[str] = None,
|
|
196
|
+
thruster_outer_color: Optional[str] = None,
|
|
197
|
+
thruster_middle_color: Optional[str] = None,
|
|
198
|
+
thruster_inner_color: Optional[str] = None,
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Replace any of the 9 color attributes and request a repaint.
|
|
201
|
+
|
|
202
|
+
All arguments are keyword-only to match the ``__init__`` style.
|
|
203
|
+
A ``None`` argument leaves the corresponding color unchanged, which
|
|
204
|
+
lets callers forward ``**cfg.colors`` without worrying about missing
|
|
205
|
+
keys. A single ``update()`` is issued at the end so the repaint is
|
|
206
|
+
coalesced.
|
|
207
|
+
"""
|
|
208
|
+
params_mapping = {
|
|
209
|
+
"plane_color": plane_color,
|
|
210
|
+
"wing_color": wing_color,
|
|
211
|
+
"accent_color": accent_color,
|
|
212
|
+
"decor_color": decor_color,
|
|
213
|
+
"banner_color": banner_color,
|
|
214
|
+
"thruster_outer_color": thruster_outer_color,
|
|
215
|
+
"thruster_middle_color": thruster_middle_color,
|
|
216
|
+
"thruster_inner_color": thruster_inner_color,
|
|
217
|
+
}
|
|
218
|
+
for attr, value in params_mapping.items():
|
|
219
|
+
if value is not None and hasattr(self._params, attr):
|
|
220
|
+
setattr(self._params, attr, value)
|
|
221
|
+
if text_color is not None:
|
|
222
|
+
self._text_color = QColor(text_color)
|
|
223
|
+
if hasattr(self._params, "text_color"):
|
|
224
|
+
self._params.text_color = text_color
|
|
225
|
+
self.__dict__.pop("_plane_cache", None)
|
|
226
|
+
self.update()
|
|
227
|
+
|
|
228
|
+
def apply_preset(self, preset, params) -> None:
|
|
229
|
+
"""Replace the active preset and its params, then request a repaint."""
|
|
230
|
+
self._preset = preset
|
|
231
|
+
self._params = params
|
|
232
|
+
# sync banner/text colors if the new params carry them
|
|
233
|
+
if hasattr(params, "text_color"):
|
|
234
|
+
self._text_color = QColor(params.text_color)
|
|
235
|
+
self._recalculate_size()
|
|
236
|
+
self.__dict__.pop("_plane_cache", None)
|
|
237
|
+
self.update()
|
|
238
|
+
|
|
239
|
+
def paintEvent(self, event):
|
|
240
|
+
painter = QPainter(self)
|
|
241
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
242
|
+
float_y = int(self._plane_offset * 6)
|
|
243
|
+
|
|
244
|
+
attach_x = getattr(self._params, 'banner_attach_x', 0)
|
|
245
|
+
attach_y = getattr(self._params, 'banner_attach_y', 35)
|
|
246
|
+
extra_left = max(0, -attach_x)
|
|
247
|
+
|
|
248
|
+
if self._facing_direction == 1:
|
|
249
|
+
# 左→右:飞船在右,横幅在左
|
|
250
|
+
plane_x = extra_left + self._banner_width + 10
|
|
251
|
+
plane_y = 35 + float_y
|
|
252
|
+
|
|
253
|
+
mount_x = plane_x + attach_x
|
|
254
|
+
mount_y = plane_y + attach_y
|
|
255
|
+
|
|
256
|
+
bx = mount_x - self._banner_width - 10
|
|
257
|
+
by = mount_y - self._banner_height // 2
|
|
258
|
+
|
|
259
|
+
self._draw_banner(painter, bx, by, tail_on_right=True)
|
|
260
|
+
self._draw_plane(painter, plane_x, plane_y, facing=1)
|
|
261
|
+
else:
|
|
262
|
+
# 右→左:飞船在左,横幅在右
|
|
263
|
+
plane_x = extra_left
|
|
264
|
+
plane_y = 35 + float_y
|
|
265
|
+
|
|
266
|
+
# facing=-1 时 _draw_plane 内部进行了 scale(-1,1) + translate(-100,0)
|
|
267
|
+
# 局部坐标 (x,y) 在世界坐标中 = (plane_x + 100 - x, plane_y + y)
|
|
268
|
+
mount_x = plane_x + 100 - attach_x
|
|
269
|
+
mount_y = plane_y + attach_y
|
|
270
|
+
|
|
271
|
+
bx = mount_x + 10
|
|
272
|
+
by = mount_y - self._banner_height // 2
|
|
273
|
+
|
|
274
|
+
self._draw_banner(painter, bx, by, tail_on_right=False)
|
|
275
|
+
self._draw_plane(painter, plane_x, plane_y, facing=-1)
|
|
276
|
+
|
|
277
|
+
# 绘制点击反馈气泡
|
|
278
|
+
if self._click_feedback_text:
|
|
279
|
+
self._draw_click_feedback(painter)
|
|
280
|
+
|
|
281
|
+
painter.end()
|
|
282
|
+
|
|
283
|
+
def _draw_click_feedback(self, painter: QPainter):
|
|
284
|
+
"""Draw a temporary click feedback bubble above the plane."""
|
|
285
|
+
fm = QFontMetrics(QFont("Microsoft YaHei", 10, QFont.Weight.Bold))
|
|
286
|
+
text_w = fm.horizontalAdvance(self._click_feedback_text) + 16
|
|
287
|
+
text_h = fm.height() + 8
|
|
288
|
+
bubble_x = (self.width() - text_w) // 2
|
|
289
|
+
bubble_y = 5
|
|
290
|
+
|
|
291
|
+
path = QPainterPath()
|
|
292
|
+
path.addRoundedRect(bubble_x, bubble_y, text_w, text_h, 8, 8)
|
|
293
|
+
painter.fillPath(path, QColor(0, 0, 0, 180))
|
|
294
|
+
|
|
295
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
296
|
+
font = QFont("Microsoft YaHei", 10, QFont.Weight.Bold)
|
|
297
|
+
painter.setFont(font)
|
|
298
|
+
painter.drawText(bubble_x, bubble_y, text_w, text_h, Qt.AlignmentFlag.AlignCenter, self._click_feedback_text)
|
|
299
|
+
|
|
300
|
+
def _draw_banner(self, painter: QPainter, bx: int, by: int, tail_on_right: bool):
|
|
301
|
+
"""Draw the notification banner at (bx, by).
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
tail_on_right: True = tail on right edge (plane on right);
|
|
305
|
+
False = tail on left edge (plane on left).
|
|
306
|
+
"""
|
|
307
|
+
path = QPainterPath()
|
|
308
|
+
rect_w = self._banner_width
|
|
309
|
+
rect_h = self._banner_height
|
|
310
|
+
radius = 12
|
|
311
|
+
|
|
312
|
+
path.moveTo(bx + radius, by)
|
|
313
|
+
path.lineTo(bx + rect_w - radius, by)
|
|
314
|
+
path.arcTo(bx + rect_w - radius * 2, by, radius * 2, radius * 2, 90, -90)
|
|
315
|
+
path.lineTo(bx + rect_w, by + rect_h - radius)
|
|
316
|
+
path.arcTo(bx + rect_w - radius * 2, by + rect_h - radius * 2, radius * 2, radius * 2, 0, -90)
|
|
317
|
+
path.lineTo(bx + radius, by + rect_h)
|
|
318
|
+
path.arcTo(bx, by + rect_h - radius * 2, radius * 2, radius * 2, -90, -90)
|
|
319
|
+
path.lineTo(bx, by + radius)
|
|
320
|
+
path.arcTo(bx, by, radius * 2, radius * 2, 180, -90)
|
|
321
|
+
path.closeSubpath()
|
|
322
|
+
|
|
323
|
+
tail_y = by + rect_h // 2
|
|
324
|
+
if tail_on_right:
|
|
325
|
+
tail_x = bx + rect_w
|
|
326
|
+
path.moveTo(tail_x, tail_y - 6)
|
|
327
|
+
path.lineTo(tail_x + 10, tail_y)
|
|
328
|
+
path.lineTo(tail_x, tail_y + 6)
|
|
329
|
+
else:
|
|
330
|
+
tail_x = bx
|
|
331
|
+
path.moveTo(tail_x, tail_y - 6)
|
|
332
|
+
path.lineTo(tail_x - 10, tail_y)
|
|
333
|
+
path.lineTo(tail_x, tail_y + 6)
|
|
334
|
+
path.closeSubpath()
|
|
335
|
+
|
|
336
|
+
painter.fillPath(path, self._get_color("banner_color"))
|
|
337
|
+
|
|
338
|
+
painter.setPen(self._text_color)
|
|
339
|
+
font = QFont("Microsoft YaHei", 12, QFont.Weight.Bold)
|
|
340
|
+
painter.setFont(font)
|
|
341
|
+
fm = QFontMetrics(font)
|
|
342
|
+
text_y = by + (rect_h + fm.ascent() - fm.descent()) // 2
|
|
343
|
+
painter.drawText(bx + 20, text_y, self._text)
|
|
344
|
+
|
|
345
|
+
def _draw_plane(self, painter: QPainter, px: int, py: int, facing: int):
|
|
346
|
+
"""Draw the plane at (px, py) using cached pixmap.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
facing: 1 = head points right; -1 = head points left.
|
|
350
|
+
"""
|
|
351
|
+
if not hasattr(self, '_plane_cache') or self._plane_cache.isNull():
|
|
352
|
+
self._generate_plane_cache()
|
|
353
|
+
painter.save()
|
|
354
|
+
painter.translate(px, py)
|
|
355
|
+
if facing == -1:
|
|
356
|
+
painter.scale(-1, 1)
|
|
357
|
+
painter.translate(-100, 0)
|
|
358
|
+
painter.drawPixmap(0, 0, self._plane_cache)
|
|
359
|
+
painter.restore()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Type
|
|
4
|
+
|
|
5
|
+
from .airplane import AirplanePreset
|
|
6
|
+
from .base import PlanePreset
|
|
7
|
+
from .bird import BirdPreset
|
|
8
|
+
from .rocket import RocketPreset
|
|
9
|
+
from .ufo import UFOPreset
|
|
10
|
+
|
|
11
|
+
PRESETS: dict[str, Type[PlanePreset]] = {
|
|
12
|
+
"airplane": AirplanePreset,
|
|
13
|
+
"rocket": RocketPreset,
|
|
14
|
+
"ufo": UFOPreset,
|
|
15
|
+
"bird": BirdPreset,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_preset(key: str) -> PlanePreset:
|
|
20
|
+
if key not in PRESETS:
|
|
21
|
+
key = "airplane"
|
|
22
|
+
return PRESETS[key]()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_presets() -> list[tuple[str, str, str]]:
|
|
26
|
+
return [(k, p.name, p.icon) for k, p in PRESETS.items()]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from PyQt6.QtCore import Qt
|
|
6
|
+
from PyQt6.QtGui import QColor, QPainter, QPainterPath
|
|
7
|
+
|
|
8
|
+
from .base import ParamDef, PlanePreset
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class AirplaneParameters:
|
|
13
|
+
plane_color: str = "#FF69B4"
|
|
14
|
+
wing_color: str = "#FF1493"
|
|
15
|
+
accent_color: str = "#FFFFFF"
|
|
16
|
+
decor_color: str = "#FF69B4"
|
|
17
|
+
banner_color: str = "#FFB6C1"
|
|
18
|
+
thruster_outer_color: str = "#FFA500"
|
|
19
|
+
thruster_middle_color: str = "#FF4500"
|
|
20
|
+
thruster_inner_color: str = "#FFFF00"
|
|
21
|
+
body_scale: float = 1.0
|
|
22
|
+
text_color: str = "#FFFFFF"
|
|
23
|
+
rotation: float = 0.0
|
|
24
|
+
banner_attach_x: int = 0
|
|
25
|
+
banner_attach_y: int = 30
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AirplanePreset(PlanePreset):
|
|
29
|
+
name = "飞机"
|
|
30
|
+
icon = "✈️"
|
|
31
|
+
|
|
32
|
+
def draw(self, painter: QPainter, params: AirplaneParameters) -> None:
|
|
33
|
+
s = params.body_scale
|
|
34
|
+
painter.save()
|
|
35
|
+
self._draw_thruster(painter, params, s)
|
|
36
|
+
self._draw_fuselage(painter, params, s)
|
|
37
|
+
self._draw_wings(painter, params, s)
|
|
38
|
+
painter.restore()
|
|
39
|
+
|
|
40
|
+
def get_parameters(self) -> list[ParamDef]:
|
|
41
|
+
return [
|
|
42
|
+
ParamDef("plane_color", "飞机主体", "color", "#FF69B4"),
|
|
43
|
+
ParamDef("wing_color", "机翼", "color", "#FF1493"),
|
|
44
|
+
ParamDef("accent_color", "眼睛/高光", "color", "#FFFFFF"),
|
|
45
|
+
ParamDef("decor_color", "小圆装饰", "color", "#FF69B4"),
|
|
46
|
+
ParamDef("banner_color", "横幅装饰", "color", "#FFB6C1"),
|
|
47
|
+
ParamDef("thruster_outer_color", "推进器外焰", "color", "#FFA500"),
|
|
48
|
+
ParamDef("thruster_middle_color", "推进器中焰", "color", "#FF4500"),
|
|
49
|
+
ParamDef("thruster_inner_color", "推进器内焰", "color", "#FFFF00"),
|
|
50
|
+
ParamDef("body_scale", "机身缩放", "float", 1.0, 0.5, 2.0, 0.1),
|
|
51
|
+
ParamDef("rotation", "旋转角度", "float", 0.0, -45.0, 45.0, 1.0),
|
|
52
|
+
ParamDef("banner_attach_x", "横幅挂载X", "int", 0, -50, 100),
|
|
53
|
+
ParamDef("banner_attach_y", "横幅挂载Y", "int", 30, -50, 100),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def get_default_params(self) -> AirplaneParameters:
|
|
57
|
+
return AirplaneParameters()
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _draw_fuselage(painter, params, s):
|
|
61
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
62
|
+
painter.setBrush(QColor(params.plane_color))
|
|
63
|
+
painter.drawEllipse(int(10*s), int(18*s), int(45*s), int(22*s))
|
|
64
|
+
painter.drawEllipse(int(48*s), int(19*s), int(14*s), int(20*s))
|
|
65
|
+
painter.setBrush(QColor(params.accent_color))
|
|
66
|
+
painter.drawEllipse(int(52*s), int(24*s), int(6*s), int(6*s))
|
|
67
|
+
painter.drawEllipse(int(38*s), int(24*s), int(5*s), int(5*s))
|
|
68
|
+
painter.setBrush(QColor(params.decor_color))
|
|
69
|
+
painter.drawEllipse(int(60*s), int(26*s), int(4*s), int(6*s))
|
|
70
|
+
painter.setBrush(QColor(params.banner_color))
|
|
71
|
+
painter.drawEllipse(int(56*s), int(22*s), int(12*s), int(3*s))
|
|
72
|
+
painter.drawEllipse(int(56*s), int(33*s), int(12*s), int(3*s))
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _draw_wings(painter, params, s):
|
|
76
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
77
|
+
painter.setBrush(QColor(params.wing_color))
|
|
78
|
+
wing_path = QPainterPath()
|
|
79
|
+
wing_path.moveTo(int(25*s), int(25*s))
|
|
80
|
+
wing_path.lineTo(int(15*s), int(8*s))
|
|
81
|
+
wing_path.lineTo(int(35*s), int(8*s))
|
|
82
|
+
wing_path.lineTo(int(40*s), int(25*s))
|
|
83
|
+
wing_path.closeSubpath()
|
|
84
|
+
painter.drawPath(wing_path)
|
|
85
|
+
tail_path = QPainterPath()
|
|
86
|
+
tail_path.moveTo(int(12*s), int(28*s))
|
|
87
|
+
tail_path.lineTo(int(2*s), int(18*s))
|
|
88
|
+
tail_path.lineTo(int(12*s), int(22*s))
|
|
89
|
+
tail_path.closeSubpath()
|
|
90
|
+
painter.drawPath(tail_path)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _draw_thruster(painter, params, s, intensity=1.0):
|
|
94
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
95
|
+
outer_w = int(14 * intensity * s)
|
|
96
|
+
painter.setBrush(QColor(params.thruster_outer_color))
|
|
97
|
+
painter.drawEllipse(int(5*s), int(25*s), outer_w, int(10*s))
|
|
98
|
+
mid_w = int(10 * intensity * s)
|
|
99
|
+
painter.setBrush(QColor(params.thruster_middle_color))
|
|
100
|
+
painter.drawEllipse(int(5*s), int(26*s), mid_w, int(7*s))
|
|
101
|
+
inner_w = int(5 * intensity * s)
|
|
102
|
+
painter.setBrush(QColor(params.thruster_inner_color))
|
|
103
|
+
painter.drawEllipse(int(5*s), int(27*s), inner_w, int(4*s))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtGui import QPainter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ParamDef:
|
|
12
|
+
name: str
|
|
13
|
+
label: str
|
|
14
|
+
type: str
|
|
15
|
+
default: Any
|
|
16
|
+
min: Optional[Any] = None
|
|
17
|
+
max: Optional[Any] = None
|
|
18
|
+
step: Optional[Any] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PlanePreset(ABC):
|
|
22
|
+
name: str = ""
|
|
23
|
+
icon: str = ""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def draw(self, painter: QPainter, params: Any) -> None:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def get_parameters(self) -> list[ParamDef]:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def get_default_params(self) -> Any:
|
|
35
|
+
pass
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtCore import Qt
|
|
7
|
+
from PyQt6.QtGui import QColor, QPainter, QPainterPath
|
|
8
|
+
|
|
9
|
+
from .base import ParamDef, PlanePreset
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class BirdParameters:
|
|
14
|
+
body_size: int = 20
|
|
15
|
+
wing_span: int = 40
|
|
16
|
+
body_color: str = "#4169E1"
|
|
17
|
+
wing_color: str = "#1E90FF"
|
|
18
|
+
beak_color: str = "#FFA500"
|
|
19
|
+
eye_color: str = "#000000"
|
|
20
|
+
wing_flap_speed: float = 4.0
|
|
21
|
+
banner_color: str = "#87CEEB"
|
|
22
|
+
text_color: str = "#FFFFFF"
|
|
23
|
+
rotation: float = 0.0
|
|
24
|
+
banner_attach_x: int = -10
|
|
25
|
+
banner_attach_y: int = 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BirdPreset(PlanePreset):
|
|
29
|
+
name = "小鸟"
|
|
30
|
+
icon = "🐦"
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self._animation_time = 0.0
|
|
34
|
+
|
|
35
|
+
def draw(self, painter: QPainter, params: BirdParameters) -> None:
|
|
36
|
+
bs = params.body_size
|
|
37
|
+
ws = params.wing_span
|
|
38
|
+
self._animation_time += 0.016 # ~60fps assumption
|
|
39
|
+
phase = (math.sin(self._animation_time * params.wing_flap_speed) + 1.0) / 2.0
|
|
40
|
+
wing_offset = int(ws // 2 * phase)
|
|
41
|
+
painter.save()
|
|
42
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
43
|
+
painter.setBrush(QColor(params.body_color))
|
|
44
|
+
painter.drawEllipse(-bs // 2, -bs // 4, bs, bs // 2)
|
|
45
|
+
painter.drawEllipse(bs // 4, -bs // 3, bs // 2, bs // 2)
|
|
46
|
+
painter.setBrush(QColor(params.beak_color))
|
|
47
|
+
beak = QPainterPath()
|
|
48
|
+
beak.moveTo(bs * 3 // 4, -bs // 8)
|
|
49
|
+
beak.lineTo(bs + 4, 0)
|
|
50
|
+
beak.lineTo(bs * 3 // 4, bs // 8)
|
|
51
|
+
beak.closeSubpath()
|
|
52
|
+
painter.drawPath(beak)
|
|
53
|
+
painter.setBrush(QColor(params.eye_color))
|
|
54
|
+
painter.drawEllipse(bs // 2, -bs // 6, 2, 2)
|
|
55
|
+
painter.setBrush(QColor(params.wing_color))
|
|
56
|
+
wing_top = QPainterPath()
|
|
57
|
+
wing_top.moveTo(-bs // 4, -bs // 4)
|
|
58
|
+
wing_top.lineTo(-bs // 4 - wing_offset, -wing_offset)
|
|
59
|
+
wing_top.lineTo(bs // 4, -bs // 4)
|
|
60
|
+
wing_top.closeSubpath()
|
|
61
|
+
painter.drawPath(wing_top)
|
|
62
|
+
wing_bot = QPainterPath()
|
|
63
|
+
wing_bot.moveTo(-bs // 4, bs // 4)
|
|
64
|
+
wing_bot.lineTo(-bs // 4 - wing_offset, wing_offset)
|
|
65
|
+
wing_bot.lineTo(bs // 4, bs // 4)
|
|
66
|
+
wing_bot.closeSubpath()
|
|
67
|
+
painter.drawPath(wing_bot)
|
|
68
|
+
painter.restore()
|
|
69
|
+
|
|
70
|
+
def get_parameters(self) -> list[ParamDef]:
|
|
71
|
+
return [
|
|
72
|
+
ParamDef("body_size", "身体大小", "int", 20, 10, 40),
|
|
73
|
+
ParamDef("wing_span", "翼展", "int", 40, 20, 80),
|
|
74
|
+
ParamDef("body_color", "身体颜色", "color", "#4169E1"),
|
|
75
|
+
ParamDef("wing_color", "翅膀颜色", "color", "#1E90FF"),
|
|
76
|
+
ParamDef("beak_color", "鸟喙颜色", "color", "#FFA500"),
|
|
77
|
+
ParamDef("eye_color", "眼睛颜色", "color", "#000000"),
|
|
78
|
+
ParamDef("wing_flap_speed", "扇翅速度", "float", 4.0, 0.0, 10.0, 0.5),
|
|
79
|
+
ParamDef("banner_color", "横幅颜色", "color", "#87CEEB"),
|
|
80
|
+
ParamDef("rotation", "旋转角度", "float", 0.0, -45.0, 45.0, 1.0),
|
|
81
|
+
ParamDef("text_color", "文字颜色", "color", "#FFFFFF"),
|
|
82
|
+
ParamDef("banner_attach_x", "横幅挂载X", "int", -10, -50, 100),
|
|
83
|
+
ParamDef("banner_attach_y", "横幅挂载Y", "int", 0, -50, 100),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
def get_default_params(self) -> BirdParameters:
|
|
87
|
+
return BirdParameters()
|