Mandelbrot 1k

这是一篇旧文,其中的内容可能已经过时。

最近发现了一个有趣的代码竞赛JS1k,规则是用 1KB 以内的 JS 写出最炫酷的东西。第一届在 2010 举办,一年一度,至今仍在开展。

1KB 的代码能写出什么来?举个例子,今年的冠军是一颗炫酷的罗马花椰菜,只用了 1023 个字节,大家可以去随意感受一下。

比赛要求提交一个 JS 脚本,满足如下规则:

  • 至多 1024 字节;
  • 严禁引用外部资源;
  • 能在现代浏览器中运行;
  • 允许代码压缩或者 hack。
  • 另外,为了方便起见,内置全局变量:
    • a是一个占满屏幕的canvas元素;
    • b是document.body;
    • c是a的 2d 上下文;
    • d是document;
    • g是a的 WebGL 上下文。

主要规则就这么多。它让我想起了年轻时刚学会写代码,总是追求短小而 hacky 的代码,以让别人读不懂为荣;而现在处处追求工程美感。好比人吃惯了正常饭菜,却总想着再尝一下儿时路边酸酸的野草莓。

2016 年的比赛早已结束,2017 年的还未开始,所以我下面做的事情纯属自娱自乐。之前画过一个 Mandelbrot 集,你若在 CodePen 上搜索关键字“Mandelbrot”,它至今仍排在首位。它的最大特色在于,用户可以选中 Mandelbrot 集的一部分来查看它的细节,而这个过程可以无限重复下去。

See the Pen Mandelbrot by Zhou Qi ( @handsomeone) on CodePen.

看一下它的资源文件大小,JS 将近 6KB,选框的样式表约有 1KB。这些有可能被压缩到 1KB 以内吗?试试才知道。

首先删功能:规则不允许引用外部资源,那么先删掉 dat.GUI 控件相关代码。没有了控件,色彩配置也没意义了,删掉。其次,改写面向对象的代码为过程式的。再然后,取消所有变量声明关键字,全部使用全局变量,并且,每个变量只用一个字母。最后,能删的空白符删掉,for 循环里的语句能省则省,大括号能去就去。

让我们来看看结果……嗯不错,978 个字符。以前的我看到这种代码一定会高兴地去模仿吧。

C=d.createElement('i')
E=C.style
E.border='1px solid #39f'
E.background='#39f6'
E.position='absolute'
O='display'
b.appendChild(C)
M=Math
w=M.abs
r=M.sign
n=M.min
f=h=-2
o=4
S=512
U=()=>{c.fillStyle='#069'
c.fillRect(0,0,S,S)
p=c.getImageData(0,0,S,S)
E[O]='none'
z=s=B=i=k=0
u=[]
for(;i<S;i++){u[i]=[]
for(j=0;j<S;){G=i*o/S+f
g=j*o/S+h
u[i][j++]=[G,g,1,G,g]}}}
D=()=>{Z=z&&255*(1-Math.pow(.96,++s))
for(k=0;k<S;k++)for(m=0;m<S;m++){[K,R,I,P,Q]=u[k][m]
if(I>=s+z)if(K*K+R*R<4)u[k][m]=[K*K-R*R+P,2*K*R+Q,I+1,P,Q]
else{z=z||I
p.data[m*S*4+k*4+3]=Z}}c.putImageData(p,0,0)
requestAnimationFrame(D)}
H='clientX'
L='clientY'
onmousedown=(e)=>{B=e[H]
N=e[L]}
onmousemove=(e)=>{if(B){F=e[H]
J=e[L]
K=n(w(F-B),w(J-N))
A='px'
E.left=n(B,B+r(F-B)*K)+A
E.top=n(N,N+r(J-N)*K)+A
E.width=E.height=K+A
E[O]='block'}}
onmouseup=(e)=>{if(B){x=e[H]
y=e[L]
V=n(w(x-B),w(y-N))
if(V){
f+=n(B,B+r(x-B)*V)/S*o
h+=n(N,N+r(y-N)*V)/S*o
o*=V/S
U()}}}
onclick=()=>B=0
ondblclick=()=>{f=h=-2
o=4
U()}
U()
D()

压缩已经到极限了吗?还没有。在学习“罗马花椰菜”的源代码的时候,发现了一个专为此竞赛开发的压缩工具:RegPack。它能识别代码中的重复模式,接着进行我不懂但是很厉害的 Huffman 编码,就把 978 个字节的“源代码”压缩成了 845 个字节。

for(_="='PatO=e.clientNNX;Ll()K++)Jonfor(=[];+'px',Y,a]*o/S+=n(if(<S;=()=>click-B),w(c.fill=(e)=>{u[k][m]=0;0,0,S,S)C.style.)*q)/S*o;X,X+r(B,B+r( ;)*M)displayPMOh.tImageDOa(f=h=-2;o=4;B){};mouseC=d.creOeElement('i')borderP1px solid #39f'backgroundP#39f6'positiPabsolute';b.appendChild(C);w=abs;r=sign;n=min;S=512;l{StyleP#069';Rect(;p=c.gene';v=s=B=i=ku;iiJ{u[i]jj){A=if;g=jh;u[i][j++]=[A,g,1,A,g]}}};D{t=v&&255*(1-pow(.96,++s));kkkJmmmJ{[M,R,I=;I>=s+v)M*M+R*R<4)=[M*M-R*R+Y,2*M*R+a,I+1;else{v=v||I;p.dOa[m*S*4+k*4+3]=t}}c.pup,0,0);requestAnimOiFrame(D)downBLXNYmoveFLENY;Mw(FE-X))left F-BtopE-Xwidth=height=Mblock'}upxLyNY;qw(xy-X));q){;f+ x-Bh+y-Xo*=q/S;K}}};Bdbl{K};K;D()";G=/[^ -IMR-}]/.exec(_);)with(_.split(G))_=join(shift());eval(_)

关于结果,请移步JS Bin查看。它的操作跟原版稍有区别,复位坐标改成了双击,放大仍然是拖拽画框。