aardio 文档

aardio 范例: 自绘小游戏 - 彩色玻璃出逃(Color Glass Escape)

import win.ui;
/*DSG{{*/
var winform = win.form(text="自绘小游戏 - 彩色玻璃出逃(Color Glass Escape)";right=980;bottom=720;bgcolor=0x171126;border="dialog frame";max=false)
winform.add(
glassCanvas={cls="plus";left=0;top=0;right=980;bottom=720;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
)
/*}}*/

import gdip;

/*
彩色玻璃出逃:
- 点击带箭头的玻璃块。前方路线清空时,玻璃块会从同色光门飞出;否则会撞上挡路的玻璃块。
- 每块成功出逃的玻璃都会点亮右侧花窗的一块拼片。全部点亮后进入下一关。
- H 显示提示,R 或右键重置;通关后单击、空格或回车进入下一关。
- 5 关分别有 6 / 8 / 10 / 12 / 14 块玻璃。关卡依赖图均为 DAG,任意选择当前可出逃块都不会形成死局。
- 使用 DPI 原尺寸场景缓存:棋盘、花窗、静止方块和界面仅在状态改变时重建;逐帧只绘制路线预览、飞行方块和短寿命碎光。
*/
var CANVAS_W = 980;
var CANVAS_H = 720;
var GRID_SIZE = 8;
var CELL_SIZE = 63;
var BOARD_SIZE = GRID_SIZE*CELL_SIZE;
var BOARD_X = 48;
var BOARD_Y = 132;
var ART_CX = 782;
var ART_CY = 310;
var ART_RADIUS = 100;
var ESCAPE_DURATION = 0.66;
var STATE_PLAYING = 1;
var STATE_SOLVED = 2;

var palette = [
    0xFF52E8FF,0xFFFF6FAE,0xFFFFCF59,
    0xFF8C7CFF,0xFF55E2A4,0xFFFF8A5B
];

// 坐标为 0 起始网格坐标;w / h 为占用格数。
var levels = [
    {
        name="曙光花窗";subtitle="让第一束彩光穿过清晨";accent=0xFFFFD466;rotation=0.08;
        blocks=[
            {dir="D";h=1;w=1;x=6;y=3},{dir="U";h=2;w=1;x=1;y=2},
            {dir="R";h=1;w=2;x=0;y=1},{dir="L";h=1;w=2;x=5;y=5},
            {dir="D";h=2;w=1;x=6;y=0},{dir="L";h=1;w=2;x=6;y=7}
        ]
    },
    {
        name="海潮罗盘";subtitle="沿潮汐的方向依次解开航路";accent=0xFF55E8E0;rotation=0.28;
        blocks=[
            {dir="R";h=1;w=1;x=5;y=7},{dir="R";h=1;w=3;x=3;y=6},
            {dir="D";h=2;w=1;x=0;y=6},{dir="U";h=2;w=1;x=6;y=6},
            {dir="L";h=1;w=1;x=3;y=4},{dir="L";h=1;w=2;x=6;y=4},
            {dir="U";h=2;w=1;x=6;y=0},{dir="D";h=2;w=1;x=0;y=3}
        ]
    },
    {
        name="星羽棱镜";subtitle="星光会在每一次出逃后折射";accent=0xFF8C82FF;rotation=-0.12;
        blocks=[
            {dir="R";h=1;w=1;x=4;y=0},{dir="U";h=2;w=1;x=4;y=5},
            {dir="D";h=1;w=1;x=5;y=0},{dir="L";h=1;w=2;x=1;y=4},
            {dir="U";h=2;w=1;x=1;y=6},{dir="L";h=1;w=2;x=4;y=2},
            {dir="L";h=1;w=2;x=0;y=2},{dir="R";h=1;w=3;x=0;y=5},
            {dir="U";h=2;w=1;x=0;y=3},{dir="D";h=2;w=1;x=7;y=3}
        ]
    },
    {
        name="森灵万花筒";subtitle="清理枝叶交叠的彩色脉络";accent=0xFF66E89B;rotation=0.18;
        blocks=[
            {dir="L";h=1;w=2;x=3;y=3},{dir="R";h=1;w=2;x=6;y=7},
            {dir="U";h=2;w=1;x=4;y=6},{dir="L";h=1;w=1;x=0;y=7},
            {dir="D";h=2;w=1;x=6;y=3},{dir="D";h=3;w=1;x=7;y=0},
            {dir="R";h=1;w=3;x=3;y=0},{dir="D";h=2;w=1;x=0;y=4},
            {dir="U";h=2;w=1;x=4;y=4},{dir="R";h=1;w=3;x=1;y=4},
            {dir="R";h=1;w=2;x=0;y=1},{dir="U";h=1;w=1;x=1;y=3}
        ]
    },
    {
        name="极光圣轮";subtitle="在最后的花窗里汇聚全部颜色";accent=0xFFFF76CF;rotation=-0.24;
        blocks=[
            {dir="U";h=2;w=1;x=2;y=2},{dir="R";h=1;w=2;x=3;y=7},
            {dir="R";h=1;w=2;x=2;y=1},{dir="L";h=1;w=2;x=2;y=6},
            {dir="L";h=1;w=1;x=7;y=0},{dir="U";h=2;w=1;x=7;y=6},
            {dir="D";h=1;w=1;x=4;y=2},{dir="U";h=3;w=1;x=6;y=3},
            {dir="R";h=1;w=1;x=1;y=2},{dir="D";h=1;w=1;x=3;y=2},
            {dir="R";h=1;w=2;x=0;y=5},{dir="U";h=3;w=1;x=6;y=0},
            {dir="D";h=3;w=1;x=0;y=2},{dir="L";h=1;w=2;x=4;y=4}
        ]
    }
];

var game = {
    state=STATE_PLAYING;
    level=1;
    cycle=1;
    blocks=[];
    moving=null;
    bumping=null;
    removedCount=0;
    moves=0;
    bumps=0;
    hoverId=null;
    hintId=null;
    hintCursor=0;
    hintTimer=0;
    statusText="点击玻璃块,让它沿箭头方向飞向同色光门";
    statusTime=2600;
    pulse=0;
    trailClock=0;
    solveGlow=0;
    particles=[];
};

var CACHE_DPI_X = winform.dpiScale(1);
var CACHE_DPI_Y = CACHE_DPI_X;
var sceneCache;
var sceneDirty = true;

var stars=[];
for(i=1;64){
    table.push(stars,{
        x=math.random()*CANVAS_W;
        y=math.random()*CANVAS_H;
        size=0.5+math.random()*1.6;
        alpha=35+math.random(0,75);
    });
}

import sys.midiOut;
var midiOut = sys.midiOut();

// -------------------- 颜色、几何与关卡逻辑 --------------------
var alphaColor = function(color,alpha){
    return ((math.clamp(math.floor(alpha),0,255)<<24) | (color&0xFFFFFF));
}

var shadeColor = function(color,amount){
    var r=math.clamp(((color>>16)&0xFF)+amount,0,255);
    var g=math.clamp(((color>>8)&0xFF)+amount,0,255);
    var b=math.clamp((color&0xFF)+amount,0,255);
    return 0xFF000000 | (math.floor(r)<<16) | (math.floor(g)<<8) | math.floor(b);
}

var intersects = function(a,b){
    return a.x<b.x+b.w && a.x+a.w>b.x && a.y<b.y+b.h && a.y+a.h>b.y;
}

var pathRect = function(block){
    select(block.dir){
        case "R" { return {x=block.x+block.w;y=block.y;w=GRID_SIZE-block.x-block.w;h=block.h}; }
        case "L" { return {x=0;y=block.y;w=block.x;h=block.h}; }
        case "D" { return {x=block.x;y=block.y+block.h;w=block.w;h=GRID_SIZE-block.y-block.h}; }
        case "U" { return {x=block.x;y=0;w=block.w;h=block.y}; }
    }
}

var blockerDistance = function(block,other){
    select(block.dir){
        case "R" { return other.x-(block.x+block.w); }
        case "L" { return block.x-(other.x+other.w); }
        case "D" { return other.y-(block.y+block.h); }
        case "U" { return block.y-(other.y+other.h); }
    }
}

var findBlocker = function(block){
    var path=pathRect(block);
    var nearest;
    var nearestDistance=1000;
    if(!path || path.w<=0 || path.h<=0) return null;
    for(i,other in game.blocks){
        if(other.id!=block.id && !other.removed && !other.escaping){
            if(intersects(path,other)){
                var distance=blockerDistance(block,other);
                if(distance<nearestDistance){
                    nearest=other;
                    nearestDistance=distance;
                }
            }
        }
    }
    return nearest;
}

var isAvailable = function(block){
    return !block.removed && !block.escaping && !findBlocker(block);
}

var blockRect = function(block){
    return {
        x=BOARD_X+block.x*CELL_SIZE+5;
        y=BOARD_Y+block.y*CELL_SIZE+5;
        w=block.w*CELL_SIZE-10;
        h=block.h*CELL_SIZE-10;
    };
}

var gateLane = function(block){
    return (block.dir=="R" || block.dir=="L") ? block.y : block.x;
}

var gateColor = function(block,levelIndex){
    var offset=0;
    select(block.dir){
        case "R" { offset=0; }
        case "L" { offset=2; }
        case "U" { offset=4; }
        case "D" { offset=1; }
    }
    return palette[(gateLane(block)+offset+levelIndex*2)%#palette+1];
}

var gateKey = function(block){
    return block.dir++":"++gateLane(block);
}

var gatePoint = function(block,outside){
    var lane=gateLane(block);
    select(block.dir){
        case "R" { return BOARD_X+BOARD_SIZE+(outside ? 29 : 16),BOARD_Y+(lane+0.5)*CELL_SIZE; }
        case "L" { return BOARD_X-(outside ? 29 : 16),BOARD_Y+(lane+0.5)*CELL_SIZE; }
        case "D" { return BOARD_X+(lane+0.5)*CELL_SIZE,BOARD_Y+BOARD_SIZE+(outside ? 29 : 16); }
        case "U" { return BOARD_X+(lane+0.5)*CELL_SIZE,BOARD_Y-(outside ? 29 : 16); }
    }
}

var defaultStatus = function(){
    if(game.state==STATE_SOLVED) return "花窗已经点亮,单击或按空格进入下一关";
    return "观察箭头路线:前方没有玻璃阻挡时即可出逃";
}

var setStatus = function(text,duration){
    game.statusText=text;
    game.statusTime=duration;
    sceneDirty=true;
}

var spawnParticles = function(x,y,count,color,burst){
    for(i=1;count){
        var angle=math.random()*math.pi*2;
        var speed=(burst ? 70 : 28)+math.random()*(burst ? 145 : 70);
        var life=0.35+math.random()*(burst ? 0.9 : 0.55);
        table.push(game.particles,{
            x=x;y=y;
            vx=math.cos(angle)*speed;
            vy=math.sin(angle)*speed;
            life=life;maxLife=life;
            size=1.4+math.random()*3.8;
            spin=math.random()*math.pi;
            color=color;
        });
    }
}

var artPieceCenter = function(pieceIndex,count){
    var levelData=levels[game.level];
    var angle=-math.pi/2+levelData.rotation+(pieceIndex-0.5)*math.pi*2/count;
    var radius=68;
    return ART_CX+math.cos(angle)*radius,ART_CY+math.sin(angle)*radius;
}

var initLevel = function(levelIndex){
    // 动态玻璃精灵按关卡与 DPI 缓存,切关时主动释放旧位图。
    for(i,oldBlock in game.blocks){
        if(oldBlock.sprite && oldBlock.sprite.bitmap) oldBlock.sprite.bitmap.delete();
    }

    game.level=levelIndex;
    game.state=STATE_PLAYING;
    game.blocks=[];
    game.moving=null;
    game.bumping=null;
    game.removedCount=0;
    game.moves=0;
    game.bumps=0;
    game.hoverId=null;
    game.hintId=null;
    game.hintCursor=0;
    game.hintTimer=0;
    game.statusText="点击玻璃块,让它沿箭头方向飞向同色光门";
    game.statusTime=2800;
    game.particles=[];
    game.trailClock=0;
    game.solveGlow=0;

    var source=levels[levelIndex].blocks;
    for(i,data in source){
        var block=table.clone(data);
        block.id=i;
        block.color=gateColor(block,levelIndex);
        block.removed=false;
        block.escaping=false;
        block.travel=0;
        block.bump=0;
        table.push(game.blocks,block);
    }
    sceneDirty=true;
}

var nextLevel = function(){
    var next=game.level+1;
    if(next>#levels){
        next=1;
        game.cycle++;
    }
    initLevel(next);
}

var escapeDistance = function(block){
    var rc=blockRect(block);
    select(block.dir){
        case "R" { return BOARD_X+BOARD_SIZE+85-rc.x; }
        case "L" { return -(rc.x+rc.w-(BOARD_X-85)); }
        case "D" { return BOARD_Y+BOARD_SIZE+85-rc.y; }
        case "U" { return -(rc.y+rc.h-(BOARD_Y-85)); }
    }
}

var escapeOffset = function(block,progress){
    var eased=progress*progress*(2-progress);
    var distance=escapeDistance(block)*eased;
    if(block.dir=="R" || block.dir=="L") return distance,0;
    return 0,distance;
}

var startEscape = function(block){
    block.escaping=true;
    block.travel=0;
    game.moving=block.id;
    game.hoverId=null;
    game.hintId=null;
    game.hintTimer=0;
    game.trailClock=0;
    setStatus("光路畅通:玻璃正飞向同色光门",900);
    sceneDirty=true;
    if(midiOut) midiOut.play("changeInstrument(98), 1_,5_", "C6", 36);
}

var finishLevel = function(){
    game.state=STATE_SOLVED;
    game.solveGlow=0;
    setStatus("花窗点亮!单击或按空格进入下一关",100000);
    spawnParticles(ART_CX,ART_CY,54,levels[game.level].accent,true);
    if(midiOut) midiOut.play("changeInstrument(10), 1_,3_,5_,1'__,3'__", "C4", 95, 70);
}

var finishEscape = function(block){
    block.removed=true;
    block.escaping=false;
    block.travel=1;
    game.moving=null;
    game.removedCount++;

    var gx,gy=gatePoint(block,true);
    spawnParticles(gx,gy,13,block.color,true);
    var ax,ay=artPieceCenter(block.id,#game.blocks);
    spawnParticles(ax,ay,9,block.color,false);
    sceneDirty=true;
    if(midiOut) midiOut.play("changeInstrument(11), 1_,5_,1'__", "C5", 55);

    if(game.removedCount==#game.blocks){
        finishLevel();
    }
    else{
        setStatus("一块彩玻已嵌入花窗,继续清理光路",1300);
    }
}

var startBump = function(block,blocker){
    block.bump=1;
    game.bumping=block.id;
    game.bumps++;
    game.hoverId=block.id;
    setStatus("路线被挡住了:先移走前方发红光的玻璃",1500);

    var rc=blockRect(blocker);
    spawnParticles(rc.x+rc.w/2,rc.y+rc.h/2,8,0xFFFF5F78,false);
    sceneDirty=true;
    if(midiOut) midiOut.play("changeInstrument(116), 1_,7_", "C3", 45);
}

var tryEscape = function(block){
    if(game.state!=STATE_PLAYING || game.moving || game.bumping) return;
    game.moves++;
    var blocker=findBlocker(block);
    if(blocker) startBump(block,blocker);
    else startEscape(block);
}

initLevel(1);

// -------------------- 输入 --------------------
var logicalMousePos = function(lParam){
    var x,y=win.getMessagePos(lParam);
    var dpiX,dpiY=winform.dpiScale(1,1);
    return x/dpiX,y/dpiY;
}

var hitBlock = function(x,y){
    for(i=#game.blocks;1;-1){
        var block=game.blocks[i];
        if(!block.removed && !block.escaping){
            var rc=blockRect(block);
            if(x>=rc.x && x<=rc.x+rc.w && y>=rc.y && y<=rc.y+rc.h) return block;
        }
    }
}

winform.glassCanvas.onMouseMove = function(wParam,lParam){
    if(game.moving || game.bumping || game.state!=STATE_PLAYING){
        game.hoverId=null;
        return;
    }
    var x,y=logicalMousePos(lParam);
    var block=hitBlock(x,y);
    game.hoverId=block ? block.id;
}

winform.glassCanvas.onMouseLeave = function(){
    game.hoverId=null;
}

winform.glassCanvas.onMouseDown = function(wParam,lParam){
    if(game.state==STATE_SOLVED){
        nextLevel();
        return;
    }
    if(game.moving || game.bumping) return;
    var x,y=logicalMousePos(lParam);
    var block=hitBlock(x,y);
    if(block) tryEscape(block);
    else setStatus("请选择棋盘中带箭头的彩色玻璃",1100);
}

winform.glassCanvas.onRightMouseUp = function(){
    initLevel(game.level);
}

winform.glassCanvas.dlgCode=4/*_DLGC_WANTALLKEYS*/;
winform.glassCanvas.onKeyDown = function(keyCode,lParam,repeat){
    if(repeat) return;
    if(keyCode==0x52/*_VK_R*/){
        initLevel(game.level);
    }
    elseif(keyCode==0x48/*_VK_H*/ && game.state==STATE_PLAYING && !game.moving){
        var count=#game.blocks;
        for(step=1;count){
            var index=(game.hintCursor+step-1)%count+1;
            var block=game.blocks[index];
            if(isAvailable(block)){
                game.hintId=index;
                game.hintCursor=index;
                game.hintTimer=2200;
                setStatus("提示:正在发出明亮光环的玻璃可以安全出逃",2200);
                if(midiOut) midiOut.play("changeInstrument(10), 1_,3_", "C6", 42);
                break;
            }
        }
    }
    elseif((keyCode==0x20/*_VK_SPACE*/ || keyCode==0x0D/*_VK_RETURN*/) && game.state==STATE_SOLVED){
        nextLevel();
    }
}

// -------------------- 绘图辅助 --------------------
var drawText = function(graphics,text,size,x,y,width,height,color,bold,align){
    var style=bold ? 1/*_FontStyleBold*/ : 0;
    var textFont=gdip.font("Microsoft YaHei UI",size,style,2/*_UnitPixel*/);
    var format=gdip.stringFormat();
    format.align=align;
    format.lineAlign=1/*_StringAlignmentCenter*/;
    var brush=gdip.solidBrush(color);
    graphics.drawString(text,textFont,::RECTF(x,y,width,height),format,brush);
    brush.delete();
    format.delete();
    textFont.delete();
}

var arrowPoints = function(dir,cx,cy,size,offsetX,offsetY){
    var base=[
        {x=-0.66*size;y=-0.27*size},{x=-0.03*size;y=-0.27*size},
        {x=-0.03*size;y=-0.53*size},{x=0.72*size;y=0},
        {x=-0.03*size;y=0.53*size},{x=-0.03*size;y=0.27*size},
        {x=-0.66*size;y=0.27*size}
    ];
    var points=[];
    for(i,p in base){
        var px=p.x;
        var py=p.y;
        var x,y;
        select(dir){
            case "R" { x=cx+px;y=cy+py; }
            case "L" { x=cx-px;y=cy+py; }
            case "D" { x=cx-py;y=cy+px; }
            case "U" { x=cx+py;y=cy-px; }
        }
        table.push(points,{x=x+offsetX;y=y+offsetY});
    }
    return points;
}

var drawArrow = function(graphics,dir,cx,cy,size,color,alpha){
    var shadow=gdip.solidBrush(alphaColor(0xFF07101E,alpha*0.55));
    graphics.fillPolygon(shadow,arrowPoints(dir,cx,cy,size,2,3));
    shadow.delete();
    var brush=gdip.solidBrush(alphaColor(color,alpha));
    graphics.fillPolygon(brush,arrowPoints(dir,cx,cy,size,0,0));
    brush.delete();
}

var drawGlassBlock = function(graphics,block,offsetX,offsetY,alpha,emphasis){
    var rc=blockRect(block);
    rc.x+=offsetX;
    rc.y+=offsetY;

    if(emphasis>0){
        var glow=gdip.solidBrush(alphaColor(block.color,28+emphasis*36));
        graphics.fillRoundRect(glow,rc.x-5,rc.y-5,rc.w+10,rc.h+10,13);
        glow.delete();
    }

    var shadow=gdip.solidBrush(alphaColor(0xFF000000,alpha*0.34));
    graphics.fillRoundRect(shadow,rc.x+4,rc.y+6,rc.w,rc.h,11);
    shadow.delete();

    var state=graphics.save();
    var clip=gdip.path();
    clip.addRoundRect(rc.x,rc.y,rc.w,rc.h,11);
    graphics.setClipPath(clip);

    var glass=gdip.lineBrush(
        ::POINTF(rc.x,rc.y),::POINTF(rc.x+rc.w,rc.y+rc.h),
        alphaColor(shadeColor(block.color,48),alpha),
        alphaColor(shadeColor(block.color,-62),alpha)
    );
    graphics.fillRect(glass,rc.x,rc.y,rc.w,rc.h);
    glass.delete();

    var facet1=gdip.solidBrush(alphaColor(0xFFFFFFFF,alpha*0.16));
    graphics.fillPolygon(facet1,[
        {x=rc.x;y=rc.y},{x=rc.x+rc.w*0.72;y=rc.y},
        {x=rc.x+rc.w*0.36;y=rc.y+rc.h},{x=rc.x;y=rc.y+rc.h}
    ]);
    facet1.delete();

    var facet2=gdip.solidBrush(alphaColor(shadeColor(block.color,-90),alpha*0.18));
    graphics.fillPolygon(facet2,[
        {x=rc.x+rc.w*0.72;y=rc.y},{x=rc.x+rc.w;y=rc.y},
        {x=rc.x+rc.w;y=rc.y+rc.h},{x=rc.x+rc.w*0.36;y=rc.y+rc.h}
    ]);
    facet2.delete();

    var shine=gdip.pen(alphaColor(0xFFFFFFFF,alpha*0.5),1.5);
    graphics.drawLine(shine,rc.x+10,rc.y+8,rc.x+rc.w-11,rc.y+8);
    shine.delete();
    clip.delete();
    graphics.restore(state);

    var border=gdip.pen(alphaColor(shadeColor(block.color,70),alpha*0.9),1.7+emphasis*0.7);
    graphics.drawRoundRect(border,rc.x,rc.y,rc.w,rc.h,11);
    border.delete();
    var darkBorder=gdip.pen(alphaColor(0xFF081321,alpha*0.8),1);
    graphics.drawRoundRect(darkBorder,rc.x+2,rc.y+2,rc.w-4,rc.h-4,9);
    darkBorder.delete();

    var arrowSize=math.min(19,math.min(rc.w,rc.h)*0.32);
    drawArrow(graphics,block.dir,rc.x+rc.w/2,rc.y+rc.h/2,arrowSize,0xFFFFFFFF,alpha*0.92);
}

// 飞行与碰撞动画只贴一次预绘玻璃精灵,避免在 200% DPI 下逐帧重建渐变、路径与剪切区。
var getBlockSprite = function(block){
    var sprite=block.sprite;
    if(sprite && (math.abs(sprite.dpiX-CACHE_DPI_X)>0.001 || math.abs(sprite.dpiY-CACHE_DPI_Y)>0.001)){
        sprite.bitmap.delete();
        block.sprite=null;
        sprite=null;
    }
    if(sprite) return sprite;

    var rc=blockRect(block);
    var margin=11;
    var width=rc.w+margin*2;
    var height=rc.h+margin*2;
    var bitmap=gdip.bitmap(math.ceil(width*CACHE_DPI_X),math.ceil(height*CACHE_DPI_Y));
    var spriteGraphics=gdip.graphics(bitmap);
    spriteGraphics.smoothingMode=4/*_SmoothingModeAntiAlias*/;
    spriteGraphics.scale(CACHE_DPI_X,CACHE_DPI_Y);
    drawGlassBlock(spriteGraphics,block,margin-rc.x,margin-rc.y,255,0.68);
    spriteGraphics.delete();

    sprite={bitmap=bitmap;margin=margin;width=width;height=height;dpiX=CACHE_DPI_X;dpiY=CACHE_DPI_Y};
    block.sprite=sprite;
    return sprite;
}

var drawBlockSprite = function(graphics,block,offsetX,offsetY){
    var sprite=getBlockSprite(block);
    var rc=blockRect(block);
    graphics.drawImageRect(sprite.bitmap,
        rc.x+offsetX-sprite.margin,rc.y+offsetY-sprite.margin,
        sprite.width,sprite.height
    );
}

var drawGate = function(graphics,block){
    var lane=gateLane(block);
    var x,y,w,h;
    if(block.dir=="R" || block.dir=="L"){
        x=block.dir=="R" ? BOARD_X+BOARD_SIZE+2 : BOARD_X-16;
        y=BOARD_Y+lane*CELL_SIZE+9;
        w=14;h=CELL_SIZE-18;
    }
    else{
        x=BOARD_X+lane*CELL_SIZE+9;
        y=block.dir=="D" ? BOARD_Y+BOARD_SIZE+2 : BOARD_Y-16;
        w=CELL_SIZE-18;h=14;
    }

    var halo=gdip.solidBrush(alphaColor(block.color,42));
    graphics.fillRoundRect(halo,x-5,y-5,w+10,h+10,8);
    halo.delete();
    var gateBrush=gdip.lineBrush(::POINTF(x,y),::POINTF(x+w,y+h),
        shadeColor(block.color,65),shadeColor(block.color,-45));
    graphics.fillRoundRect(gateBrush,x,y,w,h,5);
    gateBrush.delete();
    var edge=gdip.pen(alphaColor(0xFFFFFFFF,155),1.2);
    graphics.drawRoundRect(edge,x,y,w,h,5);
    edge.delete();

    var gx,gy=gatePoint(block,false);
    drawArrow(graphics,block.dir,gx,gy,7,0xFFFFFFFF,190);
}

var drawBoard = function(graphics){
    var shadow=gdip.solidBrush(0x66000000);
    graphics.fillRoundRect(shadow,BOARD_X-15,BOARD_Y-13,BOARD_SIZE+34,BOARD_SIZE+38,22);
    shadow.delete();

    var frame=gdip.lineBrush(
        ::POINTF(BOARD_X,BOARD_Y),::POINTF(BOARD_X+BOARD_SIZE,BOARD_Y+BOARD_SIZE),
        0xFF2C4660,0xFF0A1423
    );
    graphics.fillRoundRect(frame,BOARD_X-10,BOARD_Y-10,BOARD_SIZE+20,BOARD_SIZE+20,18);
    frame.delete();

    var board=gdip.lineBrush(
        ::POINTF(BOARD_X,BOARD_Y),::POINTF(BOARD_X,BOARD_Y+BOARD_SIZE),
        0xFF13273A,0xFF091421
    );
    graphics.fillRoundRect(board,BOARD_X,BOARD_Y,BOARD_SIZE,BOARD_SIZE,11);
    board.delete();

    var cellBrush1=gdip.solidBrush(0x0816D9E8);
    var cellBrush2=gdip.solidBrush(0x0615A0D0);
    for(row=0;GRID_SIZE-1){
        for(col=0;GRID_SIZE-1){
            var brush=(row+col)%2 ? cellBrush1 : cellBrush2;
            graphics.fillRoundRect(brush,BOARD_X+col*CELL_SIZE+4,BOARD_Y+row*CELL_SIZE+4,CELL_SIZE-8,CELL_SIZE-8,7);
        }
    }
    cellBrush2.delete();
    cellBrush1.delete();

    var gridPen=gdip.pen(0x183EB7D1,1);
    for(i=1;GRID_SIZE-1){
        graphics.drawLine(gridPen,BOARD_X+i*CELL_SIZE,BOARD_Y+5,BOARD_X+i*CELL_SIZE,BOARD_Y+BOARD_SIZE-5);
        graphics.drawLine(gridPen,BOARD_X+5,BOARD_Y+i*CELL_SIZE,BOARD_X+BOARD_SIZE-5,BOARD_Y+i*CELL_SIZE);
    }
    gridPen.delete();

    var seen={};
    for(i,block in game.blocks){
        var key=gateKey(block);
        if(!seen[key]){
            seen[key]=true;
            drawGate(graphics,block);
        }
    }
}

var artPiecePoints = function(index,count){
    var levelData=levels[game.level];
    var gap=0.025;
    var a1=-math.pi/2+levelData.rotation+(index-1)*math.pi*2/count+gap;
    var a2=-math.pi/2+levelData.rotation+index*math.pi*2/count-gap;
    var inner=31+(index%2)*4;
    var outer=ART_RADIUS-10-(index%3==0 ? 9 : 0);
    return [
        {x=ART_CX+math.cos(a1)*inner;y=ART_CY+math.sin(a1)*inner},
        {x=ART_CX+math.cos(a1)*outer;y=ART_CY+math.sin(a1)*outer},
        {x=ART_CX+math.cos(a2)*outer;y=ART_CY+math.sin(a2)*outer},
        {x=ART_CX+math.cos(a2)*inner;y=ART_CY+math.sin(a2)*inner}
    ];
}

var drawStainedWindow = function(graphics){
    var levelData=levels[game.level];
    var halo=gdip.solidBrush(alphaColor(levelData.accent,24+game.removedCount*2));
    graphics.fillCircle(halo,ART_CX,ART_CY,ART_RADIUS+20);
    halo.delete();

    var outerShadow=gdip.solidBrush(0x75000000);
    graphics.fillCircle(outerShadow,ART_CX+5,ART_CY+8,ART_RADIUS+10);
    outerShadow.delete();
    var rim=gdip.lineBrush(
        ::POINTF(ART_CX-ART_RADIUS,ART_CY-ART_RADIUS),
        ::POINTF(ART_CX+ART_RADIUS,ART_CY+ART_RADIUS),
        0xFF617589,0xFF111B2A
    );
    graphics.fillCircle(rim,ART_CX,ART_CY,ART_RADIUS+7);
    rim.delete();
    var glassBack=gdip.solidBrush(0xFF0A1626);
    graphics.fillCircle(glassBack,ART_CX,ART_CY,ART_RADIUS);
    glassBack.delete();

    var lead=gdip.pen(0xE7060A12,4.2);
    for(i,block in game.blocks){
        var points=artPiecePoints(i,#game.blocks);
        if(block.removed){
            var fill=gdip.lineBrush(
                ::POINTF(points[1].x,points[1].y),::POINTF(points[3].x,points[3].y),
                shadeColor(block.color,58),shadeColor(block.color,-48)
            );
            graphics.fillPolygon(fill,points);
            fill.delete();
            var glint=gdip.pen(alphaColor(0xFFFFFFFF,105),1.1);
            graphics.drawLine(glint,points[1],points[2]);
            glint.delete();
        }
        else{
            var dark=gdip.solidBrush(0xFF112234);
            graphics.fillPolygon(dark,points);
            dark.delete();
        }
        graphics.drawPolygon(lead,points);
    }
    lead.delete();

    var centerColor=game.state==STATE_SOLVED ? levelData.accent : alphaColor(levelData.accent,70+game.removedCount*8);
    var center=gdip.solidBrush(centerColor);
    graphics.fillCircle(center,ART_CX,ART_CY,29);
    center.delete();
    var centerGlow=gdip.pen(alphaColor(0xFFFFFFFF,game.state==STATE_SOLVED ? 210 : 80),2);
    graphics.drawCircle(centerGlow,ART_CX,ART_CY,29);
    centerGlow.delete();

    var ring1=gdip.pen(0xC9080D16,4);
    graphics.drawCircle(ring1,ART_CX,ART_CY,ART_RADIUS);
    ring1.delete();
    var ring2=gdip.pen(alphaColor(levelData.accent,105),1.2);
    graphics.drawCircle(ring2,ART_CX,ART_CY,ART_RADIUS+3);
    ring2.delete();

    drawText(graphics,game.removedCount++" / "++#game.blocks,12,ART_CX-42,ART_CY-12,84,24,
        game.state==STATE_SOLVED ? 0xFFFFFFFF : 0xFFDCEAF5,true,1);
}

var starCount = function(){
    if(game.bumps==0) return 3;
    if(game.bumps<=2) return 2;
    return 1;
}

var drawSidePanel = function(graphics){
    var levelData=levels[game.level];
    var panelShadow=gdip.solidBrush(0x58000000);
    graphics.fillRoundRect(panelShadow,606,112,340,548,22);
    panelShadow.delete();
    var panel=gdip.lineBrush(::POINTF(610,108),::POINTF(940,660),0xE61B2D42,0xEE0C1727);
    graphics.fillRoundRect(panel,600,104,340,548,22);
    panel.delete();
    var border=gdip.pen(0x425F8AA4,1.2);
    graphics.drawRoundRect(border,600,104,340,548,22);
    border.delete();

    drawText(graphics,"第 "++game.level++" / "++#levels++" 关",11,626,122,312,20,alphaColor(levelData.accent,210),true,1);
    drawText(graphics,levelData.name,22,620,143,300,34,0xFFF2F7FF,true,1);
    drawText(graphics,levelData.subtitle,11,620,176,300,26,0xAFC8D8E5,false,1);
    drawStainedWindow(graphics);

    drawText(graphics,"花窗点亮进度",11,628,423,270,20,0xBFD5E3EE,false,0);
    var barBack=gdip.solidBrush(0xFF091420);
    graphics.fillRoundRect(barBack,628,449,284,12,6);
    barBack.delete();
    var ratio=game.removedCount/#game.blocks;
    if(ratio>0){
        var bar=gdip.lineBrush(::POINTF(628,449),::POINTF(912,449),
            shadeColor(levelData.accent,45),shadeColor(levelData.accent,-30));
        graphics.fillRoundRect(bar,628,449,284*ratio,12,6);
        bar.delete();
    }

    drawText(graphics,"操作 "++game.moves,11,628,474,82,26,0xFFDCEAF4,true,0);
    drawText(graphics,"碰撞 "++game.bumps,11,720,474,82,26,game.bumps ? 0xFFFFA3AE : 0xFF9DEBBE,true,0);
    drawText(graphics,"剩余 "++(#game.blocks-game.removedCount),11,812,474,100,26,0xFFDCEAF4,true,0);

    var statusBack=gdip.solidBrush(game.state==STATE_SOLVED ? alphaColor(levelData.accent,72) : 0x60101F31);
    graphics.fillRoundRect(statusBack,624,510,292,62,13);
    statusBack.delete();
    var statusBorder=gdip.pen(game.state==STATE_SOLVED ? alphaColor(levelData.accent,195) : 0x334D7892,1.3);
    graphics.drawRoundRect(statusBorder,624,510,292,62,13);
    statusBorder.delete();
    drawText(graphics,game.statusText,11,638,518,264,46,
        game.state==STATE_SOLVED ? 0xFFFFFFFF : 0xFFD8E6F0,game.state==STATE_SOLVED,1);

    if(game.state==STATE_SOLVED){
        var starsText="";
        for(i=1;3) starsText++=(i<=starCount() ? "★" : "☆");
        drawText(graphics,starsText,22,628,580,284,34,0xFFFFD96A,true,1);
        drawText(graphics,"零碰撞可以获得三星评价",11,628,615,284,22,0xBFDDE8F2,false,1);
    }
    else{
        drawText(graphics,"H  提示",11,628,586,84,26,0xFFC7D8E5,true,0);
        drawText(graphics,"R / 右键  重置",11,722,586,190,26,0xFFC7D8E5,true,0);
        drawText(graphics,"同色光门只接收沿箭头飞来的玻璃",10,628,617,284,20,0xC2C6D7E3,false,1);
    }
}

var drawBackground = function(graphics){
    var background=gdip.lineBrush(::POINTF(0,0),::POINTF(CANVAS_W,CANVAS_H),0xFF0B1C30,0xFF21112C);
    graphics.fillRect(background,0,0,CANVAS_W,CANVAS_H);
    background.delete();

    var glow1=gdip.solidBrush(0x1437DDF2);
    graphics.fillCircle(glow1,150,180,230);
    glow1.delete();
    var glow2=gdip.solidBrush(0x16E958B6);
    graphics.fillCircle(glow2,900,120,250);
    glow2.delete();

    var facetPen=gdip.pen(0x1018C4DA,1);
    graphics.drawPolygon(facetPen,[{x=0;y=82},{x=250;y=0},{x=430;y=110},{x=180;y=260}]);
    graphics.drawPolygon(facetPen,[{x=760;y=0},{x=980;y=90},{x=900;y=310},{x=690;y=210}]);
    graphics.drawPolygon(facetPen,[{x=670;y=560},{x=980;y=410},{x=980;y=720},{x=730;y=720}]);
    facetPen.delete();

    for(i,star in stars){
        var starBrush=gdip.solidBrush(alphaColor(0xFFFFFFFF,star.alpha));
        graphics.fillCircle(starBrush,star.x,star.y,star.size);
        starBrush.delete();
    }

    drawText(graphics,"彩 色 玻 璃 出 逃",29,0,18,590,42,0xFFF4F8FF,true,1);
    drawText(graphics,"COLOR GLASS ESCAPE",10,0,57,590,20,0x8DC1D6E6,false,1);
    drawText(graphics,"先清理挡路的玻璃,让彩块沿箭头穿过同色光门",13,0,84,590,26,0xC4D8E5EF,false,1);
}

var buildSceneCache = function(){
    var width=math.ceil(CANVAS_W*CACHE_DPI_X);
    var height=math.ceil(CANVAS_H*CACHE_DPI_Y);
    if(sceneCache) sceneCache.delete();
    sceneCache=gdip.bitmap(width,height);
    var graphics=gdip.graphics(sceneCache);
    graphics.smoothingMode=4/*_SmoothingModeAntiAlias*/;
    graphics.textRenderingHint=4/*_TextRenderingHintAntiAlias*/;
    graphics.scale(CACHE_DPI_X,CACHE_DPI_Y);

    drawBackground(graphics);
    drawBoard(graphics);
    for(i,block in game.blocks){
        if(!block.removed && !block.escaping && game.bumping!=block.id){
            drawGlassBlock(graphics,block,0,0,255,0);
        }
    }
    drawSidePanel(graphics);

    graphics.delete();
    sceneDirty=false;
}

// -------------------- 动态绘图 --------------------
var routePoints = function(block,blocker){
    var rc=blockRect(block);
    var sx,sy,ex,ey;
    select(block.dir){
        case "R" { sx=rc.x+rc.w;sy=rc.y+rc.h/2;ex=BOARD_X+BOARD_SIZE+23;ey=sy; }
        case "L" { sx=rc.x;sy=rc.y+rc.h/2;ex=BOARD_X-23;ey=sy; }
        case "D" { sx=rc.x+rc.w/2;sy=rc.y+rc.h;ex=sx;ey=BOARD_Y+BOARD_SIZE+23; }
        case "U" { sx=rc.x+rc.w/2;sy=rc.y;ex=sx;ey=BOARD_Y-23; }
    }
    if(blocker){
        var br=blockRect(blocker);
        select(block.dir){
            case "R" { ex=br.x-5; }
            case "L" { ex=br.x+br.w+5; }
            case "D" { ey=br.y-5; }
            case "U" { ey=br.y+br.h+5; }
        }
    }
    return sx,sy,ex,ey;
}

var drawRoutePreview = function(graphics,block){
    var blocker=findBlocker(block);
    var sx,sy,ex,ey=routePoints(block,blocker);
    var routeColor=blocker ? 0xFFFF617B : block.color;
    var glow=gdip.pen(alphaColor(routeColor,38),8);
    graphics.drawLine(glow,sx,sy,ex,ey);
    glow.delete();
    var line=gdip.pen(alphaColor(routeColor,190),1.8);
    graphics.drawLine(line,sx,sy,ex,ey);
    line.delete();

    var dx=ex-sx;
    var dy=ey-sy;
    for(i=1;6){
        var t=(i/7+((game.pulse%900)/900)*0.14)%1;
        var dot=gdip.solidBrush(alphaColor(routeColor,70+i*20));
        graphics.fillCircle(dot,sx+dx*t,sy+dy*t,1.6+i*0.18);
        dot.delete();
    }

    if(blocker){
        var br=blockRect(blocker);
        var pulse=3+2*math.sin(game.pulse/90);
        var alert=gdip.pen(alphaColor(0xFFFF617B,155),2.2);
        graphics.drawRoundRect(alert,br.x-pulse,br.y-pulse,br.w+pulse*2,br.h+pulse*2,13);
        alert.delete();
    }
    else{
        var gx,gy=gatePoint(block,false);
        var gateGlow=gdip.pen(alphaColor(block.color,120+50*math.sin(game.pulse/130)),3.2);
        graphics.drawCircle(gateGlow,gx,gy,12+2*math.sin(game.pulse/150));
        gateGlow.delete();
    }

    var emphasis=0.45+0.25*math.sin(game.pulse/150);
    var rc=blockRect(block);
    var border=gdip.pen(alphaColor(routeColor,145),2.2);
    graphics.drawRoundRect(border,rc.x-2-emphasis,rc.y-2-emphasis,rc.w+4+emphasis*2,rc.h+4+emphasis*2,13);
    border.delete();
}

var bumpOffset = function(block){
    var progress=1-block.bump;
    var amount=(math.sin(progress*math.pi*5)*5+math.sin(progress*math.pi)*5)*block.bump;
    select(block.dir){
        case "R" { return amount,0; }
        case "L" { return -amount,0; }
        case "D" { return 0,amount; }
        case "U" { return 0,-amount; }
    }
}

var drawParticles = function(graphics){
    for(i,p in game.particles){
        var ratio=math.clamp(p.life/p.maxLife,0,1);
        var size=p.size*(0.5+ratio*0.7);
        var angle=p.spin+(1-ratio)*3;
        var cs=math.cos(angle);
        var sn=math.sin(angle);
        var points=[
            {x=p.x+cs*size;y=p.y+sn*size},
            {x=p.x-sn*size*0.55;y=p.y+cs*size*0.55},
            {x=p.x-cs*size;y=p.y-sn*size},
            {x=p.x+sn*size*0.55;y=p.y-cs*size*0.55}
        ];
        var brush=gdip.solidBrush(alphaColor(p.color,ratio*220));
        graphics.fillPolygon(brush,points);
        brush.delete();
    }
}

winform.glassCanvas.onDrawBackground = function(graphics,rc,backgroundColor,color){
    return true;
}

winform.glassCanvas.onDrawForeground = function(graphics,rc,foregroundRect,foregroundColor,color,font){
    var dpiX,dpiY=winform.dpiScale(1,1);
    if(math.abs(CACHE_DPI_X-dpiX)>0.001 || math.abs(CACHE_DPI_Y-dpiY)>0.001){
        CACHE_DPI_X=dpiX;
        CACHE_DPI_Y=dpiY;
        sceneDirty=true;
    }
    if(sceneDirty || !sceneCache) buildSceneCache();

    graphics.interpolationMode=3/*_InterpolationModeBilinear*/;
    graphics.drawImageRect(sceneCache,0,0,rc.width,rc.height);

    var saved=graphics.save();
    graphics.scale(dpiX,dpiY);
    graphics.smoothingMode=4/*_SmoothingModeAntiAlias*/;

    if(game.hoverId && !game.moving && game.state==STATE_PLAYING){
        var hoverBlock=game.blocks[game.hoverId];
        if(hoverBlock && !hoverBlock.removed) drawRoutePreview(graphics,hoverBlock);
    }

    if(game.hintId && game.hintTimer>0 && !game.moving){
        var hintBlock=game.blocks[game.hintId];
        if(hintBlock && !hintBlock.removed){
            var rcHint=blockRect(hintBlock);
            var pulse=6+3*math.sin(game.pulse/105);
            var hintGlow=gdip.pen(alphaColor(hintBlock.color,125+60*math.sin(game.pulse/125)),4.2);
            graphics.drawRoundRect(hintGlow,rcHint.x-pulse,rcHint.y-pulse,rcHint.w+pulse*2,rcHint.h+pulse*2,15);
            hintGlow.delete();
        }
    }

    if(game.bumping){
        var bumpBlock=game.blocks[game.bumping];
        var bx,by=bumpOffset(bumpBlock);
        drawBlockSprite(graphics,bumpBlock,bx,by);
    }

    if(game.moving){
        var movingBlock=game.blocks[game.moving];
        var movingRect=blockRect(movingBlock);
        var ox,oy=escapeOffset(movingBlock,movingBlock.travel);

        // 轻量半透明拖尾代替重复绘制完整渐变玻璃,保留速度感并显著降低高 DPI 开销。
        for(trail=3;1;-1){
            var oldProgress=math.max(0,movingBlock.travel-trail*0.055);
            var tx,ty=escapeOffset(movingBlock,oldProgress);
            var trailBrush=gdip.solidBrush(alphaColor(movingBlock.color,12+trail*10));
            graphics.fillRoundRect(trailBrush,
                movingRect.x+tx,movingRect.y+ty,movingRect.w,movingRect.h,11
            );
            trailBrush.delete();
        }
        drawBlockSprite(graphics,movingBlock,ox,oy);
    }

    drawParticles(graphics);

    if(game.state==STATE_SOLVED){
        var levelData=levels[game.level];
        var glow=math.min(1,game.solveGlow);
        var rayPen=gdip.pen(alphaColor(levelData.accent,(28+22*math.sin(game.pulse/180))*glow),2);
        for(i=1;12){
            var angle=i*math.pi/6+game.pulse/4000;
            var r1=ART_RADIUS+14;
            var r2=ART_RADIUS+38+8*math.sin(game.pulse/160+i);
            graphics.drawLine(rayPen,
                ART_CX+math.cos(angle)*r1,ART_CY+math.sin(angle)*r1,
                ART_CX+math.cos(angle)*r2,ART_CY+math.sin(angle)*r2);
        }
        rayPen.delete();
        var solvedRing=gdip.pen(alphaColor(levelData.accent,80+70*math.sin(game.pulse/145)),3+glow*3);
        graphics.drawCircle(solvedRing,ART_CX,ART_CY,ART_RADIUS+9+3*math.sin(game.pulse/170));
        solvedRing.delete();
    }

    graphics.restore(saved);
}

// -------------------- 动画 --------------------
winform.glassCanvas.onAnimation = function(prevState,beginning,change,timestamp,duration,deltaMs){
    deltaMs=math.min(deltaMs,60);
    var dt=deltaMs/1000;
    game.pulse+=deltaMs;

    if(game.statusTime<90000){
        game.statusTime=math.max(0,game.statusTime-deltaMs);
        if(game.statusTime==0 && game.statusText!=defaultStatus()){
            game.statusText=defaultStatus();
            sceneDirty=true;
        }
    }
    game.hintTimer=math.max(0,game.hintTimer-deltaMs);

    if(game.moving){
        var block=game.blocks[game.moving];
        block.travel=math.min(1,block.travel+dt/ESCAPE_DURATION);
        game.trailClock+=deltaMs;
        if(game.trailClock>=48 && block.travel>0.18){
            game.trailClock=0;
            var rc=blockRect(block);
            var ox,oy=escapeOffset(block,block.travel);
            var px=rc.x+rc.w/2+ox;
            var py=rc.y+rc.h/2+oy;
            spawnParticles(px,py,1,block.color,false);
        }
        if(block.travel>=1) finishEscape(block);
    }

    if(game.bumping){
        var block=game.blocks[game.bumping];
        block.bump=math.max(0,block.bump-dt*3.4);
        if(block.bump<=0){
            block.bump=0;
            game.bumping=null;
            sceneDirty=true;
        }
    }

    if(game.state==STATE_SOLVED){
        game.solveGlow=math.min(1,game.solveGlow+dt/0.75);
    }

    for(i=#game.particles;1;-1){
        var p=game.particles[i];
        p.life-=dt;
        p.x+=p.vx*dt;
        p.y+=p.vy*dt;
        var damping=0.975 ** (deltaMs/16);
        p.vx*=damping;
        p.vy=p.vy*damping+24*dt;
        if(p.life<=0) table.remove(game.particles,i);
    }
    return true;
}

winform.glassCanvas.cursor="hand";
winform.glassCanvas.startAnimation(24);
winform.glassCanvas.setFocus();

winform.show();
win.loopMessage();

Markdown 格式