应用实例六:将军饮马问题演示

知识点:第十三章《轴对称》—— 最短路径问题。
功能:动态演示经典的"将军饮马"问题。屏幕上有点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);
  }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐