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 格式