From 2d660e05a0a6a8e9e7eaa02c7f08fc6192d25420 Mon Sep 17 00:00:00 2001 From: Mark Kalsbeek Date: Wed, 21 Jan 2026 20:42:56 +0100 Subject: [PATCH] fix: pause animation loop while not rendering --- ui/src/ShadingSandbox.vue | 71 +++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/ui/src/ShadingSandbox.vue b/ui/src/ShadingSandbox.vue index f9e7b06..f934149 100644 --- a/ui/src/ShadingSandbox.vue +++ b/ui/src/ShadingSandbox.vue @@ -192,6 +192,7 @@ let mesh = null; let directionalLight = null; let ambientLight = null; let animationId = null; +let animationPaused = false; // Geometry cache for height exaggeration let geometryCache = null; @@ -233,8 +234,19 @@ const initThreeJS = () => { updateLightPosition(); - // Animation loop + // Don't start animation loop yet - will be controlled by pause/resume +}; + +// Animation loop control +const startAnimation = () => { + if (animationId) return; // Already running + const animate = () => { + if (animationPaused) { + animationId = null; + return; + } + animationId = requestAnimationFrame(animate); if (renderer && scene && camera) { renderer.render(scene, camera); @@ -243,6 +255,26 @@ const initThreeJS = () => { animate(); }; +const pauseAnimation = () => { + animationPaused = true; + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } +}; + +const resumeAnimation = () => { + animationPaused = false; + startAnimation(); +}; + +// Render a single frame (for when animation is paused but we need to update the view) +const renderSingleFrame = () => { + if (renderer && scene && camera) { + renderer.render(scene, camera); + } +}; + // Handle canvas resize const handleResize = () => { if (!canvasRef.value || !renderer || !camera) return; @@ -422,11 +454,13 @@ const updateHeightScale = () => { geometryCache.geometry.attributes.position.needsUpdate = true; geometryCache.geometry.computeVertexNormals(); + renderSingleFrame(); // Render once to show the change }; // Update scene (for real-time preview) const updateScene = () => { updateLightPosition(); + renderSingleFrame(); // Render once to show the change }; // Color conversion utilities @@ -438,6 +472,7 @@ const updateColor = (e) => { settings.terrainColor = parseInt(e.target.value.replace('#', ''), 16); if (mesh && mesh.material) { mesh.material.color.setHex(settings.terrainColor); + renderSingleFrame(); // Render once to show the change } }; @@ -634,9 +669,7 @@ const downloadLastRender = () => { // Cleanup const cleanup = () => { - if (animationId) { - cancelAnimationFrame(animationId); - } + pauseAnimation(); if (resizeObserver) { resizeObserver.disconnect(); resizeObserver = null; @@ -663,6 +696,11 @@ onMounted(() => { }); resizeObserver.observe(canvasRef.value); } + + // Only start animation if visible and not offscreen + if (!props.offscreen) { + resumeAnimation(); + } }); } }); @@ -680,7 +718,29 @@ watch(() => props.visible, (newVal) => { }); resizeObserver.observe(canvasRef.value); } + + // Start animation when visible and not offscreen + if (!props.offscreen) { + resumeAnimation(); + } }); + } else if (newVal && !props.offscreen) { + // Component already initialized, just resume animation + resumeAnimation(); + } else if (!newVal) { + // Hidden - pause animation + pauseAnimation(); + } +}); + +// Watch for offscreen changes +watch(() => props.offscreen, (newVal) => { + if (newVal) { + // Offscreen mode - pause animation (we only render single frames on demand) + pauseAnimation(); + } else if (props.visible) { + // Not offscreen and visible - resume animation + resumeAnimation(); } }); @@ -721,8 +781,7 @@ defineExpose({ } .modal-container.offscreen { - left: -10000px; - top: -10000px; + display: none; } .modal-header {