hlassiege/darylpublic Fork
⑂ 5e269af
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(kit): add a cut-paper material to the design system

The design system only changed colors and fonts. A material now adds its own
stylesheet on top of the base one: the Paper theme becomes white cut paper with
grain, hand-cut edges, a short shadow and a subtle stop-motion boil, drawn on
pseudo-elements so GSAP transforms are left untouched.
hlassiege committed 2026-10-12T17:40:39+02:00 Browse files
5e269af parent: 5607e6c
modified backend/studio/modules/animations/kit.py +20 -11
@@ -8,7 +8,8 @@ Deux couches :
88
99 style.css est la charte de base dont on remplace les variables par celles du design system. Les
1010 animations du kit n'emploient que ces variables : changer une couleur ou une police ne demande de
11-régénérer aucune animation.
11+régénérer aucune animation. Une matière (kit/textures/<id>.css) peut s'y ajouter : elle change l'allure
12+des briques (.card, .node…) au-delà des couleurs, sans toucher aux animations non plus.
1213 """
1314 import hashlib
1415 import json
@@ -28,28 +29,30 @@ FONTS = ("font", "mono")
2829 # les graisses chargées : le texte des cartes, les étiquettes mono
2930 WEIGHTS = {"font": "400;500;600;700", "mono": "400;500"}
3031 FALLBACK = {"font": '"Inter", system-ui, sans-serif', "mono": "ui-monospace, monospace"}
32+# les matières : « lisse » est la charte telle quelle, les autres ajoutent kit/textures/<id>.css
33+TEXTURES = ("lisse", "papier")
3134
3235 # des ambiances complètes à choisir comme point de départ (le nom affiché est dans l'i18n, par id).
3336 # Contrastes vérifiés sur le fond des cartes : texte ≥ 15:1, texte discret ≥ 5.5:1, accent ≥ 4.5:1.
3437 PRESETS = (
3538 {"id": "atelier", "card": "#1a1512", "card-2": "#2b231e", "line": "#54463c", "text": "#f6ede3",
3639 "muted": "#b8a797", "accent": "#ee8435", "accent-ink": "#1a1512", "radius": 22,
37- "font": "Space Grotesk", "mono": "JetBrains Mono"},
40+ "font": "Space Grotesk", "mono": "JetBrains Mono", "texture": "lisse"},
3841 {"id": "nuit", "card": "#0f1420", "card-2": "#1a2233", "line": "#334058", "text": "#eef2fa",
3942 "muted": "#97a3ba", "accent": "#4ea8ff", "accent-ink": "#0b1020", "radius": 16,
40- "font": "Inter Tight", "mono": "JetBrains Mono"},
41- {"id": "papier", "card": "#f5f0e6", "card-2": "#e8dfcf", "line": "#c8bba5", "text": "#1e1a16",
42- "muted": "#675d52", "accent": "#bb3d24", "accent-ink": "#fff8f0", "radius": 10,
43- "font": "Fraunces", "mono": "IBM Plex Mono"},
43+ "font": "Inter Tight", "mono": "JetBrains Mono", "texture": "lisse"},
44+ {"id": "papier", "card": "#fcfaf5", "card-2": "#ddd0b8", "line": "#8a7f72", "text": "#1e1a16",
45+ "muted": "#675d52", "accent": "#bb3d24", "accent-ink": "#fff8f0", "radius": 0,
46+ "font": "Fraunces", "mono": "IBM Plex Mono", "texture": "papier"},
4447 {"id": "terminal", "card": "#0e1411", "card-2": "#18221d", "line": "#2f4239", "text": "#e6f2ea",
4548 "muted": "#8fa89a", "accent": "#3ddc97", "accent-ink": "#0b130f", "radius": 6,
46- "font": "Manrope", "mono": "Space Mono"},
49+ "font": "Manrope", "mono": "Space Mono", "texture": "lisse"},
4750 {"id": "neon", "card": "#16121f", "card-2": "#241d33", "line": "#433a5c", "text": "#f3eefc",
4851 "muted": "#a79cc0", "accent": "#c084fc", "accent-ink": "#16121f", "radius": 28,
49- "font": "Sora", "mono": "DM Mono"},
52+ "font": "Sora", "mono": "DM Mono", "texture": "lisse"},
5053 {"id": "contraste", "card": "#0a0a0a", "card-2": "#1f1f1f", "line": "#3d3d3d", "text": "#ffffff",
5154 "muted": "#a3a3a3", "accent": "#ff4b3e", "accent-ink": "#0a0a0a", "radius": 0,
52- "font": "Archivo", "mono": "IBM Plex Mono"},
55+ "font": "Archivo", "mono": "IBM Plex Mono", "texture": "lisse"},
5356 )
5457
5558 _HEX = re.compile(r"^#[0-9a-fA-F]{6}$")
@@ -66,12 +69,13 @@ def _root_var(css: str, name: str) -> str | None:
6669
6770
6871 def defaults() -> dict:
69- """Le design system de la charte de base : {couleur: #hex, radius: px, font, mono}."""
72+ """Le design system de la charte de base : {couleur: #hex, radius: px, font, mono, texture}."""
7073 css = _base_css()
7174 out = {k: _root_var(css, k) for k in COLORS}
7275 out["radius"] = int(re.sub(r"\D", "", _root_var(css, "radius") or "22") or 22)
7376 for k in FONTS:
7477 out[k] = (_root_var(css, k) or "").split(",")[0].strip().strip('"')
78+ out["texture"] = TEXTURES[0]
7579 return out
7680
7781
@@ -91,13 +95,15 @@ def save_design(values: dict) -> dict:
9195 for k in FONTS:
9296 if isinstance(values.get(k), str) and _FAMILY.match(values[k].strip()):
9397 out[k] = values[k].strip()
98+ if values.get("texture") in TEXTURES:
99+ out["texture"] = values["texture"]
94100 USER_DIR.mkdir(parents=True, exist_ok=True)
95101 DESIGN_FILE.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
96102 return out
97103
98104
99105 def style_css() -> str:
100- """La charte de base, avec les variables et les polices du design system."""
106+ """La charte de base, avec les variables et les polices du design system, puis sa matière."""
101107 d = design()
102108 css = _base_css()
103109 families = "&".join(f"family={quote_plus(d[k])}:wght@{WEIGHTS[k]}" for k in FONTS)
@@ -107,6 +113,9 @@ def style_css() -> str:
107113 values |= {k: f'"{d[k]}", {FALLBACK[k]}' for k in FONTS}
108114 for k, v in values.items():
109115 css = re.sub(rf"(--{re.escape(k)}:\s*)[^;]+;", lambda m, v=v: f"{m.group(1)}{v};", css, count=1)
116+ texture = BUILTIN_DIR / "textures" / f"{d['texture']}.css"
117+ if texture.is_file():
118+ css += texture.read_text(encoding="utf-8")
110119 return css
111120
112121
@@ -8,7 +8,8 @@ Deux couches :
8 8
9 style.css est la charte de base dont on remplace les variables par celles du design system. Les9 style.css est la charte de base dont on remplace les variables par celles du design system. Les
10 animations du kit n'emploient que ces variables : changer une couleur ou une police ne demande de10 animations du kit n'emploient que ces variables : changer une couleur ou une police ne demande de
11-régénérer aucune animation.11+régénérer aucune animation. Une matière (kit/textures/<id>.css) peut s'y ajouter : elle change l'allure
12+des briques (.card, .node…) au-delà des couleurs, sans toucher aux animations non plus.
12 """13 """
13 import hashlib14 import hashlib
14 import json15 import json
@@ -28,28 +29,30 @@ FONTS = ("font", "mono")
28 # les graisses chargées : le texte des cartes, les étiquettes mono29 # les graisses chargées : le texte des cartes, les étiquettes mono
29 WEIGHTS = {"font": "400;500;600;700", "mono": "400;500"}30 WEIGHTS = {"font": "400;500;600;700", "mono": "400;500"}
30 FALLBACK = {"font": '"Inter", system-ui, sans-serif', "mono": "ui-monospace, monospace"}31 FALLBACK = {"font": '"Inter", system-ui, sans-serif', "mono": "ui-monospace, monospace"}
32+# les matières : « lisse » est la charte telle quelle, les autres ajoutent kit/textures/<id>.css
33+TEXTURES = ("lisse", "papier")
31 34
32 # des ambiances complètes à choisir comme point de départ (le nom affiché est dans l'i18n, par id).35 # des ambiances complètes à choisir comme point de départ (le nom affiché est dans l'i18n, par id).
33 # Contrastes vérifiés sur le fond des cartes : texte ≥ 15:1, texte discret ≥ 5.5:1, accent ≥ 4.5:1.36 # Contrastes vérifiés sur le fond des cartes : texte ≥ 15:1, texte discret ≥ 5.5:1, accent ≥ 4.5:1.
34 PRESETS = (37 PRESETS = (
35 {"id": "atelier", "card": "#1a1512", "card-2": "#2b231e", "line": "#54463c", "text": "#f6ede3",38 {"id": "atelier", "card": "#1a1512", "card-2": "#2b231e", "line": "#54463c", "text": "#f6ede3",
36 "muted": "#b8a797", "accent": "#ee8435", "accent-ink": "#1a1512", "radius": 22,39 "muted": "#b8a797", "accent": "#ee8435", "accent-ink": "#1a1512", "radius": 22,
37- "font": "Space Grotesk", "mono": "JetBrains Mono"},40+ "font": "Space Grotesk", "mono": "JetBrains Mono", "texture": "lisse"},
38 {"id": "nuit", "card": "#0f1420", "card-2": "#1a2233", "line": "#334058", "text": "#eef2fa",41 {"id": "nuit", "card": "#0f1420", "card-2": "#1a2233", "line": "#334058", "text": "#eef2fa",
39 "muted": "#97a3ba", "accent": "#4ea8ff", "accent-ink": "#0b1020", "radius": 16,42 "muted": "#97a3ba", "accent": "#4ea8ff", "accent-ink": "#0b1020", "radius": 16,
40- "font": "Inter Tight", "mono": "JetBrains Mono"},43+ "font": "Inter Tight", "mono": "JetBrains Mono", "texture": "lisse"},
41- {"id": "papier", "card": "#f5f0e6", "card-2": "#e8dfcf", "line": "#c8bba5", "text": "#1e1a16",44+ {"id": "papier", "card": "#fcfaf5", "card-2": "#ddd0b8", "line": "#8a7f72", "text": "#1e1a16",
42- "muted": "#675d52", "accent": "#bb3d24", "accent-ink": "#fff8f0", "radius": 10,45+ "muted": "#675d52", "accent": "#bb3d24", "accent-ink": "#fff8f0", "radius": 0,
43- "font": "Fraunces", "mono": "IBM Plex Mono"},46+ "font": "Fraunces", "mono": "IBM Plex Mono", "texture": "papier"},
44 {"id": "terminal", "card": "#0e1411", "card-2": "#18221d", "line": "#2f4239", "text": "#e6f2ea",47 {"id": "terminal", "card": "#0e1411", "card-2": "#18221d", "line": "#2f4239", "text": "#e6f2ea",
45 "muted": "#8fa89a", "accent": "#3ddc97", "accent-ink": "#0b130f", "radius": 6,48 "muted": "#8fa89a", "accent": "#3ddc97", "accent-ink": "#0b130f", "radius": 6,
46- "font": "Manrope", "mono": "Space Mono"},49+ "font": "Manrope", "mono": "Space Mono", "texture": "lisse"},
47 {"id": "neon", "card": "#16121f", "card-2": "#241d33", "line": "#433a5c", "text": "#f3eefc",50 {"id": "neon", "card": "#16121f", "card-2": "#241d33", "line": "#433a5c", "text": "#f3eefc",
48 "muted": "#a79cc0", "accent": "#c084fc", "accent-ink": "#16121f", "radius": 28,51 "muted": "#a79cc0", "accent": "#c084fc", "accent-ink": "#16121f", "radius": 28,
49- "font": "Sora", "mono": "DM Mono"},52+ "font": "Sora", "mono": "DM Mono", "texture": "lisse"},
50 {"id": "contraste", "card": "#0a0a0a", "card-2": "#1f1f1f", "line": "#3d3d3d", "text": "#ffffff",53 {"id": "contraste", "card": "#0a0a0a", "card-2": "#1f1f1f", "line": "#3d3d3d", "text": "#ffffff",
51 "muted": "#a3a3a3", "accent": "#ff4b3e", "accent-ink": "#0a0a0a", "radius": 0,54 "muted": "#a3a3a3", "accent": "#ff4b3e", "accent-ink": "#0a0a0a", "radius": 0,
52- "font": "Archivo", "mono": "IBM Plex Mono"},55+ "font": "Archivo", "mono": "IBM Plex Mono", "texture": "lisse"},
53 )56 )
54 57
55 _HEX = re.compile(r"^#[0-9a-fA-F]{6}$")58 _HEX = re.compile(r"^#[0-9a-fA-F]{6}$")
@@ -66,12 +69,13 @@ def _root_var(css: str, name: str) -> str | None:
66 69
67 70
68 def defaults() -> dict:71 def defaults() -> dict:
69- """Le design system de la charte de base : {couleur: #hex, radius: px, font, mono}."""72+ """Le design system de la charte de base : {couleur: #hex, radius: px, font, mono, texture}."""
70 css = _base_css()73 css = _base_css()
71 out = {k: _root_var(css, k) for k in COLORS}74 out = {k: _root_var(css, k) for k in COLORS}
72 out["radius"] = int(re.sub(r"\D", "", _root_var(css, "radius") or "22") or 22)75 out["radius"] = int(re.sub(r"\D", "", _root_var(css, "radius") or "22") or 22)
73 for k in FONTS:76 for k in FONTS:
74 out[k] = (_root_var(css, k) or "").split(",")[0].strip().strip('"')77 out[k] = (_root_var(css, k) or "").split(",")[0].strip().strip('"')
78+ out["texture"] = TEXTURES[0]
75 return out79 return out
76 80
77 81
@@ -91,13 +95,15 @@ def save_design(values: dict) -> dict:
91 for k in FONTS:95 for k in FONTS:
92 if isinstance(values.get(k), str) and _FAMILY.match(values[k].strip()):96 if isinstance(values.get(k), str) and _FAMILY.match(values[k].strip()):
93 out[k] = values[k].strip()97 out[k] = values[k].strip()
98+ if values.get("texture") in TEXTURES:
99+ out["texture"] = values["texture"]
94 USER_DIR.mkdir(parents=True, exist_ok=True)100 USER_DIR.mkdir(parents=True, exist_ok=True)
95 DESIGN_FILE.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")101 DESIGN_FILE.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
96 return out102 return out
97 103
98 104
99 def style_css() -> str:105 def style_css() -> str:
100- """La charte de base, avec les variables et les polices du design system."""106+ """La charte de base, avec les variables et les polices du design system, puis sa matière."""
101 d = design()107 d = design()
102 css = _base_css()108 css = _base_css()
103 families = "&".join(f"family={quote_plus(d[k])}:wght@{WEIGHTS[k]}" for k in FONTS)109 families = "&".join(f"family={quote_plus(d[k])}:wght@{WEIGHTS[k]}" for k in FONTS)
@@ -107,6 +113,9 @@ def style_css() -> str:
107 values |= {k: f'"{d[k]}", {FALLBACK[k]}' for k in FONTS}113 values |= {k: f'"{d[k]}", {FALLBACK[k]}' for k in FONTS}
108 for k, v in values.items():114 for k, v in values.items():
109 css = re.sub(rf"(--{re.escape(k)}:\s*)[^;]+;", lambda m, v=v: f"{m.group(1)}{v};", css, count=1)115 css = re.sub(rf"(--{re.escape(k)}:\s*)[^;]+;", lambda m, v=v: f"{m.group(1)}{v};", css, count=1)
116+ texture = BUILTIN_DIR / "textures" / f"{d['texture']}.css"
117+ if texture.is_file():
118+ css += texture.read_text(encoding="utf-8")
110 return css119 return css
111 120
112 121
added backend/studio/modules/animations/kit/textures/papier.css +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+
2+/* --- matière : papier découpé ---
3+ * Les cartes et les pastilles sont des feuilles de papier posées sur la vidéo : grain, bords coupés
4+ * à la main, ombre courte. La découpe de la feuille vit à peine (4 poses par seconde, un clin d'œil
5+ * au stop motion) ; le texte posé dessus reste net et immobile.
6+ * La feuille est dessinée par ::before (et son ombre par ::after) : dans une animation, ne pas
7+ * donner de background à .card ou .node, ne pas utiliser leurs ::before/::after.
8+ */
9+:root {
10+ --paper-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .3 0 0 0 0 .24 0 0 0 0 .18 .3 0 0 0 -.06'/%3E%3C/filter%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.008 .03' numOctaves='2' seed='4' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .4 0 0 0 0 .32 0 0 0 0 .22 .1 0 0 0 -.03'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
11+}
12+
13+.card, .node { position: relative; isolation: isolate; background: none; box-shadow: none; border-radius: 0; }
14+.node.hot { background: none; }
15+
16+.card::before, .node::before, .card::after, .node::after {
17+ content: ""; position: absolute; pointer-events: none;
18+ animation: kit-paper-boil .75s steps(1, end) 240 both;
19+}
20+.card::before, .node::before { inset: 0; z-index: -1; background: var(--paper-grain), var(--card); }
21+.node::before { background: var(--paper-grain), var(--card-2); }
22+.node.hot::before { background: var(--paper-grain), var(--accent); }
23+/* l'ombre : la même découpe, décalée, posée sous la feuille */
24+.card::after, .node::after { inset: 7px -5px -9px 5px; z-index: -2; background: rgba(0, 0, 0, .22); }
25+.node::after { inset: 3px -2px -4px 2px; background: rgba(0, 0, 0, .18); }
26+
27+/* chaque feuille vit à son rythme : pas toutes ensemble */
28+.node:nth-child(3n+1)::before, .node:nth-child(3n+1)::after { animation-delay: -.25s; }
29+.node:nth-child(3n+2)::before, .node:nth-child(3n+2)::after { animation-delay: -.5s; }
30+
31+/* trois poses, à peine différentes : la feuille garde sa place et son inclinaison, seuls quelques
32+ points de la découpe bougent d'un ou deux pixels (assez pour qu'elle vive, pas pour qu'elle tremble) */
33+@keyframes kit-paper-boil {
34+ 0% {
35+ rotate: -.2deg;
36+ clip-path: polygon(3px 4px, 31% 1px, 64% 3px, calc(100% - 2px) 1px, 100% 36%, calc(100% - 3px) 71%,
37+ calc(100% - 1px) calc(100% - 3px), 67% 100%, 34% calc(100% - 4px), 2px calc(100% - 1px), 0 63%, 3px 30%);
38+ }
39+ 33.333% {
40+ rotate: -.17deg;
41+ clip-path: polygon(2px 3px, 31% 2px, 64% 2px, calc(100% - 3px) 2px, 100% 36%, calc(100% - 2px) 71%,
42+ calc(100% - 1px) calc(100% - 2px), 67% calc(100% - 1px), 34% calc(100% - 3px), 3px calc(100% - 1px), 1px 63%, 3px 30%);
43+ }
44+ 66.666% {
45+ rotate: -.22deg;
46+ clip-path: polygon(3px 3px, 31% 1px, 64% 4px, calc(100% - 2px) 2px, calc(100% - 1px) 36%, calc(100% - 3px) 71%,
47+ 100% calc(100% - 3px), 67% calc(100% - 1px), 34% calc(100% - 4px), 2px 100%, 0 63%, 2px 30%);
48+ }
49+}
50+
51+svg path, svg line, svg polyline { stroke-linecap: round; stroke-linejoin: round; }
new file mode 100644
@@ -0,0 +1,51 @@
1+
2+/* --- matière : papier découpé ---
3+ * Les cartes et les pastilles sont des feuilles de papier posées sur la vidéo : grain, bords coupés
4+ * à la main, ombre courte. La découpe de la feuille vit à peine (4 poses par seconde, un clin d'œil
5+ * au stop motion) ; le texte posé dessus reste net et immobile.
6+ * La feuille est dessinée par ::before (et son ombre par ::after) : dans une animation, ne pas
7+ * donner de background à .card ou .node, ne pas utiliser leurs ::before/::after.
8+ */
9+:root {
10+ --paper-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .3 0 0 0 0 .24 0 0 0 0 .18 .3 0 0 0 -.06'/%3E%3C/filter%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.008 .03' numOctaves='2' seed='4' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .4 0 0 0 0 .32 0 0 0 0 .22 .1 0 0 0 -.03'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
11+}
12+
13+.card, .node { position: relative; isolation: isolate; background: none; box-shadow: none; border-radius: 0; }
14+.node.hot { background: none; }
15+
16+.card::before, .node::before, .card::after, .node::after {
17+ content: ""; position: absolute; pointer-events: none;
18+ animation: kit-paper-boil .75s steps(1, end) 240 both;
19+}
20+.card::before, .node::before { inset: 0; z-index: -1; background: var(--paper-grain), var(--card); }
21+.node::before { background: var(--paper-grain), var(--card-2); }
22+.node.hot::before { background: var(--paper-grain), var(--accent); }
23+/* l'ombre : la même découpe, décalée, posée sous la feuille */
24+.card::after, .node::after { inset: 7px -5px -9px 5px; z-index: -2; background: rgba(0, 0, 0, .22); }
25+.node::after { inset: 3px -2px -4px 2px; background: rgba(0, 0, 0, .18); }
26+
27+/* chaque feuille vit à son rythme : pas toutes ensemble */
28+.node:nth-child(3n+1)::before, .node:nth-child(3n+1)::after { animation-delay: -.25s; }
29+.node:nth-child(3n+2)::before, .node:nth-child(3n+2)::after { animation-delay: -.5s; }
30+
31+/* trois poses, à peine différentes : la feuille garde sa place et son inclinaison, seuls quelques
32+ points de la découpe bougent d'un ou deux pixels (assez pour qu'elle vive, pas pour qu'elle tremble) */
33+@keyframes kit-paper-boil {
34+ 0% {
35+ rotate: -.2deg;
36+ clip-path: polygon(3px 4px, 31% 1px, 64% 3px, calc(100% - 2px) 1px, 100% 36%, calc(100% - 3px) 71%,
37+ calc(100% - 1px) calc(100% - 3px), 67% 100%, 34% calc(100% - 4px), 2px calc(100% - 1px), 0 63%, 3px 30%);
38+ }
39+ 33.333% {
40+ rotate: -.17deg;
41+ clip-path: polygon(2px 3px, 31% 2px, 64% 2px, calc(100% - 3px) 2px, 100% 36%, calc(100% - 2px) 71%,
42+ calc(100% - 1px) calc(100% - 2px), 67% calc(100% - 1px), 34% calc(100% - 3px), 3px calc(100% - 1px), 1px 63%, 3px 30%);
43+ }
44+ 66.666% {
45+ rotate: -.22deg;
46+ clip-path: polygon(3px 3px, 31% 1px, 64% 4px, calc(100% - 2px) 2px, calc(100% - 1px) 36%, calc(100% - 3px) 71%,
47+ 100% calc(100% - 3px), 67% calc(100% - 1px), 34% calc(100% - 4px), 2px 100%, 0 63%, 2px 30%);
48+ }
49+}
50+
51+svg path, svg line, svg polyline { stroke-linecap: round; stroke-linejoin: round; }
modified backend/studio/modules/projects/api.py +1 -1
@@ -117,7 +117,7 @@ def remove_animation(name: str):
117117 @router.get("/library/kit/design")
118118 def get_design():
119119 return {"design": kit.design(), "defaults": kit.defaults(), "colors": kit.COLORS, "fonts": kit.FONTS,
120- "presets": kit.PRESETS}
120+ "textures": kit.TEXTURES, "presets": kit.PRESETS}
121121
122122
123123 @router.put("/library/kit/design")
@@ -117,7 +117,7 @@ def remove_animation(name: str):
117 @router.get("/library/kit/design")117 @router.get("/library/kit/design")
118 def get_design():118 def get_design():
119 return {"design": kit.design(), "defaults": kit.defaults(), "colors": kit.COLORS, "fonts": kit.FONTS,119 return {"design": kit.design(), "defaults": kit.defaults(), "colors": kit.COLORS, "fonts": kit.FONTS,
120- "presets": kit.PRESETS}120+ "textures": kit.TEXTURES, "presets": kit.PRESETS}
121 121
122 122
123 @router.put("/library/kit/design")123 @router.put("/library/kit/design")
modified web/src/i18n/locales/en/library.ts +2 -0
@@ -33,7 +33,9 @@ export default {
3333 tokens: {
3434 card: "Card background", "card-2": "Secondary background", line: "Lines", text: "Text", muted: "Muted text",
3535 accent: "Accent", "accent-ink": "Text on accent", radius: "Corner radius", font: "Font", mono: "Mono font",
36+ texture: "Material",
3637 },
38+ textures: { lisse: "Smooth", papier: "Cut paper (stop motion)" },
3739 },
3840 tiers: { tier1: "Full screen", tier2: "Overlay", transition: "Transitions", social: "Reels 9:16", custom: "Special" },
3941 description: "Description",
@@ -33,7 +33,9 @@ export default {
33 tokens: {33 tokens: {
34 card: "Card background", "card-2": "Secondary background", line: "Lines", text: "Text", muted: "Muted text",34 card: "Card background", "card-2": "Secondary background", line: "Lines", text: "Text", muted: "Muted text",
35 accent: "Accent", "accent-ink": "Text on accent", radius: "Corner radius", font: "Font", mono: "Mono font",35 accent: "Accent", "accent-ink": "Text on accent", radius: "Corner radius", font: "Font", mono: "Mono font",
36+ texture: "Material",
36 },37 },
38+ textures: { lisse: "Smooth", papier: "Cut paper (stop motion)" },
37 },39 },
38 tiers: { tier1: "Full screen", tier2: "Overlay", transition: "Transitions", social: "Reels 9:16", custom: "Special" },40 tiers: { tier1: "Full screen", tier2: "Overlay", transition: "Transitions", social: "Reels 9:16", custom: "Special" },
39 description: "Description",41 description: "Description",
modified web/src/i18n/locales/fr/library.ts +2 -0
@@ -33,7 +33,9 @@ export default {
3333 tokens: {
3434 card: "Fond des cartes", "card-2": "Fond secondaire", line: "Traits", text: "Texte", muted: "Texte discret",
3535 accent: "Accent", "accent-ink": "Texte sur l’accent", radius: "Arrondi", font: "Police", mono: "Police mono",
36+ texture: "Matière",
3637 },
38+ textures: { lisse: "Lisse", papier: "Papier découpé (stop motion)" },
3739 },
3840 tiers: { tier1: "Plein écran", tier2: "Surcouche", transition: "Transitions", social: "Reels 9:16", custom: "Spéciales" },
3941 description: "Description",
@@ -33,7 +33,9 @@ export default {
33 tokens: {33 tokens: {
34 card: "Fond des cartes", "card-2": "Fond secondaire", line: "Traits", text: "Texte", muted: "Texte discret",34 card: "Fond des cartes", "card-2": "Fond secondaire", line: "Traits", text: "Texte", muted: "Texte discret",
35 accent: "Accent", "accent-ink": "Texte sur l’accent", radius: "Arrondi", font: "Police", mono: "Police mono",35 accent: "Accent", "accent-ink": "Texte sur l’accent", radius: "Arrondi", font: "Police", mono: "Police mono",
36+ texture: "Matière",
36 },37 },
38+ textures: { lisse: "Lisse", papier: "Papier découpé (stop motion)" },
37 },39 },
38 tiers: { tier1: "Plein écran", tier2: "Surcouche", transition: "Transitions", social: "Reels 9:16", custom: "Spéciales" },40 tiers: { tier1: "Plein écran", tier2: "Surcouche", transition: "Transitions", social: "Reels 9:16", custom: "Spéciales" },
39 description: "Description",41 description: "Description",
modified web/src/pages/library/KitDesign.vue +9 -2
@@ -1,6 +1,6 @@
11 <script setup lang="ts">
22 /**
3- * Le design system du kit : couleurs, arrondi, polices. Chaque réglage régénère style.css, que
3+ * Le design system du kit : couleurs, arrondi, polices, matière. Chaque réglage régénère style.css, que
44 * toutes les animations du kit lisent : aucune animation n'est à refaire.
55 * Point de départ au choix : une ambiance toute faite, ou les couleurs d'une image de tournage.
66 */
@@ -11,7 +11,7 @@ import { themesFromImage } from "../../lib/palette";
1111 import { toast } from "../../lib/toast";
1212
1313 type Design = Record<string, string | number>;
14-interface DesignView { design: Design; defaults: Design; colors: string[]; fonts: string[]; presets: Design[] }
14+interface DesignView { design: Design; defaults: Design; colors: string[]; fonts: string[]; textures: string[]; presets: Design[] }
1515
1616 const emit = defineEmits<{ saved: [] }>();
1717 const { t } = useI18n();
@@ -100,6 +100,12 @@ async function readPhoto(e: Event) {
100100 <input class="input" :value="view.design[k]" list="kit-fonts" @change="set(k, ($event.target as HTMLInputElement).value)">
101101 <span>{{ t(`library.design.tokens.${k}`) }}</span>
102102 </label>
103+ <label class="field" :class="{ changed: changed('texture') }">
104+ <select class="input" :value="view.design.texture" @change="set('texture', ($event.target as HTMLSelectElement).value)">
105+ <option v-for="k in view.textures" :key="k" :value="k">{{ t(`library.design.textures.${k}`) }}</option>
106+ </select>
107+ <span>{{ t("library.design.tokens.texture") }}</span>
108+ </label>
103109 </div>
104110 <!-- des polices Google courantes ; n'importe quel nom de Google Fonts marche -->
105111 <datalist id="kit-fonts">
@@ -125,4 +131,5 @@ async function readPhoto(e: Event) {
125131 .field input[type="color"] { width: 30px; height: 30px; padding: 0; border: 1px solid var(--line-strong); border-radius: 4px; background: none; cursor: pointer; }
126132 .field input[type="range"] { width: 110px; }
127133 .field.font .input { width: 170px; height: 32px; }
134+.field select.input { height: 32px; }
128135 </style>
@@ -1,6 +1,6 @@
1 <script setup lang="ts">1 <script setup lang="ts">
2 /**2 /**
3- * Le design system du kit : couleurs, arrondi, polices. Chaque réglage régénère style.css, que3+ * Le design system du kit : couleurs, arrondi, polices, matière. Chaque réglage régénère style.css, que
4 * toutes les animations du kit lisent : aucune animation n'est à refaire.4 * toutes les animations du kit lisent : aucune animation n'est à refaire.
5 * Point de départ au choix : une ambiance toute faite, ou les couleurs d'une image de tournage.5 * Point de départ au choix : une ambiance toute faite, ou les couleurs d'une image de tournage.
6 */6 */
@@ -11,7 +11,7 @@ import { themesFromImage } from "../../lib/palette";
11 import { toast } from "../../lib/toast";11 import { toast } from "../../lib/toast";
12 12
13 type Design = Record<string, string | number>;13 type Design = Record<string, string | number>;
14-interface DesignView { design: Design; defaults: Design; colors: string[]; fonts: string[]; presets: Design[] }14+interface DesignView { design: Design; defaults: Design; colors: string[]; fonts: string[]; textures: string[]; presets: Design[] }
15 15
16 const emit = defineEmits<{ saved: [] }>();16 const emit = defineEmits<{ saved: [] }>();
17 const { t } = useI18n();17 const { t } = useI18n();
@@ -100,6 +100,12 @@ async function readPhoto(e: Event) {
100 <input class="input" :value="view.design[k]" list="kit-fonts" @change="set(k, ($event.target as HTMLInputElement).value)">100 <input class="input" :value="view.design[k]" list="kit-fonts" @change="set(k, ($event.target as HTMLInputElement).value)">
101 <span>{{ t(`library.design.tokens.${k}`) }}</span>101 <span>{{ t(`library.design.tokens.${k}`) }}</span>
102 </label>102 </label>
103+ <label class="field" :class="{ changed: changed('texture') }">
104+ <select class="input" :value="view.design.texture" @change="set('texture', ($event.target as HTMLSelectElement).value)">
105+ <option v-for="k in view.textures" :key="k" :value="k">{{ t(`library.design.textures.${k}`) }}</option>
106+ </select>
107+ <span>{{ t("library.design.tokens.texture") }}</span>
108+ </label>
103 </div>109 </div>
104 <!-- des polices Google courantes ; n'importe quel nom de Google Fonts marche -->110 <!-- des polices Google courantes ; n'importe quel nom de Google Fonts marche -->
105 <datalist id="kit-fonts">111 <datalist id="kit-fonts">
@@ -125,4 +131,5 @@ async function readPhoto(e: Event) {
125 .field input[type="color"] { width: 30px; height: 30px; padding: 0; border: 1px solid var(--line-strong); border-radius: 4px; background: none; cursor: pointer; }131 .field input[type="color"] { width: 30px; height: 30px; padding: 0; border: 1px solid var(--line-strong); border-radius: 4px; background: none; cursor: pointer; }
126 .field input[type="range"] { width: 110px; }132 .field input[type="range"] { width: 110px; }
127 .field.font .input { width: 170px; height: 32px; }133 .field.font .input { width: 170px; height: 32px; }
134+.field select.input { height: 32px; }
128 </style>135 </style>