From 40e7e1068dbd9233415687117e6febcf9cbfc3f6 Mon Sep 17 00:00:00 2001 From: Mel Ludowise Date: Mon, 16 Mar 2026 22:59:05 -0700 Subject: [PATCH 1/2] Fix touch gesture conflicts in WebGL example apps on mobile --- examples/map-server/mcp-app.html | 1 + examples/map-server/src/mcp-app.ts | 11 +++++++++++ examples/shadertoy-server/src/mcp-app.css | 3 +++ examples/threejs-server/src/threejs-app.tsx | 16 +++++++++++++++- examples/wiki-explorer-server/src/mcp-app.css | 1 + examples/wiki-explorer-server/src/mcp-app.ts | 14 ++++++++++++++ 6 files changed, 45 insertions(+), 1 deletion(-) diff --git a/examples/map-server/mcp-app.html b/examples/map-server/mcp-app.html index 473eb50f6..c7919bc88 100644 --- a/examples/map-server/mcp-app.html +++ b/examples/map-server/mcp-app.html @@ -19,6 +19,7 @@ #cesiumContainer { width: 100%; height: 100%; + touch-action: none; } #fullscreen-btn { position: absolute; diff --git a/examples/map-server/src/mcp-app.ts b/examples/map-server/src/mcp-app.ts index 1475d81e7..f70cdb5fe 100644 --- a/examples/map-server/src/mcp-app.ts +++ b/examples/map-server/src/mcp-app.ts @@ -397,6 +397,17 @@ async function initCesium(): Promise { // CesiumJS sets image-rendering: pixelated by default which looks bad on scaled displays // Setting to "auto" allows the browser to apply smooth interpolation cesiumViewer.canvas.style.imageRendering = "auto"; + // Prevent touch events from propagating to the parent scroll view. + // CesiumJS uses pointer events internally, which don't suppress native + // scroll gesture recognition on touch devices. Explicit non-passive touch + // listeners with preventDefault() are needed. + for (const eventName of ["touchstart", "touchmove"] as const) { + cesiumViewer.canvas.addEventListener( + eventName, + (e: TouchEvent) => e.preventDefault(), + { passive: false }, + ); + } // Note: DO NOT set resolutionScale = devicePixelRatio here! // When useBrowserRecommendedResolution: false, Cesium already uses devicePixelRatio. // Setting resolutionScale = devicePixelRatio would double the scaling (e.g., 2x2=4x on Retina) diff --git a/examples/shadertoy-server/src/mcp-app.css b/examples/shadertoy-server/src/mcp-app.css index 0449ea5ea..33fb308c3 100644 --- a/examples/shadertoy-server/src/mcp-app.css +++ b/examples/shadertoy-server/src/mcp-app.css @@ -19,12 +19,15 @@ html, body { border-radius: 0; } + #canvas { width: 100%; height: 100%; display: block; + touch-action: none; } + #canvas.hidden { display: none; } diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index 235931641..bf122166c 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -85,6 +85,7 @@ function LoadingShimmer({ height, code }: { height: number; code?: string }) { display: "flex", flexDirection: "column", overflow: "hidden", + touchAction: "none", background: "linear-gradient(135deg, var(--color-background-secondary, light-dark(#f0f0f5, #2a2a3c)) 0%, var(--color-background-tertiary, light-dark(#e5e5ed, #1e1e2e)) 100%)", }} @@ -275,6 +276,14 @@ export default function ThreeJSApp({ useEffect(() => { if (!code || !canvasRef.current || !containerRef.current) return; + // Prevent touch events from propagating to the parent scroll view. + // Three.js OrbitControls uses pointer events, which don't suppress native + // scroll gesture recognition on touch devices. + const canvas = canvasRef.current; + const preventDefault = (e: TouchEvent) => e.preventDefault(); + canvas.addEventListener("touchstart", preventDefault, { passive: false }); + canvas.addEventListener("touchmove", preventDefault, { passive: false }); + // Cleanup previous animation animControllerRef.current?.cleanup(); animControllerRef.current = createAnimationController(); @@ -289,7 +298,11 @@ export default function ThreeJSApp({ animControllerRef.current.visibilityAwareRAF, ).catch((e) => setError(e instanceof Error ? e.message : "Unknown error")); - return () => animControllerRef.current?.cleanup(); + return () => { + canvas.removeEventListener("touchstart", preventDefault); + canvas.removeEventListener("touchmove", preventDefault); + animControllerRef.current?.cleanup(); + }; }, [code, height]); if (isStreaming || !code) { @@ -314,6 +327,7 @@ export default function ThreeJSApp({ height, borderRadius: "var(--border-radius-lg, 8px)", display: "block", + touchAction: "none", }} /> {error &&
Error: {error}
} diff --git a/examples/wiki-explorer-server/src/mcp-app.css b/examples/wiki-explorer-server/src/mcp-app.css index 8f6f0246e..ea13ac2ac 100644 --- a/examples/wiki-explorer-server/src/mcp-app.css +++ b/examples/wiki-explorer-server/src/mcp-app.css @@ -1,6 +1,7 @@ #graph { width: 100%; height: 100vh; + touch-action: none; } #popup { diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 7d95cb8d6..d8382ba6e 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -111,6 +111,20 @@ const graph = new ForceGraph(container) }) .graphData(graphData); +// Prevent touch events from propagating to the parent scroll view. +// force-graph uses pointer events, which don't suppress native scroll gesture +// recognition on touch devices. +const graphCanvas = container.querySelector("canvas"); +if (graphCanvas) { + for (const eventName of ["touchstart", "touchmove"] as const) { + graphCanvas.addEventListener( + eventName, + (e) => e.preventDefault(), + { passive: false }, + ); + } +} + // Handle window resize function handleResize() { const { width, height } = container.getBoundingClientRect(); From 37a1c3e80ed41f5b69cd8fc40c089a208c3329a7 Mon Sep 17 00:00:00 2001 From: Mel Ludowise Date: Mon, 16 Mar 2026 23:05:34 -0700 Subject: [PATCH 2/2] Format with prettier --- examples/map-server/mcp-app.html | 132 ++++++++++--------- examples/shadertoy-server/src/mcp-app.css | 9 +- examples/wiki-explorer-server/src/mcp-app.ts | 8 +- 3 files changed, 79 insertions(+), 70 deletions(-) diff --git a/examples/map-server/mcp-app.html b/examples/map-server/mcp-app.html index c7919bc88..acb9f384f 100644 --- a/examples/map-server/mcp-app.html +++ b/examples/map-server/mcp-app.html @@ -1,73 +1,83 @@ - + - - - + + + CesiumJS Globe - - + +
Loading globe...
- + diff --git a/examples/shadertoy-server/src/mcp-app.css b/examples/shadertoy-server/src/mcp-app.css index 33fb308c3..0d5b6f114 100644 --- a/examples/shadertoy-server/src/mcp-app.css +++ b/examples/shadertoy-server/src/mcp-app.css @@ -1,4 +1,5 @@ -html, body { +html, +body { margin: 0; width: 100%; height: 100%; @@ -19,7 +20,6 @@ html, body { border-radius: 0; } - #canvas { width: 100%; height: 100%; @@ -27,7 +27,6 @@ html, body { touch-action: none; } - #canvas.hidden { display: none; } @@ -71,7 +70,9 @@ html, body { display: none; /* Hidden by default, shown when fullscreen available */ align-items: center; justify-content: center; - transition: background 0.2s, opacity 0.2s; + transition: + background 0.2s, + opacity 0.2s; opacity: 0; /* Initially invisible, shown on View hover */ z-index: 100; } diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index d8382ba6e..69bec05d4 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -117,11 +117,9 @@ const graph = new ForceGraph(container) const graphCanvas = container.querySelector("canvas"); if (graphCanvas) { for (const eventName of ["touchstart", "touchmove"] as const) { - graphCanvas.addEventListener( - eventName, - (e) => e.preventDefault(), - { passive: false }, - ); + graphCanvas.addEventListener(eventName, (e) => e.preventDefault(), { + passive: false, + }); } }