Skip to content

Commit

Permalink
render/animation: Add animation.Transformation widget. (#222)
Browse files Browse the repository at this point in the history
The animation.Transformation widget animates a child widget by interpolating between transforms (translate, scale, rotate) using keyframes similar to how CSS transforms and animations work.
  • Loading branch information
fxb authored May 18, 2022
1 parent d4c3e7d commit 00af67e
Show file tree
Hide file tree
Showing 43 changed files with 2,477 additions and 23 deletions.
163 changes: 163 additions & 0 deletions docs/animation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@
Pixlet supports a few animation primitives. These are used to animate
widgets from frame to frame.

All animations allow specifying an easing curve. This can either be one
of the built-in "linear", "ease_in", "ease_out" or "ease_in_out" curves,
a custom cubic bézier curve in the form "cubic-bezier(a, b, c, d)" or a
custom easing function.

**Warning**: The animation module is in a state of flux. Especially
`Transformation` and related classes are likely to change in the near
term. Please be on the lookout for bugs, issues and potential
improvements!


## AnimatedPositioned
Animate a widget from start to end coordinates.

**DEPRECATED**: Please use `animation.Transformation` instead.

#### Attributes
| Name | Type | Description | Required |
| --- | --- | --- | --- |
Expand All @@ -24,3 +36,154 @@ Animate a widget from start to end coordinates.



## Keyframe
A keyframe defining specific point in time in the animation.

The keyframe _percentage_ can is expressed as a floating point value between `0.0` and `1.0`.

#### Attributes
| Name | Type | Description | Required |
| --- | --- | --- | --- |
| `percentage` | `float` | Percentage of the time at which this keyframe occurs through the animation. | **Y** |
| `transforms` | `[Transform]` | List of transforms at this keyframe to interpolate to or from. | **Y** |
| `curve` | `str / function` | Easing curve to use, default is 'linear' | N |



## Origin
An relative anchor point to use for scaling and rotation transforms.

#### Attributes
| Name | Type | Description | Required |
| --- | --- | --- | --- |
| `x` | `float` | Horizontal anchor point | **Y** |
| `y` | `float` | Vertical anchor point | **Y** |



## Rotate
Transform by rotating by a given angle in degrees.

#### Attributes
| Name | Type | Description | Required |
| --- | --- | --- | --- |
| `angle` | `float / int` | Angle to rotate by in degrees | **Y** |



## Scale
Transform by scaling by a given factor.

#### Attributes
| Name | Type | Description | Required |
| --- | --- | --- | --- |
| `x` | `float / int` | Horizontal scale factor | **Y** |
| `y` | `float / int` | Vertical scale factor | **Y** |



## Transformation
Transformation makes it possible to animate a child widget by
transitioning between transforms which are applied to the child wiget.

It supports animating translation, scale and rotation of its child.

If you have used CSS transforms and animations before, some of the
following concepts will be familiar to you.

Keyframes define a list of transforms to apply at a specific point in
time, which is given as a percentage of the total animation duration.

A keyframe is created via `animation.Keyframe(percentage, transforms, curve)`.

The `percentage` specifies its point in time and can be expressed as
a floating point number in the range `0.0` to `1.0`.

In case a keyframe at percentage 0% or 100% is missing, a default
keyframe without transforms and with a "linear" easing curve is inserted.

As the animation progresses, transforms defined by the previous and
next keyframe will be interpolated to determine the transform to apply
at the current frame.

The `duration` and `delay` of the animation are expressed as a number
of frames.

By default a transform `origin` of `animation.Origin(0.5, 0.5)` is used,
which defines the anchor point for scaling and rotation to be exactly the
center of the child widget. A different `origin` can be specified by
providing a custom `animation.Origin`.

The animation `direction` defaults to `normal`, playing the animation
forwards. Other possible values are `reverse` to play it backwards,
`alternate` to play it forwards, then backwards or `alternate-reverse`
to play it backwards, then forwards.

The animation `fill_mode` defaults to `forwards`, and controls which
transforms will be applied to the child widget after the animation
finishes. A value of `forwards` will retain the transforms of the last
keyframe, while a value of `backwards` will rever to the transforms
of the first keyframe.

When translating the child widget on the X- or Y-axis, it often is
desireable to round to even integers, which can be controlled via
`rounding`, which defaults to `round`. Possible values are `round` to
round to the nearest integer, `floor` to round down, `ceil` to round
up or `none` to not perform any rounding. Rounding only is applied for
translation transforms, but not to scaling or rotation transforms.

If `wait_for_child` is set to `True`, the animation will finish and
then wait for all child frames to play before restarting. If it is set
to `False`, it will not wait.

#### Attributes
| Name | Type | Description | Required |
| --- | --- | --- | --- |
| `child` | `Widget` | Widget to animate | **Y** |
| `keyframes` | `[Keyframe]` | List of animation keyframes | **Y** |
| `duration` | `int` | Duration of animation (in frames) | **Y** |
| `delay` | `int` | Duration to wait before animation (in frames) | N |
| `width` | `int` | Width of the animation canvas | N |
| `height` | `int` | Height of the animation canvas | N |
| `origin` | `Origin` | Origin for transforms, default is '50%, 50%' | N |
| `direction` | `str` | Direction of the animation, default is 'normal' | N |
| `fill_mode` | `str` | Fill mode of the animation, default is 'forwards' | N |
| `rounding` | `str` | Rounding to use for interpolated translation coordinates (not used for scale and rotate), default is 'round' | N |
| `wait_for_child` | `bool` | Wait for all child frames to play after finishing | N |

#### Example
```
animation.Transformation(
child = render.Box(render.Circle(diameter = 6, color = "#0f0")),
duration = 100,
delay = 0,
origin = animation.Origin(0.5, 0.5),
direction = "alternate",
fill_mode = "forwards",
keyframes = [
animation.Keyframe(
percentage = 0.0,
transforms = [animation.Rotate(0), animation.Translate(-10, 0), animation.Rotate(0)],
curve = "ease_in_out",
),
animation.Keyframe(
percentage = 1.0,
transforms = [animation.Rotate(360), animation.Translate(-10, 0), animation.Rotate(-360)],
),
],
),
```
![](img/widget_Transformation_0.gif)


## Translate
Transform by translating by a given offset.

#### Attributes
| Name | Type | Description | Required |
| --- | --- | --- | --- |
| `x` | `float / int` | Horizontal offset | **Y** |
| `y` | `float / int` | Vertical offset | **Y** |



Binary file added docs/img/widget_Animate_0.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions docs/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,31 @@ render.Row(
![](img/widget_Row_1.gif)


## Sequence
Sequence renders a list of child widgets in sequence.

Each child widget is rendered for the duration of its
frame count, then the next child wiget in the list will
be rendered and so on.

#### Attributes
| Name | Type | Description | Required |
| --- | --- | --- | --- |
| `children` | `[Widget]` | List of child widgets | **Y** |

#### Example
```
render.Sequence(
children = [
animation.Transformation(...),
animation.Transformation(...),
...
],
),
```
![](img/widget_Sequence_0.gif)


## Stack
Stack draws its children on top of each other.

Expand Down
51 changes: 51 additions & 0 deletions render/animation/direction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package animation

type Direction interface {
FrameCount(delay, duration int) int
Progress(delay, duration int, fill float64, frameIdx int) float64
}

type DirectionImpl struct {
Alternate bool
Reverse bool
}

func (self DirectionImpl) FrameCount(delay, duration int) int {
if self.Alternate {
return 2 * (delay + duration)
}

return delay + duration + delay
}

func (self DirectionImpl) Progress(delay, duration int, fill float64, frameIdx int) (progress float64) {
idx1 := delay
idx2 := delay + duration
idx3 := delay + duration + delay
idx4 := delay + duration + delay + duration

if frameIdx < idx1 {
progress = 0.0
} else if frameIdx < idx2 {
progress = float64(frameIdx-idx1) / float64(duration-1)
} else if frameIdx < idx3 {
progress = 1.0
} else if self.Alternate && frameIdx < idx4 {
progress = float64(frameIdx-idx3) / float64(duration-1)
progress = 1.0 - progress
} else {
progress = fill
}

if self.Reverse {
progress = 1.0 - progress
}

return
}

var DirectionNormal = DirectionImpl{Alternate: false, Reverse: false}
var DirectionReverse = DirectionImpl{Alternate: false, Reverse: true}
var DirectionAlternate = DirectionImpl{Alternate: true, Reverse: false}
var DirectionAlternateReverse = DirectionImpl{Alternate: true, Reverse: true}
var DefaultDirection = DirectionNormal
109 changes: 109 additions & 0 deletions render/animation/direction_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package animation

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestDirectionNormal(t *testing.T) {
d := DirectionNormal

assert.Equal(t, 7, d.FrameCount(1, 5))

// Delay
assert.Equal(t, 0.0, d.Progress(1, 5, 1.0, 0))

// Progress
assert.Equal(t, 0.0, d.Progress(1, 5, 1.0, 1))
assert.Equal(t, 0.25, d.Progress(1, 5, 1.0, 2))
assert.Equal(t, 0.5, d.Progress(1, 5, 1.0, 3))
assert.Equal(t, 0.75, d.Progress(1, 5, 1.0, 4))
assert.Equal(t, 1.0, d.Progress(1, 5, 1.0, 5))

// Delay
assert.Equal(t, 1.0, d.Progress(1, 5, 1.0, 6))

// Fill
assert.Equal(t, 0.5, d.Progress(1, 5, 0.5, 7))
}

func TestDirectionReverse(t *testing.T) {
d := DirectionReverse

assert.Equal(t, 7, d.FrameCount(1, 5))

// Delay
assert.Equal(t, 1.0, d.Progress(1, 5, 0.0, 0))

// Progress
assert.Equal(t, 1.0, d.Progress(1, 5, 0.0, 1))
assert.Equal(t, 0.75, d.Progress(1, 5, 0.0, 2))
assert.Equal(t, 0.5, d.Progress(1, 5, 0.0, 3))
assert.Equal(t, 0.25, d.Progress(1, 5, 0.0, 4))
assert.Equal(t, 0.0, d.Progress(1, 5, 0.0, 5))

// Delay
assert.Equal(t, 0.0, d.Progress(1, 5, 0.0, 6))

// Fill
assert.Equal(t, 0.5, d.Progress(1, 5, 0.5, 7))
}

func TestDirectionAlternate(t *testing.T) {
d := DirectionAlternate

assert.Equal(t, 12, d.FrameCount(1, 5))

// Delay
assert.Equal(t, 0.0, d.Progress(1, 5, 1.0, 0))

// Progress
assert.Equal(t, 0.0, d.Progress(1, 5, 1.0, 1))
assert.Equal(t, 0.25, d.Progress(1, 5, 1.0, 2))
assert.Equal(t, 0.5, d.Progress(1, 5, 1.0, 3))
assert.Equal(t, 0.75, d.Progress(1, 5, 1.0, 4))
assert.Equal(t, 1.0, d.Progress(1, 5, 1.0, 5))

// Delay
assert.Equal(t, 1.0, d.Progress(1, 5, 1.0, 6))

// Progress
assert.Equal(t, 1.0, d.Progress(1, 5, 1.0, 7))
assert.Equal(t, 0.75, d.Progress(1, 5, 1.0, 8))
assert.Equal(t, 0.5, d.Progress(1, 5, 1.0, 9))
assert.Equal(t, 0.25, d.Progress(1, 5, 1.0, 10))
assert.Equal(t, 0.0, d.Progress(1, 5, 1.0, 11))

// Fill
assert.Equal(t, 0.5, d.Progress(1, 5, 0.5, 12))
}

func TestDirectionAlternateReverse(t *testing.T) {
d := DirectionAlternateReverse

assert.Equal(t, 12, d.FrameCount(1, 5))

// Delay
assert.Equal(t, 1.0, d.Progress(1, 5, 1.0, 0))

// Progress
assert.Equal(t, 1.0, d.Progress(1, 5, 0.0, 1))
assert.Equal(t, 0.75, d.Progress(1, 5, 0.0, 2))
assert.Equal(t, 0.5, d.Progress(1, 5, 0.0, 3))
assert.Equal(t, 0.25, d.Progress(1, 5, 0.0, 4))
assert.Equal(t, 0.0, d.Progress(1, 5, 0.0, 5))

// Delay
assert.Equal(t, 0.0, d.Progress(1, 5, 1.0, 6))

// Progress
assert.Equal(t, 0.0, d.Progress(1, 5, 0.0, 7))
assert.Equal(t, 0.25, d.Progress(1, 5, 0.0, 8))
assert.Equal(t, 0.5, d.Progress(1, 5, 0.0, 9))
assert.Equal(t, 0.75, d.Progress(1, 5, 0.0, 10))
assert.Equal(t, 1.0, d.Progress(1, 5, 0.0, 11))

// Fill
assert.Equal(t, 0.5, d.Progress(1, 5, 0.5, 12))
}
19 changes: 19 additions & 0 deletions render/animation/fill_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package animation

type FillMode interface {
Value() float64
}

type FillModeForwards struct{}

func (self FillModeForwards) Value() float64 {
return 1.0
}

type FillModeBackwards struct{}

func (self FillModeBackwards) Value() float64 {
return 0.0
}

var DefaultFillMode = FillModeForwards{}
Loading

0 comments on commit 00af67e

Please sign in to comment.