应用实例九:表面积探索器

知识点:理解表面积的概念,掌握长方体和正方体表面积的计算方法。
功能:展示一个可旋转的3D长方体模型。学生可以点击“展开”按钮,模型动画展开成平面展开图,并标注每个面的面积。应用自动计算表面积,帮助学生理解表面积就是所有面的面积之和。
在这里插入图片描述

// SurfaceAreaExplorer.ets

interface FaceInfo {
  name: string
  width: number
  height: number
  area: number
  color: string
}

interface UnfoldedFace {
  x: number
  y: number
  width: number
  height: number
  name: string
  color: string
  dims: string
  area: number
}

interface Point3D {
  x: number
  y: number
  z: number
}

interface Face3D {
  vertices: number[]
  color: string
  name: string
  normal: Point3D
}

@Entry
@Component
struct SurfaceAreaExplorer {
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

  @State boxLength: number = 5;
  @State boxWidth: number = 4;
  @State boxHeight: number = 3;
  @State surfaceArea: number = 94;
  @State unfoldProgress: number = 0;
  @State isAnimating: boolean = false;
  @State rotationX: number = 25;
  @State rotationY: number = 35;
  @State showLabels: boolean = true;
  @State currentView: '3d' | 'unfolding' | 'unfolded' = '3d';

  private faces: FaceInfo[] = [];
  private lastTouchX: number = 0;
  private lastTouchY: number = 0;
  private isDragging: boolean = false;

  aboutToAppear(): void {
    this.calculate();
  }

  private calculate(): void {
    this.surfaceArea = 2 * (this.boxLength * this.boxWidth + this.boxLength * this.boxHeight + this.boxWidth * this.boxHeight);
    
    this.faces = [
      { name: '上面', width: this.boxLength, height: this.boxWidth, area: this.boxLength * this.boxWidth, color: '#FF6B6B' },
      { name: '下面', width: this.boxLength, height: this.boxWidth, area: this.boxLength * this.boxWidth, color: '#FF8A80' },
      { name: '前面', width: this.boxLength, height: this.boxHeight, area: this.boxLength * this.boxHeight, color: '#4ECDC4' },
      { name: '后面', width: this.boxLength, height: this.boxHeight, area: this.boxLength * this.boxHeight, color: '#80DEEA' },
      { name: '左面', width: this.boxWidth, height: this.boxHeight, area: this.boxWidth * this.boxHeight, color: '#45B7D1' },
      { name: '右面', width: this.boxWidth, height: this.boxHeight, area: this.boxWidth * this.boxHeight, color: '#81D4FA' }
    ];
    
    this.draw();
  }

  build() {
    Column({ space: 12 }) {
      Text('📦 长方体表面积探索器')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor('#2C3E50')

      Row({ space: 6 }) {
        Column() {
          Text('长')
            .fontSize(10)
            .fontColor('#7F8C8D')
          TextInput({ text: this.boxLength.toString() })
            .width(50)
            .height(32)
            .type(InputType.Number)
            .backgroundColor('#F5F5F5')
            .borderRadius(6)
            .textAlign(TextAlign.Center)
            .onChange((v: string) => {
              this.boxLength = Math.max(1, parseInt(v) || 1);
              this.calculate();
            })
        }

        Column() {
          Text('宽')
            .fontSize(10)
            .fontColor('#7F8C8D')
          TextInput({ text: this.boxWidth.toString() })
            .width(50)
            .height(32)
            .type(InputType.Number)
            .backgroundColor('#F5F5F5')
            .borderRadius(6)
            .textAlign(TextAlign.Center)
            .onChange((v: string) => {
              this.boxWidth = Math.max(1, parseInt(v) || 1);
              this.calculate();
            })
        }

        Column() {
          Text('高')
            .fontSize(10)
            .fontColor('#7F8C8D')
          TextInput({ text: this.boxHeight.toString() })
            .width(50)
            .height(32)
            .type(InputType.Number)
            .backgroundColor('#F5F5F5')
            .borderRadius(6)
            .textAlign(TextAlign.Center)
            .onChange((v: string) => {
              this.boxHeight = Math.max(1, parseInt(v) || 1);
              this.calculate();
            })
        }

        Column() {
          Text('标注')
            .fontSize(10)
            .fontColor('#7F8C8D')
          Toggle({ type: ToggleType.Switch, isOn: this.showLabels })
            .width(50)
            .height(26)
            .onChange((isOn: boolean) => {
              this.showLabels = isOn;
              this.draw();
            })
        }
      }
      .width('95%')
      .justifyContent(FlexAlign.Center)

      Canvas(this.context)
        .width('95%')
        .height(260)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .shadow({ radius: 5, color: '#00000010' })
        .onReady(() => {
          this.draw();
        })
        .onTouch((event: TouchEvent) => this.handleTouch(event))

      Row({ space: 8 }) {
        Button('🔄 重置视角')
          .fontSize(12)
          .height(36)
          .layoutWeight(1)
          .backgroundColor('#95A5A6')
          .onClick(() => {
            this.rotationX = 25;
            this.rotationY = 35;
            this.draw();
          })

        if (this.currentView === '3d') {
          Button('📄 展开动画')
            .fontSize(12)
            .height(36)
            .layoutWeight(1)
            .backgroundColor('#27AE60')
            .enabled(!this.isAnimating)
            .onClick(() => this.startUnfoldAnimation())
        } else {
          Button('📦 折叠动画')
            .fontSize(12)
            .height(36)
            .layoutWeight(1)
            .backgroundColor('#E74C3C')
            .enabled(!this.isAnimating)
            .onClick(() => this.startFoldAnimation())
        }
      }
      .width('95%')

      Column() {
        Text(`表面积 = ${this.surfaceArea} 平方厘米`)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2C3E50')

        Text(`= 2 × (${this.boxLength}×${this.boxWidth} + ${this.boxLength}×${this.boxHeight} + ${this.boxWidth}×${this.boxHeight})`)
          .fontSize(11)
          .fontColor('#7F8C8D')
          .margin({ top: 2 })
      }
      .width('95%')
      .padding(10)
      .backgroundColor('#E8F5E9')
      .borderRadius(8)

      Column() {
        Text('📊 各面面积详情')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2C3E50')
          .width('100%')

        Row({ space: 4 }) {
          ForEach(this.faces, (face: FaceInfo, index: number) => {
            Column() {
              Text(face.name)
                .fontSize(9)
                .fontColor('#FFFFFF')
              Text(`${face.area}cm²`)
                .fontSize(8)
                .fontColor('#FFFFFF')
            }
            .width(48)
            .height(36)
            .backgroundColor(face.color)
            .borderRadius(5)
            .justifyContent(FlexAlign.Center)
          })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceEvenly)
        .margin({ top: 6 })

        Text(`总面积 = ${this.faces[0].area}×2 + ${this.faces[2].area}×2 + ${this.faces[4].area}×2 = ${this.surfaceArea}cm²`)
          .fontSize(10)
          .fontColor('#7F8C8D')
          .margin({ top: 6 })
          .width('100%')
      }
      .width('95%')
      .padding(10)
      .backgroundColor('#FAFAFA')
      .borderRadius(8)

      Column() {
        Text('💡 知识要点')
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2C3E50')
        
        Text('• 长方体有6个面,相对的两个面完全相同')
          .fontSize(10)
          .fontColor('#7F8C8D')
          .margin({ top: 2 })
        
        Text('• 表面积 = 所有面的面积之和')
          .fontSize(10)
          .fontColor('#7F8C8D')
          .margin({ top: 1 })
        
        Text('• 公式:S = 2(ab + ah + bh)')
          .fontSize(10)
          .fontColor('#E74C3C')
          .fontWeight(FontWeight.Bold)
          .margin({ top: 1 })
        
        Text('• 拖动3D模型可旋转查看各个面')
          .fontSize(10)
          .fontColor('#3498DB')
          .margin({ top: 1 })
      }
      .width('95%')
      .padding(8)
      .backgroundColor('#FFF9C4')
      .borderRadius(8)
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F0F3F6')
    .padding(12)
  }

  private handleTouch(event: TouchEvent): void {
    if (this.currentView !== '3d' || this.isAnimating) return;

    const touch = event.touches[0];
    
    if (event.type === TouchType.Down) {
      this.isDragging = true;
      this.lastTouchX = touch.x;
      this.lastTouchY = touch.y;
    } else if (event.type === TouchType.Move && this.isDragging) {
      const deltaX = touch.x - this.lastTouchX;
      const deltaY = touch.y - this.lastTouchY;
      
      this.rotationY += deltaX * 0.5;
      this.rotationX += deltaY * 0.5;
      
      this.rotationX = Math.max(-90, Math.min(90, this.rotationX));
      
      this.lastTouchX = touch.x;
      this.lastTouchY = touch.y;
      
      this.draw();
    } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
      this.isDragging = false;
    }
  }

  private startUnfoldAnimation(): void {
    if (this.isAnimating) return;
    
    this.isAnimating = true;
    this.currentView = 'unfolding';
    const duration = 2000;
    const startTime = Date.now();

    const animate = () => {
      const elapsed = Date.now() - startTime;
      const progress = Math.min(elapsed / duration, 1);
      
      const easeProgress = 1 - Math.pow(1 - progress, 3);
      this.unfoldProgress = easeProgress;
      
      this.draw();

      if (progress < 1) {
        setTimeout(animate, 16);
      } else {
        this.currentView = 'unfolded';
        this.isAnimating = false;
      }
    };
    animate();
  }

  private startFoldAnimation(): void {
    if (this.isAnimating) return;
    
    this.isAnimating = true;
    this.currentView = 'unfolding';
    const duration = 2000;
    const startTime = Date.now();

    const animate = () => {
      const elapsed = Date.now() - startTime;
      const progress = Math.min(elapsed / duration, 1);
      
      const easeProgress = Math.pow(progress, 3);
      this.unfoldProgress = 1 - easeProgress;
      
      this.draw();

      if (progress < 1) {
        setTimeout(animate, 16);
      } else {
        this.currentView = '3d';
        this.isAnimating = false;
      }
    };
    animate();
  }

  private draw(): void {
    const ctx = this.context;
    const w = ctx.width;
    const h = ctx.height;
    const centerX = w / 2;
    const centerY = h / 2;

    ctx.clearRect(0, 0, w, h);
    ctx.fillStyle = '#FFFFFF';
    ctx.fillRect(0, 0, w, h);

    if (this.unfoldProgress < 0.01) {
      this.draw3DBox(centerX, centerY);
      ctx.fillStyle = '#7F8C8D';
      ctx.font = '11px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('👆 拖动旋转 3D 模型', centerX, 20);
    } else if (this.unfoldProgress > 0.99) {
      this.drawUnfoldedNet(centerX, centerY);
      ctx.fillStyle = '#27AE60';
      ctx.font = '11px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('✅ 展开图 - 6个面的面积之和', centerX, 20);
    } else {
      this.drawUnfoldingAnimation(centerX, centerY);
      ctx.fillStyle = '#3498DB';
      ctx.font = '11px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText(`展开中... ${Math.round(this.unfoldProgress * 100)}%`, centerX, 20);
    }
  }

  private draw3DBox(centerX: number, centerY: number): void {
    const ctx = this.context;
    const scale = 18;
    const l = this.boxLength * scale;
    const w = this.boxWidth * scale;
    const h = this.boxHeight * scale;

    const radX = this.rotationX * Math.PI / 180;
    const radY = this.rotationY * Math.PI / 180;

    const project = (x: number, y: number, z: number): number[] => {
      const cosY = Math.cos(radY);
      const sinY = Math.sin(radY);
      const cosX = Math.cos(radX);
      const sinX = Math.sin(radX);

      const x1 = x * cosY - z * sinY;
      const z1 = x * sinY + z * cosY;
      const y1 = y * cosX - z1 * sinX;
      const z2 = y * sinX + z1 * cosX;

      const scale = 400 / (400 + z2);
      return [centerX + x1 * scale, centerY + y1 * scale, z2];
    };

    const vertices: Point3D[] = [
      { x: -l/2, y: -h/2, z: -w/2 },
      { x: l/2, y: -h/2, z: -w/2 },
      { x: l/2, y: h/2, z: -w/2 },
      { x: -l/2, y: h/2, z: -w/2 },
      { x: -l/2, y: -h/2, z: w/2 },
      { x: l/2, y: -h/2, z: w/2 },
      { x: l/2, y: h/2, z: w/2 },
      { x: -l/2, y: h/2, z: w/2 }
    ];

    const faceDefinitions: Face3D[] = [
      { vertices: [4, 5, 6, 7], color: '#FF6B6B', name: '上面', normal: { x: 0, y: -1, z: 0 } },
      { vertices: [3, 2, 1, 0], color: '#FF8A80', name: '下面', normal: { x: 0, y: 1, z: 0 } },
      { vertices: [0, 1, 5, 4], color: '#4ECDC4', name: '前面', normal: { x: 0, y: 0, z: -1 } },
      { vertices: [2, 3, 7, 6], color: '#80DEEA', name: '后面', normal: { x: 0, y: 0, z: 1 } },
      { vertices: [0, 4, 7, 3], color: '#45B7D1', name: '左面', normal: { x: -1, y: 0, z: 0 } },
      { vertices: [1, 2, 6, 5], color: '#81D4FA', name: '右面', normal: { x: 1, y: 0, z: 0 } }
    ];

    const projectedVertices: number[][] = [];
    for (let i = 0; i < vertices.length; i++) {
      projectedVertices.push(project(vertices[i].x, vertices[i].y, vertices[i].z));
    }

    const sortedFaces: Face3D[] = [];
    for (let i = 0; i < faceDefinitions.length; i++) {
      sortedFaces.push(faceDefinitions[i]);
    }

    sortedFaces.sort((a: Face3D, b: Face3D): number => {
      let avgZA = 0;
      let avgZB = 0;
      for (let i = 0; i < a.vertices.length; i++) {
        avgZA += projectedVertices[a.vertices[i]][2];
      }
      for (let i = 0; i < b.vertices.length; i++) {
        avgZB += projectedVertices[b.vertices[i]][2];
      }
      return avgZB - avgZA;
    });

    for (let f = 0; f < sortedFaces.length; f++) {
      const face = sortedFaces[f];
      const v0 = projectedVertices[face.vertices[0]];
      
      const v1 = projectedVertices[face.vertices[1]];
      const v2 = projectedVertices[face.vertices[2]];
      
      const edge1X = v1[0] - v0[0];
      const edge1Y = v1[1] - v0[1];
      const edge2X = v2[0] - v0[0];
      const edge2Y = v2[1] - v0[1];
      
      const cross = edge1X * edge2Y - edge1Y * edge2X;
      
      if (cross > 0) continue;

      ctx.beginPath();
      ctx.moveTo(projectedVertices[face.vertices[0]][0], projectedVertices[face.vertices[0]][1]);
      for (let i = 1; i < face.vertices.length; i++) {
        ctx.lineTo(projectedVertices[face.vertices[i]][0], projectedVertices[face.vertices[i]][1]);
      }
      ctx.closePath();

      ctx.fillStyle = face.color;
      ctx.fill();
      ctx.strokeStyle = '#2C3E50';
      ctx.lineWidth = 2;
      ctx.stroke();

      if (this.showLabels) {
        let sumX = 0;
        let sumY = 0;
        for (let i = 0; i < face.vertices.length; i++) {
          sumX += projectedVertices[face.vertices[i]][0];
          sumY += projectedVertices[face.vertices[i]][1];
        }
        const centerXFace = sumX / face.vertices.length;
        const centerYFace = sumY / face.vertices.length;

        ctx.fillStyle = '#FFFFFF';
        ctx.font = 'bold 11px sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(face.name, centerXFace, centerYFace - 6);
        
        const faceInfo = this.faces.find((f: FaceInfo) => f.name === face.name);
        if (faceInfo) {
          ctx.font = '9px sans-serif';
          ctx.fillText(`${faceInfo.area}cm²`, centerXFace, centerYFace + 8);
        }
      }
    }
  }

  private drawUnfoldingAnimation(centerX: number, centerY: number): void {
    const ctx = this.context;
    const scale = 14;
    const l = this.boxLength * scale;
    const w = this.boxWidth * scale;
    const h = this.boxHeight * scale;
    const p = this.unfoldProgress;

    const easeP = 1 - Math.pow(1 - p, 2);

    ctx.save();
    ctx.translate(centerX, centerY);

    const frontY = 0;
    ctx.fillStyle = '#4ECDC4';
    ctx.fillRect(-l/2, frontY - h/2, l, h);
    if (this.showLabels) {
      this.drawFaceInfo(ctx, 0, frontY, l, h, '前面', this.boxLength * this.boxHeight);
    }
    ctx.strokeStyle = '#2C3E50';
    ctx.lineWidth = 2;
    ctx.strokeRect(-l/2, frontY - h/2, l, h);

    const topAngle = easeP * Math.PI / 2;
    ctx.save();
    ctx.translate(0, frontY - h/2);
    ctx.rotate(-topAngle);
    ctx.fillStyle = '#FF6B6B';
    ctx.fillRect(-l/2, -w, l, w);
    if (this.showLabels) {
      ctx.save();
      ctx.rotate(topAngle);
      this.drawFaceInfo(ctx, 0, -h/2 - w/2, l, w, '上面', this.boxLength * this.boxWidth);
      ctx.restore();
    }
    ctx.strokeStyle = '#2C3E50';
    ctx.strokeRect(-l/2, -w, l, w);
    ctx.restore();

    const bottomY = frontY + h/2 + easeP * w;
    ctx.fillStyle = '#FF8A80';
    ctx.fillRect(-l/2, bottomY, l, w);
    if (this.showLabels) {
      this.drawFaceInfo(ctx, 0, bottomY + w/2, l, w, '下面', this.boxLength * this.boxWidth);
    }
    ctx.strokeStyle = '#2C3E50';
    ctx.strokeRect(-l/2, bottomY, l, w);

    const leftAngle = easeP * Math.PI / 2;
    ctx.save();
    ctx.translate(-l/2, frontY);
    ctx.rotate(leftAngle);
    ctx.fillStyle = '#45B7D1';
    ctx.fillRect(0, -h/2, w, h);
    if (this.showLabels) {
      ctx.save();
      ctx.rotate(-leftAngle);
      this.drawFaceInfo(ctx, -l/2 - w/2, 0, w, h, '左面', this.boxWidth * this.boxHeight);
      ctx.restore();
    }
    ctx.strokeStyle = '#2C3E50';
    ctx.strokeRect(0, -h/2, w, h);
    ctx.restore();

    const rightAngle = easeP * Math.PI / 2;
    ctx.save();
    ctx.translate(l/2, frontY);
    ctx.rotate(-rightAngle);
    ctx.fillStyle = '#81D4FA';
    ctx.fillRect(-w, -h/2, w, h);
    if (this.showLabels) {
      ctx.save();
      ctx.rotate(rightAngle);
      this.drawFaceInfo(ctx, l/2 + w/2, 0, w, h, '右面', this.boxWidth * this.boxHeight);
      ctx.restore();
    }
    ctx.strokeStyle = '#2C3E50';
    ctx.strokeRect(-w, -h/2, w, h);
    ctx.restore();

    const backY = frontY + h + easeP * w + easeP * h;
    ctx.fillStyle = '#80DEEA';
    ctx.fillRect(-l/2, backY, l, h);
    if (this.showLabels) {
      this.drawFaceInfo(ctx, 0, backY + h/2, l, h, '后面', this.boxLength * this.boxHeight);
    }
    ctx.strokeStyle = '#2C3E50';
    ctx.strokeRect(-l/2, backY, l, h);

    ctx.restore();
  }

  private drawUnfoldedNet(centerX: number, centerY: number): void {
    const ctx = this.context;
    const scale = 12;
    const l = this.boxLength * scale;
    const w = this.boxWidth * scale;
    const h = this.boxHeight * scale;

    const totalHeight = h + w * 2 + h;
    const startY = centerY - totalHeight / 2 + 20;

    const faces: UnfoldedFace[] = [
      { x: centerX - l/2, y: startY, width: l, height: w, name: '上面', color: '#FF6B6B', dims: `${this.boxLength}×${this.boxWidth}`, area: this.boxLength * this.boxWidth },
      { x: centerX - l/2, y: startY + w, width: l, height: h, name: '前面', color: '#4ECDC4', dims: `${this.boxLength}×${this.boxHeight}`, area: this.boxLength * this.boxHeight },
      { x: centerX - l/2, y: startY + w + h, width: l, height: w, name: '下面', color: '#FF8A80', dims: `${this.boxLength}×${this.boxWidth}`, area: this.boxLength * this.boxWidth },
      { x: centerX - l - w/2, y: startY + w, width: w, height: h, name: '左面', color: '#45B7D1', dims: `${this.boxWidth}×${this.boxHeight}`, area: this.boxWidth * this.boxHeight },
      { x: centerX + l/2 - w/2, y: startY + w, width: w, height: h, name: '右面', color: '#81D4FA', dims: `${this.boxWidth}×${this.boxHeight}`, area: this.boxWidth * this.boxHeight },
      { x: centerX - l/2, y: startY + w + h + w, width: l, height: h, name: '后面', color: '#80DEEA', dims: `${this.boxLength}×${this.boxHeight}`, area: this.boxLength * this.boxHeight }
    ];

    for (let i = 0; i < faces.length; i++) {
      const face = faces[i];
      
      ctx.fillStyle = face.color;
      ctx.fillRect(face.x, face.y, face.width, face.height);
      ctx.strokeStyle = '#2C3E50';
      ctx.lineWidth = 2;
      ctx.strokeRect(face.x, face.y, face.width, face.height);

      if (this.showLabels) {
        ctx.fillStyle = '#FFFFFF';
        ctx.font = 'bold 11px sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(face.name, face.x + face.width/2, face.y + face.height/2 - 6);
        
        ctx.font = '9px sans-serif';
        ctx.fillText(`${face.area}cm²`, face.x + face.width/2, face.y + face.height/2 + 8);
      }
    }

    ctx.fillStyle = '#7F8C8D';
    ctx.font = '10px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(`总面积 = ${this.surfaceArea}cm²`, centerX, startY + totalHeight + 15);
  }

  private drawFaceInfo(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, name: string, area: number): void {
    ctx.fillStyle = '#FFFFFF';
    ctx.font = 'bold 10px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(name, x, y - 5);
    ctx.font = '8px sans-serif';
    ctx.fillText(`${area}cm²`, x, y + 7);
  }
}
Logo

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

更多推荐