Best animation questions in January 2011

How to create a snowstorm on your Windows desktop?

11 votes

Practical uses aside, how (if it is possible at all) could you create a "snowing" effect on your desktop PC running Windows? Preferably with nothing but raw C/C++ and WinAPI.

The requirements for the snow are:

  • Appears over everything else shown (Note: always-on-top windows may get on top of snow still, that's OK. I understand that there can be no "absolute on top" flag for any app)
  • Snowflakes are small, possibly simple dots or clusters of a few white pixels;
  • Does not bother working with the computer (clicking a snowflake sends the click through to the underlying window);
  • Plays nicely with users dragging windows;
  • Multi-monitor capable.

Bonus points for any of the following features:

  • Snow accumulates on the lower edge of the window or the taskbar (if it's at the bottom of the screen);
  • Snow accumulates also on top-level windows. Or perhaps some snow accumulates, some continues down, accumulating on every window with a title bar;
  • Snow accumulated on windows gets "shaken off" when windows are dragged;
  • Snow accumulated on taskbar is aware of the extended "Start" button under Vista/7.
  • Snowflakes have shadows/outlines, so they are visible on white backgrounds;
  • Snowflakes have complex snowflike-alike shapes (they must still be tiny).
  • Clicking on a snowflake does send the click through to the underlying window, but the snowflake evaporates with a little cool animation;

Most of these effects are straightforward enough, except the part where snow is click-through and plays nicely with dragging of windows. In my early days I've made an implementation that draws on the HDC you get from GetDesktopWindow(), which was click-through, but had problems with users dragging windows (snowflakes rendered on them got "dragged along").

The solution may use Vista/7 Aero features, but, of course, a universal solution is preferred. Any ideas?

For the sake of brevity and simplicity, this answer has been trimmed to a limited set of requirements. It is trivial to expand this and make it more robust.

This answer uses WPF on Windows XP. It should work on up to 2 monitors, and should work on other Windows systems as well.

It starts with a simple window:

<Window x:Class="TestDump.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" WindowStartupLocation="Manual" Loaded="Window_Loaded"
WindowStyle="None" AllowsTransparency="True" Background="Transparent"
>
    <Grid x:Name="FieldOfSnow"/>
</Window>

To this window, we will add Snowflakes defined as follows:

<UserControl x:Class="TestDump.SnowFlake"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="5" Width="5">
    <Border Background="White" CornerRadius="2" BorderThickness="1" BorderBrush="LightGray"/>
</UserControl>

The snowflakes have the default UserControl code-behind with no changes.

Finally, the Window CodeBehind

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Runtime.InteropServices;
using System.Windows.Interop;

namespace TestDump
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        private TimeSpan _lastRender;

        public Window1()
        {
            InitializeComponent();
            _lastRender = TimeSpan.FromTicks(0);
            CompositionTarget.Rendering += SnowflakeTick;
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            this.Topmost = true;
            this.Top = 0;
            this.Left = 0;

            this.Width = System.Windows.SystemParameters.PrimaryScreenWidth;
            this.Height = System.Windows.SystemParameters.PrimaryScreenHeight;

            if (System.Windows.Forms.SystemInformation.MonitorCount == 2)
            {
                System.Drawing.Rectangle SecondScreenArea = System.Windows.Forms.Screen.AllScreens[1].Bounds;

                this.Width += SecondScreenArea.Width;
                this.Height = this.Height > SecondScreenArea.Height ? this.Height : SecondScreenArea.Height;
            }
        }

        public const int WS_EX_TRANSPARENT = 0x00000020;
        public const int GWL_EXSTYLE = (-20);

        [DllImport("user32.dll")]
        public static extern int GetWindowLong(IntPtr hwnd, int index);

        [DllImport("user32.dll")]
        public static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

        protected override void OnSourceInitialized(EventArgs e)
        {
            base.OnSourceInitialized(e);

            // Get this window's handle
            IntPtr hwnd = new WindowInteropHelper(this).Handle;

            // Change the extended window style to include WS_EX_TRANSPARENT
            int extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
            SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TRANSPARENT);
        }

        List<TranslateTransform> Flakes = new List<TranslateTransform>();
        Random rand = new Random();

        private void SnowflakeTick(object sender, EventArgs e)
        {
            RenderingEventArgs renderingArgs = (RenderingEventArgs)e;
            TimeSpan dTime = (renderingArgs.RenderingTime - _lastRender);
            double deltaTime = dTime.TotalMilliseconds;
            _lastRender = renderingArgs.RenderingTime;

            if ( _lastRender.Milliseconds < deltaTime)
            {
                TranslateTransform SnowPos = new TranslateTransform(this.Width * rand.Next(1000) / 1000.0 - this.Width/2, -this.Height/2);

                SnowFlake sf = new SnowFlake();
                sf.RenderTransform = SnowPos;

                // The flakes are centered when added, so all positions must be translated with this in mind.
                FieldOfSnow.Children.Add(sf);
                Flakes.Add(SnowPos);
            }

            foreach (TranslateTransform Flake in Flakes)
            {
                double ScreenHeight = this.Height / 2 - 2;

                if (Flake.Y < ScreenHeight)
                {
                    Flake.Y += deltaTime / 50;
                }
            }
        }
    }
}

I had to use a bit of forms code to get the multiscreen stuff, and I had to include the references to the Assemblies in my project.

I haven't tested it much, but it does work on my system, and the snow sits at the bottom of the screen when done.

I used this reference for the click through behavior.

A more dedicated fellow than I should be able to adapt this, plus some edge detection to the task of getting the snow to sit elsewhere.

Please be aware that snowflakes are never cleaned up in this example, and after running it long enough, you may notice some slowdown.

Have Fun!

10 votes

404 Page or 500 Page

Anyone have any idea how to do this sort of thing? The animation that moves with your mouse? Thanks for the correction, @Alin. Just a link to a tutorial would be nice.

EDIT: Just also learned it's the parallax effect. That should help.

The effect is accomplished with javascript, not just CSS.

The source code is on the page you linked to.

Have a look at jParallax, which makes it easy to implement the effect in a robust way on your own site: http://webdev.stephband.info/parallax.html

Acceleration/Deceleration Ratio equivalent with KeyFrame

6 votes

Is there a formula to translate AccelerationRatio or DecelerationRatio to Bezier control points for use in a KeySpline in a SplineDoubleKeyFrame? For example, an "Ease Out" may be DecelerationRatio=0.5, but this doesn't seem equivalent to KeySpline="0.0,0.0 0.5,1.0" or KeySpline="0.5,0 1,0.5".

Would this involve multiple SplineDoubleKeyFrame to achieve DecelerationRatio=0.5? Or is their a particular formula that makes them equivalent in a single frame?

Or is this not to be achieved via a SplineDoubleKeyFrame but a EasingDoubleKeyFrame instead (if so, what is the EasingFunction/EasingMode/Other Attributes)?

Essentially, I'm trying to achieve <DoubleAnimation Storyboard.TargetName="deceleratedRectangle" Storyboard.TargetProperty="(Rectangle.Width)" DecelerationRatio="0.5" Duration="0:0:10" From="20" To="400" /> with KeyFrames as there will be multiple frames that target the same property path and accelerate/decelerate it.


UPDATE: According to the Microsoft WPF-Silverlight Comparison Whitepaper.pdf on page 7:

The linear interpolation can be modified somewhat by adding AccelerationRatio and DecelerationRatio properties to the animation. These attributes essentially create three linear interpolations for the entire animation in order to modify the starting and stopping speeds. For example, a designer would use these attributes to have an object gradually pick up speed or stop suddenly. Unfortunately, Silverlight does not implement these two attributes, but the effect can be duplicated using keyframe animations with linear interpolation.

So I guess this means it can be done with just 3 key frames, but what the formula is I have no idea.


SOLUTION: For others that may come along that need this, posting the ECMAScript solution crafted by Peter Taylor:

<html>
    <head>
        <title>Acceleration or deceleration with Bezier splines</title>
        <script type="text/javascript"><!--
            function calcBezier() {
                var res = new Array();
                var y0 = +document.getElementById("y0").value;
                var y1 = +document.getElementById("y1").value;
                var t0 = +document.getElementById("t0").value;
                var t1 = +document.getElementById("t1").value;
                var ra = +document.getElementById("ra").value;
                var rd = +document.getElementById("rd").value;

                var ta = t0 + ra * (t1 - t0);
                if (ra > 0) res.push("ta = " + ta + "<br />");
                var td = t1 - rd * (t1 - t0);
                if (rd > 0) res.push("td = " + td + "<br />");

                var vm = 2 * (y1 - y0) / (t1 + td - ta - t0);
                res.push("vm = " + vm + "<br />");

                var ya = y0 + vm * (ta - t0) / 2
                if (ra > 0) res.push("ya = " + ya + "<br />");
                var yd = y1 - vm * (t1 - td) / 2
                if (rd > 0) res.push("yd = " + yd + "<br />");

                res.push('&lt;DiscreteDoubleKeyFrame KeyTime="'+t0+'" Value="'+y0+'" /&gt;<br />');

                if (ra > 0) {
                    // y - ya = (t - ta) * vm => t = ta + (y - ya) / vm
                    var p1t = ta - (ya - y0) / vm;
                    // Scale for spline params: (t0,y0):(0,0) and (ta,ya):(1,1)
                    p1t = (p1t - t0) / (ta - t0);
                    // Lift to cubic.
                    res.push('&lt;SplineDoubleKeyFrame KeyTime="'+ta+'" Value="'+ya+'" KeySpline="'+((2*p1t)/3)+',0 '+((2*p1t+1)/3)+','+(1/3)+'" /&gt;<br />');
                }

                if (ra + rd < 1) {
                    res.push('&lt;LinearDoubleKeyFrame KeyTime="'+td+'" Value="'+yd+'" /&gt;<br />');
                }

                if (rd > 0) {
                    var q1y = 1;
                    var q1t = td + (y1 - yd) / vm;
                    q1t = (q1t - td) / (t1 - td);
                    res.push('&lt;SplineDoubleKeyFrame KeyTime="'+t1+'" Value="'+y1+'" KeySpline="'+((2*q1t)/3)+','+(2/3)+' '+((2*q1t+1)/3)+',1" /&gt;<br />');
                }

                document.getElementById("results").innerHTML = res.join("");
            }
        //-->
        </script>
    </head>
    <body>
        <p>Interpolate y from <input id="y0" /> to <input id="y1" />.</p>
        <p>Start time: <input id="t0" />; end time: <input id="t1" />.</p>
        <p>Acceleration ratio: <input id="ra" />; deceleration ratio: <input id="rd" />.</p>
        <p><input type="submit" value="Calculate" onclick="calcBezier();" /></p>
        <p id="results">&nbsp;</p>
    </body>
</html>

General solution

Update: using the geometric interpretation of calculus as the area under a line, I've worked out how to simplify the derivation a lot.

So we're interpolating from y0 at time t0 to y1 at time t1 with acceleration ratio ra and deceleration ratio rd. The definition of the ratios give us the time at which we stop accelerating, ta = t0 + ra * (t1 - t0), and at which we start decelerating, td = t1 - rd * (t1 - t0).

I understand the documentation you quote to mean that it's constant acceleration from t0 to ta, and constant deceleration from td to t1. We'll take the maximum speed reached to be vm.

 Speed
   |        _____________________________________
vm +       /|                                   |\
   |      /                                       \
   |     /  |                                   |  \
   |    /                                           \
   |   /    |                                   |    \
   |  /                                               \
   | /      |                                   |      \
   |/                                                   \
   +--------+-----------------------------------+--------+---- Time
   t0       ta                                  td       t1

Then the area of the parallelogram is the distance travelled from t0 to t1, which is y1 - y0. The area of a parallelogram is the product of the height with the average of the parallel sides. So

y1 - y0 = vm * ((t1 - t0) + (td - ta)) / 2

or

vm = 2 * (y1 - y0) / (t1 + td - ta - t0)

Using just the area of the triangles at the end, we can find how far we've travelled when we stop accelerating, ya = y(ta), and when we start decelerating, yd = y(td).

ya = y0 + vm * (ta - t0) / 2

yd = y1 - vm * (t1 - td) / 2

Finally we produce a quadratic Bezier for [t0, ta], a straight line (ta, ya) - (td, yd), and a quadratic Bezier for [td, t1].

For the first Bezier, we have the obvious control points P0 = (t0, y0) and P2 = (ta, ya). To find P1 we use the property that P0-P1 is tangent to the curve and P1-P2 is tangent to the curve (in general, for an order-n curve P0-P1 and P(n-1)-Pn are tangent). So P1 is at the intersection of y=y0 and the straight line of the middle segment. Similarly for the other Bezier: Q0 = (td, yd), Q2 = (t1, y1), and Q1 is at the intersection of y=y1 and the straight line of the middle segment.


Worked example:

No fade in (acceleration ratio = 0), deceleration ratio = 0.5, t0 = 0, t1 = 10 (seconds), y0 = 20, y1 = 400. I think this corresponds to your specific question.

ta = 0 (and we can omit the first quadratic Bezier); td = t1 - 0.5 * (t1 - t0) = 5.

vm = 2 * (y1 - y0) / (t1 + td - ta - t0) = 2 * (400 - 20) / (10 + 5 - 0 - 0) = 2 * 380 / 15 = 152 / 3 ~= 50.67.

Ignore ya because we're skipping that Bezier.

yd = y1 - vm * (t1 - td) / 2 = 400 - 152/3 * (10-5)/2 = 400 - 380/3 = 820/3 ~= 273.3

So the straight line goes from (t=0, y=20) to (t=5, y=273.3). The deceleration Bezier has Q0 = (5, 273.3), Q2 = (10, 400).

To find Q1 we extend the straight line to y=400. The line has equation y - 20 = (t - 0) * (273.3 - 20) / (5 - 0), so t = 5 * (400 - 20) / (273.3 - 20) = 7.5.

So we have straight line (0,20)-(5,273.3) and a quadratic Bezier with control points (5,273.3), (7.5,400) and (10,400).

Translating this into your keyframe

However, there's a slight hitch, which is that Microsoft hasn't deigned to give us quadratic splines. We have to lift the quadratic Q0, Q1, Q2, to the cubic Q0, (Q0 + 2 Q1) /3, (2 Q1 + Q2) / 3, Q2.

We also have to rescale the control points to 0-1. If we apply that rescaling first, we have (0,0)-(0.5,1)-(1,1). So the cubic is (0,0)-(0.333,0.667)-(0.667,1)-(1,1).

I know splines, but not WPF. I think that the following will do what you want:

<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="20" />
<LinearDoubleKeyFrame KeyTime="0:0:5" Value="273.333" />
<SplineDoubleKeyFrame KeyTime="0:0:10" Value="400" KeySpline="0.333,0.667,0.667,1"/>

Universality of the transformation

Rescaling the first Bezier, we map (t0, y0) to (0, 0) and (ta, ya) to (1, 1). Therefore we map (P1.t, P1.y) to ((P1.t - t0) / (ta - t0), (P1.y - y0) / (ya - y0)). But P1 is at the intersection of y = y0 with the straight line of gradient vm through (ta, ya), which therefore has equation (y - ya) = (t - ta) * vm. So P1.y = y0 and P1.t = ta + (y0 - ya) / vm = ta - (ya - y0) / vm. Plugging in our identity ya = y0 + vm * (ta - t0) / 2, we get P1.t = ta - (vm * (ta - t0) / 2) / vm = ta - (ta - t0) / 2 = (ta + t0) / 2.

So rescaling we map P1 to (0.5, 0). Therefore when we lift it to a cubic Bezier, the intermediate control points are always at (1/3, 0) and (2/3, 1/3).

Similarly the deceleration spline always has its scaled intermediate points at (1/3, 2/3) and (2/3, 1).