mirror of https://github.com/node-red/node-red.git
Implement smooth zoom functionality with pinch-to-zoom support
- Add smooth zoom animation with 125ms duration and easing curves - Implement space+scroll zoom mode alongside existing Alt+scroll - Fix pinch-to-zoom with proper ratio-based scaling and fixed focal point - Add gesture state management for consistent zoom behavior - Enhance spacebar handling to prevent scroll artifacts - Fix zoom button layout (correct zoom in/out direction) - Add zoom animation utilities (view-zoom-animator.js) - Add zoom configuration constants (view-zoom-constants.js) - Fix scale lock issues with improved tolerance handling - Update Gruntfile to include new zoom modules in build Features implemented: - Smooth animated zoom transitions (125ms with ease-out) - Space+scroll for zoom mode - Fixed focal point during pinch gestures - No scroll artifacts when pressing space - Proper state management when cursor leaves canvas - Natural acceleration/deceleration curves Known issue: Trackpad pinch-to-zoom needs additional work on macOSpull/5312/head
parent
bd51b0c153
commit
eaf68815fd
|
|
@ -173,6 +173,8 @@ module.exports = function(grunt) {
|
|||
"packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/statusBar.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-constants.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-animator.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view-navigator.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js",
|
||||
|
|
|
|||
223
packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-animator.js
vendored
Normal file
223
packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-animator.js
vendored
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
/**
|
||||
* Copyright JS Foundation and other contributors, http://js.foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
RED.view.zoomAnimator = (function() {
|
||||
|
||||
/**
|
||||
* Easing function for smooth deceleration
|
||||
* Creates natural-feeling animation curves
|
||||
* @param {number} t - Progress from 0 to 1
|
||||
* @returns {number} - Eased value from 0 to 1
|
||||
*/
|
||||
function easeOut(t) {
|
||||
// Cubic ease-out for smooth deceleration
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate values using requestAnimationFrame with easing
|
||||
* Based on Excalidraw's implementation for smooth zoom transitions
|
||||
*
|
||||
* @param {Object} options - Animation options
|
||||
* @param {Object} options.fromValues - Starting values object
|
||||
* @param {Object} options.toValues - Target values object
|
||||
* @param {Function} options.onStep - Callback for each animation frame
|
||||
* @param {number} [options.duration=250] - Animation duration in ms
|
||||
* @param {Function} [options.interpolateValue] - Custom interpolation function
|
||||
* @param {Function} [options.onStart] - Animation start callback
|
||||
* @param {Function} [options.onEnd] - Animation end callback
|
||||
* @param {Function} [options.onCancel] - Animation cancel callback
|
||||
* @returns {Function} - Cancel function to stop animation
|
||||
*/
|
||||
function easeToValuesRAF(options) {
|
||||
const {
|
||||
fromValues,
|
||||
toValues,
|
||||
onStep,
|
||||
duration = 250,
|
||||
interpolateValue,
|
||||
onStart,
|
||||
onEnd,
|
||||
onCancel
|
||||
} = options;
|
||||
|
||||
let startTime = null;
|
||||
let animationId = null;
|
||||
let cancelled = false;
|
||||
|
||||
function step(timestamp) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startTime) {
|
||||
startTime = timestamp;
|
||||
if (onStart) {
|
||||
onStart();
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = timestamp - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const easedProgress = easeOut(progress);
|
||||
|
||||
const interpolatedValues = {};
|
||||
|
||||
for (const key in fromValues) {
|
||||
const from = fromValues[key];
|
||||
const to = toValues[key];
|
||||
|
||||
if (interpolateValue && key === 'zoom') {
|
||||
// Special interpolation for zoom to feel more natural
|
||||
// Exponential interpolation preserves relative zoom feel
|
||||
interpolatedValues[key] = from * Math.pow(to / from, easedProgress);
|
||||
} else {
|
||||
// Linear interpolation for other values
|
||||
interpolatedValues[key] = from + (to - from) * easedProgress;
|
||||
}
|
||||
}
|
||||
|
||||
onStep(interpolatedValues);
|
||||
|
||||
if (progress < 1) {
|
||||
animationId = requestAnimationFrame(step);
|
||||
} else {
|
||||
if (onEnd) {
|
||||
onEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(step);
|
||||
|
||||
// Return cancel function
|
||||
return function cancel() {
|
||||
cancelled = true;
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate smooth zoom delta with acceleration
|
||||
* Provides consistent zoom speed regardless of input device
|
||||
*
|
||||
* @param {number} currentScale - Current zoom scale
|
||||
* @param {number} delta - Input delta (wheel, gesture, etc)
|
||||
* @param {boolean} isTrackpad - Whether input is from trackpad
|
||||
* @returns {number} - Calculated zoom delta
|
||||
*/
|
||||
function calculateZoomDelta(currentScale, delta, isTrackpad) {
|
||||
// Normalize delta across different input devices
|
||||
let normalizedDelta = delta;
|
||||
|
||||
if (isTrackpad) {
|
||||
// Trackpad deltas are typically smaller and more frequent
|
||||
normalizedDelta = delta * 0.01;
|
||||
} else {
|
||||
// Mouse wheel deltas are larger and less frequent
|
||||
normalizedDelta = delta > 0 ? 0.1 : -0.1;
|
||||
}
|
||||
|
||||
// Apply acceleration based on current zoom level
|
||||
// Zoom faster when zoomed out, slower when zoomed in
|
||||
const acceleration = Math.max(0.5, Math.min(2, 1 / currentScale));
|
||||
|
||||
return normalizedDelta * acceleration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gesture state management for consistent focal points
|
||||
*/
|
||||
const gestureState = {
|
||||
active: false,
|
||||
initialFocalPoint: null,
|
||||
initialScale: 1,
|
||||
currentScale: 1,
|
||||
lastDistance: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Start a zoom gesture with fixed focal point
|
||||
* @param {Array} focalPoint - [x, y] coordinates of focal point
|
||||
* @param {number} scale - Initial scale value
|
||||
*/
|
||||
function startGesture(focalPoint, scale) {
|
||||
gestureState.active = true;
|
||||
gestureState.initialFocalPoint = focalPoint ? [...focalPoint] : null;
|
||||
gestureState.initialScale = scale;
|
||||
gestureState.currentScale = scale;
|
||||
return gestureState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update gesture maintaining fixed focal point
|
||||
* @param {number} newScale - New scale value
|
||||
* @returns {Object} - Gesture state with fixed focal point
|
||||
*/
|
||||
function updateGesture(newScale) {
|
||||
if (!gestureState.active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
gestureState.currentScale = newScale;
|
||||
|
||||
return {
|
||||
scale: newScale,
|
||||
focalPoint: gestureState.initialFocalPoint,
|
||||
active: gestureState.active
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* End the current gesture
|
||||
*/
|
||||
function endGesture() {
|
||||
gestureState.active = false;
|
||||
gestureState.initialFocalPoint = null;
|
||||
gestureState.lastDistance = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a gesture is currently active
|
||||
*/
|
||||
function isGestureActive() {
|
||||
return gestureState.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fixed focal point for the current gesture
|
||||
*/
|
||||
function getGestureFocalPoint() {
|
||||
return gestureState.initialFocalPoint;
|
||||
}
|
||||
|
||||
return {
|
||||
easeOut: easeOut,
|
||||
easeToValuesRAF: easeToValuesRAF,
|
||||
calculateZoomDelta: calculateZoomDelta,
|
||||
gestureState: gestureState,
|
||||
startGesture: startGesture,
|
||||
updateGesture: updateGesture,
|
||||
endGesture: endGesture,
|
||||
isGestureActive: isGestureActive,
|
||||
getGestureFocalPoint: getGestureFocalPoint
|
||||
};
|
||||
})();
|
||||
21
packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-constants.js
vendored
Normal file
21
packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-constants.js
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Zoom configuration constants
|
||||
*/
|
||||
RED.view.zoomConstants = {
|
||||
// Zoom limits
|
||||
MIN_ZOOM: 0.3,
|
||||
MAX_ZOOM: 2.0,
|
||||
|
||||
// Zoom step for keyboard/button controls
|
||||
ZOOM_STEP: 0.1,
|
||||
|
||||
// Animation settings
|
||||
DEFAULT_ZOOM_DURATION: 125, // ms, faster animation
|
||||
|
||||
// Gesture thresholds
|
||||
PINCH_THRESHOLD: 10, // minimum pixel movement to trigger zoom
|
||||
|
||||
// Momentum and friction for smooth scrolling
|
||||
FRICTION: 0.92,
|
||||
BOUNCE_DAMPING: 0.6
|
||||
};
|
||||
|
|
@ -38,11 +38,14 @@ RED.view = (function() {
|
|||
node_height = 30,
|
||||
dblClickInterval = 650;
|
||||
|
||||
var cancelInProgressAnimation = null; // For smooth zoom animation
|
||||
|
||||
var touchLongPressTimeout = 1000,
|
||||
startTouchDistance = 0,
|
||||
startTouchCenter = [],
|
||||
moveTouchCenter = [],
|
||||
touchStartTime = 0;
|
||||
touchStartTime = 0,
|
||||
gesture = {};
|
||||
|
||||
var spacebarPressed = false;
|
||||
|
||||
|
|
@ -395,11 +398,15 @@ RED.view = (function() {
|
|||
if (RED.touch.radialMenu.active()) {
|
||||
return;
|
||||
}
|
||||
// End gesture when touches end
|
||||
RED.view.zoomAnimator.endGesture();
|
||||
canvasMouseUp.call(this);
|
||||
})
|
||||
.on("touchcancel", function() {
|
||||
if (RED.view.DEBUG) { console.warn("eventLayer.touchcancel", mouse_mode); }
|
||||
d3.event.preventDefault();
|
||||
// End gesture when touches are cancelled
|
||||
RED.view.zoomAnimator.endGesture();
|
||||
canvasMouseUp.call(this);
|
||||
})
|
||||
.on("touchstart", function() {
|
||||
|
|
@ -425,6 +432,19 @@ RED.view = (function() {
|
|||
touch1["pageY"]+(a/2)
|
||||
]
|
||||
startTouchDistance = Math.sqrt((a*a)+(b*b));
|
||||
|
||||
// Store initial scale for ratio-based zoom calculation
|
||||
gesture = {
|
||||
initialScale: scaleFactor,
|
||||
initialDistance: startTouchDistance
|
||||
};
|
||||
|
||||
// Start gesture with fixed focal point (screen coordinates)
|
||||
var focalPoint = [
|
||||
(touch0["pageX"] + touch1["pageX"]) / 2 - offset.left,
|
||||
(touch0["pageY"] + touch1["pageY"]) / 2 - offset.top
|
||||
];
|
||||
RED.view.zoomAnimator.startGesture(focalPoint, scaleFactor);
|
||||
} else {
|
||||
var obj = d3.select(document.body);
|
||||
touch0 = d3.event.touches.item(0);
|
||||
|
|
@ -474,26 +494,33 @@ RED.view = (function() {
|
|||
var offset = chart.offset();
|
||||
var scrollPos = [chart.scrollLeft(),chart.scrollTop()];
|
||||
var moveTouchDistance = Math.sqrt((a*a)+(b*b));
|
||||
var touchCenter = [
|
||||
touch1["pageX"]+(b/2),
|
||||
touch1["pageY"]+(a/2)
|
||||
];
|
||||
|
||||
if (!isNaN(moveTouchDistance)) {
|
||||
oldScaleFactor = scaleFactor;
|
||||
var newScaleFactor = Math.min(2,Math.max(0.3, scaleFactor + (Math.floor(((moveTouchDistance*100)-(startTouchDistance*100)))/10000)));
|
||||
// Use smooth ratio-based scaling for natural pinch-to-zoom
|
||||
var zoomRatio = moveTouchDistance / startTouchDistance;
|
||||
var newScaleFactor = Math.min(2, Math.max(0.3, gesture.initialScale * zoomRatio));
|
||||
|
||||
// Calculate pinch center relative to chart
|
||||
var pinchCenter = [
|
||||
touchCenter[0] - offset.left,
|
||||
touchCenter[1] - offset.top
|
||||
];
|
||||
// Use gesture state management to maintain fixed focal point
|
||||
var gestureState = RED.view.zoomAnimator.updateGesture(newScaleFactor);
|
||||
|
||||
if (gestureState && gestureState.focalPoint) {
|
||||
// Use the fixed focal point from gesture start
|
||||
zoomView(newScaleFactor, gestureState.focalPoint);
|
||||
} else {
|
||||
// Fallback to current behavior if gesture not active
|
||||
var touchCenter = [
|
||||
touch1["pageX"]+(b/2),
|
||||
touch1["pageY"]+(a/2)
|
||||
];
|
||||
var pinchCenter = [
|
||||
touchCenter[0] - offset.left,
|
||||
touchCenter[1] - offset.top
|
||||
];
|
||||
zoomView(newScaleFactor, pinchCenter);
|
||||
}
|
||||
|
||||
// Use zoomView with pinch center as focal point
|
||||
zoomView(newScaleFactor, pinchCenter);
|
||||
|
||||
startTouchDistance = moveTouchDistance;
|
||||
moveTouchCenter = touchCenter;
|
||||
// Don't update startTouchDistance - keep initial distance for ratio calculation
|
||||
}
|
||||
}
|
||||
d3.event.preventDefault();
|
||||
|
|
@ -531,14 +558,35 @@ RED.view = (function() {
|
|||
spacebarPressed = true;
|
||||
// Prevent default space scrolling behavior
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else if (e.type === "keyup" && spacebarPressed) {
|
||||
spacebarPressed = false;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Window-level keyup listener to catch spacebar release when cursor is outside canvas
|
||||
function handleWindowSpacebarUp(e) {
|
||||
if ((e.keyCode === 32 || e.key === ' ') && spacebarPressed) {
|
||||
spacebarPressed = false;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
document.addEventListener("keyup", handleSpacebarToggle)
|
||||
document.addEventListener("keydown", handleSpacebarToggle)
|
||||
// Additional window-level keyup listener to ensure spacebar state is cleared
|
||||
// when cursor leaves canvas area while spacebar is held
|
||||
window.addEventListener("keyup", handleWindowSpacebarUp)
|
||||
|
||||
// Reset spacebar state when window loses focus to prevent stuck state
|
||||
window.addEventListener("blur", function() {
|
||||
if (spacebarPressed) {
|
||||
spacebarPressed = false;
|
||||
}
|
||||
})
|
||||
|
||||
// Workspace Background
|
||||
eventLayer.append("svg:rect")
|
||||
|
|
@ -633,11 +681,11 @@ RED.view = (function() {
|
|||
'</span>')
|
||||
})
|
||||
|
||||
$("#red-ui-view-zoom-out").on("click", zoomOut);
|
||||
$("#red-ui-view-zoom-out").on("click", zoomIn);
|
||||
RED.popover.tooltip($("#red-ui-view-zoom-out"),RED._('actions.zoom-out'),'core:zoom-out');
|
||||
$("#red-ui-view-zoom-zero").on("click", zoomZero);
|
||||
RED.popover.tooltip($("#red-ui-view-zoom-zero"),RED._('actions.zoom-reset'),'core:zoom-reset');
|
||||
$("#red-ui-view-zoom-in").on("click", zoomIn);
|
||||
$("#red-ui-view-zoom-in").on("click", zoomOut);
|
||||
RED.popover.tooltip($("#red-ui-view-zoom-in"),RED._('actions.zoom-in'),'core:zoom-in');
|
||||
chart.on("DOMMouseScroll mousewheel", function (evt) {
|
||||
if ( evt.altKey || spacebarPressed ) {
|
||||
|
|
@ -856,6 +904,10 @@ RED.view = (function() {
|
|||
});
|
||||
chart.on("blur", function() {
|
||||
$("#red-ui-workspace-tabs").removeClass("red-ui-workspace-focussed");
|
||||
// Reset spacebar state when chart loses focus to prevent stuck state
|
||||
if (spacebarPressed) {
|
||||
spacebarPressed = false;
|
||||
}
|
||||
});
|
||||
|
||||
RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection);
|
||||
|
|
@ -2208,6 +2260,11 @@ RED.view = (function() {
|
|||
canvasMouseUp.call(this)
|
||||
})
|
||||
}
|
||||
// Reset spacebar state when mouse leaves canvas to prevent interaction artifacts
|
||||
if (spacebarPressed) {
|
||||
// Note: We don't reset spacebarPressed here as user may still be holding spacebar
|
||||
// The window-level keyup listener will handle the actual release
|
||||
}
|
||||
}
|
||||
function canvasMouseUp() {
|
||||
lastClickPosition = [d3.event.offsetX/scaleFactor,d3.event.offsetY/scaleFactor];
|
||||
|
|
@ -2553,16 +2610,16 @@ RED.view = (function() {
|
|||
}
|
||||
|
||||
function zoomIn(focalPoint) {
|
||||
if (scaleFactor < 2) {
|
||||
zoomView(scaleFactor+0.1, focalPoint);
|
||||
if (scaleFactor < RED.view.zoomConstants.MAX_ZOOM) {
|
||||
animatedZoomView(scaleFactor + RED.view.zoomConstants.ZOOM_STEP, focalPoint);
|
||||
}
|
||||
}
|
||||
function zoomOut(focalPoint) {
|
||||
if (scaleFactor > 0.3) {
|
||||
zoomView(scaleFactor-0.1, focalPoint);
|
||||
if (scaleFactor > RED.view.zoomConstants.MIN_ZOOM) {
|
||||
animatedZoomView(scaleFactor - RED.view.zoomConstants.ZOOM_STEP, focalPoint);
|
||||
}
|
||||
}
|
||||
function zoomZero() { zoomView(1); }
|
||||
function zoomZero() { animatedZoomView(1); }
|
||||
function searchFlows() { RED.actions.invoke("core:search", $(this).data("term")); }
|
||||
function searchPrev() { RED.actions.invoke("core:search-previous"); }
|
||||
function searchNext() { RED.actions.invoke("core:search-next"); }
|
||||
|
|
@ -2605,6 +2662,91 @@ RED.view = (function() {
|
|||
}
|
||||
}
|
||||
|
||||
function animatedZoomView(targetFactor, focalPoint) {
|
||||
// Cancel any in-progress animation
|
||||
if (cancelInProgressAnimation) {
|
||||
cancelInProgressAnimation();
|
||||
cancelInProgressAnimation = null;
|
||||
}
|
||||
|
||||
// Clamp target factor to valid range
|
||||
targetFactor = Math.max(RED.view.zoomConstants.MIN_ZOOM,
|
||||
Math.min(RED.view.zoomConstants.MAX_ZOOM, targetFactor));
|
||||
|
||||
// If we're already at the target, no need to animate
|
||||
// Use a more tolerant threshold to account for floating-point precision
|
||||
if (Math.abs(scaleFactor - targetFactor) < 0.01) {
|
||||
return;
|
||||
}
|
||||
|
||||
var startFactor = scaleFactor;
|
||||
var screenSize = [chart.width(), chart.height()];
|
||||
var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
|
||||
|
||||
// Calculate the focal point in workspace coordinates (will remain constant)
|
||||
var center;
|
||||
if (focalPoint) {
|
||||
// focalPoint is in screen coordinates, convert to workspace coordinates
|
||||
center = [(scrollPos[0] + focalPoint[0])/scaleFactor, (scrollPos[1] + focalPoint[1])/scaleFactor];
|
||||
} else {
|
||||
// Default to viewport center
|
||||
center = [(scrollPos[0] + screenSize[0]/2)/scaleFactor, (scrollPos[1] + screenSize[1]/2)/scaleFactor];
|
||||
}
|
||||
|
||||
// Start the animation
|
||||
cancelInProgressAnimation = RED.view.zoomAnimator.easeToValuesRAF({
|
||||
fromValues: {
|
||||
zoom: startFactor
|
||||
},
|
||||
toValues: {
|
||||
zoom: targetFactor
|
||||
},
|
||||
duration: RED.view.zoomConstants.DEFAULT_ZOOM_DURATION,
|
||||
interpolateValue: true, // Use exponential interpolation for zoom
|
||||
onStep: function(values) {
|
||||
var currentFactor = values.zoom;
|
||||
scaleFactor = currentFactor;
|
||||
|
||||
// Calculate new scroll position to maintain focal point
|
||||
var currentScreenSize = [chart.width(), chart.height()];
|
||||
var newScrollPos;
|
||||
|
||||
if (focalPoint) {
|
||||
// Keep the focal point at the same screen position
|
||||
newScrollPos = [
|
||||
center[0] * scaleFactor - focalPoint[0],
|
||||
center[1] * scaleFactor - focalPoint[1]
|
||||
];
|
||||
} else {
|
||||
// Keep viewport center steady
|
||||
newScrollPos = [
|
||||
center[0] * scaleFactor - currentScreenSize[0]/2,
|
||||
center[1] * scaleFactor - currentScreenSize[1]/2
|
||||
];
|
||||
}
|
||||
|
||||
chart.scrollLeft(newScrollPos[0]);
|
||||
chart.scrollTop(newScrollPos[1]);
|
||||
|
||||
RED.view.navigator.resize();
|
||||
redraw();
|
||||
},
|
||||
onEnd: function() {
|
||||
cancelInProgressAnimation = null;
|
||||
// Ensure scaleFactor is exactly the target to prevent precision issues
|
||||
scaleFactor = targetFactor;
|
||||
if (RED.settings.get("editor.view.view-store-zoom")) {
|
||||
RED.settings.setLocal('zoom-level', targetFactor.toFixed(1));
|
||||
}
|
||||
},
|
||||
onCancel: function() {
|
||||
cancelInProgressAnimation = null;
|
||||
// Ensure scaleFactor is set to current target on cancel
|
||||
scaleFactor = targetFactor;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Momentum scrolling functions
|
||||
function startMomentumScroll() {
|
||||
if (scrollAnimationId) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue