Harmonyos应用实例99:表面积探索器
·
应用实例九:表面积探索器
知识点:理解表面积的概念,掌握长方体和正方体表面积的计算方法。
功能:展示一个可旋转的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);
}
}
更多推荐



所有评论(0)