Skip to content

Commit ecc6225

Browse files
Copilotmattleibow
andcommitted
Fix pan speed and rotation/zoom pivot point calculations
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
1 parent ffb2e93 commit ecc6225

File tree

1 file changed

+81
-35
lines changed

1 file changed

+81
-35
lines changed

source/SkiaSharp.Extended/Gestures/SKGestureTracker.cs

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -147,17 +147,23 @@ public Func<long> TimeProvider
147147
public SKPoint Offset => _offset;
148148

149149
/// <summary>Gets the composite transform matrix.</summary>
150+
/// <remarks>
151+
/// The matrix is composed as: screen offset, then center-origin, scale, rotate, and restore.
152+
/// This ensures pan moves content at 1:1 screen speed regardless of zoom level.
153+
/// </remarks>
150154
public SKMatrix Matrix
151155
{
152156
get
153157
{
154158
var w2 = _viewWidth / 2f;
155159
var h2 = _viewHeight / 2f;
160+
// Order (PreConcat reverses):
161+
// 1. Translate to center 2. Rotate 3. Scale 4. Translate to origin 5. Screen-space offset
156162
var m = SKMatrix.CreateTranslation(w2, h2);
157-
m = m.PreConcat(SKMatrix.CreateScale(_scale, _scale));
158163
m = m.PreConcat(SKMatrix.CreateRotationDegrees(_rotation));
159-
m = m.PreConcat(SKMatrix.CreateTranslation(_offset.X, _offset.Y));
164+
m = m.PreConcat(SKMatrix.CreateScale(_scale, _scale));
160165
m = m.PreConcat(SKMatrix.CreateTranslation(-w2, -h2));
166+
m = m.PreConcat(SKMatrix.CreateTranslation(_offset.X, _offset.Y));
161167
return m;
162168
}
163169
}
@@ -474,29 +480,27 @@ private void OnEnginePanDetected(object? s, SKPanEventArgs e)
474480
if (e.Handled || (dragArgs?.Handled ?? false))
475481
return;
476482

477-
// Update offset
478-
var d = ScreenToContentDelta(e.Delta.X, e.Delta.Y);
479-
_offset = new SKPoint(_offset.X + d.X, _offset.Y + d.Y);
483+
// Update offset (screen-space: apply delta directly)
484+
_offset = new SKPoint(_offset.X + e.Delta.X, _offset.Y + e.Delta.Y);
480485
TransformChanged?.Invoke(this, EventArgs.Empty);
481486
}
482487

483488
private void OnEnginePinchDetected(object? s, SKPinchEventArgs e)
484489
{
485490
PinchDetected?.Invoke(this, e);
486491

487-
// Apply center movement as pan
492+
// Apply center movement as pan (screen-space)
488493
if (IsPanEnabled)
489494
{
490-
var panDelta = ScreenToContentDelta(
491-
e.Center.X - e.PreviousCenter.X,
492-
e.Center.Y - e.PreviousCenter.Y);
493-
_offset = new SKPoint(_offset.X + panDelta.X, _offset.Y + panDelta.Y);
495+
var dx = e.Center.X - e.PreviousCenter.X;
496+
var dy = e.Center.Y - e.PreviousCenter.Y;
497+
_offset = new SKPoint(_offset.X + dx, _offset.Y + dy);
494498
}
495499

496500
if (IsPinchEnabled)
497501
{
498502
var newScale = Clamp(_scale * e.Scale, MinScale, MaxScale);
499-
AdjustOffsetForPivot(e.Center, _scale, newScale, _rotation, _rotation);
503+
AdjustOffsetForScalePivot(e.Center, _scale, newScale);
500504
_scale = newScale;
501505
}
502506

@@ -511,7 +515,7 @@ private void OnEngineRotateDetected(object? s, SKRotateEventArgs e)
511515
return;
512516

513517
var newRotation = _rotation + e.RotationDelta;
514-
AdjustOffsetForPivot(e.Center, _scale, _scale, _rotation, newRotation);
518+
AdjustOffsetForRotatePivot(e.Center, _rotation, newRotation);
515519
_rotation = newRotation;
516520
TransformChanged?.Invoke(this, EventArgs.Empty);
517521
}
@@ -538,7 +542,7 @@ private void OnEngineScrollDetected(object? s, SKScrollEventArgs e)
538542

539543
var scaleDelta = 1f + e.DeltaY * ScrollZoomFactor;
540544
var newScale = Clamp(_scale * scaleDelta, MinScale, MaxScale);
541-
AdjustOffsetForPivot(e.Location, _scale, newScale, _rotation, _rotation);
545+
AdjustOffsetForScalePivot(e.Location, _scale, newScale);
542546
_scale = newScale;
543547
TransformChanged?.Invoke(this, EventArgs.Empty);
544548
}
@@ -565,30 +569,73 @@ private void OnEngineGestureEnded(object? s, SKGestureStateEventArgs e)
565569

566570
#region Transform Helpers
567571

568-
private SKPoint ScreenToContentDelta(float dx, float dy)
572+
/// <summary>
573+
/// Adjusts the screen-space offset so that the point under screenPivot stays fixed
574+
/// when scale changes from oldScale to newScale.
575+
/// </summary>
576+
private void AdjustOffsetForScalePivot(SKPoint screenPivot, float oldScale, float newScale)
569577
{
570-
var inv = SKMatrix.CreateRotationDegrees(-_rotation);
571-
var mapped = inv.MapVector(dx, dy);
572-
return new SKPoint(mapped.X / _scale, mapped.Y / _scale);
578+
// Screen-space: pivot point relative to view center
579+
var w2 = _viewWidth / 2f;
580+
var h2 = _viewHeight / 2f;
581+
582+
// Point in screen space before applying offset
583+
// To keep screenPivot fixed: offset_new = offset_old + pivot * (1 - newScale/oldScale)
584+
// But since Matrix is: offset then center then scale etc, we need:
585+
// The content point under screenPivot must map to the same screen point after scale change.
586+
587+
// Before scale change: screenPivot maps to some content point P
588+
// After scale change: we want P to still be under screenPivot
589+
//
590+
// Let's derive: screenPivot = M * contentPoint
591+
// We want: screenPivot = M_new * contentPoint
592+
// So: offset_new = offset_old + (screenPivot - center) * (1 - oldScale/newScale)
593+
594+
var dx = screenPivot.X - w2;
595+
var dy = screenPivot.Y - h2;
596+
var factor = 1f - oldScale / newScale;
597+
598+
_offset = new SKPoint(
599+
_offset.X + dx * factor,
600+
_offset.Y + dy * factor);
573601
}
574602

575-
private void AdjustOffsetForPivot(SKPoint screenPivot, float oldScale, float newScale, float oldRotDeg, float newRotDeg)
603+
/// <summary>
604+
/// Adjusts the screen-space offset so that the point under screenPivot stays fixed
605+
/// when rotation changes from oldRotation to newRotation.
606+
/// </summary>
607+
private void AdjustOffsetForRotatePivot(SKPoint screenPivot, float oldRotationDeg, float newRotationDeg)
576608
{
609+
// Screen-space: rotate offset around the pivot point
577610
var w2 = _viewWidth / 2f;
578611
var h2 = _viewHeight / 2f;
579-
var d = new SKPoint(screenPivot.X - w2, screenPivot.Y - h2);
580-
581-
var rotOld = SKMatrix.CreateRotationDegrees(-oldRotDeg);
582-
var qOld = rotOld.MapVector(d.X, d.Y);
583-
qOld = new SKPoint(qOld.X / oldScale, qOld.Y / oldScale);
584-
585-
var rotNew = SKMatrix.CreateRotationDegrees(-newRotDeg);
586-
var qNew = rotNew.MapVector(d.X, d.Y);
587-
qNew = new SKPoint(qNew.X / newScale, qNew.Y / newScale);
588-
589-
_offset = new SKPoint(
590-
_offset.X + qNew.X - qOld.X,
591-
_offset.Y + qNew.Y - qOld.Y);
612+
613+
// The rotation happens around view center in screen space.
614+
// To keep screenPivot fixed, we need to adjust offset.
615+
//
616+
// Content under screenPivot: inverse_M(screenPivot)
617+
// After rotation change, we want inverse_M_new(screenPivot) to be the same content point.
618+
//
619+
// Simpler approach: rotate the offset around the pivot point
620+
var deltaDeg = newRotationDeg - oldRotationDeg;
621+
var deltaRad = deltaDeg * (float)Math.PI / 180f;
622+
623+
// Pivot relative to center
624+
var px = screenPivot.X - w2;
625+
var py = screenPivot.Y - h2;
626+
627+
// Offset relative to pivot
628+
var ox = _offset.X - px;
629+
var oy = _offset.Y - py;
630+
631+
// Rotate offset around pivot
632+
var cos = (float)Math.Cos(deltaRad);
633+
var sin = (float)Math.Sin(deltaRad);
634+
var rotX = ox * cos - oy * sin;
635+
var rotY = ox * sin + oy * cos;
636+
637+
// Translate back
638+
_offset = new SKPoint(rotX + px, rotY + py);
592639
}
593640

594641
private static float Clamp(float value, float min, float max)
@@ -648,9 +695,8 @@ private void HandleFlingFrame()
648695

649696
Flinging?.Invoke(this, new SKFlingEventArgs(_flingVelocityX, _flingVelocityY, deltaX, deltaY));
650697

651-
// Apply as pan offset
652-
var d = ScreenToContentDelta(deltaX, deltaY);
653-
_offset = new SKPoint(_offset.X + d.X, _offset.Y + d.Y);
698+
// Apply as pan offset (screen-space)
699+
_offset = new SKPoint(_offset.X + deltaX, _offset.Y + deltaY);
654700
TransformChanged?.Invoke(this, EventArgs.Empty);
655701

656702
// Apply friction (FlingFriction: 0 = no friction, 1 = full friction)
@@ -713,7 +759,7 @@ private void HandleZoomFrame()
713759
// Apply scale change
714760
var oldScale = _scale;
715761
var newScale = Clamp(_zoomStartScale * cumulative, MinScale, MaxScale);
716-
AdjustOffsetForPivot(_zoomFocalPoint, oldScale, newScale, _rotation, _rotation);
762+
AdjustOffsetForScalePivot(_zoomFocalPoint, oldScale, newScale);
717763
_scale = newScale;
718764
TransformChanged?.Invoke(this, EventArgs.Empty);
719765

0 commit comments

Comments
 (0)