Improve: Web remote desktop two-finger gesture recognition

This commit is contained in:
yuanyuanxiang
2026-04-20 23:56:39 +02:00
parent ef4d316492
commit 80f95a41b2
3 changed files with 134 additions and 36 deletions

View File

@@ -1325,8 +1325,9 @@ inline std::string GetWebPageHTML() {
const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2;
// Convert canvas coords to position on unzoomed canvas
const relX = canvasX / canvas.width; // 0-1
const relY = canvasY / canvas.height; // 0-1
// Map [0, width-1] to [0, 1] for proper edge-to-edge display
const relX = canvas.width > 1 ? canvasX / (canvas.width - 1) : 0;
const relY = canvas.height > 1 ? canvasY / (canvas.height - 1) : 0;
// Position on unzoomed canvas (in pixels from canvas top-left)
const unzoomedX = relX * canvasDisplayWidth;
@@ -1349,6 +1350,52 @@ inline std::string GetWebPageHTML() {
cursorOverlay.style.top = screenY + 'px';
}
// Inverse transform: screen position -> canvas coordinates
// Used to recalculate cursor position after zoom/pan
function screenToCanvas(screenX, screenY) {
const canvas = document.getElementById('screen-canvas');
const container = document.querySelector('.canvas-container');
const containerRect = container.getBoundingClientRect();
const canvasDisplayWidth = canvas.offsetWidth;
const canvasDisplayHeight = canvas.offsetHeight;
const canvasLeft = containerRect.left + (containerRect.width - canvasDisplayWidth) / 2;
const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2;
// Reverse the transform chain
const scaledX = screenX - canvasLeft;
const scaledY = screenY - canvasTop;
const originX = canvasDisplayWidth * zoomState.originX / 100;
const originY = canvasDisplayHeight * zoomState.originY / 100;
// Reverse: scaledX = originX + (unzoomedX - originX) * scale + translateX * scale
const unzoomedX = (scaledX - originX) / zoomState.scale + originX - zoomState.translateX;
const unzoomedY = (scaledY - originY) / zoomState.scale + originY - zoomState.translateY;
// Reverse: unzoomedX = relX * canvasDisplayWidth
const relX = canvasDisplayWidth > 0 ? unzoomedX / canvasDisplayWidth : 0;
const relY = canvasDisplayHeight > 0 ? unzoomedY / canvasDisplayHeight : 0;
// Reverse: relX = canvasX / (canvas.width - 1)
const canvasX = relX * (canvas.width - 1);
const canvasY = relY * (canvas.height - 1);
// Clamp to valid range
return {
x: Math.max(0, Math.min(canvas.width - 1, canvasX)),
y: Math.max(0, Math.min(canvas.height - 1, canvasY))
};
}
)HTML";
// Part 14b: JavaScript - Zoom state and touch helpers
html += R"HTML(
// Two-finger gesture constants
const ZOOM_THRESHOLD = 0.05; // 5% distance change to trigger zoom
const SCROLL_SENSITIVITY = 3; // Scroll speed multiplier
const SCROLL_DEADZONE = 2; // Minimum scroll delta to send
// Pinch-to-zoom state
let zoomState = {
scale: 1,
@@ -1362,7 +1409,11 @@ inline std::string GetWebPageHTML() {
pinchCenterY: 0,
// Transform origin relative to canvas (percentage)
originX: 50,
originY: 50
originY: 50,
// Two-finger gesture detection
hasZoomed: false, // Whether zoom occurred in current gesture
lastScrollY: 0, // For scroll delta calculation
initialPinchDist: 0 // Distance at gesture start (for cumulative detection)
};
const zoomIndicator = document.getElementById('zoom-indicator');
let zoomIndicatorTimer = null;
@@ -1469,8 +1520,8 @@ inline std::string GetWebPageHTML() {
initCursor();
// Sensitivity multiplier (adjust for comfortable control)
const sensitivity = 1.5;
cursorState.x = Math.max(0, Math.min(canvas.width, cursorState.x + dx * sensitivity));
cursorState.y = Math.max(0, Math.min(canvas.height, cursorState.y + dy * sensitivity));
cursorState.x = Math.max(0, Math.min(canvas.width - 1, cursorState.x + dx * sensitivity));
cursorState.y = Math.max(0, Math.min(canvas.height - 1, cursorState.y + dy * sensitivity));
updateCursorOverlay(cursorState.x, cursorState.y);
// Send move to remote
sendMouse('move', Math.round(cursorState.x), Math.round(cursorState.y), 0);
@@ -1538,12 +1589,16 @@ inline std::string GetWebPageHTML() {
touchState.touchCount = e.touches.length;
if (e.touches.length === 2) {
// Two finger touch - start pinch zoom
// Two finger touch - could be pinch zoom or scroll
zoomState.isPinching = true;
zoomState.lastPinchDist = getPinchDistance(e.touches);
const initialDist = getPinchDistance(e.touches);
zoomState.initialPinchDist = initialDist; // For cumulative change detection
zoomState.lastPinchDist = initialDist; // For frame-by-frame zoom calculation
zoomState.hasZoomed = false; // Track if zoom occurred during this gesture
const center = getPinchCenter(e.touches);
zoomState.pinchCenterX = center.x;
zoomState.pinchCenterY = center.y;
zoomState.lastScrollY = center.y; // For scroll delta calculation
// Calculate pinch center relative to canvas for transform-origin
// Only set origin when starting a new zoom (scale == 1)
@@ -1627,35 +1682,63 @@ inline std::string GetWebPageHTML() {
e.stopPropagation(); // Prevent exiting fullscreen
if (e.touches.length === 2 && zoomState.isPinching) {
// Two finger move - pinch zoom AND pan simultaneously
// Two finger move - zoom+pan or scroll
const newDist = getPinchDistance(e.touches);
const newCenter = getPinchCenter(e.touches);
const frameDelta = newDist / zoomState.lastPinchDist; // Frame-by-frame change
const totalDelta = newDist / zoomState.initialPinchDist; // Cumulative change from gesture start
// Calculate zoom
const delta = newDist / zoomState.lastPinchDist;
const newScale = Math.max(zoomState.minScale, Math.min(zoomState.maxScale, zoomState.scale * delta));
// Detect gesture type: zoom vs scroll
// Use CUMULATIVE change to detect zoom intent (catches slow pinch gestures)
// Also treat as zoom if already at scale boundary and trying to zoom further
const atMinScale = zoomState.scale <= zoomState.minScale;
const atMaxScale = zoomState.scale >= zoomState.maxScale;
const tryingToShrink = totalDelta < 1; // Use cumulative for direction
const tryingToEnlarge = totalDelta > 1;
// Calculate pan (movement of pinch center)
if (zoomState.scale > 1 || newScale > 1) {
if (Math.abs(totalDelta - 1) > ZOOM_THRESHOLD ||
(atMinScale && tryingToShrink) ||
(atMaxScale && tryingToEnlarge)) {
zoomState.hasZoomed = true;
}
if (zoomState.hasZoomed) {
// Zoom + pan mode (once zoomed in this gesture, stay in this mode)
const newScale = Math.max(zoomState.minScale, Math.min(zoomState.maxScale, zoomState.scale * frameDelta));
const dx = newCenter.x - zoomState.pinchCenterX;
const dy = newCenter.y - zoomState.pinchCenterY;
zoomState.translateX += dx / zoomState.scale;
zoomState.translateY += dy / zoomState.scale;
}
// Update state
zoomState.pinchCenterX = newCenter.x;
zoomState.pinchCenterY = newCenter.y;
zoomState.lastPinchDist = newDist;
// Pan when zoomed or zooming
if (zoomState.scale > 1 || newScale > 1) {
zoomState.translateX += dx / zoomState.scale;
zoomState.translateY += dy / zoomState.scale;
}
if (newScale !== zoomState.scale) {
zoomState.scale = newScale;
showZoomIndicator();
}
applyZoomTransform();
// Update cursor overlay to follow canvas transform
if (cursorState.initialized) {
updateCursorOverlay(cursorState.x, cursorState.y);
// Update state
zoomState.pinchCenterX = newCenter.x;
zoomState.pinchCenterY = newCenter.y;
zoomState.lastPinchDist = newDist;
if (newScale !== zoomState.scale) {
zoomState.scale = newScale;
showZoomIndicator();
}
applyZoomTransform();
// Note: cursor overlay stays fixed on screen during zoom (Microsoft RD style)
// cursorState will be recalculated on touchend via screenToCanvas()
} else {
// Scroll mode (no zoom occurred yet)
const scrollDelta = newCenter.y - zoomState.lastScrollY;
if (Math.abs(scrollDelta) > SCROLL_DEADZONE) {
initCursor();
// Send wheel event at cursor position
sendMouse('wheel', Math.round(cursorState.x), Math.round(cursorState.y), 0, -scrollDelta * SCROLL_SENSITIVITY);
zoomState.lastScrollY = newCenter.y;
}
// Always update state to prevent jump if user starts zooming
zoomState.pinchCenterX = newCenter.x;
zoomState.pinchCenterY = newCenter.y;
zoomState.lastPinchDist = newDist;
}
return;
}
@@ -1740,6 +1823,17 @@ inline std::string GetWebPageHTML() {
// Handle pinch end
if (zoomState.isPinching) {
if (e.touches.length < 2) {
// Recalculate cursor position after zoom/pan (Microsoft RD style)
// Cursor stayed fixed on screen, so reverse-calculate its new canvas coords
if (cursorState.initialized && zoomState.hasZoomed) {
const cursorOverlay = document.getElementById('cursor-overlay');
const cursorScreenX = parseFloat(cursorOverlay.style.left) || 0;
const cursorScreenY = parseFloat(cursorOverlay.style.top) || 0;
const newCoords = screenToCanvas(cursorScreenX, cursorScreenY);
cursorState.x = newCoords.x;
cursorState.y = newCoords.y;
}
zoomState.isPinching = false;
// Update pan center and lastX/lastY for smooth transition
if (e.touches.length === 1) {