应用实例三:圆锥体积实验室

知识点:理解圆锥体积是等底等高圆柱体积的三分之一。
功能:提供一个“倒沙子”模拟实验。学生有一个装满“沙子”的圆柱容器,点击“倒沙”按钮,沙子会以动画形式倒入一个等底等高的圆锥容器中。需要倒3次才能倒满圆锥,直观验证 V锥=13V柱V_{锥} = \frac{1}{3} V_{柱}V=31V
在这里插入图片描述

/**
 * 圆锥体积实验室
 * 核心功能:模拟倒沙实验,验证 V锥 = 1/3 V柱
 * 实验设计:圆锥(源头) -> 圆柱(目标),倒3次填满圆柱
 */

// 沙粒粒子模型
interface SandParticle {
  x: number;
  y: number;
  speedX: number;
  speedY: number;
  size: number;
  opacity: number;
}

@Entry
@Component
struct ConeVolumeLab {
  // 画布上下文
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

  // 实验状态
  @State private coneFillLevel: number = 1.0;  // 圆锥填充比例 (1.0 = 满)
  @State private cylFillLevel: number = 0.0;   // 圆柱填充比例 (0.0 = 空)
  @State private pourCount: number = 0;        // 倒沙次数统计
  @State private isPouring: boolean = false;   // 是否正在倒沙
  @State private coneRotation: number = 0;     // 圆锥旋转角度(倒沙时倾斜)

  // 动画粒子系统
  private particles: SandParticle[] = [];

  // 容器尺寸常量
  private readonly CONTAINER_W: number = 120;  // 容器最大宽度
  private readonly CONTAINER_H: number = 220;  // 容器高度
  private readonly CYLINDER_X: number = 220;   // 圆柱中心X
  private readonly CONE_X: number = 80;        // 圆锥中心X
  private readonly BASE_Y: number = 300;       // 底部基准线Y

  aboutToAppear(): void {
    // 初始化粒子池
    for (let i = 0; i < 60; i++) {
      this.particles.push(this.createParticle());
    }
  }

  // 创建粒子
  private createParticle(): SandParticle {
    return { x: 0, y: 0, speedX: 0, speedY: 0, size: 3, opacity: 0 };
  }

  // 绘制主场景
  private drawScene(): void {
    const ctx = this.context;
    const w = ctx.width;
    const h = ctx.height;

    ctx.clearRect(0, 0, w, h);

    // 1. 绘制背景与实验台
    ctx.fillStyle = '#F0F4F8';
    ctx.fillRect(0, 0, w, h);

    // 实验台面
    ctx.fillStyle = '#B0BEC5';
    ctx.fillRect(0, this.BASE_Y, w, 100);
    ctx.fillStyle = '#78909C';
    ctx.fillRect(0, this.BASE_Y, w, 4);

    // 2. 绘制圆柱 (右侧目标容器)
    this.drawCylinder(ctx);

    // 3. 绘制圆锥 (左侧源头容器)
    this.drawCone(ctx);

    // 4. 绘制流动沙粒
    if (this.isPouring) {
      this.drawParticles(ctx);
    }

    // 5. 绘制刻度与标注
    this.drawLabels(ctx);
  }

  // 绘制圆柱
  private drawCylinder(ctx: CanvasRenderingContext2D): void {
    const x = this.CYLINDER_X;
    const y = this.BASE_Y;
    const w = this.CONTAINER_W;
    const h = this.CONTAINER_H;
    const ellipseH = 15;

    ctx.save();

    // 1. 绘制容器轮廓 (透明玻璃效果)
    ctx.strokeStyle = '#78909C';
    ctx.lineWidth = 2;

    // 底部椭圆
    ctx.beginPath();
    ctx.ellipse(x, y, w/2, ellipseH, 0, 0, Math.PI * 2);
    ctx.stroke();

    // 圆柱侧边
    ctx.beginPath();
    ctx.moveTo(x - w/2, y);
    ctx.lineTo(x - w/2, y - h);
    ctx.moveTo(x + w/2, y);
    ctx.lineTo(x + w/2, y - h);
    ctx.stroke();

    // 2. 绘制内部沙子
    const sandHeight = h * this.cylFillLevel;
    if (sandHeight > 1) {
      ctx.fillStyle = '#FFCC80'; // 沙子颜色

      // 沙子主体矩形
      ctx.fillRect(x - w/2 + 2, y - sandHeight, w - 4, sandHeight);

      // 沙子顶部椭圆
      ctx.beginPath();
      ctx.ellipse(x, y - sandHeight, w/2 - 2, ellipseH - 2, 0, 0, Math.PI * 2);
      ctx.fill();

      // 绘制顶部遮罩线
      ctx.strokeStyle = '#E6A23C';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.ellipse(x, y - sandHeight, w/2 - 2, ellipseH - 2, 0, 0, Math.PI * 2);
      ctx.stroke();
    }

    // 3. 顶部椭圆 (容器口)
    ctx.strokeStyle = '#78909C';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.ellipse(x, y - h, w/2, ellipseH, 0, 0, Math.PI * 2);
    ctx.stroke();

    ctx.restore();
  }

  // 绘制圆锥
  private drawCone(ctx: CanvasRenderingContext2D): void {
    const x = this.CONE_X;
    const y = this.BASE_Y;
    const w = this.CONTAINER_W;
    const h = this.CONTAINER_H;

    ctx.save();

    // 应用旋转变换 (倒沙时倾斜)
    ctx.translate(x, y - h/2);
    ctx.rotate(this.coneRotation * Math.PI / 180);
    ctx.translate(-x, -(y - h/2));

    // 1. 绘制容器轮廓
    ctx.strokeStyle = '#78909C';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(x, y - h);       // 顶点
    ctx.lineTo(x - w/2, y);     // 左下
    ctx.lineTo(x + w/2, y);     // 右下
    ctx.closePath();
    ctx.stroke();

    // 2. 绘制内部沙子
    if (this.coneFillLevel > 0) {
      ctx.fillStyle = '#FFCC80';

      // 计算沙子高度和宽度 (圆锥体积与高度是立方关系,这里简化为线性高度展示便于理解)
      // 数学上:如果体积是1/3,高度也是1/3位置?不,体积减少,高度也减少。
      // 为了视觉直观,我们用填充高度代表剩余量
      const currentH = h * this.coneFillLevel;
      // 根据高度计算顶部宽度 (相似三角形)
      const topW = w * this.coneFillLevel;

      // 绘制沙子三角形
      ctx.beginPath();
      ctx.moveTo(x, y - currentH); // 沙子顶点
      ctx.lineTo(x - topW/2, y);
      ctx.lineTo(x + topW/2, y);
      ctx.closePath();
      ctx.fill();

      // 顶点遮罩
      ctx.strokeStyle = '#E6A23C';
      ctx.beginPath();
      ctx.moveTo(x, y - currentH);
      ctx.lineTo(x - topW/2, y);
      ctx.lineTo(x + topW/2, y);
      ctx.stroke();
    }

    ctx.restore();
  }

  // 绘制流动粒子
  private drawParticles(ctx: CanvasRenderingContext2D): void {
    ctx.fillStyle = '#FFB74D';
    this.particles.forEach(p => {
      if (p.opacity > 0) {
        ctx.globalAlpha = p.opacity;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
        ctx.fill();
      }
    });
    ctx.globalAlpha = 1.0;
  }

  // 绘制标签
  private drawLabels(ctx: CanvasRenderingContext2D): void {
    // 圆锥标签
    ctx.fillStyle = '#333';
    ctx.font = 'bold 16px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('圆锥 (源)', this.CONE_X, this.BASE_Y + 35);

    // 圆柱标签
    ctx.fillText('圆柱 (目标)', this.CYLINDER_X, this.BASE_Y + 35);

    // 刻度标记 (圆柱上)
    ctx.fillStyle = '#E74C3C';
    ctx.font = '12px sans-serif';
    const h = this.CONTAINER_H;
    for(let i=1; i<=3; i++) {
      const markY = this.BASE_Y - (h * i/3);
      ctx.fillText(`${i}/3`, this.CYLINDER_X + this.CONTAINER_W/2 + 15, markY + 4);

      // 刻度线
      ctx.strokeStyle = '#E74C3C';
      ctx.beginPath();
      ctx.moveTo(this.CYLINDER_X + this.CONTAINER_W/2, markY);
      ctx.lineTo(this.CYLINDER_X + this.CONTAINER_W/2 + 5, markY);
      ctx.stroke();
    }
  }

  // 开始倒沙
  private startPouring(): void {
    if (this.isPouring || this.coneFillLevel <= 0) return;

    this.isPouring = true;

    // 第一步:圆锥倾斜动画
    animateTo({ duration: 400, curve: Curve.EaseOut }, () => {
      this.coneRotation = -45; // 向右倾斜45度
    });

    // 第二步:延迟后开始流沙动画
    setTimeout(() => {
      this.runPourAnimation();
    }, 400);
  }

  // 执行倒沙动画逻辑
  private runPourAnimation(): void {
    const duration = 2000; // 倒一次持续时间
    const startTime = Date.now();
    const startConeLevel = this.coneFillLevel;
    const startCylLevel = this.cylFillLevel;
    const amountToPour = 1.0 / 3.0; // 每次倒1/3

    const animLoop = () => {
      const elapsed = Date.now() - startTime;
      let progress = Math.min(elapsed / duration, 1.0);

      // 平滑缓动
      progress = this.easeInOutQuad(progress);

      // 更新填充高度
      this.coneFillLevel = startConeLevel - amountToPour * progress;
      this.cylFillLevel = startCylLevel + amountToPour * progress;

      // 更新粒子物理效果
      this.updateParticles(progress);

      // 重绘
      this.drawScene();

      if (progress < 1.0) {
        setTimeout(animLoop, 16);
      } else {
        this.finishPouring();
      }
    };

    animLoop();
  }

  // 缓动函数
  private easeInOutQuad(t: number): number {
    return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
  }

  // 更新粒子物理
  private updateParticles(progress: number): void {
    // 生成新粒子
    const spawnX = this.CONE_X + 30; // 圆锥口位置
    const spawnY = this.BASE_Y - this.CONTAINER_H + 40;

    for (let i = 0; i < this.particles.length; i++) {
      const p = this.particles[i];

      if (p.opacity <= 0 && Math.random() > 0.6) {
        // 生成粒子
        p.x = spawnX + (Math.random() - 0.5) * 20;
        p.y = spawnY;
        p.speedX = 5 + Math.random() * 3;
        p.speedY = -1 + Math.random() * 2;
        p.opacity = 1;
        p.size = 2 + Math.random() * 2;
      } else if (p.opacity > 0) {
        // 更新位置
        p.x += p.speedX;
        p.y += p.speedY;
        p.speedY += 0.6; // 重力

        // 到达圆柱口则消失
        if (p.x > this.CYLINDER_X - 20) {
          p.opacity -= 0.1;
        }
      }
    }
  }

  // 结束倒沙
  private finishPouring(): void {
    // 圆锥复位动画
    animateTo({ duration: 400, curve: Curve.EaseOut }, () => {
      this.coneRotation = 0;
    });

    setTimeout(() => {
      this.isPouring = false;
      this.pourCount++;

      // 清理粒子
      this.particles.forEach(p => p.opacity = 0);

      // 自动重置圆锥 (模拟连续实验:拿回装满的圆锥)
      // 教学演示:假设老师又拿了一个装满沙的圆锥
      this.coneFillLevel = 1.0;

      this.drawScene();
    }, 400);
  }

  // 重置实验
  private resetLab(): void {
    this.coneFillLevel = 1.0;
    this.cylFillLevel = 0.0;
    this.pourCount = 0;
    this.isPouring = false;
    this.coneRotation = 0;
    this.particles.forEach(p => p.opacity = 0);
    this.drawScene();
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('📐 圆锥体积实验室')
          .fontSize(26)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2C3E50')

        Blank()

        Text(`倒沙次数: ${this.pourCount}`)
          .fontSize(16)
          .fontColor('#7F8C8D')
      }
      .width('92%')
      .margin({ top: 20 })

      Text('实验:将圆锥中的沙子倒入圆柱')
        .fontSize(14)
        .fontColor('#95A5A6')
        .margin({ top: 5 })

      // 实验画布区
      Stack() {
        Canvas(this.context)
          .width('100%')
          .height(420)
          .backgroundColor('#FFFFFF')
          .onReady(() => {
            this.drawScene();
          })
      }
      .shadow({ radius: 15, color: '#00000015' })

      // 结果验证面板
      Row() {
        Column() {
          Text('圆锥体积')
            .fontSize(14)
            .fontColor('#666')
          Text('1 份')
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#E67E22')
        }
        .layoutWeight(1)

        Text('➡️')
          .fontSize(24)
          .fontColor('#BDC3C7')

        Column() {
          Text('圆柱体积')
            .fontSize(14)
            .fontColor('#666')
          Text('3 份')
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#3498DB')
        }
        .layoutWeight(1)
      }
      .width('90%')
      .padding(15)
      .backgroundColor('#FFFFFF')
      .borderRadius(15)
      .margin({ top: 20 })
      .justifyContent(FlexAlign.SpaceAround)

      // 操作按钮
      Row() {
        Button('🏜️ 倒沙')
          .fontSize(18)
          .width(140)
          .height(50)
          .backgroundColor(this.isPouring ? '#95A5A6' : '#3498DB')
          .enabled(!this.isPouring && this.cylFillLevel < 0.95)
          .onClick(() => this.startPouring())

        Button('🔄 重置')
          .fontSize(18)
          .width(140)
          .height(50)
          .backgroundColor('#E67E22')
          .margin({ left: 25 })
          .onClick(() => this.resetLab())
      }
      .margin({ top: 25 })

      // 结论提示
      Column() {
        Text('💡 实验结论')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
        Text('倒 3 次正好装满!')
          .fontSize(18)
          .fontColor('#E74C3C')
          .fontWeight(FontWeight.Bold)
          .margin({ top: 5 })
        Text('圆柱体积 = 3 × 圆锥体积\n即:V锥 = 1/3 V柱')
          .fontSize(14)
          .fontColor('#555')
          .textAlign(TextAlign.Center)
          .lineHeight(22)
          .margin({ top: 5 })
      }
      .width('90%')
      .padding(12)
      .backgroundColor(this.cylFillLevel > 0.9 ? '#E8F8F5' : '#FFF3E0')
      .borderRadius(10)
      .margin({ top: 20, bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F0F3F6')
  }
}
Logo

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

更多推荐