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

@@ -61,7 +61,7 @@ elseif ($msBuild -match "\\18\\") { $vsYear = "2019 Insiders" }
Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan
$rootDir = $PSScriptRoot $rootDir = $PSScriptRoot
$slnFile = Join-Path $rootDir "2019Remote.sln" $slnFile = Join-Path $rootDir "YAMA.sln"
$upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe" $upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe"
# Publish mode overrides # Publish mode overrides

View File

@@ -1325,8 +1325,9 @@ inline std::string GetWebPageHTML() {
const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2; const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2;
// Convert canvas coords to position on unzoomed canvas // Convert canvas coords to position on unzoomed canvas
const relX = canvasX / canvas.width; // 0-1 // Map [0, width-1] to [0, 1] for proper edge-to-edge display
const relY = canvasY / canvas.height; // 0-1 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) // Position on unzoomed canvas (in pixels from canvas top-left)
const unzoomedX = relX * canvasDisplayWidth; const unzoomedX = relX * canvasDisplayWidth;
@@ -1349,6 +1350,52 @@ inline std::string GetWebPageHTML() {
cursorOverlay.style.top = screenY + 'px'; 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 // Pinch-to-zoom state
let zoomState = { let zoomState = {
scale: 1, scale: 1,
@@ -1362,7 +1409,11 @@ inline std::string GetWebPageHTML() {
pinchCenterY: 0, pinchCenterY: 0,
// Transform origin relative to canvas (percentage) // Transform origin relative to canvas (percentage)
originX: 50, 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'); const zoomIndicator = document.getElementById('zoom-indicator');
let zoomIndicatorTimer = null; let zoomIndicatorTimer = null;
@@ -1469,8 +1520,8 @@ inline std::string GetWebPageHTML() {
initCursor(); initCursor();
// Sensitivity multiplier (adjust for comfortable control) // Sensitivity multiplier (adjust for comfortable control)
const sensitivity = 1.5; const sensitivity = 1.5;
cursorState.x = Math.max(0, Math.min(canvas.width, cursorState.x + dx * sensitivity)); cursorState.x = Math.max(0, Math.min(canvas.width - 1, cursorState.x + dx * sensitivity));
cursorState.y = Math.max(0, Math.min(canvas.height, cursorState.y + dy * sensitivity)); cursorState.y = Math.max(0, Math.min(canvas.height - 1, cursorState.y + dy * sensitivity));
updateCursorOverlay(cursorState.x, cursorState.y); updateCursorOverlay(cursorState.x, cursorState.y);
// Send move to remote // Send move to remote
sendMouse('move', Math.round(cursorState.x), Math.round(cursorState.y), 0); sendMouse('move', Math.round(cursorState.x), Math.round(cursorState.y), 0);
@@ -1538,12 +1589,16 @@ inline std::string GetWebPageHTML() {
touchState.touchCount = e.touches.length; touchState.touchCount = e.touches.length;
if (e.touches.length === 2) { if (e.touches.length === 2) {
// Two finger touch - start pinch zoom // Two finger touch - could be pinch zoom or scroll
zoomState.isPinching = true; 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); const center = getPinchCenter(e.touches);
zoomState.pinchCenterX = center.x; zoomState.pinchCenterX = center.x;
zoomState.pinchCenterY = center.y; zoomState.pinchCenterY = center.y;
zoomState.lastScrollY = center.y; // For scroll delta calculation
// Calculate pinch center relative to canvas for transform-origin // Calculate pinch center relative to canvas for transform-origin
// Only set origin when starting a new zoom (scale == 1) // Only set origin when starting a new zoom (scale == 1)
@@ -1627,18 +1682,34 @@ inline std::string GetWebPageHTML() {
e.stopPropagation(); // Prevent exiting fullscreen e.stopPropagation(); // Prevent exiting fullscreen
if (e.touches.length === 2 && zoomState.isPinching) { 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 newDist = getPinchDistance(e.touches);
const newCenter = getPinchCenter(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 // Detect gesture type: zoom vs scroll
const delta = newDist / zoomState.lastPinchDist; // Use CUMULATIVE change to detect zoom intent (catches slow pinch gestures)
const newScale = Math.max(zoomState.minScale, Math.min(zoomState.maxScale, zoomState.scale * delta)); // 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 (Math.abs(totalDelta - 1) > ZOOM_THRESHOLD ||
if (zoomState.scale > 1 || newScale > 1) { (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 dx = newCenter.x - zoomState.pinchCenterX;
const dy = newCenter.y - zoomState.pinchCenterY; const dy = newCenter.y - zoomState.pinchCenterY;
// Pan when zoomed or zooming
if (zoomState.scale > 1 || newScale > 1) {
zoomState.translateX += dx / zoomState.scale; zoomState.translateX += dx / zoomState.scale;
zoomState.translateY += dy / zoomState.scale; zoomState.translateY += dy / zoomState.scale;
} }
@@ -1653,9 +1724,21 @@ inline std::string GetWebPageHTML() {
showZoomIndicator(); showZoomIndicator();
} }
applyZoomTransform(); applyZoomTransform();
// Update cursor overlay to follow canvas transform // Note: cursor overlay stays fixed on screen during zoom (Microsoft RD style)
if (cursorState.initialized) { // cursorState will be recalculated on touchend via screenToCanvas()
updateCursorOverlay(cursorState.x, cursorState.y); } 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; return;
} }
@@ -1740,6 +1823,17 @@ inline std::string GetWebPageHTML() {
// Handle pinch end // Handle pinch end
if (zoomState.isPinching) { if (zoomState.isPinching) {
if (e.touches.length < 2) { 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; zoomState.isPinching = false;
// Update pan center and lastX/lastY for smooth transition // Update pan center and lastX/lastY for smooth transition
if (e.touches.length === 1) { if (e.touches.length === 1) {

View File

@@ -13,6 +13,7 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <cstring> #include <cstring>
#include <ctime> #include <ctime>
#include <cstdlib>
#include <map> #include <map>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
@@ -88,16 +89,19 @@ private:
int daysBetweenDates(const tm& date1, const tm& date2) int daysBetweenDates(const tm& date1, const tm& date2)
{ {
auto timeToTimePoint = [](const tm& tmTime) { // Use Julian Day Number to avoid DST issues
std::time_t tt = mktime(const_cast<tm*>(&tmTime)); // Formula: https://en.wikipedia.org/wiki/Julian_day
return std::chrono::system_clock::from_time_t(tt); auto toJulianDay = [](int year, int month, int day) {
int a = (14 - month) / 12;
int y = year + 4800 - a;
int m = month + 12 * a - 3;
return day + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32045;
}; };
auto tp1 = timeToTimePoint(date1); int jd1 = toJulianDay(date1.tm_year + 1900, date1.tm_mon + 1, date1.tm_mday);
auto tp2 = timeToTimePoint(date2); int jd2 = toJulianDay(date2.tm_year + 1900, date2.tm_mon + 1, date2.tm_mday);
auto duration = tp1 > tp2 ? tp1 - tp2 : tp2 - tp1; return std::abs(jd1 - jd2);
return static_cast<int>(std::chrono::duration_cast<std::chrono::hours>(duration).count() / 24);
} }
tm getCurrentDate() tm getCurrentDate()