Skip to content

Instantly share code, notes, and snippets.

@tompng
Last active May 20, 2024 08:41
Show Gist options
  • Save tompng/029a56686bda838961e7eebd8a1fc8f5 to your computer and use it in GitHub Desktop.
Save tompng/029a56686bda838961e7eebd8a1fc8f5 to your computer and use it in GitHub Desktop.
rk wasm signage
<link href="https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=Montserrat:wght@400;500;600;700;800&family=Poppins:wght@400;800;900&display=swap" rel="stylesheet">
<style>canvas{background:white;width:100vw;}body{background:gray;margin:0;}</style>
<script src="/dist/browser.script.iife.js"></script>
<script type="text/ruby">
Time.singleton_class.prepend Module.new{
def now
$called = true
$time ||= 0.0
end
}
require 'js'
frame = 0
fps = 30
capture_width = 1600
JS.global[:devicePixelRatio] = capture_width / JS.global[:innerWidth].to_f
completed = false
JS.global.setInterval(->{
return if completed
return unless $time
return unless $called
canvas = JS.global[:document].querySelector('canvas')
ctx = canvas.getContext('2d')
ctx.save
ctx[:fillStyle] = 'white'
ctx[:globalCompositeOperation] = 'destination-over'
ctx.fillRect(0, 0, canvas[:width], canvas[:height])
ctx.restore
data = canvas.toDataURL
JS.global.fetch('/capture', {
method: 'POST',
headers: { 'Content-Type' => 'application/json' },
body: JS.global[:JSON].stringify({data:, frame:})
})
completed = true if frame == fps * 15
frame += 1
$called = false
$time = frame.fdiv fps
}, 16)
# ffmpeg -r 30 -i images/%d.png -vcodec libx264 -pix_fmt yuv420p -s 1600x900 -r 30 -an images/rubykaigi.mp4
</script>
<script type="text/ruby" src="rubykaigi.rb"></script>
<link href="https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=Montserrat:wght@400;500;600;700;800&family=Poppins:wght@400;800;900&display=swap" rel="stylesheet">
<style>canvas{background:white;width:100vw;}body{background:gray;margin:0;}</style>
<script src="https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.5.0/dist/browser.script.iife.js"></script>
<script type="text/ruby" src="rubykaigi.rb"></script>
c=%q(require'js';L,G,R,Y,W,B='#d2f100','#80f142','#ff5450','#ffec00','#fff','#000';global=JS.global;doc=globa
l[:document];cv=doc.createElement('canvas');doc[:body].appendChild(cv);ctx=cv.getContext('2d');ga=:globalAlph
a;fs=:fillStyle;m=->{ctx.moveTo(*_1.rect)};l=->{ctx.lineTo(*_1.rect)};path=->p{ctx.beginPath;q=p.each_slice(2
){q!=_1&&m[_1];l[q=_2]}};at=->x,y,r=0,&b{ctx.save;ctx.translate(x,y);ctx.rotate(r);b[];ctx.restore};circles=-
>(*a){ctx.beginPath;2.times{(r=a[2*_1])&&m[r]&&ctx.arc(0,0,r,0,Math::PI*2,_1<1)};ctx[fs]=a[1];ctx.fill;ctx.st
roke};minsa=->s{circles[190,W];circl es[197.5,G,188-11*s];circles[166.5-3
2*s,G,157-42*s];(110-s*60).times{| i|a=1i.**i/b=(27.5-15*s);b=1i**(-~i /b);ctx[fs]=B;3.times{|j|c,d=[166-
32*s,174.1-25*s,180.4-18*s,189-1 1*s][j,2];~-i%5/3==(~i+j)%2*9&&(ctx.beg inPath;m[c*a];l[c*b];l[d*b];l[d*
a];ctx.closePath;ctx.fill)}}}; shikuwasa=->{circles[50,G,40];circles[40,L] ;path[8.times.map{1i**(_1/2%4*
0.5+_1%2*2)*40}];ctx.stroke} ;goya=->{w=->i{c,s=1i.**(i*2/3.0).rect;(1i**(i/ 30.0+c/90.0)*(48+5*s))};ctx.
beginPath;m[(w[-1]+w[1])/2 ];40.times{|i|ctx.bezierCurveTo(*[w[3*i+1],_=w[3*i+ 2],(_+w[3*i+4])/2].flat_ma
p(&:rect))};ctx.closePat h;m[37];ctx[fs]=G;ctx.arc(0,0,37,0,2*Math::PI,1);ctx.fi ll;ctx.stroke;circles[37
.5,L,27];};ruby=->{ctx [fs]=R;rb=[-90-57i,-57i,:RubyKaigi.itself&&90-57i,-124,-52, 52,124,145i].map{_1-20
i};path[rb.values_at (0,2,2,6,6,7,7,3,3,0)];[ 2024,MAY15-17 ]if(!ctx.fill);ctx.close Path;ctx.stroke;path
.(rb.values_at(0,4,4 ,1,1,5,5,2,3,6,4,7,5,7));@naha,okinawa,=ctx.stroke,1;};codes=(' c=%q('+c+');eval(c.d
elete(""<<10<<32))').s plit$/;code=->(h){ctx[:textAlign]='center';rk=/R.{3}K.{4}|2 0.{9}17/;34.times{|y,l
=codes[y]|ctx[:letterSpa cing]="#{h/1e+3*y/l.size}px";ctx[fs]='#ccc';ctx[:font]= f="#{s=h*0.8/34}px#{sp='
'<<32}ui-monospace,Menlo,C onsolas,monospace";ctx.fillText(l.gsub(rk){sp*_1.si ze},0,y=h*((y+0.7)/34-0.5)
);ctx[fs]=W;(msg=l[rk])&&(w1 =ctx.measureText(msg)[:width].to_f;f[sp]=sp+['D ela','Gothic','One,']*sp;ctx
[:font]=f;f[/.+px/]="#{s*w1/ct x.measureText(msg)[:width].to_f}px";ctx[:fo nt]=f;ctx.fillText(msg,0,y))}}
;t0=Time.now;animate=->{w=(globa l[:innerWidth].to_i*global[:devicePixel Ratio].to_f).round;h=w*9/16;cv[k
=:width].to_i!=w&&cv[k]=w;cv[k=:he ight].to_i!=h&&cv[k]=h;ctx[:lineJoi n]='round';ctx[lw=:lineWidth]=3;ct
x.save;t=(Time.now-t0)%15;ctx.scale( s=w/800.0,s);ctx.clearRect(0,0, 800,450);s=((t-7)/6.0).clamp(0,1);sc
=1+600*1e80**(-(1-s)**2)+32*(1-1/(1+1e 4**(3*s-2)));ctx.translate( 400,z=403+90/(32+sc));ctx.scale(sc,sc)
;ctx.translate(-400,-z);ctx.beginPath;w= ->x{x+(600-1200*t/(1+2* t)-9**[4*t-20,3].min+8*(Math.sin(0.011*x
-3*t)+Math.sin(0.015*x+5*t))).i};m[(1+1i)* 800];l[800i];l[a=w[ -10]];27.times{ctx.bezierCurveTo(*(0.5.*a+
a=w[30*_1+10]).rect,*(a=w[30*_1+20]).rect,*( 0.5.*a+a=w[30*_ 1+40]).rect)};ctx[fs]=Y;ctx.fill;14.times{|i
|v=1+Math.sin(34*i)*9%1;x=850+80*(i+Math.sin(9 8*i)*9%1*3) -140*v*t;b=1+Math.sin(56*i)*9%1;-50<x&&x<850&&
(y=400-t%b*(b-t%b)*200;rot=-v*(t-b);6560[i]>0?at [x,y,ro t]{circles[51,B];ctx.scale(s=0.254,s);ctx[lw]=5;
minsa[1];ctx.rotate(-rot);ctx.scale(s=0.72,s);ctx[ lw] =6;ruby[]}:at[x,y,rot,&[goya,shikuwasa][i%2]])};x=
400+10.0*[13-t,0].max**2;at[x,y=220,(x-400)/190]{min sa[0];ctx[ga]=a=((t-10)*0.4).clamp(0,1);a>0&&(at[0,1
83.5]{code[5.7]};t<13&&(at[0,170.9]{code[5.7]};[-1,1]. map{at[10.1*_1,176.91,-0.057*_1]{code[5.7]}}))};at[x,y
]{ctx[lw]=3.4;ruby[]};(0<a=4*(t-14.75))&&(ctx[ga]=a;ctx[fs]=W;ctx.fillRect(0,0,1024,1024));ctx.restore};!glob
al.setInterval(animate,~-(url=%[https://rubykaigi.org/2024/])[/^[^.]+/].bytesize));eval(c.delete(""<<10<<32))
require'js';
L,G,R,Y,W,B='#d2f100','#80f142','#ff5450','#ffec00','#fff','#000';
global = JS.global;
doc = global[:document];
cv = doc.createElement('canvas');
doc[:body].appendChild(cv);
ctx=cv.getContext('2d');
ga=:globalAlpha;
fs=:fillStyle;
m=->{ctx.moveTo(*_1.rect)};
l=->{ctx.lineTo(*_1.rect)};
path=->p{
ctx.beginPath;
q=p.each_slice(2){
q!=_1&&m[_1];
l[q=_2]
}
};
at=->x,y,r=0,&b{ctx.save;ctx.translate(x,y);ctx.rotate(r);b[];ctx.restore};
circles = ->(*a){
ctx.beginPath;
2.times{(r=a[2*_1])&&m[r]&&ctx.arc(0,0,r,0,Math::PI*2,_1<1)};
ctx[fs]=a[1];
ctx.fill;
ctx.stroke
};
minsa=->s{
circles[190,W];
circles[197.5,G,188-11*s];
circles[166.5-32*s,G,157-42*s];
(110-s*60).times{|i|
a=1i.**i/b=(27.5-15*s);
b=1i**(-~i/b);
ctx[fs]=B;
3.times{|j|
c,d=[166-32*s,174.1-25*s,180.4-18*s,189-11*s][j,2];
~-i%5/3==(~i+j)%2*9&&(
ctx.beginPath;
m[c*a];
l[c*b];
l[d*b];
l[d*a];
ctx.closePath;
ctx.fill
)
}
}
};
shikuwasa=->{
circles[50,G,40];
circles[40,L];
path[8.times.map{1i**(_1/2%4*0.5+_1%2*2)*40}];
ctx.stroke
};
goya=->{
w=->i{
c,s=1i.**(i*2/3.0).rect;
(1i**(i/30.0+c/90.0)*(48+5*s))
};
ctx.beginPath;
m[(w[-1]+w[1])/2];
40.times{|i|ctx.bezierCurveTo(*[w[3*i+1],_=w[3*i+2],(_+w[3*i+4])/2].flat_map(&:rect))};
ctx.closePath;
m[37];
ctx[fs]=G;
ctx.arc(0,0,37,0,2*Math::PI,1);
ctx.fill;
ctx.stroke;
circles[37.5,L,27];
};
ruby=->{
ctx[fs]=R;
rb=[-90-57i,-57i,:RubyKaigi.itself&&90-57i,-124,-52,52,124,145i].map{_1-20i};
path[rb.values_at(0,2,2,6,6,7,7,3,3,0)];
[2024,MAY15-17]if(!ctx.fill);
ctx.closePath;
ctx.stroke;
path.(rb.values_at(0,4,4,1,1,5,5,2,3,6,4,7,5,7));
@naha,okinawa,=ctx.stroke,1;
};
WN=85;
HN=30;
codes = (WN*HN).times.map{rand(32..126).chr}.each_slice(WN).to_a.map(&:join);
codes[HN/2][WN/2-4,9]='RubyKaigi';
codes[HN/2+1][WN/2-4,9]='2024@NAHA';
code=->(h){
ctx[:textAlign]='center';
rk=/R.{3}K.{4}|20.{9}17/;
HN.times{|y,l=codes[y]|
ctx[:letterSpacing]="#{h/1e+3*y/l.size}px";
ctx[fs]='#ccc';
ctx[:font]=f="#{s=h*0.8/HN}px#{sp=''<<32}ui-monospace,Menlo,Consolas,monospace";
ctx.fillText(l.gsub(rk){sp*_1.size},0,y=h*((y+0.7)/HN-0.5));
ctx[fs]=W;
(msg=l[rk])&&(
w1=ctx.measureText(msg)[:width].to_f;
f[sp]=sp+['Dela','Gothic','One,']*sp;
ctx[:font]=f;
f[/.+px/]="#{s*w1/ctx.measureText(msg)[:width].to_f}px";
ctx[:font]=f;
ctx.fillText(msg,0,y)
)
}
};
t0=Time.now;
animate=->{
w=(global[:innerWidth].to_i*global[:devicePixelRatio].to_f).round;
h=w*9/16;
cv[k=:width].to_i!=w&&cv[k]=w;
cv[k=:height].to_i!=h&&cv[k]=h;
ctx[:lineJoin]='round';
ctx[lw=:lineWidth]=3;
ctx.save;
t=(Time.now-t0)%15;
ctx.scale(s=w/800.0,s);
ctx.clearRect(0,0,800,450);
s=((t-7)/6.0).clamp(0,1);
sc=1+600*1e80**(-(1-s)**2)+32*(1-1/(1+1e4**(3*s-2)));
ctx.translate(400,z=403+90/(32+sc));
ctx.scale(sc,sc);
ctx.translate(-400,-z);
ctx.beginPath;
w=->x{x+(600-1200*t/(1+2*t)-9**[4*t-20,3].min+8*(Math.sin(0.011*x-3*t)+Math.sin(0.015*x+5*t))).i};
m[(1+1i)*800];l[800i];l[a=w[-10]];
27.times{ctx.bezierCurveTo(*(0.5.*a+a=w[30*_1+10]).rect,*(a=w[30*_1+20]).rect,*(0.5.*a+a=w[30*_1+40]).rect)};
ctx[fs]=Y;
ctx.fill;
14.times{|i|
v=1+Math.sin(34*i)*9%1;
x=850+80*(i+Math.sin(98*i)*9%1*3)-140*v*t;
b=1+Math.sin(56*i)*9%1;
-50<x&&x<850&&(
y=400-t%b*(b-t%b)*200;
rot=-v*(t-b);
6560[i]>0?
at[x,y,rot]{
circles[51,B];
ctx.scale(s=0.254,s);
ctx[lw]=5;
minsa[1];
ctx.rotate(-rot);
ctx.scale(s=0.72,s);
ctx[lw]=6;
ruby[]
}:
at[x,y,rot,&[goya,shikuwasa][i%2]]
)
};
x=400+10.0*[13-t,0].max**2;
at[x,y=220,(x-400)/190]{
minsa[0];
ctx[ga]=a=((t-10)*0.4).clamp(0,1);
a>0&&(
at[0,183.5]{code[5.7]};
t<13&&(at[0,170.9]{code[5.7]};
[-1,1].map{at[10.1*_1,176.91,-0.057*_1]{code[5.7]}})
)
};
at[x,y]{ctx[lw]=3.4;ruby[]};
(0<a=4*(t-14.75))&&(
ctx[ga]=a;
ctx[fs]=W;
ctx.fillRect(0,0,1024,1024)
);
ctx.restore
};
!global.setInterval(animate,~-(url=%[https://rubykaigi.org/2024/])[/^[^.]+/].bytesize);
require 'sinatra'
def ruby(x,y,w)
[(x.abs+y.abs-1)/2**0.5, y-0.5].max.abs<w
end
def generate(base_file)
w = 109
h = 34
code = File.read(base_file).delete(" \n").gsub('HN', h.to_s)
qbegin = 'c=%q('
qend = ');eval(c.delete(""<<10<<32))'
code.gsub!(/WN=85;.+NAHA'/, "codes=('#{qbegin}'+c+'#{qend}').split$/")
chars = code.gsub(/defRubyKaigi/, 'def RubyKaigi').gsub('2024,MAY15-17', ' 2024,MAY15-17 ').chars
aa = h.times.map{|y|
w.times.map{|x|
s = 1.0 * h
ruby((x - w / 2) / s, (h - 2 * y) / s - 0.2, 0.035) ? ' ' : '#'
}.join
}
aa[0][0,qbegin.size] = qbegin
aa.last[-qend.size..] = qend
aa.join("\n").gsub('#') do
chars.shift || ';'
end
end
get '/rubykaigi.rb' do
content_type 'text/plain'
code = generate('rubykaigi_code.rb')
File.write "rubykaigi.rb", code + "\n"
code
end
get '/' do
html = File.read 'rubykaigi.html'
html.gsub(%r{https://.+\.js}, '/dist/browser.script.iife.js')
end
mime_types = {
html: 'text/html',
js: 'application/javascript',
wasm: 'application/wasm'
}
%w[rubykaigi.html capture.html dist/browser.script.iife.js dist/ruby+stdlib.wasm].each do |path|
get "/#{path}" do
content_type mime_types[path.split('.').last.to_sym]
File.read path
end
end
post '/capture' do
params = JSON.parse request.body.read
frame = params['frame']
data = params['data'][22..].unpack1('m')
File.write "images/#{frame}.png", data
'ok'
end
puts generate 'rubykaigi_code.rb'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment