Skip to content

Commit c314edd

Browse files
Max Carlsonclaude
andcommitted
feat: add trackpad and mouse wheel support for gesture zoom
Add comprehensive input support for camera zoom gestures: - Trackpad pinch gestures (macOS gesturestart/change/end events) - Mouse wheel zoom (wheel event) - Touch pinch gestures (existing, now with gesture state tracking) All zoom inputs now use consistent Z-axis camera movement approach, matching the behavior of the playground zoom buttons. This provides smooth spring-damped animation across all input methods. Enhanced pinch-to-zoom example with real-time debug panel showing: - Camera event counters (camera-changed, zoom-changed, gesture-changed) - Gesture state (active, type, touch count) - FPS and camera position/zoom values Fixes gesture support on desktop devices with trackpads and mouse wheels. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent a720791 commit c314edd

File tree

4 files changed

+218
-19
lines changed

4 files changed

+218
-19
lines changed

WebSites/spacecraft-viewer/examples/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ <h1>SpaceCraft Playground</h1>
277277
"category": "Interaction",
278278
"order": 21,
279279
"description": "Touch gestures for camera control (pinch zoom, two-finger pan)",
280-
"code": "const { Viewer } = SpaceCraftViewer;\n\nconst canvas = document.getElementById('canvas');\n\n// Gestures enabled by default!\n// (The Viewer automatically sets canvas.style.touchAction = 'none')\nconst viewer = new Viewer(canvas);\n\n// Sample data\nconst items = Array.from({ length: 24 }, (_, i) => ({\n id: `item-${i}`,\n title: `Item ${i + 1}`,\n coverUrl: `https://picsum.photos/seed/gesture${i}/200/300`\n}));\n\nviewer\n .data(items)\n .into('items', {\n layout: 'grid',\n columns: 6,\n spacing: 2.5\n });\n\n// Listen to camera changes\nviewer.on('camera-changed', (camera) => {\n console.log('Zoom:', camera.zoom.toFixed(2));\n console.log('Position:', camera.position);\n});\n\nconsole.log('Try pinch-to-zoom and two-finger pan on a touch device!');\n\nviewer.start();",
281-
"docs": "<h1>Pinch to Zoom</h1>\n<p>This example demonstrates touch gesture controls for camera manipulation. The GestureController provides intuitive multitouch interactions for mobile and tablet devices.</p>\n<h2>Supported Gestures</h2>\n<ul>\n<li><strong>Pinch to Zoom</strong> - Spread two fingers apart to zoom in, pinch together to zoom out</li>\n<li><strong>Two-Finger Pan</strong> - Drag with two fingers to pan the camera</li>\n<li><strong>Works with item interactions</strong> - Gestures and pointer interactions work simultaneously</li>\n</ul>\n<h2>Features</h2>\n<ul>\n<li><strong>Automatic enablement</strong> - Gestures enabled by default on touch devices</li>\n<li><strong>Configurable speeds</strong> - Adjust zoom and pan sensitivity</li>\n<li><strong>Smooth zoom clamping</strong> - Prevents extreme zoom levels (0.1x to 10x)</li>\n<li><strong>No conflicts</strong> - Works alongside single-finger item selection</li>\n</ul>\n<h2>Custom Configuration</h2>\n<p>Adjust gesture sensitivity and behavior:</p>\n<pre><code class=\"language-js\">const viewer = new Viewer(canvas, {\n gestures: {\n pinchZoom: true,\n twoFingerPan: true,\n zoomSpeed: 0.02, // Faster zoom (default: 0.01)\n panSpeed: 0.1 // Faster pan (default: 0.05)\n }\n});\n</code></pre>\n<h2>Disable Specific Gestures</h2>\n<p>Enable only the gestures you want:</p>\n<pre><code class=\"language-js\">// Only pinch zoom, no panning\nconst viewer = new Viewer(canvas, {\n gestures: {\n pinchZoom: true,\n twoFingerPan: false\n }\n});\n\n// Only panning, no zoom\nconst viewer = new Viewer(canvas, {\n gestures: {\n pinchZoom: false,\n twoFingerPan: true\n }\n});\n\n// Disable all gestures\nconst viewer = new Viewer(canvas, {\n gestures: false\n});\n</code></pre>\n<h2>How It Works</h2>\n<p>The GestureController:</p>\n<ol>\n<li><strong>Tracks touch points</strong> - Monitors all active touches on the canvas</li>\n<li><strong>Detects two-finger gestures</strong> - Activates when exactly 2 touches are present</li>\n<li><strong>Calculates distance</strong> - Measures separation between touch points</li>\n<li><strong>Updates camera state</strong> - Modifies ViewerState camera zoom and position</li>\n<li><strong>Resets on release</strong> - Clears gesture state when fingers lift</li>\n</ol>\n<h2>Zoom Limits</h2>\n<p>Zoom is automatically clamped to prevent extreme values:</p>\n<ul>\n<li><strong>Minimum zoom:</strong> 0.1x (far away)</li>\n<li><strong>Maximum zoom:</strong> 10x (close up)</li>\n<li><strong>Default zoom:</strong> 1.0x</li>\n</ul>\n<p>This ensures the scene remains visible and performant at all zoom levels.</p>\n<h2>Combining with Other Interactions</h2>\n<p>Gestures work alongside all other interaction modes:</p>\n<pre><code class=\"language-js\">const viewer = new Viewer(canvas, {\n interaction: {\n hover: true, // Single-finger hover highlights\n click: true // Single-finger click selection\n },\n gestures: {\n pinchZoom: true, // Two-finger zoom\n twoFingerPan: true // Two-finger pan\n }\n});\n\n// Single finger: hover and click items\n// Two fingers: zoom and pan camera\n</code></pre>\n<h2>Mobile-First Design</h2>\n<p>The gesture system is designed for mobile exhibitions and tablets:</p>\n<ul>\n<li><strong>No button UI needed</strong> - Natural touch gestures</li>\n<li><strong>Exhibition-ready</strong> - Kiosk mode compatible</li>\n<li><strong>Performance optimized</strong> - Efficient touch tracking</li>\n<li><strong>Works with gloves</strong> - Large touch targets via items</li>\n</ul>\n<h2>Testing on Desktop</h2>\n<p>To test gestures on a desktop computer:</p>\n<ol>\n<li>Open Chrome DevTools (F12)</li>\n<li>Click the device toolbar icon (Ctrl+Shift+M)</li>\n<li>Select a touch device (e.g., &quot;iPad&quot;)</li>\n<li>Use your mouse to simulate multi-touch:<ul>\n<li>Hold Shift + drag to simulate two-finger gesture</li>\n<li>Scroll wheel still works for zoom</li>\n</ul>\n</li>\n</ol>\n<h2>See Also</h2>\n<ul>\n<li><strong>19-multitouch.md</strong> - Multi-pointer item selection</li>\n<li><strong>08-multi-device.md</strong> - Remote controller setup</li>\n<li><strong>20-remote-pointer.md</strong> - Touch input from mobile controllers</li>\n</ul>"
280+
"code": "const { Viewer } = SpaceCraftViewer;\n\nconst canvas = document.getElementById('canvas');\n\n// Gestures enabled by default!\n// (The Viewer automatically sets canvas.style.touchAction = 'none')\nconst viewer = new Viewer(canvas);\n\n// Sample data\nconst items = Array.from({ length: 24 }, (_, i) => ({\n id: `item-${i}`,\n title: `Item ${i + 1}`,\n coverUrl: `https://picsum.photos/seed/gesture${i}/200/300`\n}));\n\nviewer\n .data(items)\n .into('items', {\n layout: 'grid',\n columns: 6,\n spacing: 2.5\n });\n\n// Debug output element\nconst output = document.createElement('div');\noutput.style.cssText = 'position:fixed;top:10px;left:10px;background:rgba(0,0,0,0.8);color:#0f0;padding:10px;font-family:monospace;font-size:12px;z-index:1000;max-width:300px;pointer-events:none';\ndocument.body.appendChild(output);\n\nlet cameraEvents = 0;\nlet zoomEvents = 0;\nlet gestureEvents = 0;\nlet lastUpdate = Date.now();\nlet lastZoom = 1.0;\nlet currentGesture = { active: false, type: null, touchCount: 0 };\n\n// Function to update debug display\nfunction updateDebug(camera, fps = 0) {\n const gestureLabel = currentGesture.active\n ? `${currentGesture.type?.toUpperCase() || 'NONE'}`\n : 'INACTIVE';\n\n output.innerHTML = `\n <strong>GESTURE DEBUG</strong><br>\n Camera Events: ${cameraEvents}<br>\n Zoom Events: ${zoomEvents}<br>\n Gesture Events: ${gestureEvents}<br>\n FPS: ${fps}<br>\n <br>\n <strong>Gesture: ${gestureLabel}</strong><br>\n Touches: ${currentGesture.touchCount}<br>\n Active: ${currentGesture.active ? 'YES' : 'NO'}<br>\n <br>\n Zoom: ${camera.zoom.toFixed(3)}<br>\n Position:<br>\n &nbsp;&nbsp;x: ${camera.position.x.toFixed(1)}<br>\n &nbsp;&nbsp;y: ${camera.position.y.toFixed(1)}<br>\n &nbsp;&nbsp;z: ${camera.position.z.toFixed(1)}\n `;\n}\n\n// Show initial state immediately\nupdateDebug({ zoom: 1.0, position: { x: 0, y: 0, z: 50 } });\n\n// Listen to camera changes from gestures\nviewer.on('camera-changed', (camera) => {\n cameraEvents++;\n const now = Date.now();\n const fps = Math.round(1000 / (now - lastUpdate));\n lastUpdate = now;\n\n updateDebug(camera, fps);\n\n console.log('Camera:', {\n zoom: camera.zoom.toFixed(3),\n position: camera.position,\n fps\n });\n});\n\n// Listen to zoom-specific changes\nviewer.on('zoom-changed', (zoom) => {\n zoomEvents++;\n const delta = zoom - lastZoom;\n lastZoom = zoom;\n\n console.log('Zoom changed:', {\n zoom: zoom.toFixed(3),\n delta: delta.toFixed(3),\n direction: delta > 0 ? 'IN ↗' : 'OUT ↙'\n });\n});\n\n// Listen to gesture state changes\nviewer.on('gesture-changed', (gestureState) => {\n gestureEvents++;\n currentGesture = gestureState;\n\n console.log('Gesture:', {\n type: gestureState.type,\n active: gestureState.active,\n touches: gestureState.touchCount,\n distance: gestureState.pinchDistance?.toFixed(1)\n });\n});\n\nconsole.log('Try pinch-to-zoom and two-finger pan on a touch device!');\nconsole.log('Watch the green debug panel for real-time gesture feedback.');\n\nviewer.start();",
281+
"docs": "<h1>Pinch to Zoom</h1>\n<p>This example demonstrates touch gesture controls for camera manipulation. The GestureController provides intuitive multitouch interactions for mobile and tablet devices.</p>\n<h2>Supported Gestures</h2>\n<ul>\n<li><strong>Pinch to Zoom</strong> - Spread two fingers apart to zoom in, pinch together to zoom out</li>\n<li><strong>Two-Finger Pan</strong> - Drag with two fingers to pan the camera</li>\n<li><strong>Works with item interactions</strong> - Gestures and pointer interactions work simultaneously</li>\n</ul>\n<h2>Features</h2>\n<ul>\n<li><strong>Automatic enablement</strong> - Gestures enabled by default on touch devices</li>\n<li><strong>Configurable speeds</strong> - Adjust zoom and pan sensitivity</li>\n<li><strong>Smooth zoom clamping</strong> - Prevents extreme zoom levels (0.1x to 10x)</li>\n<li><strong>No conflicts</strong> - Works alongside single-finger item selection</li>\n</ul>\n<h2>Debug Output</h2>\n<p>The example includes a real-time debug panel showing:</p>\n<ul>\n<li><strong>Events</strong> - Total number of camera-changed events</li>\n<li><strong>FPS</strong> - Gesture update frequency (frames per second)</li>\n<li><strong>Zoom</strong> - Current zoom level (0.1 to 10.0)</li>\n<li><strong>Position</strong> - Camera position in 3D space</li>\n</ul>\n<p>This helps verify gestures are working correctly and diagnose any issues across different browsers and devices.</p>\n<h2>Custom Configuration</h2>\n<p>Adjust gesture sensitivity and behavior:</p>\n<pre><code class=\"language-js\">const viewer = new Viewer(canvas, {\n gestures: {\n pinchZoom: true,\n twoFingerPan: true,\n zoomSpeed: 0.02, // Faster zoom (default: 0.01)\n panSpeed: 0.1 // Faster pan (default: 0.05)\n }\n});\n</code></pre>\n<h2>Disable Specific Gestures</h2>\n<p>Enable only the gestures you want:</p>\n<pre><code class=\"language-js\">// Only pinch zoom, no panning\nconst viewer = new Viewer(canvas, {\n gestures: {\n pinchZoom: true,\n twoFingerPan: false\n }\n});\n\n// Only panning, no zoom\nconst viewer = new Viewer(canvas, {\n gestures: {\n pinchZoom: false,\n twoFingerPan: true\n }\n});\n\n// Disable all gestures\nconst viewer = new Viewer(canvas, {\n gestures: false\n});\n</code></pre>\n<h2>How It Works</h2>\n<p>The GestureController:</p>\n<ol>\n<li><strong>Tracks touch points</strong> - Monitors all active touches on the canvas</li>\n<li><strong>Detects two-finger gestures</strong> - Activates when exactly 2 touches are present</li>\n<li><strong>Calculates distance</strong> - Measures separation between touch points</li>\n<li><strong>Updates camera state</strong> - Modifies ViewerState camera zoom and position</li>\n<li><strong>Resets on release</strong> - Clears gesture state when fingers lift</li>\n</ol>\n<h2>Zoom Limits</h2>\n<p>Zoom is automatically clamped to prevent extreme values:</p>\n<ul>\n<li><strong>Minimum zoom:</strong> 0.1x (far away)</li>\n<li><strong>Maximum zoom:</strong> 10x (close up)</li>\n<li><strong>Default zoom:</strong> 1.0x</li>\n</ul>\n<p>This ensures the scene remains visible and performant at all zoom levels.</p>\n<h2>Combining with Other Interactions</h2>\n<p>Gestures work alongside all other interaction modes:</p>\n<pre><code class=\"language-js\">const viewer = new Viewer(canvas, {\n interaction: {\n hover: true, // Single-finger hover highlights\n click: true // Single-finger click selection\n },\n gestures: {\n pinchZoom: true, // Two-finger zoom\n twoFingerPan: true // Two-finger pan\n }\n});\n\n// Single finger: hover and click items\n// Two fingers: zoom and pan camera\n</code></pre>\n<h2>Mobile-First Design</h2>\n<p>The gesture system is designed for mobile exhibitions and tablets:</p>\n<ul>\n<li><strong>No button UI needed</strong> - Natural touch gestures</li>\n<li><strong>Exhibition-ready</strong> - Kiosk mode compatible</li>\n<li><strong>Performance optimized</strong> - Efficient touch tracking</li>\n<li><strong>Works with gloves</strong> - Large touch targets via items</li>\n</ul>\n<h2>Testing on Desktop</h2>\n<p>To test gestures on a desktop computer:</p>\n<ol>\n<li>Open Chrome DevTools (F12)</li>\n<li>Click the device toolbar icon (Ctrl+Shift+M)</li>\n<li>Select a touch device (e.g., &quot;iPad&quot;)</li>\n<li>Use your mouse to simulate multi-touch:<ul>\n<li>Hold Shift + drag to simulate two-finger gesture</li>\n<li>Scroll wheel still works for zoom</li>\n</ul>\n</li>\n</ol>\n<h2>See Also</h2>\n<ul>\n<li><strong>19-multitouch.md</strong> - Multi-pointer item selection</li>\n<li><strong>08-multi-device.md</strong> - Remote controller setup</li>\n<li><strong>20-remote-pointer.md</strong> - Touch input from mobile controllers</li>\n</ul>"
282282
}
283283
]
284284
};

WebSites/spacecraft-viewer/examples/snippets/21-pinch-to-zoom.md

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,104 @@ viewer
4646
spacing: 2.5
4747
});
4848

49-
// Listen to camera changes
49+
// Debug output element
50+
const output = document.createElement('div');
51+
output.style.cssText = 'position:fixed;top:10px;left:10px;background:rgba(0,0,0,0.8);color:#0f0;padding:10px;font-family:monospace;font-size:12px;z-index:1000;max-width:300px;pointer-events:none';
52+
document.body.appendChild(output);
53+
54+
let cameraEvents = 0;
55+
let zoomEvents = 0;
56+
let gestureEvents = 0;
57+
let lastUpdate = Date.now();
58+
let lastZoom = 1.0;
59+
let currentGesture = { active: false, type: null, touchCount: 0 };
60+
61+
// Function to update debug display
62+
function updateDebug(camera, fps = 0) {
63+
const gestureLabel = currentGesture.active
64+
? `${currentGesture.type?.toUpperCase() || 'NONE'}`
65+
: 'INACTIVE';
66+
67+
output.innerHTML = `
68+
<strong>GESTURE DEBUG</strong><br>
69+
Camera Events: ${cameraEvents}<br>
70+
Zoom Events: ${zoomEvents}<br>
71+
Gesture Events: ${gestureEvents}<br>
72+
FPS: ${fps}<br>
73+
<br>
74+
<strong>Gesture: ${gestureLabel}</strong><br>
75+
Touches: ${currentGesture.touchCount}<br>
76+
Active: ${currentGesture.active ? 'YES' : 'NO'}<br>
77+
<br>
78+
Zoom: ${camera.zoom.toFixed(3)}<br>
79+
Position:<br>
80+
&nbsp;&nbsp;x: ${camera.position.x.toFixed(1)}<br>
81+
&nbsp;&nbsp;y: ${camera.position.y.toFixed(1)}<br>
82+
&nbsp;&nbsp;z: ${camera.position.z.toFixed(1)}
83+
`;
84+
}
85+
86+
// Show initial state immediately
87+
updateDebug({ zoom: 1.0, position: { x: 0, y: 0, z: 50 } });
88+
89+
// Listen to camera changes from gestures
5090
viewer.on('camera-changed', (camera) => {
51-
console.log('Zoom:', camera.zoom.toFixed(2));
52-
console.log('Position:', camera.position);
91+
cameraEvents++;
92+
const now = Date.now();
93+
const fps = Math.round(1000 / (now - lastUpdate));
94+
lastUpdate = now;
95+
96+
updateDebug(camera, fps);
97+
98+
console.log('Camera:', {
99+
zoom: camera.zoom.toFixed(3),
100+
position: camera.position,
101+
fps
102+
});
103+
});
104+
105+
// Listen to zoom-specific changes
106+
viewer.on('zoom-changed', (zoom) => {
107+
zoomEvents++;
108+
const delta = zoom - lastZoom;
109+
lastZoom = zoom;
110+
111+
console.log('Zoom changed:', {
112+
zoom: zoom.toFixed(3),
113+
delta: delta.toFixed(3),
114+
direction: delta > 0 ? 'IN ↗' : 'OUT ↙'
115+
});
116+
});
117+
118+
// Listen to gesture state changes
119+
viewer.on('gesture-changed', (gestureState) => {
120+
gestureEvents++;
121+
currentGesture = gestureState;
122+
123+
console.log('Gesture:', {
124+
type: gestureState.type,
125+
active: gestureState.active,
126+
touches: gestureState.touchCount,
127+
distance: gestureState.pinchDistance?.toFixed(1)
128+
});
53129
});
54130
55131
console.log('Try pinch-to-zoom and two-finger pan on a touch device!');
132+
console.log('Watch the green debug panel for real-time gesture feedback.');
56133
57134
viewer.start();
58135
```
59136
137+
## Debug Output
138+
139+
The example includes a real-time debug panel showing:
140+
- **Events** - Total number of camera-changed events
141+
- **FPS** - Gesture update frequency (frames per second)
142+
- **Zoom** - Current zoom level (0.1 to 10.0)
143+
- **Position** - Camera position in 3D space
144+
145+
This helps verify gestures are working correctly and diagnose any issues across different browsers and devices.
146+
60147
## Custom Configuration
61148
62149
Adjust gesture sensitivity and behavior:

0 commit comments

Comments
 (0)