Regroupe l'état de travail en cours réalisé dans un même worktree sur plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164), mélangeant des tickets QA et inProgress. Ne constitue pas une feature terminée : commit de sauvegarde avant triage/split par ticket en branches feature/* dédiées. Exclut les dossiers d'environnement de build locaux et le heap dump parasite (.gitignore mis à jour). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
175 lines
6.6 KiB
Python
175 lines
6.6 KiB
Python
import base64
|
|
import pathlib
|
|
import textwrap
|
|
|
|
root = pathlib.Path("/home/anthony/Documents/Projects/GameTime")
|
|
anton = base64.b64encode((root / "assets/fonts/Anton-Regular.ttf").read_bytes()).decode()
|
|
archivo = base64.b64encode((root / "assets/fonts/Archivo-Variable.ttf").read_bytes()).decode()
|
|
|
|
# Palette Court Blazer (theme sombre)
|
|
GOLD = "#C9A24A"
|
|
CRIMSON = "#D72638"
|
|
BG = "#080A12"
|
|
SURFACE = "#141824"
|
|
TEXT = "#F5F1E8"
|
|
TEXT_SECONDARY = "#A7ADBA"
|
|
BORDER = "#242A3A"
|
|
|
|
TILE_W = 260
|
|
TILE_H = 330
|
|
FACE_D = 216
|
|
GAP = 24
|
|
N = 5
|
|
WIDTH = N * TILE_W + (N + 1) * GAP
|
|
HEIGHT = TILE_H + 190
|
|
|
|
def watch_face(cx, cy, r, label_top, big_value, big_color, status_line, button_label, button_style, disconnected=False):
|
|
parts = []
|
|
# bezel
|
|
parts.append(f'<circle cx="{cx}" cy="{cy}" r="{r+6}" fill="{BORDER}"/>')
|
|
parts.append(f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="{SURFACE}"/>')
|
|
# crimson top accent arc (clin d\'oeil liseré 2px des cartes cles, adapte en arc sur le bezel interne)
|
|
if not disconnected:
|
|
parts.append(f'<path d="M {cx - r*0.55} {cy - r*0.82} A {r} {r} 0 0 1 {cx + r*0.55} {cy - r*0.82}" '
|
|
f'fill="none" stroke="{CRIMSON}" stroke-width="3" stroke-linecap="round"/>')
|
|
else:
|
|
parts.append(f'<path d="M {cx - r*0.55} {cy - r*0.82} A {r} {r} 0 0 1 {cx + r*0.55} {cy - r*0.82}" '
|
|
f'fill="none" stroke="{BORDER}" stroke-width="3" stroke-linecap="round"/>')
|
|
|
|
top_y = cy - r * 0.44
|
|
value_y = cy - r * 0.02
|
|
status_y = cy + r * 0.30
|
|
btn_cy = cy + r * 0.62
|
|
btn_w = r * 1.5
|
|
btn_h = 40
|
|
|
|
label_color = TEXT_SECONDARY if not disconnected else "#5A6072"
|
|
parts.append(
|
|
f'<text x="{cx}" y="{top_y}" text-anchor="middle" font-family="Archivo" font-weight="600" '
|
|
f'font-size="13" letter-spacing="1" fill="{label_color}">{label_top}</text>'
|
|
)
|
|
|
|
parts.append(
|
|
f'<text x="{cx}" y="{value_y}" text-anchor="middle" font-family="Anton" font-size="56" '
|
|
f'fill="{big_color}">{big_value}</text>'
|
|
)
|
|
|
|
parts.append(
|
|
f'<text x="{cx}" y="{status_y}" text-anchor="middle" font-family="Archivo" font-weight="400" '
|
|
f'font-size="13.5" fill="{TEXT_SECONDARY if not disconnected else "#5A6072"}">{status_line}</text>'
|
|
)
|
|
|
|
# button pill
|
|
bx = cx - btn_w / 2
|
|
by = btn_cy - btn_h / 2
|
|
if button_style == "gold_fill":
|
|
fill, stroke, textcol = GOLD, GOLD, "#1A1206"
|
|
elif button_style == "crimson_outline":
|
|
fill, stroke, textcol = "none", CRIMSON, CRIMSON
|
|
elif button_style == "crimson_fill":
|
|
fill, stroke, textcol = CRIMSON, CRIMSON, TEXT
|
|
else: # muted outline (disconnected)
|
|
fill, stroke, textcol = "none", "#3A4152", TEXT_SECONDARY
|
|
|
|
parts.append(
|
|
f'<rect x="{bx:.1f}" y="{by:.1f}" width="{btn_w:.1f}" height="{btn_h}" rx="{btn_h/2:.1f}" '
|
|
f'fill="{fill}" stroke="{stroke}" stroke-width="1.6"/>'
|
|
)
|
|
parts.append(
|
|
f'<text x="{cx}" y="{btn_cy + 5}" text-anchor="middle" font-family="Archivo" font-weight="700" '
|
|
f'font-size="15" fill="{textcol}">{button_label}</text>'
|
|
)
|
|
return "\n".join(parts)
|
|
|
|
|
|
states = [
|
|
dict(
|
|
caption="1 — Pret a demarrer",
|
|
sub="Etape / exercice sous chrono, pas encore lance",
|
|
label_top="DRIBBLE COMBO",
|
|
big_value="00:00",
|
|
big_color=TEXT_SECONDARY,
|
|
status_line="Serie 2 / 4",
|
|
button_label="Demarrer",
|
|
button_style="gold_fill",
|
|
),
|
|
dict(
|
|
caption="2 — Chrono en cours",
|
|
sub="Le chrono actif tourne (exercice ou etape)",
|
|
label_top="DRIBBLE COMBO",
|
|
big_value="00:23",
|
|
big_color=GOLD,
|
|
status_line="Serie 2 / 4 - en cours",
|
|
button_label="Arreter",
|
|
button_style="crimson_outline",
|
|
),
|
|
dict(
|
|
caption="3 — Chrono en pause",
|
|
sub="Chrono arrete avec valeur, reprise possible",
|
|
label_top="DRIBBLE COMBO",
|
|
big_value="00:23",
|
|
big_color=GOLD,
|
|
status_line="Serie 2 / 4 - en pause",
|
|
button_label="Reprendre",
|
|
button_style="gold_fill",
|
|
),
|
|
dict(
|
|
caption="4 — Pas de chrono actif",
|
|
sub="Serie reps/score libre : l'action utile est de conclure",
|
|
label_top="TIRS EN SUSPENSION",
|
|
big_value="2 / 4",
|
|
big_color=GOLD,
|
|
status_line="Serie en cours",
|
|
button_label="Terminer la serie",
|
|
button_style="crimson_fill",
|
|
),
|
|
dict(
|
|
caption="5 — Montre non synchronisee",
|
|
sub="Perte de liaison : dernier etat connu, sans alarmer",
|
|
label_top="DERNIERE DONNEE RECUE",
|
|
big_value="00:23",
|
|
big_color="#5A6072",
|
|
status_line="Non synchronisee",
|
|
button_label="Reessayer",
|
|
button_style="muted",
|
|
disconnected=True,
|
|
),
|
|
]
|
|
|
|
svg_parts = [
|
|
f'<svg xmlns="http://www.w3.org/2000/svg" width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}">',
|
|
'<defs><style>',
|
|
f'@font-face {{ font-family: "Anton"; src: url(data:font/ttf;base64,{anton}) format("truetype"); }}',
|
|
f'@font-face {{ font-family: "Archivo"; src: url(data:font/ttf;base64,{archivo}) format("truetype"); }}',
|
|
'</style></defs>',
|
|
f'<rect x="0" y="0" width="{WIDTH}" height="{HEIGHT}" fill="{BG}"/>',
|
|
f'<text x="{WIDTH/2}" y="46" text-anchor="middle" font-family="Anton" font-size="30" fill="{TEXT}">GAMETIME — MONTRE SYNCHRONISEE</text>',
|
|
f'<text x="{WIDTH/2}" y="72" text-anchor="middle" font-family="Archivo" font-weight="400" font-size="15" fill="{TEXT_SECONDARY}">Ecran unique, bouton contextuel unique — style Court Blazer</text>',
|
|
]
|
|
|
|
for i, st in enumerate(states):
|
|
cx = GAP + TILE_W * i + TILE_W / 2
|
|
cy = 96 + FACE_D / 2 + 10
|
|
r = FACE_D / 2
|
|
svg_parts.append(watch_face(cx, cy, r, st["label_top"], st["big_value"], st["big_color"],
|
|
st["status_line"], st["button_label"], st["button_style"],
|
|
disconnected=st.get("disconnected", False)))
|
|
cap_y = cy + r + 34
|
|
sub_y = cap_y + 22
|
|
svg_parts.append(
|
|
f'<text x="{cx}" y="{cap_y}" text-anchor="middle" font-family="Archivo" font-weight="700" '
|
|
f'font-size="14" fill="{TEXT}">{st["caption"]}</text>'
|
|
)
|
|
lines = textwrap.wrap(st["sub"], width=30)
|
|
for li, line in enumerate(lines):
|
|
svg_parts.append(
|
|
f'<text x="{cx}" y="{sub_y + li * 15}" text-anchor="middle" font-family="Archivo" font-weight="400" '
|
|
f'font-size="11.5" fill="{TEXT_SECONDARY}">{line}</text>'
|
|
)
|
|
|
|
svg_parts.append('</svg>')
|
|
|
|
out = root / ".ideai/tickets/91/assets/watch_screen_mockup.svg"
|
|
out.write_text("\n".join(svg_parts), encoding="utf-8")
|
|
print("written", out, out.stat().st_size)
|