8x32 PI : vitesse de lecture, QR code et affiche, sens de l'image sur la vitrine

- Vitesse de lecture : réglage VITESSE du lecteur et menu « Vitesse » sur la
  page web (×0,25 à ×2, gardé dans .vitesse.txt du dossier des animations).
- QR code d'accès à la page web et affiche A5 (outils/generer_qrcode.py →
  qrcode/) : une coulée inspirée de « cascade » en haut, une partie de
  « tetris » en bas, couleurs tirées des animations de l'équipe
  (qrcode/sources/), titre en pixels de couleur.
- Matrice de la vitrine : image retournée d'un demi-tour (INVERSER_X et
  INVERSER_Y) et nouveau réglage DECALAGE_X / DECALAGE_Y (ici 1 pixel vers
  la droite).
- Doc : README, INTERFACE_WEB.md et CLAUDE.md à jour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bach
2026-10-09 16:04:13 +02:00
co-authored by Claude Opus 5.5
parent 156f9ce438
commit 5b0e858abe
135 changed files with 434 additions and 11 deletions
+11 -1
View File
@@ -36,6 +36,13 @@ Prenez celle du réseau sur lequel **vous** êtes :
| en **Wi-Fi** (téléphone, ordinateur portable) | **`http://172.16.50.27`** |
| par **câble** (ordinateur fixe de l'école) | `http://172.16.7.199` |
> **Le plus simple : le QR code.** Le dossier [`qrcode/`](qrcode/) contient
> le QR code de la page et une **affiche A5 à imprimer**
> ([`affiche_interface_web.pdf`](qrcode/affiche_interface_web.pdf)) : on la
> scanne avec l'appareil photo du téléphone, connecté au Wi-Fi de l'école.
> Si l'adresse du Pi change, refaites-les :
> `python3 outils/generer_qrcode.py http://NOUVELLE_ADRESSE`.
> **Pourquoi deux adresses ?** Une adresse, c'est comme l'adresse postale du
> Pi, mais **sur un réseau donné**. Le réseau Wi-Fi et le réseau filaire de
> l'école sont deux « villes » différentes, et l'école ne fait pas passer le
@@ -53,7 +60,10 @@ Prenez celle du réseau sur lequel **vous** êtes :
> marchent pas : la page n'est pas sur Internet, seulement sur le réseau local.
En haut de la page, en permanence : **ce que la matrice joue en ce moment**
(mis à jour toutes les 2 secondes) et le bouton **Suivante ⏭**.
(mis à jour toutes les 2 secondes), le bouton **Suivante ⏭** et le menu
**Vitesse** : ×0,5 joue toutes les animations deux fois plus lentement, ×2
deux fois plus vite. Le choix est gardé même après un redémarrage, et
s'applique à partir de l'animation suivante.
---
+6 -2
View File
@@ -439,8 +439,10 @@ animation, pause entre deux, horaires) et de [`matrice.py`](matrice.py) :
| `LUMINOSITE` | matrice.py | `40` | de 0 à 255 ; montez en plein jour, en vitrine |
| `COURANT_MAX_MA` | matrice.py | `2500` | un peu **sous** la limite de l'alimentation |
| `ROTATION` | matrice.py | `90` | `90` = matrice debout (image 8 × 32), `0` = couchée (32 × 8) |
| `INVERSER_X`, `INVERSER_Y` | matrice.py | `False` | sens de l'image (partie 8) |
| `INVERSER_X`, `INVERSER_Y` | matrice.py | `True` | sens de l'image (partie 8) ; réglés sur la matrice de la vitrine |
| `DECALAGE_X`, `DECALAGE_Y` | matrice.py | `1`, `0` | décale toute l'image d'un pixel (ici vers la droite : la colonne de gauche reste éteinte) |
| `GAMMA` | matrice.py | `2.2` | correction des couleurs (1.0 = aucune) |
| `VITESSE` | lecteur.py | `1.0` | vitesse de lecture (0.5 = deux fois plus lent) ; le menu « Vitesse » de la page web a la priorité |
| `DUREE_MINIMUM` | lecteur.py | `10` | une animation courte est rejouée pendant au moins 10 s |
| `HORAIRES` | lecteur.py | `None` | ex. `("09:00", "22:30")` |
@@ -475,6 +477,7 @@ navigateur, et on gère la vitrine sans terminal ni carte SD.
| **Changer l'ordre** de lecture | ▲ ▼ (les fichiers sont renumérotés `01_`, `02_`…) |
| **Retirer** une animation de la boucle, sans la supprimer | ⏸ (elle passe dans « Pas jouées ») ; + pour la remettre |
| **Jouer tout de suite** une animation, ou **passer** à la suivante | ▶ et **Suivante ⏭** |
| **Ralentir ou accélérer** toutes les animations | menu **Vitesse** en haut (×0,25 à ×2) |
| **Supprimer** | ✕ : elle va à la corbeille ; « Vider » efface pour de bon |
| **Savoir ce que joue la matrice** | en haut de la page, mis à jour toutes les 2 secondes |
@@ -490,7 +493,7 @@ réseau filaire de l'école ne communiquent pas entre eux : prenez celle de
| Vous êtes connecté… | Adresse |
|---|---|
| en **Wi-Fi** (téléphone, portable) | **http://172.16.50.27** |
| en **Wi-Fi** (téléphone, portable) | **http://172.16.50.27**, ou scannez le QR code du dossier [`qrcode/`](qrcode/) |
| par **câble** | http://172.16.7.199 |
Ces adresses sont distribuées par le réseau et **peuvent changer** : les
@@ -640,6 +643,7 @@ Pour voir ce que fait le lecteur quand il tourne tout seul (mode vitrine) :
| [`test_matrice.py`](test_matrice.py) | | test de branchement et d'orientation |
| [`mes_animations.py`](mes_animations.py) | | les animations de l'Arduino, traduites en Python |
| [`animations/`](animations/) | | exemples : GIF, séquence de PNG, APNG |
| [`qrcode/`](qrcode/) | tous | QR code de la page web et affiche A5 à imprimer (refaits par `outils/generer_qrcode.py`) |
| [`schemas/`](schemas/) | | schémas de branchement, brochage de la puce, chaîne Krita → matrice, capture de l'interface web |
| `apercus/` | design | créé par `verifier_animation.py` : aperçus « façon LED » à partager |
| [`krita/palette_vitrine_pixels.gpl`](krita/palette_vitrine_pixels.gpl) | design | palette de couleurs adaptée aux LED, à importer dans Krita |
+21 -3
View File
@@ -91,6 +91,13 @@ IMAGES_PAR_SECONDE = 12
# Une image fixe reste affichée pendant cette durée.
DUREE_MINIMUM = 10
# VITESSE de lecture de toutes les animations :
# 1 = la vitesse prévue dans Krita ;
# 0.5 = deux fois plus lent ; 2 = deux fois plus vite.
# La page web peut la changer : elle l'écrit alors dans le fichier caché
# « .vitesse.txt » du dossier des animations, qui a la priorité sur ce réglage.
VITESSE = 1.0
# Noir entre deux animations, en secondes (0 = enchaîner directement).
PAUSE_ENTRE_ANIMATIONS = 0.3
@@ -256,8 +263,19 @@ def charger(chemin):
# 4. JOUER UNE ANIMATION
# =============================================================================
def jouer(etapes, duree_minimum=DUREE_MINIMUM):
def lire_vitesse_de_lecture(dossier):
"""La vitesse choisie sur la page web (fichier .vitesse.txt), sinon VITESSE."""
try:
with open(os.path.join(dossier, ".vitesse.txt")) as f:
vitesse = float(f.read().strip().replace(",", "."))
return min(4.0, max(0.1, vitesse)) # entre ×0,1 et ×4, par prudence
except (OSError, ValueError):
return VITESSE
def jouer(etapes, duree_minimum=DUREE_MINIMUM, vitesse=1.0):
"""Joue la liste d'étapes, et la rejoue jusqu'à atteindre la durée minimum.
vitesse = 0.5 : chaque image reste affichée deux fois plus longtemps.
S'arrête plus tôt si le serveur web demande autre chose (partie 5)."""
debut = time.monotonic()
while True:
@@ -265,7 +283,7 @@ def jouer(etapes, duree_minimum=DUREE_MINIMUM):
moment = time.monotonic()
matrice.montrer(image)
# On retire le temps déjà passé à afficher, pour garder le rythme.
if not attendre_ou_interrompre(duree - (time.monotonic() - moment)):
if not attendre_ou_interrompre(duree / vitesse - (time.monotonic() - moment)):
return
if time.monotonic() - debut >= duree_minimum:
break
@@ -296,7 +314,7 @@ def jouer_animation(chemin, dossier):
print(f" ✗ impossible de lire ce fichier ({erreur}) : on passe au suivant")
return
if etapes:
jouer(etapes)
jouer(etapes, vitesse=lire_vitesse_de_lecture(dossier))
if PAUSE_ENTRE_ANIMATIONS > 0:
matrice.effacer()
matrice.montrer()
+21 -4
View File
@@ -121,8 +121,16 @@ COURANT_MAX_MA = 2500
# - rouge et vert inversés (gauche/droite) → INVERSER_X = True
# - rouge et vert en bas au lieu d'en haut → INVERSER_Y = True
# - les deux à la fois (image à l'envers) → les deux à True
INVERSER_X = False
INVERSER_Y = False
INVERSER_X = True
INVERSER_Y = True
# DÉCALAGE de l'image sur la matrice, en pixels (après INVERSER_X / Y).
# DECALAGE_X = 1 : toute l'image glisse d'un pixel vers la DROITE ;
# la colonne de gauche reste éteinte, la dernière colonne de l'image sort
# de la matrice. Utile si un bord de la matrice est caché dans la vitrine.
# (Valeurs négatives : vers la gauche ; DECALAGE_Y : vers le bas.)
DECALAGE_X = 1
DECALAGE_Y = 0
# CORRECTION GAMMA (voir partie 4). 2.2 = comme un écran d'ordinateur.
# 1.0 = pas de correction (les couleurs sombres paraîtront trop claires).
@@ -526,9 +534,18 @@ def assombrir(pourcentage):
for (r, v, b) in pixels_de(toile)])
def decaler(image):
"""Applique DECALAGE_X / DECALAGE_Y : l'image glisse, le vide est noir."""
if DECALAGE_X == 0 and DECALAGE_Y == 0:
return image
decalee = Image.new("RGB", (LARGEUR, HAUTEUR))
decalee.paste(image, (DECALAGE_X, DECALAGE_Y))
return decalee
def montrer(image=None):
"""Affiche la toile (ou une autre image de 32 × 8) sur l'écran."""
_ecran.afficher(image if image is not None else toile)
"""Affiche la toile (ou une autre image de la taille de la matrice) sur l'écran."""
_ecran.afficher(decaler(image if image is not None else toile))
def attendre(millisecondes):
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env python3
"""
=============================================================================
QR CODE ET AFFICHE D'ACCÈS À L'INTERFACE WEB
Workshop « Vitrine pixels »
=============================================================================
Fabrique, dans le dossier « qrcode » :
- qrcode_interface_web.png : le QR code seul (pour un message, un diaporama) ;
- qrcode_interface_web.svg : le même, en vectoriel (pour l'imprimer en grand) ;
- affiche_interface_web.svg : une affiche A5 à imprimer et à poser à côté
de la vitrine ou dans l'atelier, avec deux
compositions inspirées des animations de
l'équipe (qrcode/sources : « cascade », « tetris ») ;
- affiche_interface_web.pdf : la même, prête à imprimer (si rsvg-convert
est installé : paquet « librsvg2-bin »).
On scanne le QR code avec l'appareil photo du téléphone : la page web de la
vitrine s'ouvre. ⚠️ Le téléphone doit être connecté au Wi-Fi de l'école
(la page n'est pas sur Internet).
L'adresse du Pi peut changer (voir INTERFACE_WEB.md) : il suffit alors de
relancer ce programme avec la nouvelle adresse :
python3 outils/generer_qrcode.py http://172.16.50.27
Il faut le logiciel « qrencode » :
sudo apt install qrencode (Debian, Ubuntu, Raspberry Pi OS)
sudo pacman -S qrencode (Arch)
brew install qrencode (macOS)
"""
import os
import shutil
import subprocess
import sys
ICI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SORTIE = os.path.join(ICI, "qrcode")
ADRESSE_PAR_DEFAUT = "http://172.16.50.27"
def modules_du_qrcode(texte):
"""Demande à qrencode le QR code en texte : une ligne par rangée,
« # » = case noire. Renvoie une liste de lignes de booléens."""
# -t ASCII : dessin en caractères (2 caractères par case) ; -m 0 : sans marge ;
# -l M : correction d'erreur moyenne (le code reste lisible s'il est un peu abîmé).
resultat = subprocess.run(["qrencode", "-t", "ASCII", "-m", "0", "-l", "M", texte],
capture_output=True, text=True, check=True)
lignes = [l for l in resultat.stdout.splitlines() if l.strip()]
return [[ligne[i] == "#" for i in range(0, len(ligne), 2)] for ligne in lignes]
def cases_svg(modules, x0, y0, taille):
"""Les cases noires du QR code, en rectangles SVG."""
return "".join(
f'<rect x="{x0 + x * taille:.2f}" y="{y0 + y * taille:.2f}" '
f'width="{taille:.2f}" height="{taille:.2f}"/>'
for y, ligne in enumerate(modules) for x, noir in enumerate(ligne) if noir)
def qrcode_svg(modules, marge=4):
n = len(modules) + 2 * marge
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {n} {n}" '
f'width="{n * 10}" height="{n * 10}" shape-rendering="crispEdges">'
f'<rect width="{n}" height="{n}" fill="#fff"/>'
f'<g fill="#000">{cases_svg(modules, marge, marge, 1)}</g></svg>\n')
# =============================================================================
# L'AFFICHE : deux compositions inspirées des animations de l'équipe
# =============================================================================
#
# En haut, une coulée inspirée de « cascade » : un liquide bleu glacier qui
# déborde du haut de la page, en strates séparées par de l'écume, avec des
# stalactites qui gouttent. En bas, une partie de « tetris » : des pièces de
# couleurs vives empilées sur toute la largeur, deux pièces encore en chute.
#
# Les deux compositions sont dessinées sur une grille de « LED » de 48 colonnes,
# et leurs couleurs sont prises dans les fichiers de l'équipe (qrcode/sources).
# Le hasard est « graine fixe » : on obtient la même affiche à chaque fois.
import random
SOURCES = os.path.join(SORTIE, "sources")
COLONNES = 48 # largeur de la grille de pixels de l'affiche
PAS = 148 / COLONNES # taille d'un pixel, en mm (≈ 3,1)
REMPLISSAGE = 0.88 # un pixel = un carré un peu plus petit que le pas
BLEUS_CASCADE = [(170, 199, 254), (119, 179, 254), (118, 218, 254)]
COULEURS_TETRIS = [(255, 38, 38), (255, 127, 0), (255, 214, 0), (0, 214, 60),
(0, 220, 230), (255, 0, 255)]
def couleurs_de(chemin, garder):
"""Les couleurs d'une animation de l'équipe, triées par fréquence,
filtrées par la fonction « garder ». [] si le fichier est absent."""
try:
sys.path.insert(0, ICI)
import lecteur # noqa: E402
compte = {}
for image, _ in lecteur.charger(chemin):
for c in lecteur.matrice.pixels_de(image):
compte[c] = compte.get(c, 0) + 1
return [c for c, _ in sorted(compte.items(), key=lambda kv: -kv[1]) if garder(c)]
except Exception:
return []
def hexa(c):
return f"#{c[0]:02x}{c[1]:02x}{c[2]:02x}"
def pixel(x, y, couleur, y0=0.0):
"""Un pixel de la grille de l'affiche, en colonne x, rangée y (depuis y0 mm)."""
marge = PAS * (1 - REMPLISSAGE) / 2
return (f'<rect x="{x * PAS + marge:.2f}" y="{y0 + y * PAS + marge:.2f}" '
f'width="{PAS * REMPLISSAGE:.2f}" height="{PAS * REMPLISSAGE:.2f}" rx="0.35" '
f'fill="{hexa(couleur) if isinstance(couleur, tuple) else couleur}"/>')
def composition_cascade(bleus):
"""Le liquide qui coule du haut de la page (sur fond blanc)."""
import math
hasard = random.Random(11)
clair, moyen, vif = (bleus + BLEUS_CASCADE)[:3]
cases = {}
# Profondeur du liquide dans chaque colonne : une ondulation lente.
profondeur = [int(5.2 + 1.6 * math.sin(x / 6.0) + 0.9 * math.sin(x / 2.7 + 2))
for x in range(COLONNES)]
for x in range(COLONNES):
for y in range(profondeur[x] + 1):
distance = profondeur[x] - y # distance au bord du liquide
# Deux traits d'écume blancs (des « trous ») qui suivent le bord,
# interrompus de temps en temps, comme dans l'animation.
if distance in (2, 5) and hasard.random() < 0.8:
continue
cases[(x, y)] = clair if distance <= 1 else moyen if distance <= 4 else vif
# Les coulures : des filets de 1 ou 2 pixels de large, terminés par une goutte.
x = hasard.randint(0, 2)
while x < COLONNES:
largeur = hasard.choice([1, 1, 2])
longueur = hasard.choice([1, 2, 2, 3, 4, 5])
haut = max(profondeur[min(x + i, COLONNES - 1)] for i in range(largeur)) + 1
for i in range(largeur):
for y in range(haut, haut + longueur):
cases[(x + i, y)] = clair
if longueur >= 3: # la goutte, plus large, en bas
for i in range(-1, largeur + 1):
cases[(x + i, haut + longueur)] = clair
for i in range(largeur):
cases[(x + i, haut + longueur + 1)] = clair
if hasard.random() < 0.35: # une goutte déjà détachée
cases[(x, haut + longueur + hasard.randint(3, 4))] = moyen
x += largeur + hasard.randint(2, 5)
return "".join(pixel(x, y, c) for (x, y), c in cases.items() if 0 <= x < COLONNES)
PIECES = { # les 7 pièces de Tetris, (colonne, rangée) de chaque carré
"I": [(0, 0), (1, 0), (2, 0), (3, 0)], "O": [(0, 0), (1, 0), (0, 1), (1, 1)],
"T": [(0, 0), (1, 0), (2, 0), (1, 1)], "S": [(1, 0), (2, 0), (0, 1), (1, 1)],
"Z": [(0, 0), (1, 0), (1, 1), (2, 1)], "J": [(0, 0), (0, 1), (1, 1), (2, 1)],
"L": [(2, 0), (0, 1), (1, 1), (2, 1)],
}
def tourner(cases, fois):
for _ in range(fois):
cases = [(-y, x) for x, y in cases]
mx, my = min(x for x, _ in cases), min(y for _, y in cases)
return [(x - mx, y - my) for x, y in cases]
def bloc(x, y, couleur, y0):
"""Un carré de Tetris : la couleur, avec un petit reflet clair en haut à gauche."""
clair = tuple(int(c + (255 - c) * 0.55) for c in couleur)
marge = PAS * (1 - REMPLISSAGE) / 2
cote = PAS * REMPLISSAGE
gx, gy = x * PAS + marge, y0 + y * PAS + marge
return (f'<rect x="{gx:.2f}" y="{gy:.2f}" width="{cote:.2f}" height="{cote:.2f}" '
f'rx="0.3" fill="{hexa(couleur)}"/>'
f'<rect x="{gx + cote * 0.18:.2f}" y="{gy + cote * 0.18:.2f}" width="{cote * 0.28:.2f}" '
f'height="{cote * 0.28:.2f}" fill="{hexa(clair)}"/>')
def composition_tetris(couleurs, y_bas):
"""Une partie de Tetris sur toute la largeur, le bas de la pile en y_bas (mm)."""
hasard = random.Random(5)
hauteur_max = 7
plateau = {} # (x, y) → couleur ; y = 0 en bas
def poser(forme, x0, y):
return [(x0 + x, y + dy) for x, dy in forme]
def atterrissage(forme, x0):
y = hauteur_max + 4
while y > 0 and all(c not in plateau for c in poser(forme, x0, y - 1)):
y -= 1
return y
for _ in range(400):
forme = tourner(PIECES[hasard.choice(list(PIECES))], hasard.randint(0, 3))
forme = [(x, max(y for _, y in forme) - y) for x, y in forme] # y vers le haut
largeur = max(x for x, _ in forme) + 1
essais = []
for x0 in range(COLONNES - largeur + 1):
y = atterrissage(forme, x0)
cases = poser(forme, x0, y)
# Les trous laissés sous la pièce (un bon joueur les évite).
trous = sum(1 for (cx, cy) in cases
if cy > 0 and (cx, cy - 1) not in plateau and (cx, cy - 1) not in cases)
essais.append((y + 3 * trous + hasard.random() * 1.5, x0, cases))
_, x0, cases = min(essais)
# L'horizon reste dentelé : plus haut au centre-gauche, plus bas ailleurs.
limite = hauteur_max - (2 if (x0 // 6) % 3 == 1 else 0)
if max(cy for _, cy in cases) >= limite:
continue
# Une couleur différente de celles des pièces qui la touchent.
voisines = {plateau[(cx + dx, cy + dy)] for cx, cy in cases
for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1))
if (cx + dx, cy + dy) in plateau}
libres = [c for c in couleurs if c not in voisines] or couleurs
couleur = hasard.choice(libres)
for c in cases:
plateau[c] = couleur
morceaux = [bloc(x, -y - 1, c, y_bas) for (x, y), c in plateau.items()]
# Deux pièces encore en train de tomber, au-dessus de la pile.
sommet = max(y for _, y in plateau) + 1
for nom, x0, au_dessus, tour, couleur in (("T", 4, 3, 2, couleurs[0]),
("L", 41, 4, 1, couleurs[-1])):
for x, y in tourner(PIECES[nom], tour):
morceaux.append(bloc(x0 + x, -(sommet + au_dessus) + y, couleur, y_bas))
return "".join(morceaux)
def titre_en_pixels(texte, y0, taille, couleurs, depart=0):
"""Écrit un texte avec la police 5 × 7 des exemples, centré,
une couleur par lettre (on fait le tour de la liste « couleurs »)."""
sys.path.insert(0, os.path.join(ICI, "outils"))
from generer_exemples import POLICE # noqa: E402
largeur = sum(len(POLICE[c][0]) + 1 for c in texte) - 1
x = (148 - largeur * taille) / 2
morceaux, n = [], depart
for c in texte:
couleur = hexa(couleurs[n % len(couleurs)])
if c != " ":
n += 1
for ligne, motif in enumerate(POLICE[c]):
for dx, p in enumerate(motif):
if p == "#":
morceaux.append(f'<rect x="{x + dx * taille:.2f}" y="{y0 + ligne * taille:.2f}" '
f'width="{taille * 0.88:.2f}" height="{taille * 0.88:.2f}" '
f'rx="0.2" fill="{couleur}"/>')
x += (len(POLICE[c][0]) + 1) * taille
return "".join(morceaux)
def affiche_svg(modules, adresse):
"""Affiche A5 (148 × 210 mm)."""
l, h = 148, 210
encre, gris = "#141414", "#6b665d"
bleus = couleurs_de(os.path.join(SOURCES, "cascade.gif"),
lambda c: c[2] > 200 and c[0] < 200) # les bleus
vives = couleurs_de(os.path.join(SOURCES, "tetris"),
lambda c: max(c) - min(c) > 180) # les saturées
# Une couleur par teinte (l'animation en contient des nuances très proches).
tetris = []
for c in vives + COULEURS_TETRIS:
if all(sum(abs(a - b) for a, b in zip(c, t)) > 120 for t in tetris):
tetris.append(c)
y_titre = 49
cote_qr = 56
x_qr, y_qr = (l - cote_qr) / 2, y_titre + 37
taille = cote_qr / len(modules)
y_texte = y_qr + cote_qr + 12
etapes = ["1. Connectez-vous au Wi-Fi de l'école.",
"2. Visez le QR code avec l'appareil photo.",
"3. Déposez vos animations, choisissez l'ordre."]
textes = (f'<text x="{l / 2}" y="{y_texte}" font-size="7" font-weight="bold" fill="{encre}" '
f'text-anchor="middle" font-family="Menlo, Consolas, monospace">'
f'{adresse.replace("http://", "")}</text>'
+ "".join(f'<text x="{l / 2}" y="{y_texte + 8 + i * 6}" font-size="4.2" '
f'fill="{encre}" text-anchor="middle">{e}</text>'
for i, e in enumerate(etapes))
+ f'<text x="{l / 2}" y="{y_texte + 31}" font-size="3.2" fill="{gris}" '
f'text-anchor="middle">Uniquement sur le Wi-Fi de l\'école (pas en 4G / 5G).</text>')
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{l}mm" height="{h}mm" viewBox="0 0 {l} {h}"
font-family="Helvetica, Arial, sans-serif">
<title>Vitrine pixels : accès à l'interface web</title>
<rect width="{l}" height="{h}" fill="#fff"/>
{composition_cascade(bleus)}
{titre_en_pixels("VITRINE", y_titre, 2.0, tetris)}
{titre_en_pixels("PIXELS", y_titre + 16, 2.0, tetris, depart=1)}
<rect x="{x_qr - 4}" y="{y_qr - 4}" width="{cote_qr + 8}" height="{cote_qr + 8}" fill="#fff"/>
<g fill="#000" shape-rendering="crispEdges">{cases_svg(modules, x_qr, y_qr, taille)}</g>
{textes}
{composition_tetris(tetris, h)}
</svg>
"""
def main():
adresse = sys.argv[1] if len(sys.argv) > 1 else ADRESSE_PAR_DEFAUT
if not adresse.startswith("http"):
adresse = "http://" + adresse
os.makedirs(SORTIE, exist_ok=True)
modules = modules_du_qrcode(adresse)
with open(os.path.join(SORTIE, "qrcode_interface_web.svg"), "w") as f:
f.write(qrcode_svg(modules))
with open(os.path.join(SORTIE, "affiche_interface_web.svg"), "w") as f:
f.write(affiche_svg(modules, adresse))
# Le PNG, directement par qrencode : 12 pixels par case, marge de 4 cases.
subprocess.run(["qrencode", "-o", os.path.join(SORTIE, "qrcode_interface_web.png"),
"-s", "12", "-m", "4", "-l", "M", adresse], check=True)
# Le PDF de l'affiche, si l'outil de conversion est là.
if shutil.which("rsvg-convert"):
subprocess.run(["rsvg-convert", "-f", "pdf",
"-o", os.path.join(SORTIE, "affiche_interface_web.pdf"),
os.path.join(SORTIE, "affiche_interface_web.svg")], check=True)
print(f"QR code pour {adresse} écrit dans {SORTIE}")
if __name__ == "__main__":
main()
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -0,0 +1 @@
10
Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Some files were not shown because too many files have changed in this diff Show More