Improve: Web remote desktop two-finger gesture recognition
This commit is contained in:
@@ -61,7 +61,7 @@ elseif ($msBuild -match "\\18\\") { $vsYear = "2019 Insiders" }
|
||||
Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan
|
||||
|
||||
$rootDir = $PSScriptRoot
|
||||
$slnFile = Join-Path $rootDir "2019Remote.sln"
|
||||
$slnFile = Join-Path $rootDir "YAMA.sln"
|
||||
$upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe"
|
||||
|
||||
# Publish mode overrides
|
||||
|
||||
@@ -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,18 +1682,34 @@ 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;
|
||||
|
||||
// Pan when zoomed or zooming
|
||||
if (zoomState.scale > 1 || newScale > 1) {
|
||||
zoomState.translateX += dx / zoomState.scale;
|
||||
zoomState.translateY += dy / zoomState.scale;
|
||||
}
|
||||
@@ -1653,9 +1724,21 @@ inline std::string GetWebPageHTML() {
|
||||
showZoomIndicator();
|
||||
}
|
||||
applyZoomTransform();
|
||||
// Update cursor overlay to follow canvas transform
|
||||
if (cursorState.initialized) {
|
||||
updateCursorOverlay(cursorState.x, cursorState.y);
|
||||
// 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) {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <cstdlib>
|
||||
#include <map>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
@@ -88,16 +89,19 @@ private:
|
||||
|
||||
int daysBetweenDates(const tm& date1, const tm& date2)
|
||||
{
|
||||
auto timeToTimePoint = [](const tm& tmTime) {
|
||||
std::time_t tt = mktime(const_cast<tm*>(&tmTime));
|
||||
return std::chrono::system_clock::from_time_t(tt);
|
||||
// Use Julian Day Number to avoid DST issues
|
||||
// Formula: https://en.wikipedia.org/wiki/Julian_day
|
||||
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);
|
||||
auto tp2 = timeToTimePoint(date2);
|
||||
int jd1 = toJulianDay(date1.tm_year + 1900, date1.tm_mon + 1, date1.tm_mday);
|
||||
int jd2 = toJulianDay(date2.tm_year + 1900, date2.tm_mon + 1, date2.tm_mday);
|
||||
|
||||
auto duration = tp1 > tp2 ? tp1 - tp2 : tp2 - tp1;
|
||||
return static_cast<int>(std::chrono::duration_cast<std::chrono::hours>(duration).count() / 24);
|
||||
return std::abs(jd1 - jd2);
|
||||
}
|
||||
|
||||
tm getCurrentDate()
|
||||
|
||||
Reference in New Issue
Block a user