Нашли ошибку или опечатку? Выделите текст и нажмите

Поменять цветовую

гамму сайта?

Поменять
Обновления сайта
и новые разделы

Рекомендовать в Google +1

Азимут и угловая высота в WinRT

184

Концептуально мы живем внутри небесной сферы. Если вам потребуется описать местонахождение объекта в трехмерном пространстве относительно себя (без учета расстояния), очень удобно использовать для этой цели точки, заданные относительно небесной сферы. Небесная сфера особенно хорошо подходит для программ, использующих экран компьютера для отображения мира виртуальной или дополненной реальности. В таких программах вы держите планшет так, словно собираетесь сделать снимок камерой, находящейся на задней панели, но при этом изображение на экране генерируется (частично или полностью) программой на основании ориентации экрана. Перемещение экрана позволяет рассматривать разные части этого мира.

У небесной сферы есть хорошо знакомый нам земной аналог. Для определения точек на поверхности Земли используются широта и долгота. Концептуально земная сфера делится пополам по экватору. Линии широты параллельны экватору, а значение определяется положительными углами к северу от экватора (до 90° на Северном полюсе) и отрицательными углами к югу (до -90° на Южном полюсе). Долгота определяется углом между плоскостью меридиана, проходящего через точку, и плоскостью нулевого меридиана, проходящего через Гринвич (Англия).

Точки небесной сферы можно описывать подобным образом, но когда вы находитесь в центре сферы, терминология несколько изменяется.

Протяните руку в любом направлении. Как описать это направление? Для начала переместите руку вверх или вниз, чтобы она пришла в горизонтальное положение (параллельно поверхности Земли). Угол, на который переместится ваша рука во время этого движения, называется угловой высотой (altitude).

Точки с положительными значениями угловой высоты расположены выше горизонта, а точки с отрицательными значениями — ниже горизонта. У зенита — точки, находящейся непосредственно по вертикали — угловая высота равна 90°. Непосредственно внизу находится надир — точка с угловой высотой -90°.

Ваша рука все еще протянута по направлению к горизонту? Взмахните ей так, чтобы она указывала на север. Угол, который ваша рука описала на этот раз, называется азимутом.

Угловая высота и азимут образуют горизонтальную координату, которая называется так потому, что горизонт делит небесную сферу пополам (по аналогии с экватором в земных, или географических координатах).

Горизонтальная координата не содержит информации о расстоянии. Во время солнечного затмения Солнце и Луна имеют одинаковую горизонтальную координату. Горизонтальная координата определяет не точку в трехмерном пространстве, а направление в трехмерном пространстве относительно зрителя. В этом смысле горизонтальная координата напоминает трехмерный вектор, но вектор выражается в прямоугольных координатах, а горизонтальная координата — в сферических.

Чтобы немного упростить задачу вычисления горизонтальной координаты, мы сначала определим структуру Vector3, инкапсулирующую трехмерный вектор:

using System;
using Windows.Foundation;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Media3D;

namespace WinRTTestApp
{
    public struct Vector3
    {
        // Конструкторы
        public Vector3(double x, double y, double z)
            : this()
        {
            X = x;
            Y = y;
            Z = z;
        }

        // Свойства
        public double X { private set; get; }
        public double Y { private set; get; }
        public double Z { private set; get; }

        public double LengthSquared
        {
            get { return X * X + Y * Y + Z * Z; }
        }

        public double Length
        {
            get { return Math.Sqrt(LengthSquared); }
        }

        public Vector3 Normalized
        {
            get
            {
                double length = this.Length;

                if (length != 0)
                {
                    return new Vector3(this.X / length,
                                       this.Y / length,
                                       this.Z / length);
                }
                return new Vector3();
            }
        }

        // Статичные свойства
        public static Vector3 UnitX
        {
            get { return new Vector3(1, 0, 0); }
        }

        public static Vector3 UnitY
        {
            get { return new Vector3(0, 1, 0); }
        }

        public static Vector3 UnitZ
        {
            get { return new Vector3(0, 0, 1); }
        }

        // Статичные методы
        public static Vector3 Cross(Vector3 v1, Vector3 v2)
        {
            return new Vector3(v1.Y * v2.Z - v1.Z * v2.Y,
                               v1.Z * v2.X - v1.X * v2.Z,
                               v1.X * v2.Y - v1.Y * v2.X);
        }

        public static double Dot(Vector3 v1, Vector3 v2)
        {
            return v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z;
        }

        public static double AngleBetween(Vector3 v1, Vector3 v2)
        {
            return 180 / Math.PI * Math.Acos(Vector3.Dot(v1, v2) /
                                                v1.Length * v2.Length);
        }

        public static Vector3 Transform(Vector3 v, Matrix3D m)
        {
            double x = m.M11 * v.X + m.M21 * v.Y + m.M31 * v.Z + m.OffsetX;
            double y = m.M12 * v.X + m.M22 * v.Y + m.M32 * v.Z + m.OffsetY;
            double z = m.M13 * v.X + m.M23 * v.Y + m.M33 * v.Z + m.OffsetZ;
            double w = m.M14 * v.X + m.M24 * v.Y + m.M34 * v.Z + m.M44;
            return new Vector3(x / w, y / w, z / w);
        }

        // Перегрузка операторов
        public static Vector3 operator +(Vector3 v1, Vector3 v2)
        {
            return new Vector3(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
        }

        public static Vector3 operator -(Vector3 v1, Vector3 v2)
        {
            return new Vector3(v1.X - v2.X, v1.Y - v2.Y, v1.Z - v2.Z);
        }

        public static Vector3 operator *(Vector3 v, double d)
        {
            return new Vector3(d * v.X, d * v.Y, d * v.Z);
        }

        public static Vector3 operator *(double d, Vector3 v)
        {
            return new Vector3(d * v.X, d * v.Y, d * v.Z);
        }

        public static Vector3 operator /(Vector3 v, double d)
        {
            return new Vector3(v.X / d, v.Y / d, v.Z / d);
        }

        public static Vector3 operator -(Vector3 v)
        {
            return new Vector3(-v.X, -v.Y, -v.Z);
        }

        // Переопределение ToString()
        public override string ToString()
        {
            return String.Format("({0} {1} {2})", X, Y, Z);
        }
    }
}

Структура содержит ряд вспомогательных функций, включая традиционное скалярное и векторное произведение, а также метод Transform для умножения значения Vector3 на значение Matrix3D. На практике значение Matrix3D, скорее всего, будет представлять поворот, так что умножение фактически поворачивает вектор в трехмерном пространстве.

Держа планшет вертикально в воздухе, мы смотрим на экран в направлении относительно системы координат компьютера, а конкретно в направлении вектора, направленного в глубь экрана, то есть отрицательной оси Z или (0,0,-1). Его необходимо преобразовать в горизонтальную координату.

Давайте создадим значение Matrix3D с именем matrix на основании объекта SensorRotationMatrix, предоставляемого OrientationSensor. Это значение можно инвертировать для представления преобразования из системы координат компьютера в систему координат Земли:

matrix.Invert();

Матрица используется для преобразования вектора (0,0,-1) (полученного инвертированием статического свойства UnitZ, предоставляемого структурой Vector3) в прямоугольную систему координат:

Vector3 vector = Vector3.Transform(-Vector3.UnitZ, matrix);

Этот вектор представлен в прямоугольных координатах, а нам нужно преобразовать его в горизонтальную координату. Вспомните, что в системе координат Земли ось Z направлена от Земли. Если держать планшет вертикально, то у оси, выходящей из задней панели устройства, при преобразовании в координаты Земли составляющая Z равна нулю. Это означает, что азимут можно вычислить по хорошо известному преобразованию двумерных декартовых координат в полярные координаты; также преобразуем угол из радианов в градусы:

double azimuth = 180 * Math.Atan2(vector.X, vector.Y) / Math.PI;

Вообще говоря, эта формула действительна независимо от составляющей Z преобразованного вектора. Так как угловая высота изменяется в диапазоне от -90° до 90°, для вычисления можно воспользоваться функцией арксинуса:

double altitude = 180 * Math.Asin(vector.Z) / Math.PI;

Однако мы кое-что упускаем. Мы преобразовали трехмерную матрицу поворота в координату, которая имеет только два компонента, потому что она ограничена внутренней поверхностью сферы. Что произойдет, если направить планшет на точку небесной сферы, а потом повернуть планшет вокруг этой оси? Угловая высота и азимут останутся неизменными, но вид на экране компьютера определенно должен измениться в результате вращения. Недостающий компонент иногда называется наклоном (tilt). Он вычисляется несколько сложнее, но соответствующие вычисления представлены в структуре HorizontalCoordinate:

using System;
using Windows.UI.Xaml.Media.Media3D;

namespace WinRTTestApp
{
    public struct HorizontalCoordinate
    {
        public HorizontalCoordinate(double azimuth, double altitude, double tilt)
            : this()
        {
            this.Azimuth = azimuth;
            this.Altitude = altitude;
            this.Tilt = tilt;
        }

        public HorizontalCoordinate(double azimuth, double altitude)
            : this(azimuth, altitude, 0)
        {
        }

        // Северо-восток
        public double Azimuth { private set; get; }

        public double Altitude { private set; get; }

        public double Tilt { private set; get; }

        public static HorizontalCoordinate FromVector(Vector3 vector)
        {
            double altitude = 180 * Math.Asin(vector.Z) / Math.PI;
            double azimuth = 180 * Math.Atan2(vector.X, vector.Y) / Math.PI;

            return new HorizontalCoordinate(azimuth, altitude);
        }

        public static HorizontalCoordinate FromMotionMatrix(Matrix3D matrix)
        {
            // Инвертирование матрицы
            matrix.Invert();

            // Трансформация (0, 0, -1)
            Vector3 zAxisTransformed = Vector3.Transform(-Vector3.UnitZ, matrix);

            // Получение горизонтальной координаты
            HorizontalCoordinate horzCoord = FromVector(zAxisTransformed);

            // Получение теоретического значения HorizontalCoordinate,
            // для преобразования вектора +Y при вертикальном расположении устройства
            double yUprightAltitude = 0;
            double yUprightAzimuth = 0;

            if (horzCoord.Altitude > 0)
            {
                yUprightAltitude = 90 - horzCoord.Altitude;
                yUprightAzimuth = 180 + horzCoord.Azimuth;
            }
            else
            {
                yUprightAltitude = 90 + horzCoord.Altitude;
                yUprightAzimuth = horzCoord.Azimuth;
            }
            Vector3 yUprightVector = 
                new HorizontalCoordinate(yUprightAzimuth, yUprightAltitude).ToVector();

            // Определение реального преобразования вектора +Y
            Vector3 yAxisTransformed = Vector3.Transform(Vector3.UnitY, matrix);

            // Получение угла между вертикальным вектором +Y
            // и реальным преобразованным вектором +Y
            double dotProduct = Vector3.Dot(yUprightVector, yAxisTransformed);
            Vector3 crossProduct = Vector3.Cross(yUprightVector, yAxisTransformed);
            crossProduct = crossProduct.Normalized;

            // Иногда значение dotProduct слегка превышает 1,
            // что приводит к исключению при вычислении angleBetween, поэтому ...
            dotProduct = Math.Min(dotProduct, 1);
            double angleBetween = 180 * Vector3.Dot(zAxisTransformed, crossProduct) 
                                      * Math.Acos(dotProduct) / Math.PI;
            horzCoord.Tilt = angleBetween;

            return horzCoord;
        }

        public Vector3 ToVector()
        {
            double x = Math.Cos(Math.PI * this.Altitude / 180) * 
                       Math.Sin(Math.PI * this.Azimuth / 180);
            double y = Math.Cos(Math.PI * this.Altitude / 180) * 
                       Math.Cos(Math.PI * this.Azimuth / 180);
            double z = Math.Sin(Math.PI * this.Altitude / 180);

            return new Vector3((float)x, (float)y, (float)z);
        }

        public override string ToString()
        {
            return String.Format("Azi: {0} Alt: {1} Tilt: {2}", 
                                 this.Azimuth, this.Altitude, this.Tilt);
        }
    }
}

С таким преобразованием мы прошли немалую часть пути по созданию астрономической программы, которая отображает конкретную часть ночного неба в зависимости от ориентации экрана. Но давайте ограничимся более скромной задачей.

Что делать, если вы хотите просмотреть растровое изображение, размеры которого значительно превышают размеры экрана, а уменьшение нежелательно? Одно из традиционных решений основано на использовании полос прокрутки. Другое, более современное решение позволяет перемещать изображение пальцами.

Еще один подход основан на размещении изображения на внутренней поверхности небесной сферы. Чтобы просмотреть изображение, пользователь держит планшет в руках и изменяет ориентацию экрана. Конечно, никто не собирается растягивать изображение по поверхности сферы! Вместо этого мы просто используем азимут для горизонтальной прокрутки, а угловую высоту — для вертикальной.

Программа EarthlyDelights позволяет просмотреть большое (7793 x 4409 пикселов) растровое изображение картины Иеронима Босха «Сад земных наслаждений», загружаемой из Википедии. Вот как выглядит одна из частей изображения в программе, запущенной на устройстве Microsoft Surface:

Загрузка большого растрового изображения в программу

Программа не имеет сенсорного интерфейса для масштабирования или прокрутки изображения. Содержимое экрана изменяется при изменении ориентации экрана. Впрочем, если прикоснуться к экрану, все изображение уменьшается по размерам экрана, а просматриваемая часть выделяется прямоугольником:

Масштабирование большого изображения

Эта функция усложняет код, но я считаю, что она необходима.

Очевидно, в файле XAML центральное место занимает элемент Image. Обрати внимание: свойству Stretch элемента Image задано значение None, и он содержит объект BitmapImage без URI источника данных (пока).

Панель Grid, содержащая элемент Image, помещена в панель Canvas, чтобы избежать усечения, если ее размеры превысят размеры экрана (как оно и будет):

<Page ...>

    <Grid Background="#FF1D1D1D">
        <!-- Два элемента отображаются только во время загрузки -->        
        <ProgressBar Name="progressBar" VerticalAlignment="Center" Margin="96 0" />

        <TextBlock Name="statusText"
                   Text="загрузка картинки..."
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center" />
        <Canvas>
            <Grid>
                <Image Stretch="None">
                    <Image.Source>
                        <BitmapImage x:Name="bitmapImage"
                                     ImageFailed="OnBitmapImageFailed"
                                     ImageOpened="OnBitmapImageOpened"
                                     DownloadProgress="OnBitmapImageDownloadProgress" />
                    </Image.Source>
                </Image>

                <Border Name="outlineBorder"
                        BorderBrush="White"
                        HorizontalAlignment="Left"
                        VerticalAlignment="Top">

                    <Rectangle Name="outlineRectangle" Stroke="Black" />

                    <Border.RenderTransform>
                        <CompositeTransform x:Name="borderTransform" />
                    </Border.RenderTransform>
                </Border>

                <Grid.RenderTransform>
                    <CompositeTransform x:Name="imageTransform" />
                </Grid.RenderTransform>
            </Grid>
        </Canvas>

        <TextBlock Name="titleText" Margin="2 "/>
    </Grid>
</Page>

Элемент Border с внутренним элементом Rectangle используется в режиме уменьшения для выделения части изображения, которая в нормальном режиме занимает весь экран, но этот прямоугольник виден и в нормальном режиме. Внешнее преобразование CompositeTransform применяется и к Image, и к Border. В нормальном режиме оно не делает ничего. Внутреннее преобразование CompositeTransform ориентирует Border по области картины, видимой в нормальном режиме.

Обработчик Loaded проверяет, доступна ли информация OrientationSensor, и если доступна — запускает загрузку, задавая свойство UriSource объекта BitmapImage. Если загрузка проходит нормально, размеры изображения в пикселах вместе с размерами страницы сохраняются в полях:

using System;
using Windows.Devices.Sensors;
using Windows.Graphics.Display;
using Windows.UI.Core;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Media.Media3D;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        OrientationSensor orientationSensor = OrientationSensor.GetDefault();
        double pageWidth, pageHeight, maxDimension;
        int imageWidth, imageHeight;
        string title = "Сад земных наслаждений. И. Босх";
        double zoomInScale;
        double rotation;
        bool isZoomView;

        public MainPage()
        {
            this.InitializeComponent();
            DisplayProperties.AutoRotationPreferences = DisplayProperties.NativeOrientation;
            Loaded += OnMainPageLoaded;
            SizeChanged += OnMainPageSizeChanged;
        }

        // ...

        private async void OnMainPageLoaded(object sender, RoutedEventArgs e)
        {
            if (orientationSensor == null)
            {
                await new MessageDialog("Сенсор ориентации не поддерживается",
                                        "Ошибка").ShowAsync();

                progressBar.Visibility = Visibility.Collapsed;
                statusText.Visibility = Visibility.Collapsed;
            }
            else
            {
                bitmapImage.UriSource =
                  new Uri("http://upload.wikimedia.org/wikipedia/commons/6/62/The_Garden_of_Earthly_Delights_by_Bosch_High_Resolution_2.jpg");
            }
        }

        private void OnMainPageSizeChanged(object sender, SizeChangedEventArgs e)
        {
            // Сохранение размеров страницы
            pageWidth = this.ActualWidth;
            pageHeight = this.ActualHeight;
            maxDimension = Math.Max(pageWidth, pageHeight);

            // Инициализация некоторых свойств
            outlineBorder.Width = pageWidth;
            outlineBorder.Height = pageHeight;
            borderTransform.CenterX = pageWidth / 2;
            borderTransform.CenterY = pageHeight / 2;
        }

        private void OnBitmapImageDownloadProgress(object sender, 
            DownloadProgressEventArgs e)
        {
            progressBar.Value = e.Progress;
        }

        private async void OnBitmapImageFailed(object sender, ExceptionRoutedEventArgs e)
        {
            progressBar.Visibility = Visibility.Collapsed;
            statusText.Visibility = Visibility.Collapsed;

            await new MessageDialog("Невозможно загрузить картинку: " + e.ErrorMessage,
                                    "Ошибка").ShowAsync();
        }

        private void OnBitmapImageOpened(object sender, RoutedEventArgs e)
        {
            progressBar.Visibility = Visibility.Collapsed;
            statusText.Visibility = Visibility.Collapsed;

            // Сохранение размеров изображения
            imageWidth = bitmapImage.PixelWidth;
            imageHeight = bitmapImage.PixelHeight;
            titleText.Text = String.Format("{0} ({1}\x00D7{2})", title, imageWidth, imageHeight);

            // Инициализация преобразований
            zoomInScale = Math.Min(pageWidth / imageWidth, pageHeight / imageHeight);

            // Запуск OrientationSensor
            if (orientationSensor != null)
            {
                ProcessNewOrientationReading(orientationSensor.GetCurrentReading());
                orientationSensor.ReportInterval = orientationSensor.MinimumReportInterval;
                orientationSensor.ReadingChanged += OnOrientationSensorReadingChanged;
            }
        }

        private async void OnOrientationSensorReadingChanged(OrientationSensor sender, 
                                                     OrientationSensorReadingChangedEventArgs e)
        {
            await this.Dispatcher.RunAsync(
                CoreDispatcherPriority.Normal, () =>
                {
                    ProcessNewOrientationReading(e.Reading);
                });
        }

        // ...
    }
}

Метод ProcessNewOrientationReading создает объект Matrix3D на основе SensorRotationMatrix и использует его для вычисления значения HorizontalCoordinate:

public sealed partial class MainPage : Page
{
    // ...

    private void ProcessNewOrientationReading(OrientationSensorReading orientationReading)
    {
        if (orientationReading == null)
            return;

        // Получение матрицы поворота и преобразование к горизонтальным координатам
        SensorRotationMatrix m = orientationReading.RotationMatrix;

        if (m == null)
            return;

        Matrix3D matrix3d = new Matrix3D(m.M11, m.M12, m.M13, 0,
                                 m.M21, m.M22, m.M23, 0,
                                 m.M31, m.M32, m.M33, 0,
                                 0, 0, 0, 1);
        if (!matrix3d.HasInverse)
            return;

        HorizontalCoordinate horzCoord = HorizontalCoordinate.FromMotionMatrix(matrix3d);

        // Определение центра преобразования для элемента Image
        imageTransform.CenterX = (imageWidth + maxDimension) *
                            (180 + horzCoord.Azimuth) / 360 - maxDimension / 2;
        imageTransform.CenterY = (imageHeight + maxDimension) *
                            (90 - horzCoord.Altitude) / 180 - maxDimension / 2;

        // Определение сдвига для элемента Border
        borderTransform.TranslateX = imageTransform.CenterX - pageWidth / 2;
        borderTransform.TranslateY = imageTransform.CenterY - pageHeight / 2;

        // Получение угла поворота из свойства Tilt
        rotation = -horzCoord.Tilt;
        UpdateImageTransforms();
    }
    
    // ...
}

Метод отвечает за определение части преобразований; другие преобразования задаются в методе UpdateImageTransforms, вызываемом в самом конце этого метода. Если и азимут, и угловая высота равны 0, свойствам CenterX и CenterY задаются координаты центра изображения. В противном случае им задаются значения в пределах ширины и высоты изображения с учетом полей, формирующих «рамку» вокруг изображения (чтобы исключить ситуации, когда правый край изображения отображается у левого края экрана, или левый край — у правого).

Я решил, что операция масштабирования должна анимироваться, поэтому определил для MainPage свойство зависимости, которое является целью анимации при прикосновении к экрану:

public sealed partial class MainPage : Page
{
    // Свойство зависимости для анимации приближения
    static readonly DependencyProperty interpolationFactorProperty =
        DependencyProperty.Register("InterpolationFactor",
                    typeof(double),
                    typeof(MainPage),
                    new PropertyMetadata(0.0, OnInterpolationFactorChanged));

    // ...
        
    public static DependencyProperty InterpolationFactorProperty
    {
        get { return interpolationFactorProperty; }
    }

    public double InterpolationFactor
    {
        set { SetValue(InterpolationFactorProperty, value); }
        get { return (double)GetValue(InterpolationFactorProperty); }
    }

    // ...

    protected override void OnTapped(TappedRoutedEventArgs e)
    {
        // Анимация свойства InterpolationFactor
        DoubleAnimation doubleAnimation = new DoubleAnimation
        {
        EnableDependentAnimation = true,
        To = isZoomView ? 0 : 1,
        Duration = new Duration(TimeSpan.FromSeconds(1))
        };
        Storyboard.SetTarget(doubleAnimation, this);
        Storyboard.SetTargetProperty(doubleAnimation, "InterpolationFactor");
        Storyboard storyboard = new Storyboard();
        storyboard.Children.Add(doubleAnimation);
        storyboard.Begin();
        isZoomView ^= true;
        base.OnTapped(e);
    }

    static void OnInterpolationFactorChanged(DependencyObject obj,
                         DependencyPropertyChangedEventArgs args)
    {
        (obj as MainPage).UpdateImageTransforms();
    }

    // ...
}

Метод OnInterpolationFactorChanged также вызывает метод UpdateImageTransforms, в котором выполняется основная часть работы:

public sealed partial class MainPage : Page
{
    // ...

    private void UpdateImageTransforms()
    {
        // Если выполняется уменьшение, вычислить масштаб
        double interpolatedScale = 1 + InterpolationFactor * (zoomInScale - 1);
        imageTransform.ScaleX =
        imageTransform.ScaleY = interpolatedScale;

        // Перемещение центра преобразования в центр экрана
        imageTransform.TranslateX = pageWidth / 2 - imageTransform.CenterX;
        imageTransform.TranslateY = pageHeight / 2 - imageTransform.CenterY;

        // Если выполняется уменьшение, скорректировать изменение масштаба
        imageTransform.TranslateX -= InterpolationFactor *
                (pageWidth / 2 - zoomInScale * imageTransform.CenterX);
        imageTransform.TranslateY -= InterpolationFactor *
                (pageHeight / 2 - zoomInScale * imageTransform.CenterY);

        // Если выполняется уменьшение, расположить изображение по центру экрана
        imageTransform.TranslateX += InterpolationFactor *
                (pageWidth - zoomInScale * imageWidth) / 2;
        imageTransform.TranslateY += InterpolationFactor *
                (pageHeight - zoomInScale * imageHeight) / 2;

        // Задание толщины границы
        outlineBorder.BorderThickness = new Thickness(2 / interpolatedScale);
        outlineRectangle.StrokeThickness = 2 / interpolatedScale;

        // Задание поворота для изображения и рамки
        imageTransform.Rotation = (1 - InterpolationFactor) * rotation;
        borderTransform.Rotation = -rotation;
    }
}

Этот метод вызывается при появлении нового значения OrientationSensor или при изменении свойства InterpolationFactor для операции масштабирования. Если вас интересует, как работает этот метод, попробуйте убрать весь код интерполяции. Задайте InterpolationFactor значение 0, а потом 1 — вы увидите, что все работает достаточно прямолинейно.

Пройди тесты