file360

Log | Files | Refs

TiltEffect.cs (26451B)


      1 /* 
      2     Copyright (c) 2010 Microsoft Corporation.  All rights reserved.
      3     Use of this sample source code is subject to the terms of the Microsoft license 
      4     agreement under which you licensed this sample source code and is provided AS-IS.
      5     If you did not accept the terms of the license agreement, you are not authorized 
      6     to use this sample source code.  For the terms of the license, please see the 
      7     license agreement between you and Microsoft.
      8 */
      9 
     10 
     11 using System;
     12 using System.Windows;
     13 using System.Windows.Controls;
     14 using System.Windows.Input;
     15 using System.Windows.Media;
     16 using System.Windows.Media.Animation;
     17 using System.Collections.Generic;
     18 using System.Windows.Controls.Primitives;
     19 
     20 
     21 #if WINDOWS_PHONE
     22 using Microsoft.Phone.Controls;
     23 #endif
     24 
     25 namespace File360
     26 {
     27     /// <summary>
     28     /// This code provides attached properties for adding a 'tilt' effect to all controls within a container.
     29     /// </summary>
     30     public class TiltEffect : DependencyObject
     31     {
     32 
     33         #region Constructor and Static Constructor
     34         /// <summary>
     35         /// This is not a constructable class, but it cannot be static because it derives from DependencyObject.
     36         /// </summary>
     37         private TiltEffect()
     38         {
     39         }
     40 
     41         /// <summary>
     42         /// Initialize the static properties
     43         /// </summary>
     44         static TiltEffect()
     45         {
     46             // The tiltable items list.
     47             TiltableItems = new List<Type>() { typeof(ButtonBase), typeof(ListBoxItem), };
     48             UseLogarithmicEase = false;
     49         }
     50 
     51         #endregion
     52 
     53 
     54         #region Fields and simple properties
     55 
     56         // These constants are the same as the built-in effects
     57         /// <summary>
     58         /// Maximum amount of tilt, in radians
     59         /// </summary>
     60         const double MaxAngle = 0.3;
     61 
     62         /// <summary>
     63         /// Maximum amount of depression, in pixels
     64         /// </summary>
     65         const double MaxDepression = 25;
     66 
     67         /// <summary>
     68         /// Delay between releasing an element and the tilt release animation playing
     69         /// </summary>
     70         static readonly TimeSpan TiltReturnAnimationDelay = TimeSpan.FromMilliseconds(200);
     71 
     72         /// <summary>
     73         /// Duration of tilt release animation
     74         /// </summary>
     75         static readonly TimeSpan TiltReturnAnimationDuration = TimeSpan.FromMilliseconds(100);
     76 
     77         /// <summary>
     78         /// The control that is currently being tilted
     79         /// </summary>
     80         static FrameworkElement currentTiltElement;
     81 
     82         /// <summary>
     83         /// The single instance of a storyboard used for all tilts
     84         /// </summary>
     85         static Storyboard tiltReturnStoryboard;
     86 
     87         /// <summary>
     88         /// The single instance of an X rotation used for all tilts
     89         /// </summary>
     90         static DoubleAnimation tiltReturnXAnimation;
     91 
     92         /// <summary>
     93         /// The single instance of a Y rotation used for all tilts
     94         /// </summary>
     95         static DoubleAnimation tiltReturnYAnimation;
     96 
     97         /// <summary>
     98         /// The single instance of a Z depression used for all tilts
     99         /// </summary>
    100         static DoubleAnimation tiltReturnZAnimation;
    101 
    102         /// <summary>
    103         /// The center of the tilt element
    104         /// </summary>
    105         static Point currentTiltElementCenter;
    106 
    107         /// <summary>
    108         /// Whether the animation just completed was for a 'pause' or not
    109         /// </summary>
    110         static bool wasPauseAnimation = false;
    111 
    112         /// <summary>
    113         /// Whether to use a slightly more accurate (but slightly slower) tilt animation easing function
    114         /// </summary>
    115         public static bool UseLogarithmicEase { get; set; }
    116 
    117         /// <summary>
    118         /// Default list of items that are tiltable
    119         /// </summary>
    120         public static List<Type> TiltableItems { get; private set; }
    121 
    122         #endregion
    123 
    124 
    125         #region Dependency properties
    126 
    127         /// <summary>
    128         /// Whether the tilt effect is enabled on a container (and all its children)
    129         /// </summary>
    130         public static readonly DependencyProperty IsTiltEnabledProperty = DependencyProperty.RegisterAttached(
    131           "IsTiltEnabled",
    132           typeof(bool),
    133           typeof(TiltEffect),
    134           new PropertyMetadata(OnIsTiltEnabledChanged)
    135           );
    136 
    137         /// <summary>
    138         /// Gets the IsTiltEnabled dependency property from an object
    139         /// </summary>
    140         /// <param name="source">The object to get the property from</param>
    141         /// <returns>The property's value</returns>
    142         public static bool GetIsTiltEnabled(DependencyObject source) { return (bool)source.GetValue(IsTiltEnabledProperty); }
    143 
    144         /// <summary>
    145         /// Sets the IsTiltEnabled dependency property on an object
    146         /// </summary>
    147         /// <param name="source">The object to set the property on</param>
    148         /// <param name="value">The value to set</param>
    149         public static void SetIsTiltEnabled(DependencyObject source, bool value) { source.SetValue(IsTiltEnabledProperty, value); }
    150 
    151         /// <summary>
    152         /// Suppresses the tilt effect on a single control that would otherwise be tilted
    153         /// </summary>
    154         public static readonly DependencyProperty SuppressTiltProperty = DependencyProperty.RegisterAttached(
    155           "SuppressTilt",
    156           typeof(bool),
    157           typeof(TiltEffect),
    158           null
    159           );
    160 
    161         /// <summary>
    162         /// Gets the SuppressTilt dependency property from an object
    163         /// </summary>
    164         /// <param name="source">The object to get the property from</param>
    165         /// <returns>The property's value</returns>
    166         public static bool GetSuppressTilt(DependencyObject source) { return (bool)source.GetValue(SuppressTiltProperty); }
    167 
    168         /// <summary>
    169         /// Sets the SuppressTilt dependency property from an object
    170         /// </summary>
    171         /// <param name="source">The object to get the property from</param>
    172         /// <returns>The property's value</returns>
    173         public static void SetSuppressTilt(DependencyObject source, bool value) { source.SetValue(SuppressTiltProperty, value); }
    174 
    175 
    176         /// <summary>
    177         /// Property change handler for the IsTiltEnabled dependency property
    178         /// </summary>
    179         /// <param name="target">The element that the property is atteched to</param>
    180         /// <param name="args">Event args</param>
    181         /// <remarks>
    182         /// Adds or removes event handlers from the element that has been (un)registered for tilting
    183         /// </remarks>
    184         static void OnIsTiltEnabledChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
    185         {
    186             if (target is FrameworkElement)
    187             {
    188                 // Add / remove the event handler if necessary
    189                 if ((bool)args.NewValue == true)
    190                 {
    191                     (target as FrameworkElement).ManipulationStarted += TiltEffect_ManipulationStarted;
    192                 }
    193                 else
    194                 {
    195                     (target as FrameworkElement).ManipulationStarted -= TiltEffect_ManipulationStarted;
    196                 }
    197             }
    198         }
    199 
    200         #endregion
    201 
    202 
    203         #region Top-level manipulation event handlers
    204 
    205         /// <summary>
    206         /// Event handler for ManipulationStarted
    207         /// </summary>
    208         /// <param name="sender">sender of the event - this will be the tilt container (eg, entire page)</param>
    209         /// <param name="e">event args</param>
    210         static void TiltEffect_ManipulationStarted(object sender, ManipulationStartedEventArgs e)
    211         {
    212            
    213             TryStartTiltEffect(sender as FrameworkElement, e);
    214         }
    215 
    216         /// <summary>
    217         /// Event handler for ManipulationDelta
    218         /// </summary>
    219         /// <param name="sender">sender of the event - this will be the tilting object (eg a button)</param>
    220         /// <param name="e">event args</param>
    221         static void TiltEffect_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
    222         {
    223             
    224             ContinueTiltEffect(sender as FrameworkElement, e);
    225         }
    226 
    227         /// <summary>
    228         /// Event handler for ManipulationCompleted
    229         /// </summary>
    230         /// <param name="sender">sender of the event - this will be the tilting object (eg a button)</param>
    231         /// <param name="e">event args</param>
    232         static void TiltEffect_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
    233         {
    234             
    235             EndTiltEffect(currentTiltElement);
    236         }
    237 
    238         #endregion
    239 
    240 
    241         #region Core tilt logic
    242 
    243         /// <summary>
    244         /// Checks if the manipulation should cause a tilt, and if so starts the tilt effect
    245         /// </summary>
    246         /// <param name="source">The source of the manipulation (the tilt container, eg entire page)</param>
    247         /// <param name="e">The args from the ManipulationStarted event</param>
    248         static void TryStartTiltEffect(FrameworkElement source, ManipulationStartedEventArgs e)
    249         {
    250             foreach (FrameworkElement ancestor in (e.OriginalSource as FrameworkElement).GetVisualAncestors())
    251             {
    252                 foreach (Type t in TiltableItems)
    253                 {
    254                     if (t.IsAssignableFrom(ancestor.GetType()))
    255                     {
    256                         if ((bool)ancestor.GetValue(SuppressTiltProperty) != true)
    257                         {
    258                             // Use first child of the control, so that you can add transforms and not
    259                             // impact any transforms on the control itself
    260                             FrameworkElement element = VisualTreeHelper.GetChild(ancestor, 0) as FrameworkElement;
    261                             FrameworkElement container = e.ManipulationContainer as FrameworkElement;
    262 
    263                             if (element == null || container == null)
    264                                 return;
    265 
    266                             // Touch point relative to the element being tilted
    267                             Point tiltTouchPoint = container.TransformToVisual(element).Transform(e.ManipulationOrigin);
    268 
    269                             // Center of the element being tilted
    270                             Point elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
    271 
    272                             // Camera adjustment
    273                             Point centerToCenterDelta = GetCenterToCenterDelta(element, source);
    274 
    275                             BeginTiltEffect(element, tiltTouchPoint, elementCenter, centerToCenterDelta);
    276                             return;
    277                         }
    278                     }
    279                 }
    280             }
    281         }
    282 
    283         /// <summary>
    284         /// Computes the delta between the centre of an element and its container
    285         /// </summary>
    286         /// <param name="element">The element to compare</param>
    287         /// <param name="container">The element to compare against</param>
    288         /// <returns>A point that represents the delta between the two centers</returns>
    289         static Point GetCenterToCenterDelta(FrameworkElement element, FrameworkElement container)
    290         {
    291             Point elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
    292             Point containerCenter;
    293 
    294 #if WINDOWS_PHONE
    295 
    296             // Need to special-case the frame to handle different orientations
    297             if (container is PhoneApplicationFrame)
    298             {
    299                 PhoneApplicationFrame frame = container as PhoneApplicationFrame;
    300 
    301                 // Switch width and height in landscape mode
    302                 if ((frame.Orientation & PageOrientation.Landscape) == PageOrientation.Landscape)
    303                 {
    304                    
    305                     containerCenter = new Point(container.ActualHeight / 2, container.ActualWidth / 2);
    306                 }
    307                 else
    308                     containerCenter = new Point(container.ActualWidth / 2, container.ActualHeight / 2);
    309             }
    310             else
    311                 containerCenter = new Point(container.ActualWidth / 2, container.ActualHeight / 2);
    312 #else
    313 
    314             containerCenter = new Point(container.ActualWidth / 2, container.ActualHeight / 2);
    315 
    316 #endif
    317 
    318             Point transformedElementCenter = element.TransformToVisual(container).Transform(elementCenter);
    319             Point result = new Point(containerCenter.X - transformedElementCenter.X, containerCenter.Y - transformedElementCenter.Y);
    320            
    321             return result;
    322         }
    323 
    324         /// <summary>
    325         /// Begins the tilt effect by preparing the control and doing the initial animation
    326         /// </summary>
    327         /// <param name="element">The element to tilt </param>
    328         /// <param name="touchPoint">The touch point, in element coordinates</param>
    329         /// <param name="centerPoint">The center point of the element in element coordinates</param>
    330         /// <param name="centerDelta">The delta between the <paramref name="element"/>'s center and 
    331         /// the container's center</param>
    332         static void BeginTiltEffect(FrameworkElement element, Point touchPoint, Point centerPoint, Point centerDelta)
    333         {
    334            
    335 
    336             if (tiltReturnStoryboard != null)
    337                 StopTiltReturnStoryboardAndCleanup();
    338 
    339             if (PrepareControlForTilt(element, centerDelta) == false)
    340                 return;
    341 
    342             currentTiltElement = element;
    343             currentTiltElementCenter = centerPoint;
    344             PrepareTiltReturnStoryboard(element);
    345 
    346             ApplyTiltEffect(currentTiltElement, touchPoint, currentTiltElementCenter);
    347         }
    348 
    349         /// <summary>
    350         /// Prepares a control to be tilted by setting up a plane projection and some event handlers
    351         /// </summary>
    352         /// <param name="element">The control that is to be tilted</param>
    353         /// <param name="centerDelta">Delta between the element's center and the tilt container's</param>
    354         /// <returns>true if successful; false otherwise</returns>
    355         /// <remarks>
    356         /// This method is conservative; it will fail any attempt to tilt a control that already
    357         /// has a projection on it
    358         /// </remarks>
    359         static bool PrepareControlForTilt(FrameworkElement element, Point centerDelta)
    360         {
    361             // Prevents interference with any existing transforms
    362             if (element.Projection != null || (element.RenderTransform != null && element.RenderTransform.GetType() != typeof(MatrixTransform)))
    363                 return false;
    364 
    365             TranslateTransform transform = new TranslateTransform();
    366             transform.X = centerDelta.X;
    367             transform.Y = centerDelta.Y;
    368             element.RenderTransform = transform;
    369 
    370             PlaneProjection projection = new PlaneProjection();
    371             projection.GlobalOffsetX = -1 * centerDelta.X;
    372             projection.GlobalOffsetY = -1 * centerDelta.Y;
    373             element.Projection = projection;
    374 
    375             element.ManipulationDelta += TiltEffect_ManipulationDelta;
    376             element.ManipulationCompleted += TiltEffect_ManipulationCompleted;
    377 
    378             return true;
    379         }
    380 
    381         /// <summary>
    382         /// Removes modifications made by PrepareControlForTilt
    383         /// </summary>
    384         /// <param name="element">THe control to be un-prepared</param>
    385         /// <remarks>
    386         /// This method is basic; it does not do anything to detect if the control being un-prepared
    387         /// was previously prepared
    388         /// </remarks>
    389         static void RevertPrepareControlForTilt(FrameworkElement element)
    390         {
    391             element.ManipulationDelta -= TiltEffect_ManipulationDelta;
    392             element.ManipulationCompleted -= TiltEffect_ManipulationCompleted;
    393             element.Projection = null;
    394             element.RenderTransform = null;
    395         }
    396 
    397         /// <summary>
    398         /// Creates the tilt return storyboard (if not already created) and targets it to the projection
    399         /// </summary>
    400         /// <param name="projection">the projection that should be the target of the animation</param>
    401         static void PrepareTiltReturnStoryboard(FrameworkElement element)
    402         {
    403 
    404             if (tiltReturnStoryboard == null)
    405             {
    406                 tiltReturnStoryboard = new Storyboard();
    407                 tiltReturnStoryboard.Completed += TiltReturnStoryboard_Completed;
    408 
    409                 tiltReturnXAnimation = new DoubleAnimation();
    410                 Storyboard.SetTargetProperty(tiltReturnXAnimation, new PropertyPath(PlaneProjection.RotationXProperty));
    411                 tiltReturnXAnimation.BeginTime = TiltReturnAnimationDelay;
    412                 tiltReturnXAnimation.To = 0;
    413                 tiltReturnXAnimation.Duration = TiltReturnAnimationDuration;
    414 
    415                 tiltReturnYAnimation = new DoubleAnimation();
    416                 Storyboard.SetTargetProperty(tiltReturnYAnimation, new PropertyPath(PlaneProjection.RotationYProperty));
    417                 tiltReturnYAnimation.BeginTime = TiltReturnAnimationDelay;
    418                 tiltReturnYAnimation.To = 0;
    419                 tiltReturnYAnimation.Duration = TiltReturnAnimationDuration;
    420 
    421                 tiltReturnZAnimation = new DoubleAnimation();
    422                 Storyboard.SetTargetProperty(tiltReturnZAnimation, new PropertyPath(PlaneProjection.GlobalOffsetZProperty));
    423                 tiltReturnZAnimation.BeginTime = TiltReturnAnimationDelay;
    424                 tiltReturnZAnimation.To = 0;
    425                 tiltReturnZAnimation.Duration = TiltReturnAnimationDuration;
    426 
    427                 if (UseLogarithmicEase)
    428                 {
    429                     tiltReturnXAnimation.EasingFunction = new LogarithmicEase();
    430                     tiltReturnYAnimation.EasingFunction = new LogarithmicEase();
    431                     tiltReturnZAnimation.EasingFunction = new LogarithmicEase();
    432                 }
    433 
    434                 tiltReturnStoryboard.Children.Add(tiltReturnXAnimation);
    435                 tiltReturnStoryboard.Children.Add(tiltReturnYAnimation);
    436                 tiltReturnStoryboard.Children.Add(tiltReturnZAnimation);
    437             }
    438 
    439             Storyboard.SetTarget(tiltReturnXAnimation, element.Projection);
    440             Storyboard.SetTarget(tiltReturnYAnimation, element.Projection);
    441             Storyboard.SetTarget(tiltReturnZAnimation, element.Projection);
    442         }
    443 
    444 
    445         /// <summary>
    446         /// Continues a tilt effect that is currently applied to an element, presumably because
    447         /// the user moved their finger
    448         /// </summary>
    449         /// <param name="element">The element being tilted</param>
    450         /// <param name="e">The manipulation event args</param>
    451         static void ContinueTiltEffect(FrameworkElement element, ManipulationDeltaEventArgs e)
    452         {
    453             FrameworkElement container = e.ManipulationContainer as FrameworkElement;
    454             if (container == null || element == null)
    455                 return;
    456 
    457             Point tiltTouchPoint = container.TransformToVisual(element).Transform(e.ManipulationOrigin);
    458 
    459             // If touch moved outside bounds of element, then pause the tilt (but don't cancel it)
    460             if (new Rect(0, 0, currentTiltElement.ActualWidth, currentTiltElement.ActualHeight).Contains(tiltTouchPoint) != true)
    461             {
    462                
    463                 PauseTiltEffect();
    464                 return;
    465             }
    466 
    467             // Apply the updated tilt effect
    468             ApplyTiltEffect(currentTiltElement, e.ManipulationOrigin, currentTiltElementCenter);
    469         }
    470 
    471         /// <summary>
    472         /// Ends the tilt effect by playing the animation  
    473         /// </summary>
    474         /// <param name="element">The element being tilted</param>
    475         static void EndTiltEffect(FrameworkElement element)
    476         {
    477             if (element != null)
    478             {
    479                 element.ManipulationCompleted -= TiltEffect_ManipulationCompleted;
    480                 element.ManipulationDelta -= TiltEffect_ManipulationDelta;
    481             }
    482 
    483             if (tiltReturnStoryboard != null)
    484             {
    485                 wasPauseAnimation = false;
    486                 if (tiltReturnStoryboard.GetCurrentState() != ClockState.Active)
    487                     tiltReturnStoryboard.Begin();
    488             }
    489             else
    490                 StopTiltReturnStoryboardAndCleanup();
    491         }
    492 
    493         /// <summary>
    494         /// Handler for the storyboard complete event
    495         /// </summary>
    496         /// <param name="sender">sender of the event</param>
    497         /// <param name="e">event args</param>
    498         static void TiltReturnStoryboard_Completed(object sender, EventArgs e)
    499         {
    500             if (wasPauseAnimation)
    501                 ResetTiltEffect(currentTiltElement);
    502             else
    503                 StopTiltReturnStoryboardAndCleanup();
    504         }
    505 
    506         /// <summary>
    507         /// Resets the tilt effect on the control, making it appear 'normal' again 
    508         /// </summary>
    509         /// <param name="element">The element to reset the tilt on</param>
    510         /// <remarks>
    511         /// This method doesn't turn off the tilt effect or cancel any current
    512         /// manipulation; it just temporarily cancels the effect
    513         /// </remarks>
    514         static void ResetTiltEffect(FrameworkElement element)
    515         {
    516             PlaneProjection projection = element.Projection as PlaneProjection;
    517             projection.RotationY = 0;
    518             projection.RotationX = 0;
    519             projection.GlobalOffsetZ = 0;
    520         }
    521 
    522         /// <summary>
    523         /// Stops the tilt effect and release resources applied to the currently-tilted control
    524         /// </summary>
    525         static void StopTiltReturnStoryboardAndCleanup()
    526         {
    527             if (tiltReturnStoryboard != null)
    528                 tiltReturnStoryboard.Stop();
    529 
    530             RevertPrepareControlForTilt(currentTiltElement);
    531         }
    532 
    533         /// <summary>
    534         /// Pauses the tilt effect so that the control returns to the 'at rest' position, but doesn't
    535         /// stop the tilt effect (handlers are still attached, etc.)
    536         /// </summary>
    537         static void PauseTiltEffect()
    538         {
    539             if ((tiltReturnStoryboard != null) && !wasPauseAnimation)
    540             {
    541                 tiltReturnStoryboard.Stop();
    542                 wasPauseAnimation = true;
    543                 tiltReturnStoryboard.Begin();
    544             }
    545         }
    546 
    547         /// <summary>
    548         /// Resets the storyboard to not running
    549         /// </summary>
    550         private static void ResetTiltReturnStoryboard()
    551         {
    552             tiltReturnStoryboard.Stop();
    553             wasPauseAnimation = false;
    554         }
    555 
    556         /// <summary>
    557         /// Applies the tilt effect to the control
    558         /// </summary>
    559         /// <param name="element">the control to tilt</param>
    560         /// <param name="touchPoint">The touch point, in the container's coordinates</param>
    561         /// <param name="centerPoint">The center point of the container</param>
    562         static void ApplyTiltEffect(FrameworkElement element, Point touchPoint, Point centerPoint)
    563         {
    564             // Stop any active animation
    565             ResetTiltReturnStoryboard();
    566 
    567             // Get relative point of the touch in percentage of container size
    568             Point normalizedPoint = new Point(
    569                 Math.Min(Math.Max(touchPoint.X / (centerPoint.X * 2), 0), 1),
    570                 Math.Min(Math.Max(touchPoint.Y / (centerPoint.Y * 2), 0), 1));
    571 
    572             // Shell values
    573             double xMagnitude = Math.Abs(normalizedPoint.X - 0.5);
    574             double yMagnitude = Math.Abs(normalizedPoint.Y - 0.5);
    575             double xDirection = -Math.Sign(normalizedPoint.X - 0.5);
    576             double yDirection = Math.Sign(normalizedPoint.Y - 0.5);
    577             double angleMagnitude = xMagnitude + yMagnitude;
    578             double xAngleContribution = xMagnitude + yMagnitude > 0 ? xMagnitude / (xMagnitude + yMagnitude) : 0;
    579 
    580             double angle = angleMagnitude * MaxAngle * 180 / Math.PI;
    581             double depression = (1 - angleMagnitude) * MaxDepression;
    582 
    583             // RotationX and RotationY are the angles of rotations about the x- or y-*axis*;
    584             // to achieve a rotation in the x- or y-*direction*, we need to swap the two.
    585             // That is, a rotation to the left about the y-axis is a rotation to the left in the x-direction,
    586             // and a rotation up about the x-axis is a rotation up in the y-direction.
    587             PlaneProjection projection = element.Projection as PlaneProjection;
    588             projection.RotationY = angle * xAngleContribution * xDirection;
    589             projection.RotationX = angle * (1 - xAngleContribution) * yDirection;
    590             projection.GlobalOffsetZ = -depression;
    591         }
    592 
    593         #endregion
    594 
    595 
    596         #region Custom easing function
    597 
    598         /// <summary>
    599         /// Provides an easing function for the tilt return
    600         /// </summary>
    601         private class LogarithmicEase : EasingFunctionBase
    602         {
    603             /// <summary>
    604             /// Computes the easing function
    605             /// </summary>
    606             /// <param name="normalizedTime">The time</param>
    607             /// <returns>The eased value</returns>
    608             protected override double EaseInCore(double normalizedTime)
    609             {
    610                 return Math.Log(normalizedTime + 1) / 0.693147181; // ln(t + 1) / ln(2)
    611             }
    612         }
    613 
    614         #endregion
    615     }
    616 
    617     /// <summary>
    618     /// Couple of simple helpers for walking the visual tree
    619     /// </summary>
    620     static class TreeHelpers
    621     {
    622         /// <summary>
    623         /// Gets the ancestors of the element, up to the root
    624         /// </summary>
    625         /// <param name="node">The element to start from</param>
    626         /// <returns>An enumerator of the ancestors</returns>
    627         public static IEnumerable<FrameworkElement> GetVisualAncestors(this FrameworkElement node)
    628         {
    629             FrameworkElement parent = node.GetVisualParent();
    630             while (parent != null)
    631             {
    632                 yield return parent;
    633                 parent = parent.GetVisualParent();
    634             }
    635         }
    636 
    637         /// <summary>
    638         /// Gets the visual parent of the element
    639         /// </summary>
    640         /// <param name="node">The element to check</param>
    641         /// <returns>The visual parent</returns>
    642         public static FrameworkElement GetVisualParent(this FrameworkElement node)
    643         {
    644             return VisualTreeHelper.GetParent(node) as FrameworkElement;
    645         }
    646     }
    647 }
    648