SPB Git forge

spb/admin-ka

Public
41commits 1branches 0releases
172.9 MBsize
maindefault branch
19 days agolast push
JavaScript 65.5% Python 17.8% CSS 13% HTML 3.7%

Reels: composition centree (fini le vide), tuiles auto-ajustees sur une ligne, balayage lumineux + barre de progression + halo anime

Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent b0eaf8e

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