Skip to content

Commit ad75a35

Browse files
committed
Fix issue with slight jump to the left and righ
1 parent eaa98b6 commit ad75a35

6 files changed

Lines changed: 79 additions & 62 deletions

File tree

packages/plugin-viewport/src/lib/reducer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const initialViewportDocumentState: ViewportDocumentState = {
2626
clientHeight: 0,
2727
scrollWidth: 0,
2828
scrollHeight: 0,
29+
clientLeft: 0,
30+
clientTop: 0,
2931
relativePosition: { x: 0, y: 0 },
3032
},
3133
isScrolling: false,
@@ -142,6 +144,8 @@ export const viewportReducer: Reducer<ViewportState, ViewportAction> = (
142144
clientHeight: metrics.clientHeight,
143145
scrollWidth: metrics.scrollWidth,
144146
scrollHeight: metrics.scrollHeight,
147+
clientLeft: metrics.clientLeft,
148+
clientTop: metrics.clientTop,
145149
relativePosition: {
146150
x:
147151
metrics.scrollWidth <= metrics.clientWidth

packages/plugin-viewport/src/lib/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export interface ViewportInputMetrics {
3131
clientHeight: number;
3232
scrollWidth: number;
3333
scrollHeight: number;
34+
clientLeft: number;
35+
clientTop: number;
3436
}
3537

3638
export interface ViewportMetrics extends ViewportInputMetrics {

packages/plugin-viewport/src/shared/hooks/use-viewport-ref.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export function useViewportRef(documentId: string) {
5050
scrollLeft: container.scrollLeft,
5151
scrollWidth: container.scrollWidth,
5252
scrollHeight: container.scrollHeight,
53+
clientLeft: container.clientLeft,
54+
clientTop: container.clientTop,
5355
});
5456
});
5557
resizeObserver.observe(container);

packages/plugin-viewport/src/svelte/hooks/use-viewport-ref.svelte.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export function useViewportRef(getDocumentId: () => string | null) {
5858
scrollLeft: container.scrollLeft,
5959
scrollWidth: container.scrollWidth,
6060
scrollHeight: container.scrollHeight,
61+
clientLeft: container.clientLeft,
62+
clientTop: container.clientTop,
6163
});
6264
});
6365
resizeObserver.observe(container);

packages/plugin-viewport/src/vue/hooks/use-viewport-ref.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export function useViewportRef(documentId: MaybeRefOrGetter<string>) {
5757
scrollLeft: container.scrollLeft,
5858
scrollWidth: container.scrollWidth,
5959
scrollHeight: container.scrollHeight,
60+
clientLeft: container.clientLeft,
61+
clientTop: container.clientTop,
6062
});
6163
});
6264
resizeObserver.observe(container);

packages/plugin-zoom/src/shared/utils/pinch-zoom-logic.ts

Lines changed: 67 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,15 @@ export function setupPinchZoom({
3535

3636
const viewportScope = viewportProvides.forDocument(documentId);
3737
const zoomScope = zoomProvides.forDocument(documentId);
38-
3938
const getState = () => zoomScope.getState();
4039

41-
// Shared state for gestures
40+
// Shared state
4241
let initialZoom = 0;
43-
let lastCenter = { x: 0, y: 0 };
4442
let currentScale = 1;
45-
46-
// Pinch-specific state
4743
let isPinching = false;
4844
let initialDistance = 0;
4945

50-
// Wheel zoom state
46+
// Wheel state
5147
let wheelZoomTimeout: ReturnType<typeof setTimeout> | null = null;
5248
let accumulatedWheelScale = 1;
5349

@@ -56,15 +52,21 @@ export function setupPinchZoom({
5652
let initialElementHeight = 0;
5753
let initialElementLeft = 0;
5854
let initialElementTop = 0;
59-
let containerWidth = 0;
60-
let containerHeight = 0;
55+
56+
// Container Dimensions (Bounding Box)
57+
let containerRectWidth = 0;
58+
let containerRectHeight = 0;
59+
60+
// Layout Dimensions (Client Box from Metrics)
61+
// This is the actual space the CSS uses for centering.
62+
let layoutWidth = 0;
63+
let layoutCenterX = 0; // Relative to the container Rect origin
64+
6165
let pointerLocalY = 0;
6266
let pointerContainerX = 0;
6367
let pointerContainerY = 0;
6468

65-
// NEW: Simple number for the gap
6669
let currentGap = 0;
67-
6870
let pivotLocalX = 0;
6971

7072
const clamp = (val: number, min: number, max: number) => Math.min(Math.max(val, min), max);
@@ -73,54 +75,49 @@ export function setupPinchZoom({
7375
const finalWidth = initialElementWidth * scale;
7476
const finalHeight = initialElementHeight * scale;
7577

76-
// --- 1. Unconstrained Transforms ---
7778
let ty = pointerLocalY * (1 - scale);
7879

79-
const txCenter = (containerWidth - finalWidth) / 2 - initialElementLeft;
80+
// --- 1. Center-based Transform (The "Structural" Center) ---
81+
// Instead of using containerRectWidth, we use the layoutCenterX derived from Metrics.
82+
// layoutCenterX is the specific pixel where the content center should align.
83+
84+
// Target X position relative to Container Rect Left:
85+
const targetX = layoutCenterX - finalWidth / 2;
86+
87+
// Convert to translation (tx) relative to initial position:
88+
const txCenter = targetX - initialElementLeft;
89+
90+
// --- 2. Mouse-based Transform ---
8091
const txMouse = pointerContainerX - pivotLocalX * scale - initialElementLeft;
8192

82-
const overflow = Math.max(0, finalWidth - containerWidth);
83-
const blendRange = containerWidth * 0.3;
93+
// --- 3. Blending ---
94+
// Compare finalWidth against layoutWidth (actual available space).
95+
const overflow = Math.max(0, finalWidth - layoutWidth);
96+
const blendRange = layoutWidth * 0.3;
8497
const blend = Math.min(1, overflow / blendRange);
8598

8699
let tx = txCenter + (txMouse - txCenter) * blend;
87100

88-
// --- 2. Gap-Aware Clamping ---
89-
// If the content is larger than the "Safe Area" (Container - 2 * Gap),
90-
// we must clamp it so it doesn't detach from the edges.
91-
92-
// Vertical Clamp
93-
// Safe height is container minus top gap AND bottom gap
94-
const safeHeight = containerHeight - currentGap * 2;
95-
101+
// --- 4. Gap-Aware Clamping ---
102+
const safeHeight = containerRectHeight - currentGap * 2;
96103
if (finalHeight > safeHeight) {
97104
const currentTop = initialElementTop + ty;
98-
99-
// The content top cannot be lower than the gap (toolbar)
100105
const maxTop = currentGap;
101-
102-
// The content bottom cannot be higher than the container bottom (minus gap)
103-
// So: Top position cannot be less than (ContainerBottom - ContentHeight)
104-
const minTop = containerHeight - currentGap - finalHeight;
105-
106+
const minTop = containerRectHeight - currentGap - finalHeight;
106107
const constrainedTop = clamp(currentTop, minTop, maxTop);
107108
ty = constrainedTop - initialElementTop;
108109
}
109110

110-
// Horizontal Clamp
111-
const safeWidth = containerWidth - currentGap * 2;
112-
111+
const safeWidth = containerRectWidth - currentGap * 2;
113112
if (finalWidth > safeWidth) {
114113
const currentLeft = initialElementLeft + tx;
115-
116114
const maxLeft = currentGap;
117-
const minLeft = containerWidth - currentGap - finalWidth;
118-
115+
const minLeft = containerRectWidth - currentGap - finalWidth;
119116
const constrainedLeft = clamp(currentLeft, minLeft, maxLeft);
120117
tx = constrainedLeft - initialElementLeft;
121118
}
122119

123-
return { tx, ty, blend };
120+
return { tx, ty, blend, finalWidth };
124121
};
125122

126123
const updateTransform = (scale: number) => {
@@ -137,19 +134,25 @@ export function setupPinchZoom({
137134
};
138135

139136
const commitZoom = () => {
140-
const { tx, ty } = calculateTransform(currentScale);
137+
const { tx, ty, finalWidth } = calculateTransform(currentScale);
138+
const delta = (currentScale - 1) * initialZoom;
141139

142-
const scaleDiff = 1 - currentScale;
143-
const anchorX =
144-
Math.abs(scaleDiff) > 0.001 ? initialElementLeft + tx / scaleDiff : containerWidth / 2;
140+
let anchorX: number;
141+
let anchorY: number = pointerContainerY;
145142

146-
lastCenter = {
147-
x: anchorX,
148-
y: pointerContainerY,
149-
};
143+
// --- CRITICAL FIX ---
144+
// If the content fits within the LAYOUT width (not just rect width),
145+
// we force the anchor to be the Layout Center.
146+
if (finalWidth <= layoutWidth) {
147+
// anchorX is relative to the Container Rect Origin (which zoomScope uses)
148+
anchorX = layoutCenterX;
149+
} else {
150+
const scaleDiff = 1 - currentScale;
151+
anchorX =
152+
Math.abs(scaleDiff) > 0.001 ? initialElementLeft + tx / scaleDiff : pointerContainerX;
153+
}
150154

151-
const delta = (currentScale - 1) * initialZoom;
152-
zoomScope.requestZoomBy(delta, { vx: lastCenter.x, vy: lastCenter.y });
155+
zoomScope.requestZoomBy(delta, { vx: anchorX, vy: anchorY });
153156
resetTransform();
154157
initialZoom = 0;
155158
};
@@ -158,23 +161,35 @@ export function setupPinchZoom({
158161
const contRect = viewportScope.getBoundingRect();
159162
const innerRect = element.getBoundingClientRect();
160163

161-
// NEW: Fetch the simple gap number
162-
currentGap = viewportProvides.getViewportGap() || 0;
164+
// FETCH METRICS (Single Source of Truth)
165+
const metrics = viewportScope.getMetrics();
163166

167+
currentGap = viewportProvides.getViewportGap() || 0;
164168
initialElementWidth = innerRect.width;
165169
initialElementHeight = innerRect.height;
166170
initialElementLeft = innerRect.left - contRect.origin.x;
167171
initialElementTop = innerRect.top - contRect.origin.y;
168-
containerWidth = contRect.size.width;
169-
containerHeight = contRect.size.height;
172+
173+
containerRectWidth = contRect.size.width;
174+
containerRectHeight = contRect.size.height;
175+
176+
// --- CLEAN LAYOUT CALCULATION ---
177+
// We use the viewport metrics to determine the layout geometry.
178+
// clientWidth: The width available for content (excludes scrollbars/borders)
179+
// clientLeft: The width of the left border (offset from Rect origin to Content origin)
180+
const clientLeft = metrics.clientLeft;
181+
182+
layoutWidth = metrics.clientWidth;
183+
layoutCenterX = clientLeft + layoutWidth / 2;
170184

171185
const rawPointerLocalX = clientX - innerRect.left;
172186
pointerLocalY = clientY - innerRect.top;
173187
pointerContainerX = clientX - contRect.origin.x;
174188
pointerContainerY = clientY - contRect.origin.y;
175189

176-
if (initialElementWidth < containerWidth) {
177-
pivotLocalX = (pointerContainerX * initialElementWidth) / containerWidth;
190+
// Pivot Calculation based on Layout Width
191+
if (initialElementWidth < layoutWidth) {
192+
pivotLocalX = (pointerContainerX * initialElementWidth) / layoutWidth;
178193
} else {
179194
pivotLocalX = rawPointerLocalX;
180195
}
@@ -183,38 +198,31 @@ export function setupPinchZoom({
183198
// --- Handlers ---
184199
const handleTouchStart = (e: TouchEvent) => {
185200
if (e.touches.length !== 2) return;
186-
187201
isPinching = true;
188202
initialZoom = getState().currentZoomLevel;
189203
initialDistance = getTouchDistance(e.touches);
190-
191204
const center = getTouchCenter(e.touches);
192205
initializeGestureState(center.x, center.y);
193-
194206
e.preventDefault();
195207
};
196208

197209
const handleTouchMove = (e: TouchEvent) => {
198210
if (!isPinching || e.touches.length !== 2) return;
199-
200211
const currentDistance = getTouchDistance(e.touches);
201212
const scale = currentDistance / initialDistance;
202-
203213
updateTransform(scale);
204214
e.preventDefault();
205215
};
206216

207217
const handleTouchEnd = (e: TouchEvent) => {
208218
if (!isPinching) return;
209219
if (e.touches.length >= 2) return;
210-
211220
isPinching = false;
212221
commitZoom();
213222
};
214223

215224
const handleWheel = (e: WheelEvent) => {
216225
if (!e.ctrlKey && !e.metaKey) return;
217-
218226
e.preventDefault();
219227

220228
if (wheelZoomTimeout === null) {
@@ -228,7 +236,6 @@ export function setupPinchZoom({
228236
const zoomFactor = 1 - e.deltaY * 0.01;
229237
accumulatedWheelScale *= zoomFactor;
230238
accumulatedWheelScale = Math.max(0.1, Math.min(10, accumulatedWheelScale));
231-
232239
updateTransform(accumulatedWheelScale);
233240

234241
wheelZoomTimeout = setTimeout(() => {
@@ -250,11 +257,9 @@ export function setupPinchZoom({
250257
element.removeEventListener('touchend', handleTouchEnd);
251258
element.removeEventListener('touchcancel', handleTouchEnd);
252259
element.removeEventListener('wheel', handleWheel);
253-
254260
if (wheelZoomTimeout) {
255261
clearTimeout(wheelZoomTimeout);
256262
}
257-
258263
resetTransform();
259264
};
260265
}

0 commit comments

Comments
 (0)