Animated Numbers & Counters

Introduction

Some videos show a clock progessing in time, a countdown timer counting to a number, or other visual elements that animate their numerical text. Often you see a growing visual (like a rectangle in a bar chart, or a line in a line chart) alongside with the value being animated.

While it easy to create those in After Effects when the value and animation parameters are known, it becomes more difficult when you need these elements in a personalized video. The start and end values are not known ahead of time.

With ImpossbleFX you can create dynamic counters and numbers that get their start and/or end value supplied with the render request.

You can create counters that display the numbers as text, or use a bunch of images or short videos to create an animated clock, such as a "Flip Clock".

Counter Variables

When you display text in a Dynamic Movie, the text value is supplied by a StringVariable. Usually you use these StringVariables to get text data from the parameters in the render request ("map") or use a fixed value ("constant").

Counter variables are a special type of String Variables that compute their "String" value from a given start and end value, animated over time (type "counter"). Start and end values can be constant or dynamic and the final value can be formatted as number or a floating point value. Additional the value can be a time-based value, such as a second, minute or hour and can be displayed as a time code.

Additionally, a animation function can be supplied if you want the animation progress non-linear, e.g. to ease in or easy out the speed of which the counter is progressing.

Basic Movie Setup

public class Counters {
    FXTemplateClient fx;

    public final String prjuid = "YOUR PROJECT ID";

    Counters() {
        fx = FXTemplateClient.builder()
                .withProvider(new StaticCredentialsProvider("YOUR API KEY", "YOUR API SECRET"))
                .withRegion("YOUR REGION")
                .build();
    }

    FXProto.Movie buildMovie() {
        return FXProto.Movie.newBuilder()
            .setParams(FXProto.StreamParams.newBuilder()
            .setVparams(FXProto.VideoParams.newBuilder()
                .setWidth(320)
                .setHeight(180)
                .setVideoframerate(FXProto.Fractional.newBuilder()
                    .setNum(25))))
            .addScenes(FXProto.Scene.newBuilder()
                .setNumframes(100)
                .addTracks(FXProto.VisualTrack.newBuilder()
                    .setContent(FXProto.ImageProvider.newBuilder()
                        .setType(FXProto.ImageProvider.Type.textsimple)
                        .setXalignment(FXProto.ImageProvider.TextAlignment.centered)
                        .setYalignment(FXProto.ImageProvider.TextPosition.middle)
                        .setText(createXXXXXXCounter()) ))
            .build();
    }

    public static void main(String[] args) throws Exception{
        Counters counters = new Counters();
        FXProto.Movie movie = counters.buildMovie();
        String message = counters.fx.createTemplate(prjuid, "counter_example", movie.toByteArray());
        System.out.println(message);
    }
}
FX.config.apiVersion = '2017-01-09';
FX.config.region = 'eu-west-1';
var sdl = new FX.SDL()
var project = new FX.Project({params: {ProjectId: 'YOUR PROJEVCT ID'}});


function buildMovie(counter) {
  return new sdl.Movie({
    params: {
      vparams: {
        width: 320,
        height: 180,
        videoframerate: {num: 25}
      }
    },
    scenes: [{
        numframes: 100,
        tracks: [{  
          content: {
            type: "textsimple",
            color: {green: 255, alpha: 255},
            xalignment: "centered",
            yalignment: "middle",
            text: counter()
          }
        }]
    }]
  });
}
{
    "scenes": [
        {
            "numframes": 100,
            "tracks": [
                {
                    "content": {
                        "xalignment": "centered",
                        "type": "textsimple",
                        "color": {
                            "green": 255,
                            "alpha": 255
                        },
                        "yalignment": "middle",
                        "text": {
                            ... see various counters below ...
                        }
                    }
                }
            ]
        }
    ],
    "params": {
        "vparams": {
            "width": 320,
            "videoframerate": {
                "num": 25
            },
            "height": 180,
            "videocodec": "VIDEO_X264"
        }
    }
}

For this tutorial we create a basic dynamic movie, which we will use over and over to explain the various types of the "counter" StringVariable.

For this we create a movie with 320x180 resolution and 25 frames per seconds, then a single scene with 100 fixed frames is added. To that scene we add a single Visual Track that displays a text centered on the frame.

The actual text will be supplied by a function named createCounter() (see above).

Finally we send the template to the service to create a dynamic movie, named "counter_example_01".

We will use other names for the examples further down, but we keep the basic setup and just create new "createXXXCounter" functions.

A Frame Counter


function constant(value) {
  return new sdl.StringVariable({
   type: "constant",
   value: "" + value
  });
}

function createFrameCounter() {
 return new sdl.StringVariable({
   type: "counter",
   counterstart: constant(0),
   counterend: constant(100),
   counterformat: constant("%05d")
 });
}
FXProto.StringVariable constant(Object value) {
    return FXProto.StringVariable.newBuilder()
            .setType(FXProto.StringVariable.Type.constant)
            .setValue(value.toString())
            .build();
}

FXProto.StringVariable createFrameCounter() {
    return FXProto.StringVariable.newBuilder()
            .setType(FXProto.StringVariable.Type.counter)
            .setCounterstart(constant(0))
            .setCounterend(constant(100))
            .setCounterformat(constant("%05d"))
            .build();
}
"text": {
    "type": "counter",
    "counterstart": {
        "type": "constant",
        "value": "0"
    },
    "counterend": {
        "type": "constant",
        "value": "100"
    },
    "counterformat": {
        "type": "constant",
        "value": "%05d"
    }
}

Let's start with a simple 100 frame movie in which a text shows the current frame number.

In this example the start and end value are constant, and the format of the string is a 5 digit number with leading zeros. Obviously, for a frame counter, that start and end values must correspond to the frame number, and the animation progresses linearly, with no easing.

constant() is a helper function that returns a StringVariable of type constant, with the value initialized to string representation of the supplied argument value.

The resulting videos looks like this (place the mouse over the image to show video controls):

A Time Code Counter

FXProto.StringVariable createTimeCodeCounter() {
    return FXProto.StringVariable.newBuilder()
            .setType(FXProto.StringVariable.Type.counter)
            .setCounterstart(constant(0))
            .setCounterend(constant(4))
            .setCountertimeunit(FXProto.StringVariable.TimeUnit.TU_SECOND)
            .setCounterformattype(FXProto.StringVariable.CounterFormat.FORMAT_TIME)
            .setCounterformat(constant("%02h:%02m:%02s.%03l"))
            .build();
}
function createTimecodeCounter() {
 return new sdl.StringVariable({
   type: "counter",
   counterstart: constant(0),
   counterend: constant(4),
   countertimeunit: "TU_SECOND",
   counterformattype: "FORMAT_TIME",
   counterformat: constant("%02h:%02m:%02s.%03l")
 });
}
"text": {
    "counterformat": {
        "type": "constant",
        "value": "%02h:%02m:%02s.%03l"
    },
    "type": "counter",
    "counterstart": {
        "type": "constant",
        "value": "0"
    },
    "counterend": {
        "type": "constant",
        "value": "4"
    },
    "counterformattype": "FORMAT_TIME"
}

A variation of the frame counter is a timecode display. Here the unit is not a plain integer, but a time dimension (e.g. seconds or minutes). When calculating a value the system handles the overflow from 59 seconds to 1 minute correctly.

The time code can be displayed in various formats, formatted by a time format string. The following time lists the formatting options:

Format Description
%l Milliseconds (bounded by seconds)
%L Milliseconds (unbounded)
%s Seconds (bounded by minutes)
%S Seconds (unbounded)
%m Minutes (bounded by minutes)
%M Minutes (unbounded)
%h Hours (bounded by minutes)
%H Hours (unbounded)
%D Days (unbounded)

The resulting videos looks like this:

Dynamic Counter

FXProto.StringVariable getVariable(String name, Object defaultvalue) {
    return FXProto.StringVariable.newBuilder()
            .setType(FXProto.StringVariable.Type.map)
            .setKey(name)
            .setDefaultvalue(defaultvalue.toString())
            .build();
}

FXProto.StringVariable createDynamicCounter() {
    return FXProto.StringVariable.newBuilder()
            .setType(FXProto.StringVariable.Type.counter)
            .setCounterstart(getVariable("start", 0))
            .setCounterend(getVariable("end", 100))
            .setCounterformat(constant("%d"))
            .build();
}
function getVariable(name, defaultvalue) {
  return new sdl.StringVariable({
   type: "map",
   key: name, 
   defaultvalue: "" + defaultvalue
  });
}

function createDynamicCounter() {
 return new sdl.StringVariable({
   type: "counter",
   counterstart: getVariable("start", 0),
   counterend: getVariable("end", 100),
   counterformat: constant("%d")
 });
}
"text": {
    "type": "counter",
    "counterstart": {
        "type": "map",
        "key": "start",
        "defaultvalue": "0"
    },
    "counterend": {
        "type": "map",
        "key": "end",
        "defaultvalue": "100"
    },
    "counterformat": {
        "type": "constant",
        "value": "%d"
    }
}

So far we have used fixed values for our counters. However, it just requires a single change to go from constant values to runtime supplied user values. Instead of using the constant type StringVariable, we switch it to the map type. This StringVariable requires a name (key) and optionally a default value (defaultvalue) to use when there the parameter is missing in the render request.

As we did with the constant() helper function we create another helper called getVariable(name, defaultvalue) to supply a StringVariable, which we feed into the counter.

Below you can watch videos where the start end end values are supplied by a HTML form.



Animation Easing

FXProto.StringVariable createEaseOutCounterKeyframeAnimation() {
    return createCounter().toBuilder()
            .setCounterfunction(FXProto.Function.newBuilder()
                .setType(FXProto.Function.Type.keyframe)
                .addKeyframes(FXProto.Point.newBuilder().setX(0.0).setY(1.00).build())
                .addKeyframes(FXProto.Point.newBuilder().setX(0.2).setY(0.63).build())
                .addKeyframes(FXProto.Point.newBuilder().setX(0.4).setY(0.36).build())
                .addKeyframes(FXProto.Point.newBuilder().setX(0.6).setY(0.16).build())
                .addKeyframes(FXProto.Point.newBuilder().setX(0.8).setY(0.04).build())
                .addKeyframes(FXProto.Point.newBuilder().setX(1.0).setY(0.00).build()))
            .build();
}

"text": {
    "type": "counter",
    "counterstart": {
        "type": "constant",
        "value": "0"
    },
    "counterend": {
        "type": "constant",
        "value": "100"
    },
    "counterfunction": {
        "type": "keyframe",
        "advancedkeyframes": [
            { "time": 0,  "value": 1 },
            {"time": 0.2, "value": 0.63 },
            {"time": 0.4, "value": 0.36 },
            {"time": 0.6, "value": 0.16 },
            {"time": 0.8, "value": 0.04 },
            {"time":  1,  "value": 0 }
        ]
    },
    "counterformat": {
        "type": "constant",
        "value": "%05d"
    }
}

Up to now the counters progressed linearly in time. All counters have a default animation function which is "linear".

You can use on of the predefined functions or create your own either via keyframe animations or by function composition using various compunding functions.

One the more often used animation is a "Ease Out" function where the speed of the animation is slowing down towards the end of the animation. We re-use our regular counter from the first example and add an animation function.

Ease out (Keyframe Animation)

Ease out (Function Composition)

FXProto.StringVariable createEaseOutCounter() {
    return createCounter().toBuilder()
        .setCounterfunction(FXProto.Function.newBuilder()
            .setType(FXProto.Function.Type.inverted)
            .setInnerfunction(FXProto.Function.newBuilder()
                .setType(FXProto.Function.Type.power)
                .setParam1(2.0)))
        .build();
}

"text": {
    "type": "counter",
    "counterstart": {
        "type": "constant",
        "value": "0"
    },
    "counterend": {
        "type": "constant",
        "value": "100"
    },
    "counterfunction": {
        "type": "inverted",
        "innerfunction": {
            "type": "power",
            "param1": 2
        }
    },
    "counterformat": {
        "type": "constant",
        "value": "%05d"
    }
}

Terms of Use | © 2016, Impossible Software, or its affiliates. All rights reserved.