pyterm-pixel 1.1.0__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.
- pyterm/__init__.py +3 -0
- pyterm/__main__.py +45 -0
- pyterm/alarms.py +499 -0
- pyterm/app.py +1424 -0
- pyterm/backend.py +316 -0
- pyterm/backgrounds.py +940 -0
- pyterm/bench.py +69 -0
- pyterm/boxdraw.py +242 -0
- pyterm/config.py +244 -0
- pyterm/cursors.py +464 -0
- pyterm/data/config.yaml +61 -0
- pyterm/data/fonts/README.md +2 -0
- pyterm/data/fonts/sparkle.yaml +7 -0
- pyterm/data/fonts/sparkle_0.png +0 -0
- pyterm/data/fonts/sparkle_1.png +0 -0
- pyterm/data/fonts/sparkle_2.png +0 -0
- pyterm/data/fonts/sparkle_3.png +0 -0
- pyterm/data/plugins/_template.py +35 -0
- pyterm/data/plugins/cursor_demo.py +106 -0
- pyterm/data/plugins/example_effect.py +60 -0
- pyterm/data/plugins/fireworks.py +75 -0
- pyterm/data/plugins/hud_meter.py +37 -0
- pyterm/data/plugins/party_alarm.py +56 -0
- pyterm/data/plugins/sine_scroller.py +47 -0
- pyterm/data/plugins/sixamp_support.py +75 -0
- pyterm/data/plugins/walkers.py +311 -0
- pyterm/data/shell/pyterm.bash +19 -0
- pyterm/data/shell/pyterm.ps1 +37 -0
- pyterm/data/shell/pyterm.zsh +15 -0
- pyterm/data/sounds/README.md +3 -0
- pyterm/data/themes/amber_crt.yaml +16 -0
- pyterm/data/themes/amiga.yaml +22 -0
- pyterm/data/themes/amigademo.yaml +23 -0
- pyterm/data/themes/aquarium.yaml +21 -0
- pyterm/data/themes/autumn.yaml +21 -0
- pyterm/data/themes/beach.yaml +21 -0
- pyterm/data/themes/c64.yaml +26 -0
- pyterm/data/themes/c64_40.yaml +33 -0
- pyterm/data/themes/c64_raw.yaml +33 -0
- pyterm/data/themes/c64load.yaml +23 -0
- pyterm/data/themes/christmas.yaml +22 -0
- pyterm/data/themes/computerpeople.yaml +21 -0
- pyterm/data/themes/construction.yaml +14 -0
- pyterm/data/themes/demoscene.yaml +19 -0
- pyterm/data/themes/discocity.yaml +20 -0
- pyterm/data/themes/dos.yaml +14 -0
- pyterm/data/themes/easter.yaml +22 -0
- pyterm/data/themes/emerald_mine.yaml +19 -0
- pyterm/data/themes/gameboy.yaml +27 -0
- pyterm/data/themes/garden.yaml +22 -0
- pyterm/data/themes/halloween.yaml +21 -0
- pyterm/data/themes/inferno.yaml +17 -0
- pyterm/data/themes/interlace.yaml +15 -0
- pyterm/data/themes/invaders.yaml +22 -0
- pyterm/data/themes/jumpnrun.yaml +21 -0
- pyterm/data/themes/liquid.yaml +15 -0
- pyterm/data/themes/matrix.yaml +17 -0
- pyterm/data/themes/neon.yaml +18 -0
- pyterm/data/themes/neonsigns.yaml +22 -0
- pyterm/data/themes/nes_famicom.yaml +32 -0
- pyterm/data/themes/newyear.yaml +22 -0
- pyterm/data/themes/pinball.yaml +21 -0
- pyterm/data/themes/roman.yaml +23 -0
- pyterm/data/themes/rotozoom.yaml +14 -0
- pyterm/data/themes/sakura.yaml +22 -0
- pyterm/data/themes/sandstorm.yaml +15 -0
- pyterm/data/themes/seasonal.yaml +21 -0
- pyterm/data/themes/settlers.yaml +21 -0
- pyterm/data/themes/shadebobs.yaml +15 -0
- pyterm/data/themes/spacestation.yaml +21 -0
- pyterm/data/themes/trains.yaml +21 -0
- pyterm/data/themes/vaporwave.yaml +17 -0
- pyterm/data/themes/walkers.yaml +23 -0
- pyterm/data/themes/watermirror.yaml +22 -0
- pyterm/data/themes/weather.yaml +17 -0
- pyterm/data/themes/winter.yaml +21 -0
- pyterm/data/themes/zen.yaml +13 -0
- pyterm/demo.py +306 -0
- pyterm/effects.py +917 -0
- pyterm/emoji.py +352 -0
- pyterm/fontdata.py +162 -0
- pyterm/fonts.py +1139 -0
- pyterm/images.py +289 -0
- pyterm/imgcat.py +50 -0
- pyterm/keys.py +83 -0
- pyterm/mine.py +309 -0
- pyterm/minercursor.py +334 -0
- pyterm/mqtt.py +543 -0
- pyterm/pane.py +641 -0
- pyterm/particles.py +204 -0
- pyterm/plugins.py +320 -0
- pyterm/post.py +255 -0
- pyterm/present.py +258 -0
- pyterm/recorder.py +141 -0
- pyterm/renderer.py +262 -0
- pyterm/roman.py +157 -0
- pyterm/scenes.py +1754 -0
- pyterm/scenes2.py +1265 -0
- pyterm/scroller.py +58 -0
- pyterm/seasons.py +1029 -0
- pyterm/seasons2.py +517 -0
- pyterm/shell.py +33 -0
- pyterm/signals.py +56 -0
- pyterm/sound.py +173 -0
- pyterm/splash.py +302 -0
- pyterm/studio.py +900 -0
- pyterm/themes.py +183 -0
- pyterm/vt.py +1024 -0
- pyterm/water.py +174 -0
- pyterm/weather.py +1159 -0
- pyterm/yamlio.py +40 -0
- pyterm_pixel-1.1.0.dist-info/METADATA +348 -0
- pyterm_pixel-1.1.0.dist-info/RECORD +118 -0
- pyterm_pixel-1.1.0.dist-info/WHEEL +5 -0
- pyterm_pixel-1.1.0.dist-info/entry_points.txt +2 -0
- pyterm_pixel-1.1.0.dist-info/licenses/COPYING +674 -0
- pyterm_pixel-1.1.0.dist-info/licenses/COPYING.LESSER +165 -0
- pyterm_pixel-1.1.0.dist-info/top_level.txt +1 -0
pyterm/__init__.py
ADDED
pyterm/__main__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main(argv=None):
|
|
6
|
+
from .config import build_config
|
|
7
|
+
cfg, args = build_config(argv)
|
|
8
|
+
if args.benchmark:
|
|
9
|
+
os.environ.setdefault('SDL_VIDEODRIVER', 'dummy')
|
|
10
|
+
os.environ.setdefault('SDL_AUDIODRIVER', 'dummy')
|
|
11
|
+
from .bench import run_benchmark
|
|
12
|
+
return run_benchmark(cfg, args)
|
|
13
|
+
if args.screenshot:
|
|
14
|
+
os.environ.setdefault('SDL_AUDIODRIVER', 'dummy')
|
|
15
|
+
import pygame # noqa
|
|
16
|
+
if args.list:
|
|
17
|
+
pygame.init()
|
|
18
|
+
from . import backgrounds, cursors, effects, fonts, themes
|
|
19
|
+
print('themes :', ', '.join(themes.list_themes()))
|
|
20
|
+
print('letter fx :', ', '.join(effects.APPEAR))
|
|
21
|
+
print('delete fx :', ', '.join(effects.VANISH))
|
|
22
|
+
print('cursors :', ', '.join(cursors.CURSORS))
|
|
23
|
+
print('cursor fx :', ', '.join(cursors.CURSOR_EFFECTS))
|
|
24
|
+
from . import splash
|
|
25
|
+
print('splashes :', ', '.join(splash.SPLASHES))
|
|
26
|
+
print('backgrounds:', ', '.join(backgrounds.BACKGROUNDS), '(+ mine via --miner)')
|
|
27
|
+
print('fonts :', ', '.join(fonts.list_fonts()))
|
|
28
|
+
return 0
|
|
29
|
+
if args.export_font:
|
|
30
|
+
pygame.init()
|
|
31
|
+
pygame.display.set_mode((1, 1), pygame.HIDDEN) if hasattr(pygame, 'HIDDEN') else None
|
|
32
|
+
from . import fonts
|
|
33
|
+
f = fonts.load_font(cfg.get('font') or 'builtin', cfg.get('font_size') or 18, cfg.get('antialias') is not False)
|
|
34
|
+
pf = fonts.PixelFont.from_font(f)
|
|
35
|
+
name = os.path.splitext(os.path.basename(args.export_font))[0]
|
|
36
|
+
p = pf.save(name)
|
|
37
|
+
print('exported %s (+ %s.png) - edit the PNG, then run with --font %s' % (p, name, os.path.relpath(p)))
|
|
38
|
+
return 0
|
|
39
|
+
from .app import App
|
|
40
|
+
App(cfg, args).run()
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == '__main__':
|
|
45
|
+
sys.exit(main() or 0)
|
pyterm/alarms.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""Alarms - sound + full-window display effect, triggered from Home Assistant (MQTT), the demo shell
|
|
2
|
+
(`alarm redalert Washing machine done`) or a plugin (app.trigger_alarm('doorbell', 'Someone is at the door')).
|
|
3
|
+
|
|
4
|
+
Built in:
|
|
5
|
+
redalert rotating red beacons, pulsing red screen edges, a klaxon, big ALERT card
|
|
6
|
+
warning yellow/black hazard tape crawling around the border, an urgent beep-beep
|
|
7
|
+
doorbell a friendly ding-dong, a bell swinging on a card that slides in
|
|
8
|
+
timer an old alarm clock shaking and ringing like crazy
|
|
9
|
+
calm a soft blue breathing glow and gentle rising tones (wake-up / reminder)
|
|
10
|
+
|
|
11
|
+
Any key or mouse click dismisses an alarm (the key is not passed to the program); it also stops by
|
|
12
|
+
itself after `duration` seconds. Alarms ring even when the theme's key sounds are off - only a
|
|
13
|
+
missing audio device or --no-sound keeps them silent.
|
|
14
|
+
|
|
15
|
+
Write your own:
|
|
16
|
+
|
|
17
|
+
from pyterm.alarms import Alarm
|
|
18
|
+
|
|
19
|
+
class Party(Alarm):
|
|
20
|
+
duration = 10
|
|
21
|
+
color = (255, 0, 255)
|
|
22
|
+
def sound(self, snd): # -> (pygame Sound, repeat seconds) or None
|
|
23
|
+
...
|
|
24
|
+
def draw(self, surf, app): # virtual pixel surface, after everything else
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def register(api):
|
|
28
|
+
api.register_alarm('party', Party)
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import math
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
import pygame
|
|
37
|
+
|
|
38
|
+
ADD = pygame.BLEND_RGB_ADD
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Alarm:
|
|
42
|
+
name = 'base'
|
|
43
|
+
duration = 30.0
|
|
44
|
+
color = (255, 60, 60)
|
|
45
|
+
title = 'ALERT'
|
|
46
|
+
label = None # Home Assistant button name ("Alarm <label>"), default: the registered name
|
|
47
|
+
ha_icon = 'mdi:alarm-light'
|
|
48
|
+
|
|
49
|
+
def __init__(self, app, message='', duration=None, sound=True):
|
|
50
|
+
self.app = app
|
|
51
|
+
self.message = (message or '').strip()
|
|
52
|
+
self.t = 0.0
|
|
53
|
+
self.done = False
|
|
54
|
+
self.dismissed = False
|
|
55
|
+
self.duration = float(duration) if duration else self.duration
|
|
56
|
+
self.snd = None
|
|
57
|
+
self.repeat = 0.0
|
|
58
|
+
self.next_sound = 0.0
|
|
59
|
+
if sound and app.sound.ok and not app.cfg.get('sound') is False:
|
|
60
|
+
try:
|
|
61
|
+
r = self.sound(app.sound)
|
|
62
|
+
if r:
|
|
63
|
+
self.snd, self.repeat = r
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print('[pyterm] alarm sound: %s' % e)
|
|
66
|
+
self.start()
|
|
67
|
+
|
|
68
|
+
# ---- override
|
|
69
|
+
def start(self):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def sound(self, snd):
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def draw(self, surf, app):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# ---- machinery
|
|
79
|
+
def update(self, dt):
|
|
80
|
+
self.t += dt
|
|
81
|
+
if self.snd is not None and self.t >= self.next_sound:
|
|
82
|
+
ch = pygame.mixer.find_channel(True)
|
|
83
|
+
if ch:
|
|
84
|
+
ch.set_volume(max(0.35, self.app.sound.volume))
|
|
85
|
+
ch.play(self.snd)
|
|
86
|
+
self.next_sound = self.t + (self.repeat or 1e9)
|
|
87
|
+
if self.t >= self.duration:
|
|
88
|
+
self.stop()
|
|
89
|
+
|
|
90
|
+
def stop(self, dismissed=False):
|
|
91
|
+
if self.done:
|
|
92
|
+
return
|
|
93
|
+
self.done = True
|
|
94
|
+
self.dismissed = dismissed
|
|
95
|
+
if self.snd is not None:
|
|
96
|
+
self.snd.stop()
|
|
97
|
+
|
|
98
|
+
# ---- helpers for subclasses
|
|
99
|
+
def big(self, app, text, color, k=2, shadow=True):
|
|
100
|
+
"""text in the terminal font, scaled up k times (blocky), with a hard drop shadow"""
|
|
101
|
+
cw, ch = app.font.cw, app.font.ch
|
|
102
|
+
s = pygame.Surface((len(text) * cw + 1, ch + 1), pygame.SRCALPHA)
|
|
103
|
+
if shadow:
|
|
104
|
+
app.draw_text(s, 1, 1, text, (0, 0, 0))
|
|
105
|
+
app.draw_text(s, 0, 0, text, color)
|
|
106
|
+
return pygame.transform.scale(s, (s.get_width() * k, s.get_height() * k))
|
|
107
|
+
|
|
108
|
+
def card(self, surf, app, title, color, y=None, icon=None, alpha=220, k=None):
|
|
109
|
+
"""centred message card: big blocky title, the (wrapped) message, a blinking hint and an optional
|
|
110
|
+
icon(surf, x, y, size) drawn in a column on the left"""
|
|
111
|
+
cw, ch = app.font.cw, app.font.ch
|
|
112
|
+
W, H = surf.get_size()
|
|
113
|
+
k = k or max(2, min(3, W // (cw * (len(title) + 10) * 2) + 1))
|
|
114
|
+
ts = self.big(app, title, color, k)
|
|
115
|
+
lines = _wrap(self.message, max(16, min(48, W // cw - 16)))[:4]
|
|
116
|
+
hint = 'press any key'
|
|
117
|
+
pad = cw * 2
|
|
118
|
+
isz = ch * 3 if icon else 0
|
|
119
|
+
tw = max([ts.get_width()] + [len(x) * cw for x in lines + [hint]])
|
|
120
|
+
w = tw + pad * 2 + (isz + pad if icon else 0)
|
|
121
|
+
h = pad + ts.get_height() + ch // 2 + len(lines) * ch + ch * 2 + pad // 2
|
|
122
|
+
h = max(h, isz + pad * 2)
|
|
123
|
+
x = (W - w) // 2
|
|
124
|
+
y = (H - h) // 2 if y is None else y
|
|
125
|
+
panel = pygame.Surface((w, h), pygame.SRCALPHA)
|
|
126
|
+
panel.fill((8, 6, 12, alpha))
|
|
127
|
+
surf.blit(panel, (x, y))
|
|
128
|
+
lw = max(1, ch // 8)
|
|
129
|
+
glow = 0.6 + 0.4 * math.sin(self.t * 6)
|
|
130
|
+
pygame.draw.rect(surf, tuple(int(c * glow) for c in color), (x - lw * 2, y - lw * 2, w + lw * 4, h + lw * 4), lw)
|
|
131
|
+
pygame.draw.rect(surf, color, (x, y, w, h), lw)
|
|
132
|
+
tx = x + pad + (isz + pad if icon else 0)
|
|
133
|
+
cx = tx + tw // 2
|
|
134
|
+
if icon:
|
|
135
|
+
icon(surf, x + pad, y + (h - isz) // 2, isz)
|
|
136
|
+
yy = y + pad
|
|
137
|
+
surf.blit(ts, (cx - ts.get_width() // 2, yy))
|
|
138
|
+
yy += ts.get_height() + ch // 2
|
|
139
|
+
for line in lines:
|
|
140
|
+
app.draw_text(surf, cx - len(line) * cw // 2, yy, line, (255, 255, 255))
|
|
141
|
+
yy += ch
|
|
142
|
+
if int(self.t * 2) % 2 == 0:
|
|
143
|
+
app.draw_text(surf, cx - len(hint) * cw // 2, yy + ch // 2, hint, (140, 140, 155))
|
|
144
|
+
return pygame.Rect(x, y, w, h)
|
|
145
|
+
|
|
146
|
+
def edge(self, surf, color, width, k=1.0):
|
|
147
|
+
"""soft additive glow along the window edges (cached gradient, scaled by k)"""
|
|
148
|
+
W, H = surf.get_size()
|
|
149
|
+
key = (W, H, width, color)
|
|
150
|
+
g = _EDGE.get(key)
|
|
151
|
+
if g is None:
|
|
152
|
+
_EDGE.clear()
|
|
153
|
+
x = np.minimum(np.arange(W), np.arange(W)[::-1])[:, None]
|
|
154
|
+
y = np.minimum(np.arange(H), np.arange(H)[::-1])[None, :]
|
|
155
|
+
d = np.minimum(x, y).astype(np.float32)
|
|
156
|
+
a = np.clip(1 - d / width, 0, 1) ** 2
|
|
157
|
+
g = pygame.Surface((W, H))
|
|
158
|
+
pygame.surfarray.blit_array(g, (a[..., None] * np.array(color, np.float32)).astype(np.uint8))
|
|
159
|
+
_EDGE[key] = g
|
|
160
|
+
if k >= 0.99:
|
|
161
|
+
surf.blit(g, (0, 0), special_flags=ADD)
|
|
162
|
+
elif k > 0.01:
|
|
163
|
+
c = g.copy()
|
|
164
|
+
v = int(255 * k)
|
|
165
|
+
c.fill((v, v, v), special_flags=pygame.BLEND_RGB_MULT)
|
|
166
|
+
surf.blit(c, (0, 0), special_flags=ADD)
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def tint(surf, rgb):
|
|
170
|
+
"""multiply the whole picture (rgb 0..255 per channel) - darken / color-grade what's behind"""
|
|
171
|
+
surf.fill(rgb, special_flags=pygame.BLEND_RGB_MULT)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
_EDGE = {}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _wrap(text, n):
|
|
178
|
+
out, line = [], ''
|
|
179
|
+
for w in (text or '').split():
|
|
180
|
+
while len(w) > n:
|
|
181
|
+
if line:
|
|
182
|
+
out.append(line)
|
|
183
|
+
line = ''
|
|
184
|
+
out.append(w[:n])
|
|
185
|
+
w = w[n:]
|
|
186
|
+
if line and len(line) + 1 + len(w) > n:
|
|
187
|
+
out.append(line)
|
|
188
|
+
line = w
|
|
189
|
+
else:
|
|
190
|
+
line = (line + ' ' + w).strip()
|
|
191
|
+
if line:
|
|
192
|
+
out.append(line)
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _env(n, a=0.01, r=0.05, sr=44100):
|
|
197
|
+
e = np.ones(n)
|
|
198
|
+
na, nr = min(int(a * sr), n // 2), min(int(r * sr), n // 2)
|
|
199
|
+
if na:
|
|
200
|
+
e[:na] = np.linspace(0, 1, na)
|
|
201
|
+
if nr:
|
|
202
|
+
e[-nr:] = np.linspace(1, 0, nr)
|
|
203
|
+
return e
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _tone(f, dur, wave='sine', sr=44100, vol=1.0):
|
|
207
|
+
t = np.arange(int(sr * dur)) / sr
|
|
208
|
+
if wave == 'square':
|
|
209
|
+
s = np.sign(np.sin(2 * np.pi * f * t))
|
|
210
|
+
elif wave == 'saw':
|
|
211
|
+
s = 2 * ((t * f) % 1) - 1
|
|
212
|
+
else:
|
|
213
|
+
s = np.sin(2 * np.pi * f * t)
|
|
214
|
+
return s * _env(len(t)) * vol
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ============================================================================ red alert
|
|
218
|
+
class RedAlert(Alarm):
|
|
219
|
+
label = 'red alert'
|
|
220
|
+
ha_icon = 'mdi:alarm-light'
|
|
221
|
+
name = 'redalert'
|
|
222
|
+
duration = 30.0
|
|
223
|
+
color = (255, 50, 40)
|
|
224
|
+
|
|
225
|
+
def sound(self, snd):
|
|
226
|
+
sr = 44100
|
|
227
|
+
t = np.arange(int(sr * 1.4)) / sr
|
|
228
|
+
f = 520 + 380 * np.abs(np.sin(np.pi * t / 1.4)) # whooping klaxon sweep
|
|
229
|
+
ph = 2 * np.pi * np.cumsum(f) / sr
|
|
230
|
+
s = (np.sign(np.sin(ph)) * 0.5 + np.sin(ph * 2) * 0.3) * _env(len(t), 0.02, 0.1)
|
|
231
|
+
return snd._mk(s, 0.9), 1.6
|
|
232
|
+
|
|
233
|
+
def draw(self, surf, app):
|
|
234
|
+
W, H = surf.get_size()
|
|
235
|
+
on = 0.5 + 0.5 * math.sin(self.t * 7)
|
|
236
|
+
# grade everything red and darker, pulsing with the klaxon
|
|
237
|
+
self.tint(surf, (int(170 + 70 * on), int(40 + 30 * on), int(36 + 26 * on)))
|
|
238
|
+
self.edge(surf, (255, 30, 16), max(12, min(W, H) // 6), 0.35 + 0.65 * on)
|
|
239
|
+
# rotating beacon on top: two light cones swept around
|
|
240
|
+
r = max(6, min(W, H) // 28)
|
|
241
|
+
bx, by = W // 2, r + 2
|
|
242
|
+
beams = pygame.Surface((W, H))
|
|
243
|
+
beams.fill((0, 0, 0))
|
|
244
|
+
L = math.hypot(W, H)
|
|
245
|
+
for k in (0, math.pi):
|
|
246
|
+
a = self.t * 4.5 + k
|
|
247
|
+
for spread, col in ((0.30, (60, 8, 4)), (0.16, (110, 18, 8)), (0.06, (160, 40, 20))):
|
|
248
|
+
pygame.draw.polygon(beams, col, [(bx, by), (bx + math.cos(a - spread) * L, by + math.sin(a - spread) * L),
|
|
249
|
+
(bx + math.cos(a + spread) * L, by + math.sin(a + spread) * L)])
|
|
250
|
+
surf.blit(beams, (0, 0), special_flags=ADD)
|
|
251
|
+
pygame.draw.rect(surf, (40, 40, 48), (bx - r * 2, 0, r * 4, r // 2 + 2)) # base
|
|
252
|
+
pygame.draw.circle(surf, (200, 20, 10), (bx, by), r)
|
|
253
|
+
pygame.draw.circle(surf, (255, 120, 90), (bx - r // 3, by - r // 3), max(1, r // 3))
|
|
254
|
+
flash = abs(math.cos(self.t * 4.5))
|
|
255
|
+
pygame.draw.circle(surf, (int(255 * flash), int(200 * flash), int(170 * flash)), (bx, by), max(1, r // 2))
|
|
256
|
+
self.card(surf, app, 'RED ALERT', (255, 70, 50), icon=_icon_siren(self.t))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ============================================================================ warning (hazard tape)
|
|
260
|
+
class Warning(Alarm):
|
|
261
|
+
label = 'warning'
|
|
262
|
+
ha_icon = 'mdi:alert'
|
|
263
|
+
name = 'warning'
|
|
264
|
+
duration = 20.0
|
|
265
|
+
color = (255, 210, 40)
|
|
266
|
+
|
|
267
|
+
def sound(self, snd):
|
|
268
|
+
s = np.concatenate([_tone(1800, 0.12, 'square'), np.zeros(4410), _tone(1800, 0.12, 'square'), np.zeros(26000)])
|
|
269
|
+
return snd._mk(s, 0.6), 0.9
|
|
270
|
+
|
|
271
|
+
def draw(self, surf, app):
|
|
272
|
+
W, H = surf.get_size()
|
|
273
|
+
self.tint(surf, (170, 160, 140))
|
|
274
|
+
band = max(8, min(W, H) // 16)
|
|
275
|
+
stripe = band
|
|
276
|
+
key = (W, H, band)
|
|
277
|
+
if getattr(self, '_tk', None) != key: # long tape strip, scrolled every frame
|
|
278
|
+
self._tk = key
|
|
279
|
+
n = W + H + stripe * 4
|
|
280
|
+
t = pygame.Surface((n, band))
|
|
281
|
+
t.fill((255, 200, 0))
|
|
282
|
+
for x in range(-band, n, stripe * 2):
|
|
283
|
+
pygame.draw.polygon(t, (24, 22, 20), [(x, 0), (x + stripe, 0), (x + stripe - band, band), (x - band, band)])
|
|
284
|
+
t.fill((255, 235, 120), (0, 0, n, max(1, band // 10)))
|
|
285
|
+
t.fill((120, 90, 0), (0, band - max(1, band // 10), n, max(1, band // 10)))
|
|
286
|
+
self._tape = t
|
|
287
|
+
off = int(self.t * band * 2) % (stripe * 2)
|
|
288
|
+
t = self._tape
|
|
289
|
+
surf.blit(t, (0, 0), (off, 0, W, band))
|
|
290
|
+
surf.blit(t, (0, H - band), (stripe * 2 - off, 0, W, band))
|
|
291
|
+
side = pygame.transform.rotate(t.subsurface((0, 0, H + stripe * 2, band)), 90)
|
|
292
|
+
surf.blit(side, (0, 0), (0, off, band, H))
|
|
293
|
+
surf.blit(side, (W - band, 0), (0, stripe * 2 - off, band, H))
|
|
294
|
+
blink = int(self.t * 3) % 2 == 0
|
|
295
|
+
for cx in (band // 2, W - band // 2): # flashing amber lamps in the corners
|
|
296
|
+
for cy in (band // 2, H - band // 2):
|
|
297
|
+
pygame.draw.circle(surf, (255, 170, 0) if blink else (90, 50, 0), (cx, cy), band // 2 + 1)
|
|
298
|
+
self.card(surf, app, 'WARNING', (255, 205, 30), icon=_icon_hazard(self.t))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ============================================================================ doorbell
|
|
302
|
+
class Doorbell(Alarm):
|
|
303
|
+
label = 'doorbell'
|
|
304
|
+
ha_icon = 'mdi:doorbell'
|
|
305
|
+
name = 'doorbell'
|
|
306
|
+
duration = 8.0
|
|
307
|
+
color = (120, 200, 255)
|
|
308
|
+
|
|
309
|
+
def sound(self, snd):
|
|
310
|
+
sr = 44100
|
|
311
|
+
ding = sum(np.sin(2 * np.pi * 660 * h * np.arange(int(sr * 1.2)) / sr) * a for h, a in ((1, 1), (2.76, 0.3), (5.4, 0.1)))
|
|
312
|
+
dong = sum(np.sin(2 * np.pi * 523 * h * np.arange(int(sr * 1.6)) / sr) * a for h, a in ((1, 1), (2.76, 0.3), (5.4, 0.1)))
|
|
313
|
+
ding *= np.exp(-np.arange(len(ding)) / sr * 3)
|
|
314
|
+
dong *= np.exp(-np.arange(len(dong)) / sr * 2.5)
|
|
315
|
+
s = np.concatenate([ding[:int(sr * 0.45)], dong])
|
|
316
|
+
return snd._mk(s, 0.8), 99
|
|
317
|
+
|
|
318
|
+
def draw(self, surf, app):
|
|
319
|
+
W, H = surf.get_size()
|
|
320
|
+
ch = app.font.ch
|
|
321
|
+
slide = min(1.0, self.t / 0.45)
|
|
322
|
+
e = 1 - (1 - slide) ** 3 + math.sin(slide * math.pi) * 0.08 # ease out with a small bounce
|
|
323
|
+
self.edge(surf, (40, 110, 200), max(10, min(W, H) // 8), 0.5 + 0.3 * math.sin(self.t * 3))
|
|
324
|
+
y0 = -ch * 10
|
|
325
|
+
self.card(surf, app, 'DING DONG', (120, 200, 255), y=int(y0 + (ch * 3 - y0) * e), icon=_icon_bell(self.t))
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ============================================================================ timer (alarm clock)
|
|
329
|
+
class Timer(Alarm):
|
|
330
|
+
label = 'timer'
|
|
331
|
+
ha_icon = 'mdi:timer-alert'
|
|
332
|
+
name = 'timer'
|
|
333
|
+
duration = 30.0
|
|
334
|
+
color = (255, 140, 60)
|
|
335
|
+
|
|
336
|
+
def sound(self, snd):
|
|
337
|
+
sr = 44100
|
|
338
|
+
ring = np.concatenate([_tone(2600 if i % 2 else 2300, 0.035, 'square') for i in range(40)] + [np.zeros(int(sr * 0.4))])
|
|
339
|
+
return snd._mk(ring, 0.55), 1.8
|
|
340
|
+
|
|
341
|
+
def draw(self, surf, app):
|
|
342
|
+
W, H = surf.get_size()
|
|
343
|
+
self.tint(surf, (190, 180, 170))
|
|
344
|
+
ringing = (self.t % 1.8) < 1.4
|
|
345
|
+
self.edge(surf, (255, 120, 30), max(10, min(W, H) // 8), 0.8 if ringing else 0.25)
|
|
346
|
+
r = max(14, min(W, H) // 8)
|
|
347
|
+
rect = self.card(surf, app, 'TIME IS UP', (255, 150, 60), y=H // 2 - r // 2)
|
|
348
|
+
_clock(surf, W // 2, rect.top - int(r * 1.45), r, self.t, ringing)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ============================================================================ calm
|
|
352
|
+
class Calm(Alarm):
|
|
353
|
+
label = 'calm reminder'
|
|
354
|
+
ha_icon = 'mdi:bell-ring-outline'
|
|
355
|
+
name = 'calm'
|
|
356
|
+
duration = 20.0
|
|
357
|
+
color = (100, 170, 255)
|
|
358
|
+
|
|
359
|
+
def sound(self, snd):
|
|
360
|
+
sr = 44100
|
|
361
|
+
notes = [523.25, 659.25, 783.99, 1046.5]
|
|
362
|
+
parts = []
|
|
363
|
+
for f in notes:
|
|
364
|
+
n = int(sr * 0.9)
|
|
365
|
+
t = np.arange(n) / sr
|
|
366
|
+
parts.append(np.sin(2 * np.pi * f * t) * np.exp(-t * 3) * 0.8 + np.sin(4 * np.pi * f * t) * np.exp(-t * 5) * 0.2)
|
|
367
|
+
s = np.zeros(int(sr * 3.2))
|
|
368
|
+
for i, p in enumerate(parts):
|
|
369
|
+
st = int(i * sr * 0.45)
|
|
370
|
+
s[st:st + len(p)] += p
|
|
371
|
+
return snd._mk(s, 0.5), 4.5
|
|
372
|
+
|
|
373
|
+
def draw(self, surf, app):
|
|
374
|
+
W, H = surf.get_size()
|
|
375
|
+
b = 0.5 + 0.5 * math.sin(self.t * 1.3)
|
|
376
|
+
fade = min(1.0, self.t / 1.5)
|
|
377
|
+
v = int(255 - 60 * fade)
|
|
378
|
+
self.tint(surf, (int(v * 0.86), int(v * 0.93), v))
|
|
379
|
+
self.edge(surf, (40, 110, 255), max(16, min(W, H) // 4), (0.35 + 0.65 * b) * fade)
|
|
380
|
+
self.card(surf, app, 'REMINDER', (130, 190, 255), alpha=185, icon=_icon_moon(self.t))
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# ---------------------------------------------------------------------------- card icons: f(surf, x, y, size)
|
|
385
|
+
def _icon_siren(t):
|
|
386
|
+
def f(s, x, y, n):
|
|
387
|
+
cx = x + n // 2
|
|
388
|
+
on = int(t * 6) % 2
|
|
389
|
+
pygame.draw.rect(s, (60, 60, 70), (x + n // 8, y + n * 3 // 4, n * 3 // 4, n // 6))
|
|
390
|
+
pygame.draw.ellipse(s, (255, 40, 30) if on else (120, 10, 10), (x + n // 5, y + n // 6, n * 3 // 5, n * 3 // 4))
|
|
391
|
+
s.fill((60, 60, 70), (x + n // 5, y + n * 9 // 16, n * 3 // 5, n // 5))
|
|
392
|
+
pygame.draw.circle(s, (255, 200, 180), (cx - n // 9, y + n // 3), max(1, n // 12))
|
|
393
|
+
if on:
|
|
394
|
+
for a in (-0.9, -0.45, 0, 0.45, 0.9):
|
|
395
|
+
ang = a - math.pi / 2
|
|
396
|
+
pygame.draw.line(s, (255, 120, 90), (cx + math.cos(ang) * n * 0.48, y + n * 0.45 + math.sin(ang) * n * 0.48),
|
|
397
|
+
(cx + math.cos(ang) * n * 0.62, y + n * 0.45 + math.sin(ang) * n * 0.62), max(1, n // 16))
|
|
398
|
+
return f
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _icon_hazard(t):
|
|
402
|
+
def f(s, x, y, n):
|
|
403
|
+
pts = [(x + n // 2, y + n // 10), (x + n - 1, y + n - n // 10), (x, y + n - n // 10)]
|
|
404
|
+
pygame.draw.polygon(s, (24, 22, 20), [(px, py + 2) for px, py in pts])
|
|
405
|
+
pygame.draw.polygon(s, (255, 205, 30), pts)
|
|
406
|
+
lw = max(2, n // 9)
|
|
407
|
+
s.fill((24, 22, 20), (x + n // 2 - lw // 2, y + n * 7 // 20, lw, n * 3 // 10))
|
|
408
|
+
s.fill((24, 22, 20), (x + n // 2 - lw // 2, y + n * 3 // 4, lw, lw))
|
|
409
|
+
return f
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _icon_bell(t):
|
|
413
|
+
def f(s, x, y, n):
|
|
414
|
+
a = math.sin(t * 14) * 0.45 * max(0.0, 1 - t / 2.2)
|
|
415
|
+
cx, cy = x + n / 2, y + n * 0.12
|
|
416
|
+
|
|
417
|
+
def rot(px, py):
|
|
418
|
+
dx, dy = px - cx, py - cy
|
|
419
|
+
return cx + dx * math.cos(a) - dy * math.sin(a), cy + dx * math.sin(a) + dy * math.cos(a)
|
|
420
|
+
prof = [(-0.08, 0.05), (-0.22, 0.15), (-0.28, 0.4), (-0.32, 0.62), (-0.44, 0.74),
|
|
421
|
+
(0.44, 0.74), (0.32, 0.62), (0.28, 0.4), (0.22, 0.15), (0.08, 0.05)]
|
|
422
|
+
pts = [rot(cx + px * n, cy + py * n) for px, py in prof]
|
|
423
|
+
pygame.draw.polygon(s, (190, 130, 30), [(px + 1, py + 1) for px, py in pts])
|
|
424
|
+
pygame.draw.polygon(s, (255, 205, 70), pts)
|
|
425
|
+
pygame.draw.line(s, (255, 245, 190), rot(cx - n * 0.18, cy + n * 0.2), rot(cx - n * 0.24, cy + n * 0.55), max(1, n // 14))
|
|
426
|
+
pygame.draw.circle(s, (230, 160, 40), [int(v) for v in rot(cx + math.sin(a * 2) * n * 0.12, cy + n * 0.82)], max(2, n // 9))
|
|
427
|
+
pygame.draw.circle(s, (200, 200, 210), (int(cx), int(cy)), max(1, n // 16))
|
|
428
|
+
return f
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _icon_moon(t):
|
|
432
|
+
def f(s, x, y, n):
|
|
433
|
+
cx, cy, r = x + n // 2, y + n // 2, n * 2 // 5
|
|
434
|
+
pygame.draw.circle(s, (230, 235, 255), (cx, cy), r)
|
|
435
|
+
pygame.draw.circle(s, (8, 6, 12), (cx + r // 2, cy - r // 3), r)
|
|
436
|
+
for i, (sx, sy) in enumerate(((0.85, 0.15), (0.95, 0.55), (0.7, 0.85))):
|
|
437
|
+
tw = 0.5 + 0.5 * math.sin(t * 3 + i * 2)
|
|
438
|
+
v = int(120 + 135 * tw)
|
|
439
|
+
px, py = int(x + sx * n), int(y + sy * n)
|
|
440
|
+
s.fill((v, v, 255), (px, py, max(1, n // 20), max(1, n // 20)))
|
|
441
|
+
return f
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _clock(surf, cx, cy, r, t, ringing):
|
|
445
|
+
shake = math.sin(t * 55) * r * 0.07 if ringing else 0.0
|
|
446
|
+
tilt = math.sin(t * 55) * 0.08 if ringing else 0.0
|
|
447
|
+
cx += int(shake)
|
|
448
|
+
hit = math.sin(t * 55) > 0
|
|
449
|
+
for sgn in (-1, 1): # bells + hammer
|
|
450
|
+
bxy = (int(cx + sgn * r * 0.72 + math.sin(tilt) * r), int(cy - r * 0.82))
|
|
451
|
+
pygame.draw.circle(surf, (120, 120, 130), (bxy[0] + 1, bxy[1] + 1), int(r * 0.36))
|
|
452
|
+
pygame.draw.circle(surf, (225, 225, 235), bxy, int(r * 0.36))
|
|
453
|
+
pygame.draw.circle(surf, (255, 255, 255), (bxy[0] - r // 9, bxy[1] - r // 9), max(1, r // 12))
|
|
454
|
+
hx = cx + (r * 0.22 if hit else -r * 0.22) * (1 if ringing else 0)
|
|
455
|
+
pygame.draw.line(surf, (70, 70, 80), (cx, cy - r * 0.95), (hx, cy - r * 1.18), max(2, r // 9))
|
|
456
|
+
pygame.draw.circle(surf, (70, 70, 80), (int(hx), int(cy - r * 1.18)), max(2, r // 8))
|
|
457
|
+
for sgn in (-1, 1): # feet
|
|
458
|
+
pygame.draw.line(surf, (70, 70, 80), (cx + sgn * r * 0.55, cy + r * 0.8), (cx + sgn * r * 0.85, cy + r * 1.15), max(2, r // 7))
|
|
459
|
+
pygame.draw.circle(surf, (110, 20, 20), (cx + 2, cy + 2), r)
|
|
460
|
+
pygame.draw.circle(surf, (215, 50, 40), (cx, cy), r)
|
|
461
|
+
pygame.draw.circle(surf, (255, 140, 120), (cx - r // 3, cy - r // 2), max(1, r // 8))
|
|
462
|
+
pygame.draw.circle(surf, (250, 248, 236), (cx, cy), int(r * 0.8))
|
|
463
|
+
for i in range(12):
|
|
464
|
+
a = i * math.pi / 6
|
|
465
|
+
d = max(1, r // 14) * (2 if i % 3 == 0 else 1)
|
|
466
|
+
surf.fill((40, 40, 40), (int(cx + math.cos(a) * r * 0.68) - d // 2, int(cy + math.sin(a) * r * 0.68) - d // 2, d, d))
|
|
467
|
+
now = time.localtime()
|
|
468
|
+
for frac, ln, w in ((((now.tm_hour % 12) + now.tm_min / 60) / 12, 0.42, max(2, r // 9)),
|
|
469
|
+
(now.tm_min / 60, 0.62, max(1, r // 13)), (now.tm_sec / 60, 0.66, 1)):
|
|
470
|
+
ang = frac * math.tau - math.pi / 2
|
|
471
|
+
col = (200, 30, 30) if ln == 0.66 else (30, 30, 30)
|
|
472
|
+
pygame.draw.line(surf, col, (cx, cy), (cx + math.cos(ang) * r * ln, cy + math.sin(ang) * r * ln), w)
|
|
473
|
+
pygame.draw.circle(surf, (30, 30, 30), (cx, cy), max(2, r // 12))
|
|
474
|
+
if ringing: # "RRRING" vibration marks
|
|
475
|
+
for sgn in (-1, 1):
|
|
476
|
+
for k in range(3):
|
|
477
|
+
rr = r * (1.25 + k * 0.18)
|
|
478
|
+
a0 = -math.pi / 2 + sgn * 0.9
|
|
479
|
+
pygame.draw.arc(surf, (255, 200, 120), (cx - rr, cy - rr, rr * 2, rr * 2),
|
|
480
|
+
min(a0, a0 + sgn * 0.5), max(a0, a0 + sgn * 0.5), max(1, r // 16))
|
|
481
|
+
|
|
482
|
+
ALARMS = {c.name: c for c in (RedAlert, Warning, Doorbell, Timer, Calm)}
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def parse(payload):
|
|
486
|
+
"""'redalert' | 'redalert Washing machine done' | JSON {"type": .., "message": .., "duration": .., "sound": ..}"""
|
|
487
|
+
import json
|
|
488
|
+
p = (payload or '').strip()
|
|
489
|
+
if p.startswith('{'):
|
|
490
|
+
try:
|
|
491
|
+
d = json.loads(p)
|
|
492
|
+
return str(d.get('type') or d.get('alarm') or 'redalert'), str(d.get('message') or ''), d.get('duration'), \
|
|
493
|
+
d.get('sound', True) is not False
|
|
494
|
+
except ValueError:
|
|
495
|
+
pass
|
|
496
|
+
name, _, msg = p.partition(' ')
|
|
497
|
+
if name not in ALARMS:
|
|
498
|
+
name, msg = 'redalert', p
|
|
499
|
+
return name, msg, None, True
|