Harmonyos应用实例113:圆锥体积实验室
·
应用实例三:圆锥体积实验室
知识点:理解圆锥体积是等底等高圆柱体积的三分之一。
功能:提供一个“倒沙子”模拟实验。学生有一个装满“沙子”的圆柱容器,点击“倒沙”按钮,沙子会以动画形式倒入一个等底等高的圆锥容器中。需要倒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')
}
}
更多推荐



所有评论(0)