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 +21 -0
- duter-0.1.0/PKG-INFO +88 -0
- duter-0.1.0/README.md +67 -0
- duter-0.1.0/duter/__init__.py +17 -0
- duter-0.1.0/duter/__main__.py +30 -0
- duter-0.1.0/duter/core.py +502 -0
- duter-0.1.0/duter.egg-info/PKG-INFO +88 -0
- duter-0.1.0/duter.egg-info/SOURCES.txt +11 -0
- duter-0.1.0/duter.egg-info/dependency_links.txt +1 -0
- duter-0.1.0/duter.egg-info/entry_points.txt +2 -0
- duter-0.1.0/duter.egg-info/top_level.txt +1 -0
- duter-0.1.0/pyproject.toml +33 -0
- duter-0.1.0/setup.cfg +4 -0
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 @@
|
|
|
1
|
+
|
|
@@ -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