Files
open-school/backend/tools/regenerate_fraction_exercise_svgs.py
2026-05-12 21:43:27 +02:00

294 lines
12 KiB
Python

from __future__ import annotations
from pathlib import Path
from xml.sax.saxutils import escape
ROOT = Path(__file__).resolve().parents[2]
RELATIVE = Path("contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/exercices")
TARGET_ROOTS = [ROOT / "backend" / RELATIVE, ROOT / "Programme" / RELATIVE]
STYLE = """
.bg{fill:#f6f5ef}
.panel{fill:#fff;stroke:#d9e7e1;stroke-width:3}
.card{fill:#eef7f4;stroke:#c9dfd8;stroke-width:3}
.white{fill:#fff;stroke:#c9dfd8;stroke-width:3}
.title{font-family:"Trebuchet MS",Verdana,sans-serif;font-size:48px;font-weight:800;fill:#074f4b}
.h2{font-family:"Trebuchet MS",Verdana,sans-serif;font-size:28px;font-weight:800;fill:#074f4b}
.body{font-family:"Trebuchet MS",Verdana,sans-serif;font-size:24px;fill:#1d3038}
.small{font-family:"Trebuchet MS",Verdana,sans-serif;font-size:20px;fill:#2d4148}
.frac{font-family:"Trebuchet MS",Verdana,sans-serif;font-size:36px;font-weight:800;fill:#074f4b}
.op{font-family:"Trebuchet MS",Verdana,sans-serif;font-size:34px;font-weight:800;fill:#074f4b}
.blank{stroke:#7fa79d;stroke-width:3;stroke-dasharray:10 10}
.axis{stroke:#26383d;stroke-width:4;stroke-linecap:round}
.tick{stroke:#26383d;stroke-width:3;stroke-linecap:round}
.shade{fill:#5ab7a8}
.empty{fill:#fff}
""".strip()
def text(x: int, y: int, value: str, klass: str = "body", anchor: str | None = None) -> str:
anchor_attr = f' text-anchor="{anchor}"' if anchor else ""
return f'<text x="{x}" y="{y}" class="{klass}"{anchor_attr}>{escape(value)}</text>'
def line(x1: int, y1: int, x2: int, y2: int, klass: str = "blank") -> str:
return f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" class="{klass}" />'
def fraction(x: int, y: int, n: str | int, d: str | int, size: int = 52) -> str:
n_text = str(n)
d_text = str(d)
width = max(58, int(max(len(n_text), len(d_text)) * size * 0.62) + 22)
cx = width / 2
return (
f'<g transform="translate({x} {y})" class="math-expression fraction-g" '
f'data-math="fraction" data-box="-10 -39 {width + 20} 96">'
f'<text x="{cx:g}" y="-10" text-anchor="middle" class="frac">{escape(n_text)}</text>'
f'<line x1="0" y1="8" x2="{width}" y2="8" stroke="#074f4b" stroke-width="4" stroke-linecap="round" />'
f'<text x="{cx:g}" y="46" text-anchor="middle" class="frac">{escape(d_text)}</text>'
"</g>"
)
def card(x: int, y: int, w: int, h: int, title: str, body: str, soft: bool = True) -> str:
klass = "card" if soft else "white"
return (
f'<g transform="translate({x} {y})">'
f'<rect width="{w}" height="{h}" rx="24" class="{klass}" />'
f'{text(32, 58, title, "h2")}'
f'{body}'
"</g>"
)
def sheet(title: str, cards: list[str]) -> str:
return (
'<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900">\n'
f' <defs><style>{STYLE}</style></defs>\n'
' <rect class="bg" width="1600" height="900" />\n'
' <rect x="56" y="42" width="1488" height="816" rx="34" class="panel" />\n'
f' {text(800, 112, title, "title", "middle")}\n'
f' {"".join(cards)}\n'
'</svg>\n'
)
def band(x: int, y: int, parts: int, shaded: int, width: int = 250, height: int = 44) -> str:
cell = width / parts
rects = []
for index in range(parts):
fill = "shade" if index < shaded else "empty"
rects.append(
f'<rect x="{x + cell * index:g}" y="{y}" width="{cell:g}" height="{height}" class="{fill}" '
'stroke="#074f4b" stroke-width="2" />'
)
return "".join(rects)
def number_line(x: int, y: int, parts: int, point: int | None = None, width: int = 320) -> str:
items = [line(x, y, x + width, y, "axis")]
for index in range(parts + 1):
px = x + int(width * index / parts)
items.append(line(px, y - 14, px, y + 14, "tick"))
items.append(text(x, y + 48, "0", "small", "middle"))
items.append(text(x + width, y + 48, "1", "small", "middle"))
if point is not None:
px = x + int(width * point / parts)
items.append(f'<circle cx="{px}" cy="{y}" r="11" fill="#e78232" />')
return "".join(items)
def svg_01() -> str:
c1 = "".join(
fraction(58, y, n, d) + line(190, y + 8, 360, y + 8)
for y, (n, d) in zip([125, 247, 369, 491], [(1, 2), (3, 4), (5, 8), (7, 10)])
)
c2 = "".join(
fraction(58, y, n, d) + text(175, y - 22, "Numerateur :", "small") + line(320, y - 22, 385, y - 22)
+ text(175, y + 42, "Denominateur :", "small") + line(330, y + 42, 385, y + 42)
for y, (n, d) in zip([145, 305, 465], [(2, 5), (4, 9), (6, 7)])
)
c3 = (
text(45, 140, "deux tiers", "body") + line(45, 180, 340, 180)
+ text(45, 260, "trois cinquiemes", "body") + line(45, 300, 340, 300)
+ text(45, 380, "neuf dixiemes", "body") + line(45, 420, 340, 420)
+ text(45, 510, "Ecris avec une barre horizontale.", "small")
)
return sheet("Exercices - Lire et ecrire une fraction", [
card(110, 190, 430, 560, "1. Lis", c1),
card(590, 190, 430, 560, "2. Haut et bas", c2, False),
card(1070, 190, 420, 560, "3. Ecris", c3),
])
def svg_02() -> str:
c1 = (
text(38, 120, "Quelle fraction est coloriee ?", "body")
+ band(45, 160, 3, 2) + line(330, 185, 430, 185)
+ band(45, 260, 5, 4) + line(330, 285, 430, 285)
+ band(45, 360, 10, 6) + line(330, 385, 430, 385)
)
c2 = "".join(
fraction(52, y, n, d) + text(160, y + 18, "Dessine une bande.", "small") + line(345, y + 18, 470, y + 18)
for y, (n, d) in zip([145, 285, 425], [(1, 4), (3, 6), (5, 8)])
)
c3 = (
text(40, 130, "Verifie toujours :", "body")
+ text(55, 200, "1. une seule unite", "body")
+ text(55, 270, "2. des parts egales", "body")
+ text(55, 340, "3. les parts prises", "body")
+ text(55, 450, "Explique ton choix a l'oral.", "small")
)
return sheet("Exercices - Representer une fraction partagee", [
card(95, 190, 460, 560, "1. Fraction coloriee", c1),
card(590, 190, 500, 560, "2. Dessine", c2, False),
card(1125, 190, 360, 560, "3. Verifie", c3),
])
def svg_03() -> str:
c1 = number_line(55, 190, 4) + text(55, 115, "L'unite est partagee en 4.", "body") + line(55, 330, 350, 330)
c2 = number_line(55, 210, 4, 3) + text(55, 115, "Avance de 3 petites parts.", "body") + fraction(360, 330, 3, 4)
c3 = number_line(55, 210, 5, 2) + text(55, 115, "Lis le point orange.", "body") + line(55, 330, 350, 330)
return sheet("Exercices - Fraction sur demi-droite", [
card(95, 190, 450, 500, "1. L'unite", c1),
card(575, 190, 450, 500, "2. Placer", c2, False),
card(1055, 190, 450, 500, "3. Lire", c3),
])
def svg_04() -> str:
c1 = "".join(fraction(x, 170, n, d) for x, (n, d) in zip([45, 150, 255, 360, 465], [(2, 5), (5, 5), (7, 5), (9, 4), (3, 8)]))
c1 += text(45, 330, "Entoure seulement celles qui sont > 1.", "small")
c2 = (
fraction(45, 150, 5, 4) + text(145, 170, "=", "op") + fraction(205, 150, 4, 4) + text(305, 170, "+", "op") + line(365, 170, 500, 170)
+ fraction(45, 330, 7, 3) + text(145, 350, "=", "op") + fraction(205, 330, 6, 3) + text(305, 350, "+", "op") + line(365, 350, 500, 350)
)
c3 = (
text(35, 135, "Numerateur", "small")
+ text(35, 180, "plus grand", "small")
+ text(35, 225, "que le bas :", "small")
+ text(35, 285, "souvent > 1.", "small")
+ text(35, 380, "Egal veut = 1.", "small")
)
return sheet("Exercices - Fractions superieures a 1", [
card(80, 190, 560, 500, "1. Reconnaitre", c1),
card(670, 190, 560, 500, "2. Decomposer", c2, False),
card(1260, 190, 260, 500, "3. Attention", c3),
])
def svg_05() -> str:
rows = [(2, 7, "<", 5, 7), (6, 8, ">", 3, 8), (4, 9, "=", 4, 9)]
c1 = "".join(
fraction(45, y, a, b) + text(155, y + 22, "...", "op") + fraction(235, y, c, d)
for y, (a, b, _, c, d) in zip([135, 285, 435], rows)
)
c2 = "".join(
fraction(45, y, a, b) + text(155, y + 22, "ou", "op") + fraction(245, y, c, d) + line(360, y + 22, 500, y + 22)
for y, (a, b, c, d) in zip([135, 285, 435], [(1, 2, 1, 4), (2, 3, 2, 5), (3, 6, 3, 10)])
)
c3 = (
text(45, 130, "Meme denominateur :", "body")
+ text(45, 190, "compare les numerateurs.", "small")
+ text(45, 290, "Meme numerateur :", "body")
+ text(45, 350, "les parts grandes", "small")
+ text(45, 390, "ont un bas plus petit.", "small")
)
return sheet("Exercices - Comparer des fractions", [
card(80, 190, 520, 560, "1. Complete", c1),
card(630, 190, 520, 560, "2. Choisis", c2, False),
card(1180, 190, 330, 560, "3. Strategie", c3),
])
def svg_06() -> str:
c1 = (
text(45, 125, "Avec des quarts :", "body") + text(45, 185, "1 =", "op") + line(120, 185, 220, 185) + text(235, 185, "et 2 =", "op") + line(340, 185, 440, 185)
+ text(45, 300, "Avec des tiers :", "body") + text(45, 360, "2 =", "op") + line(120, 360, 220, 360) + text(235, 360, "et 3 =", "op") + line(340, 360, 440, 360)
)
c2 = (
text(45, 177, "... <", "op") + fraction(180, 155, 5, 4) + text(290, 177, "< ...", "op")
+ text(45, 327, "... <", "op") + fraction(180, 305, 8, 3) + text(290, 327, "< ...", "op")
+ text(45, 477, "... <", "op") + fraction(180, 455, 11, 5) + text(305, 477, "< ...", "op")
)
c3 = number_line(45, 220, 4, 5, 320) + text(45, 120, "Repere l'entier juste avant", "body") + text(45, 345, "puis l'entier juste apres.", "small")
return sheet("Exercices - Encadrer une fraction", [
card(90, 190, 470, 540, "1. Entiers", c1),
card(595, 190, 410, 540, "2. Encadrer", c2, False),
card(1040, 190, 470, 540, "3. Droite", c3),
])
def svg_07() -> str:
c1 = "".join(
fraction(45, y, a, b) + text(145, y + 22, "+", "op") + fraction(205, y, c, d) + text(310, y + 22, "=", "op") + line(370, y + 22, 510, y + 22)
for y, (a, b, c, d) in zip([125, 275, 425], [(1, 5, 2, 5), (2, 7, 3, 7), (4, 10, 5, 10)])
)
c2 = "".join(
fraction(45, y, a, b) + text(145, y + 22, "-", "op") + fraction(205, y, c, d) + text(310, y + 22, "=", "op") + line(370, y + 22, 510, y + 22)
for y, (a, b, c, d) in zip([125, 275, 425], [(6, 8, 2, 8), (7, 9, 3, 9), (10, 12, 4, 12)])
)
c3 = (
text(35, 130, "On garde", "small")
+ text(35, 175, "le denominateur.", "small")
+ text(35, 260, "On calcule", "small")
+ text(35, 305, "les numerateurs.", "small")
+ line(45, 380, 330, 380)
)
return sheet("Exercices - Additionner et soustraire", [
card(75, 190, 560, 560, "1. Additionner", c1),
card(665, 190, 560, 560, "2. Soustraire", c2, False),
card(1255, 190, 270, 560, "3. Regle", c3),
])
def svg_08() -> str:
c1 = "".join(
fraction(45, y, n, d) + text(145, y + 22, f"de {q} =", "op") + line(285, y + 22, 440, y + 22)
for y, (n, d, q) in zip([135, 285, 435], [(1, 2, 18), (1, 3, 24), (1, 5, 40)])
)
c2 = "".join(
fraction(45, y, n, d) + text(145, y + 22, f"de {q} =", "op") + line(285, y + 22, 440, y + 22)
for y, (n, d, q) in zip([135, 285, 435], [(3, 4, 20), (2, 5, 30), (3, 6, 42)])
)
c3 = (
text(45, 130, "1. Je partage par le bas.", "body")
+ text(45, 210, "2. Je prends le haut.", "body")
+ text(45, 310, "Exemple :", "body")
+ fraction(45, 390, 3, 4)
+ text(145, 412, "de 20", "op")
)
return sheet("Exercices - Fraction d'une quantite", [
card(80, 190, 490, 560, "1. Une part", c1),
card(610, 190, 490, 560, "2. Plusieurs parts", c2, False),
card(1140, 190, 370, 560, "3. Methode", c3),
])
SVG_BY_DIR = {
"01_lire_ecrire_fraction_simple": svg_01,
"02_representer_fraction_partage": svg_02,
"03_fraction_sur_demi_droite_graduee": svg_03,
"04_fraction_superieure_a_un": svg_04,
"05_comparer_fractions_simples": svg_05,
"06_encadrer_fraction_entre_entiers": svg_06,
"07_additionner_soustraire_fractions_simples": svg_07,
"08_fraction_d_une_quantite": svg_08,
}
def main() -> None:
for root in TARGET_ROOTS:
for folder, builder in SVG_BY_DIR.items():
target = root / folder / "exercices.svg"
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(builder(), encoding="utf-8")
print(f"updated {target}")
if __name__ == "__main__":
main()