Cylinder vs Cone Volume Simulation
/* Custom styles for the 3D canvas container */
#scene-container {
/* Now flex-grow, so we'll set a base height */
height: 60vh;
min-height: 400px;
background-color: #f7f7f7;
border-radius: 1rem;
box-shadow: 0 10px 15px rgba(0, 10, 20, 0.1);
overflow: hidden;
touch-action: none;
}
/* Ensure canvas fills its container */
canvas {
display: block;
}
/* Custom track style for the range sliders */
input[type="range"]::-webkit-slider-runnable-track {
background: #e0e7ff;
border-radius: 9999px;
height: 8px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
background: #4f46e5;
border-radius: 50%;
cursor: pointer;
margin-top: -4px;
box-shadow: 0 0 2px rgba(0,0,0,0.3);
}
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
}
}
}
बेलना र सोलीको आयतन सम्बन्ध Simulation (Cylinder and Cone Volume Relationship Simulation)
सोलीको आयतन = 1/3 बेलनाको आयतन (Volume of Cone = 1/3 Volume of Cylinder)
तयार छ (Ready)
गणना गरिएको आयतन (Calculated Volumes)
बेलनाको आयतन (Cylinder Volume):
एकाइ³ (units³)
शंकुको आयतन (Cone Volume):
एकाइ³ (units³)
थ्रीडी (3D) आकारहरूलाई तान्न र जुम गर्न आफ्नो माउस (वा टच) प्रयोग गर्नुहोस्।
// Global variables for Three.js
let scene, camera, renderer, controls;
let cylinderMesh, coneMesh, liquidMesh, pourLiquidMesh, coneLiquidContentsMesh;
// State variables for the demonstration
let isDemonstrationMode = false;
let fillCount = 0;
// DOM elements for display and interaction
const radiusDisplay = document.getElementById('current-radius');
const heightDisplay = document.getElementById('current-height');
const volumeCylinderDisplay = document.getElementById('volume-cylinder');
const volumeConeDisplay = document.getElementById('volume-cone');
const radiusSlider = document.getElementById('radius-slider');
const heightSlider = document.getElementById('height-slider');
const demonstrateBtn = document.getElementById('demonstrate-btn');
const demonstrationStatus = document.getElementById('demonstration-status');
// Current dimensions
let R = 1.0;
let H = 2.0;
const spacing = 1.0; // Constants for shape separation
// Unified Liquid Color (Red/Pink)
const LIQUID_COLOR = 0xe94e77;
// --- Initialization ---
function init() {
const container = document.getElementById('scene-container');
// 1. Scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf7f7f7);
// 2. Camera (Aspect ratio will be corrected by onWindowResize)
camera = new THREE.PerspectiveCamera(75, 1, 0.1, 100);
camera.position.set(0, 3, 5);
// 3. Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
// Shadows disabled
renderer.shadowMap.enabled = false;
// Append the renderer's canvas to the container
container.appendChild(renderer.domElement);
// 4. Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // Soft white light
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7.5);
scene.add(directionalLight);
// 5. Ground Plane
const planeGeometry = new THREE.PlaneGeometry(10, 10);
const planeMaterial = new THREE.MeshLambertMaterial({ color: 0xcccccc, side: THREE.DoubleSide });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = Math.PI / 2;
scene.add(plane);
// CRITICAL FIX: Ensure the initial size is set correctly
onWindowResize();
// 6. Controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // smooth out camera movement
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 2;
controls.maxDistance = 10;
controls.target.set(0, H / 2, 0); // Focus controls on the center of the shapes
// 7. Initial Shapes
createShapes(R, H);
// 7.5 Pour Liquid Mesh (The moving stream)
const pourRadius = 0.1;
const pourLength = (R * 2 + spacing * 2);
// Initial geometry (will be updated dynamically)
const pourGeometry = new THREE.CylinderGeometry(pourRadius, pourRadius, pourLength, 16);
const pourMaterial = new THREE.MeshPhongMaterial({ color: LIQUID_COLOR, opacity: 0.9, transparent: true });
pourLiquidMesh = new THREE.Mesh(pourGeometry, pourMaterial);
pourLiquidMesh.rotation.z = Math.PI / 2; // Rotate to be horizontal
pourLiquidMesh.position.set(0, H, 0);
pourLiquidMesh.visible = false;
scene.add(pourLiquidMesh);
// 8. Event Listeners
setupEventListeners();
// 9. Initial calculation update
updateVolumes(R, H);
// 10. Start rendering loop
animate();
}
// --- Shape Creation and Update ---
function createShapes(radius, height) {
// Remove old shapes if they exist
if (cylinderMesh) scene.remove(cylinderMesh);
if (coneMesh) scene.remove(coneMesh);
if (liquidMesh) scene.remove(liquidMesh);
if (coneLiquidContentsMesh) scene.remove(coneLiquidContentsMesh);
const yOffset = height / 2; // Center the shapes on the ground plane (y=0)
const cylinderX = -radius - spacing;
const coneX = radius + spacing;
// Cylinder (Container - Blue)
const cylinderGeometry = new THREE.CylinderGeometry(radius, radius, height, 32);
const cylinderMaterial = new THREE.MeshStandardMaterial({
color: 0x4a90e2, // Blue
opacity: 0.7, // Default opacity
transparent: true,
side: THREE.DoubleSide
});
cylinderMesh = new THREE.Mesh(cylinderGeometry, cylinderMaterial);
cylinderMesh.position.set(cylinderX, yOffset, 0);
scene.add(cylinderMesh);
// Cone (Container/Outline - Red/Pink Wireframe)
const coneGeometry = new THREE.ConeGeometry(radius, height, 32);
// Use a highly transparent material to act as a container shell
const coneContainerMaterial = new THREE.MeshStandardMaterial({
color: LIQUID_COLOR,
opacity: 0.2,
transparent: true,
wireframe: true
});
coneMesh = new THREE.Mesh(coneGeometry, coneContainerMaterial);
coneMesh.position.set(coneX, yOffset, 0);
scene.add(coneMesh);
// Cone Liquid Mesh (Contents - starts full, Red/Pink)
const coneLiquidGeometry = new THREE.ConeGeometry(radius * 0.99, height, 32);
const coneLiquidMaterial = new THREE.MeshPhongMaterial({ color: LIQUID_COLOR, opacity: 0.9, transparent: true });
coneLiquidContentsMesh = new THREE.Mesh(coneLiquidGeometry, coneLiquidMaterial);
// Correct position for cone liquid (base should be at y=0)
coneLiquidContentsMesh.position.set(coneX, yOffset, 0);
coneLiquidContentsMesh.visible = false; // Initially hidden
scene.add(coneLiquidContentsMesh);
// Liquid Mesh (Cylinder Contents - starts empty, Red/Pink)
const liquidGeometry = new THREE.CylinderGeometry(radius * 0.99, radius * 0.99, 0.01, 32);
const liquidMaterial = new THREE.MeshPhongMaterial({ color: LIQUID_COLOR, opacity: 0.9, transparent: true });
liquidMesh = new THREE.Mesh(liquidGeometry, liquidMaterial);
// Correct position for cylinder liquid (y=0 is the bottom, so center y is height/2)
liquidMesh.position.set(cylinderX, 0.005, 0);
liquidMesh.visible = false; // Initially hidden
scene.add(liquidMesh);
// Reset pourLiquidMesh if it exists (for new R/H dimensions)
if (pourLiquidMesh) {
const pourLength = (radius * 2 + spacing * 2);
pourLiquidMesh.geometry.dispose();
pourLiquidMesh.geometry = new THREE.CylinderGeometry(0.1, 0.1, pourLength, 16);
pourLiquidMesh.rotation.z = Math.PI / 2; // Ensure it's horizontal
pourLiquidMesh.position.set(0, height, 0); // Position above the shapes
}
// Update the camera target
if (controls) {
controls.target.set(0, yOffset, 0);
controls.update();
}
}
// --- Demonstration Logic ---
function toggleDemonstrationMode() {
isDemonstrationMode = !isDemonstrationMode;
fillCount = 0;
if (isDemonstrationMode) {
// Enter Demonstration Mode
// 1. Make the cylinder container transparent
cylinderMesh.material.opacity = 0.25;
// 2. Show cone container and initial liquid inside (full)
coneMesh.material.wireframe = true; // Ensure cone outline is visible
coneLiquidContentsMesh.visible = true;
// Reset Cone Liquid to full height (scale=1)
coneLiquidContentsMesh.scale.set(1, 1, 1);
coneLiquidContentsMesh.position.y = H / 2;
// Reset Cylinder Liquid to empty (height 0.01)
liquidMesh.visible = true;
liquidMesh.geometry.dispose();
liquidMesh.geometry = new THREE.CylinderGeometry(R * 0.99, R * 0.99, 0.01, 32);
liquidMesh.position.y = 0.005;
demonstrateBtn.textContent = 'पहिलो पटक भर्नुहोस् (Pour 1st time)';
demonstrationStatus.textContent = 'प्रदर्शन मोडमा (Demonstration Mode)';
// Disable controls during demonstration
radiusSlider.disabled = true;
heightSlider.disabled = true;
} else {
// Exit Demonstration Mode
// 1. Restore container opacities
cylinderMesh.material.opacity = 0.7;
// 2. Hide demo meshes
coneLiquidContentsMesh.visible = false;
liquidMesh.visible = false;
pourLiquidMesh.visible = false;
demonstrateBtn.textContent = 'भर्ने प्रदर्शन सुरु गर्नुहोस् (Start Filling Demonstration)';
demonstrationStatus.textContent = 'तयार छ (Ready)';
// Re-enable controls
radiusSlider.disabled = false;
heightSlider.disabled = false;
// Ensure shapes are reset based on current slider values
updateShapesAndCalculations();
}
}
function pourConeContents() {
// If starting the demonstration, initiate it first
if (!isDemonstrationMode) {
toggleDemonstrationMode();
return;
}
fillCount++;
// If already filled (4th click), reset the mode
if (fillCount > 3) {
toggleDemonstrationMode();
return;
}
// --- Setup before Pouring ---
// Reset Cone to Full Before Pouring (for visual refilling)
coneLiquidContentsMesh.scale.set(1, 1, 1);
coneLiquidContentsMesh.position.y = H / 2;
coneLiquidContentsMesh.visible = true;
// Disable button during animation
demonstrateBtn.disabled = true;
// --- Cylinder Fill Targets ---
const targetCylinderHeight = (H / 3) * fillCount;
// The starting height needs to be read from the current geometry before animation starts
const startCylinderHeight = liquidMesh.geometry.parameters.height;
// --- Animation Parameters ---
const duration = 1800; // ms (longer animation for better visual)
const startTime = Date.now();
// Animation for Draining/Filling
const animateFill = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(1, elapsed / duration);
// --- 1. Fill Cylinder (LiquidMesh) ---
const currentCylinderHeight = startCylinderHeight + (targetCylinderHeight - startCylinderHeight) * progress;
const currentCylinderY = currentCylinderHeight / 2; // Center position
// Update geometry (this is computationally heavy, but necessary to resize the cylinder liquid)
liquidMesh.geometry.dispose();
liquidMesh.geometry = new THREE.CylinderGeometry(R * 0.99, R * 0.99, currentCylinderHeight, 32);
liquidMesh.position.y = currentCylinderY;
// --- 2. Drain Cone (coneLiquidContentsMesh) ---
const currentScaleFactor = 1.0 - progress; // Scales from 1 down to 0
// Scale cone liquid (shrinks from top down, simulating volume reduction)
coneLiquidContentsMesh.scale.set(currentScaleFactor, currentScaleFactor, currentScaleFactor);
// Adjust position to keep the base fixed at y=0 (or y=H/2 if we consider the base to be its center when full)
// Since the base is at y=0, the center position scales with the height/scale
coneLiquidContentsMesh.position.y = (H / 2) * currentScaleFactor;
// --- 3. Pouring Stream Visual (pourLiquidMesh) ---
const streamOpacity = progress < 0.1 || progress > 0.9 ? 0 : 1;
pourLiquidMesh.material.opacity = streamOpacity;
// Show the pouring stream only during the animation
pourLiquidMesh.visible = (progress > 0.05 && progress < 0.95);
if (progress < 1) {
requestAnimationFrame(animateFill);
} else {
// Animation finished
pourLiquidMesh.visible = false;
// Final visual state for Cone Liquid: MUST BE HIDDEN (fully empty)
coneLiquidContentsMesh.visible = false;
// Update Cylinder liquid to exact final position/height
liquidMesh.geometry.dispose();
liquidMesh.geometry = new THREE.CylinderGeometry(R * 0.99, R * 0.99, targetCylinderHeight, 32);
liquidMesh.position.y = targetCylinderHeight / 2;
// Update status text
// NOTE: Updated text to use plain 1/3 and 2/3 instead of unsupported LaTeX $...$
if (fillCount === 1) {
demonstrateBtn.textContent = 'दोस्रो पटक भर्नुहोस् (Pour 2nd time)';
demonstrationStatus.textContent = 'पहिलो पटक सोली खाली गरियो। 1/3 भाग भरियो। (1st cone emptied. 1/3 filled.)';
} else if (fillCount === 2) {
demonstrateBtn.textContent = 'तेस्रो पटक भर्नुहोस् (Pour 3rd time)';
demonstrationStatus.textContent = 'दोस्रो पटक सोली खाली गरियो। 2/3 भाग भरियो। (2nd cone emptied. 2/3 filled.)';
} else if (fillCount === 3) {
demonstrateBtn.textContent = 'पुन: सुरु गर्नुहोस् (Restart)';
demonstrationStatus.textContent = 'तेस्रो पटक सोली खाली गरियो। बेलना पूर्ण रूपमा भरियो! (3rd cone emptied. Cylinder is completely full!)';
}
demonstrateBtn.disabled = false;
}
};
animateFill();
}
function updateShapesAndCalculations() {
// Only allow changes if not in demonstration mode
if (isDemonstrationMode) return;
R = parseFloat(radiusSlider.value);
H = parseFloat(heightSlider.value);
// 1. Update 3D shapes
createShapes(R, H);
// 2. Update display values
radiusDisplay.textContent = R.toFixed(1);
heightDisplay.textContent = H.toFixed(1);
// 3. Update volume calculations
updateVolumes(R, H);
}
// --- Volume Calculation and Display ---
function calculateVolume(radius, height) {
const pi = Math.PI;
// V_cylinder = π * R² * H
const V_cylinder = pi * radius * radius * height;
// V_cone = (1/3) * π * R² * H
const V_cone = (1/3) * V_cylinder;
return { V_cylinder, V_cone };
}
function updateVolumes(radius, height) {
const { V_cylinder, V_cone } = calculateVolume(radius, height);
// Display results
volumeCylinderDisplay.textContent = V_cylinder.toFixed(2);
volumeConeDisplay.textContent = V_cone.toFixed(2);
}
// --- Event Listeners ---
function setupEventListeners() {
radiusSlider.addEventListener('input', updateShapesAndCalculations);
heightSlider.addEventListener('input', updateShapesAndCalculations);
demonstrateBtn.addEventListener('click', pourConeContents);
window.addEventListener('resize', onWindowResize, false);
}
// --- Resize Handling ---
function onWindowResize() {
if (!renderer) return; // Check if renderer is initialized
const container = document.getElementById('scene-container');
const width = container.clientWidth;
const height = container.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate);
if (controls) controls.update(); // only required if controls.enableDamping is set to true
if (renderer && scene && camera) renderer.render(scene, camera);
}
// Start the application when the window loads
window.onload = function () {
// Wait for one frame render to ensure container dimensions are finalized
requestAnimationFrame(() => {
// Initial read and setup
R = parseFloat(radiusSlider.value);
H = parseFloat(heightSlider.value);
init();
});
}