Something went wrong. Try again.
a Bluesky client for PICO-8 picosky.vinnymac.dev
pico-8 simulation bluesky games
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415-- draw
function _draw() if scr==scr_demo then draw_demo() return end if scr==scr_splash then draw_intro() return end if theme==1 then pal(0,129,1) end -- dark page ~#151d28 cls(bg()) if scr==scr_boot or scr==scr_auth then draw_splash() elseif scr==scr_compose then local te=mid(0,(time()-scr_wt)*3,1) local ee=1-te*te*(3-2*te) draw_list(0,0) -- feed/thread visible underneath the rising drawer camera(0,-flr(ee*64)) -- compose starts half-screen below (immediately visible) rectfill(0,0,127,127,bg()) -- opaque backdrop: no timeline bleed-through draw_compose() camera() else local te=mid(0,(time()-scr_wt)*3,1) local ee=1-te*te*(3-2*te) if prev_was_compose then draw_list(0,0) -- timeline static; compose drawer retreats downward camera(0,-flr((1-ee)*128)) -- exits below (not above) rectfill(0,0,127,127,bg()) line(0,9,127,9,muted()) camera() else draw_list(flr(ee*128)*trans_dir,flr((1-ee)*8)) end end draw_status() if theme==1 then pal(0,0,1) endend
-- colors per themefunction tc(a,b) return theme==0 and a or b end -- pick by theme (day/night)function fg() return tc(1,7) endfunction muted() return tc(13,6) endfunction bg() return tc(7,0) end
-- gently flowing wavy divider (the header underline) - a little whimsy.function wave(y,c) for x=0,127 do pset(x,y+sin(x/24+time()/3),c) endend
-- card-area bottom y: shared by draw_list clip + marquee clip so they can't disagree.function cardb() return in_menu and 110 or status_showing() and 111 or 118end
-- wing-flap frame: a,b,c,b cycle (sprites 0,4,6,4)bfly_seq={0,4,6,4}function bfly_frame() return bfly_seq[flr(time()*6)%4+1]end
-- big: 16x16 (sprites 0/4/6 with 2x2). anim: flap when truefunction draw_butterfly(x,y,big,anim) if big then spr(anim and bfly_frame() or 0,x,y,2,2) else spr(2,x,y) endend
-- word-wrap s into lines of at most maxc chars (4px font)function wrap(s,maxc) maxc=max(1,maxc) local lines,cur={},"" local word="" local function flush_word() if #word==0 then return end if #cur+#word>maxc and #cur>0 then add(lines,cur) cur="" end while #word>maxc do -- hard-break very long words add(lines,sub(word,1,maxc)) word=sub(word,maxc+1) end cur=cur..word word="" end for i=1,#s do local c=sub(s,i,i) if c=="\n" then flush_word() add(lines,cur) cur="" elseif c==" " then flush_word() if #cur<maxc then cur=cur.." " end else word=word..c end end flush_word() add(lines,cur) return linesend
-- butterfly with drop-shadow: shadow pass (12,13->1) then 2-colour wings (matches the logo). shared by intro + demo.function bflysh(sx,dx,dy,w) pal(12,1) pal(13,1) sspr(sx,0,16,16,dx+1,dy+1,w,w) pal(12,12) pal(13,1) sspr(sx,0,16,16,dx,dy,w,w) pal(13,13)end
function draw_intro() cls(0) local b=sin(time()/2)*2 local sc=1.5 local w=16*sc local dx,dy=64-w/2,44+b-w/2 bflysh(bfly_sx(6),dx,dy,w) print_c("\^ppicosky",73,7) local h="@vinnymac.dev" print(h,127-print(h,0,200),120,6)end
function draw_splash() local b=sin(time()/2)*2 draw_butterfly(56,40+b,true,true) local msg = scr==scr_auth and "session lost - sign in on the page" or status print_c(msg,80,fg())end
function status_showing() return time()-status_t<3 end
-- status banner sits above footer so it never covers the nav line.function draw_status() if not status_showing() then return end rectfill(0,113,127,119,12) print(sub(status,1,31),2,114,7)end
-- header: "picosky" or "picosky - <feed>", trimmed to clear butterfly at x=118.function feed_title() if #feeds<=1 then return "picosky" end return "picosky - "..feeds[active_feed]end
function draw_list(dx,titl) dx,titl=dx or 0,titl or 0 local mu=muted() -- rolling-pin title: old headline rolls up and out, new rolls in from behind the wave clip(0,0,128,9) print(prev_was_thread and "thread" or feed_title(),2,1-titl,12) print(scr==scr_thread and "thread" or feed_title(),2,9-titl,12) clip() draw_butterfly(118,0,false) wave(9,tc(6,1)) -- card-area bottom: shrinks for status banner or footer menu info row. local cb=cardb() clip(0,11,128,cb-11) -- card area; keeps tall cards off the nav bar if dx~=0 then -- true push: draw outgoing content sliding away on the other side local op=posts==feed and thread or feed camera(128*trans_dir-dx,0) local oy=12 for p in all(op) do if oy>cb+8 then break end oy=draw_card(p,oy,false) end camera() end if #posts==0 then camera(-dx,0) draw_butterfly(56,46,true,true) print_c(loading and "loading..." or "nothing here yet",70,mu) camera() clip() return end camera(-dx,0) -- incoming content slides in from opposite side local y=12 for i=scroll+1,#posts do if y>cb-2 then break end y=draw_card(posts[i],y,i==sel) end if y<=cb-6 then if loading and #posts>0 then print_c("loading...",y+1,mu) elseif hasmore then print_c("\131 more",y+1,mu) elseif #posts>0 then print_c("-- end --",y+1,mu) end end camera() clip() -- bottom fade: two lines signal clipped content below line(0,cb,127,cb,tc(6,1)) line(0,cb+1,127,cb+1,0) -- footer: the menu when the cursor is in it, else context hints. -- the 🅾-button label adapts to platform (z key vs on-screen o). if in_menu then draw_menu_bar() return end -- footer hints: o→menu, ❎→thread/back, ⬅/➡ collapse/expand. local sp=posts[sel] local is_exp=sp and sp.expanded local has_replies=sp and (sp.replies!="0" or sp.depth>0) local is_expandable=sp and sp.lines and #sp.lines>3 and not is_exp local o=obtn() local la,ra="\139","\145" if scr==scr_thread then if is_exp then print_c(la.."\23collapse \151\23back "..o.."\23menu",121,mu) elseif is_expandable then print_c(ra.."\23expand \151\23back "..o.."\23menu",121,mu) else print_c("\151\23back "..o.."\23menu",121,mu) end else if is_exp then print_c(la.."\23collapse "..o.."\23menu",121,mu) elseif is_expandable and has_replies then print_c(ra.."\23expand \151\23thread "..o.."\23menu",121,mu) elseif is_expandable then print_c(ra.."\23expand "..o.."\23menu",121,mu) elseif has_replies then print_c("\151\23thread "..o.."\23menu",121,mu) else print_c(o.."\23menu",121,mu) end endend
-- footer menu bar: icon row, selected on accent chip, contextual label above.function draw_menu_bar() local p=posts[sel] -- opaque bg so card content never bleeds into footer regardless of clip. rectfill(0,112,127,127,bg()) local mu=muted() -- like/repost reflect post state (filled red heart / green recycle). local n=#menu_icons local x=62-n*6 -- centers n icons (pitch 12) + an 8px "•" group separator for i=1,n do -- "•" separates the per-post engagement actions (1-4) from the global ones (5-7) if i==5 then rectfill(x+1,116,x+2,117,mu) x+=8 end local s=menu_icons[i] if s==11 and p and p.liked then s=10 pal(7,8) elseif s==9 and p and p.reposted then pal(7,11) else pal(7,i==menu_sel and 7 or mu) end if i==menu_sel then rectfill(x-2,112,x+9,121,12) end spr(s,x,113) x+=12 end pal(7,7) print_c(obtn().."\23sel x\23back",123,mu)end
-- scroll s in [tx,tx+avail) when it overflows; holds ~2s then loops. one clock per active post.function marquee(s,tx,ty,avail,col,rid) if rid~=alt_ref then alt_ref=rid alt_t0=time() end local span=#s*4+24 local el=(time()-alt_t0)%(2+span/14) local off=el<2 and 0 or flr((el-2)*14) clip(tx,ty,avail,7) print(s,tx-off,ty,col) print(s,tx-off+span,ty,col) clip(0,11,128,cardb()-11)end
function draw_card(p,y,active) local x=2+p.depth*4 local mu=muted() -- theme is stable for the whole card; cache the muted colour local maxc=flr((120-x)/4) p.lines=p.lines or wrap(p.text,maxc) local total=#p.lines
-- expanded window uses full card area (98px); timeline pins expanded post to top. local img_h=0 if p.img_state==2 and p.pixels then img_h=35 elseif p.img_state==1 then img_h=11 end local fit=max(1,flr((68-img_h)/7))
-- collapsed: cap at 3 lines. expanded: cap at however many fit on screen. local cap=p.expanded and fit or 3 p.vis_lines=cap -- expose to update_nav for scroll clamping
-- scroll offset: only meaningful when expanded local offset=p.expanded and (p.scroll or 0) or 0 offset=mid(0,offset,max(0,total-cap)) if p.expanded then p.scroll=offset end -- write back clamped value
local nl=min(cap,total-offset)
-- 16x16 avatar circle-cropped; else butterfly fallback inset to 13x13. if p.avatar_pixels then for pi=1,256 do local px,py=(pi-1)%16,(pi-1)\16 local dx,dy=px-7.5,py-7.5 if dx*dx+dy*dy<=64 then pset(x+px,y+py,p.avatar_pixels[pi]) end end else sspr(0,0,16,16,x+1,y+1,13,13) end -- reltime (native uppercase) at the top-right of line 1 local t=upper(p.reltime) local tx=123 if #t>0 then tx=123-#t*4 print(t,tx,y,mu) pset(tx-3,y+2,mu) -- middot separator end -- name on line 1, @handle on line 2 (m3x6); name now fills the freed width local tox=x+18 poke(0x5f58,0x81) print(sub(p.name,1,flr((tx-tox-2)/4)),tox,y,fg()) print("@"..upper(sub(p.handle,1,flr((121-tox)/4))),tox,y+7,mu) poke(0x5f58,0)
-- body lines: m3x6 font, uppercased (message contents) poke(0x5f58,0x81) local ty=y+17 for i=1,nl do local line_idx=offset+i local lx=print(upper(p.lines[line_idx]),x,ty,fg()) -- ellipsis on last visible line when there's more content below if i==nl and (offset+nl)<total then print("...",lx,ty,mu) end ty+=7 end poke(0x5f58,0)
-- expanded-post scrollbar: right-edge thumb showing scroll position if p.expanded and total>cap then local y0,th=y+17,cap*7 local sh=max(2,th*cap\total) local sy2=y0+(th-sh)*offset\(total-cap) rectfill(126,y0,126,y0+th-1,mu) rectfill(126,sy2,126,sy2+sh-1,12) end
-- stats row local sy=ty+3 pal(7,mu) local sx=x spr(8,sx,sy) sx=print(p.replies,sx+9,sy+1,7)+4 spr(9,sx,sy) sx=print(p.reposts,sx+9,sy+1,7)+4 -- heart: outline (sprite 11) when not liked; filled (10) in bsky red when liked if p.liked then pal(7,8) end spr(p.liked and 10 or 11,sx,sy) if p.liked then pal(7,mu) end print(p.likes,sx+9,sy+1,7) pal(7,7) local bottom=sy+9
-- image thumbnail below stats row if p.img_state==1 then print("loading...",x,bottom+3,mu) bottom+=11 elseif p.img_state==2 and p.pixels then local iy=bottom+3 for pi=1,1024 do pset(x+(pi-1)%32,iy+((pi-1)\32),p.pixels[pi]) end bottom=iy+32 -- alt text below thumbnail; marquee when long + selected. if #p.alt>0 then poke(0x5f58,0x81) local a,avail,ay=upper("alt: "..p.alt),120-x,bottom+2 if active and #a*4>avail then marquee(a,x,ay,avail,mu,p.refid) else print(sub(a,1,flr(avail/4)),x,ay,mu) end poke(0x5f58,0) bottom=ay+7 end end
-- link card chip: dark pill showing the title; marquee it when selected + too long if p.linktext and #p.linktext>0 then local maxw=120-x if active and #p.linktext*4>maxw-4 then rectfill(x,bottom+2,x+maxw,bottom+9,1) -- full-width pill marquee(p.linktext,x+2,bottom+3,maxw-4,6,p.refid) else local chars=min(#p.linktext,flr((maxw-4)/4)) rectfill(x,bottom+2,x+chars*4+4,bottom+9,1) print(sub(p.linktext,1,chars),x+2,bottom+3,6) end bottom+=11 end
if active then line(0,y,0,bottom-2,12) end -- selection accent if p.depth>0 then line(2,y,2,bottom-2,mu) end -- thread connector line(0,bottom,127,bottom,tc(6,1)) p.h=bottom+2-y -- cache drawn height for the scroll clamp (single source of truth) return bottom+2end
function draw_compose() print(quote_of>0 and sub("quote: "..quote_name,1,31) or reply_to>0 and "reply" or "new post",2,1,12) line(0,9,127,9,muted()) -- word-wrapped draft body local lines=wrap(draft,30) local y=14 local lastx=2 poke(0x5f58,0x81) -- draft renders in m3x6, uppercased for i=1,#lines do print(upper(lines[i]),2,y,fg()) if i==#lines then lastx=2+#lines[i]*4 end y+=7 end poke(0x5f58,0) -- blinking cursor at end of last line if sin(time()*2)>0 then rectfill(lastx,y-7,lastx+2,y-2,12) end -- counter local n=#draft print(n.."/"..maxchars,2,118,n<=maxchars and muted() or 8) -- right-aligned (the 🅾/❎ glyphs are wide, so measure rather than assume 4px/char) local h=mobile() and "\142\23send \151\23back \142+\151\23kbd" or "enter\23send esc\23back" print(h,124-print(h,0,200),118,muted())end
-- centered print; measures real width so wide glyphs (🅾/❎) center correctly.function print_c(s,y,c) print(s,64-print(s,0,200)\2,y,c)end