Skip to content

Commit

Permalink
Morphs and Partially draw Objects (#482)
Browse files Browse the repository at this point in the history
* overrides for strokepath , strokepreserve , fillpath and fillpreserve, also introduces new pathtopoly
* JPath struct added to Object.jl , getjpaths() and drawobj_jpaths(obj) written
* morph now modifies draws morphed jpaths , and calls original object.func with luxor draw disabled
* morphing with Animations.jl using functions
* partial draw
  • Loading branch information
ArbitRandomUser authored Aug 9, 2022
1 parent 6add2f2 commit 1e74392
Show file tree
Hide file tree
Showing 55 changed files with 1,608 additions and 170 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
strategy:
matrix:
version:
- '1.5'
- '1.6'
- '1'
os:
- ubuntu-latest
Expand Down
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

# PR changes
- changed render method for mp4 to use ffmpeg directly inplace of VideoIO

- Added jpaths a field in Object that is useful for morphs and partial drawing
- Added morphs to arbitrary objects and functions.
- Keyframed morphs with Animations.jl are possible.
- Added ability to partially draw any object, and have animations of showing them get created.
- One tutorial added on how to use morphs
- tutorial on partial draw / show creation
- Few tests for morphs added
- test for partial draw/ show creation

## v0.9.0 (26th of May 2022)
- Ability to use Luxor functionality without rendering an animation
Expand Down
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ Images = "0.20, 0.21, 0.22, 0.23, 0.24, 0.25"
Interact = "0.10"
LaTeXStrings = "1.1"
LightXML = "0.9"
Luxor = "2.12, 3"
Luxor = "3.5"
ProgressMeter = "1"
julia = "1.5"
julia = "1.6"
2 changes: 2 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ makedocs(;
"tutorials/tutorial_6.md",
"tutorials/tutorial_7.md",
"tutorials/tutorial_8.md",
"tutorials/tutorial_morphing.md",
"tutorials/tutorial_partialdraw.md",
],
"HowTo" => "howto.md",
"Workflows" => "workflows.md",
Expand Down
Binary file added docs/src/assets/box_to_circ_anim.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/box_to_star_to_circ_anim.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/circ_to_box.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/circ_to_box_func.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/createcircle.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/createcircle2.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/createcircle3.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/createcircle4.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/assets/demo_partialdraw.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/src/tutorials.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ Currently, these tutorials are available:
- [**Tutorial 6: Taming the Elements**](tutorials/tutorial_6.md) - how to use `change` to grow or shrink arbitrary objects and using `Javis` with other Julia packages.
- [**Tutorial 7: Using Animations.jl to Create something with more Pep!**](tutorials/tutorial_7.md) - an advanced tutorial to make your animations more interesting.
- [**Tutorial 8: Fun with Layers! An Intro to `@JLayer`**](tutorials/tutorial_8.md) - a `Javis` state of the art tutorial on how to use `@JLayer` to make composable animations.
- [**Tutorial 9: Morphing**](tutorials/tutorial_morphing.md) - Morphing `Javis` Objects.
- [**Tutorial 10: Animate Object Creation **](tutorials/tutorial_partialdraw.md) - Animate Object Creations.

If you spot an issue with any of these tutorials, please let us know! Thank you!
217 changes: 217 additions & 0 deletions docs/src/tutorials/tutorial_morphing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# **Tutorial 9:** Morphing `Javis` Objects
There are multiple ways to morph an object in Javis.

- Using `morph_to(::Object)` method. Any Object can be morphed to any other object using this method.
- Using `morph_to(::Function)` method. Similar to `morph_to(::Object)` but morphs to a function instead. Can morph an object to a function that contains Luxor calls to draw what it should morphed into.
- Specifying an Action with an `Animation` along with `morph()` to make keyframed morphings. This helps making and timing a sequence of morph animations easier.

## Morphing one object to another.

Like other animations `morph_to(::Object)` is to be used with action. To learn more about Actions refer to [Tutorial 5](tutorial_5.md).
Here is a simple code snippet on how to use `morph_to`...
```julia
using Javis

video = Video(500,500)
nframes = 160

function circdraw(color)
sethue(color)
setopacity(0.5)
circle(O,100,:fillpreserve)
setopacity(1.0)
sethue("white")
strokepath()
end

function boxdraw(color)
sethue(color)
box(O,100,100,:fillpreserve)
setopacity(1.0)
sethue("white")
strokepath()
end
Background(1:nframes,(args...)->background("black"))
boxobj = Object((v,o,f) -> boxdraw("green"))
circobj = Object((v,o,f) -> circdraw("red"))

transform_to_box = Action(20:nframes-20, morph_to(boxobj))
act!(circobj, transform_to_box)
render(video,pathname="circ_to_box.gif")
```

![](../assets/circ_to_box.gif)

If you aren't familiar with this syntax `(v,o,f)-> circdraw("red")` its an "anonymous" function or sometimes called a lambda function.
Basically a nameless function that is written on the spot in that line of code . One might as well use any other function `func` in place of it
(which takes at least 3 arguments `video,object,frame`). Elsewhere in the docs/tutorials you will come across
something of the form `Object( (args...) -> ("some code here") )`. This is [slurping](https://docs.julialang.org/en/v1/manual/faq/#The-two-uses-of-the-...-operator:-slurping-and-splatting) and is similar to packing `*args` in python.

We created two objects `circobj` and `boxobj` . `circobj` ofcourse is a circle because its drawing function `(v,o,f) -> circdraw("red")`
draws a circle, with a `color=red`, filling at `0.5` opacity, and then makes a white outline (stroke).
`boxobj`'s function draws an opaque green box, with white outline.

This Object function is called repeatedly at render-time at every frame that the object exists to draw this object. The appropriate `video`,`object`, and `frame` are passed to
this function at render time.
Javis then has other tricks up its sleeve to scale/move/morph whats going to be drawn depending on the
frame and object to effect out animations through Actions. This is roughly the idea behind Javis's Object-Action mechanism

We defined a `transform_to_box` Action which runs from frame 20 to lastframe-20 . The `Action` morphs whatever object its acted upon, into what looks
like `boxobj`. Note that `boxobj` and `circobj` are separate objects all the time, even after the `Action` (it just happens that they overlap each other). As the Action keeps getting applied at render time frame by frame, the "drawing" of `circobj` starts to look like `boxobj`'s drawing.

The Action is applied to the `circobj` with the `act!` function.

Note that the `boxobj` is present throughout as the `circobj` is morphing.
If you want to hide it you can set its opacity to 0 with another action (to make it disappear) and set its frames to be drawn for 1 frame only (for efficiency).
```julia
Background(1:nframes,(args...)->background("black"))
boxobj = Object(1:1 , (args...) -> boxdraw("green") )
circobj = Object(1:nframes,(args...) -> circdraw("red"))

transform_to_box = Action(20:nframes-20, morph_to(boxobj))
hide_action = Action(1:1, (args...)->setopacity(0.0) )

act!(circobj, transform_to_box)
act!(boxobj, hide_action)

render(video,pathname="circ_to_box_hidden.gif")
```

However you can directly specify a shape an object has to morph to without making an Object using `morph_to(f::Function)` i.e passing a function as an argument.

## Morphing an `Object` using a `Function`

```julia
Background(1:nframes,(args...)->background("black"))
#boxobj = Object(1:1 , (args...) -> boxdraw("green") )
circobj = Object(1:nframes,(args...) -> circdraw("red"))

transform_to_box = Action(20:nframes-20, morph_to(boxdraw,["blue"]))
#hide_action = Action(1:1, (args...)->setopacity(0.0) )

act!(circobj, transform_to_box)
#act!(boxobj, hide_action)

render(video,pathname="circ_to_box_func.gif")
```

![](../assets/circ_to_box_func.gif)

Here we have morphed the circle without defining an object to morph to. Rather the shape it has to morph into
is given by a `Function`.
The general syntax is `morph_to(fn::Function,args::Array=[])`
. `args` is an array of arguments that is to be passed to the function.
Here we morph `circobj` to a shape
that would is drawn by `boxdraw("blue")`. Morphed Objects can be furthur morphed into
other shapes by carrying out another `Action` further in the timeline.

## Keyframed morphs using Animations.jl

Another mechanism for morphing is by passing `morph()` to `Action` along with an `Animation`
For a tutorial on how to use Animations.jl look at [Tutorial 7](tutorial_7.md),

```julia
using Javis
using Animations
video = Video(500,500)
nframes = 160

function circdraw(color)
sethue(color)
setopacity(0.5)
circle(O,50,:fillpreserve)
setopacity(1.0)
sethue("white")
strokepath()
end

function boxdraw(color)
sethue(color)
box(O,100,100,:fillpreserve)
setopacity(1.0)
sethue("white")
strokepath()
end

function stardraw()
sethue("white")
star(O,100,5,0.5,0.0,:stroke)
end

Background(1:nframes+10,(args...)->background("black"))
boxobj = Object(1:nframes+10 , (args...) -> boxdraw("green") )

anim = Animation([0,1],MorphFunction[(boxdraw,["green"]),(circdraw,["red"])] )

action = Action(1:nframes,anim,morph())
act!(boxobj,action)
render(video,pathname="box_to_circ_hidden.gif")
```

Take a look at `anim`. It is of type `Animation`.
First lets look at a simpler instance of `Animation`.
```
Ex:1
Animation([0,1],[2,4])
```
Think of Animations like a "map" or a "function" (in the math sense) thats maps values from its first argument (`[0,1]` above) to another set of values (`[2,4]`) . This means that
0 gets mapped to 2 and 1 gets mapped to 4 and all values inbetween are linearly interpolated.
Another Example
```
Ex:2
Animation([0,0.3,1],[3,4.5,7])
```

This animation maps 0 to 3 , 0.3 -> 4.5 and 1->7. And all values inbetween are linear interpolations.

Take a look at the `Animations.jl` package for an indepth explanation on how to have different interpolations to make your animations look way cooler. ( for example the `sineio` interpolaation is slow at first speeds up in between and gradually slows to a halt )

One can in principle provide values beyond 0 and 1 for the first argument however Javis requires `Animation` objects to have the first argument to be from 0 to 1.
This `Animation` object is passed to an `Action`, and Javis interprets 0 to be the first frame
of the Action and 1 to be the final frame of the Action.

In the big code snippet above we can see that the second array passed to Animation is
an array of `MorphFunction`s.
`MorphFunction` is a struct . This struct has 2 fields. The fields are `func` and `args`. These arguments are used to specify drawing functions and the arguments to be passed to them , The `Array` of `MorphFunction` passed to the `Animation` defines a sequence of shapes/drawings that the `Object` should be morphed into one by one in that order. Each shape/drawing is what would have been got by calling `func(args...)` of the respective `MorphFunction`. In the example above there are only two in shapes in the sequence a green box and a red circle (`boxdraw("green")` and `circdraw("red")`).
Typically the first `MorphFunction` should draw the same thing that `Object` is.

The general idea of whats going on is we are making an `Animation` that maps `0` (i.e the first frame of the action.) to `MorphFunction(boxdraw,["green"])` and `1` (last frame of the action) to ` MorphFunction(circdraw,["red"])` and Javis handles the interpolation between them.

Thus we have made an `Animation` called `anim`. Then we made an `action` with this `anim`. We called it `action` . Then we applied the action on our object `boxobj` to get ...

![](../assets/box_to_circ_anim.gif)

The way of morphing shines when you have to do multiple morphs in a sequence and with different timings. Lets look at another example taking object to morph box(initial shape) -> star -> circle in a sequence.

Change the lines describing the animation to
```julia
anim = Animation([0, 0.7, 1],MorphFunction[(boxdraw, ["green"]), (stardraw, []), (circdraw, ["red"])])
```
`stardraw` draws a white star without fill. The function does not take an argument and therefore the `Tuple` with `stardraw` should have an empty `Array` at its
second index. If your drawing functions do not take any arguments you can pass it as function itself, and need not wrap it in a `Tuple`.

Ex. suppose `mydraw1` , `mydraw2` and `mydraw4` take a color as an argument but `mydraw3` does not take any arguments.

```julia
anim = Animation([0, t1, t2, 1],MorphFunction[ (mydraw1,["red"]), (mydraw2,["blue"]), mydraw3, (mydraw4,["black"]) ])
```

A third way to pass functions to morph into is to simply pass a function an its arguments in a `Tuple`

```julia
anim = Animation([0, t1, t2, 1],MorphFunction[ (mydraw1,"red"), (mydraw2,"blue"), mydraw3, (mydraw4,"black") ])
```

When passed this way the first element of the Tuple is taken to be the function and the subsequent elements are the arguments to be
passed to the function.

![](../assets/box_to_star_to_circ_anim.gif)

What we see now is from the beginning to 0.7 fraction of the `Action`'s frames it carries
out the morphing from a `boxdraw("green")` to `stardraw()`.
And the remainder of the `Action`'s frames it morphs from `stardraw()` to `circdraw("red")`. Once again , do look up Animations.jl and Tutorial 7 to see how you pass easing functions to manipulate the timing of animations (for example ... initially slow - fast in the middle - slow in the end) .
Now you know a bit about Morphing . Remember just like any other Action you can stack morphing actions with other Actions (translations, scaling etc) to bring about effects you desire.

> **Author(s):** John George Francis (@arbitrandomuser)
> **Date:** May 28th, 2022 \
> **Tag(s):** action, morphing, object, animation
89 changes: 89 additions & 0 deletions docs/src/tutorials/tutorial_partialdraw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# **Tutorial 10:** Animating Creation of Objects

A cool effect of bringing in `Objects` into your video/gif is to show it being
drawn out incrementally from nothing.

![](../assets/demo_partialdraw.gif)

In this tutorial we'll take a look at how you can make these kind of animations


## The `showcreation` function

The general syntax to animate the creation of an `object` is

```julia
action = Action(startframe:endframe, showcreation() )
act!(obj, action)
```

This starts the creation of the object at `startframe` and the `object` is completely
drawn when the timeline reaches `endframe`.

lets see this in example.

```julia
using Javis
video = Video(500,500)

Background(1:120,(args...)->begin
background("black")
sethue("white")
end)
circ = Object(1:120, (args...)-> circle(O,100,:stroke))

action_showcreate = Action(1:60,showcreation())
act!(circ,action_showcreate)

render(video,pathname="createcircle.gif")
```

![](../assets/createcircle.gif)

You should see a circle being created in your video. ( I've added in the frame numbers
in the gif so that the beginning and end of the gif are easily identifiable )

What if we wanted to show the object being created at a later point in the timeline. Say
we want it to be created at frame 30 and finish at frame 90.
Thats simple! we change the frames that the Action works on . Change the line with `action_showcreate` in the above example to...

```julia
action_showcreate = Action(30:90, showcreation())
```

![](../assets/createcircle2.gif)

Oops! , Thats (probably) not what we wanted. (look at the frame numbers) . What happened was `obj` exists from frame 1 to 120. But the show creation acts on it from 30 to 90. So the object exists from frame 1 to 30 as it is . Then its creation is animated from 30 to 90 and from 90 to 120 it remains as such.

One way to mitigate this is to change the frames `obj` exists. Make this change in the code
above

```julia
circ = Object(30:120, (args...)->circle(O,100,:stroke))
```

![](../assets/createcircle3.gif)

Somethings still wrong!. One thing we forgot is the frames you mention in `Action` are
the frames relative to the `Object`s existence. So what happened now is the object is put
on the scene from frame 30 onwards. The action acts on it from frame 30 relative to when
the object was put. So `30+30` i.e `60` is the frame at which `action` starts.
Can you fix this ?

```julia
action_showcreate = Action(1:60,showcreation())
```

![](../assets/createcircle4.gif)

There we go ! It turned out we dont need to change the frames of the action, but the frames of the Object.
Hopefully by intentionally showing you a wrong way to do it you understood the working of `Actions` a little better.

Another way is to have the object present throughout the video and to "hide" it initially till it is to be shown, with an action
that sets the objects opacity to 0.

There exists a similar function `showdestruction()` which does exactly the opposite of `showcreation()`.

> **Author(s):** John George Francis (@arbitrandomuser)
> **Date:** May 28th, 2022 \
> **Tag(s):** action, morphing, object, animation
Loading

0 comments on commit 1e74392

Please sign in to comment.