aardio 文档
aardio 范例: 自绘小游戏 - 绒线秘画(毛线拆画 / Thread Unweave)
import win.ui;
/*DSG{{*/
var winform = win.form(text="自绘小游戏 - 绒线秘画(毛线拆画 / Thread Unweave)";right=920;bottom=700;bgcolor=0x171126;border="dialog frame";max=false)
winform.add(
threadCanvas={cls="plus";left=0;top=0;right=920;bottom=700;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
)
/*}}*/
import gdip;
/*
- 玩法:点击绣绷边缘发光的可抽线头,按压线依赖从上到下逐根抽走毛线;
被压线头带红叉,误拉会抖动并提示剩余针脚。支持 H 提示、R/右键重置、通关后单击/空格下一幅,配有 `sys.midiOut` 音效。
- 4 幅递增关卡:月下猫眠(10线)、花间蝶语(11线)、鲸落星海(12线)、锦鲤逐光(13线)。
隐藏画随收线进度由半透明逐步显现,完成后显示收线团、星级、短寿命纤维粒子与绣绷光环。
- 曲线/关卡:Catmull-Rom 采样曲线;只从真实交点中建立压线针脚。
依赖始终由低编号指向高编号,天然构成 DAG。4 关初始均有 2 个合法线头,依赖针脚分别为 8/13/15/16;
自动拆线序列已验证全部可解。
- 毛线表现:4 层线体(阴影/边缘/芯线/高光)+ 斜向捻纹 + 少量边缘绒毛;
交叉锁点用沿上层线方向的短压线针脚强化上下关系。线头用毛线球、编号、红叉与动态发光环区分状态。
- 关键性能经验:在 200% DPI(实际画布 1840×1400)逐帧重画整幅场景只有约 3.4 FPS。
最终改为“DPI 原尺寸场景位图缓存 + 动态层”架构:背景、文字、隐藏画、静态毛线、针脚、侧栏只在状态变化时重建;
逐帧仅贴缓存并绘制当前抽线、线头光环和短寿命粒子。缓存分辨率会跟随 `winform.dpiScale`,兼顾高分屏锐度和跨显示器 DPI。
200% DPI 下抽线阶段实测约 28.3 FPS、平均前景绘制约 23.7 ms,较原版提升约 8 倍;视觉验收无模糊、残影或错层。
*/
// 点击露出的发光线头,把压在上层的毛线逐根抽走。
// 彩线交叉处的小针脚表示压线关系;全部拆除后会显露藏在绣绷里的秘画。
var STATE_PLAYING = 1;
var STATE_SOLVED = 2;
var HOOP_CX = 330;
var HOOP_CY = 385;
var HOOP_RADIUS = 245;
var THREAD_RADIUS = 220;
var PULL_DURATION = 0.72;
var levels = [
{
name="月下猫眠";subtitle="月光藏在柔软的线层下面";art="cat";
strandCount=10;variant=1;maxLocks=1;
accent=0xFFFFD36A;fabricTop=0xFF241D46;fabricBottom=0xFF11152E;
colors=[0xFF52E5FF,0xFFFF6FAE,0xFFFFCC58,0xFF9A7CFF,0xFF54E0A4,0xFFFF8A5B,0xFF4FA8FF,0xFFE76DFF,0xFFFFF08A,0xFF7DE8D6]
},
{
name="花间蝶语";subtitle="让被缠住的蝶翼重新舒展";art="butterfly";
strandCount=11;variant=2;maxLocks=2;
accent=0xFFFF77C8;fabricTop=0xFF3A173E;fabricBottom=0xFF1A1531;
colors=[0xFFFF6D9E,0xFFFFA75E,0xFFFFDF6A,0xFF6FE3A5,0xFF55C7FF,0xFF8A84FF,0xFFD56FFF,0xFFFF87D8,0xFF80F0DE,0xFFFFBC72,0xFFB794FF]
},
{
name="鲸落星海";subtitle="潮汐会带走最后一缕乱线";art="whale";
strandCount=12;variant=3;maxLocks=2;
accent=0xFF61E8FF;fabricTop=0xFF142C50;fabricBottom=0xFF0A132B;
colors=[0xFF4BE5FF,0xFF4C9FFF,0xFF6C7CFF,0xFFA66EFF,0xFFFF72C6,0xFFFF8A70,0xFFFFD764,0xFF62E5B1,0xFF61D5E8,0xFFB68CFF,0xFFFFA8D5,0xFF9EF4FF]
},
{
name="锦鲤逐光";subtitle="解开水纹,让好运游回画中";art="koi";
strandCount=13;variant=4;maxLocks=2;
accent=0xFFFFB85C;fabricTop=0xFF173843;fabricBottom=0xFF0D202D;
colors=[0xFFFF745E,0xFFFFA65A,0xFFFFD45E,0xFF73E0A8,0xFF4ED7D1,0xFF55B9FF,0xFF777EFF,0xFFB66DFF,0xFFFF71C6,0xFFFF947D,0xFFA7EB66,0xFF6DEAE4,0xFFFFE29A]
}
];
var game = {
state=STATE_PLAYING;
level=1;
cycle=1;
strands=[];
pulling=null;
removedCount=0;
moves=0;
snags=0;
pulse=0;
hintTimer=2600;
statusText="点击发光的线头,先拆最上层";
statusTime=2600;
particles=[];
fiberClock=0;
revealGlow=0;
};
// 缓存不会逐帧变化的完整场景,只动态绘制线头、抽线和短寿命粒子。
// 缓存分辨率跟随当前 DPI,逐帧只需原尺寸贴图,兼顾高分屏清晰度与速度。
var SCENE_CACHE_SCALE = winform.dpiScale(1,1);
var sceneCache;
var sceneDirty = true;
var lint = [];
for(i=1;54){
table.push(lint,{
x=math.random()*920;y=math.random()*700;
size=0.5+math.random()*1.8;
phase=math.random()*math.pi*2;
drift=0.4+math.random()*0.8;
});
}
import sys.midiOut;
var midiOut = sys.midiOut();
// -------------------- 曲线与压线关系 --------------------
var sampleCurve = function(anchors,steps){
var points = [];
for(i=1;#anchors-1){
var p0 = anchors[math.max(1,i-1)];
var p1 = anchors[i];
var p2 = anchors[i+1];
var p3 = anchors[math.min(#anchors,i+2)];
for(step=0;steps-1){
var t = step/steps;
var t2 = t*t;
var t3 = t2*t;
table.push(points,{
x=0.5*((2*p1.x)+(-p0.x+p2.x)*t+(2*p0.x-5*p1.x+4*p2.x-p3.x)*t2+(-p0.x+3*p1.x-3*p2.x+p3.x)*t3);
y=0.5*((2*p1.y)+(-p0.y+p2.y)*t+(2*p0.y-5*p1.y+4*p2.y-p3.y)*t2+(-p0.y+3*p1.y-3*p2.y+p3.y)*t3);
});
}
}
var last = anchors[#anchors];
table.push(points,{x=last.x;y=last.y});
return points;
}
// 每根线从绣绷的一侧穿向另一侧,波幅与相位随关卡变化。
var makeAnchors = function(strandIndex,strandCount,variant){
var endAngle = -math.pi/2+(strandIndex-1)*math.pi*2/strandCount;
var sway = ((strandIndex+variant)%3-1)*0.16;
var startAngle = endAngle+math.pi+sway;
var sx = HOOP_CX+math.cos(startAngle)*THREAD_RADIUS;
var sy = HOOP_CY+math.sin(startAngle)*THREAD_RADIUS;
var ex = HOOP_CX+math.cos(endAngle)*THREAD_RADIUS;
var ey = HOOP_CY+math.sin(endAngle)*THREAD_RADIUS;
var dx = ex-sx;
var dy = ey-sy;
var length = math.sqrt(dx*dx+dy*dy);
var nx = -dy/length;
var ny = dx/length;
var anchors = [{x=sx;y=sy}];
for(k=1;3){
var t = k/4;
var amplitude = 46+variant*5;
var wave = math.sin((t*2+strandIndex*0.37+variant*0.21)*math.pi)*amplitude
+ math.cos((strandIndex+k*variant)*0.73)*16;
table.push(anchors,{
x=sx+dx*t+nx*wave;
y=sy+dy*t+ny*wave;
});
}
table.push(anchors,{x=ex;y=ey});
return anchors;
}
var segmentIntersection = function(a,b,c,d){
var denominator = (a.x-b.x)*(c.y-d.y)-(a.y-b.y)*(c.x-d.x);
if(math.abs(denominator)<0.0001) return null;
var d1 = (b.x-a.x)*(c.y-a.y)-(b.y-a.y)*(c.x-a.x);
var d2 = (b.x-a.x)*(d.y-a.y)-(b.y-a.y)*(d.x-a.x);
var d3 = (d.x-c.x)*(a.y-c.y)-(d.y-c.y)*(a.x-c.x);
var d4 = (d.x-c.x)*(b.y-c.y)-(d.y-c.y)*(b.x-c.x);
if(d1*d2>=-0.01 || d3*d4>=-0.01) return null;
var n1 = a.x*b.y-a.y*b.x;
var n2 = c.x*d.y-c.y*d.x;
return {
x=(n1*(c.x-d.x)-(a.x-b.x)*n2)/denominator;
y=(n1*(c.y-d.y)-(a.y-b.y)*n2)/denominator;
};
}
var curveIntersection = function(pointsA,pointsB){
for(i=1;#pointsA-1){
for(j=1;#pointsB-1){
var pt = segmentIntersection(pointsA[i],pointsA[i+1],pointsB[j],pointsB[j+1]);
if(pt){
pt.indexA=i;
pt.indexB=j;
return pt;
}
}
}
}
var isAvailable = function(strand){
if(strand.removed || game.pulling==strand.id) return false;
for(i,blockerId in strand.blockers){
if(!game.strands[blockerId].removed) return false;
}
return true;
}
var remainingBlockers = function(strand){
var count = 0;
for(i,blockerId in strand.blockers){
if(!game.strands[blockerId].removed) count++;
}
return count;
}
var countAvailable = function(){
var count = 0;
for(i,strand in game.strands){
if(isAvailable(strand)) count++;
}
return count;
}
var initLevel = function(levelIndex){
game.level = levelIndex;
game.state = STATE_PLAYING;
game.strands = [];
game.pulling = null;
game.removedCount = 0;
game.moves = 0;
game.snags = 0;
game.hintTimer = 2600;
game.statusText = "点击发光的线头,先拆最上层";
game.statusTime = 2600;
game.particles = [];
game.fiberClock = 0;
game.revealGlow = 0;
sceneDirty = true;
var levelData = levels[levelIndex];
for(i=1;levelData.strandCount){
var anchors = makeAnchors(i,levelData.strandCount,levelData.variant);
table.push(game.strands,{
id=i;
color=levelData.colors[(i-1)%#levelData.colors+1];
points=sampleCurve(anchors,10);
blockers=[];
locks=[];
removed=false;
pull=0;
failPulse=0;
});
}
// 只把部分真实交叉点设为“压线针脚”,形成清晰而可解的有向无环依赖。
// 最上面的两根线没有针脚,玩家始终至少有一个合法起点。
var freeTop = 2;
for(i=1;#game.strands-freeTop){
var candidates = [];
for(j=i+1;#game.strands){
var pt = curveIntersection(game.strands[i].points,game.strands[j].points);
if(pt) table.push(candidates,{id=j;point=pt});
}
if(#candidates){
var pos = (i*levelData.variant*2)%#candidates+1;
var picked = candidates[pos];
table.push(game.strands[i].blockers,picked.id);
table.push(game.strands[i].locks,{blocker=picked.id;point=picked.point});
if(levelData.maxLocks>=2 && #candidates>=3 && (i+levelData.variant)%2==0){
var picked2 = candidates[#candidates];
if(picked2.id==picked.id) picked2=candidates[1];
if(picked2.id!=picked.id){
table.push(game.strands[i].blockers,picked2.id);
table.push(game.strands[i].locks,{blocker=picked2.id;point=picked2.point});
}
}
}
}
}
var nextLevel = function(){
var nextIndex = game.level+1;
if(nextIndex>#levels){
nextIndex=1;
game.cycle++;
}
initLevel(nextIndex);
}
// -------------------- 动画反馈与游戏操作 --------------------
var alphaColor = function(color,alpha){
alpha = math.floor(math.clamp(alpha,0,255));
return (alpha<<24)|(color&0x00FFFFFF);
}
var spawnFibers = function(x,y,count,color,burst){
for(i=1;count){
var angle = math.random()*math.pi*2;
var speed = 28+math.random()*(burst ? 150 : 70);
var life = 0.32+math.random()*0.58;
table.push(game.particles,{
x=x;y=y;
vx=math.cos(angle)*speed;
vy=math.sin(angle)*speed;
life=life;maxLife=life;
length=3+math.random()*8;
size=1+math.random()*1.8;
color=color;
});
}
}
var pullTip = function(strand){
var eased = strand.pull*strand.pull*(3-2*strand.pull);
var count = math.max(2,math.floor(#strand.points*(1-eased)));
return strand.points[count],count;
}
var finishPull = function(strand){
var availableBefore = [];
for(i,s in game.strands) availableBefore[i]=isAvailable(s);
strand.removed = true;
strand.pull = 1;
game.removedCount++;
game.pulling = null;
game.moves++;
sceneDirty = true;
var endpoint = strand.points[1];
spawnFibers(endpoint.x,endpoint.y,16,strand.color,false);
for(i,s in game.strands){
if(!s.removed && !availableBefore[i] && isAvailable(s)){
var looseEnd = s.points[#s.points];
spawnFibers(looseEnd.x,looseEnd.y,7,s.color,false);
}
}
if(game.removedCount==#game.strands){
game.state = STATE_SOLVED;
game.revealGlow = 0;
game.statusText = "秘画已经显现";
game.statusTime = 999999;
spawnFibers(HOOP_CX,HOOP_CY,54,levels[game.level].accent,true);
if(midiOut) midiOut.play("changeInstrument(10), 1_,3_,5_,1'__,5_,3_,1__", "C5", 95, 72);
}
else{
game.statusText = "抽出第 "++strand.id++" 根线,新的线头松开了";
game.statusTime = 1200;
if(midiOut) midiOut.play("changeInstrument(10), 3_,5_", "C5", 58);
}
}
var tryPull = function(strandId){
if(game.state==STATE_SOLVED){
nextLevel();
return;
}
if(game.pulling) return;
var strand = game.strands[strandId];
if(!strand || strand.removed) return;
if(!isAvailable(strand)){
var blockedCount = remainingBlockers(strand);
strand.failPulse = 1;
game.snags++;
game.statusText = "线头 "++strand.id++" 仍被 "++blockedCount++" 个针脚压住";
game.statusTime = 1300;
sceneDirty = true;
var endpoint = strand.points[#strand.points];
spawnFibers(endpoint.x,endpoint.y,8,0xFFFF668C,false);
if(midiOut) midiOut.play("changeInstrument(115), 5_,3_", "C3", 80);
return;
}
game.pulling = strand.id;
strand.pull = 0;
game.fiberClock = 0;
game.hintTimer = 0;
game.statusText = "正在抽出第 "++strand.id++" 根线";
game.statusTime = 900;
sceneDirty = true;
if(midiOut) midiOut.play("changeInstrument(98), 1_,5_", "C6", 38);
}
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 hitLooseEnd = function(x,y){
for(i=#game.strands;1;-1){
var strand = game.strands[i];
if(!strand.removed && game.pulling!=i){
var endpoint = strand.points[#strand.points];
if(math.dist(x,y,endpoint.x,endpoint.y)<=23) return i;
}
}
}
winform.threadCanvas.onMouseDown = function(wParam,lParam){
if(game.state==STATE_SOLVED){
nextLevel();
return;
}
var x,y = logicalMousePos(lParam);
var strandId = hitLooseEnd(x,y);
if(strandId) tryPull(strandId);
}
winform.threadCanvas.onRightMouseUp = function(){
initLevel(game.level);
}
winform.threadCanvas.dlgCode = 4/*_DLGC_WANTALLKEYS*/;
winform.threadCanvas.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.hintTimer=2200;
game.statusText="发光的线头可以安全抽出";
game.statusTime=2200;
sceneDirty=true;
if(midiOut) midiOut.play("changeInstrument(10), 1_,3_", "C6", 45);
}
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 drawFabric = function(graphics,levelData){
var fabric = gdip.lineBrush(
::POINTF(HOOP_CX,HOOP_CY-HOOP_RADIUS),
::POINTF(HOOP_CX,HOOP_CY+HOOP_RADIUS),
levelData.fabricTop,levelData.fabricBottom
);
graphics.fillCircle(fabric,HOOP_CX,HOOP_CY,HOOP_RADIUS);
fabric.delete();
// 交错经纬线在拆线过程中逐渐淡去,完成画面不会残留“未拆毛线”的错觉。
var revealRatio = game.removedCount/#game.strands;
var lightAlpha = math.floor(14-revealRatio*10);
var darkAlpha = math.floor(10-revealRatio*7);
var weavePen1 = gdip.pen((lightAlpha<<24)|0xFFFFFF,0.75);
var weavePen2 = gdip.pen((darkAlpha<<24)|0x000000,0.75);
for(pos=-220;220;12){
graphics.drawLine(weavePen1,HOOP_CX+pos,HOOP_CY-240,HOOP_CX+pos+48,HOOP_CY+240);
graphics.drawLine(weavePen2,HOOP_CX-240,HOOP_CY+pos,HOOP_CX+240,HOOP_CY+pos+28);
}
weavePen2.delete();
weavePen1.delete();
}
var drawCatArt = function(graphics,alpha){
// 多层半透明圆柔化月晕,避免两个生硬同心圆。
for(i=1;5){
var glowRadius=128-i*8;
var glowAlpha=alpha*(0.035+i*0.018);
var moonGlow=gdip.solidBrush(alphaColor(0xFFFFD979,glowAlpha));
graphics.fillCircle(moonGlow,365,315,glowRadius);
moonGlow.delete();
}
var moon = gdip.solidBrush(alphaColor(0xFFFFE9A5,alpha*0.84));
graphics.fillCircle(moon,365,315,82);
moon.delete();
var hill = gdip.solidBrush(alphaColor(0xFF342857,alpha*0.88));
graphics.fillClosedCurve(hill,[[85,525],[175,455],[270,510],[390,458],[575,535],[575,630],[85,630]]);
hill.delete();
// 坐姿猫使用竖向身体、厚尾巴和前爪,轮廓更舒展。
var cat = gdip.solidBrush(alphaColor(0xFF201832,alpha));
graphics.fillEllipse(cat,304,384,128,170);
graphics.fillCircle(cat,385,365,59);
graphics.fillPolygon(cat,[[343,333],[355,276],[383,326]]);
graphics.fillPolygon(cat,[[397,323],[435,282],[431,345]]);
graphics.fillEllipse(cat,327,492,43,72);
graphics.fillEllipse(cat,374,493,43,72);
cat.delete();
var tail = gdip.pen(alphaColor(0xFF201832,alpha),31);
graphics.drawCurve2(tail,0.58,[[318,478],[245,445],[213,508],[285,548]]);
tail.delete();
var outline = gdip.pen(alphaColor(0xFFCBB6F0,alpha*0.34),2.2);
graphics.drawEllipse(outline,304,384,128,170);
graphics.drawCircle(outline,385,365,59);
graphics.drawPolygon(outline,[[343,333],[355,276],[383,326]]);
graphics.drawPolygon(outline,[[397,323],[435,282],[431,345]]);
outline.delete();
// 弯月形闭眼、鼻尖与胡须让“猫眠”表情更生动。
var facePen = gdip.pen(alphaColor(0xFFFFD75D,alpha),3.2);
graphics.drawCurve2(facePen,0.5,[[358,362],[367,369],[376,362]]);
graphics.drawCurve2(facePen,0.5,[[394,362],[403,369],[412,362]]);
graphics.drawLine(facePen,384,377,388,379);
facePen.delete();
var whisker = gdip.pen(alphaColor(0xFFFFE8B8,alpha*0.7),1.4);
graphics.drawLine(whisker,370,383,340,378);
graphics.drawLine(whisker,370,388,338,391);
graphics.drawLine(whisker,402,383,432,378);
graphics.drawLine(whisker,402,388,434,391);
whisker.delete();
var star = gdip.solidBrush(alphaColor(0xFFFFFFFF,alpha*0.78));
for(i,pt in [[205,260],[250,205],[475,270],[485,390],[155,360]]){
graphics.fillCircle(star,pt[1],pt[2],2.2);
graphics.fillRect(star,pt[1]-0.6,pt[2]-6,1.2,12);
}
star.delete();
}
var drawButterflyArt = function(graphics,alpha){
var glow = gdip.solidBrush(alphaColor(0xFFFF63BE,alpha*0.15));
graphics.fillCircle(glow,330,375,150);
glow.delete();
var pink = gdip.solidBrush(alphaColor(0xFFFF6EBF,alpha*0.82));
var violet = gdip.solidBrush(alphaColor(0xFF9D79FF,alpha*0.82));
var gold = gdip.solidBrush(alphaColor(0xFFFFD870,alpha*0.9));
graphics.fillClosedCurve(pink,[[320,370],[265,245],[155,250],[185,355],[292,397]]);
graphics.fillClosedCurve(violet,[[340,370],[395,245],[505,250],[475,355],[368,397]]);
graphics.fillClosedCurve(violet,[[302,405],[220,385],[180,485],[278,508],[326,427]]);
graphics.fillClosedCurve(pink,[[358,405],[440,385],[480,485],[382,508],[334,427]]);
graphics.fillCircle(gold,235,315,13);
graphics.fillCircle(gold,425,315,13);
graphics.fillCircle(gold,248,450,10);
graphics.fillCircle(gold,412,450,10);
gold.delete();violet.delete();pink.delete();
var body = gdip.solidBrush(alphaColor(0xFF211736,alpha));
graphics.fillEllipse(body,318,315,24,135);
graphics.fillCircle(body,330,310,16);
body.delete();
var antenna = gdip.pen(alphaColor(0xFFFFE8B5,alpha),3);
graphics.drawCurve2(antenna,0.5,[[327,300],[300,270],[278,275]]);
graphics.drawCurve2(antenna,0.5,[[333,300],[360,270],[382,275]]);
antenna.delete();
var flower = gdip.solidBrush(alphaColor(0xFFFFB4D8,alpha*0.75));
for(i,pt in [[150,420],[505,405],[205,545],[455,555]]){
for(k=0;5){
var a=k*math.pi/3;
graphics.fillCircle(flower,pt[1]+math.cos(a)*11,pt[2]+math.sin(a)*11,7);
}
}
flower.delete();
}
var drawWhaleArt = function(graphics,alpha){
var seaGlow = gdip.solidBrush(alphaColor(0xFF4ADFFF,alpha*0.13));
graphics.fillEllipse(seaGlow,130,240,410,300);
seaGlow.delete();
var whale = gdip.solidBrush(alphaColor(0xFF56BFE8,alpha*0.88));
graphics.fillClosedCurve(whale,[[145,385],[220,305],[370,285],[505,345],[470,410],[335,455],[205,445]]);
graphics.fillPolygon(whale,[[155,382],[95,330],[115,395],[82,440],[166,418]]);
graphics.fillClosedCurve(whale,[[330,430],[390,470],[335,500],[300,448]]);
whale.delete();
var belly = gdip.solidBrush(alphaColor(0xFFB7F3FF,alpha*0.58));
graphics.fillClosedCurve(belly,[[205,405],[320,425],[445,385],[392,438],[270,458]]);
belly.delete();
var eye = gdip.solidBrush(alphaColor(0xFF13263D,alpha));
graphics.fillCircle(eye,426,345,5);
eye.delete();
var wavePen = gdip.pen(alphaColor(0xFF8DEEFF,alpha*0.7),3);
for(i=0;3){
graphics.drawCurve2(wavePen,0.55,[[145,520+i*18],[235,500+i*18],[330,525+i*18],[425,502+i*18],[515,522+i*18]]);
}
wavePen.delete();
var bubble = gdip.pen(alphaColor(0xFFB9F7FF,alpha*0.72),2);
for(i,b in [[210,270,8],[260,220,5],[455,245,9],[505,295,5],[165,245,4]]){
graphics.drawCircle(bubble,b[1],b[2],b[3]);
}
bubble.delete();
}
var drawKoiArt = function(graphics,alpha){
var water = gdip.pen(alphaColor(0xFF74E8D8,alpha*0.32),3);
graphics.drawArc(water,145,220,370,330,20,145);
graphics.drawArc(water,125,250,410,310,190,125);
graphics.drawArc(water,190,285,280,230,35,250);
water.delete();
var koi1 = gdip.solidBrush(alphaColor(0xFFFF855F,alpha*0.9));
graphics.fillClosedCurve(koi1,[[180,330],[255,270],[365,290],[420,350],[355,385],[250,380]]);
graphics.fillPolygon(koi1,[[185,330],[125,285],[145,345],[112,385],[190,365]]);
koi1.delete();
var koi2 = gdip.solidBrush(alphaColor(0xFFFFD071,alpha*0.9));
graphics.fillClosedCurve(koi2,[[480,455],[405,510],[300,480],[250,425],[320,395],[425,405]]);
graphics.fillPolygon(koi2,[[478,455],[535,500],[520,440],[555,405],[475,420]]);
koi2.delete();
var patch = gdip.solidBrush(alphaColor(0xFFFFFFFF,alpha*0.72));
graphics.fillEllipse(patch,270,287,72,85);
graphics.fillEllipse(patch,337,413,70,77);
patch.delete();
var eyes = gdip.solidBrush(alphaColor(0xFF1B2635,alpha));
graphics.fillCircle(eyes,385,328,5);
graphics.fillCircle(eyes,278,447,5);
eyes.delete();
var lotus = gdip.solidBrush(alphaColor(0xFFFF91C8,alpha*0.72));
for(k=0;7){
var a=k*math.pi/4;
graphics.fillEllipse(lotus,180+math.cos(a)*17-7,510+math.sin(a)*12-12,14,24);
}
lotus.delete();
}
var drawHiddenArt = function(graphics){
var ratio = game.removedCount/#game.strands;
// 初始能隐约辨出主题轮廓,仍保留逐步揭画的期待感。
var alpha = 78+ratio*177;
var artName = levels[game.level].art;
if(artName=="cat") drawCatArt(graphics,alpha);
elseif(artName=="butterfly") drawButterflyArt(graphics,alpha);
elseif(artName=="whale") drawWhaleArt(graphics,alpha);
else drawKoiArt(graphics,alpha);
}
var partialPoints = function(points,count){
var part = [];
for(i=1;count) table.push(part,points[i]);
return part;
}
var drawStrand = function(graphics,strand){
if(strand.removed) return;
var count = #strand.points;
var points = strand.points;
if(game.pulling==strand.id){
var tip;
tip,count = pullTip(strand);
points = partialPoints(strand.points,count);
}
var shadowPen = gdip.pen(0x62000000,15);
var edgePen = gdip.pen(alphaColor(strand.color,225),11);
var corePen = gdip.pen(alphaColor(strand.color,255),7.5);
var shinePen = gdip.pen(0x3EFFFFFF,0.9);
graphics.drawLines(shadowPen,points);
graphics.drawLines(edgePen,points);
graphics.drawLines(corePen,points);
graphics.drawLines(shinePen,points);
shinePen.delete();corePen.delete();edgePen.delete();shadowPen.delete();
// 斜向捻纹 + 线体边缘的短绒毛,削弱光滑塑料管观感。
var fiberPen = gdip.pen(0x68FFFFFF,0.9);
var fuzzPen = gdip.pen(alphaColor(strand.color,78),0.75);
for(i=5;count-2;7){
var p0=points[i-1];
var p1=points[i+1];
var dx=p1.x-p0.x;
var dy=p1.y-p0.y;
var length=math.sqrt(dx*dx+dy*dy);
if(length>0.01){
var nx=-dy/length;
var ny=dx/length;
graphics.drawLine(fiberPen,points[i].x-nx*3.6,points[i].y-ny*3.6,points[i].x+nx*3.6,points[i].y+ny*3.6);
var direction=1;
if((i+strand.id)%2==0) direction=-1;
var hair=4+(i*strand.id)%4;
graphics.drawLine(fuzzPen,
points[i].x+nx*direction*4.2,points[i].y+ny*direction*4.2,
points[i].x+nx*direction*(4.2+hair),points[i].y+ny*direction*(4.2+hair)
);
}
}
fuzzPen.delete();
fiberPen.delete();
if(game.pulling==strand.id){
var tip = points[#points];
var tipGlow = gdip.solidBrush(alphaColor(strand.color,72));
var tipBrush = gdip.solidBrush(strand.color);
graphics.fillCircle(tipGlow,tip.x,tip.y,13+math.sin(game.pulse/65)*2);
graphics.fillCircle(tipBrush,tip.x,tip.y,5.5);
tipBrush.delete();tipGlow.delete();
}
}
var drawLockStitches = function(graphics,excludedStrandId){
for(i,strand in game.strands){
if(!strand.removed && strand.id!=excludedStrandId){
for(k,lock in strand.locks){
var blocker = game.strands[lock.blocker];
if(!blocker.removed && blocker.id!=excludedStrandId){
var p=lock.point;
var pointIndex=math.clamp(p.indexB,2,#blocker.points-1);
var p0=blocker.points[pointIndex-1];
var p1=blocker.points[pointIndex+1];
var dx=p1.x-p0.x;
var dy=p1.y-p0.y;
var length=math.sqrt(dx*dx+dy*dy);
var tx=dx/length*7.5;
var ty=dy/length*7.5;
var stitchShadow=gdip.pen(0xD0151028,16);
var stitch=gdip.pen(blocker.color,11.5);
var gleam=gdip.pen(0x9AFFFFFF,1.2);
graphics.drawLine(stitchShadow,p.x-tx,p.y-ty,p.x+tx,p.y+ty);
graphics.drawLine(stitch,p.x-tx,p.y-ty,p.x+tx,p.y+ty);
graphics.drawLine(gleam,p.x-tx*0.75,p.y-ty*0.75,p.x+tx*0.75,p.y+ty*0.75);
gleam.delete();stitch.delete();stitchShadow.delete();
}
}
}
}
}
var drawLooseEndBase = function(graphics,strand){
if(strand.removed || game.pulling==strand.id) return;
var endpoint=strand.points[#strand.points];
var x=endpoint.x;
var y=endpoint.y;
var available=isAvailable(strand);
var shadow=gdip.solidBrush(0x88000000);
var ball=gdip.solidBrush(strand.color);
graphics.fillCircle(shadow,x+2,y+3,16.5);
graphics.fillCircle(ball,x,y,15.5);
ball.delete();shadow.delete();
var yarnPen=gdip.pen(0x72FFFFFF,1.2);
graphics.drawArc(yarnPen,x-10,y-7,20,14,12,156);
graphics.drawArc(yarnPen,x-9,y-3,18,12,188,150);
graphics.drawArc(yarnPen,x-7,y-11,14,22,75,160);
yarnPen.delete();
var badge=gdip.solidBrush(0xA8181328);
graphics.fillCircle(badge,x,y,8.4);
badge.delete();
drawText(graphics,tostring(strand.id),strand.id>=10 ? 9 : 10.5,x-9,y-9,18,18,0xFFFFFFFF,true,1);
if(!available){
var knotBack=gdip.solidBrush(0xE82A2038);
graphics.fillCircle(knotBack,x+11,y+11,6.2);
knotBack.delete();
var knotPen=gdip.pen(0xFFFF8BA8,1.8);
graphics.drawLine(knotPen,x+8,y+8,x+14,y+14);
graphics.drawLine(knotPen,x+14,y+8,x+8,y+14);
knotPen.delete();
}
}
var drawLooseEndEffect = function(graphics,strand){
if(strand.removed || game.pulling==strand.id) return;
var endpoint=strand.points[#strand.points];
var pulse=0.5+0.5*math.sin(game.pulse/240+strand.id);
if(isAvailable(strand)){
var x=endpoint.x;
var y=endpoint.y;
var glowAlpha=game.hintTimer>0 ? 120+pulse*85 : 55+pulse*50;
var glowRing=gdip.pen(alphaColor(strand.color,glowAlpha),7+2*pulse);
var readyRing=gdip.pen(game.hintTimer>0 ? 0xEFFFFFFF : 0xA8FFFFFF,2.2);
graphics.drawCircle(glowRing,x,y,21+3*pulse);
graphics.drawCircle(readyRing,x,y,17.5+pulse*1.8);
if(game.hintTimer>0) graphics.drawArc(readyRing,x-22,y-22,44,44,game.pulse/4+strand.id*20,125);
readyRing.delete();glowRing.delete();
}
elseif(strand.failPulse>0){
var fail=strand.failPulse;
var shakeX=math.sin(game.pulse/22)*fail*5;
var failRing=gdip.pen(alphaColor(0xFFFF4779,fail*190),5+fail*3);
graphics.drawCircle(failRing,endpoint.x+shakeX,endpoint.y,20+fail*3);
failRing.delete();
}
}
var drawHoopFrame = function(graphics,levelData){
var halo = gdip.pen(alphaColor(levelData.accent,30+game.revealGlow*65),22+game.revealGlow*8);
var outer = gdip.pen(0xFF7A4D2B,18);
var wood = gdip.pen(0xFFD39755,11);
var shine = gdip.pen(0xFFFFD994,2.2);
graphics.drawCircle(halo,HOOP_CX,HOOP_CY,HOOP_RADIUS+2);
graphics.drawCircle(outer,HOOP_CX,HOOP_CY,HOOP_RADIUS+2);
graphics.drawCircle(wood,HOOP_CX,HOOP_CY,HOOP_RADIUS+1);
graphics.drawArc(shine,HOOP_CX-HOOP_RADIUS-1,HOOP_CY-HOOP_RADIUS-1,(HOOP_RADIUS+1)*2,(HOOP_RADIUS+1)*2,198,128);
shine.delete();wood.delete();outer.delete();halo.delete();
var clasp = gdip.solidBrush(0xFFD8A964);
var claspDark = gdip.solidBrush(0xFF684329);
graphics.fillRoundRect(claspDark,303,116,54,30,8);
graphics.fillRoundRect(clasp,309,119,42,23,6);
graphics.fillRect(claspDark,326,104,8,18);
graphics.fillCircle(clasp,330,104,7);
claspDark.delete();clasp.delete();
}
var drawYarnBall = function(graphics,x,y,strand){
var slot = gdip.solidBrush(0x72201A35);
graphics.fillCircle(slot,x,y,19);
slot.delete();
if(strand.removed){
var glow = gdip.solidBrush(alphaColor(strand.color,45));
var shadow=gdip.solidBrush(0x76000000);
var ball = gdip.solidBrush(strand.color);
graphics.fillCircle(glow,x,y,23);
graphics.fillEllipse(shadow,x-14,y-11,30,31);
graphics.fillCircle(ball,x,y,16);
ball.delete();shadow.delete();glow.delete();
var darkPen=gdip.pen(0x4A151126,1.4);
var pen = gdip.pen(0x7AFFFFFF,1.1);
graphics.drawArc(darkPen,x-13,y-7,26,16,8,165);
graphics.drawArc(darkPen,x-11,y-1,22,14,178,160);
graphics.drawArc(pen,x-11,y-9,22,14,10,155);
graphics.drawArc(pen,x-9,y-3,18,13,185,145);
graphics.drawArc(pen,x-7,y-13,14,26,72,170);
graphics.drawCurve2(pen,0.5,[[x+12,y+7],[x+19,y+12],[x+22,y+8]]);
pen.delete();darkPen.delete();
}
else{
var ring = gdip.pen(0x385E5875,1.2);
graphics.drawCircle(ring,x,y,18);
ring.delete();
}
drawText(graphics,tostring(strand.id),9,x-10,y-10,20,20,strand.removed ? 0xFFFFFFFF : 0x708E88A2,true,1);
}
var drawSidePanel = function(graphics,width,height){
var levelData=levels[game.level];
var panelShadow=gdip.solidBrush(0x48000000);
var panel=gdip.solidBrush(0xB51C1730);
graphics.fillRoundRect(panelShadow,626,142,270,516,22);
graphics.fillRoundRect(panel,620,136,270,516,22);
panel.delete();panelShadow.delete();
drawText(graphics,"第 "++game.level++" / "++#levels++" 幅",12,642,151,100,24,0xAFCFC8E5,false,0);
drawText(graphics,levelData.name,22,638,177,230,38,levelData.accent,true,0);
drawText(graphics,levelData.subtitle,11,640,214,225,25,0xAFCFC8DF,false,0);
var progressBack=gdip.solidBrush(0x442D2742);
var progress=gdip.solidBrush(alphaColor(levelData.accent,205));
graphics.fillRoundRect(progressBack,644,251,220,10,5);
graphics.fillRoundRect(progress,644,251,220*game.removedCount/#game.strands,10,5);
progress.delete();progressBack.delete();
var status = "已收 "++game.removedCount++" / "++#game.strands++" 可抽 "++countAvailable();
drawText(graphics,status,12,642,265,222,26,0xFFEAE4F5,true,1);
drawText(graphics,"收 线 篮",13,642,295,222,26,0xFFB9B0CE,true,1);
for(i,strand in game.strands){
var col=(i-1)%4;
var row=math.floor((i-1)/4);
drawYarnBall(graphics,670+col*58,340+row*58,strand);
}
if(game.state==STATE_PLAYING){
drawText(graphics,game.statusText,12,640,520,230,46,0xFFDAD3E8,false,1);
}
if(game.state==STATE_SOLVED){
var stars = game.snags==0 ? 3 : (game.snags<=2 ? 2 : 1);
var starText="";
for(i=1;3) starText ++= i<=stars ? "★ " : "☆ ";
drawText(graphics,"秘 画 显 现",18,642,560,222,28,levelData.accent,true,1);
drawText(graphics,starText,22,642,590,222,30,0xFFFFD66F,true,1);
drawText(graphics,"单击 / 空格:下一幅",11,642,621,222,22,0xC5EAE4F3,false,1);
}
else{
drawText(graphics,"H:提示可抽线头",11,642,574,222,22,0xAEBFB8D1,false,1);
drawText(graphics,"R 或右键:重置本幅",11,642,600,222,22,0xAEBFB8D1,false,1);
drawText(graphics,"误拉 "++game.snags++" 次",11,642,624,222,20,0x8FAFA8C0,false,1);
}
}
var buildSceneCache = function(){
if(sceneCache) sceneCache.delete();
var cacheWidth=math.floor(920*SCENE_CACHE_SCALE);
var cacheHeight=math.floor(700*SCENE_CACHE_SCALE);
sceneCache=gdip.bitmap(cacheWidth,cacheHeight);
var graphics=gdip.graphics(sceneCache);
graphics.scale(SCENE_CACHE_SCALE,SCENE_CACHE_SCALE);
graphics.smoothingMode=4/*_SmoothingModeAntiAlias*/;
graphics.textRenderingHint=4/*_TextRenderingHintAntiAlias*/;
var levelData=levels[game.level];
var background=gdip.lineBrush(::POINTF(0,0),::POINTF(920,700),0xFF25162F,0xFF080C19);
graphics.fillRect(background,0,0,920,700);
background.delete();
// 背景绒屑冻结在缓存中;只有真正影响玩法的元素逐帧动画。
for(i,speck in lint){
var alpha=math.floor(28+(math.sin(game.pulse/700*speck.drift+speck.phase)+1)*28);
var lintBrush=gdip.solidBrush((alpha<<24)|0xC9B9DD);
graphics.fillCircle(lintBrush,speck.x,speck.y,speck.size);
lintBrush.delete();
}
drawText(graphics,"绒 线 秘 画",27,0,14,620,38,0xFFF6EEFF,true,1);
drawText(graphics,"THREAD UNWEAVE",10,0,50,620,22,0x86CFC4E1,false,1);
drawText(graphics,"看清交叉处的压线针脚,从最上层线头开始抽离",13,0,79,620,25,0xC7DCD5E8,false,1);
var boardShadow=gdip.solidBrush(0x58000000);
graphics.fillCircle(boardShadow,HOOP_CX+10,HOOP_CY+14,HOOP_RADIUS+16);
boardShadow.delete();
var clipState=graphics.save();
var hoopClip=gdip.path();
hoopClip.addCircle(HOOP_CX,HOOP_CY,HOOP_RADIUS);
graphics.setClipPath(hoopClip);
drawFabric(graphics,levelData);
drawHiddenArt(graphics);
// 抽动中的毛线留给动态层;其他毛线与针脚只在状态变化时重建。
for(i,strand in game.strands){
if(strand.id!=game.pulling) drawStrand(graphics,strand);
}
drawLockStitches(graphics,game.pulling);
// 所有静态线头进入缓存;动态层只叠加呼吸光环与误拉反馈。
for(i,strand in game.strands) drawLooseEndBase(graphics,strand);
hoopClip.delete();
graphics.restore(clipState);
drawHoopFrame(graphics,levelData);
drawSidePanel(graphics,920,700);
graphics.delete();
sceneDirty=false;
}
winform.threadCanvas.onDrawBackground = function(graphics,rc,backgroundColor,color){
return true;
}
winform.threadCanvas.onDrawForeground = function(graphics,rc,foregroundRect,foregroundColor,color,font){
var dpiX,dpiY=winform.dpiScale(1,1);
if(math.abs(SCENE_CACHE_SCALE-dpiX)>0.001){
SCENE_CACHE_SCALE=dpiX;
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*/;
graphics.textRenderingHint=4/*_TextRenderingHintAntiAlias*/;
var clipState=graphics.save();
var hoopClip=gdip.path();
hoopClip.addCircle(HOOP_CX,HOOP_CY,HOOP_RADIUS);
graphics.setClipPath(hoopClip);
if(game.pulling) drawStrand(graphics,game.strands[game.pulling]);
for(i,strand in game.strands){
if(isAvailable(strand) || strand.failPulse>0) drawLooseEndEffect(graphics,strand);
}
// 短寿命纤维粒子数量受控,既有拉丝感也不会长期积累。
for(i,p in game.particles){
var ratio=math.clamp(p.life/p.maxLife,0,1);
var particlePen=gdip.pen(alphaColor(p.color,ratio*210),p.size);
var speed=math.sqrt(p.vx*p.vx+p.vy*p.vy);
var nx=p.length;
var ny=0;
if(speed>0.01){
nx=p.vx/speed*p.length;
ny=p.vy/speed*p.length;
}
graphics.drawLine(particlePen,p.x,p.y,p.x-nx,p.y-ny);
particlePen.delete();
}
hoopClip.delete();
graphics.restore(clipState);
if(game.state==STATE_SOLVED && game.revealGlow>0){
var levelData=levels[game.level];
var haloAlpha=35+game.revealGlow*(45+20*math.sin(game.pulse/180));
var solvedHalo=gdip.pen(alphaColor(levelData.accent,haloAlpha),5+game.revealGlow*4);
graphics.drawCircle(solvedHalo,HOOP_CX,HOOP_CY,HOOP_RADIUS+3);
solvedHalo.delete();
}
graphics.restore(saved);
}
// -------------------- 动画 --------------------
winform.threadCanvas.onAnimation = function(prevState,beginning,change,timestamp,duration,deltaMs){
deltaMs=math.min(deltaMs,60);
game.pulse+=deltaMs;
game.hintTimer=math.max(0,game.hintTimer-deltaMs);
game.statusTime=math.max(0,game.statusTime-deltaMs);
if(game.statusTime==0 && game.state==STATE_PLAYING && game.statusText!="选择绣绷边缘的彩色线头"){
game.statusText="选择绣绷边缘的彩色线头";
sceneDirty=true;
}
var dt=deltaMs/1000;
for(i,strand in game.strands){
strand.failPulse=math.max(0,strand.failPulse-dt*2.3);
}
if(game.pulling){
var strand=game.strands[game.pulling];
strand.pull=math.min(1,strand.pull+dt/PULL_DURATION);
game.fiberClock+=deltaMs;
if(game.fiberClock>=54){
game.fiberClock=0;
var tip=pullTip(strand);
spawnFibers(tip.x,tip.y,2,strand.color,false);
}
if(strand.pull>=1) finishPull(strand);
}
if(game.state==STATE_SOLVED){
game.revealGlow=math.min(1,game.revealGlow+dt/0.8);
}
for(i=#game.particles;1;-1){
var p=game.particles[i];
p.x+=p.vx*dt;
p.y+=p.vy*dt;
p.vx*=0.985;
p.vy=p.vy*0.985+12*dt;
p.life-=dt;
if(p.life<=0) table.remove(game.particles,i);
}
return true;
}
winform.threadCanvas.cursor="hand";
winform.threadCanvas.startAnimation(24);
winform.threadCanvas.setFocus();
winform.show();
win.loopMessage();
Markdown 格式