# aardio 范例: 拼图



```aardio
import fsys.dlg;
import fonts.fontAwesome;
import win.ui;
/*DSG{{*/
var winform = win.form(text='\uD83E\uDDE9 拼图 - 自绘小游戏';right=918;bottom=587;bgcolor=0xF0F0F0;border="thin";max=false)
winform.add(
btnOpen={cls="button";text='\uD83D\uDCC2 选择图片';left=718;top=11;right=809;bottom=39;z=1};
btnStart={cls="button";text='\u25B7 开始游戏';left=821;top=11;right=912;bottom=39;z=2};
cbDifficulty={cls="combobox";left=611;top=12;right=706;bottom=38;edge=1;items={"3 x 3","4 x 4","5 x 5","6 x 6"};mode="dropdown";z=3};
dragOrphan={cls="plus";left=664;top=723;right=823;bottom=889;clipBk=false;transparent=1;z=9};
gameCanvas={cls="plus";left=0;top=0;right=570;bottom=587;db=1;dl=1;dt=1;edge=1;notify=1;z=4};
lblStatus={cls="plus";text="请选择图片并开始计时";left=581;top=339;right=911;bottom=369;align="left";clip=1;color=0x666666;font=LOGFONT(h=-14);notify=1;z=7};
lblTime={cls="plus";text="时间: 0s";left=581;top=304;right=911;bottom=332;align="left";color=0x333333;font=LOGFONT(h=-16);z=6};
previewBox={cls="plus";left=582;top=51;right=912;bottom=291;border={color=0xCCCCCC;radius=4;width=1};z=5};
sideArea={cls="plus";left=581;top=376;right=911;bottom=584;bgcolor=0xE8E8E8;border={color=0xCCCCCC;radius=4;width=1};db=1;dl=1;dr=1;z=8}
)
/*}}*/

winform.cbDifficulty.selIndex = 1;

var gameState = {
	imagePath = null;
	srcBitmap = null;
	blurBitmap = null;
	gridN = 3;
	cellW = 0;
	cellH = 0;
	boardX = 0;
	boardY = 0;
	boardW = 600;
	boardH = 600;
	knobRatio = 0.22;
	pieces = {};
	dragging = null;
	dragOffX = 0;
	dragOffY = 0;
	gameStep = 0;
	gameCompleted = false;
	startTime = 0;
	elapsedTime = 0;
	placedCount = 0;
	totalPieces = 0;
};

import sys.midiOut;
var midiOut = sys.midiOut();
var snapThreshold;

// 生成边信息表
var generateEdges = function(n) {
	var edges = {};
	for(row=1; n) {
		edges[row] = {};
		for(col=1; n) {
			edges[row][col] = {top=0; right=0; bottom=0; left=0};
		}
	}

	for(row=1; n) {
		for(col=1; n) {
			if(col < n) {
				var v = (math.random(0,1) == 0) ? 1 : -1;
				edges[row][col].right = v;
				edges[row][col+1].left = -v;
			}
			if(row < n) {
				var v = (math.random(0,1) == 0) ? 1 : -1;
				edges[row][col].bottom = v;
				edges[row+1][col].top = -v;
			}
		}
	}
	return edges;
};

// 生成边的路径点序列
var buildEdgePoints = function(x0, y0, x1, y1, edgeType, knobSize) {
	if(edgeType == 0) {
		return { {x=x1; y=y1} };
	}

	var dx = x1 - x0;
	var dy = y1 - y0;
	var len = math.sqrt(dx*dx + dy*dy);
	if(len < 1) return { {x=x1; y=y1} };

	var ux = dx / len;
	var uy = dy / len;
	var nx = -uy;
	var ny = ux;

	var dir = edgeType;
	var ks = knobSize * len;

	var t1 = 0.35;
	var t2 = 0.65;
	var neckW = 0.10 * len;
	var headH = ks;

	var p1x = x0 + dx * t1;
	var p1y = y0 + dy * t1;
	var p2x = p1x + nx * dir * neckW * 0.3;
	var p2y = p1y + ny * dir * neckW * 0.3;
	var p3x = x0 + dx * (t1 - 0.05) + nx * dir * headH;
	var p3y = y0 + dy * (t1 - 0.05) + ny * dir * headH;
	var p4x = x0 + dx * 0.5 + nx * dir * headH;
	var p4y = y0 + dy * 0.5 + ny * dir * headH;
	var p5x = x0 + dx * (t2 + 0.05) + nx * dir * headH;
	var p5y = y0 + dy * (t2 + 0.05) + ny * dir * headH;
	var p6x = x0 + dx * t2 + nx * dir * neckW * 0.3;
	var p6y = y0 + dy * t2 + ny * dir * neckW * 0.3;
	var p7x = x0 + dx * t2;
	var p7y = y0 + dy * t2;

	return {
		{x=p1x; y=p1y}, {x=p2x; y=p2y}, {x=p3x; y=p3y}, {x=p4x; y=p4y},
		{x=p5x; y=p5y}, {x=p6x; y=p6y}, {x=p7x; y=p7y}, {x=x1; y=y1}
	};
};

// 构建拼图块路径
var buildPiecePath = function(row, col, cellW, cellH, edgeInfo, knobRatio) {
	var x0, y0 = 0, 0;
	var x1, y1 = cellW, cellH;

	var path = gdip.path();

	var topPts = buildEdgePoints(x0, y0, x1, y0, edgeInfo.top, knobRatio);
	var rightPts = buildEdgePoints(x1, y0, x1, y1, edgeInfo.right, knobRatio);
	var bottomPts = buildEdgePoints(x1, y1, x0, y1, edgeInfo.bottom, knobRatio);
	var leftPts = buildEdgePoints(x0, y1, x0, y0, edgeInfo.left, knobRatio);

	var addEdge = function(pts, startX, startY) {
		if(#pts == 1) {
			path.addLine(startX, startY, pts[1].x, pts[1].y);
		}
		else {
			path.addLine(startX, startY, pts[1].x, pts[1].y);
			path.addBezier(pts[1].x, pts[1].y, pts[2].x, pts[2].y, pts[3].x, pts[3].y, pts[4].x, pts[4].y);
			path.addBezier(pts[4].x, pts[4].y, pts[5].x, pts[5].y, pts[6].x, pts[6].y, pts[7].x, pts[7].y);
			path.addLine(pts[7].x, pts[7].y, pts[8].x, pts[8].y);
		}
	};

	addEdge(topPts, x0, y0);
	addEdge(rightPts, x1, y0);
	addEdge(bottomPts, x1, y1);
	addEdge(leftPts, x0, y1);

	path.closeFigure();
	return path;
};

// 切割拼图块位图
var cutPieceBitmap = function(srcBmp, row, col, cellW, cellH, piecePath, knobRatio) {
	var margin = math.ceil(cellW * knobRatio * 1.2);
	var marginH = math.ceil(cellH * knobRatio * 1.2);
	if(marginH > margin) margin = marginH;

	var bmpW = cellW + margin * 2;
	var bmpH = cellH + margin * 2;

	var pieceBmp = gdip.bitmap(bmpW, bmpH);
	var g = gdip.graphics(pieceBmp);
	g.smoothingMode = 4;
	g.interpolationMode = 7;
	g.pixelOffsetMode = 4;

	g.translate(margin, margin);
	g.setClipPath(piecePath);

	var srcX = (col - 1) * cellW;
	var srcY = (row - 1) * cellH;
	g.drawImageRectRect(srcBmp, -margin, -margin, bmpW, bmpH, srcX - margin, srcY - margin, bmpW, bmpH);

	g.resetClip();
	g.resetTransform();
	g.translate(margin, margin);

	var pen = gdip.pen(0x80000000, 1.5);
	g.drawPath(pen, piecePath);
	pen.delete();

	g.delete();
	return pieceBmp, margin;
};

// 碰撞检测
var hitTestPiece = function(piece, mx, my) {
	var p = piece;
	var px = p.currentX - p.margin;
	var py = p.currentY - p.margin;
	return mx >= px && mx <= px + p.bmpW && my >= py && my <= py + p.bmpH;
};

// 吸附检测
var checkSnap = function(piece) {
	if(piece.rotation % 360 != 0) return false;

	var dx = math.abs(piece.currentX - piece.targetX);
	var dy = math.abs(piece.currentY - piece.targetY);

	if(!snapThreshold) snapThreshold = gameState.cellW * 0.25;
	return dx < snapThreshold && dy < snapThreshold;
};

// 执行吸附并返回是否成功
var trySnapPiece = function(piece) {
	if(checkSnap(piece)) {
		piece.currentX = piece.targetX;
		piece.currentY = piece.targetY;
		piece.rotation = 0;
		piece.placed = true;
		gameState.placedCount++;

		if(midiOut) {
			midiOut.play("changeInstrument(14), 5_", "C5", 80);
		}

		if(gameState.placedCount >= gameState.totalPieces) {
			gameState.gameCompleted = true;
			gameState.elapsedTime = math.floor((time.tick() - gameState.startTime) / 1000);
			winform.lblStatus.text = string.format("恭喜完成！用时 %d 秒", gameState.elapsedTime);
			winform.btnStart.text = "▷ 开始游戏";
		}
		return true;
	}
	return false;
};

var constrainPiecePosition = function(piece) {
	var gs = gameState;
	var canvasRc = winform.gameCanvas.getClientRect();
	var canvasW = canvasRc.right;
	var canvasH = canvasRc.bottom;
	
	// 确保至少有一部分图块在画布内可见
	var minVisible = math.min(gs.cellW, gs.cellH) * 0.4;
	
	// 左边界
	if(piece.currentX + gs.cellW < minVisible) {
		piece.currentX = minVisible - gs.cellW * 0.5;
	}
	// 右边界
	if(piece.currentX > canvasW - minVisible) {
		piece.currentX = canvasW - minVisible - gs.cellW * 0.5;
	}
	// 上边界
	if(piece.currentY + gs.cellH < minVisible) {
		piece.currentY = minVisible - gs.cellH * 0.5;
	}
	// 下边界
	if(piece.currentY > canvasH - minVisible) {
		piece.currentY = canvasH - minVisible - gs.cellH * 0.5;
	}
};

// 绘制拼图块
var drawPieceOnGraphics = function(graphics, piece) {
	var p = piece;
	var drawX = p.currentX - p.margin;
	var drawY = p.currentY - p.margin;

	if(p.rotation % 360 == 0) {
		graphics.drawImage(p.bitmap, drawX, drawY);
	}
	else {
		var cx = drawX + p.bmpW / 2;
		var cy = drawY + p.bmpH / 2;
		graphics.save();
		graphics.translate(cx, cy);
		graphics.rotate(p.rotation);
		graphics.translate(-cx, -cy);
		graphics.drawImage(p.bitmap, drawX, drawY);
		graphics.restore();
	}
};

// 计算 dragOrphan 的精确屏幕位置
var calcDragOrphanPos = function(piece) {
	var p = piece;
	var size = math.max(p.bmpW, p.bmpH) + 20;
	
	var bmpOffsetX = (size - p.bmpW) / 2;
	var bmpOffsetY = (size - p.bmpH) / 2;
	
	var canvasDrawX = p.currentX - p.margin;
	var canvasDrawY = p.currentY - p.margin;
	
	var screenX, screenY = win.toScreen(winform.gameCanvas.hwnd, canvasDrawX, canvasDrawY);
	
	var dragX = screenX - bmpOffsetX;
	var dragY = screenY - bmpOffsetY;
	
	return dragX, dragY, size;
};

var updateDragWindow = function(piece) {
	if(!piece) {
		winform.dragOrphan.show(false);
		return;
	}

	var p = piece;
	var size = math.max(p.bmpW, p.bmpH) + 20;

	var dragBmp = gdip.bitmap(size, size);
	var g = gdip.graphics(dragBmp);
	g.smoothingMode = 4;
	g.interpolationMode = 7;

	g.save();
	g.translate(size / 2, size / 2);
	if(p.rotation % 360 != 0) g.rotate(p.rotation);
	g.translate(-p.bmpW / 2, -p.bmpH / 2);
	g.drawImage(p.bitmap, 0, 0);
	g.restore();

	g.delete();

	winform.dragOrphan.background = dragBmp;

	var dragX, dragY, dragSize = calcDragOrphanPos(p);
	winform.dragOrphan.setPos(dragX, dragY, dragSize, dragSize);
	winform.dragOrphan.show(4/*_SW_SHOWNOACTIVATE*/);
};

// 随机散布拼图块
var scatterPieces = function() {
	var gs = gameState;
	var canvasRc = winform.gameCanvas.getClientRect();
	var canvasW = canvasRc.right;
	var canvasH = canvasRc.bottom;

	for(i=1; #gs.pieces) {
		var p = gs.pieces[i];
		if(!p.placed) {
			p.rotation = math.random(0, 3) * 90;
			var maxX = canvasW - gs.cellW;
			var maxY = canvasH - gs.cellH;
			p.currentX = math.random(10, math.max(10, maxX));
			p.currentY = math.random(10, math.max(10, maxY));
		}
	}

	for(i=#gs.pieces; 1; -1) {
		var j = math.random(1, i);
		gs.pieces[i], gs.pieces[j] = gs.pieces[j], gs.pieces[i];
	}
};

var cleanupPieces = function() {
	var gs = gameState;
	for(i=1; #gs.pieces) {
		var p = gs.pieces[i];
		if(p.bitmap) { p.bitmap.delete(); p.bitmap = null; }
		if(p.path) { p.path.delete(); p.path = null; }
	}
	gs.pieces = {};
};

var initPuzzle = function() {
	var gs = gameState;

	cleanupPieces();
	gs.placedCount = 0;
	gs.gameCompleted = false;
	gs.elapsedTime = 0;
	gs.dragging = null;

	if(!gs.srcBitmap) return;

	var n = gs.gridN;
	var canvasRc = winform.gameCanvas.getClientRect();
	var canvasW = canvasRc.right;
	var canvasH = canvasRc.bottom;

	var boardMargin = 30;
	var boardSize = math.min(canvasW, canvasH) - boardMargin * 2;
	gs.boardW = boardSize;
	gs.boardH = boardSize;
	gs.boardX = (canvasW - boardSize) / 2;
	gs.boardY = (canvasH - boardSize) / 2;

	gs.cellW = boardSize / n;
	gs.cellH = boardSize / n;
	
	snapThreshold = gs.cellW * 0.25;

	var scaledBmp = gdip.bitmap(boardSize, boardSize);
	var sg = gdip.graphics(scaledBmp);
	sg.interpolationMode = 7;
	sg.drawImageRect(gs.srcBitmap, 0, 0, boardSize, boardSize);
	sg.delete();

	var edges = generateEdges(n);

	gs.totalPieces = n * n;
	var pieceIndex = 0;

	for(row=1; n) {
		for(col=1; n) {
			pieceIndex++;

			var edgeInfo = edges[row][col];
			var pPath = buildPiecePath(row, col, gs.cellW, gs.cellH, edgeInfo, gs.knobRatio);
			var pBmp, margin = cutPieceBitmap(scaledBmp, row, col, gs.cellW, gs.cellH, pPath, gs.knobRatio);

			var targetX = gs.boardX + (col - 1) * gs.cellW;
			var targetY = gs.boardY + (row - 1) * gs.cellH;

			var piece = {
				index = pieceIndex;
				row = row;
				col = col;
				bitmap = pBmp;
				path = pPath;
				margin = margin;
				bmpW = pBmp.width;
				bmpH = pBmp.height;
				targetX = targetX;
				targetY = targetY;
				currentX = 0;
				currentY = 0;
				rotation = 0;
				placed = false;
			};

			table.push(gs.pieces, piece);
		}
	}

	scaledBmp.delete();
	scatterPieces();
};

winform.gameCanvas.onDrawBackground = function(graphics,rc,backgroundColor,foregroundColor){
	var p1 = ::POINTF(0, 0);
	var p2 = ::POINTF(rc.right, rc.bottom);
	var brush = gdip.lineBrush(p1, p2, 0xFF2c3e50, 0xFF34495e);
	graphics.fillRectangle(brush, rc);
	brush.delete();
};

winform.gameCanvas.onDrawForeground = function(graphics,rc,foregroundRect,foregroundColor,color,font){
	
	var gs = gameState;
	
	if(gs.gameStep < 2 && #gs.pieces == 0) {
		var font = gdip.font("Tahoma", 16, 0, 3);
		var fmt = gdip.stringFormat();
		fmt.align = 1;
		fmt.lineAlign = 1;
		var brush = gdip.solidBrush(0x80FFFFFF);
		
		var tip;
		if(gs.gameStep) {
			tip = "尽快记住图片，点这里「开始拼图」"	
		} else {
			tip = gs.srcBitmap ? "图片已加载，点这里「开始游戏」" : "点这里「选择图片」"
		}
		
		graphics.drawString(tip, font, ::RECTF(0, 0, rc.right, rc.bottom), fmt, brush);
		brush.delete(); fmt.delete(); font.delete();
		return;
	}

	if(#gs.pieces == 0) return;

	var boardBrush = gdip.solidBrush(0x20FFFFFF);
	graphics.fillRectangle(boardBrush, ::RECTF(gs.boardX, gs.boardY, gs.boardW, gs.boardH));
	boardBrush.delete();

	// 绘制网格线
	var gridPen = gdip.pen(0x40FFFFFF, 1);
	gridPen.setDashStyle(2);
	var n = gs.gridN;
	for(i=0; n) {
		var x = gs.boardX + i * gs.cellW;
		graphics.drawLine(gridPen, x, gs.boardY, x, gs.boardY + gs.boardH);
	}
	for(i=0; n) {
		var y = gs.boardY + i * gs.cellH;
		graphics.drawLine(gridPen, gs.boardX, y, gs.boardX + gs.boardW, y);
	}
	gridPen.delete();

	// 先绘制已放置的拼图块
	for(i=1; #gs.pieces) {
		var p = gs.pieces[i];
		if(p.placed) {
			drawPieceOnGraphics(graphics, p);
		}
	}

	// 再绘制未放置的拼图块（不含正在拖拽的）
	for(i=1; #gs.pieces) {
		var p = gs.pieces[i];
		if(!p.placed && !(gs.dragging && gs.dragging.index == p.index)) {
			drawPieceOnGraphics(graphics, p);
		}
	}

	var borderPen = gdip.pen(0x80FFFFFF, 2);
	graphics.drawRectangle(borderPen, gs.boardX, gs.boardY, gs.boardW, gs.boardH);
	borderPen.delete();

	// 游戏结束提示
	if(gs.gameCompleted) {
		var maskBrush = gdip.solidBrush(0xA000b894);
		graphics.fillRectangle(maskBrush, ::RECTF(0, rc.bottom * 0.35, rc.right, rc.bottom * 0.3));
		maskBrush.delete();

		var font = gdip.font("Tahoma", 22, 0, 3);
		var fmt = gdip.stringFormat();
		fmt.align = 1;
		fmt.lineAlign = 1;

		var msg = string.format("恭喜完成！用时 %d 秒", gs.elapsedTime);
		var msgBrush = gdip.solidBrush(0xFFFFFFFF);
		graphics.drawString(msg, font, ::RECTF(0, rc.bottom * 0.35, rc.right, rc.bottom * 0.3), fmt, msgBrush);

		msgBrush.delete(); fmt.delete(); font.delete();
		
		winform.btnStart.text = "▷ 开始游戏";
		winform.btnStart.disabled = false;
		winform.btnOpen.disabled = false;
		winform.previewBox.redraw();
		
		winform.lblStatus.text = "已完成，点击「开始游戏」重新开始";

		if(midiOut) midiOut.play("changeInstrument(10), 1_,3_,5_,1'__", "C4", 150);
	}
};

winform.gameCanvas.onMouseDown = function(wParam, lParam) {
	var gs = gameState;
	if((gs.gameStep != 2) || gs.gameCompleted) {
		
		if( !gs.gameSte ){
			if(! gs.srcBitmap) return winform.btnOpen.oncommand(); 
			return winform.btnStart.oncommand();	
		}
		
	}

	var mx, my = win.getMessagePos(lParam);

	for(i=#gs.pieces; 1; -1) {
		var p = gs.pieces[i];
		if(!p.placed && hitTestPiece(p, mx, my)) {
			gs.dragging = p;
			gs.dragOffX = mx - p.currentX;
			gs.dragOffY = my - p.currentY;

			table.remove(gs.pieces, i);
			table.push(gs.pieces, p);

			win.setCapture(winform.gameCanvas.hwnd);

			updateDragWindow(p);
			owner.redraw();
			return;
		}
	}
};

winform.gameCanvas.onMouseDrag = function(wParam, lParam) {
	var gs = gameState;
	if(!gs.dragging) return;

	var mx, my = win.getMessagePos(lParam);
	gs.dragging.currentX = mx - gs.dragOffX;
	gs.dragging.currentY = my - gs.dragOffY;

	// 只更新悬浮窗位置，不重绘画布
	var p = gs.dragging;
	var dragX, dragY, size = calcDragOrphanPos(p);
	winform.dragOrphan.setPos(dragX, dragY, size, size);
};

winform.gameCanvas.onMouseUp = function(wParam, lParam) {
	var gs = gameState;
	win.releaseCapture();
	
	if(!gs.dragging) return;

	var p = gs.dragging;
	var mx, my = win.getMessagePos(lParam);
	p.currentX = mx - gs.dragOffX;
	p.currentY = my - gs.dragOffY;

	constrainPiecePosition(p);
	trySnapPiece(p);

	gs.dragging = null;
	
	// 先隐藏悬浮窗再重绘画布
	winform.dragOrphan.show(false);
	owner.redraw();
};

winform.gameCanvas.onRightMouseDown = function(wParam, lParam) {
	var gs = gameState;
	if((gs.gameStep != 2) || gs.gameCompleted) return;

	var mx, my = win.getMessagePos(lParam);

	for(i=#gs.pieces; 1; -1) {
		var p = gs.pieces[i];
		if(!p.placed && hitTestPiece(p, mx, my)) {
			p.rotation = (p.rotation + 90) % 360;

			if(trySnapPiece(p)) {
				owner.redraw();
				return;
			}

			if(gs.dragging && gs.dragging.index == p.index) {
				updateDragWindow(p);
			}

			owner.redraw();

			if(midiOut) {
				midiOut.play("changeInstrument(115), 3_", "C4", 60);
			}
			return;
		}
	}
};

winform.previewBox.onDrawForeground = function(graphics, rc, foregroundRect) {
	
	var gs = gameState;
	var img = (gs.gameStep == 1 || gs.gameCompleted) ? gs.srcBitmap : gs.blurBitmap;
	
	// 缩小图像，再将插值设为 _InterpolationModeNearestNeighbor 实现马赛克（pixelate）效果
	graphics.interpolationMode = (gs.gameStep == 1 || gs.gameCompleted) ? 7 : 5/*_InterpolationModeNearestNeighbor*/;
	graphics.smoothingMode = 4;
	
	if(!img) {
		var font = gdip.font("Tahoma", 11, 0, 3);
		var fmt = gdip.stringFormat();
		fmt.align = 1; fmt.lineAlign = 1;
		var brush = gdip.solidBrush(0xFF999999);
		graphics.drawString("预览图", font, ::RECTF(rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top), fmt, brush);
		brush.delete(); fmt.delete(); font.delete();
		return;
	}

	graphics.drawImageScale(img, rc);
};

winform.setInterval(function() {
	var gs = gameState;
	if(gs.gameStep > 0 && !gs.gameCompleted) {
		gs.elapsedTime = math.floor((time.tick() - gs.startTime) / 1000);
		winform.lblTime.text = string.format("⏱ 时间: %d 秒", gs.elapsedTime);
		if(gs.gameStep > 1) {
			winform.lblStatus.text = string.format("已完成: %d / %d", gs.placedCount, gs.totalPieces);
		} else {
			winform.lblStatus.text = "记住图片并点击开始拼图";
		}
	}
}, 1000);

winform.btnOpen.oncommand = function() { 
	var path = fsys.dlg.open("图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif||",,"选择拼图图片");
	if(!path) return;

	if(gameState.srcBitmap) {
		gameState.srcBitmap.delete();
		gameState.srcBitmap = null;
	}
	if(gameState.blurBitmap) {
		gameState.blurBitmap.delete();
		gameState.blurBitmap = null;
	}

	gameState.imagePath = path;
	gameState.srcBitmap = gdip.bitmap(path);
	gameState.gameCompleted = false;
	gameState.gameStep = 0;
	
	if(!gameState.srcBitmap) {
		winform.msgboxErr("无法加载图片文件");
		return;
	}
	
	/*
	将 gdip.bitmap 图像缩小然后放大就实现了模糊图像的效果（blur effect）
	放大时将 graphics 或 plus 控件的 interpolationMode 设为 _InterpolationModeNearestNeighbor 实现马赛克（pixelate）效果
	*/
	gameState.blurBitmap = gameState.srcBitmap.getThumbnailImage(0.015);
	winform.previewBox.redraw(); //图像模式 expand 自动放大
	
	winform.lblStatus.text = "图片已加载，点击「开始游戏」";
	winform.gameCanvas.redraw();
};

winform.btnStart.oncommand = function() {
	var gs = gameState;

	if(!gs.srcBitmap) {
		winform.msgbox("请先选择一张图片", "提示");
		return;
	}
	
	winform.btnOpen.disabled = true;
	
	if(gs.gameStep == 0 || gs.gameCompleted) {
		cleanupPieces();
		
		gs.gameCompleted = false;
		gs.placedCount = 0;
		gs.dragging = null;
		gs.gameStep = 1;
		gs.startTime = time.tick();
		gs.elapsedTime = 0;
	
		winform.btnStart.text = "🧩 开始拼图";
		winform.btnStart.disabled = false;
		winform.gameCanvas.redraw();
		winform.previewBox.redraw();
		return; 
	}
	
	// gameStep==1 时，开始拼图
	if(gs.gameStep != 1) {
		return;
	}
	
	var selIdx = winform.cbDifficulty.selIndex;
	var difficulties = {3, 4, 5, 6};
	gs.gridN = difficulties[selIdx] : 3;

	snapThreshold = null;

	initPuzzle();
	gs.gameStep = 2;
	winform.previewBox.redraw();
	winform.btnStart.text = "🧩 正在拼图";
	winform.btnStart.disabled = true;
	
	winform.lblStatus.text = string.format("拼图进行中！%dx%d = %d 块", gs.gridN, gs.gridN, gs.totalPieces);
	winform.gameCanvas.redraw();

	if(midiOut) {
		midiOut.play("changeInstrument(10), 1_,2_,3_,4_,5_", "C4", 80);
	}
};

winform.sideArea.onDrawForeground = function(graphics, rc, foregroundRect) {
	graphics.smoothingMode = 4;
	
	var font = gdip.font(_WINXP ? "Tahoma" : "Segoe UI Emoji", 10, 0, 3);
	var fmt = gdip.stringFormat();
	fmt.align = 1;
	var brush = gdip.solidBrush(0xFF666666);

	var tips = {
		"🖱 左键拖拽拼图块";
		"🔄 右键旋转拼图块";
		"📌 靠近正确位置自动吸附";
		"🎯 旋转角度正确才能吸附";
	};

	var dpiX, dpiY = winform.dpiScale(1, 1);
	var lineHeight = 35 * dpiY;
	var y = 20 * dpiY;
	
	for(i=1; #tips) {
		graphics.drawString(tips[i], font, ::RECTF(rc.left, y, rc.right - rc.left, 30 * dpiY), fmt, brush);
		y += lineHeight;
	}

	brush.delete(); fmt.delete(); font.delete();
};

//winform.gameCanvas.dlgCode = 4/*_DLGC_WANTALLKEYS*/
winform.gameCanvas.cursor = ::User32.LoadCursor(null,32649/*_IDC_HAND*/);

winform.show();

//悬浮窗口，独立窗口，与所有者窗口（原来的父窗口）保持相对位置
winform.dragOrphan.orphanWindow(true/*转为透明分层窗口*/);
win.loopMessage();
```

