#Matrix67的LCD电子公告牌、GB2312-80编码、HZK16字模及其他
很久以前[matrix67的博客首页](http://www.matrix67.com/ "matrix67的博客首页")会随机显示一些很有意思的一句话,有小知识、小幽默、有意思的座右铭、对联等等。但是不知道什么时候,他把首页换掉了,换成了一个被我称为“LCD公告牌”的东西。
我一开始的时候对这个“LCD电子公告牌”并不感冒,反而觉得,远不如之前的“一句话逻辑”更生动……
[![Matrix67的博客首页](index.png "Matrix67的博客首页")](http://www.matrix67.com/ "Matrix67的博客首页")
而且我发现它有很多问题,首先,他上面说“`Clicks take effect immediately`”,但事实上,有时候鼠标点击速度快的时候,转换令人很抓狂……
其次,经过我的一番计算,整个“LCD公告牌”才只有`16*25`个“像素”,这对于一般人来说根本无法表达一个完整的意思……
最后,因为它很小,也必然引发了第三个问题,那就是如果很多人同时想在上面画的时候就会出现冲突,比如说,我终于遇到了这种情况……
因为之前很不看好这个东西,所以,并没有对它做过多的关注,但是前些天我彻底无聊了,开始在上面乱点……我要在上边写出“`matrix67`”几个字符……
可是,就在我快要成功的时候,不知道是谁,把它清空……这件事令我很生气……我觉得我得体现出作为极客的风格了,我要用更为迅速的绘制方式!!!
于是我打开了chrome的js控制台,开始研究这个公告牌的前台(js)代码……我很快的有所收获了……
与这个公告牌有关的是这么几个函数:
- `inverse(num)`,用来通知后台并转换当前格的颜色,参数是当前格的索引,由于表格是`16*25`的,那么第m行n列的索引就是`(m-1)*25+n`
- `allChange(num)`,参数为`-1`时,反转全屏颜色,参数为`-2`时,清空整个屏幕
但是继续研究会发现,这两个函数其实是可以混用的,因为它们访问的是同一个页面使用了相同的参数名进行传参……
后台根据参数的不同进行区分,而这两个函数的不同之处在于,`inverse`不进行全屏刷新,而`allchange`会……
又因为整个程序是每两秒钟检查一次的,也就是说,这两个函数达到的效果除了效率和前台某些效果之外基本类似……
但是需要注意的是,在js控制台这两个函数是不能直接运行的,因为这两个函数在一个name为“`main`”的frame页面里……
因为js控制台是整个面向整个页面的,所以要在js控制台调用需要这么写“`window.frames.main.inverse()`”,“`window.frames.main.allChange()`”……
当然,你可能觉得这么写,太长了,还可以简写做“`main.allChange()`”、“`main.inverse()`”……
好吧,既然分析的差不多了,那就……开始行动吧……先从最简单的开始,显示黑白相间的棋盘样式……好吧,一条语句搞定:
```JavaScript
for(var m=0;m<400;m++)if(m%2)main.inverse(m);
```
效果看上去很棒的样子……呃……下一步该干点什么呢?(下图对以上代码的结果进行了略微修改,虽说看上去很头晕~~(叫你撸太多)~~,但是个人认为还是挺有创意的……)
![棋盘格](chessboard.png "棋盘格")
说来说去还是要在上面写字呀……好吧,弄个`ASCII`字模先……这个东西貌似很容易弄到……
面对`16*25`的LCD,貌似要用最小的ASCII字模,`8*5`的貌似最合适了,下下来改一改,就变成符合js语法的字模了(`8*5`字模一般都是纵向优先排列的):
```JavaScript
var asc2=[0x00, 0x00, 0x00, 0x00, 0x00, // sp
0x00, 0x00, 0x2f, 0x00, 0x00, // !
0x00, 0x07, 0x00, 0x07, 0x00, // "
0x14, 0x7f, 0x14, 0x7f, 0x14, // #
0x24, 0x2a, 0x7f, 0x2a, 0x12, // $
0x62, 0x64, 0x08, 0x13, 0x23, // %
0x36, 0x49, 0x55, 0x22, 0x50, // &
0x00, 0x05, 0x03, 0x00, 0x00, // ’
0x00, 0x1c, 0x22, 0x41, 0x00, // (
0x00, 0x41, 0x22, 0x1c, 0x00, // )
0x14, 0x08, 0x3E, 0x08, 0x14, // *
0x08, 0x08, 0x3E, 0x08, 0x08, // +
0x00, 0x00, 0xA0, 0x60, 0x00, // ,
0x08, 0x08, 0x08, 0x08, 0x08, // -
0x00, 0x60, 0x60, 0x00, 0x00, // .
0x20, 0x10, 0x08, 0x04, 0x02, // /
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0
0x00, 0x42, 0x7F, 0x40, 0x00, // 1
0x42, 0x61, 0x51, 0x49, 0x46, // 2
0x21, 0x41, 0x45, 0x4B, 0x31, // 3
0x18, 0x14, 0x12, 0x7F, 0x10, // 4
0x27, 0x45, 0x45, 0x45, 0x39, // 5
0x3C, 0x4A, 0x49, 0x49, 0x30, // 6
0x01, 0x71, 0x09, 0x05, 0x03, // 7
0x36, 0x49, 0x49, 0x49, 0x36, // 8
0x06, 0x49, 0x49, 0x29, 0x1E, // 9
0x00, 0x36, 0x36, 0x00, 0x00, // :
0x00, 0x56, 0x36, 0x00, 0x00, // ;
0x08, 0x14, 0x22, 0x41, 0x00, // <
0x14, 0x14, 0x14, 0x14, 0x14, // =
0x00, 0x41, 0x22, 0x14, 0x08, // >
0x02, 0x01, 0x51, 0x09, 0x06, // ?
0x32, 0x49, 0x59, 0x51, 0x3E, // @
0x7C, 0x12, 0x11, 0x12, 0x7C, // A
0x7F, 0x49, 0x49, 0x49, 0x36, // B
0x3E, 0x41, 0x41, 0x41, 0x22, // C
0x7F, 0x41, 0x41, 0x22, 0x1C, // D
0x7F, 0x49, 0x49, 0x49, 0x41, // E
0x7F, 0x09, 0x09, 0x09, 0x01, // F
0x3E, 0x41, 0x49, 0x49, 0x7A, // G
0x7F, 0x08, 0x08, 0x08, 0x7F, // H
0x00, 0x41, 0x7F, 0x41, 0x00, // I
0x20, 0x40, 0x41, 0x3F, 0x01, // J
0x7F, 0x08, 0x14, 0x22, 0x41, // K
0x7F, 0x40, 0x40, 0x40, 0x40, // L
0x7F, 0x02, 0x0C, 0x02, 0x7F, // M
0x7F, 0x04, 0x08, 0x10, 0x7F, // N
0x3E, 0x41, 0x41, 0x41, 0x3E, // O
0x7F, 0x09, 0x09, 0x09, 0x06, // P
0x3E, 0x41, 0x51, 0x21, 0x5E, // Q
0x7F, 0x09, 0x19, 0x29, 0x46, // R
0x46, 0x49, 0x49, 0x49, 0x31, // S
0x01, 0x01, 0x7F, 0x01, 0x01, // T
0x3F, 0x40, 0x40, 0x40, 0x3F, // U
0x1F, 0x20, 0x40, 0x20, 0x1F, // V
0x3F, 0x40, 0x38, 0x40, 0x3F, // W
0x63, 0x14, 0x08, 0x14, 0x63, // X
0x07, 0x08, 0x70, 0x08, 0x07, // Y
0x61, 0x51, 0x49, 0x45, 0x43, // Z
0x00, 0x7F, 0x41, 0x41, 0x00, // [
0x55, 0x2A, 0x55, 0x2A, 0x55, // 55
0x00, 0x41, 0x41, 0x7F, 0x00, // ]
0x04, 0x02, 0x01, 0x02, 0x04, // ^
0x40, 0x40, 0x40, 0x40, 0x40, // _
0x00, 0x01, 0x02, 0x04, 0x00, // ’
0x20, 0x54, 0x54, 0x54, 0x78, // a
0x7F, 0x48, 0x44, 0x44, 0x38, // b
0x38, 0x44, 0x44, 0x44, 0x20, // c
0x38, 0x44, 0x44, 0x48, 0x7F, // d
0x38, 0x54, 0x54, 0x54, 0x18, // e
0x08, 0x7E, 0x09, 0x01, 0x02, // f
0x18, 0xA4, 0xA4, 0xA4, 0x7C, // g
0x7F, 0x08, 0x04, 0x04, 0x78, // h
0x00, 0x44, 0x7D, 0x40, 0x00, // i
0x40, 0x80, 0x84, 0x7D, 0x00, // j
0x7F, 0x10, 0x28, 0x44, 0x00, // k
0x00, 0x41, 0x7F, 0x40, 0x00, // l
0x7C, 0x04, 0x18, 0x04, 0x78, // m
0x7C, 0x08, 0x04, 0x04, 0x78, // n
0x38, 0x44, 0x44, 0x44, 0x38, // o
0xFC, 0x24, 0x24, 0x24, 0x18, // p
0x18, 0x24, 0x24, 0x18, 0xFC, // q
0x7C, 0x08, 0x04, 0x04, 0x08, // r
0x48, 0x54, 0x54, 0x54, 0x20, // s
0x04, 0x3F, 0x44, 0x40, 0x20, // t
0x3C, 0x40, 0x40, 0x20, 0x7C, // u
0x1C, 0x20, 0x40, 0x20, 0x1C, // v
0x3C, 0x40, 0x30, 0x40, 0x3C, // w
0x44, 0x28, 0x10, 0x28, 0x44, // x
0x1C, 0xA0, 0xA0, 0xA0, 0x7C, // y
0x44, 0x64, 0x54, 0x4C, 0x44, // z
0x00, 0x08, 0x36, 0x41, 0x00, // {
0x00, 0x00, 0x7F, 0x00, 0x00, // |
0x00, 0x41, 0x36, 0x08, 0x00, // }
0x08, 0x10, 0x08, 0x04, 0x08 // ~
];
```
按理说能显示`10`个字符,但是,这样的话,相邻两个字符就连在一起了……所以必须有一列的间隔,于是最后算了算,只能显示`4*2`个字符……
好了,万事俱备,就差写字了!由于js用的是`unicode`码,而`unicode`码是兼容`ASCII`的,所以我们只需要简单的换算就可以很容易的将字模显示出来了!
```JavaScript
function drawWord(str)
{
if(str.length>8)return;
var s,x,y;
main.allChange(-2);//清屏
for(var n=0;n<str.length;n++){
s=(str.charCodeAt(n)-32)*5;//n处字符的字模开始点
x=n%4*6;//字符左侧开始位置,n%4算出字符的位置,6为一个字符的宽度
y=parseInt(n/4)*8;//字符上侧开始位置,n/4算出字符的位置,8为一个字符的高度
for(var i=0;i<8;i++)
for(var j=0;j<5;j++)
if(asc2[s+j]&(1<<i))
main.inverse((y+i)*25+(j+x)+2);
}
};
```
恩,试了一下,比如说输出“`matrix67`”这几个字符……只需要调用
```JavaScript
drawWord("matrix67");
```
即可,恩,效果不错的样子……
![显示Matrix67](showMatrix67.png "显示Matrix67")
唉,只可惜这个“`LCD显示屏`”太小了,不具有太强的表现力,根本没办法满足我们的需求……
就算是画满了整个屏幕也表达不出一条像样的语句……在万般不过瘾之下,我决定在上面写汉字……
想了想,最小的汉字字模也是`16*16`的,屏幕上也就能显示一个汉字而已吧,不过这依然不能削减我们对此的热情!
有的同学依然能够下到`HZK16`的字模,但是大部分同学能下到的是`HZK16`的一个二进制文件……
不过无所谓,我们了解`HZK16`的文件结构,照样能把字模代码取出来……
`HZK16`文件中的二进制是按照区位码排列的,`GB2312编码`(`GBK`兼容`GB2312`)就是根据区位码设计的……
区位码把编码表分为94个区,每个区对应94个位,每个字符的区号和位号组合起来就是该汉字的区位码……
`GB2312`编码的一个字符由两个字节组成:高位字节称为汉字的区码,减`0xA0`就是这个汉字的区号;
同理低位字节为该汉字的位码,减`0xA0`得到的是该汉字的位号……
平常我们在汉化`windows`下存储的文本文件,汉字是都是默认以`GBK`编码存储的……
想当年不管是中考还是高考,要涂写信息录入卡,要查“区位码”想当年班里的查询表都不够用……
现在我教大家一个办法来生成区位码对照表,方法很简单……有了它以后不管是中考还是高考或者其他神马考试填报信息录入卡都不用担心了(坑爹呢……如果我有计算机能运行这个程序,我还用查表么……)……当然,这里要用C语言……
有同学问,你肿么不用java啊?java的`char`默认都是以`unicode`的方式存储的,在输入跟输出过程中,无端的进行了两次转码……
其实很不划算,反而使得事情变得更复杂……当然,java的目标是跨平台,与编码无关……当然要用平台无关的固定编码格式了……
昨天晚上随手一写,这个程序就这么写出来了,其实很简单,如下:
```C
#include <stdio.h>
int main(){
int i,j;
for(i=0xa1;i<0xaa;i++){
printf("%02d区\n",i-0xa0);
for(j=0xa1;j<0xff;j++)
printf("%02d%02d%c%c\t",i-0xa0,j-0xa0,i,j);
printf("\n———————————————————————————\n");
}
printf("\n\n\n\n");
for(i=0xb0;i<0xf8;i++){
printf("%02d区\n",i-0xa0);
for(j=0xa1;j<0xff;j++)
printf("%02d%02d%c%c\t",i-0xa0,j-0xa0,i,j);
printf("\n———————————————————————————\n");
}
return 0;
}
```
有同学问,为什么中间有一块你跳过去了啊?这与区位码的结构有关:
>区位码中01-09区是符号、数字区,
16-87区是汉字区(16-55区为一级汉字共3755个,按拼音索引;56-87区为二级汉字共3008个,按笔画顺序索引),
10-15和88-94是未定义的空白区
……中间没有输出的那一块,即便是输出了也是一片空白字符……所以干脆跳过了……
具体区位码是怎么分区的,运行一下上面的程序就自然知道了……当然,只有在中文windwos平台下才能运行……有的同学说,你这都屏幕输出了,我想保存到文件怎么办?那你就自己添加写文件语句吧……
呵呵,其实不用那么麻烦,用windows自带的重定向符吧,如果你编译后的文件为`printall.exe`,那么你可以这么写:
```Shell
printall.exe > GB2312.txt
```
那么所有的输出便都重定向到`GB2312.txt`里了……(好吧,这个大家其实都知道了……)
![GB2312](gb2312.png "GB2312")
(有了这个对照表,我就可以继续写我想当年没完成的那个`BrainF**k`在`非IE`下的解释器了……这个以后再聊……)
知道了这些我们再看`HZK16`的文件结构……其实`HZK16`就是按照区位码排列的二进制字模……
由于`HZK16`是`16*16`的字模,那么一个字模是`16*16/8=32`字节,而每个区有94位,那么每个字模的偏移便是:`(区号*94+位号)*32`……
那么它与`GB2312编码`的关系是:`每个字模的偏移=[(区码-0xa1)×94+ (位码-0xa1)]×32`
那么这个读`HZK16`二进制文件的程序应该这么写(这个程序虽说很简单,但是让我昨晚折腾到很晚):
```C
#include <stdio.h>
int main(){
FILE * f=fopen("hzk16","rb");
int i=0,j=-1;
//char zm[32];
unsigned char zm[32];
int q=0,w=0;
printf("var hzk=[\n");
while(!feof(f)){
++j;
fread(zm,32,sizeof(char),f);
q=j/94+1;w=j%94+1;
if((q>9&&q<16)||q>87)continue;
printf("[");
for(i=0;i<31;i++)
//printf("0x%02x,",zm[i]&0xff);
printf("0x%02X,",zm[i]);
//printf("0x%02x],/*%02d%02d %c%c*/\n",zm[31]&0xff,q,w,j/94+0xa1,j%94+0xa1);
printf("0x%02X],/*%02d%02d %c%c*/\n",zm[31],q,w,j/94+0xa1,j%94+0xa1);
}
printf("]");
fclose(f);
return 0;
}
```
当然,不用说`HZK16`文件要在当前目录,依然可以使用重定向符将其重定向到文件……下面为文件截图(由于每行太长,文件中间全是十六进制,就最后的字符有的看,所以就只能从行尾部分截图了,另外,`printf`的时候`用0x%02X`输出`A~F`是大写,`0x%02x`是小写,程序已被我改成大写,但下面截图是小写的情况):
![hzk16小写输出](hzk16.png "hzk16小写输出")
程序中如果不使用`unsigned char`而使用`char`,会得到类似`0xFFFFF**`的东西,原因是当高位是`1`时,被当做负数了,当然,你可以将其与`0xff`进行与运算将那些麻烦的前导`F`去掉,但是更方便的当然还是直接使用`unsigned char`类型……
在形成字模的时候,我们往往习惯低位在前高位在后……因为高位在前低位在后的后果是取的时候才用`0x80`右移,而高位在后低位在前就可以使用`1`左移得到了……这样取的时候貌似会更方便……这样我们就需要一个将`unsigned char`高低位偏转函数……当然你可以采用一个很传统的办法逐位交换,不过我知道一个被大家疯传牛叉算法……至于这个算法的详细解释在网上一查便知,此处不做解释,直接上代码:
```C
unsigned char reverse8( unsigned char c )
{
c = ( c & 0x55 ) << 1 | ( c & 0xAA ) >> 1;
c = ( c & 0x33 ) << 2 | ( c & 0xCC ) >> 2;
c = ( c & 0x0F ) << 4 | ( c & 0xF0 ) >> 4;
return c;
}
```
当然,很多人相知道导出的这个字模对不对……这个……可以随便找个字人肉呈现一下……恩,就找“爱”这个字吧……因为这个字比较有爱……下面是我人肉呈现的……呃,好像有几个点不太对劲,但是大体证明了字模木有错误……
![爱](ai.png "爱")
好了……字模有了……下一步要在上边显示了……这个程序更简单:
```JavaScript
function drawhz(zm){
main.allChange(-2);//清屏
for(var i=0;i<16;i++)for(var j=0;j<16;j++)if(zm[i*2+parseInt(j/8)]&(0x80>>(j%8)))main.inverse(i*25+j+4);
}
```
比如说,我们要显示“楹”,首先在上面的文件中查到“楹”的字模为
```JavaScript
[0x10,0x00,0x17,0xf8,0x11,0x10,0x11,0x1c,0xfd,0xe4,0x12,0xa4,0x3a,0x44,0x35,0xb4,0x54,0x08,0x5b,0xfc,0x92,0xa8,0x12,0xa8,0x12,0xa8,0x12,0xa8,0x1f,0xfe,0x10,0x00]/*7326 楹*/
```
那么显示语句可以这么写:
```JavaScript
drawhz([0x10,0x00,0x17,0xf8,0x11,0x10,0x11,0x1c,0xfd,0xe4,0x12,0xa4,0x3a,0x44,0x35,0xb4,0x54,0x08,0x5b,0xfc,0x92,0xa8,0x12,0xa8,0x12,0xa8,0x12,0xa8,0x1f,0xfe,0x10,0x00]/*7326 楹*/);
```
显示效果如下:
![楹](ying.png "楹")
唉,只能显示一个字,太约束了……不如我们让一句话循环显示吧……比如说,我想让“我爱你”这三个字循环输出……
需要现在刚才输出的字模文件里找这三个字,并连成数组赋给一个变量,然后定时输出,整个代码可能是这样:
```JavaScript
//循环输出“我爱你”
var zms=[
[0x04,0x80,0x0e,0xa0,0x78,0x90,0x08,0x90,0x08,0x84,0xff,0xfe,0x08,0x80,0x08,0x90,0x0a,0x90,0x0c,0x60,0x18,0x40,0x68,0xa0,0x09,0x20,0x0a,0x14,0x28,0x14,0x10,0x0c],/*4650 我*/
[0x00,0x78,0x3f,0x80,0x11,0x10,0x09,0x20,0x7f,0xfe,0x42,0x02,0x82,0x04,0x7f,0xf8,0x04,0x00,0x07,0xf0,0x0a,0x20,0x09,0x40,0x10,0x80,0x11,0x60,0x22,0x1c,0x0c,0x08],/*1614 爱*/
[0x11,0x00,0x11,0x00,0x11,0x00,0x23,0xfc,0x22,0x04,0x64,0x08,0xa8,0x40,0x20,0x40,0x21,0x50,0x21,0x48,0x22,0x4c,0x24,0x44,0x20,0x40,0x20,0x40,0x21,0x40,0x20,0x80]/*3667 你*/
];
var num=0;
function drawhz(zm){
main.allChange(-2);
for(var i=0;i<16;i++)for(var j=0;j<16;j++)if(zm[i*2+parseInt(j/8)]&(0x80>>(j%8)))main.inverse(i*25+j+4);
}
var timer=setInterval(function(){drawhz(zms[(num++)%3]);},15000);
```
这样,每`15`秒文字切换一次……选择的间隔时间这么大的原因是考虑到`matrix67`的服务器的承受能力……
这种游戏试一下就行了,最好还是别老玩……把人家服务器弄崩了就不好了……所以,看到结果之后,我们还是把程序停止吧……
停止的办法可以很简单粗暴,比如说关闭网页,另一种比较体现水平的办法是在js控制台运行:
```JavaScript
clearInterval(timer);
```
好吧,这就是刚才的语句里定义`timer`变量的原因……
可能还有些Geek觉得总是不过瘾,非要在上面公开玩俄罗斯方块或者是贪吃蛇游戏……这个也不是不可能……这些我也试过,但是我觉得有些事就适可而止吧……要想玩还是自己画个`table`要么自己弄个LCD玩吧……或许会更有成就感呢……
最后,祝大家玩的愉快……