From 62ba1635827c1513c81bbf4e25689b89e7d39362 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 24 Sep 2026 16:50:26 -0700 Subject: [PATCH] =?UTF-8?q?Fleet:=20the=20laptops=20are=20lyric=20screens?= =?UTF-8?q?=20=E2=80=94=20seat=20colour=20edge=20to=20edge,=20the=20sung?= =?UTF-8?q?=20syllable=20lit;=20singers'=20faces=20opaque=20with=20big=20c?= =?UTF-8?q?aptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .../macneopolitan/bin/trio-fleet-plan.mjs | 1 + .../macneopolitan/fleet/native-trio.mjs | 113 +++++++----------- 2 files changed, 42 insertions(+), 72 deletions(-) diff --git a/grants/culturehub-la-2026/macneopolitan/bin/trio-fleet-plan.mjs b/grants/culturehub-la-2026/macneopolitan/bin/trio-fleet-plan.mjs index 43303f7235..34874cdf3d 100644 --- a/grants/culturehub-la-2026/macneopolitan/bin/trio-fleet-plan.mjs +++ b/grants/culturehub-la-2026/macneopolitan/bin/trio-fleet-plan.mjs @@ -64,6 +64,7 @@ export function buildPlan(score,profiles,fleet,levels={}) { const pairs=singerPayload({p:{voice,member},vj:profiles[member],score,bpm:score.bpm,epoch:0}); const info=Object.fromEntries(pairs.map(p=>{const i=p.indexOf('=');return[p.slice(0,i),p.slice(i+1)];})); delete info.startEpoch; + info.faceAlpha='1';info.captionSize='110'; // the room: a full, opaque face and big captions on every singer's screen return {member,info}; }); const parts=score.voices.map(v=>notesOf(v,score.bpm)); diff --git a/grants/culturehub-la-2026/macneopolitan/fleet/native-trio.mjs b/grants/culturehub-la-2026/macneopolitan/fleet/native-trio.mjs index 704d714246..57d7f75a95 100644 --- a/grants/culturehub-la-2026/macneopolitan/fleet/native-trio.mjs +++ b/grants/culturehub-la-2026/macneopolitan/fleet/native-trio.mjs @@ -117,80 +117,49 @@ let noteCursor=0,lyricCursor=0,routeCursor=0,lastPaintT=-1; const NOTES=72,OTHERS=40,LABELS=16,MERGE=.06; const laneLastT=[-9,-9,-9,-9]; function inkRgb(ink,r,g,b,k){ink(Math.round(r*k),Math.round(g*k),Math.round(b*k));} -export function paint({sound,wipe,ink,box,line,write,screen}){ +export function paint({sound,wipe,ink,box,write,screen}){ + // Lyric screens. Nothing else: the seat's colour edge to edge, the line + // being sung in the singer's ink, the syllable being sung lit, the next + // line faint beneath. The room's notation is on the Xbox and ac7. const w=screen.width,h=screen.height,t=origin===null?-1:sound.time-origin,active=phase==='playing'; - // The screen IS the seat's colour: full strength while a piece is prepared - // or playing, a darker shade at rest so the room still reads as six colours. - // Everything else is drawn in an ink chosen against that colour's luminance. - const color=cfg?.color||[143,209,63],lit=['prepared','countdown','playing'].includes(phase)?1:.35; - wipe(Math.round(color[0]*lit),Math.round(color[1]*lit),Math.round(color[2]*lit)); - const lum=(.2126*color[0]+.7152*color[1]+.0722*color[2])*lit,dark=lum>110; - const ir=dark?16:248,ig=dark?16:248,ib=dark?24:250; // the ink - const tint=k=>dark?[Math.round(color[0]*lit/k),Math.round(color[1]*lit/k),Math.round(color[2]*lit/k)]:[Math.round(255-(255-color[0]*lit)/k),Math.round(255-(255-color[1]*lit)/k),Math.round(255-(255-color[2]*lit)/k)]; // darker on a bright ground, lighter on a deep one - if(error){ink(ir,ig,ib);write(error,{x:8,y:h-24,font:FONT,size:1});} + const color=cfg?.color||[143,209,63]; + const live=phase==='playing'||phase==='countdown'||phase==='prepared'; + const bg=live?color:color.map(v=>Math.round(v*.35));wipe(...bg); + const lum=(0.2126*bg[0]+0.7152*bg[1]+0.0722*bg[2])/255; + const inkRGB=lum>.5?[8,8,12]:[250,250,250],dimRGB=lum>.5?[Math.round(bg[0]*.55),Math.round(bg[1]*.55),Math.round(bg[2]*.55)]:[Math.round(bg[0]*.5+110),Math.round(bg[1]*.5+110),Math.round(bg[2]*.5+110)]; if(!cfg)return; - // seat name, top left - ink(ir,ig,ib);write((cfg.label||cfg.receiverId||'').toLowerCase(),{x:10,y:10,font:FONT,size:2}); - if(phase==='countdown'){const left=Math.max(0,origin-sound.time);centered(write,String(Math.ceil(left)),Math.round(h/2-40),8,w);inkRgb(ink,ir,ig,ib,.75);centered(write,(cfg.title||'').toLowerCase(),Math.round(h/2+50),2,w);return;} - if(phase==='prepared'){inkRgb(ink,ir,ig,ib,.75);centered(write,'ready',Math.round(h/2-10),3,w);return;} - if(!active&&phase!=='finished'){inkRgb(ink,ir,ig,ib,.6);centered(write,phase,Math.round(h/2-10),2,w);return;} - const beat=60/cfg.bpm,bpb=cfg.beatsPerBar||4,bar=Math.floor(t/beat/bpb)+1,beatIn=Math.floor(t/beat)%bpb+1; - // the beat lamp (a flash of ink) and the bar count, top right - const pulse=Math.max(0,1-((t/beat)%1)*1.6);const tp=tint(1.35);ink(Math.round(tp[0]+(ir-tp[0])*pulse),Math.round(tp[1]+(ig-tp[1])*pulse),Math.round(tp[2]+(ib-tp[2])*pulse));box(w-34,10,22,22); - inkRgb(ink,ir,ig,ib,.8);const bc=`${bar}.${beatIn}`;write(bc,{x:w-44-textWidth(bc,2),y:14,font:FONT,size:2}); - // sections along the top: a thin strip, the current one in ink, its name - const secs=cfg.sections||[]; - if(secs.length){const total=cfg.duration;let cur=null;const td=tint(1.35); - for(const s of secs){const x0=Math.round(s.startSec/total*(w-20))+10,x1=Math.round(s.endSec/total*(w-20))+10,on=t>=s.startSec&&t=lyrics[lyricCursor].t+lyrics[lyricCursor].dur+.8)lyricCursor++; - let curL=null;for(let i=lyricCursor;i=routes[routeCursor].t+routes[routeCursor].dur)routeCursor++; - let r=null;for(let i=routeCursor;ir.gain))r=x;} - if(r){ - const size=fitSize(r.text,w*.6,4),tw=textWidth(r.text,size),bw=tw+30,bh=size*10+30,bx=Math.round((w-bw)/2),by=Math.round(h*.24); - const fade=Math.min(1,(t-r.t)/.15,(r.t+r.dur-t)/.4),tb=tint(r.role==='lead'?1.5:1.25);ink(tb[0],tb[1],tb[2]);box(bx,by,bw,bh);ink(ir,ig,ib);box(bx,by,bw,bh,'outline');inkRgb(ink,ir,ig,ib,.4+.6*fade);write(r.text,{x:bx+15,y:by+20,font:FONT,size}); - ink(r.rgb[0],r.rgb[1],r.rgb[2]);box(bx,by+bh-5,bw,5);ink(ir,ig,ib);write(r.role==='lead'?'LEAD':'ECHO',{x:bx+15,y:by+5,font:FONT,size:1});} - // the roll - const notes=cfg.notes||[];if(!notes.length)return; - const ahead=6,behind=1.5,head=Math.round(w/3),px=w/(ahead+behind),top=Math.round(h*.36),bottom=Math.round(h*.78),laneY=[bottom+14,bottom+32,bottom+46,bottom+60]; - const lo=cfg.midiLow||36,hi=cfg.midiHigh||108,yOf=m=>Math.round(bottom-(Math.max(lo,Math.min(hi,m))-lo)/(hi-lo)*(bottom-top)); - inkRgb(ink,ir,ig,ib,.35);line(0,bottom,w,bottom);for(const y of laneY)line(0,y,w,y); - inkRgb(ink,ir,ig,ib,.5+.5*pulse);line(head,top-10,head,laneY[3]+8); - while(noteCursorahead)break; - const kind=KIND[n.i]; - if(!n.mine&&others>=OTHERS)continue; - if(kind&&n.i!=='riser'&&n.i!=='voice'){if(n.t-laneLastT[kind.lane]=n.t&&t=0&&just<.18?1-just/.18:0; - const dimF=n.mine?1:.32,rgb=n.rgb||(kind?kind.rgb:noteRgb(n.midi||60)); - if(kind&&n.i!=='riser'&&n.i!=='voice'){ // percussion: a mark in its lane, bigger for louder - const y=laneY[kind.lane],sz=Math.max(3,Math.round(4+(n.gain||.1)*24)); - inkRgb(ink,rgb[0],rgb[1],rgb[2],dimF*(on?1:.6));if(n.i==='hat'){ink(ir,ig,ib);box(x-1,y-2,3,3);}else if(n.i==='donk'){box(x-sz/2,y-sz/2,sz,sz,'outline');}else{box(x-Math.round(sz/2),y-Math.round(sz/2),sz,sz);if(n.mine){ink(ir,ig,ib);box(x-Math.round(sz/2),y-Math.round(sz/2),sz,sz,'outline');}} - if(hit&&n.mine){ink(rgb[0],rgb[1],rgb[2]);box(x-sz,y-sz,sz*2,sz*2,'outline');} - continue; - } - if(n.i==='riser'||n.i==='voice'){const y=laneY[3];inkRgb(ink,rgb[0],rgb[1],rgb[2],dimF);box(x,y-4,len,8,'outline');if(n.text&&x0&&dt<1.2&&labels=lyrics[lyricCursor].t+lyrics[lyricCursor].dur+.8)lyricCursor++; + if(lyricCursor>0&&t=lyrics[lyricCursor].t-.3?lyrics[lyricCursor]:null; + const nxt=lyrics[lyricCursor+(cur?1:0)]||null; + if(cur){ + const words=cur.text.split(' '),syls=cur.syllables||[]; + let sung=-1;for(let i=0;i=2;size--){rows=[];let row='';for(const wd of words){const cand=row?row+' '+wd:wd;if(textWidth(cand,size)>w*.9&&row){rows.push(row);row=wd;}else row=cand;}if(row)rows.push(row);if(rows.length<=3)break;} + const rowH=size*10+Math.round(size*4),y0=Math.round(h/2-rows.length*rowH/2)-10; + ink(...dimRGB);write(cur.member,{x:Math.round((w-textWidth(cur.member,2))/2),y:y0-30,font:FONT,size:2}); + // syllables map onto characters in order; a word's syllables are its pieces + let sylIdx=0; + rows.forEach((row,r)=>{ + let x=Math.round((w-textWidth(row,size))/2),y=y0+r*rowH; + for(const wd of row.split(' ')){ + // how many syllables does this word take? consume until their joined text covers the word + let take=0,acc='';while(sylIdx+take