Harmonyos应用实例146:将军饮马问题演示
·
应用实例六:将军饮马问题演示
知识点:第十三章《轴对称》—— 最短路径问题。
功能:动态演示经典的"将军饮马"问题。屏幕上有点A(将军)、点B(营地)和一条直线L(河流)。学生移动A或B,系统自动找出对称点A’,连接A’B与直线的交点P,展示为何AP+PB最短。

/**
* 最短路径问题演示 - 将军饮马
* 核心数学原理:两点之间线段最短 + 轴对称性质
*/
interface Point {
x: number;
y: number;
}
@Entry
@Component
struct ShortestPathDemo {
// 画布尺寸
private readonly CANVAS_WIDTH: number = 360;
private readonly CANVAS_HEIGHT: number = 500;
// 状态变量:关键点坐标
@State private pointA: Point = { x: 100, y: 120 }; // 将军位置
@State private pointB: Point = { x: 260, y: 150 }; // 营地位置
@State private pointP: Point = { x: 0, y: 0 }; // 饮马点(交点)
@State private pointAp: Point = { x: 0, y: 0 }; // A的对称点
// 河流位置 (水平直线 y = 300)
private readonly RIVER_Y: number = 300;
// 计算结果数据
@State private shortestDistance: number = 0;
@State private randomDistance: number = 0; // 用于对比的随机路径距离
// 画布上下文
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
aboutToAppear() {
this.calculateGeometry();
// 初始化一个随机的对比路径点
this.randomDistance = this.calculateRandomPath({x: 180, y: this.RIVER_Y});
}
build() {
Column() {
// 标题栏
Row() {
Text('🐎 将军饮马问题')
.fontSize(22)
.fontWeight(FontWeight.Bold)
Blank()
Text('最短路径演示')
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.padding(10)
// 核心画布区域
Stack() {
Canvas(this.context)
.width(this.CANVAS_WIDTH)
.height(this.CANVAS_HEIGHT)
.backgroundColor('#FDFDFD')
.borderRadius(12)
.onReady(() => {
this.drawScene();
})
// 可拖拽的点 A (将军)
this.DraggablePoint(
this.pointA,
'A (将军)',
'#E74C3C',
(pos) => {
this.pointA = pos;
this.calculateGeometry();
this.drawScene();
}
)
// 可拖拽的点 B (营地)
this.DraggablePoint(
this.pointB,
'B (营地)',
'#3498DB',
(pos) => {
this.pointB = pos;
this.calculateGeometry();
this.drawScene();
}
)
}
.width(this.CANVAS_WIDTH)
.height(this.CANVAS_HEIGHT)
.margin({ top: 10 })
// 数据分析面板
Column() {
Row() {
Text('对称点 A\' 坐标: ')
.fontSize(14)
Text(`(${this.pointAp.x.toFixed(0)}, ${this.pointAp.y.toFixed(0)})`)
.fontSize(14)
.fontColor('#8E44AD')
.fontWeight(FontWeight.Bold)
}.margin({ top: 5 })
Row() {
Text('最短路径 AP + PB: ')
.fontSize(14)
Text(`${this.shortestDistance.toFixed(1)}`)
.fontSize(18)
.fontColor('#27AE60')
.fontWeight(FontWeight.Bold)
}.margin({ top: 5 })
Row() {
Text('原理验证 A\'B 长度: ')
.fontSize(14)
Text(`${this.getDistance(this.pointAp, this.pointB).toFixed(1)}`)
.fontSize(14)
.fontColor('#E67E22')
}.margin({ top: 5 })
Text('💡 拖动红点A或蓝点B,观察最短路径变化')
.fontSize(12)
.fontColor('#95A5A6')
.margin({ top: 10 })
}
.width('90%')
.padding(15)
.margin({ top: 15 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({ radius: 5, color: '#00000010', offsetY: 2 })
Blank()
}
.width('100%')
.height('100%')
.backgroundColor('#F0F4F8')
}
// ------------------ 组件构建区 ------------------
@Builder
DraggablePoint(
pos: Point,
label: string,
color: string,
onUpdate: (pos: Point) => void
) {
Column() {
Circle()
.width(24)
.height(24)
.fill(color)
.shadow({ radius: 3, color: color, offsetX: 0, offsetY: 0 })
Text(label)
.fontSize(10)
.fontColor('#333333')
.backgroundColor(Color.White)
.padding(2)
.borderRadius(2)
.margin({ top: -5 })
}
.position({ x: pos.x - 12, y: pos.y - 12 }) // 居中定位
.gesture(
PanGesture()
.onActionUpdate((e: GestureEvent) => {
// 更新位置,限制在画布范围内
let newX = pos.x + e.offsetX;
let newY = pos.y + e.offsetY;
newX = Math.max(20, Math.min(this.CANVAS_WIDTH - 20, newX));
newY = Math.max(20, Math.min(this.RIVER_Y - 20, newY)); // 限制在河流上方
onUpdate({ x: newX, y: newY });
})
)
}
// ------------------ 绘图与数学逻辑 ------------------
private drawScene() {
const ctx = this.context;
ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT);
// 1. 绘制网格背景
this.drawGrid(ctx);
// 2. 绘制河流
this.drawRiver(ctx);
// 3. 绘制辅助线:A 到 A' 的虚线
ctx.beginPath();
ctx.strokeStyle = '#AAAAAA';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.moveTo(this.pointA.x, this.pointA.y);
ctx.lineTo(this.pointAp.x, this.pointAp.y);
ctx.stroke();
ctx.setLineDash([]);
// 4. 绘制对称点 A'
ctx.beginPath();
ctx.arc(this.pointAp.x, this.pointAp.y, 6, 0, 6.28);
ctx.fillStyle = '#8E44AD'; // 紫色
ctx.fill();
ctx.font = '12px sans-serif';
ctx.fillStyle = '#8E44AD';
ctx.fillText("A'", this.pointAp.x + 10, this.pointAp.y + 5);
// 5. 绘制连线 A'B (寻找交点的依据)
ctx.beginPath();
ctx.strokeStyle = '#BDC3C7';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.moveTo(this.pointAp.x, this.pointAp.y);
ctx.lineTo(this.pointB.x, this.pointB.y);
ctx.stroke();
ctx.setLineDash([]);
// 6. 绘制一条"非最短"路径进行对比
this.drawRandomPath(ctx);
// 7. 绘制最短路径 A -> P -> B
ctx.beginPath();
ctx.strokeStyle = '#27AE60'; // 绿色
ctx.lineWidth = 3;
ctx.moveTo(this.pointA.x, this.pointA.y);
ctx.lineTo(this.pointP.x, this.pointP.y);
ctx.lineTo(this.pointB.x, this.pointB.y);
ctx.stroke();
// 8. 绘制交点 P (饮马点)
ctx.beginPath();
ctx.arc(this.pointP.x, this.pointP.y, 6, 0, 6.28);
ctx.fillStyle = '#F1C40F'; // 黄色
ctx.fill();
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = '#333';
ctx.fillText("P (饮马点)", this.pointP.x + 10, this.pointP.y - 10);
}
private drawGrid(ctx: CanvasRenderingContext2D) {
ctx.strokeStyle = '#EEEEEE';
ctx.lineWidth = 1;
for (let i = 0; i < this.CANVAS_WIDTH; i += 20) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, this.CANVAS_HEIGHT);
ctx.stroke();
}
for (let i = 0; i < this.CANVAS_HEIGHT; i += 20) {
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(this.CANVAS_WIDTH, i);
ctx.stroke();
}
}
private drawRiver(ctx: CanvasRenderingContext2D) {
// 绘制河流区域
ctx.fillStyle = '#3498DB22'; // 淡蓝色
ctx.fillRect(0, this.RIVER_Y, this.CANVAS_WIDTH, this.CANVAS_HEIGHT - this.RIVER_Y);
// 绘制河流边界线
ctx.beginPath();
ctx.strokeStyle = '#3498DB';
ctx.lineWidth = 3;
ctx.moveTo(0, this.RIVER_Y);
ctx.lineTo(this.CANVAS_WIDTH, this.RIVER_Y);
ctx.stroke();
// 文字标注
ctx.fillStyle = '#2980B9';
ctx.font = '14px sans-serif';
ctx.fillText("河流 L", 10, this.RIVER_Y + 20);
}
private drawRandomPath(ctx: CanvasRenderingContext2D) {
// 绘制一条经过河流上任意点的路径,展示其比最优解长
const randX = 50; // 随便取一个点
const randP: Point = { x: randX, y: this.RIVER_Y };
ctx.beginPath();
ctx.strokeStyle = '#E74C3C55'; // 淡红色
ctx.lineWidth = 2;
ctx.setLineDash([8, 4]);
ctx.moveTo(this.pointA.x, this.pointA.y);
ctx.lineTo(randP.x, randP.y);
ctx.lineTo(this.pointB.x, this.pointB.y);
ctx.stroke();
ctx.setLineDash([]);
}
// 几何计算核心逻辑
private calculateGeometry() {
// 1. 计算 A 关于河流(L) 的对称点 A'
// 对称点坐标公式 (x, 2*y_l - y)
this.pointAp = {
x: this.pointA.x,
y: 2 * this.RIVER_Y - this.pointA.y
};
// 2. 计算 A'B 连线与河流 L 的交点 P
// 直线方程两点式: (y - y1)/(y2 - y1) = (x - x1)/(x2 - x1)
// 令 y = RIVER_Y,求 x
// x = x1 + (RIVER_Y - y1) * (x2 - x1) / (y2 - y1)
const y1 = this.pointAp.y;
const y2 = this.pointB.y;
const x1 = this.pointAp.x;
const x2 = this.pointB.x;
// 防止除以0(A'B垂直于河流的情况)
if (y2 !== y1) {
const px = x1 + (this.RIVER_Y - y1) * (x2 - x1) / (y2 - y1);
this.pointP = { x: px, y: this.RIVER_Y };
} else {
// 平行或重合的特殊情况处理
this.pointP = { x: x1, y: this.RIVER_Y };
}
// 3. 计算最短路径长度
this.shortestDistance = this.getDistance(this.pointA, this.pointP) + this.getDistance(this.pointP, this.pointB);
// 重新计算随机路径长度用于对比
this.randomDistance = this.calculateRandomPath({x: 50, y: this.RIVER_Y});
}
// 计算两点距离
private getDistance(p1: Point, p2: Point): number {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
// 计算经过任意点的路径长度
private calculateRandomPath(p: Point): number {
return this.getDistance(this.pointA, p) + this.getDistance(p, this.pointB);
}
}
更多推荐


所有评论(0)