oledgifstudio 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.
- oledgifstudio-0.1.0/LICENSE +21 -0
- oledgifstudio-0.1.0/PKG-INFO +245 -0
- oledgifstudio-0.1.0/README.md +203 -0
- oledgifstudio-0.1.0/oledgif/__init__.py +3 -0
- oledgifstudio-0.1.0/oledgif/__main__.py +3 -0
- oledgifstudio-0.1.0/oledgif/cli.py +176 -0
- oledgifstudio-0.1.0/oledgif/describe.py +60 -0
- oledgifstudio-0.1.0/oledgif/effects.py +321 -0
- oledgifstudio-0.1.0/oledgif/fonts.py +34 -0
- oledgifstudio-0.1.0/oledgif/patterns.py +165 -0
- oledgifstudio-0.1.0/oledgif/presets.py +29 -0
- oledgifstudio-0.1.0/oledgif/render.py +209 -0
- oledgifstudio-0.1.0/oledgifstudio.egg-info/PKG-INFO +245 -0
- oledgifstudio-0.1.0/oledgifstudio.egg-info/SOURCES.txt +18 -0
- oledgifstudio-0.1.0/oledgifstudio.egg-info/dependency_links.txt +1 -0
- oledgifstudio-0.1.0/oledgifstudio.egg-info/entry_points.txt +2 -0
- oledgifstudio-0.1.0/oledgifstudio.egg-info/requires.txt +1 -0
- oledgifstudio-0.1.0/oledgifstudio.egg-info/top_level.txt +1 -0
- oledgifstudio-0.1.0/pyproject.toml +34 -0
- oledgifstudio-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MaticeMrll
|
|
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.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oledgifstudio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Générateur de GIFs animés 1 bit pour écrans OLED (SteelSeries, SSD1306, ...)
|
|
5
|
+
Author-email: MaticeMrll <marill.matice@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 MaticeMrll
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/MaticeMrll/oled-gif-studio
|
|
29
|
+
Project-URL: Repository, https://github.com/MaticeMrll/oled-gif-studio
|
|
30
|
+
Project-URL: Issues, https://github.com/MaticeMrll/oled-gif-studio/issues
|
|
31
|
+
Keywords: oled,gif,steelseries,ssd1306,keyboard,pillow
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
36
|
+
Classifier: Environment :: Console
|
|
37
|
+
Requires-Python: >=3.10
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: Pillow>=9.0
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# oled-gif-studio
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<img src="docs/hero.gif" width="384" alt="OLED GIF STUDIO qui défile sur un écran 128x40">
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
Générateur de GIFs animés **1 bit** pour les petits écrans OLED : claviers/souris
|
|
50
|
+
SteelSeries, modules SSD1306/SH1106 (Arduino, Raspberry Pi, macropads QMK)...
|
|
51
|
+
|
|
52
|
+
Pas de modèle IA, pas d'API payante : tout est **procédural** (Python + Pillow),
|
|
53
|
+
donc instantané, gratuit et pixel-perfect. Un mode « description en langage
|
|
54
|
+
naturel » (français ou anglais) choisit l'effet et les paramètres pour toi.
|
|
55
|
+
|
|
56
|
+
> Tous les aperçus de ce README sont agrandis ×3 — la taille réelle des GIFs
|
|
57
|
+
> est celle de l'écran cible (128×40 par défaut).
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
Aucune, si Python (≥ 3.10) + Pillow sont déjà installés. Sinon :
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
pip install Pillow
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Optionnel, pour avoir la commande `oledgif` partout :
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
pip install -e .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage rapide
|
|
74
|
+
|
|
75
|
+
```powershell
|
|
76
|
+
# Depuis le dossier du projet :
|
|
77
|
+
python -m oledgif "HELLO WORLD" # effet auto → hello.gif
|
|
78
|
+
python -m oledgif "GG" -e slot -o gg.gif # machine à sous
|
|
79
|
+
python -m oledgif "PWNED" -e glitch -p rival # pour l'OLED d'une souris
|
|
80
|
+
python -m oledgif "42" -e matrix --size 128x64 --fps 20 # taille custom
|
|
81
|
+
|
|
82
|
+
# En langage naturel :
|
|
83
|
+
python -m oledgif -d "le texte 'BONJOUR' défile lentement"
|
|
84
|
+
python -m oledgif -d "'GAME OVER' qui clignote vite pendant 3s"
|
|
85
|
+
python -m oledgif -d "un radar qui balaie l'écran"
|
|
86
|
+
|
|
87
|
+
# À partir d'une image :
|
|
88
|
+
python -m oledgif -i photo.jpg --fit cover # photo plein écran, effet vhs
|
|
89
|
+
python -m oledgif -i comic.jpg --fit cover --style comic # illustration/BD
|
|
90
|
+
python -m oledgif -i logo.png -e bounce # le logo rebondit
|
|
91
|
+
python -m oledgif -i meme.gif # GIF animé converti tel quel
|
|
92
|
+
|
|
93
|
+
# Motifs sans texte (écrans de veille) :
|
|
94
|
+
python -m oledgif -e starfield
|
|
95
|
+
python -m oledgif -e plasma --seconds 6
|
|
96
|
+
|
|
97
|
+
# Un exemple de chaque effet dans ./samples :
|
|
98
|
+
python -m oledgif --demo
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Effets texte / image (`--list-effects`)
|
|
102
|
+
|
|
103
|
+
<table>
|
|
104
|
+
<tr>
|
|
105
|
+
<td align="center"><img src="docs/scroll.gif" alt="scroll"><br><code>scroll</code> — défilement en boucle parfaite</td>
|
|
106
|
+
<td align="center"><img src="docs/typewriter.gif" alt="typewriter"><br><code>typewriter</code> — machine à écrire avec curseur</td>
|
|
107
|
+
</tr>
|
|
108
|
+
<tr>
|
|
109
|
+
<td align="center"><img src="docs/wave.gif" alt="wave"><br><code>wave</code> — lettres en vague</td>
|
|
110
|
+
<td align="center"><img src="docs/blink.gif" alt="blink"><br><code>blink</code> — clignotement</td>
|
|
111
|
+
</tr>
|
|
112
|
+
<tr>
|
|
113
|
+
<td align="center"><img src="docs/bounce.gif" alt="bounce"><br><code>bounce</code> — rebond façon logo DVD</td>
|
|
114
|
+
<td align="center"><img src="docs/matrix.gif" alt="matrix"><br><code>matrix</code> — pluie de caractères qui révèle le texte</td>
|
|
115
|
+
</tr>
|
|
116
|
+
<tr>
|
|
117
|
+
<td align="center"><img src="docs/slot.gif" alt="slot"><br><code>slot</code> — chaque lettre cycle puis se fige</td>
|
|
118
|
+
<td align="center"><img src="docs/glitch.gif" alt="glitch"><br><code>glitch</code> — bandes décalées + bruit</td>
|
|
119
|
+
</tr>
|
|
120
|
+
<tr>
|
|
121
|
+
<td align="center"><img src="docs/pulse.gif" alt="pulse"><br><code>pulse</code> — battement de cœur</td>
|
|
122
|
+
<td align="center"><img src="docs/vhs.gif" alt="vhs"><br><code>vhs</code> — tracking façon cassette vidéo</td>
|
|
123
|
+
</tr>
|
|
124
|
+
<tr>
|
|
125
|
+
<td align="center"><img src="docs/slide.gif" alt="slide"><br><code>slide</code> — générique bas → haut</td>
|
|
126
|
+
<td></td>
|
|
127
|
+
</tr>
|
|
128
|
+
</table>
|
|
129
|
+
|
|
130
|
+
`typewriter`, `wave`, `matrix` et `slot` sont réservés au texte ; les autres
|
|
131
|
+
acceptent aussi une image (`--image`).
|
|
132
|
+
|
|
133
|
+
`--effect auto` (défaut) : `scroll` si le texte est trop large pour l'écran,
|
|
134
|
+
sinon `wave` ; `vhs` pour une image ; conversion directe pour un GIF animé.
|
|
135
|
+
|
|
136
|
+
## Motifs sans texte (écrans de veille)
|
|
137
|
+
|
|
138
|
+
<table>
|
|
139
|
+
<tr>
|
|
140
|
+
<td align="center"><img src="docs/starfield.gif" alt="starfield"><br><code>starfield</code> — hyperespace</td>
|
|
141
|
+
<td align="center"><img src="docs/plasma.gif" alt="plasma"><br><code>plasma</code> — plasma rétro tramé</td>
|
|
142
|
+
</tr>
|
|
143
|
+
<tr>
|
|
144
|
+
<td align="center"><img src="docs/life.gif" alt="life"><br><code>life</code> — jeu de la vie de Conway</td>
|
|
145
|
+
<td align="center"><img src="docs/eq.gif" alt="eq"><br><code>eq</code> — égaliseur audio</td>
|
|
146
|
+
</tr>
|
|
147
|
+
<tr>
|
|
148
|
+
<td align="center"><img src="docs/scope.gif" alt="scope"><br><code>scope</code> — oscilloscope</td>
|
|
149
|
+
<td align="center"><img src="docs/radar.gif" alt="radar"><br><code>radar</code> — balayage avec échos</td>
|
|
150
|
+
</tr>
|
|
151
|
+
</table>
|
|
152
|
+
|
|
153
|
+
## Images : les 4 styles de rendu
|
|
154
|
+
|
|
155
|
+
Convertir une image couleur en 1 bit sur 5120 pixels est un exercice de
|
|
156
|
+
sacrifice — le bon `--style` dépend de la source. Démonstration sur la même
|
|
157
|
+
scène (lune, silhouettes sur ciel en dégradé, lumières de ville) :
|
|
158
|
+
|
|
159
|
+
| Rendu | Style |
|
|
160
|
+
|---|---|
|
|
161
|
+
| <img src="docs/style_source.png" alt="source" width="288"> | **Source** (niveaux de gris) |
|
|
162
|
+
| <img src="docs/style_photo.png" alt="photo" width="288"> | `--style photo` (défaut) — auto-contraste + netteté + trame Floyd-Steinberg. Le bon choix pour les **photos réelles** : la trame simule les niveaux de gris. Sur une illustration, elle devient du bruit. |
|
|
163
|
+
| <img src="docs/style_solid.png" alt="solid" width="288"> | `--style solid` — seuillage net (alias `--no-dither`). Parfait pour les **logos** et dessins au trait… mais tout ce qui est sombre-sur-sombre disparaît dans le noir. |
|
|
164
|
+
| <img src="docs/style_comic.png" alt="comic" width="288"> | `--style comic` — comme `solid`, mais un 2ᵉ seuil automatique (Otsu) sépare les tons sombres et retrace en **contour blanc** les formes noyées dans le noir. Idéal pour les **illustrations/BD**. |
|
|
165
|
+
| <img src="docs/style_edges.png" alt="edges" width="288"> | `--style edges` — détection de contours, traits blancs sur fond noir. Look néon/filaire, souvent le plus lisible pour un **paysage ou un visage**. |
|
|
166
|
+
|
|
167
|
+
Autres options d'image :
|
|
168
|
+
|
|
169
|
+
- `--fit cover` — l'image remplit tout l'écran (recadrée) au lieu d'être
|
|
170
|
+
réduite à un timbre-poste au milieu. Recommandé pour les photos paysage.
|
|
171
|
+
- **Dé-bruitage** (actif par défaut) : un filtre médian + une suppression des
|
|
172
|
+
pixels isolés éliminent le « poivre et sel » (reflets, lampadaires, étoiles).
|
|
173
|
+
`--no-denoise` le désactive si tu veux justement garder ces points.
|
|
174
|
+
- Un **GIF animé** en entrée est converti frame par frame en conservant les
|
|
175
|
+
durées d'origine (avec `--effect auto`).
|
|
176
|
+
|
|
177
|
+
## Langage naturel (`--describe`)
|
|
178
|
+
|
|
179
|
+
```powershell
|
|
180
|
+
python -m oledgif -d "le texte 'BRB' qui rebondit pendant 5 secondes"
|
|
181
|
+
python -m oledgif -d "'REC' en mode vhs vintage"
|
|
182
|
+
python -m oledgif -d "a fast blinking 'GO' for 3 seconds"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Le parseur (FR/EN, insensible aux accents) reconnaît l'effet par mots-clés
|
|
186
|
+
(« défile », « clignote », « tape », « radar », « vintage »…), le texte entre
|
|
187
|
+
guillemets, la durée (« pendant 3s ») et la vitesse (« lentement », « vite »).
|
|
188
|
+
|
|
189
|
+
## Écrans préconfigurés (`--list-presets`)
|
|
190
|
+
|
|
191
|
+
| Preset | Taille | Matériel |
|
|
192
|
+
|---------------|---------|----------|
|
|
193
|
+
| `apex` (défaut) | 128×40 | SteelSeries Apex 5 / 7 / Pro (OLED clavier) |
|
|
194
|
+
| `rival` | 128×36 | SteelSeries Rival 700 / 710 (OLED souris) |
|
|
195
|
+
| `oled-128x64` | 128×64 | SSD1306 / SH1106 / SSD1309 |
|
|
196
|
+
| `oled-128x32` | 128×32 | SSD1306 |
|
|
197
|
+
| `oled-96x16` | 96×16 | SSD1306 |
|
|
198
|
+
| `oled-256x64` | 256×64 | SSD1322 |
|
|
199
|
+
|
|
200
|
+
N'importe quelle autre taille : `--size LARGEURxHAUTEUR`.
|
|
201
|
+
|
|
202
|
+
## Options utiles
|
|
203
|
+
|
|
204
|
+
- `--charset alnum|digits|upper|letters|ascii` ou une chaîne littérale — le
|
|
205
|
+
jeu de caractères utilisé par `matrix` et `slot` (défaut : les 62
|
|
206
|
+
alphanumériques `0-9A-Za-z`).
|
|
207
|
+
- `--font chemin.ttf --font-size N` — police custom (défaut : Consolas,
|
|
208
|
+
taille ajustée à l'écran).
|
|
209
|
+
- `--invert` — noir sur blanc.
|
|
210
|
+
- `--scale 4` — GIF agrandi ×4 (pour prévisualiser confortablement).
|
|
211
|
+
- `--seed 42` — rendu reproductible pour les effets aléatoires.
|
|
212
|
+
- `--fps`, `--seconds`, `--speed` (px/s pour scroll).
|
|
213
|
+
|
|
214
|
+
## Envoyer le GIF sur l'écran SteelSeries
|
|
215
|
+
|
|
216
|
+
Deux options :
|
|
217
|
+
|
|
218
|
+
1. **SteelSeries GG** : dans les réglages de ton clavier (section OLED),
|
|
219
|
+
tu peux importer une image/GIF 128×40 — les GIFs générés ici sont au bon
|
|
220
|
+
format (1 bit, taille exacte).
|
|
221
|
+
2. **GameSense API** (programmatique) : comme le fait
|
|
222
|
+
[SteelseriesAnimGif](https://github.com/bolner/SteelseriesAnimGif), on peut
|
|
223
|
+
streamer les frames vers l'écran via l'API locale de SteelSeries GG.
|
|
224
|
+
Les GIFs produits ici sont directement exploitables frame par frame.
|
|
225
|
+
|
|
226
|
+
## Structure du projet
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
oledgif/
|
|
230
|
+
cli.py # ligne de commande + make_gif()
|
|
231
|
+
effects.py # les 11 effets texte/image (registre @effect)
|
|
232
|
+
patterns.py # les 6 motifs sans texte
|
|
233
|
+
render.py # préparation d'images, binarisation, écriture GIF
|
|
234
|
+
describe.py # parseur langage naturel FR/EN
|
|
235
|
+
presets.py # tailles d'écrans du marché
|
|
236
|
+
fonts.py # chargement de police
|
|
237
|
+
samples/ # un GIF d'exemple par effet (taille réelle, --demo)
|
|
238
|
+
docs/ # illustrations du README (agrandies ×3)
|
|
239
|
+
```
|
|
240
|
+
---
|
|
241
|
+
## Mon choix
|
|
242
|
+
|
|
243
|
+
<img src="docs/keyboard.jpg" alt="source" width="288">
|
|
244
|
+
|
|
245
|
+
Ninja Turtles !
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# oled-gif-studio
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="docs/hero.gif" width="384" alt="OLED GIF STUDIO qui défile sur un écran 128x40">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Générateur de GIFs animés **1 bit** pour les petits écrans OLED : claviers/souris
|
|
8
|
+
SteelSeries, modules SSD1306/SH1106 (Arduino, Raspberry Pi, macropads QMK)...
|
|
9
|
+
|
|
10
|
+
Pas de modèle IA, pas d'API payante : tout est **procédural** (Python + Pillow),
|
|
11
|
+
donc instantané, gratuit et pixel-perfect. Un mode « description en langage
|
|
12
|
+
naturel » (français ou anglais) choisit l'effet et les paramètres pour toi.
|
|
13
|
+
|
|
14
|
+
> Tous les aperçus de ce README sont agrandis ×3 — la taille réelle des GIFs
|
|
15
|
+
> est celle de l'écran cible (128×40 par défaut).
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Aucune, si Python (≥ 3.10) + Pillow sont déjà installés. Sinon :
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pip install Pillow
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Optionnel, pour avoir la commande `oledgif` partout :
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage rapide
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
# Depuis le dossier du projet :
|
|
35
|
+
python -m oledgif "HELLO WORLD" # effet auto → hello.gif
|
|
36
|
+
python -m oledgif "GG" -e slot -o gg.gif # machine à sous
|
|
37
|
+
python -m oledgif "PWNED" -e glitch -p rival # pour l'OLED d'une souris
|
|
38
|
+
python -m oledgif "42" -e matrix --size 128x64 --fps 20 # taille custom
|
|
39
|
+
|
|
40
|
+
# En langage naturel :
|
|
41
|
+
python -m oledgif -d "le texte 'BONJOUR' défile lentement"
|
|
42
|
+
python -m oledgif -d "'GAME OVER' qui clignote vite pendant 3s"
|
|
43
|
+
python -m oledgif -d "un radar qui balaie l'écran"
|
|
44
|
+
|
|
45
|
+
# À partir d'une image :
|
|
46
|
+
python -m oledgif -i photo.jpg --fit cover # photo plein écran, effet vhs
|
|
47
|
+
python -m oledgif -i comic.jpg --fit cover --style comic # illustration/BD
|
|
48
|
+
python -m oledgif -i logo.png -e bounce # le logo rebondit
|
|
49
|
+
python -m oledgif -i meme.gif # GIF animé converti tel quel
|
|
50
|
+
|
|
51
|
+
# Motifs sans texte (écrans de veille) :
|
|
52
|
+
python -m oledgif -e starfield
|
|
53
|
+
python -m oledgif -e plasma --seconds 6
|
|
54
|
+
|
|
55
|
+
# Un exemple de chaque effet dans ./samples :
|
|
56
|
+
python -m oledgif --demo
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Effets texte / image (`--list-effects`)
|
|
60
|
+
|
|
61
|
+
<table>
|
|
62
|
+
<tr>
|
|
63
|
+
<td align="center"><img src="docs/scroll.gif" alt="scroll"><br><code>scroll</code> — défilement en boucle parfaite</td>
|
|
64
|
+
<td align="center"><img src="docs/typewriter.gif" alt="typewriter"><br><code>typewriter</code> — machine à écrire avec curseur</td>
|
|
65
|
+
</tr>
|
|
66
|
+
<tr>
|
|
67
|
+
<td align="center"><img src="docs/wave.gif" alt="wave"><br><code>wave</code> — lettres en vague</td>
|
|
68
|
+
<td align="center"><img src="docs/blink.gif" alt="blink"><br><code>blink</code> — clignotement</td>
|
|
69
|
+
</tr>
|
|
70
|
+
<tr>
|
|
71
|
+
<td align="center"><img src="docs/bounce.gif" alt="bounce"><br><code>bounce</code> — rebond façon logo DVD</td>
|
|
72
|
+
<td align="center"><img src="docs/matrix.gif" alt="matrix"><br><code>matrix</code> — pluie de caractères qui révèle le texte</td>
|
|
73
|
+
</tr>
|
|
74
|
+
<tr>
|
|
75
|
+
<td align="center"><img src="docs/slot.gif" alt="slot"><br><code>slot</code> — chaque lettre cycle puis se fige</td>
|
|
76
|
+
<td align="center"><img src="docs/glitch.gif" alt="glitch"><br><code>glitch</code> — bandes décalées + bruit</td>
|
|
77
|
+
</tr>
|
|
78
|
+
<tr>
|
|
79
|
+
<td align="center"><img src="docs/pulse.gif" alt="pulse"><br><code>pulse</code> — battement de cœur</td>
|
|
80
|
+
<td align="center"><img src="docs/vhs.gif" alt="vhs"><br><code>vhs</code> — tracking façon cassette vidéo</td>
|
|
81
|
+
</tr>
|
|
82
|
+
<tr>
|
|
83
|
+
<td align="center"><img src="docs/slide.gif" alt="slide"><br><code>slide</code> — générique bas → haut</td>
|
|
84
|
+
<td></td>
|
|
85
|
+
</tr>
|
|
86
|
+
</table>
|
|
87
|
+
|
|
88
|
+
`typewriter`, `wave`, `matrix` et `slot` sont réservés au texte ; les autres
|
|
89
|
+
acceptent aussi une image (`--image`).
|
|
90
|
+
|
|
91
|
+
`--effect auto` (défaut) : `scroll` si le texte est trop large pour l'écran,
|
|
92
|
+
sinon `wave` ; `vhs` pour une image ; conversion directe pour un GIF animé.
|
|
93
|
+
|
|
94
|
+
## Motifs sans texte (écrans de veille)
|
|
95
|
+
|
|
96
|
+
<table>
|
|
97
|
+
<tr>
|
|
98
|
+
<td align="center"><img src="docs/starfield.gif" alt="starfield"><br><code>starfield</code> — hyperespace</td>
|
|
99
|
+
<td align="center"><img src="docs/plasma.gif" alt="plasma"><br><code>plasma</code> — plasma rétro tramé</td>
|
|
100
|
+
</tr>
|
|
101
|
+
<tr>
|
|
102
|
+
<td align="center"><img src="docs/life.gif" alt="life"><br><code>life</code> — jeu de la vie de Conway</td>
|
|
103
|
+
<td align="center"><img src="docs/eq.gif" alt="eq"><br><code>eq</code> — égaliseur audio</td>
|
|
104
|
+
</tr>
|
|
105
|
+
<tr>
|
|
106
|
+
<td align="center"><img src="docs/scope.gif" alt="scope"><br><code>scope</code> — oscilloscope</td>
|
|
107
|
+
<td align="center"><img src="docs/radar.gif" alt="radar"><br><code>radar</code> — balayage avec échos</td>
|
|
108
|
+
</tr>
|
|
109
|
+
</table>
|
|
110
|
+
|
|
111
|
+
## Images : les 4 styles de rendu
|
|
112
|
+
|
|
113
|
+
Convertir une image couleur en 1 bit sur 5120 pixels est un exercice de
|
|
114
|
+
sacrifice — le bon `--style` dépend de la source. Démonstration sur la même
|
|
115
|
+
scène (lune, silhouettes sur ciel en dégradé, lumières de ville) :
|
|
116
|
+
|
|
117
|
+
| Rendu | Style |
|
|
118
|
+
|---|---|
|
|
119
|
+
| <img src="docs/style_source.png" alt="source" width="288"> | **Source** (niveaux de gris) |
|
|
120
|
+
| <img src="docs/style_photo.png" alt="photo" width="288"> | `--style photo` (défaut) — auto-contraste + netteté + trame Floyd-Steinberg. Le bon choix pour les **photos réelles** : la trame simule les niveaux de gris. Sur une illustration, elle devient du bruit. |
|
|
121
|
+
| <img src="docs/style_solid.png" alt="solid" width="288"> | `--style solid` — seuillage net (alias `--no-dither`). Parfait pour les **logos** et dessins au trait… mais tout ce qui est sombre-sur-sombre disparaît dans le noir. |
|
|
122
|
+
| <img src="docs/style_comic.png" alt="comic" width="288"> | `--style comic` — comme `solid`, mais un 2ᵉ seuil automatique (Otsu) sépare les tons sombres et retrace en **contour blanc** les formes noyées dans le noir. Idéal pour les **illustrations/BD**. |
|
|
123
|
+
| <img src="docs/style_edges.png" alt="edges" width="288"> | `--style edges` — détection de contours, traits blancs sur fond noir. Look néon/filaire, souvent le plus lisible pour un **paysage ou un visage**. |
|
|
124
|
+
|
|
125
|
+
Autres options d'image :
|
|
126
|
+
|
|
127
|
+
- `--fit cover` — l'image remplit tout l'écran (recadrée) au lieu d'être
|
|
128
|
+
réduite à un timbre-poste au milieu. Recommandé pour les photos paysage.
|
|
129
|
+
- **Dé-bruitage** (actif par défaut) : un filtre médian + une suppression des
|
|
130
|
+
pixels isolés éliminent le « poivre et sel » (reflets, lampadaires, étoiles).
|
|
131
|
+
`--no-denoise` le désactive si tu veux justement garder ces points.
|
|
132
|
+
- Un **GIF animé** en entrée est converti frame par frame en conservant les
|
|
133
|
+
durées d'origine (avec `--effect auto`).
|
|
134
|
+
|
|
135
|
+
## Langage naturel (`--describe`)
|
|
136
|
+
|
|
137
|
+
```powershell
|
|
138
|
+
python -m oledgif -d "le texte 'BRB' qui rebondit pendant 5 secondes"
|
|
139
|
+
python -m oledgif -d "'REC' en mode vhs vintage"
|
|
140
|
+
python -m oledgif -d "a fast blinking 'GO' for 3 seconds"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Le parseur (FR/EN, insensible aux accents) reconnaît l'effet par mots-clés
|
|
144
|
+
(« défile », « clignote », « tape », « radar », « vintage »…), le texte entre
|
|
145
|
+
guillemets, la durée (« pendant 3s ») et la vitesse (« lentement », « vite »).
|
|
146
|
+
|
|
147
|
+
## Écrans préconfigurés (`--list-presets`)
|
|
148
|
+
|
|
149
|
+
| Preset | Taille | Matériel |
|
|
150
|
+
|---------------|---------|----------|
|
|
151
|
+
| `apex` (défaut) | 128×40 | SteelSeries Apex 5 / 7 / Pro (OLED clavier) |
|
|
152
|
+
| `rival` | 128×36 | SteelSeries Rival 700 / 710 (OLED souris) |
|
|
153
|
+
| `oled-128x64` | 128×64 | SSD1306 / SH1106 / SSD1309 |
|
|
154
|
+
| `oled-128x32` | 128×32 | SSD1306 |
|
|
155
|
+
| `oled-96x16` | 96×16 | SSD1306 |
|
|
156
|
+
| `oled-256x64` | 256×64 | SSD1322 |
|
|
157
|
+
|
|
158
|
+
N'importe quelle autre taille : `--size LARGEURxHAUTEUR`.
|
|
159
|
+
|
|
160
|
+
## Options utiles
|
|
161
|
+
|
|
162
|
+
- `--charset alnum|digits|upper|letters|ascii` ou une chaîne littérale — le
|
|
163
|
+
jeu de caractères utilisé par `matrix` et `slot` (défaut : les 62
|
|
164
|
+
alphanumériques `0-9A-Za-z`).
|
|
165
|
+
- `--font chemin.ttf --font-size N` — police custom (défaut : Consolas,
|
|
166
|
+
taille ajustée à l'écran).
|
|
167
|
+
- `--invert` — noir sur blanc.
|
|
168
|
+
- `--scale 4` — GIF agrandi ×4 (pour prévisualiser confortablement).
|
|
169
|
+
- `--seed 42` — rendu reproductible pour les effets aléatoires.
|
|
170
|
+
- `--fps`, `--seconds`, `--speed` (px/s pour scroll).
|
|
171
|
+
|
|
172
|
+
## Envoyer le GIF sur l'écran SteelSeries
|
|
173
|
+
|
|
174
|
+
Deux options :
|
|
175
|
+
|
|
176
|
+
1. **SteelSeries GG** : dans les réglages de ton clavier (section OLED),
|
|
177
|
+
tu peux importer une image/GIF 128×40 — les GIFs générés ici sont au bon
|
|
178
|
+
format (1 bit, taille exacte).
|
|
179
|
+
2. **GameSense API** (programmatique) : comme le fait
|
|
180
|
+
[SteelseriesAnimGif](https://github.com/bolner/SteelseriesAnimGif), on peut
|
|
181
|
+
streamer les frames vers l'écran via l'API locale de SteelSeries GG.
|
|
182
|
+
Les GIFs produits ici sont directement exploitables frame par frame.
|
|
183
|
+
|
|
184
|
+
## Structure du projet
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
oledgif/
|
|
188
|
+
cli.py # ligne de commande + make_gif()
|
|
189
|
+
effects.py # les 11 effets texte/image (registre @effect)
|
|
190
|
+
patterns.py # les 6 motifs sans texte
|
|
191
|
+
render.py # préparation d'images, binarisation, écriture GIF
|
|
192
|
+
describe.py # parseur langage naturel FR/EN
|
|
193
|
+
presets.py # tailles d'écrans du marché
|
|
194
|
+
fonts.py # chargement de police
|
|
195
|
+
samples/ # un GIF d'exemple par effet (taille réelle, --demo)
|
|
196
|
+
docs/ # illustrations du README (agrandies ×3)
|
|
197
|
+
```
|
|
198
|
+
---
|
|
199
|
+
## Mon choix
|
|
200
|
+
|
|
201
|
+
<img src="docs/keyboard.jpg" alt="source" width="288">
|
|
202
|
+
|
|
203
|
+
Ninja Turtles !
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""CLI : python -m oledgif "TEXTE" [options]"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import string
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .describe import parse as parse_description
|
|
11
|
+
from .effects import ALNUM, DESCRIPTIONS, EFFECTS, Ctx
|
|
12
|
+
from .fonts import load_font
|
|
13
|
+
from .patterns import PATTERNS
|
|
14
|
+
from .presets import DEFAULT_PRESET, PRESETS, resolve_size
|
|
15
|
+
from .render import convert_animation, load_source, save_gif
|
|
16
|
+
|
|
17
|
+
from PIL import Image
|
|
18
|
+
|
|
19
|
+
CHARSETS = {
|
|
20
|
+
"alnum": ALNUM,
|
|
21
|
+
"digits": string.digits,
|
|
22
|
+
"upper": string.digits + string.ascii_uppercase,
|
|
23
|
+
"letters": string.ascii_letters,
|
|
24
|
+
"ascii": "".join(c for c in string.printable if c.isprintable() and c != " "),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_parser():
|
|
29
|
+
p = argparse.ArgumentParser(
|
|
30
|
+
prog="oledgif",
|
|
31
|
+
description="Génère des GIFs animés 1 bit pour écrans OLED (SteelSeries, SSD1306...).",
|
|
32
|
+
)
|
|
33
|
+
p.add_argument("text", nargs="?", help="texte à animer (optionnel avec --image ou un motif)")
|
|
34
|
+
p.add_argument("-d", "--describe", metavar="PHRASE",
|
|
35
|
+
help="décrire l'animation en langage naturel, ex: 'GG qui clignote vite'")
|
|
36
|
+
p.add_argument("-i", "--image", metavar="FICHIER",
|
|
37
|
+
help="image source (PNG/JPG/GIF...) : convertie en 1 bit avec tramage ; "
|
|
38
|
+
"un GIF animé est converti tel quel si --effect vaut auto/convert")
|
|
39
|
+
p.add_argument("--style", default="photo", choices=["photo", "solid", "comic", "edges"],
|
|
40
|
+
help="rendu de --image : photo = contraste+netteté+tramage (défaut), "
|
|
41
|
+
"solid = seuillage net (logos), comic = solid + contours blancs "
|
|
42
|
+
"des formes perdues dans le noir (illustrations), "
|
|
43
|
+
"edges = contours blancs sur noir")
|
|
44
|
+
p.add_argument("--fit", default="contain", choices=["contain", "cover"],
|
|
45
|
+
help="contain = image entière (défaut), cover = remplit l'écran en "
|
|
46
|
+
"recadrant — recommandé pour les photos paysage")
|
|
47
|
+
p.add_argument("--no-dither", action="store_true",
|
|
48
|
+
help="alias de --style solid")
|
|
49
|
+
p.add_argument("--no-denoise", action="store_true",
|
|
50
|
+
help="garde le bruit : désactive le filtre médian et la suppression "
|
|
51
|
+
"des pixels isolés (reflets, sources de lumière ponctuelles)")
|
|
52
|
+
p.add_argument("-o", "--out", default=None, help="fichier de sortie (.gif)")
|
|
53
|
+
p.add_argument("-p", "--preset", default=None,
|
|
54
|
+
help=f"écran cible (défaut: {DEFAULT_PRESET}) — voir --list-presets")
|
|
55
|
+
p.add_argument("--size", default=None, metavar="WxH", help="taille custom, ex: 128x40")
|
|
56
|
+
p.add_argument("-e", "--effect", default="auto",
|
|
57
|
+
help="effet (défaut: auto) — voir --list-effects")
|
|
58
|
+
p.add_argument("--fps", type=int, default=15)
|
|
59
|
+
p.add_argument("--seconds", type=float, default=4.0, help="durée cible (certains effets la déduisent)")
|
|
60
|
+
p.add_argument("--speed", type=float, default=None, help="vitesse de défilement en px/s (scroll)")
|
|
61
|
+
p.add_argument("--font", default=None, help="chemin d'une police .ttf")
|
|
62
|
+
p.add_argument("--font-size", type=int, default=None)
|
|
63
|
+
p.add_argument("--charset", default="alnum",
|
|
64
|
+
help="alnum|digits|upper|letters|ascii ou chaîne littérale (matrix/slot)")
|
|
65
|
+
p.add_argument("--invert", action="store_true", help="noir sur blanc")
|
|
66
|
+
p.add_argument("--scale", type=int, default=1, help="agrandit le GIF xN (aperçu)")
|
|
67
|
+
p.add_argument("--seed", type=int, default=None, help="graine aléatoire (rendu reproductible)")
|
|
68
|
+
p.add_argument("--demo", action="store_true", help="génère un exemple de chaque effet dans ./samples")
|
|
69
|
+
p.add_argument("--list-effects", action="store_true")
|
|
70
|
+
p.add_argument("--list-presets", action="store_true")
|
|
71
|
+
p.add_argument("--version", action="version", version=f"oledgif {__version__}")
|
|
72
|
+
return p
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def make_gif(text, out, *, preset=None, size=None, effect="auto", fps=15, seconds=4.0,
|
|
76
|
+
speed=None, font_path=None, font_size=None, charset=ALNUM,
|
|
77
|
+
invert=False, scale=1, seed=None, image=None, style="photo", fit="contain",
|
|
78
|
+
denoise=True):
|
|
79
|
+
W, H = resolve_size(preset, size)
|
|
80
|
+
font = load_font(font_path, font_size, screen_h=H)
|
|
81
|
+
|
|
82
|
+
src = None
|
|
83
|
+
if image:
|
|
84
|
+
im = Image.open(image)
|
|
85
|
+
if getattr(im, "is_animated", False) and effect in ("auto", "convert"):
|
|
86
|
+
# GIF animé -> conversion frame par frame, durées d'origine conservées
|
|
87
|
+
frames, durations = convert_animation(image, W, H, style, fit, denoise)
|
|
88
|
+
n, duration = save_gif(frames, out, fps=fps, invert=invert,
|
|
89
|
+
scale=scale, durations=durations)
|
|
90
|
+
return out, "convert", W, H, n, duration
|
|
91
|
+
im.close()
|
|
92
|
+
src = load_source(image, W, H, style, fit, denoise)
|
|
93
|
+
if effect in ("auto", "convert"):
|
|
94
|
+
effect = "vhs"
|
|
95
|
+
|
|
96
|
+
if effect == "auto":
|
|
97
|
+
try:
|
|
98
|
+
wide = font.getlength(text) > W - 4
|
|
99
|
+
except AttributeError:
|
|
100
|
+
wide = len(text) * 7 > W - 4
|
|
101
|
+
effect = "scroll" if wide else "wave"
|
|
102
|
+
if effect not in EFFECTS:
|
|
103
|
+
raise SystemExit(f"Effet inconnu: {effect!r}. Effets: {', '.join(EFFECTS)}")
|
|
104
|
+
|
|
105
|
+
ctx = Ctx(text=text or "", W=W, H=H, fps=fps, seconds=seconds, font=font,
|
|
106
|
+
font_path=font_path, font_size=font_size, charset=charset,
|
|
107
|
+
speed=speed, src=src, rng=random.Random(seed))
|
|
108
|
+
frames = EFFECTS[effect](ctx)
|
|
109
|
+
n, duration = save_gif(frames, out, fps=fps, invert=invert, scale=scale)
|
|
110
|
+
return out, effect, W, H, n, duration
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv=None):
|
|
114
|
+
args = build_parser().parse_args(argv)
|
|
115
|
+
|
|
116
|
+
if args.list_effects:
|
|
117
|
+
for name, desc in DESCRIPTIONS.items():
|
|
118
|
+
print(f" {name:<11} {desc}")
|
|
119
|
+
return 0
|
|
120
|
+
if args.list_presets:
|
|
121
|
+
for name, (w, h, desc) in PRESETS.items():
|
|
122
|
+
print(f" {name:<12} {w}x{h:<4} {desc}")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
charset = CHARSETS.get(args.charset, args.charset)
|
|
126
|
+
|
|
127
|
+
if args.demo:
|
|
128
|
+
outdir = "samples"
|
|
129
|
+
os.makedirs(outdir, exist_ok=True)
|
|
130
|
+
text = args.text or "GG WP 42"
|
|
131
|
+
for name in EFFECTS:
|
|
132
|
+
out = os.path.join(outdir, f"{name}.gif")
|
|
133
|
+
make_gif(text, out, preset=args.preset, size=args.size, effect=name,
|
|
134
|
+
fps=args.fps, seconds=args.seconds, speed=args.speed,
|
|
135
|
+
font_path=args.font, font_size=args.font_size, charset=charset,
|
|
136
|
+
invert=args.invert, scale=args.scale, seed=args.seed or 42)
|
|
137
|
+
print(f" {out}")
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
text, effect, fps, seconds = args.text, args.effect, args.fps, args.seconds
|
|
141
|
+
if args.describe:
|
|
142
|
+
parsed = parse_description(args.describe)
|
|
143
|
+
text = parsed.get("text", text)
|
|
144
|
+
if effect == "auto":
|
|
145
|
+
effect = parsed.get("effect", "auto")
|
|
146
|
+
fps = parsed.get("fps", fps)
|
|
147
|
+
seconds = parsed.get("seconds", seconds)
|
|
148
|
+
|
|
149
|
+
textless_ok = args.image or effect in PATTERNS or effect == "matrix"
|
|
150
|
+
if not text and not textless_ok:
|
|
151
|
+
print("Erreur: aucun texte. Passe-le en argument, entre guillemets dans --describe, "
|
|
152
|
+
"ou utilise --image / un motif (starfield, plasma, life, eq, scope, radar).",
|
|
153
|
+
file=sys.stderr)
|
|
154
|
+
return 2
|
|
155
|
+
|
|
156
|
+
if args.out:
|
|
157
|
+
out = args.out
|
|
158
|
+
elif text:
|
|
159
|
+
out = "".join(c if c.isalnum() else "_" for c in text)[:24] + ".gif"
|
|
160
|
+
elif args.image:
|
|
161
|
+
out = os.path.splitext(os.path.basename(args.image))[0] + "_oled.gif"
|
|
162
|
+
else:
|
|
163
|
+
out = f"{effect}.gif"
|
|
164
|
+
out, effect, W, H, n, duration = make_gif(
|
|
165
|
+
text, out, preset=args.preset, size=args.size, effect=effect, fps=fps,
|
|
166
|
+
seconds=seconds, speed=args.speed, font_path=args.font,
|
|
167
|
+
font_size=args.font_size, charset=charset, invert=args.invert,
|
|
168
|
+
scale=args.scale, seed=args.seed, image=args.image,
|
|
169
|
+
style="solid" if args.no_dither else args.style, fit=args.fit,
|
|
170
|
+
denoise=not args.no_denoise)
|
|
171
|
+
print(f"{out} — {W}x{H}, effet {effect}, {n} frames @ {duration} ms")
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
raise SystemExit(main())
|