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.
@@ -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()