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.
5e269af parent: 5607e6c modified
backend/studio/modules/animations/kit.py +20 -11 | @@ -8,7 +8,8 @@ Deux couches : | ||
| 8 | 8 | |
| 9 | 9 | style.css est la charte de base dont on remplace les variables par celles du design system. Les |
| 10 | 10 | 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 | 14 | import hashlib |
| 14 | 15 | import json |
| @@ -28,28 +29,30 @@ FONTS = ("font", "mono") | ||
| 28 | 29 | # les graisses chargées : le texte des cartes, les étiquettes mono |
| 29 | 30 | WEIGHTS = {"font": "400;500;600;700", "mono": "400;500"} |
| 30 | 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 | 35 | # des ambiances complètes à choisir comme point de départ (le nom affiché est dans l'i18n, par id). |
| 33 | 36 | # Contrastes vérifiés sur le fond des cartes : texte ≥ 15:1, texte discret ≥ 5.5:1, accent ≥ 4.5:1. |
| 34 | 37 | PRESETS = ( |
| 35 | 38 | {"id": "atelier", "card": "#1a1512", "card-2": "#2b231e", "line": "#54463c", "text": "#f6ede3", |
| 36 | 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 | 41 | {"id": "nuit", "card": "#0f1420", "card-2": "#1a2233", "line": "#334058", "text": "#eef2fa", |
| 39 | 42 | "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"}, | |
| 44 | 47 | {"id": "terminal", "card": "#0e1411", "card-2": "#18221d", "line": "#2f4239", "text": "#e6f2ea", |
| 45 | 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 | 50 | {"id": "neon", "card": "#16121f", "card-2": "#241d33", "line": "#433a5c", "text": "#f3eefc", |
| 48 | 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 | 53 | {"id": "contraste", "card": "#0a0a0a", "card-2": "#1f1f1f", "line": "#3d3d3d", "text": "#ffffff", |
| 51 | 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 | 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 | 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 | 73 | css = _base_css() |
| 71 | 74 | out = {k: _root_var(css, k) for k in COLORS} |
| 72 | 75 | out["radius"] = int(re.sub(r"\D", "", _root_var(css, "radius") or "22") or 22) |
| 73 | 76 | for k in FONTS: |
| 74 | 77 | out[k] = (_root_var(css, k) or "").split(",")[0].strip().strip('"') |
| 78 | + out["texture"] = TEXTURES[0] | |
| 75 | 79 | return out |
| 76 | 80 | |
| 77 | 81 | |
| @@ -91,13 +95,15 @@ def save_design(values: dict) -> dict: | ||
| 91 | 95 | for k in FONTS: |
| 92 | 96 | if isinstance(values.get(k), str) and _FAMILY.match(values[k].strip()): |
| 93 | 97 | out[k] = values[k].strip() |
| 98 | + if values.get("texture") in TEXTURES: | |
| 99 | + out["texture"] = values["texture"] | |
| 94 | 100 | USER_DIR.mkdir(parents=True, exist_ok=True) |
| 95 | 101 | DESIGN_FILE.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8") |
| 96 | 102 | return out |
| 97 | 103 | |
| 98 | 104 | |
| 99 | 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 | 107 | d = design() |
| 102 | 108 | css = _base_css() |
| 103 | 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 | 113 | values |= {k: f'"{d[k]}", {FALLBACK[k]}' for k in FONTS} |
| 108 | 114 | for k, v in values.items(): |
| 109 | 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 | 119 | return css |
| 111 | 120 | |
| 112 | 121 | |
| @@ -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. Les | 9 | 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 de | 10 | 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 hashlib | 14 | import hashlib |
| 14 | import json | 15 | import json |
| @@ -28,28 +29,30 @@ FONTS = ("font", "mono") | |||
| 28 | # les graisses chargées : le texte des cartes, les étiquettes mono | 29 | # 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 out | 79 | 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 out | 102 | 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 css | 119 | 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): | ||
| 117 | 117 | @router.get("/library/kit/design") |
| 118 | 118 | def get_design(): |
| 119 | 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 | 123 | @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 { | ||
| 33 | 33 | tokens: { |
| 34 | 34 | card: "Card background", "card-2": "Secondary background", line: "Lines", text: "Text", muted: "Muted text", |
| 35 | 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 | 40 | tiers: { tier1: "Full screen", tier2: "Overlay", transition: "Transitions", social: "Reels 9:16", custom: "Special" }, |
| 39 | 41 | 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 { | ||
| 33 | 33 | tokens: { |
| 34 | 34 | card: "Fond des cartes", "card-2": "Fond secondaire", line: "Traits", text: "Texte", muted: "Texte discret", |
| 35 | 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 | 40 | tiers: { tier1: "Plein écran", tier2: "Surcouche", transition: "Transitions", social: "Reels 9:16", custom: "Spéciales" }, |
| 39 | 41 | 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 @@ | ||
| 1 | 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, que | |
| 3 | + * Le design system du kit : couleurs, arrondi, polices, matière. Chaque réglage régénère style.css, que | |
| 4 | 4 | * toutes les animations du kit lisent : aucune animation n'est à refaire. |
| 5 | 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 | 11 | import { toast } from "../../lib/toast"; |
| 12 | 12 | |
| 13 | 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 | 16 | const emit = defineEmits<{ saved: [] }>(); |
| 17 | 17 | const { t } = useI18n(); |
| @@ -100,6 +100,12 @@ async function readPhoto(e: Event) { | ||
| 100 | 100 | <input class="input" :value="view.design[k]" list="kit-fonts" @change="set(k, ($event.target as HTMLInputElement).value)"> |
| 101 | 101 | <span>{{ t(`library.design.tokens.${k}`) }}</span> |
| 102 | 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 | 109 | </div> |
| 104 | 110 | <!-- des polices Google courantes ; n'importe quel nom de Google Fonts marche --> |
| 105 | 111 | <datalist id="kit-fonts"> |
| @@ -125,4 +131,5 @@ async function readPhoto(e: Event) { | ||
| 125 | 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 | 132 | .field input[type="range"] { width: 110px; } |
| 127 | 133 | .field.font .input { width: 170px; height: 32px; } |
| 134 | +.field select.input { height: 32px; } | |
| 128 | 135 | </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, que | 3 | + * 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> |