Dweeting Outside the Box
This is a continuation of my previous post on Dwitter, where I gave an overview of Dwitter and a few JavaScript golfing tricks.
The subject was the "default dweet", which renders 9 bars swaying back and forth. I'll keep the subject the same, but show rendering those 9 bars in different ways.
Changing up how you render a scene often opens up new opportunities for visual effects because it's like stepping into another dimension.
Let's start with recalling the default dweet:
c.width|=0;for(i=9;i--;)x.fillRect(400+i*100+S(t)*300,400,50,200)
Scale
Seeing that the bars click into a 50x50 grid, one might be tempted to scale the context to save on digits. Let's scale the context by 50 and divide all dimensions by 50:
c.width|=0;x.scale(50,50);for(i=9;i--;)x.fillRect(8+i*2+S(t)*6,8,1,4)
In this particular case, the extra length of x.scale(50,50)
is not offset by
the savings we got from fewer digits. We went from 65 to 69 characters. The
function call overhead (x.scale()
) costs more characters than what we save by
using smaller numbers.
In tiny-space coding, intuitions often don't pan out, you just have to try things for fit.
Transform
Instead of positioning the elements of the scene by x and y offsets from the top
left, we can use translate()
to shift the frame of reference to the center of
the canvas. This can yield two benefits:
- If there are multiple primitives in the scene, we don't have to repeat adding offsets to them and therefore save on characters.
- We can use
rotate()
to rotate them around the center of the canvas.
translate(960, 540)
moves the origin (0, 0)
to the center of the canvas.
Assuming we want to rotate by angle A
using rotate(A)
, we can combine
these stacked transformations into a single setTransform()
call, saving
precious space. Let's also throw Z
, the zoom factor, into the mix. The verbose
form is:
x.setTransform(C(A) * Z, S(A) * Z, -S(A) * Z, C(A) * Z, 960, 540);
Here's the default scene using such combined transformations:
for(c.width|=i=9,Z=1,A=0;i--;x.fillRect(-560+i*100+S(t)*300,-140,50,200))x.setTransform(k=C(A)*Z,z=S(A)*Z,-z,k,960,540)
Note the k
and z
variable assignments that reduce repetition. It makes
intuitive sense to assign repeated expressions to variables, but it only saves
space when the expression we're substituting is longer than 2-3 characters, or
when it's repeated for more than 2 times.
This transformation setup enables perfect-loop animations like so:
for(c.width|=i=9;i--;x.fillRect(-425+i*100,-103,99-p**.3*50,206))p=t/2%1,Z=2.26+p*7.34,x.setTransform(k=C(A=1.57*p)*Z,z=S(A)*Z,-z,k,960,540)
And so:
eval(unescape(escape`挮睩摴桼㵦㵢㴾房❣汥慲剥捴✺❦楬汒散琧㭳㵓⡴⤻娽㤵〪猪⨴⬵〻甽䌨琩⩚㭸学⡵㸰⥝⠰ⰰⰲ攳ⰲ攳⤻砮瑲慮獦潲洨甬稽猪娬Ⱶⰹ㘰ⰵ㐰⤻景爨椽ㄸ㭩ⴭ㬩硛昨椦ㄩ崨椭㤬ⴲⰱⰴ⤻`.replace(/u(..)/g,"$1%")))
That gibberish? It's an oft-used compression hack to stuff more than 140 characters into a dweet. The raw characters are encoded as UTF-16 code units, and then escaped as UTF-8 code units. While this doesn't technically violate the "140 characters" rule of Dwitter (since the rule was about characters, not bytes), it's not as pleasing as fitting a dweet into 140 characters without compression.
A quick way to see what the uncompressed version is, is to replace the eval()
with a throw
. The perpetual-beta version of Dwitter comes with a toggle
to show uncompressed code.
Here's the uncompressed version of the dweet above:
c.width|=f=b=>b?'clearRect':'fillRect';s=S(t);Z=950*s**4+50;u=C(t)*Z;x[f(u>0)](0,0,2e3,2e3);x.transform(u,z=s*Z,-z,u,960,540);for(i=18;i--;)x[f(i&1)](i-9,-2,1,4)
To compress, you can use the wonderful CapJS tool created by the one and only Frank Force.
Slice and Dice
We can chop up the bars into tiny squares and perturb their individual positions or colors to create interesting effects. Like this specular highlight:
c.width|=0;for(j=n=10;--j;)for(i=100;i--;X=i%5+j*n+S(t)*30,Y=i/5|0,x.fillStyle=R(r=255-(X-50)**2-Y**2,r,r),x.fillRect(400+X*n,400+Y*n,n,n));
And this twirl:
c.width|=0;for(j=9;j--;)for(i=100;i--;X=i%5+j*10+S(t)*30,Y=i/5|0,x.fillRect(400+X*10+S(t+X+Y)*C(t)*9,400+Y*10,10,10));
Note the i/5|0
expression that exploits the fact that JavaScript floors
numbers prior to performing binary operations like the binary OR used here. This
is much shorter than Math.floor(i/5)
.
Another technique to note is the elimination of nested loops for X and Y. There is a single loop and the X and Y values are derived through modulo and division. This sometimes saves space, depending on how many times the X and Y values are used.
Also note the abuse of the comma operator to perform multiple operations. This
is usually shorter than creating a {}
code block with statements separated by
semicolons.
XOR
We can also play with different compositing operations. Here, I'm overlaying a bunch of rectangles in XOR mode to let them alternate between black and white to create the default bars. You have to wait a bit to see the reveal:
c.width=1920
x.globalCompositeOperation='xor'
for(i=18;i--;)x.fillRect(400+i*50+S(t)*300,400+i*S(t/9)**9*200,2e3,200)
Thematic End
And here's a thematic end to this post. Not quite the default bars, but they're hidden in there:
c.width|=0
for(i=17;i--;)for(j=5;j--;)[2057,1,32897,0,2057][j]+87380&1<<i&&x.fillRect(1200-i*50+S(t)*300,400+j*40,50,40)
A Merry Christmas and a Happy New Year!