Files
mcn-short-video/project/短视频脚本创作/V1.0/subskills/lieflat-charts/examples/lenny-2026-survey.html
T
maogeigei cad356047c 工作台视频列表页新增点赞/评论趋势折线图(lieflat-charts G8 骨架)
- 引入 lieflat-charts 第三方图表技能到 subskills/(SKILL.md+catalog+templates,已删嵌套 .git)
- ECharts 6.1.0 本地化至 mcn-work-shop/public/vendor/(零 CDN 依赖,离线可用)
- 账号详情视频列表 tab 上方新增趋势图:X=发布时间升序,点赞/评论双序列面积折线
  (G8 Rainfall 骨架:双 grid 独立量级、同 X 联动 pointer、tipLight tooltip、quarticOut 入场)
- 全量视频数据单独拉取(pageSize=500,不随分页变化);numOf 解析 'x.x万' 文本
- V1.0/SKILL.md 登记 lieflat-charts 外部技能依赖
2026-09-02 11:09:12 +08:00

446 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>How Tech Workers Are Feeling in 2026 · 8 charts, Mono</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root{--bg:#F0EFEB;--dark:#1C1C1A;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
.pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}
.pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
.pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}
.card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
.card.dark{background:var(--dark);color:#F0EFEB}
.card.dark .sub{color:#8F8E88}
.card.dark .src{color:#55554F}
.card.wide{grid-column:1/-1}
h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
.sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
.src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
svg{width:100%;max-height:330px;display:block;margin:0 auto}
svg text{font-family:'Inter',sans-serif}
.pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
@keyframes pop{from{transform:scale(0)}to{transform:none}}
.fade{animation:fade .9s ease both}
@keyframes fade{from{opacity:0}}
.draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
@keyframes draw{to{stroke-dashoffset:0}}
@media (prefers-reduced-motion:reduce){
.pop,.fade{animation:none}
.draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
}
</style>
</head>
<body>
<div class="pagehead">
<h1>How tech workers are feeling in 2026 · eight charts</h1>
<p>Data: Lenny's Newsletter annual tech worker sentiment survey (Jul 2026). Every chart is a gallery template with only its DATA swapped — one unit is always one real person or one real answer. Scroll to draw; click any chart to replay.</p>
</div>
<div class="grid2">
<!-- 1 · type colonnade ← AI identity -->
<div class="card">
<h2>What AI did to a hundred identities</h2>
<div class="sub">one hairline = one person in a hundred · each thread joins the identity they chose · rounding ate 2 people</div>
<svg id="colonnade" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">TYPE COLONNADE · AI IDENTITY, 5 NUMBERS</div>
</div>
<!-- 2 · ballot tally ← the contradiction -->
<div class="card">
<h2>Love the work, worn out, losing faith</h2>
<div class="sub">one tick = one person in a hundred · same respondents, three different questions</div>
<svg id="tally" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">BALLOT TALLY · ENJOYMENT / BURNOUT / OPTIMISM, 3 NUMBERS</div>
</div>
<!-- 3 · hundred faces ← layoff worry, wide -->
<div class="card wide">
<h2>How worried is the room</h2>
<div class="sub">one face = one person in a hundred · the mouth is the data · dark ink piles up where worry does</div>
<svg id="faces" viewBox="0 0 720 340" preserveAspectRatio="xMidYMid meet" style="max-height:380px"></svg>
<div class="src">HUNDRED FACES · LAYOFF WORRY, 5 NUMBERS</div>
</div>
<!-- 4 · tick rows ← fears -->
<div class="card">
<h2>What they fear, tick by tick</h2>
<div class="sub">one tick = one person in a hundred who picked it · they could pick several · sorted by dread</div>
<svg id="fears" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">TICK ROWS · WORKPLACE FEARS, 4 NUMBERS</div>
</div>
<!-- 5 · hourglass stream ← ambivalence narrowing -->
<div class="card">
<h2>Ambivalence, layer by layer</h2>
<div class="sub">one tick = one person in a hundred · everyone, then both-feelings, then the openly conflicted</div>
<svg id="hourglass" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">HOURGLASS STREAM · FEELINGS ABOUT AI, 3 NUMBERS</div>
</div>
<!-- 6 · brand spectrum ← NPS by seniority -->
<div class="card">
<h2>Who would still recommend it</h2>
<div class="sub">each rank sits between NEVER and GLADLY · the pale ribbon slides left as you climb down</div>
<svg id="spectrum" viewBox="0 0 460 300" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">BRAND SPECTRUM · NPS BY SENIORITY, 3 NUMBERS</div>
</div>
<!-- 7 · radial convergence ← industry in one sentence -->
<div class="card">
<h2>A hundred answers, five ways to say it</h2>
<div class="sub">every rim node = one answer · threads bundle into the theme it landed in · chaos pulls hardest</div>
<svg id="converge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="src">RADIAL CONVERGENCE · "THE INDUSTRY IN A SENTENCE", 5 THEMES</div>
</div>
<!-- 8 · ballot rings ← AI helping at work, wide finale -->
<div class="card wide">
<h2>Is AI actually making them better?</h2>
<div class="sub">one tick = one person in a hundred · inked = better at their job because of AI · the right dial lives inside the left</div>
<svg id="rings" viewBox="0 0 720 300" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
<div class="src">BALLOT RINGS · AI PRODUCTIVITY, 2 NUMBERS</div>
</div>
</div>
<script>
// ── shared Mono tokens ──
const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
const NS='http://www.w3.org/2000/svg';
const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000; // deterministic
const D2R=Math.PI/180;
const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
const obsReveal=(id,fn)=>{
const n=document.getElementById(id);
const go=()=>{n.innerHTML='';fn(n)};
const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
io.observe(n);
n.style.cursor='pointer';
n.addEventListener('click',go);
};
// ════ 1 · type colonnade ════ AI identity: 49/27.4/13.9/5/3.2 → 49+27+14+5+3 = 98,
// rounding ate 2 people — the footer says so instead of inventing threads.
(()=>{
const TEAM=[['AMPLIFIED','I can do more, and better',49],['REDEFINED','changing shape, not clearly good or bad',27],['DESTABILIZED','less sure what is still mine',14],['DIMINISHED','I feel less essential',5],['UNCHANGED','',3]];
obsReveal('colonnade',s=>{
const N=98;
const teamY=j=>60+j*56,repoY=i=>14+i*2.9;
const blocks=TEAM.flatMap(([name,,n],h)=>Array(n).fill(h));
for(let i=0;i<N;i++){
const yi=repoY(i),j=blocks[i],yj=teamY(j);
el(s,'rect',{x:64,y:yi-1,width:4,height:2,fill:'#B0AFA9',
class:'fade',style:`animation-delay:${i*.006}s`});
if(i%10===0)txt(s,{x:58,y:yi+2.5,'font-size':5.5,fill:'#C6C5BF','text-anchor':'end',
class:'fade',style:`animation-delay:${i*.006}s`},i+1);
el(s,'path',{d:`M70 ${yi} C 160 ${yi} 190 ${yj} 262 ${yj}`,fill:'none',
stroke:'#A8A7A0','stroke-width':.6,opacity:.6,pathLength:1,
class:'draw',style:`animation-delay:${.2+i*.008}s;animation-duration:.7s`});
}
TEAM.forEach(([name,desc,n],j)=>{
const y=teamY(j);
const hub=el(s,'circle',{cx:267,cy:y,r:Math.sqrt(n)*1.7,fill:n>=27?INK:n>=14?'#6A6963':'#B0AFA9',
class:'pop',style:`animation-delay:${.6+j*.05}s`});
tip(hub,`${name} — ${n} in 100`);
txt(s,{x:280+Math.sqrt(n)*1.2,y:y+1,'font-size':8,'font-weight':800,fill:'#1C1C1A',
'letter-spacing':'.04em',class:'fade',style:`animation-delay:${.65+j*.05}s`},`${name} · ${n}`);
if(desc)txt(s,{x:280+Math.sqrt(n)*1.2,y:y+11,'font-size':5.8,'font-weight':500,fill:'#8F8E88',
class:'fade',style:`animation-delay:${.7+j*.05}s`},desc);
});
txt(s,{x:200,y:322,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.2s'},
'ONE THREAD = ONE PERSON · 49+27+14+5+3 = 98 · ROUNDING ATE THE OTHER TWO');
});
})();
// ════ 2 · ballot tally ════ same respondents, three questions:
// still enjoy the work 79 · significantly burned out 56 · optimistic 49.
(()=>{
const ASK=[['STILL ENJOY THE WORK (MODERATELY+)',79],['SIGNIFICANTLY BURNED OUT',56],['OPTIMISTIC ABOUT THEIR CAREER',49]];
obsReveal('tally',s=>{
ASK.forEach(([name,v],i)=>{
const base=88+i*78;
txt(s,{x:28,y:base-30,'font-size':7.5,'font-weight':700,fill:'#6A6963',
'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.1}s`},name);
el(s,'line',{x1:28,y1:base,x2:372,y2:base,stroke:GRID,'stroke-width':.6,
class:'fade',style:`animation-delay:${i*.1}s`});
for(let k=0;k<100;k++){
const x=28+k*3.44,picked=k<v;
const h=picked?14+rnd(k+1,i+2)*6:5+rnd(k+1,i+5)*2.5;
el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
stroke:picked?INK:'#CFCEC7','stroke-width':picked?.9:.55,
class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:'#C6C5BF',
class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
}
const lab=txt(s,{x:28+(v-1)*3.44+9,y:base-13,'font-size':11.5,'font-weight':800,fill:INK,
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.5+i*.1}s`,
class:'fade'},v);
tip(lab,`${v} in 100`);
});
txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
'ONE TICK = ONE PERSON · SAME RESPONDENTS · ALL THREE CAN BE TRUE AT ONCE');
});
})();
// ════ 3 · hundred faces ════ layoff worry 28/31/21/12/8 = 100. One face = one person;
// mouth curve deepens with worry, grey ladder fills in step — ink piles up where worry does.
(()=>{
const SEG=[
['NOT AT ALL',28,PAPER,INK,-2.6], // fill, feature color, mouth curve
['SLIGHTLY',31,'#DEDDD6',INK,-.6],
['MODERATELY',21,'#B0AFA9',INK,1.4],
['VERY',12,'#55554F',PAPER,2.6],
['EXTREMELY',8,INK,PAPER,3.6],
];
obsReveal('faces',s=>{
const COLS=20,cell=26,R=8.6,X0=98,Y0=52;
let idx=0;
SEG.forEach(([name,n,fill,fg,curve],si)=>{
for(let k=0;k<n;k++,idx++){
const col=idx%COLS,row=Math.floor(idx/COLS);
const x=X0+col*cell,y=Y0+row*cell;
const g=el(s,'g',{class:'pop',style:`animation-delay:${idx*.008}s`});
el(g,'circle',{cx:x,cy:y,r:R,fill,stroke:INK,'stroke-width':1});
el(g,'circle',{cx:x-3,cy:y-2.4,r:1,fill:fg});
el(g,'circle',{cx:x+3,cy:y-2.4,r:1,fill:fg});
if(si===4){
el(g,'line',{x1:x-4.4,y1:y-5.6,x2:x-1.8,y2:y-4.4,stroke:fg,'stroke-width':.9});
el(g,'line',{x1:x+4.4,y1:y-5.6,x2:x+1.8,y2:y-4.4,stroke:fg,'stroke-width':.9});
}
const my=y+3.2;
el(g,'path',{d:`M ${x-3.4} ${my} Q ${x} ${my+curve} ${x+3.4} ${my}`,
fill:'none',stroke:fg,'stroke-width':1});
tip(g,`${name} — one of ${n} in 100`);
}
});
// top legend: shares with names
let lx=98;
SEG.forEach(([name,n],si)=>{
txt(s,{x:lx,y:24,'font-size':8,'font-weight':800,fill:INK,
class:'fade',style:`animation-delay:${.2+si*.06}s`},n+'%');
txt(s,{x:lx+26,y:24,'font-size':6.6,'font-weight':600,fill:MUTED,
'letter-spacing':'.06em',class:'fade',style:`animation-delay:${.2+si*.06}s`},name);
lx+=Math.max(88,name.length*5.4+40);
});
// bracket: everyone past the first 28 carries some worry
const bx=98+20*26-6;
el(s,'line',{x1:bx+14,y1:Y0+26-13,x2:bx+14,y2:Y0+4*26+13,stroke:'#8F8E88','stroke-width':1,
class:'fade',style:'animation-delay:1s'});
[[Y0+26-13],[Y0+4*26+13]].forEach(([yy])=>
el(s,'line',{x1:bx+9,y1:yy,x2:bx+14,y2:yy,stroke:'#8F8E88','stroke-width':1,
class:'fade',style:'animation-delay:1s'}));
txt(s,{x:bx+22,y:Y0+2.5*26-6,'font-size':10.5,'font-weight':800,fill:INK,
class:'fade',style:'animation-delay:1.1s'},'72%');
txt(s,{x:bx+22,y:Y0+2.5*26+6,'font-size':6.6,'font-weight':600,fill:MUTED,
'letter-spacing':'.06em',class:'fade',style:'animation-delay:1.1s'},'CARRY SOME');
txt(s,{x:bx+22,y:Y0+2.5*26+15,'font-size':6.6,'font-weight':600,fill:MUTED,
'letter-spacing':'.06em',class:'fade',style:'animation-delay:1.1s'},'WORRY');
txt(s,{x:360,y:330,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
'ONE FACE = ONE PERSON IN A HUNDRED · THE MOUTH IS THE DATA · 28+31+21+12+8 = 100');
});
})();
// ════ 4 · tick rows ════ fears (multi-select): one tick = one person who picked it.
// Horizontal queues, labels stay level — readable at any size.
(()=>{
const D=[['MORE FOR SAME PAY',51],['UNSUSTAINABLE PACE',46],['QUALITY SLIPPING',41],['REPLACED BY AI',22]];
obsReveal('fears',s=>{
const X0=160,PX=3.9,y0=i=>66+i*62;
D.forEach(([name,v],i)=>{
const y=y0(i);
txt(s,{x:150,y:y+3,'font-size':7.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.08}s`},name);
el(s,'line',{x1:X0,y1:y+9,x2:X0+55*PX,y2:y+9,stroke:GRID,'stroke-width':.6,
class:'fade',style:`animation-delay:${i*.08}s`});
for(let k=0;k<v;k++){
const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:INK,'stroke-width':.9,
opacity:.55+rnd(k+3,i+5)*.45,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.8,fill:'#C6C5BF',
class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
}
const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,fill:INK,
class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
tip(lab,`${name} — ${v} of 100 · they could pick several`);
});
txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
'ONE TICK = ONE PERSON IN A HUNDRED · MULTI-SELECT · DOT MARKS EVERY FIFTH');
});
})();
// ════ 5 · hourglass stream ════ ambivalence narrowing: 100 → 77 → 51, one tick = one person.
(()=>{
const STAGE=[['ALL RESPONDENTS',100],['PICKED POSITIVE AND NEGATIVE',77],['SAID "CONFLICTED" OUTRIGHT',51]];
obsReveal('hourglass',s=>{
const CXm=175,sy=k=>50+k*104,w=c=>c/100*300;
STAGE.forEach(([name,c],k)=>{
const y=sy(k),hw=w(c)/2;
for(let t=0;t<c;t++){
const x=CXm-hw+(t+.5)/c*hw*2+(rnd(t+1,k+3)-.5)*2.4;
el(s,'line',{x1:x,y1:y-7,x2:x,y2:y+7,stroke:INK,'stroke-width':.8,
opacity:.45+rnd(t+2,k+5)*.5,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});
}
if(k<2){
const hw1=w(STAGE[k+1][1])/2;
for(let t=0;t<34;t++){
const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;
const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;
el(s,'path',{d:`M${xt} ${y+9} C${xt} ${y+44} ${xb} ${sy(k+1)-44} ${xb} ${sy(k+1)-9}`,
fill:'none',stroke:'#B0AFA9','stroke-width':.5,opacity:.32,pathLength:1,
class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});
}
const pct=Math.round(STAGE[k+1][1]/c*100);
txt(s,{x:16,y:(y+sy(k+1))/2+3,'font-size':9,'font-weight':800,fill:'#8F8E88',
class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');
txt(s,{x:16,y:(y+sy(k+1))/2+14,'font-size':6,'font-weight':600,fill:'#C6C5BF',
'letter-spacing':'.06em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'STAY IN');
}
el(s,'line',{x1:CXm+hw+6,y1:y,x2:332,y2:y,stroke:'#DEDDD6','stroke-width':.8,
class:'fade',style:`animation-delay:${.3+k*.12}s`});
txt(s,{x:336,y:y-2,'font-size':6.8,'font-weight':700,fill:'#4A4944',
'letter-spacing':'.04em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);
txt(s,{x:336,y:y+11,'font-size':11,'font-weight':800,fill:INK,
class:'fade',style:`animation-delay:${.4+k*.12}s`},c);
});
txt(s,{x:200,y:322,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
'ONE TICK = ONE PERSON · THE AVERAGE PERSON PICKED 5+ EMOTIONS · ONE PICKED 13');
});
})();
// ════ 6 · brand spectrum ════ NPS by seniority: −49 / −23 / −5.
// The template's competitor dots are dropped — Lenny has no such data, we don't invent it.
(()=>{
const ROWS=[['NEVER AGAIN','GLADLY'],['NEVER AGAIN','GLADLY'],['NEVER AGAIN','GLADLY']];
const RANK=['SENIOR / STAFF IC','VP','FOUNDER'];
const NPS=[-49,-23,-5];
const US=NPS.map(v=>(v+60)/80); // scale −60 … +20
const X0=118,X1=342,Y=i=>66+i*72,px=t=>X0+t*(X1-X0);
obsReveal('spectrum',s=>{
let d=`M${px(US[0])} ${Y(0)}`;
for(let i=1;i<3;i++)
d+=` C${px(US[i-1])} ${Y(i-1)+36} ${px(US[i])} ${Y(i)-36} ${px(US[i])} ${Y(i)}`;
el(s,'path',{d,fill:'none',stroke:'#F0EFEB','stroke-width':30,'stroke-linecap':'round',
'stroke-linejoin':'round',opacity:.9,pathLength:1,class:'draw',
style:'animation-duration:1.4s'});
ROWS.forEach(([a,b],i)=>{
const y=Y(i);
el(s,'line',{x1:X0,y1:y,x2:X1,y2:y,stroke:'#B0AFA9','stroke-width':1,
class:'fade',style:`animation-delay:${i*.08}s`});
[X0,X1].forEach(x=>el(s,'line',{x1:x,y1:y-4,x2:x,y2:y+4,stroke:'#B0AFA9','stroke-width':1,
class:'fade',style:`animation-delay:${i*.08}s`}));
txt(s,{x:X0-14,y:y+3,'font-size':8,'font-weight':600,fill:'#6A6963','text-anchor':'end',
'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},a);
txt(s,{x:X1+14,y:y+3,'font-size':8,'font-weight':600,fill:'#6A6963',
'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},b);
const xz=px((0+60)/80);
el(s,'line',{x1:xz,y1:y-5,x2:xz,y2:y+5,stroke:'#C6C5BF','stroke-width':1,
'stroke-dasharray':'2 2',class:'fade',style:`animation-delay:${.2+i*.08}s`});
const us=el(s,'circle',{cx:px(US[i]),cy:y,r:8,fill:INK,
class:'pop',style:`animation-delay:${.5+i*.1}s`});
tip(us,`${RANK[i]} — NPS ${NPS[i]}`);
txt(s,{x:px(US[i]),y:y-14,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.6+i*.1}s`,
class:'fade'},NPS[i]);
txt(s,{x:px(US[i]),y:y+22,'font-size':7,'font-weight':700,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.65+i*.1}s`},RANK[i]);
});
txt(s,{x:X0,y:284,'font-size':7.5,'font-weight':600,fill:'#B0AFA9','letter-spacing':'.12em',
class:'fade',style:'animation-delay:.9s'},'● NPS BY RANK ┊ ZERO — WHERE YES BALANCES NO');
});
})();
// ════ 7 · radial convergence ════ "the industry in a sentence": chaos 30 / too fast 17 /
// bubble 12 / excitement 11 / everything else 30. Leakage off — shares are exact.
(()=>{
const N=100,CX=200,CY=162,R=116;
const THEME=[['CHAOS · UNCERTAIN',30,-75],['TOO FAST TO KEEP UP',17,-3],['AI BUBBLE TALK',12,69],['EXCITEMENT',11,141],['EVERYTHING ELSE',30,213]];
obsReveal('converge',s=>{
const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));
const hubOf=i=>blocks[i];
const hubPt=h=>pol(CX,CY,34,THEME[h][2]);
const counts=[0,0,0,0,0];
for(let i=0;i<N;i++)counts[hubOf(i)]++;
for(let i=0;i<N;i++){
const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);
const h=hubOf(i),[hx,hy]=hubPt(h);
const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;
const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;
el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,
fill:'none',stroke:'#A8A7A0','stroke-width':.7,opacity:.55,pathLength:1,
class:'draw',style:`animation-delay:${i*.01}s`});
el(s,'circle',{cx:px,cy:py,r:1.6,fill:'#6A6963',
class:'pop',style:`animation-delay:${i*.01}s`});
}
THEME.forEach(([name,,deg],h)=>{
const [hx,hy]=hubPt(h);
const grey=name==='EVERYTHING ELSE';
const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,
fill:grey?'#B0AFA9':INK,
class:'pop',style:`animation-delay:${.5+h*.08}s`});
tip(hub,`${name} — ${counts[h]} of 100 answers`);
const [tx,ty]=pol(CX,CY,R+26,deg);
txt(s,{x:tx,y:ty,'font-size':7.5,'font-weight':800,fill:grey?'#8F8E88':INK,'text-anchor':'middle',
'dominant-baseline':'middle','letter-spacing':'.06em',
style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.6+h*.08}s`,
class:'fade'},`${name} · ${counts[h]}`);
const [gx,gy]=pol(CX,CY,R+14,deg);
el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:'#C6C5BF','stroke-width':.7,
'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});
});
txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.2s'},
'ONE RIM NODE = ONE ANSWER · THE CHAOS QUOTES SPLIT 37 POSITIVE / 37 NEGATIVE / 26 NEUTRAL');
});
})();
// ════ 8 · ballot rings ════ AI productivity: 82 say at least moderately better,
// 49 of those say very or extremely. Two dials, one story.
(()=>{
const D=[['AT LEAST MODERATELY BETTER',82],['VERY OR EXTREMELY BETTER',49]];
obsReveal('rings',s=>{
D.forEach(([name,v],i)=>{
const cx=214+i*292,cy=134,R0=64;
for(let k=0;k<100;k++){
const a=k*3.6-90,picked=k<v;
const len=picked?12+rnd(k+1,i+2)*6:4.5+rnd(k+1,i+5)*2;
const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
el(s,'line',{x1,y1,x2,y2,stroke:picked?INK:'#CFCEC7','stroke-width':picked?.95:.55,
class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
if(k%10===0){
const [dx,dy]=pol(cx,cy,R0-5,a);
el(s,'circle',{cx:dx,cy:dy,r:.8,fill:'#C6C5BF',
class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
}
}
const aEnd=v*3.6-90,[ex,ey]=pol(cx,cy,R0+18,aEnd);
el(s,'circle',{cx:ex,cy:ey,r:2.2,fill:INK,class:'pop',style:`animation-delay:${.5+i*.1}s`});
const num=txt(s,{x:cx,y:cy+2,'font-size':24,'font-weight':800,fill:INK,'text-anchor':'middle',
class:'fade',style:`animation-delay:${.4+i*.1}s`},v);
tip(num,`${name} — ${v} of 100`);
txt(s,{x:cx,y:cy+18,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
'letter-spacing':'.1em',class:'fade',style:`animation-delay:${.4+i*.1}s`},'OF 100');
txt(s,{x:cx,y:cy+R0+42,'font-size':7.5,'font-weight':700,fill:'#55554F','text-anchor':'middle',
'letter-spacing':'.06em',class:'fade',style:`animation-delay:${.5+i*.1}s`},name);
});
txt(s,{x:360,y:290,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
'ONE TICK = ONE PERSON · TWELVE O’CLOCK IS ZERO · THE RIGHT DIAL LIVES INSIDE THE LEFT');
});
})();
</script>
</body>
</html>