duter 0.1.0__tar.gz

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.
duter-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 halibote
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
duter-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: duter
3
+ Version: 0.1.0
4
+ Summary: Summon a sprite: a magical being appears and sparks of joy bloom, ending with HAPPINESS glowing in the sky — one line of magic, zero dependencies, and a little joy.
5
+ Author: halibote
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/halibote-bit/duter
8
+ Keywords: duter,sprite,happiness,joy,animation,tkinter,magic,toy
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Games/Entertainment
15
+ Classifier: Topic :: Multimedia :: Graphics
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # duter ✨
23
+
24
+ Summon a sprite with one line of Python: a magical being appears, dances across a starry sky,
25
+ sparks of joy bloom around it — and at the end, **HAPPINESS**
26
+ appears in rainbow colours, one letter at a time.
27
+
28
+ Run the window whenever you need it: it just might bring you a good mood
29
+ and a little joy. 🌟
30
+
31
+ **Zero third-party dependencies** — pure standard-library `tkinter`.
32
+
33
+ ```python
34
+ import duter
35
+ duter.summon()
36
+ ```
37
+
38
+ While the window is open:
39
+
40
+ - **Click anywhere** — the sprite creates more sparks of joy
41
+ - **Esc** or close the window — end early
42
+
43
+ ## API
44
+
45
+ ```python
46
+ duter.summon(mood="rainbow", duration=8.0, finale="sparkle")
47
+ ```
48
+
49
+ | Parameter | Default | Meaning |
50
+ |------------|-------------|--------------------------------------------------------------|
51
+ | `mood` | `"rainbow"` | Colour theme: `rainbow`, `gold`, `silver`, `sunrise`, `ocean` |
52
+ | `duration` | `8.0` | Rough show length in seconds (≥ 2); more sparks for longer shows |
53
+ | `finale` | `"sparkle"` | Finale: sparkle sky + rainbow "HAPPINESS"; `None` to skip |
54
+
55
+ ## Command line
56
+
57
+ ```console
58
+ python -m duter
59
+ python -m duter --mood sunrise --duration 12
60
+ duter --mood gold --no-finale # after `pip install duter`
61
+ ```
62
+
63
+ ## What happens on screen
64
+
65
+ 1. A tiny sprite materializes from stardust at the bottom of a starry sky
66
+ 2. It begins to dance, leaving trails of sparkling light
67
+ 3. Bursts of joy explode around the sprite with colorful particles
68
+ 4. ~100 particles bloom with gravity, drag, flicker, and colour cooling
69
+ (white-hot → theme colour → dying ember)
70
+ 5. Finale: sparkles fill the sky, and **HAPPINESS** glows in rainbow
71
+ letters before gently fading away, then the window closes itself
72
+
73
+ ## Requirements
74
+
75
+ - Python ≥ 3.8 with `tkinter` (included in the official installers on
76
+ Windows and macOS; on Linux: `sudo apt install python3-tk` or equivalent)
77
+
78
+ ## Development
79
+
80
+ ```console
81
+ pip install build twine
82
+ python -m build
83
+ twine upload dist/*
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT
duter-0.1.0/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # duter ✨
2
+
3
+ Summon a sprite with one line of Python: a magical being appears, dances across a starry sky,
4
+ sparks of joy bloom around it — and at the end, **HAPPINESS**
5
+ appears in rainbow colours, one letter at a time.
6
+
7
+ Run the window whenever you need it: it just might bring you a good mood
8
+ and a little joy. 🌟
9
+
10
+ **Zero third-party dependencies** — pure standard-library `tkinter`.
11
+
12
+ ```python
13
+ import duter
14
+ duter.summon()
15
+ ```
16
+
17
+ While the window is open:
18
+
19
+ - **Click anywhere** — the sprite creates more sparks of joy
20
+ - **Esc** or close the window — end early
21
+
22
+ ## API
23
+
24
+ ```python
25
+ duter.summon(mood="rainbow", duration=8.0, finale="sparkle")
26
+ ```
27
+
28
+ | Parameter | Default | Meaning |
29
+ |------------|-------------|--------------------------------------------------------------|
30
+ | `mood` | `"rainbow"` | Colour theme: `rainbow`, `gold`, `silver`, `sunrise`, `ocean` |
31
+ | `duration` | `8.0` | Rough show length in seconds (≥ 2); more sparks for longer shows |
32
+ | `finale` | `"sparkle"` | Finale: sparkle sky + rainbow "HAPPINESS"; `None` to skip |
33
+
34
+ ## Command line
35
+
36
+ ```console
37
+ python -m duter
38
+ python -m duter --mood sunrise --duration 12
39
+ duter --mood gold --no-finale # after `pip install duter`
40
+ ```
41
+
42
+ ## What happens on screen
43
+
44
+ 1. A tiny sprite materializes from stardust at the bottom of a starry sky
45
+ 2. It begins to dance, leaving trails of sparkling light
46
+ 3. Bursts of joy explode around the sprite with colorful particles
47
+ 4. ~100 particles bloom with gravity, drag, flicker, and colour cooling
48
+ (white-hot → theme colour → dying ember)
49
+ 5. Finale: sparkles fill the sky, and **HAPPINESS** glows in rainbow
50
+ letters before gently fading away, then the window closes itself
51
+
52
+ ## Requirements
53
+
54
+ - Python ≥ 3.8 with `tkinter` (included in the official installers on
55
+ Windows and macOS; on Linux: `sudo apt install python3-tk` or equivalent)
56
+
57
+ ## Development
58
+
59
+ ```console
60
+ pip install build twine
61
+ python -m build
62
+ twine upload dist/*
63
+ ```
64
+
65
+ ## License
66
+
67
+ MIT
@@ -0,0 +1,17 @@
1
+ """duter — summon a sprite, bloom sparks of joy, feel the happiness.
2
+
3
+ Quickstart
4
+ ----------
5
+ >>> import duter
6
+ >>> duter.summon() # random-colour sparks
7
+ >>> duter.summon(mood="sunrise") # themed colours
8
+ >>> duter.summon(duration=12) # a longer show
9
+
10
+ While the window is open: click anywhere to create extra sparks of joy,
11
+ press Esc (or close the window) to end early.
12
+ """
13
+
14
+ from .core import MOODS, summon
15
+
16
+ __version__ = "0.1.0"
17
+ __all__ = ["summon", "MOODS", "__version__"]
@@ -0,0 +1,30 @@
1
+ """Command-line entry point: ``python -m duter`` or ``duter``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from .core import MOODS, summon
8
+
9
+
10
+ def main(argv=None):
11
+ parser = argparse.ArgumentParser(
12
+ prog="duter",
13
+ description="Summon a sprite: a magical being appears, sparks of joy bloom, "
14
+ "and HAPPINESS glows in the sky.")
15
+ parser.add_argument(
16
+ "--mood", default="rainbow", choices=sorted(MOODS),
17
+ help="colour theme (default: rainbow)")
18
+ parser.add_argument(
19
+ "--duration", type=float, default=8.0,
20
+ help="rough show length in seconds (default: 8)")
21
+ parser.add_argument(
22
+ "--no-finale", action="store_true",
23
+ help="skip the HAPPINESS finale")
24
+ args = parser.parse_args(argv)
25
+ summon(mood=args.mood, duration=args.duration,
26
+ finale=None if args.no_finale else "sparkle")
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
@@ -0,0 +1,502 @@
1
+ """duter.core — the sprite animation engine.
2
+
3
+ Zero third-party dependencies: pure ``tkinter`` + standard library.
4
+
5
+ Animation Outline
6
+ -----------------
7
+ 1. A tiny sprite materializes from stardust at the bottom of a starry night sky.
8
+ 2. It begins to dance, leaving trails of sparkling light.
9
+ 3. Bursts of joy explode around the sprite with colorful particles.
10
+ 4. A particle system simulates the bloom: gravity, air drag, colour cooling
11
+ (white-hot -> theme colour -> dying ember) and flickering sparks.
12
+ 5. Optional finale: the sparks fade into a sky full of sparkles and the
13
+ words "HAPPINESS" appear one rainbow-coloured letter at a time, glow,
14
+ and gently vanish.
15
+
16
+ Click anywhere to summon extra sparks. Press Esc to close early.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import colorsys
22
+ import math
23
+ import random
24
+ import time
25
+ import tkinter as tk
26
+
27
+ __all__ = ["summon", "MOODS"]
28
+
29
+ # ----------------------------------------------------------------- palette
30
+
31
+ _BG = (6, 10, 30) # deep night sky
32
+ _GOLD = (255, 200, 60)
33
+
34
+ _WIDTH, _HEIGHT = 920, 620
35
+ _MS = 16 # frame interval (~60 fps)
36
+
37
+ #: Built-in colour themes. ``"rainbow"`` picks a fresh random hue per burst.
38
+ MOODS = {
39
+ "rainbow": None,
40
+ "gold": [(255, 200, 60), (255, 160, 50), (255, 235, 160)],
41
+ "silver": [(210, 225, 255), (160, 185, 235), (240, 246, 255)],
42
+ "sunrise": [(255, 140, 66), (255, 200, 120), (255, 100, 80)],
43
+ "ocean": [(70, 130, 180), (100, 200, 230), (173, 216, 230)],
44
+ }
45
+
46
+
47
+ def _hex(c):
48
+ return "#%02x%02x%02x" % (int(c[0]), int(c[1]), int(c[2]))
49
+
50
+
51
+ def _mix(a, b, t):
52
+ return (a[0] + (b[0] - a[0]) * t,
53
+ a[1] + (b[1] - a[1]) * t,
54
+ a[2] + (b[2] - a[2]) * t)
55
+
56
+
57
+ def _scale(c, f):
58
+ return (min(255, c[0] * f), min(255, c[1] * f), min(255, c[2] * f))
59
+
60
+
61
+ def _random_hue():
62
+ h = random.random()
63
+ return tuple(int(v * 255) for v in colorsys.hsv_to_rgb(h, 0.75, 1.0))
64
+
65
+
66
+ # ----------------------------------------------------------------- objects
67
+
68
+ class _Spark:
69
+ """One firework particle with gravity, drag, colour cooling, flicker."""
70
+
71
+ __slots__ = ("x", "y", "vx", "vy", "age", "life", "c0", "c1", "r",
72
+ "item", "flicker", "grav", "drag")
73
+
74
+ def __init__(self, cv, x, y, vx, vy, life, color, r=2.0,
75
+ flicker=False, grav=0.045, drag=0.986):
76
+ self.x, self.y, self.vx, self.vy = x, y, vx, vy
77
+ self.age, self.life = 0, life
78
+ self.c0 = _mix(color, (255, 255, 255), 0.65) # white-hot core
79
+ self.c1 = color
80
+ self.r = r
81
+ self.flicker = flicker
82
+ self.grav, self.drag = grav, drag
83
+ self.item = cv.create_oval(x - r, y - r, x + r, y + r,
84
+ fill=_hex(self.c0), outline="")
85
+
86
+ def step(self, cv, frame):
87
+ """Advance one frame; return False when the spark is dead."""
88
+ self.age += 1
89
+ if self.age >= self.life:
90
+ cv.delete(self.item)
91
+ return False
92
+ self.vx *= self.drag
93
+ self.vy = self.vy * self.drag + self.grav
94
+ self.x += self.vx
95
+ self.y += self.vy
96
+ t = self.age / self.life
97
+ if t < 0.22: # hot white -> theme
98
+ c = _mix(self.c0, self.c1, t / 0.22)
99
+ else: # theme -> dark ember
100
+ c = _mix(self.c1, _scale(self.c1, 0.10), (t - 0.22) / 0.78)
101
+ if self.flicker and (frame + id(self)) % 3 == 0:
102
+ c = _BG
103
+ r = self.r * (1.0 - 0.55 * t)
104
+ cv.coords(self.item, self.x - r, self.y - r, self.x + r, self.y + r)
105
+ cv.itemconfigure(self.item, fill=_hex(c))
106
+ return True
107
+
108
+
109
+ class _Shell:
110
+ """A rocket fired from the wand tip; explodes near its apex."""
111
+
112
+ GRAV = 0.085
113
+
114
+ def __init__(self, cv, x0, y0, tx, ty, colors):
115
+ h = y0 - ty
116
+ self.x, self.y = x0, y0
117
+ self.vy = -math.sqrt(2 * self.GRAV * h)
118
+ tf = math.sqrt(2 * h / self.GRAV)
119
+ self.vx = (tx - x0) / tf
120
+ self.colors = colors
121
+ self.item = cv.create_oval(x0 - 3, y0 - 3, x0 + 3, y0 + 3,
122
+ fill="#fff2c8", outline="")
123
+
124
+ def step(self, cv, sparks):
125
+ self.x += self.vx
126
+ self.vy += self.GRAV
127
+ self.y += self.vy
128
+ cv.coords(self.item, self.x - 3, self.y - 3, self.x + 3, self.y + 3)
129
+ for _ in range(2): # golden trail
130
+ sparks.append(_Spark(
131
+ cv,
132
+ self.x + random.uniform(-2, 2),
133
+ self.y + random.uniform(-2, 2),
134
+ random.uniform(-0.4, 0.4),
135
+ random.uniform(0.2, 0.9),
136
+ random.randint(18, 30),
137
+ (255, 190, 90), r=1.4, grav=0.01, drag=0.95,
138
+ ))
139
+ return self.vy > -0.35 # near apex -> boom
140
+
141
+
142
+ # ------------------------------------------------------------------- show
143
+
144
+ class _Show:
145
+ """Owns the tkinter window and runs the whole animation state machine."""
146
+
147
+ def __init__(self, mood, duration, finale):
148
+ try:
149
+ self.root = tk.Tk()
150
+ except tk.TclError as exc: # no display available
151
+ raise RuntimeError(
152
+ "duter needs a graphical display (a desktop) to summon "
153
+ "its sprite, but none was found: " + str(exc)
154
+ ) from exc
155
+
156
+ self.mood = mood
157
+ self.duration = max(2.0, duration)
158
+ self.finale = finale
159
+ self._summons = min(8, max(2, round((self.duration - 0.8) / 2.4)))
160
+
161
+ self.root.title("duter ✨ a sprite, sparks of joy & a little happiness")
162
+ self.root.configure(bg=_hex(_BG))
163
+ self.root.resizable(False, False)
164
+ self.cv = tk.Canvas(self.root, width=_WIDTH, height=_HEIGHT,
165
+ bg=_hex(_BG), highlightthickness=0)
166
+ self.cv.pack()
167
+
168
+ self.root.update_idletasks()
169
+ self.root.geometry("+%d+%d" % (
170
+ (self.root.winfo_screenwidth() - _WIDTH) // 2,
171
+ (self.root.winfo_screenheight() - _HEIGHT) // 2,
172
+ ))
173
+
174
+ self._frame = 0
175
+ self._phase = "materialize" # materialize -> dance -> burst -> settle
176
+ self._pt = 0 # frames spent in current phase
177
+ self._summon_no = 0
178
+ self._sparks = []
179
+ self._shells = []
180
+ self._flashes = []
181
+ self._letters = []
182
+ self._sub = None
183
+ self._done = False
184
+ self._t0 = time.time()
185
+
186
+ self._build_sky()
187
+ self._build_sprite()
188
+ self._build_glow_items()
189
+
190
+ self.root.protocol("WM_DELETE_WINDOW", self._close)
191
+ self.root.bind("<Escape>", self._close)
192
+ self.cv.bind("<Button-1>", self._on_click)
193
+
194
+ # ------------------------------------------------------------- scenery
195
+
196
+ def _build_sky(self):
197
+ self._stars = []
198
+ for _ in range(110):
199
+ x = random.uniform(0, _WIDTH)
200
+ y = random.uniform(0, _HEIGHT * 0.92)
201
+ r = random.choice((0.7, 0.9, 1.2, 1.6))
202
+ c = random.choice(((200, 215, 255), (255, 245, 225), (235, 240, 255)))
203
+ item = self.cv.create_oval(x - r, y - r, x + r, y + r,
204
+ fill=_hex(_scale(c, random.uniform(0.35, 1.0))),
205
+ outline="")
206
+ self._stars.append((item, c))
207
+
208
+ def _build_sprite(self):
209
+ """Build the tiny magical sprite that dances across the sky."""
210
+ self._sprite_pos = (_WIDTH / 2, _HEIGHT * 0.85)
211
+ self._sprite_dy = 0
212
+ # Sprite body: a glowing orb with wings
213
+ self._sprite_body = self.cv.create_oval(0, 0, 0, 0, fill="#ffe4b5", outline="")
214
+ self._sprite_glow = self.cv.create_oval(0, 0, 0, 0, fill="#ffd700", stipple="gray50", outline="")
215
+ self._sprite_wing_l = self.cv.create_oval(0, 0, 0, 0, fill="#fffacd", stipple="gray25", outline="")
216
+ self._sprite_wing_r = self.cv.create_oval(0, 0, 0, 0, fill="#fffacd", stipple="gray25", outline="")
217
+ self._sprite_eyes = []
218
+
219
+ def _build_glow_items(self):
220
+ """Build glow items for the sprite's aura."""
221
+ sx, sy = self._sprite_pos
222
+ self._aura_a = self.cv.create_oval(0, 0, 0, 0, fill="#ffd970",
223
+ stipple="gray50", outline="")
224
+ self._aura_b = self.cv.create_oval(0, 0, 0, 0, fill="#ffe9a8",
225
+ stipple="gray75", outline="")
226
+
227
+ def _sprite_position(self):
228
+ """Get current sprite position with dance animation."""
229
+ sx, sy = self._sprite_pos
230
+ if self._phase == "materialize":
231
+ t = min(1.0, self._pt / 42.0)
232
+ e = 1.0 - (1.0 - t) ** 3
233
+ dy = (1.0 - e) * 100
234
+ elif self._phase == "dance":
235
+ dy = 8 * math.sin(self._frame * 0.06)
236
+ else:
237
+ dy = 4 * math.sin(self._frame * 0.04)
238
+ return (sx, sy - dy)
239
+
240
+ # -------------------------------------------------------------- casting
241
+
242
+ def _mood_colors(self):
243
+ if self.mood == "rainbow":
244
+ base = _random_hue()
245
+ second = _random_hue() if random.random() < 0.35 else base
246
+ else:
247
+ pal = MOODS[self.mood]
248
+ base = random.choice(pal)
249
+ second = random.choice(pal)
250
+ return base, second
251
+
252
+ def _burst(self, colors=None):
253
+ sx, sy = self._sprite_position()
254
+ target = (random.uniform(_WIDTH * 0.15, _WIDTH * 0.85),
255
+ random.uniform(_HEIGHT * 0.14, _HEIGHT * 0.34))
256
+ self._shells.append(
257
+ _Shell(self.cv, sx, sy - 20, target[0], target[1], colors or self._mood_colors()))
258
+
259
+ def _explode(self, x, y, colors):
260
+ base, second = colors
261
+ self._flashes.append({"x": x, "y": y, "t": 0})
262
+ n = random.randint(85, 125)
263
+ speed = random.uniform(2.8, 4.2)
264
+ for _ in range(n):
265
+ a = random.uniform(0, math.tau)
266
+ v = speed * random.uniform(0.25, 1.0)
267
+ col = base if random.random() < 0.75 else second
268
+ self._sparks.append(_Spark(
269
+ self.cv, x, y,
270
+ math.cos(a) * v, math.sin(a) * v,
271
+ random.randint(45, 85), col,
272
+ r=random.uniform(1.6, 2.6),
273
+ flicker=random.random() < 0.4,
274
+ ))
275
+ for _ in range(12): # slow falling embers
276
+ a = random.uniform(0, math.tau)
277
+ v = random.uniform(0.5, 1.4)
278
+ self._sparks.append(_Spark(
279
+ self.cv, x, y,
280
+ math.cos(a) * v, math.sin(a) * v,
281
+ random.randint(90, 130), second,
282
+ r=2.2, drag=0.965, grav=0.05,
283
+ ))
284
+
285
+ def _on_click(self, _event):
286
+ """Click anywhere: the sprite creates more sparks of joy."""
287
+ if not self._done and self._phase != "materialize":
288
+ self._burst()
289
+
290
+ # --------------------------------------------------------------- frames
291
+
292
+ def _update_sprite(self):
293
+ """Update sprite position and animation."""
294
+ sx, sy = self._sprite_position()
295
+ # Body
296
+ r = 12 + 2 * math.sin(self._frame * 0.1)
297
+ self.cv.coords(self._sprite_body, sx - r, sy - r, sx + r, sy + r)
298
+ # Glow aura
299
+ ga, gb = r * 2.5, r * 1.8
300
+ self.cv.coords(self._sprite_glow, sx - ga, sy - ga, sx + ga, sy + ga)
301
+ # Wings that flutter
302
+ wing_offset = 3 * math.sin(self._frame * 0.15)
303
+ self.cv.coords(self._sprite_wing_l, sx - r - 10, sy - wing_offset - 5, sx - r + 5, sy + wing_offset + 8)
304
+ self.cv.coords(self._sprite_wing_r, sx + r - 5, sy - wing_offset - 5, sx + r + 10, sy + wing_offset + 8)
305
+
306
+ def _update_aura(self):
307
+ """Update sprite's glowing aura."""
308
+ sx, sy = self._sprite_position()
309
+ if self._phase == "materialize":
310
+ r = 4.0 + 2.5 * math.sin(self._pt * 0.45)
311
+ ga, gb = r * 2.8, r * 1.6
312
+ else:
313
+ r = 3.0 + 0.6 * math.sin(self._frame * 0.1)
314
+ ga, gb = r * 2.2, r * 1.4
315
+ for item, rad in ((self._aura_a, ga), (self._aura_b, gb)):
316
+ self.cv.coords(item, sx - rad, sy - rad, sx + rad, sy + rad)
317
+
318
+ def _update_shells(self):
319
+ alive = []
320
+ for sh in self._shells:
321
+ if sh.step(self.cv, self._sparks):
322
+ self._explode(sh.x, sh.y, sh.colors)
323
+ self.cv.delete(sh.item)
324
+ else:
325
+ alive.append(sh)
326
+ self._shells = alive
327
+
328
+ def _update_sparks(self):
329
+ self._sparks = [s for s in self._sparks if s.step(self.cv, self._frame)]
330
+
331
+ def _update_flashes(self):
332
+ keep = []
333
+ for f in self._flashes:
334
+ f["t"] += 1
335
+ t = f["t"] / 12.0
336
+ if t >= 1.0:
337
+ if "item" in f:
338
+ self.cv.delete(f["item"])
339
+ continue
340
+ r = 10 + 70 * t
341
+ c = _mix((255, 250, 235), _BG, t)
342
+ if "item" not in f:
343
+ f["item"] = self.cv.create_oval(
344
+ f["x"] - r, f["y"] - r, f["x"] + r, f["y"] + r,
345
+ fill=_hex(c), stipple="gray50", outline="")
346
+ else:
347
+ self.cv.coords(f["item"], f["x"] - r, f["y"] - r,
348
+ f["x"] + r, f["y"] + r)
349
+ self.cv.itemconfigure(f["item"], fill=_hex(c))
350
+ keep.append(f)
351
+ self._flashes = keep
352
+
353
+ def _update_stars(self):
354
+ for item, c in random.sample(self._stars, 6):
355
+ self.cv.itemconfigure(item, fill=_hex(_scale(c, random.uniform(0.3, 1.0))))
356
+
357
+ # ------------------------------------------------------- phase machine
358
+
359
+ def _update_phase(self):
360
+ self._pt += 1
361
+ ph = self._phase
362
+
363
+ if ph == "materialize" and self._pt >= 42:
364
+ self._phase, self._pt = "dance", 0
365
+
366
+ elif ph == "dance" and self._pt >= 26:
367
+ self._summon_no += 1
368
+ self._burst()
369
+ self._phase, self._pt = "burst", 0
370
+
371
+ elif ph == "burst" and not self._shells:
372
+ self._phase, self._pt = "settle", 0
373
+
374
+ elif ph == "settle" and self._pt >= 70:
375
+ if self._summon_no < self._summons:
376
+ self._phase, self._pt = "dance", 0
377
+ else:
378
+ self._phase, self._pt = "finale", 0
379
+ if self.finale:
380
+ self._build_finale_text()
381
+
382
+ elif ph == "finale":
383
+ if self.finale:
384
+ self._update_finale()
385
+ elif self._pt >= 25:
386
+ self._phase, self._pt = "over", 0
387
+
388
+ elif ph == "over" and self._pt >= 10:
389
+ self._close()
390
+
391
+ def _build_finale_text(self):
392
+ """Each letter of "DUTER" gets its own rainbow hue."""
393
+ self._letters = []
394
+ text = "DUTER"
395
+ n = len(text)
396
+ spacing = 56
397
+ x0 = _WIDTH / 2 - spacing * (n - 1) / 2
398
+ for i, ch in enumerate(text):
399
+ if ch == " ":
400
+ continue
401
+ hue = (i / n) * 0.85 # rainbow sweep
402
+ col = tuple(int(v * 255)
403
+ for v in colorsys.hsv_to_rgb(hue, 0.9, 1.0))
404
+ item = self.cv.create_text(
405
+ x0 + i * spacing, _HEIGHT * 0.42, text=ch,
406
+ font=("Georgia", 52, "bold"), fill=_hex(_BG))
407
+ self._letters.append((item, col))
408
+ self._sub = self.cv.create_text(
409
+ _WIDTH / 2, _HEIGHT * 0.42 + 65,
410
+ text="— a magical sprite bringing you joy —",
411
+ font=("Georgia", 14, "italic"), fill=_hex(_BG))
412
+
413
+ def _update_finale(self):
414
+ pt = self._pt
415
+ for _ in range(3): # sky glitter
416
+ self._sparks.append(_Spark(
417
+ self.cv,
418
+ random.uniform(_WIDTH * 0.08, _WIDTH * 0.92),
419
+ random.uniform(_HEIGHT * 0.08, _HEIGHT * 0.55),
420
+ random.uniform(-0.3, 0.3), random.uniform(-0.2, 0.2),
421
+ random.randint(30, 60),
422
+ random.choice(((255, 240, 190), (255, 255, 255), _GOLD)),
423
+ r=random.uniform(1.0, 2.0), grav=0.008, drag=0.99,
424
+ flicker=True,
425
+ ))
426
+ if pt >= 160: # all done
427
+ for item, _col in self._letters:
428
+ self.cv.delete(item)
429
+ self.cv.delete(self._sub)
430
+ self._phase, self._pt = "over", 0
431
+ return
432
+ # letters: staggered fade-in, hold, staggered fade-out
433
+ for i, (item, col) in enumerate(self._letters):
434
+ a_in = max(0.0, min(1.0, (pt - 3 * i) / 42.0))
435
+ a_out = max(0.0, min(1.0, (pt - 120 - 2 * i) / 32.0))
436
+ self.cv.itemconfigure(
437
+ item, fill=_hex(_mix(_BG, col, a_in * (1.0 - a_out))))
438
+ a_sub = max(0.0, min(1.0, pt / 45.0)) * (
439
+ 1.0 - max(0.0, min(1.0, (pt - 120) / 32.0)))
440
+ self.cv.itemconfigure(
441
+ self._sub, fill=_hex(_mix(_BG, (150, 150, 170), a_sub)))
442
+
443
+ # -------------------------------------------------------------- control
444
+
445
+ def _close(self, *_args):
446
+ if self._done:
447
+ return
448
+ self._done = True
449
+ try:
450
+ self.root.destroy()
451
+ except tk.TclError:
452
+ pass
453
+
454
+ def _tick(self):
455
+ if self._done:
456
+ return
457
+ self._frame += 1
458
+ self._update_sprite()
459
+ self._update_aura()
460
+ self._update_shells()
461
+ self._update_sparks()
462
+ self._update_flashes()
463
+ self._update_stars()
464
+ self._update_phase()
465
+ if time.time() - self._t0 > self.duration + 15: # safety net
466
+ self._close()
467
+ return
468
+ self.root.after(_MS, self._tick)
469
+
470
+ def run(self):
471
+ self.root.after(_MS, self._tick)
472
+ self.root.mainloop()
473
+
474
+
475
+ # ----------------------------------------------------------------- public
476
+
477
+ def summon(mood="rainbow", duration=8.0, finale="sparkle"):
478
+ """Summon a sprite: a magical being appears and sparks of joy bloom in the night sky.
479
+
480
+ Parameters
481
+ ----------
482
+ mood : str
483
+ Colour theme: ``"rainbow"`` (default), ``"gold"``, ``"silver"``,
484
+ ``"sunrise"`` or ``"ocean"``.
485
+ duration : float
486
+ Rough length of the show in seconds (>= 2). At least two sparks
487
+ are created; more for longer durations.
488
+ finale : str or None
489
+ ``"sparkle"`` (default) ends with a sky of sparkles and the
490
+ rainbow-coloured words "HAPPINESS"; ``None`` ends right after the
491
+ last burst.
492
+
493
+ Notes
494
+ -----
495
+ Opens a tkinter window and blocks until it closes (automatically at the
496
+ end of the show, or earlier via Esc / closing the window). While open,
497
+ click anywhere to create extra sparks of joy.
498
+ """
499
+ if mood not in MOODS:
500
+ raise ValueError("unknown mood %r; choose from %s"
501
+ % (mood, sorted(MOODS)))
502
+ _Show(mood, float(duration), finale).run()
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: duter
3
+ Version: 0.1.0
4
+ Summary: Summon a sprite: a magical being appears and sparks of joy bloom, ending with HAPPINESS glowing in the sky — one line of magic, zero dependencies, and a little joy.
5
+ Author: halibote
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/halibote-bit/duter
8
+ Keywords: duter,sprite,happiness,joy,animation,tkinter,magic,toy
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Games/Entertainment
15
+ Classifier: Topic :: Multimedia :: Graphics
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # duter ✨
23
+
24
+ Summon a sprite with one line of Python: a magical being appears, dances across a starry sky,
25
+ sparks of joy bloom around it — and at the end, **HAPPINESS**
26
+ appears in rainbow colours, one letter at a time.
27
+
28
+ Run the window whenever you need it: it just might bring you a good mood
29
+ and a little joy. 🌟
30
+
31
+ **Zero third-party dependencies** — pure standard-library `tkinter`.
32
+
33
+ ```python
34
+ import duter
35
+ duter.summon()
36
+ ```
37
+
38
+ While the window is open:
39
+
40
+ - **Click anywhere** — the sprite creates more sparks of joy
41
+ - **Esc** or close the window — end early
42
+
43
+ ## API
44
+
45
+ ```python
46
+ duter.summon(mood="rainbow", duration=8.0, finale="sparkle")
47
+ ```
48
+
49
+ | Parameter | Default | Meaning |
50
+ |------------|-------------|--------------------------------------------------------------|
51
+ | `mood` | `"rainbow"` | Colour theme: `rainbow`, `gold`, `silver`, `sunrise`, `ocean` |
52
+ | `duration` | `8.0` | Rough show length in seconds (≥ 2); more sparks for longer shows |
53
+ | `finale` | `"sparkle"` | Finale: sparkle sky + rainbow "HAPPINESS"; `None` to skip |
54
+
55
+ ## Command line
56
+
57
+ ```console
58
+ python -m duter
59
+ python -m duter --mood sunrise --duration 12
60
+ duter --mood gold --no-finale # after `pip install duter`
61
+ ```
62
+
63
+ ## What happens on screen
64
+
65
+ 1. A tiny sprite materializes from stardust at the bottom of a starry sky
66
+ 2. It begins to dance, leaving trails of sparkling light
67
+ 3. Bursts of joy explode around the sprite with colorful particles
68
+ 4. ~100 particles bloom with gravity, drag, flicker, and colour cooling
69
+ (white-hot → theme colour → dying ember)
70
+ 5. Finale: sparkles fill the sky, and **HAPPINESS** glows in rainbow
71
+ letters before gently fading away, then the window closes itself
72
+
73
+ ## Requirements
74
+
75
+ - Python ≥ 3.8 with `tkinter` (included in the official installers on
76
+ Windows and macOS; on Linux: `sudo apt install python3-tk` or equivalent)
77
+
78
+ ## Development
79
+
80
+ ```console
81
+ pip install build twine
82
+ python -m build
83
+ twine upload dist/*
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ duter/__init__.py
5
+ duter/__main__.py
6
+ duter/core.py
7
+ duter.egg-info/PKG-INFO
8
+ duter.egg-info/SOURCES.txt
9
+ duter.egg-info/dependency_links.txt
10
+ duter.egg-info/entry_points.txt
11
+ duter.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ duter = duter.__main__:main
@@ -0,0 +1 @@
1
+ duter
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "duter"
7
+ version = "0.1.0"
8
+ description = "Summon a sprite: a magical being appears and sparks of joy bloom, ending with HAPPINESS glowing in the sky — one line of magic, zero dependencies, and a little joy."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.8"
13
+ authors = [{ name = "halibote" }]
14
+ keywords = ["duter", "sprite", "happiness", "joy", "animation", "tkinter", "magic", "toy"]
15
+ classifiers = [
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: End Users/Desktop",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Topic :: Games/Entertainment",
22
+ "Topic :: Multimedia :: Graphics",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/halibote-bit/duter"
28
+
29
+ [project.scripts]
30
+ duter = "duter.__main__:main"
31
+
32
+ [tool.setuptools.packages.find]
33
+ include = ["duter*"]
duter-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+