Reels: composition centree (fini le vide), tuiles auto-ajustees sur une ligne, balayage lumineux + barre de progression + halo anime
1 changed file +84 −70
modified
server/social/make_reel.py
+84 −70
@@ -86,84 +86,98 @@ def build_spotlight(ins): | ||
| 86 | 86 | def build_html(ins): |
| 87 | 87 | if ins.get('genre')=='spotlight' and (ins.get('product') or {}).get('image_data'): |
| 88 | 88 | return build_spotlight(ins) |
| 89 | − H=HREEL | |
| 90 | − pt=130 if H>=1600 else 80 | |
| 91 | − pb=400 if H>=1600 else 220 | |
| 92 | − p=pal(ins["site"]) | |
| 89 | + H=HREEL; p=pal(ins["site"]) | |
| 93 | 90 | tiles=ins.get("tiles") or [] |
| 94 | 91 | hl=str(ins["headline"]); L=len(hl) |
| 95 | − numsize = 150 if L>=9 else (180 if L>=7 else (230 if L>=5 else 300)) | |
| 96 | − tiles_html="".join(f'<div class="tile t{i}"><div class="tv">{esc(v)}</div><div class="tl">{esc(l)}</div></div>' for i,(v,l) in enumerate(tiles)) | |
| 97 | − return f"""<!doctype html><html><head><meta charset="utf-8"><style> | |
| 98 | − *{{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased}} | |
| 99 | − html,body{{width:1080px;height:{H}px;overflow:hidden;font-family:{FONT};color:#fff}} | |
| 100 | − .stage{{position:relative;width:1080px;height:{H}px; | |
| 101 | − background:radial-gradient(1200px 900px at 80% 0%, {p['a']}44,transparent 55%), | |
| 102 | − radial-gradient(1000px 1000px at -10% 100%, {p['b']}33,transparent 55%), | |
| 103 | − linear-gradient(160deg,{p['g1']},{p['g2']}); | |
| 104 | − background-size:140% 140%;animation:drift 12s ease-in-out infinite alternate}} | |
| 105 | − @keyframes drift{{0%{{background-position:0% 0%}}100%{{background-position:100% 100%}}}} | |
| 106 | − .pad{{position:absolute;inset:0;padding:{pt}px 90px {pb}px;display:flex;flex-direction:column}} | |
| 107 | − .eyebrow{{display:flex;align-items:center;gap:16px;font-weight:800;letter-spacing:.16em;font-size:28px; | |
| 108 | − text-transform:uppercase;color:{p['a']};opacity:0;animation:fadeUp .8s ease .2s forwards}} | |
| 109 | − .eyebrow .dot{{width:52px;height:6px;border-radius:3px;background:{p['a']}}} | |
| 110 | − .kabadge{{position:absolute;top:120px;right:90px;background:{p['a']};color:#0d0d0d;font-weight:900; | |
| 111 | − font-size:56px;padding:12px 26px;border-radius:20px;transform:rotate(-4deg) scale(.4);opacity:0; | |
| 112 | − animation:pop .7s cubic-bezier(.2,1.4,.4,1) .4s forwards;box-shadow:0 16px 40px {p['a']}66}} | |
| 113 | − .site{{font-size:120px;font-weight:900;letter-spacing:-.02em;margin-top:40px;opacity:0; | |
| 114 | − animation:fadeUp .8s ease .5s forwards}} | |
| 115 | − .tag{{font-size:40px;font-weight:600;color:#ffffffb0;margin-top:12px;opacity:0;animation:fadeUp .8s ease .7s forwards}} | |
| 116 | − .num{{font-size:{numsize}px;font-weight:900;line-height:.9;letter-spacing:-.03em;margin-top:80px;white-space:nowrap; | |
| 117 | − background:linear-gradient(180deg,#fff,{p['a']} 130%);-webkit-background-clip:text;-webkit-text-fill-color:transparent; | |
| 118 | − filter:drop-shadow(0 16px 50px {p['a']}55);opacity:0;transform:translateY(40px) scale(.9); | |
| 119 | − animation:heroIn 1s cubic-bezier(.2,1,.3,1) 1.0s forwards}} | |
| 120 | − .lab{{font-size:56px;font-weight:700;margin-top:30px;max-width:900px;line-height:1.12;opacity:0; | |
| 121 | − animation:fadeUp .8s ease 1.4s forwards}} | |
| 122 | − .tiles{{display:flex;gap:22px;margin-top:auto;margin-bottom:0}} | |
| 123 | − .tile{{flex:1;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.14);border-radius:26px; | |
| 124 | − padding:30px 26px;opacity:0;transform:translateY(30px);animation:fadeUp .7s ease forwards}} | |
| 125 | − .t0{{animation-delay:1.8s}}.t1{{animation-delay:2.0s}}.t2{{animation-delay:2.2s}}.t3{{animation-delay:2.4s}} | |
| 126 | − .tv{{font-size:60px;font-weight:900;color:{p['a']}}} | |
| 127 | − .tl{{font-size:29px;font-weight:600;color:#ffffffa0;margin-top:8px;line-height:1.2}} | |
| 128 | − .cta{{position:absolute;left:90px;right:90px;bottom:150px;opacity:0;animation:fadeUp .8s ease 2.8s forwards}} | |
| 129 | − .cta .go{{font-size:46px;font-weight:900;color:{p['a']}}} | |
| 130 | − .cta .brand{{font-size:30px;font-weight:700;color:#ffffff88;letter-spacing:.12em;margin-top:6px}} | |
| 131 | − .sig{{position:absolute;left:90px;bottom:90px;font-size:26px;color:#ffffff66;font-weight:600;opacity:0; | |
| 132 | − animation:fadeUp .8s ease 3.1s forwards}} | |
| 133 | − @keyframes fadeUp{{to{{opacity:1;transform:translateY(0)}}}} | |
| 134 | − @keyframes pop{{to{{opacity:1;transform:rotate(-4deg) scale(1)}}}} | |
| 135 | − @keyframes heroIn{{to{{opacity:1;transform:translateY(0) scale(1)}}}} | |
| 136 | − </style></head><body> | |
| 137 | − <div class="stage"><div class="pad"> | |
| 138 | − <div class="eyebrow"><span class="dot"></span>{esc(ins['label'].upper())} · {today().upper()}</div> | |
| 139 | − <div class="kabadge">KA</div> | |
| 140 | − <div class="site">{esc(ins['label'])}</div> | |
| 141 | − {f'<div class="tag">{esc(ins.get("tagline",""))}</div>' if ins.get('tagline') else ''} | |
| 142 | − <div class="num" id="num" data-target="{esc(ins['headline'])}">{esc(ins['headline'])}</div> | |
| 143 | − <div class="lab">{esc(ins['headline_label'])}</div> | |
| 144 | − {f'<div class="tiles">{tiles_html}</div>' if tiles else ''} | |
| 145 | − <div class="cta"><div class="go">👉 www.{ins['site']}.com</div><div class="brand">GROUPE ·KA</div></div> | |
| 146 | − <div class="sig">✶ Rédigé et publié par l'Agent KA</div> | |
| 147 | − </div></div> | |
| 148 | − <script> | |
| 149 | − // count-up si le nombre-héros est numérique | |
| 150 | − (function(){{ | |
| 151 | − var el=document.getElementById('num'); var raw=el.getAttribute('data-target'); | |
| 152 | − if(/[,%\\/]/.test(raw)) return; | |
| 153 | − var m=raw.match(/^[\\d\\u00a0\\u202f ]+/); if(!m) return; | |
| 154 | − var digits=m[0].replace(/[^\\d]/g,''); if(digits.length<3) return; | |
| 92 | + numsize = 150 if L>=9 else (185 if L>=7 else (235 if L>=5 else 300)) | |
| 93 | + tiles_html="".join('<div class="tile t'+str(i)+'"><div class="tv" data-fit>'+esc(v)+'</div><div class="tl">'+esc(l)+'</div></div>' for i,(v,l) in enumerate(tiles)) | |
| 94 | + css = """ | |
| 95 | + *{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased} | |
| 96 | + html,body{width:1080px;height:__H__px;overflow:hidden;font-family:__FONT__;color:#fff} | |
| 97 | + .stage{position:relative;width:1080px;height:__H__px;overflow:hidden; | |
| 98 | + background:radial-gradient(1300px 900px at 78% -5%, __A__55,transparent 55%), | |
| 99 | + radial-gradient(1100px 1000px at -12% 105%, __B__3a,transparent 55%), | |
| 100 | + linear-gradient(160deg,__G1__,__G2__);background-size:150% 150%;animation:drift 14s ease-in-out infinite alternate} | |
| 101 | + @keyframes drift{0%{background-position:0% 0%}100%{background-position:100% 100%}} | |
| 102 | + .mesh{position:absolute;inset:-20%;background:radial-gradient(closest-side, __A__22, transparent 70%); | |
| 103 | + filter:blur(30px);animation:float 16s ease-in-out infinite alternate;opacity:.7} | |
| 104 | + @keyframes float{0%{transform:translate(-6%, -4%) scale(1)}100%{transform:translate(8%, 6%) scale(1.15)}} | |
| 105 | + .pad{position:absolute;inset:0;padding:120px 90px 250px;display:flex;flex-direction:column} | |
| 106 | + .eyebrow{display:flex;align-items:center;gap:16px;font-weight:800;letter-spacing:.16em;font-size:27px; | |
| 107 | + text-transform:uppercase;color:__A__;opacity:0;animation:fadeUp .8s ease .15s forwards} | |
| 108 | + .eyebrow .dot{width:52px;height:6px;border-radius:3px;background:__A__} | |
| 109 | + .kabadge{position:absolute;top:118px;right:90px;background:__A__;color:#0d0d0d;font-weight:900; | |
| 110 | + font-size:54px;padding:10px 26px;border-radius:20px;transform:rotate(-4deg) scale(.4);opacity:0; | |
| 111 | + animation:pop .7s cubic-bezier(.2,1.4,.4,1) .35s forwards;box-shadow:0 16px 46px __A__66} | |
| 112 | + .site{font-size:112px;font-weight:900;letter-spacing:-.02em;margin-top:34px;line-height:1;opacity:0;animation:fadeUp .8s ease .45s forwards} | |
| 113 | + .tag{font-size:38px;font-weight:600;color:#ffffffb0;margin-top:12px;opacity:0;animation:fadeUp .8s ease .65s forwards} | |
| 114 | + .mid{flex:1;display:flex;flex-direction:column;justify-content:center;min-height:0} | |
| 115 | + .numwrap{position:relative;display:inline-block;align-self:flex-start;opacity:0;transform:translateY(40px) scale(.92);animation:heroIn .95s cubic-bezier(.2,1,.3,1) .95s forwards} | |
| 116 | + .num{font-size:__NUM__px;font-weight:900;line-height:.92;letter-spacing:-.03em;white-space:nowrap; | |
| 117 | + background:linear-gradient(180deg,#fff,__A__ 135%);-webkit-background-clip:text;-webkit-text-fill-color:transparent; | |
| 118 | + filter:drop-shadow(0 18px 55px __A__55)} | |
| 119 | + .numwrap::after{content:"";position:absolute;top:0;left:-60%;width:45%;height:100%; | |
| 120 | + background:linear-gradient(100deg,transparent,rgba(255,255,255,.55),transparent);transform:skewX(-18deg); | |
| 121 | + animation:shine 2.6s ease-in-out 1.6s infinite} | |
| 122 | + @keyframes shine{0%{left:-60%}55%{left:130%}100%{left:130%}} | |
| 123 | + .lab{font-size:54px;font-weight:700;margin-top:26px;max-width:920px;line-height:1.12;opacity:0;animation:fadeUp .8s ease 1.35s forwards} | |
| 124 | + .tiles{display:flex;gap:20px;margin-top:56px} | |
| 125 | + .tile{flex:1 1 0;min-width:0;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15); | |
| 126 | + border-radius:26px;padding:28px 22px;opacity:0;transform:translateY(30px);animation:fadeUp .7s ease forwards;backdrop-filter:blur(6px)} | |
| 127 | + .t0{animation-delay:1.7s}.t1{animation-delay:1.85s}.t2{animation-delay:2.0s}.t3{animation-delay:2.15s} | |
| 128 | + .tv{font-size:58px;font-weight:900;color:__A__;white-space:nowrap;line-height:1} | |
| 129 | + .tl{font-size:27px;font-weight:600;color:#ffffffa8;margin-top:10px;line-height:1.15} | |
| 130 | + .cta{position:absolute;left:90px;right:90px;bottom:150px;opacity:0;animation:fadeUp .8s ease 2.5s forwards} | |
| 131 | + .cta .go{font-size:46px;font-weight:900;color:__A__} | |
| 132 | + .cta .brand{font-size:30px;font-weight:700;color:#ffffff8c;letter-spacing:.12em;margin-top:6px} | |
| 133 | + .sig{position:absolute;left:90px;bottom:92px;font-size:26px;color:#ffffff6a;font-weight:600;opacity:0;animation:fadeUp .8s ease 2.8s forwards} | |
| 134 | + .prog{position:absolute;left:0;bottom:0;height:8px;background:__A__;width:0;animation:prog __DUR__s linear .2s forwards;box-shadow:0 0 20px __A__} | |
| 135 | + @keyframes prog{to{width:100%}} | |
| 136 | + @keyframes fadeUp{to{opacity:1;transform:translateY(0)}} | |
| 137 | + @keyframes pop{to{opacity:1;transform:rotate(-4deg) scale(1)}} | |
| 138 | + @keyframes heroIn{to{opacity:1;transform:translateY(0) scale(1)}} | |
| 139 | + """ | |
| 140 | + css=(css.replace("__H__",str(H)).replace("__FONT__",FONT).replace("__A__",p["a"]) | |
| 141 | + .replace("__B__",p["b"]).replace("__G1__",p["g1"]).replace("__G2__",p["g2"]) | |
| 142 | + .replace("__NUM__",str(numsize)).replace("__DUR__",str(DUR))) | |
| 143 | + tag = ('<div class="tag">'+esc(ins.get("tagline",""))+'</div>') if ins.get("tagline") else "" | |
| 144 | + tiles_block = ('<div class="tiles">'+tiles_html+'</div>') if tiles else "" | |
| 145 | + body = ('<div class="stage"><div class="mesh"></div><div class="pad">' | |
| 146 | + '<div class="eyebrow"><span class="dot"></span>'+esc(ins["label"].upper())+' · '+today().upper()+'</div>' | |
| 147 | + '<div class="kabadge">KA</div>' | |
| 148 | + '<div class="site">'+esc(ins["label"])+'</div>'+tag+ | |
| 149 | + '<div class="mid">' | |
| 150 | + '<div class="numwrap"><div class="num" id="num" data-target="'+esc(ins["headline"])+'">'+esc(ins["headline"])+'</div></div>' | |
| 151 | + '<div class="lab">'+esc(ins["headline_label"])+'</div>'+tiles_block+ | |
| 152 | + '</div>' | |
| 153 | + '<div class="cta"><div class="go">👉 www.'+ins["site"]+'.com</div><div class="brand">GROUPE ·KA</div></div>' | |
| 154 | + '<div class="sig">✦ Rédigé et publié par l\'Agent KA</div>' | |
| 155 | + '<div class="prog"></div>' | |
| 156 | + '</div></div>') | |
| 157 | + script = """<script> | |
| 158 | + (function(){ | |
| 159 | + // ajuste la taille des valeurs de tuiles pour tenir sur une ligne | |
| 160 | + document.querySelectorAll('.tv[data-fit]').forEach(function(el){ | |
| 161 | + var s=58; el.style.fontSize=s+'px'; | |
| 162 | + while(el.scrollWidth>el.clientWidth && s>26){ s-=3; el.style.fontSize=s+'px'; } | |
| 163 | + }); | |
| 164 | + var el=document.getElementById('num'); if(!el) return; var raw=el.getAttribute('data-target'); | |
| 165 | + if(/[,%\/]/.test(raw)) return; | |
| 166 | + var m=raw.match(/^[\d\u00a0\u202f ]+/); if(!m) return; | |
| 167 | + var digits=m[0].replace(/[^\d]/g,''); if(digits.length<3) return; | |
| 155 | 168 | var target=parseInt(digits,10); var suffix=raw.slice(m[0].length); |
| 156 | 169 | var start=null, dur=1400, begin=1000; |
| 157 | − function fmt(n){{return n.toLocaleString('fr-CA').replace(/[\\u00a0\\u202f,]/g,' ')}} | |
| 158 | − function step(ts){{ if(!start)start=ts; var t=ts-start; | |
| 159 | − if(t<begin){{requestAnimationFrame(step);return;}} | |
| 170 | + function fmt(n){return n.toLocaleString('fr-CA').replace(/[\u00a0\u202f,]/g,' ')} | |
| 171 | + function step(ts){ if(!start)start=ts; var t=ts-start; | |
| 172 | + if(t<begin){requestAnimationFrame(step);return;} | |
| 160 | 173 | var k=Math.min(1,(t-begin)/dur); var e=1-Math.pow(1-k,3); |
| 161 | 174 | el.textContent=fmt(Math.round(target*e))+suffix; |
| 162 | 175 | if(k<1)requestAnimationFrame(step); |
| 163 | − }} | |
| 176 | + } | |
| 164 | 177 | el.textContent=fmt(0)+suffix; requestAnimationFrame(step); |
| 165 | − }})(); | |
| 166 | − </script></body></html>""" | |
| 178 | + })(); | |
| 179 | + </script>""" | |
| 180 | + return "<!doctype html><html><head><meta charset=\"utf-8\"><style>"+css+"</style></head><body>"+body+script+"</body></html>" | |
| 167 | 181 | |
| 168 | 182 | def record_webm(html_path, out_webm, tmpdir): |
| 169 | 183 | from playwright.sync_api import sync_playwright |
| 170 | 184 | |