ates.dev

Dweeting Outside the Box

Posted on Dec 25, 2024

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)

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.

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:

  1. If there are multiple primitives in the scene, we don't have to repeat adding offsets to them and therefore save on characters.
  2. 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.

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.

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)

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!

Discourse

Comments, questions, corrections? Please drop them on Bluesky!