diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 149c78270..1d0aa1cf1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,8 @@ jobs: - x64 steps: - uses: actions/checkout@v2 - - uses: actions/checkout@v2 + - run: sudo apt-get install xvfb && Xvfb :99 & + if: matrix.os == 'ubuntu-latest' - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} @@ -36,6 +37,8 @@ jobs: ${{ runner.os }}- - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-runtest@latest + env: + DISPLAY: :99 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v1 with: @@ -45,6 +48,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - run: sudo apt-get install xvfb && Xvfb :99 & - uses: julia-actions/setup-julia@v1 with: version: '1.4' @@ -57,3 +61,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} + DISPLAY: :99 diff --git a/.gitignore b/.gitignore index 2ef99b508..1dc58d7cc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ Manifest.toml .vscode/ test.png -test/current/ \ No newline at end of file +test/current/ +.commit \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f12f86d07..840ae4b4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,32 @@ # Javis.jl - Changelog +## 0.2.0 (25th of September 2020) +- Ability to use [Animations.jl](https://github.com/jkrumbiegel/Animations.jl) + - for Transformations and `appear` and `disappear` +- Show progress of rendering using [ProgressMeter.jl](https://github.com/timholy/ProgressMeter.jl) +- Use [VideoIO](https://github.com/JuliaIO/VideoIO.jl) for faster rendering without temporary images +- Ability to draw animated text via `appear(:draw_text)` + - Must be called inside a `SubAction` +- Ability to morph with `fill` or `stroke` and using `SubAction` to specify changes in color +- Added live viewer based on `Gtk.jl` in the `javis` function + - Activate in `javis` by setting `liveview = true` +- Prototype returning single frame of Javis animation with `get_javis_frame` + - Currently must be invoked after `javis` function call + - Can be called via `Javis.get_javis_frame` as it is not exported yet +- An object described by an action can follow a path (a vector of points). See `follow_path` +- Bugfix when scaling to 0. Before this every object on that frame would disappear even in a different layer +- Bugfix in interpolation: Interpolation of a single frame like `1:1` returns `1.0` now instead of `NaN`. + + ## 0.1.5 (14th of September 2020) - Bugfix in svg parser when a layer gets both transformed and scaled ## 0.1.4 (13th of September 2020) - Bugfix in svg parser when a reflected Bรฉzier curve followed a move operation +### Removed +- `latex` no longer takes the `fontsize` as an argument [PR #180](https://github.com/Wikunia/Javis.jl/pull/180) + ## 0.1.3 (11th of September 2020) - First `SubAction` for an `Action` no longer requires explicit frame range and will default to the frames of the `Action` - Ability to scale an object with `Scaling`. Works similar to `Translation` and `Rotation` diff --git a/Project.toml b/Project.toml index 217e6060e..9086b48f9 100644 --- a/Project.toml +++ b/Project.toml @@ -1,18 +1,34 @@ name = "Javis" uuid = "78b212ba-a7f9-42d4-b726-60726080707e" authors = ["Ole Krรถger ", "Jacob Zelko "] -version = "0.1.5" +version = "0.2.0" [deps] +Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" +Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" +ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" +Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" +GtkReactive = "27996c0f-39cd-5cc1-a27a-05f136f946b6" +Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" LightXML = "9c8b4983-aa76-5018-a973-4c85ecc9e179" Luxor = "ae8d54c2-7ccd-5906-9d76-62fc9837b5bc" +ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +VideoIO = "d6d074c3-1acf-5d4c-9a43-ef38773959a2" [compat] +Animations = "0.4" +Cairo = "1" +ColorTypes = "0.10" FFMPEG = "0.3, 0.4" +Gtk = "1.1" +GtkReactive = "1.0.3" +Images = "0.20, 0.21, 0.22" LaTeXStrings = "1.1" LightXML = "0.9" Luxor = "2" +ProgressMeter = "1" +VideoIO = "0.6, 0.7, 0.8" julia = "1.4" diff --git a/README.md b/README.md index 0a6643fe5..8fd6f522a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,23 @@ Check out our [documentation](https://wikunia.github.io/Javis.jl/dev/) for tutor ## Gallery -![](assets/showcase.gif) +### Tutorials + +| Animation | Animation | +|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| +| ![](assets/eeg.gif) | ![](assets/dancing_circles.gif) | +| **Tutorial:** [What Are Actions?](https://wikunia.github.io/Javis.jl/stable/tutorials/tutorial_2/) | **Tutorial:** [Making Your First Javis Animation!](https://wikunia.github.io/Javis.jl/stable/tutorials/tutorial_1/) | +| ![](assets/atomic.gif) | ![](assets/jarvis.gif) | +| **Tutorial:** [Taming the Elements](https://wikunia.github.io/Javis.jl/stable/tutorials/tutorial_5/) | **Tutorial:** [Do You Know Our Mascot?](https://wikunia.github.io/Javis.jl/stable/tutorials/tutorial_4/) | +| ![](assets/matrix.gif) | ![](assets/loading.gif) | +| **Tutorial:** [Rendering LaTeX with Javis!](https://wikunia.github.io/Javis.jl/stable/tutorials/tutorial_3/) | **Tutorial:** [Using Animations.jl with Javis!](https://wikunia.github.io/Javis.jl/stable/tutorials/tutorial_6/) | + +### Examples + +| Animation | +|-------------------------------------------| +| ![](examples/gifs/follow_bezier_path.gif) | +| [Follow a Path](/examples/follow_path.jl) | ## Installation diff --git a/assets/atomic.gif b/assets/atomic.gif new file mode 100644 index 000000000..b8decdfaf Binary files /dev/null and b/assets/atomic.gif differ diff --git a/assets/dancing_circles.gif b/assets/dancing_circles.gif new file mode 100644 index 000000000..3fff6483d Binary files /dev/null and b/assets/dancing_circles.gif differ diff --git a/assets/eeg.gif b/assets/eeg.gif index 0767ff581..00f05faeb 100644 Binary files a/assets/eeg.gif and b/assets/eeg.gif differ diff --git a/assets/jarvis.gif b/assets/jarvis.gif index 5c8f82eba..e9e7b4230 100644 Binary files a/assets/jarvis.gif and b/assets/jarvis.gif differ diff --git a/assets/loading.gif b/assets/loading.gif new file mode 100644 index 000000000..9912479c9 Binary files /dev/null and b/assets/loading.gif differ diff --git a/assets/matrix.gif b/assets/matrix.gif new file mode 100644 index 000000000..66d027562 Binary files /dev/null and b/assets/matrix.gif differ diff --git a/docs/Project.toml b/docs/Project.toml index a8fce0f92..aa99ddba7 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,4 +1,5 @@ [deps] +Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Javis = "78b212ba-a7f9-42d4-b726-60726080707e" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" diff --git a/docs/make.jl b/docs/make.jl index a12c3af4a..6aff1c0b7 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -20,11 +20,14 @@ makedocs(; "tutorials/tutorial_3.md", "tutorials/tutorial_4.md", "tutorials/tutorial_5.md", + "tutorials/tutorial_6.md", ], + "HowTo" => "howto.md", + "Examples" => "examples.md", "Mission" => "mission.md", "References" => "references.md", "Contributing" => "contributing.md", ], ) -deploydocs(; repo = "github.com/Wikunia/Javis.jl") +deploydocs(; repo = "github.com/Wikunia/Javis.jl", push_preview = true) diff --git a/docs/src/assets/follow_bezier_path.gif b/docs/src/assets/follow_bezier_path.gif new file mode 100644 index 000000000..c10ff8166 Binary files /dev/null and b/docs/src/assets/follow_bezier_path.gif differ diff --git a/docs/src/contributing.md b/docs/src/contributing.md index 5997f7507..044d25c57 100644 --- a/docs/src/contributing.md +++ b/docs/src/contributing.md @@ -123,7 +123,7 @@ Finally, click "Create pull request". You may get some questions about it, and possibly suggestions of how to make it ready to go into the main project. If you had test errors or problems, we are happy to help you. -Then, if all goes according to plan, it gets merged... **Thanks for the contribution!!** :tada: :tada: :tada: +Then, if all goes according to plan, it gets merged... **Thanks for the contribution!!** ๐ŸŽ‰ ๐ŸŽ‰ ๐ŸŽ‰ ## Note on Adding Dependencies diff --git a/docs/src/examples.md b/docs/src/examples.md new file mode 100644 index 000000000..a863d6682 --- /dev/null +++ b/docs/src/examples.md @@ -0,0 +1,9 @@ +## Examples + +Aside from tutorials which take you step by step on how to create a tutorial, we also felt that it useful to show additional examples on things you can make with `Javis`! +These animations do not have an accompanying tutorial but rather, scripts that you can examine, tweak, and modify to suit your own purposes. +If you have an interesting example that you would like to share with us, open an issue (check out our [Contributing Guide](contributing.md) on how to do this) and we'd be happy to add your example to this page! + +| Example | Link | Details | +|------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ![](assets/follow_bezier_path.gif) | [Follow a Path](https://github.com/Wikunia/Javis.jl/blob/master/examples/follow_path.jl) | This creates a bezier curve to draw the outline of a car and have a circle go around that outlined path. [`follow_path`](@ref), `makebezierpath`, and [`appear`](@ref) were used to create this animation. | diff --git a/docs/src/howto.md b/docs/src/howto.md new file mode 100644 index 000000000..2f180e663 --- /dev/null +++ b/docs/src/howto.md @@ -0,0 +1,215 @@ +# How To? + +This is a list of frequently ask questions answering basic "How to do X, Y, Z?" questions. + +For all the questions below you need to start with: + +```julia +using Javis + +function ground(args...) + background("black") + sethue("white") +end + +video = Video(500, 500) + +javis(video, [ + BackgroundAction(1:100, ground), + SNIPPETS_GO_HERE # Replace this line with the provided snippet! +]; pathname="how_to.gif") +``` + +Each of the code snippets should replace the variable `SNIPPETS_GO_HERE`. + +## How can I move a circle from A to B? + +First of all you need to define an [`Action`](@ref) which draws a circle. + +```julia +Action(1:100, (args...)->circle(O, 50, :fill)) +``` + +and then you need the [`Translation`](@ref) command to move the circle. + +```julia +Action(1:100, (args...)->circle(O, 50, :fill), Translation(O, Point(100, 100)) +``` + +The circle then moves from the origin (center of frame) 100 px down and to the right. + +## How can I define frames? + +There are currently three different ways to define frames inside Javis. +The simplest one is to define the `UnitRange` like `1:100` as above such that the action is called for every frame from `1` to `100`. + +**Examples:** +```julia +Action(1:100, (args...)->circle(O, 50, :fill)), +Action(1:50, (args...)->circle(O, 70, :stroke)) +``` + +It is relatively often the case that the following action should work with the same frames as the previous action this can be done with. + +**Examples:** +```julia +Action(1:100, (args...)->circle(O, 50, :fill)), +Action(:same, (args...)->circle(Point(100, 100), 20, :stroke)), +Action((args...)->circle(Point(-100, 100), 20, :stroke)) +``` + +so either use the symbol `:same` or just don't mention frames. + +The last option is to define frames relative to the previous frame. More precisely the end of the last frame. + +**Examples:** +```julia +Action(1:50, (args...)->circle(O, 50, :fill)), +Action(Rel(1:50), (args...)->circle(Point(100, 100), 20, :stroke)), +``` + +This is the same as: +```julia +Action(1:50, (args...)->circle(O, 50, :fill)), +Action(51:100, (args...)->circle(Point(100, 100), 20, :stroke)), +``` + +## How can I make an object fade in from the background? + +Let's make the standard circle we used before appear from the background. + +```julia +Action(1:100, (args...)->circle(O, 50, :fill); subactions=[ + SubAction(1:50, appear(:fade)) +]), +``` + +this is using a change in opacity to show the circle. + +There are two other options `:scale` and `:fade_line_width`. `:scale` also works for every kind of [`Action`](@ref) whereas `:fade_line_width` only works if you only draw the stroke instead of using fill. + +**Example:** +```julia +Action(1:100, (args...)->circle(O, 50, :stroke); subactions=[ + SubAction(1:50, appear(:fade_line_width)) +]), +``` + +Additionally you can use all of these three options for the [`disappear`](@ref) functionality. + +> **NOTE:** A [`SubAction`](@ref) gets also called for frames after the last specified subaction frame such that disappeared objects stay disappeared. + +## How can I move one object based on another object? + +In this case we need to define our own `circ` function which draws the circle and returns the center point of the circle. + +```julia +function circ(point, radius, action) + circle(point, radius, action) + return point +end +``` + +Now we define two actions: +1. Drawing a circle and saving the position inside `:my_circle` +2. Drawing a rectangle above the circle + +```julia +Action(1:100, :my_circle, (args...)->circ(O, 50, :stroke), Translation(Point(100,100))), +Action(1:100, (args...)->rect(pos(:my_circle)+Point(-10, -100), 20, 20, :fill)) +``` + +In this animation the position of the circle is saved inside `:my_circle` and can be used with `pos(:my_circle)` inside the `rect` function. + +## How can I show a text being drawn? + +A `text` can appear as *any* other object with `appear(:fade)` and `appear(:scale)`, However, it also has a special [`appear`](@ref) functionality called +`:draw_text`. + +You can use +```julia +Action( + 1:100, + (args...) -> text("Hello World!"; halign = :center); + subactions = [ + SubAction(1:15, sineio(), appear(:draw_text)), + SubAction(76:100, sineio(), disappear(:draw_text)), + ] +) +``` + +to let the text `"Hello World!"` appear from left to right in an animated way. + +## How can I have an object follow a path? + +We need to create a path by providing a list of points that the object can follow. +All objects that return a list of points can be used directly like `star` and `poly` for others a list of points must be provided as the input. + +An action can look like this: + +```julia +Action( + 1:150 + (args...) -> star(O, 20, 5, 0.5, 0, :fill); + subactions = [ + SubAction(1:150, follow_path(star(O, 300))), + ], +) +``` + +in this case a star is following the path of a bigger star. +> **NOTE:** the star inside [`follow_path`](@ref) should have the `action=:none` which is the default for most Luxor functions. + +> **NOTE:** Unfortunately the above currently only works for some Luxor functions like `ngon` and `star` but not for `circle` and `rect` as they return `true` instead of the points. + +In that case you need to define a function like: +```julia +function ground(args...) + background("white") + sethue("black") +end + +function luxor2poly(func::Function) + newpath() + func() + closepath() + return pathtopoly()[1] +end + +video = Video(600, 400) +javis(video, [ + BackgroundAction(1:150, ground), + Action( + 1:150, + (args...) -> star(O, 20, 5, 0.5, 0, :fill); + subactions = [ + SubAction(1:150, follow_path(luxor2poly(()->rect(O, 100, 100, :path)))) + ] + ) +]; pathname="follow_path.gif") +``` + + +Another possibility is to specify a vector of points like this: + +```julia +Action( + 1:150 + (args...) -> star(O, 20, 5, 0.5, 0, :fill); + subactions = [ + SubAction(1:150, sineio(), follow_path([Point(100, 200), Point(-20, -250), Point(-80, -10)]; closed=false)), + ], +) +``` + +In this case I want the star to follow a path consisting of two edges and I use `; closed=false` to specify that it's just two edges and not a closed triangle. + +An interesting possibility is to define paths using Bรฉzier curves which can be defined with Luxor see: [Polygons to Bรฉzier paths and back again](https://juliagraphics.github.io/Luxor.jl/stable/polygons/#Polygons-to-B%C3%A9zier-paths-and-back-again) + +## How can I see a live view of the animation? + +A live view of the animation can be useful for creating an animation where one doesn't need the overhead of building a gif or mp4 all the time. It also has the advantage that it's very easy to jump to a specific frame. + +The live viewer can be called with adding `; liveview=true` to the [`javis`](@ref) call. + +> **NOTE:** If `liveview=true` the `tempdirectory` and `pathname` arguments are ignored. \ No newline at end of file diff --git a/docs/src/mission.md b/docs/src/mission.md index 280218884..7f1b51ed4 100644 --- a/docs/src/mission.md +++ b/docs/src/mission.md @@ -2,7 +2,7 @@ ## What is Javis? -`Javis.jl` is a tool focused on providing an easy to use interface for making animations and developing visualizations quickly - while having fun! :smiley: +`Javis.jl` is a tool focused on providing an easy to use interface for making animations and developing visualizations quickly - while having fun! ๐Ÿ˜ƒ That being said, we decided to make this mission statement to clearly explain the scope of this project. That is, to explain what this project _is_ and what it _is not_. Here are the core tenents of `Javis` concisely explained: @@ -14,7 +14,7 @@ That being said, we decided to make this mission statement to clearly explain th - **Javis is not neccesarilly geared towards data analytics.** Admittedly, there are ways to use Javis to visualize data while creating animations. However, the intent of Javis is not focused on creating functionality to analyze datasets _as of this moment_. This may change in the future. -- **We love documentation and tutorials! ๐Ÿค“** One of the things we prioritize in each release of Javis is to document functionalities of the tools we add. Furthermore, we like to make tutorials to also show what is possible in Javis. Do you have a cool animation or blog that you have written using Javis? Let us know by making an issue to let us know! +- **We love documentation and tutorials! ๐Ÿค“** One of the things we prioritize in each release of Javis is to document functionalities of the tools we add. Furthermore, we like to make tutorials to also show what is possible in Javis. Do you have a cool animation or blog that you have written using Javis? Let us know by opening an issue! ## Summary diff --git a/docs/src/tutorials.md b/docs/src/tutorials.md index cdf06d162..052638575 100644 --- a/docs/src/tutorials.md +++ b/docs/src/tutorials.md @@ -18,5 +18,6 @@ Currently, these tutorials are available: - [**Tutorial 3: Rendering `LaTeX` with `Javis`!**](tutorials/tutorial_3.md) - a simple tutorial on how to render `LaTeX` in `Javis` animations. - [**Tutorial 4: Do You Know Our Mascot? Learn about Transitions and SubActions!**](tutorials/tutorial_4.md) - a fun tutorial to make our mascot and learn about the `SubAction` and `Transition` type methods. - [**Tutorial 5: Taming the Elements**](tutorials/tutorial_5.md) - how to use `Scaling` to grow or shrink arbitrary objects and using `Javis` with other Julia packages. +- [**Tutorial 6: Using Animations.jl to Create something with more Pep!**](tutorials/tutorial_6.md) - an advanced tutorial to make your animations more interesting. If you spot an issue with any of these tutorials, please let us know! Thank you! diff --git a/docs/src/tutorials/assets/atomic.gif b/docs/src/tutorials/assets/atomic.gif index 2fb894a62..823d85738 100644 Binary files a/docs/src/tutorials/assets/atomic.gif and b/docs/src/tutorials/assets/atomic.gif differ diff --git a/docs/src/tutorials/assets/loading.gif b/docs/src/tutorials/assets/loading.gif new file mode 100644 index 000000000..392aef820 Binary files /dev/null and b/docs/src/tutorials/assets/loading.gif differ diff --git a/docs/src/tutorials/assets/loading_circle_linear.gif b/docs/src/tutorials/assets/loading_circle_linear.gif new file mode 100644 index 000000000..ad4e7d717 Binary files /dev/null and b/docs/src/tutorials/assets/loading_circle_linear.gif differ diff --git a/docs/src/tutorials/assets/loading_circle_sineio.gif b/docs/src/tutorials/assets/loading_circle_sineio.gif new file mode 100644 index 000000000..fc9e7575e Binary files /dev/null and b/docs/src/tutorials/assets/loading_circle_sineio.gif differ diff --git a/docs/src/tutorials/assets/loading_color.gif b/docs/src/tutorials/assets/loading_color.gif new file mode 100644 index 000000000..7c3de43e9 Binary files /dev/null and b/docs/src/tutorials/assets/loading_color.gif differ diff --git a/docs/src/tutorials/assets/loading_movement.gif b/docs/src/tutorials/assets/loading_movement.gif new file mode 100644 index 000000000..76e877135 Binary files /dev/null and b/docs/src/tutorials/assets/loading_movement.gif differ diff --git a/docs/src/tutorials/assets/loading_with_friends.gif b/docs/src/tutorials/assets/loading_with_friends.gif new file mode 100644 index 000000000..3219790be Binary files /dev/null and b/docs/src/tutorials/assets/loading_with_friends.gif differ diff --git a/docs/src/tutorials/assets/min_atomic_info.gif b/docs/src/tutorials/assets/min_atomic_info.gif index ef92b4369..1599a97bb 100644 Binary files a/docs/src/tutorials/assets/min_atomic_info.gif and b/docs/src/tutorials/assets/min_atomic_info.gif differ diff --git a/docs/src/tutorials/assets/sineio_plot.png b/docs/src/tutorials/assets/sineio_plot.png new file mode 100644 index 000000000..89c88a965 Binary files /dev/null and b/docs/src/tutorials/assets/sineio_plot.png differ diff --git a/docs/src/tutorials/tutorial_1.md b/docs/src/tutorials/tutorial_1.md index 764a8ffbe..b614c6524 100644 --- a/docs/src/tutorials/tutorial_1.md +++ b/docs/src/tutorials/tutorial_1.md @@ -28,7 +28,7 @@ With all that said, let's dive into this tutorial! โœจ `Javis.jl` is an abstraction on top of powerful graphics tools to make animations and visualizations easy to create. It is built on top of the fantastic Julia drawing packages, [`Luxor.jl`](https://github.com/JuliaGraphics/Luxor.jl) and [`Cairo.jl`](https://github.com/JuliaGraphics/Cairo.jl). `Cairo.jl` is much too complex to explain here, but `Luxor.jl` gives one the ability to define and draw on a canvas. -`Luxor.jl` provides simple functions like `line`, `circle` and `Draw` by which one can make animations. +`Luxor.jl` provides simple functions like `line`, `circle` and `poly` by which one can make animations. > **NOTE:** If you're interested in 2D graphics, you should definitely check out the awesome `Luxor.jl` package. > It has a [great tutorial](https://juliagraphics.github.io/Luxor.jl/stable/tutorial/) that will give you an even greater understanding of how `Javis.jl` works. diff --git a/docs/src/tutorials/tutorial_2.md b/docs/src/tutorials/tutorial_2.md index fc0b20f5b..03ad8e369 100644 --- a/docs/src/tutorials/tutorial_2.md +++ b/docs/src/tutorials/tutorial_2.md @@ -81,6 +81,71 @@ The following invocation will create the head: ... ``` +`Action` objects consist of at least one part, namely calling a function which draws something on to the canvas. +`Action` objects are fully comprised of `frames` (which can be optional), an optional `id`, a `function` that draws something on a canvas, an optional `Animation`, an optional `Movement`, and optional `SubAction` definitions. + +### Frames + +The default of an `Action` is to use the same frames as a previous `Action`. +Besides that there are three other options: + +- Define the range explicitly i.e `1:100`. +- Use the default or explicitly write `:same` into the unit range location which means the same frames as before +- Use [`Rel`](@ref) to specify it relative to the previous frame + - `Rel(10)` which is short for `Rel(1:10)` after an `Action` which is defined for `1:100` would mean `101:110`. + You just want to make sure that you don't use frame numbers higher than the `BackgroundAction`. + +### Action ID + +The action id which in the above example is `:head` can be used to store a return value from the drawing function you call. +This can be used in later actions such that two actions can interact. +One example was shown in the [previous tutorial](tutorial_1.md) where one object rotated around another one. +In this tutorial it's basically used as a comment to keep track of what each `Action` is creating. + +### Function + +As mentioned the most important part of each [`Action`](@ref) is to define what should be drawn in these frames. +Javis calls these functions with three arguments: `video`, `action` and `framenumber`. +Normally this is not needed but instead one defines own arguments. +In this case the following syntax is used. + +```julia +(args...) -> my_function(argument_1, argument_2, ..., argument_X) +``` + +In [Tutorial 1](tutorial_1.md), it showed that `my_function` can be a Luxor function or a function which calls some Luxor functions to draw on the canvas. + +### Animation + +An `Action` can be used to define a simple movement of what can be drawn. +An example for this was shown in the [previous tutorial](tutorial_1.md) where objects rotate. +This movement is normally linear which is rather dull. +Therefore, it's possible to define the speed using so called easing functions (for more info, see [Tutorial 6](tutorial_6.md). + +### Movement + +It's possible to give an `Action`, a movement that persists over the entirety of its frames. +This can be done using [`Translation`](@ref), [`Rotation`](@ref) and [`Scaling`](@ref). +However, it is suggested, and for more complex movements required, to use [`SubAction`](@ref)s to steer the animation of an `Action`. + +### SubAction + +Actions have one more additional keyword argument called `subactions`. +A `SubAction` is used to have fine grained control of how an objects can move from frame to frame. +[Tutorial 4](tutorial_4.md) and [tutorial 6](tutorial_6.md) explain more about a `SubAction`. + +Now that those explanations are out of the way, back to the brain! + +The code + +```julia +... + Action(:same, :head, (args...) -> circ(O, "black", :stroke, 170)), +... +``` + +creates + ![](assets/head.gif) Now we are getting a - _head_! ๐Ÿ˜ƒ @@ -230,7 +295,7 @@ Also, we need to define the radius of our electrodes; we will set that to 15: ```julia ... - radius = 15 + radius = 15 # needs to be defined before calling `javis` Action( :electrodes, (args...) -> @@ -354,7 +419,7 @@ Once everything is executed, we get this very nice and clean looking animation w ## Conclusion -Congratulations! ๐ŸŽ‰ ๐ŸŽ‰ ๐ŸŽ‰ +Congratulations! ๐ŸŽ‰ ๐ŸŽ‰ ๐ŸŽ‰ You made a brain! To recap, by working through this animation you should now: diff --git a/docs/src/tutorials/tutorial_3.md b/docs/src/tutorials/tutorial_3.md index 288aff8ec..3f9f3f5fd 100644 --- a/docs/src/tutorials/tutorial_3.md +++ b/docs/src/tutorials/tutorial_3.md @@ -1,7 +1,6 @@ # **Tutorial 3:** Rendering LaTeX with Javis! This is a rather brief tutorial about an exciting functionality of `Javis.jl`: the ability to render $\LaTeX$! -By the end of this tutorial, you will be able to create If you have never heard of `LaTeX` before, we highly recommend the following resources: @@ -107,7 +106,7 @@ The `latex` function is called to render a `LaTeXString` object. This particular string makes a matrix! The last two arguments position the latex string in the top left corner. -> **Note:** The default position is the origin (default: the center of the canvas) +> **NOTE:** The default position is the origin (default: the center of the canvas) We can run this code block to render the `LaTeX`: @@ -128,7 +127,7 @@ Let's spice it up! ## Throw it in the Blender! -A fun function that `Javis` provides is the ability to blend colors together! +A fun function that `Luxor` provides is the ability to blend colors together! To do so, let's modify the `draw_latex` function: ```julia @@ -148,8 +147,8 @@ end ``` The biggest change is that we added the `blend` and `setblend` functions. -The `blend` function creates a linear blend between two points using two given colors - in this case, black and red. -The `setblend` function applies the blend to the drawn object. +`blend` creates a linear blend between two points using two given colors - in this case, black and red. +`setblend` applies the blend to the drawn object. We also use the `translate` function this time as it makes writing the `blend` function easier. Can you guess what happens when we execute the code with this newly updated `draw_latex` function? diff --git a/docs/src/tutorials/tutorial_4.md b/docs/src/tutorials/tutorial_4.md index 08aef86e5..855635749 100644 --- a/docs/src/tutorials/tutorial_4.md +++ b/docs/src/tutorials/tutorial_4.md @@ -88,7 +88,7 @@ Action(16:150, (args...)->circle(O, 100, :stroke); subactions=[ This is very similar to the previous action. Here we can see that `SubAction` uses relative frame numbers such that the head appears in the frames `16:30` and then is at full opacity afterwards. ->> **Note:** Just a small refresher: We need the anonymous function `(args...)->circle(O, 100, :stroke)` as each function gets called with the three arguments `video, action, frame`. +> **NOTE:** Just a small refresher: We need the anonymous function `(args...)->circle(O, 100, :stroke)` as each function gets called with the three arguments `video, action, frame`. ### The Power of Splatting diff --git a/docs/src/tutorials/tutorial_5.md b/docs/src/tutorials/tutorial_5.md index 93bcf0596..107f88a19 100644 --- a/docs/src/tutorials/tutorial_5.md +++ b/docs/src/tutorials/tutorial_5.md @@ -89,11 +89,16 @@ Further, it helps one to keep track of units and easily convert between differen As always, let's import our needed packages: ```julia +using Animations using Javis using PeriodicTable using Unitful ``` +> **NOTE:** For this tutorial, we will also use `Animations.jl` to provide what are called "easing functions". +These are used to control the speed at which an animation is drawn. +This is further explained in [Tutorial 6](tutorial_6.md) so for now, don't worry too much about what we are doing with it. + And let's define our background function. This background function will also write the current frame being drawn: @@ -165,6 +170,21 @@ From there, we need to define more `Action` objects for our `javis` function for This will grow our element from `1` to `12`, from `12` to `20`, `20` to `7`, and finally `7` to `1`. +> **IMPORTANT:** `Scaling` does not really scale an object but instead the entire canvas the object is drawn on. +> This produces the desired effect for two reasons: +> 1. The `Action` is inside a Luxor layer which means that scaling inside this layer does not scale elements outside the layer (e.g. the frame counter in the upper right corner). +> 2. As it scales the canvas and not the `Action`, scaling only works nicely if the action is defined at the origin. +If you want to display the element somewhere else, for example, you should **not** change in the following snippet +> ```julia +> function element(;color = "black") +> sethue(color) +> circle(O, 4, :fill) +> end +> ``` +> the point where the circle appears by changing `O` to the desired `Point(x, y)`. +> Instead use another `SubAction`, like `SubAction(1:1, Translation(x, y))`, to move the origin of the object to the desired location. +> Then scaling will work fine as it is defined on the first frame only. + That scaling looks like this: ![](assets/blank_atom_scaling.gif) @@ -266,14 +286,35 @@ Of course, we need to further update our `javis` function to this: SubAction(521:550, Scaling(7, 1)) ] ), - Action(1:100, (args...) -> info_box(value = val(:atom))), - Action(141:240, (args...) -> info_box(value = val(:atom))), - Action(281:380, (args...) -> info_box(value = val(:atom))), - Action(421:520, (args...) -> info_box(value = val(:atom))), + Action( + 1:100, + (args...) -> info_box(value = val(:atom)), + subactions = [SubAction(1:30, sineio(), appear(:draw_text))], + ), + Action( + 141:240, + (args...) -> info_box(value = val(:atom)), + subactions = [SubAction(1:30, sineio(), appear(:draw_text))], + ), + Action( + 281:380, + (args...) -> info_box(value = val(:atom)), + subactions = [SubAction(1:30, sineio(), appear(:draw_text))], + ), + Action( + 421:520, + (args...) -> info_box(value = val(:atom)), + subactions = [SubAction(1:30, sineio(), appear(:draw_text))], + ), ... ``` The current scale of the circle object is now passed to the `info_box` function via the `:atom` symbol. +Furthermore, we use a `SubAction` to have the text appear using the method `appear(:draw_text)` and we control the speed at which is appears using `sineio()`. + +> **NOTE:** `sineio()` comes from `Animations.jl` and is an easing function. +More on this in [Tutorial 6](tutorial_6.md). + This produces the following animation: ![](assets/min_atomic_info.gif) @@ -326,6 +367,7 @@ Good luck and have fun making more animations! ## Full Code ```julia +using Animations using Javis using PeriodicTable using Unitful @@ -336,7 +378,7 @@ function ground(video, action, frame) text("$frame / 550", -240, -230) end -function element(;color = "black") +function element(; color = "black") sethue(color) circle(O, 4, :fill) return val(:_current_scale)[1] @@ -372,22 +414,39 @@ javis( demo, [ BackgroundAction(1:550, ground), - Action(1:550, + Action( + 1:550, :atom, (args...) -> element(), - subactions = [ + subactions = [ SubAction(101:140, Scaling(1, 12)), SubAction(241:280, Scaling(12, 20)), SubAction(381:420, Scaling(20, 7)), - SubAction(521:550, Scaling(7, 1)) - ] + SubAction(521:550, Scaling(7, 1)), + ], + ), + Action( + 1:100, + (args...) -> info_box(value = val(:atom)), + subactions = [SubAction(1:30, sineio(), appear(:draw_text))], + ), + Action( + 141:240, + (args...) -> info_box(value = val(:atom)), + subactions = [SubAction(1:30, sineio(), appear(:draw_text))], + ), + Action( + 281:380, + (args...) -> info_box(value = val(:atom)), + subactions = [SubAction(1:30, sineio(), appear(:draw_text))], + ), + Action( + 421:520, + (args...) -> info_box(value = val(:atom)), + subactions = [SubAction(1:30, sineio(), appear(:draw_text))], ), - Action(1:100, (args...) -> info_box(value = val(:atom))), - Action(141:240, (args...) -> info_box(value = val(:atom))), - Action(281:380, (args...) -> info_box(value = val(:atom))), - Action(421:520, (args...) -> info_box(value = val(:atom))), ], - pathname = "atomic.gif", + pathname = "min_atomic.gif", framerate = 10, ) ``` diff --git a/docs/src/tutorials/tutorial_6.md b/docs/src/tutorials/tutorial_6.md new file mode 100644 index 000000000..00452f5d8 --- /dev/null +++ b/docs/src/tutorials/tutorial_6.md @@ -0,0 +1,357 @@ +# **Tutorial 6:** Using Animations.jl to Create something with more Pep! + +In the last couple of tutorials you've learned the basics of Javis and some of the more advanced stuff like [`SubAction`](@ref). + +## Our goal + +The goal of this tutorial is to explain a new feature we have added in v0.2 of Javis. Before this every animation was basically linear. +What I mean by this is: If you move an object from `A` to `B` using [`Translation`](@ref) it would do so in a linear and therefore boring fashion. + +We'll create an interesting loading animation for this. It consists of five circles which move from the center to the outside rotate around the center and back to the origin. During this process they appear and disappear as well as changing color. + +## Learning Outcomes + +This tutorial shows some more power of subactions that we introduced in v0.2. Combined with the power of the awesome library [Animations.jl](https://github.com/jkrumbiegel/Animations.jl) you have very fine grained control over the objects you animate and understand the ease of its easing functions. ๐Ÿ˜„ + +Today you'll learn how to +- use easing functions to have animations with pep +- have full control over the animation of objects +- create a beautiful (if I can say so myself) loading animation + +## Moving with Different Speed + +The main point of this tutorial is to explain how to basically "interact" with objects with a non-linear speed. Our first step will be to rotate a circle around the origin at a increasing and then decreasing speed. + +Let's start with the boring linear animation: + +```julia +using Javis + +function ground(args...) + background("black") + sethue("white") +end + +video = Video(600, 400) + +actions = [ + Action( + (args...) -> circle(Point(150, 0), 20, :fill); + subactions = [ + SubAction(Rotation(0.0, 2ฯ€)), + ], + ) +] + +javis( + video, + [BackgroundAction(1:200, ground), actions...], + pathname = "loading.gif", +) +``` + +![Linear movement](assets/loading_circle_linear.gif) + +I would say that this looks a bit dull. Let us rotate it with varying speeds. For this I'll use the `sineio` easing function. You can try another easing function. +They are all described [here](https://jkrumbiegel.github.io/Animations.jl/stable/#Easings-1). +Easing functions basically describe how to interpolate between the values. If one goes from `0` to `5` in the time range of `0` to `1`. It can be at `2.5` at `t=0.5` or it can start slowly and speed up until it reaches the final value of `5` such that at `t=0.5` it is only at let say `1.5`. +This way one can describe the speed/acceleration of the object. + +First of all we need `Animations` for this and I also load `Colors` here for later. + +These can be installed via `] add Animations, Colors` inside the REPL. + +- **Animations.jl** defines easing functions such that we can have non-linear movement +- **Colors.jl** defines colors and gives access to different color spaces such that we can transition from a color to another by using Animations.jl. + +```julia +using Javis, Animations, Colors + +function ground(args...) + background("black") + sethue("white") +end + +rotate_anim = Animation( + [0, 1], # must go from 0 to 1 + [0, 2ฯ€], + [sineio()], +) + +video = Video(600, 400) + +actions = [ + Action( + (args...) -> circle(Point(150, 0), 20, :fill); + subactions = [ + SubAction(rotate_anim, rotate()), + ], + ) +] + +javis( + video, + [BackgroundAction(1:200, ground), actions...], + pathname = "loading.gif", +) +``` + +![Rotation with varying speed](assets/loading_circle_sineio.gif) + +I think that looks more interesting. The [Animations.jl](https://jkrumbiegel.github.io/Animations.jl/stable) package is very powerful so you might wanna head over to their documentation and try different things. + +Nevertheless, let me explain this part a bit and you can check out the documentation for more details. + +``` +rotate_anim = Animation( + [0, 1], # must go from 0 to 1 + [0, 2ฯ€], + [sineio()], +) +``` + +The `Animation` function takes in three arguments which are all vectors. +1. Describe the time stamps. They should always go from `0` to `1` inside Javis. +2. The values at the different time stamps. Here we start at 0 radians and end with 2ฯ€ radians. +3. The easing functions describe how to move from one value to the next. It must be always one less than the number of time stamps/values + +We can actually have a look at this in a graphical plot: + +```julia +using Animations +using Plots + +rotate_anim = Animations.Animation( + [0, 1], # must go from 0 to 1 + [0, 2ฯ€], + [sineio()], +) + +ts = 0:0.01:1 +ys = at.(rotate_anim, ts) + +plot(ts, ys; labels=false, xaxis="t", yaxis="value") +``` + +![Sineio plot](assets/sineio_plot.png) + + +## Precise Movement + +Okay we now know how to rotate with a different speed but let's do what we actually wanted. Moving out from the center, rotate and then move back to the center. +The code gets a bit longer from time to time so I'll only add changes from now on in the following way. +If I add something called `_anim` you can put it directly after `rotate_anim`. I'll otherwise only change the `actions` array. + +Our new animations: + +One translating from the origin to the right + +```julia +translate_anim = Animation( + [0, 1], # must go from 0 to 1 + [O, Point(150, 0)], + [sineio()], +) +``` + +and one translating back. One needs to make sure that it always starts at the origin (the current origin of the already translated view) to not introduce a break in the animation. More about that later. + +```julia +translate_back_anim = Animation( + [0, 1], # must go from 0 to 1 + [O, Point(-150, 0)], + [sineio()], +) +``` + +and our `actions` (yes it's still a single [`Action`](@ref) but we add more soon) + +```julia +actions = [ + Action( + (args...) -> circle(O, 20, :fill); + subactions = [ + SubAction(1:50, translate_anim, translate()), + SubAction(51:150, rotate_anim, rotate_around(Point(-150, 0))), + SubAction(151:200, translate_back_anim, translate()), + ], + ) +] +``` + +I changed the starting position of the circle to `O` and we now have three subactions. + +Let's walk through the steps Javis takes to create the animation: +- For the first 50 frames of the action the circle is translated from `O` to `(150, 0)` so just to the right. + - Remember the `O` is the same as `(0, 0)` and is at the center of the canvas at the beginning. + - Then the circle is at position `(150, 0)`. Remember that it is still drawn at the origin so actually we shifted our whole view to the right. +- Therefore for the next 100 frames we need to specify that we actually want to rotate around the world origin which is at `Point(-150, 0)` from our current perspective. +- Afterwards we are now again at `(150, 0)` but see it as our origin and therefore need to move our circle to the left to `Point(-150, 0)` which is the world origin. + +Hope that makes sense! Let's see it in action: + +![The loading movement](assets/loading_movement.gif) + +## Adding more Blobs! + +The blob does start to feel lonely a bit so let's give him some friends. They should all do the same movement but start at different times. + +```julia +actions = [ + Action( + frame_start:frame_start+149, + (args...) -> circle(O, 20, :fill); + subactions = [ + SubAction(1:30, translate_anim, translate()), + SubAction(31:120, rotate_anim, rotate_around(Point(-150, 0))), + SubAction(121:150, translate_back_anim, translate()), + ], + ) for frame_start in 1:10:50 +] +``` + +I've basically added four more blobs by using `for frame_start in 1:10:50` inside the array definition. It's a basic list comprehension which might be hard to get at first as it's spreading over several lines. + +Then I defined the start of the `Action` with: `frame_start:frame_start+149` such that every blob is there for 150 frames and reduced the number of frames in the subactions a bit to have 150 frames. + +![The loading movement with some friends](assets/loading_with_friends.gif) + +## How about Color? + +Okay everything is dull when we only use white and black. Let's make it such that our blobs change color from red over cyan to black which also make them disappear. + +```julia +color_anim = Animation( + [0, 0.5, 1], # must go from 0 to 1 + [Lab(colorant"red"), Lab(colorant"cyan"), Lab(colorant"black")], + [sineio(), sineio()], +) +``` + +This time we actually have not only a start and end point of our animation but a point in between. We therefore have three timestamps `0.0` , `0.5` and `1.0`. +Our three colors red, cyan and black. You can play with different colors and color spaces if you want. +And we need two easing functions: One defines the movement from red to cyan and the second from cyan to black. + +```julia +actions = [ + Action( + frame_start:frame_start+149, + (args...) -> circle(O, 20, :fill); + subactions = [ + SubAction(1:30, translate_anim, translate()), + SubAction(31:120, rotate_anim, rotate_around(Point(-150, 0))), + SubAction(121:150, translate_back_anim, translate()), + SubAction(1:150, color_anim, sethue()), + ], + ) for frame_start in 1:10:50 +] +``` + +The change in color is over the entire action in this case. + +![Loading with color](assets/loading_color.gif) + +I think that already looks quite nice. The appearance of the blobs is a bit off though. How about fading them in by scaling them up? + +```julia +actions = [ + Action( + frame_start:frame_start+149, + (args...) -> circle(O, 20, :fill); + subactions = [ + SubAction(1:10, sineio(), appear(:scale)), + SubAction(11:40, translate_anim, translate()), + SubAction(41:120, rotate_anim, rotate_around(Point(-150, 0))), + SubAction(121:150, translate_back_anim, translate()), + SubAction(1:150, color_anim, sethue()), + ], + ) for frame_start in 1:10:50 +] +``` + +A deeper look into the line we added? +```julia +SubAction(1:10, sineio(), appear(:scale)), +``` + +If we want to animate from `0` to `1` we don't need to write an `Animation` object for this and can simply specify the easing function. +Additionally we use the `appear(:scale)` which does the same thing as: + +```julia +SubAction(1:10, sineio(), scale()), +``` + +but it might be easier to read when we attach the meaning of `appear` to it. + +Maybe have a look at [`appear`](@ref) and [`disappear`](@ref). + +![The loading animation](assets/loading.gif) + +## Conclusion + +To recap, by working through this animation you should now: + +1. Understand how to make objects move in a non-linear way using Animations.jl +2. Be able to scale, translate, rotate objects in a finer control +3. Have fun with colors! + +## The Code + +```julia +using Javis, Animations, Colors + +function ground(args...) + background("black") + sethue("white") +end + +rotate_anim = Animation( + [0, 1], # must go from 0 to 1 + [0, 2ฯ€], + [sineio()], +) + +translate_anim = Animation( + [0, 1], # must go from 0 to 1 + [O, Point(150, 0)], + [sineio()], +) + +translate_back_anim = Animation( + [0, 1], # must go from 0 to 1 + [O, Point(-150, 0)], + [sineio()], +) + +color_anim = Animation( + [0, 0.5, 1], # must go from 0 to 1 + [Lab(colorant"red"), Lab(colorant"cyan"), Lab(colorant"black")], + [sineio(), sineio()], +) + +video = Video(600, 400) + +actions = [ + Action( + frame_start:frame_start+149, + (args...) -> circle(O, 20, :fill); + subactions = [ + SubAction(1:10, sineio(), appear(:scale)), + SubAction(11:40, translate_anim, translate()), + SubAction(41:120, rotate_anim, rotate_around(Point(-150, 0))), + SubAction(121:150, translate_back_anim, translate()), + SubAction(1:150, color_anim, sethue()), + ], + ) for frame_start in 1:10:50 +] + +javis( + video, + [BackgroundAction(1:200, ground), actions...], + pathname = "loading.gif", +) +``` + +> **Author(s):** Ole Krรถger \ +> **Date:** September 10th, 2020 \ +> **Tag(s):** loading, subactions, Animations.jl \ No newline at end of file diff --git a/examples/follow_path.jl b/examples/follow_path.jl new file mode 100644 index 000000000..d49737c61 --- /dev/null +++ b/examples/follow_path.jl @@ -0,0 +1,86 @@ +using Javis, Animations + +function ground(args...) + background("white") + sethue("black") +end + +function circle_with_color(pos, radius, color) + sethue(color) + circle(pos, radius, :fill) +end + +# the safest option is to declare the Video first all the time +video = Video(600, 600) + +# the points that describe our car +points = [ + Point(-200, 20), + Point(-170, 20), + Point(-160, 10), + Point(-150, 10), + Point(-140, 20), + Point(140, 20), + Point(150, 10), + Point(160, 10), + Point(170, 20), + Point(200, 20), + Point(180, -20), + Point(100, -60), + Point(80, -70), + Point(60, -90), + Point(0, -90), + Point(-90, -40), + Point(-190, -20), +] +npoints = length(points) + +# let the points appear one by one +draw_points = [ + Action( + frame_start:200, + (args...) -> circle(O, 10, :fill); + subactions = [ + # easiest to move canvas to draw at origin + SubAction(1:1, Translation(points[i])), + SubAction(1:5, appear(:scale)), + SubAction((100 - frame_start):(100 - frame_start + 20), Scaling(0.5)), + SubAction((200 - frame_start - 10):(200 - frame_start), disappear(:scale)), + ], + ) for (frame_start, i) in zip(1:2:(2 * npoints), 1:npoints) +] + +# generate the bezier path +bezierpath = makebezierpath(points) +bezierpathpoly = bezierpathtopoly(bezierpath) + +# let the bezier path appear and disappear in the end +draw_bezier = Action( + (2 * npoints + 10):200, + (args...) -> drawbezierpath(bezierpath, :stroke); + subactions = [ + SubAction(1:10, appear(:fade)), + SubAction( + (200 - (2 * npoints + 10) - 10):(200 - (2 * npoints + 10)), + disappear(:fade), + ), + ], +) + +# let a red circle appear and follow the bezier path polygon +circle_action = Action( + 120:220, + (args...) -> circle_with_color(first(points), 10, "red"); + subactions = [ + SubAction(1:20, appear(:fade)), + SubAction(21:70, sineio(), follow_path(bezierpathpoly .- first(points))), + SubAction(71:80, disappear(:fade)), + ], +) + +# render everything using javis +javis( + video, + [BackgroundAction(1:220, ground), draw_points..., draw_bezier, circle_action]; + pathname = "gifs/follow_bezier_path.gif", +) diff --git a/examples/gifs/follow_bezier_path.gif b/examples/gifs/follow_bezier_path.gif new file mode 100644 index 000000000..c10ff8166 Binary files /dev/null and b/examples/gifs/follow_bezier_path.gif differ diff --git a/src/Javis.jl b/src/Javis.jl index 15856b79a..6ce7e6326 100644 --- a/src/Javis.jl +++ b/src/Javis.jl @@ -1,103 +1,32 @@ module Javis +using Animations +using Cairo: CairoImageSurface, image +using ColorTypes: ARGB32 using FFMPEG +using Gtk +using GtkReactive +using Images using LaTeXStrings using LightXML import Luxor import Luxor: Point, @layer +using ProgressMeter using Random +using VideoIO const FRAMES_SYMBOL = [:same] -""" - Video - -Defines the video canvas for an animation. - -# Fields -- `width::Int` the width in pixel -- `height::Int` the height in pixel -- `defs::Dict{Symbol, Any}` Some definitions which should be accessible throughout the video. -""" -mutable struct Video - width::Int - height::Int - defs::Dict{Symbol,Any} -end - -""" - CURRENT_VIDEO - -holds the current video in an array to be declared as a constant -The current video can be accessed using CURRENT_VIDEO[1] -""" -const CURRENT_VIDEO = Array{Video,1}() - - -""" - Video(width, height) - -Create a video with a certain `width` and `height` in pixel. -This also sets `CURRENT_VIDEO`. -""" -function Video(width, height) - video = Video(width, height, Dict{Symbol,Any}()) - if isempty(CURRENT_VIDEO) - push!(CURRENT_VIDEO, video) - else - CURRENT_VIDEO[1] = video - end - return video -end - -""" - Rel - -Ability to define frames in a relative fashion. - -# Example -``` - Action(1:100, ground; in_global_layer=true), - Action(1:90, :red_ball, (args...)->circ(p1, "red"), Rotation(from_rot, to_rot)), - Action(Rel(10), :blue_ball, (args...)->circ(p2, "blue"), Rotation(2ฯ€, from_rot, :red_ball)), - Action((video, args...)->path!(path_of_red, pos(:red_ball), "red")) -``` -is the same as -``` -Action(1:100, ground; in_global_layer=true), -Action(1:90, :red_ball, (args...)->circ(p1, "red"), Rotation(from_rot, to_rot)), -Action(91:100, :blue_ball, (args...)->circ(p2, "blue"), Rotation(2ฯ€, from_rot, :red_ball)), -Action(91:100, (video, args...)->path!(path_of_red, pos(:red_ball), "red")) -``` - -# Fields -- rel::UnitRange defines the frames in a relative fashion. -""" -struct Rel - rel::UnitRange -end - -""" - Rel(i::Int) - -Shorthand for Rel(1:i) -""" -Rel(i::Int) = Rel(1:i) +abstract type Transition end +abstract type InternalTransition end -""" - Frames +abstract type AbstractAction end -Stores the actual computed frames and the user input -which can be i.e. `:same` or `Rel(10)`. -The `frames` are computed in `javis`. -""" -mutable struct Frames{T} - frames::Union{Nothing,UnitRange} - user::T -end +include("structs/Video.jl") +include("structs/Easing.jl") +include("structs/Rel.jl") +include("structs/Frames.jl") -Base.convert(::Type{Frames}, x::Union{Symbol,Rel}) = Frames(nothing, x) -Base.convert(::Type{Frames}, x::UnitRange) = Frames(x, x) """ Transformation @@ -117,442 +46,11 @@ mutable struct Transformation angle::Float64 end -abstract type Transition end -abstract type InternalTransition end - -abstract type AbstractAction end - -""" - SubAction <: AbstractAction - -A SubAction can be used in the keyword arguments of an [`Action`](@ref) to define small -sub actions on the action function, such as [`appear`](@ref). - -A SubAction should not be created by hand but instead by using one of the constructors. - -# Fields -- `frames::Frames`: the frames relative to the parent [`Action`](@ref) -- `func::Function`: the function that gets called in each of those frames. - Takes the following arguments: `video, action, subaction, rel_frame` -- `transitions::Vector{Transition}`: A list of transitions like [`Translation`](@ref) -- `internal_transitions::Vector{InternalTransition}`: - A list of internal transitions which store the current transition for a specific frame. -""" -mutable struct SubAction <: AbstractAction - frames::Frames - func::Function - transitions::Vector{Transition} - internal_transitions::Vector{InternalTransition} -end - -SubAction(transitions::Transition...) = SubAction(:same, transitions...) -SubAction(func::Function) = SubAction(:same, func) - -""" - SubAction(frames, func::Function) - -A `SubAction` can be defined with frames and a function -inside the `subactions` kwarg of an [`Action`](@ref). -In the following example a filled circle with radius 50 appears in the first 20 frames, -which means the opacity is increased from 0 to 1.0. -Then it stays at full opacity and disappears the same way in the last 20 frames. - -# Example -javis(demo, [ - BackgroundAction(1:100, ground), - Action((args...)->circle(O, 50, :fill); subactions = [ - SubAction(1:20, appear(:fade)), - SubAction(81:100, disappear(:fade)) - ]) -]) - -# Arguments -- `frames`: A list of frames for which the function should be called. - - The frame numbers are relative to the parent [`Action`](@ref). -- `func::Function`: The function that gets called for the frames. - - Needs to have four arguments: `video, action, subaction, rel_frame` - - For [`appear`](@ref) and [`disappear`](@ref) a closure exists, - such that `appear(:fade)` works. -""" -SubAction(frames, func::Function) = SubAction(frames, func, [], []) - -""" - SubAction(frames, trans::Transition...) - -A `SubAction` can also be defined this way with having a list of transitions. -This is similar to defining transitions inside [`Action`](@ref) - -In the following example a circle is faded in during the first 25 frames then moves to -- `Point(100, 20)` then to `Point(120, -20)` (the translations are added) -- and then back to the origin -In the last 25 frames it disappears from the world. - -# Example -``` -javis(demo, [ - BackgroundAction(1:200, ground_opacity), - Action((args...)->circle(O, 50, :fill); subactions = [ - SubAction(1:25, appear(:fade)), - SubAction(26:75, Translation(Point(100, 20))), - SubAction(76:100, Translation(Point(20, -40))), - SubAction(101:175, Translation(Point(-120, 20))), - SubAction(176:200, disappear(:fade)) - ]), - ], tempdirectory="current/images", pathname="current/circle_square.gif") -``` - -# Arguments -- `frames`: A list of frames for which the function should be called. - - The frame numbers are relative to the parent [`Action`](@ref). -- `trans::Transition...`: A list of transitions that shall be performed. -""" -SubAction(frames, trans::Transition...) = - SubAction(frames, (args...) -> 1, collect(trans), []) - -""" - ActionSetting - -The current settings of an [`Action`](@ref) which are saved in `action.current_setting`. - -# Fields -- `line_width::Float64`: the current line width -- `mul_line_width::Float64`: the current multiplier for line width. - The actual line width is then: `mul_line_width * line_width` -- `opacity::Float64`: the current opacity -- `mul_opacity::Float64`: the current multiplier for opacity. - The actual opacity is then: `mul_opacity * opacity` -- `fontsize::Float64` the current font size -- `current_scale::Tuple{Float64, Float64}`: the current scale -- `desired_scale::Tuple{Float64, Float64}`: the new desired scale -- `mul_scale::Float64`: the multiplier for the new desired scale. - The actual new scale is then: `mul_scale * desired_scale` -""" -mutable struct ActionSetting - line_width::Float64 - mul_line_width::Float64 # the multiplier of line width is between 0 and 1 - opacity::Float64 - mul_opacity::Float64 # the multiplier of opacity is between 0 and 1 - fontsize::Float64 - # scale has three fields instead of just the normal two - # current scale - # desired scale and scale multiplier => `desired_scale*mul_scale` is the new desired scale - # the scale change needs to be computed using `current_scale` and the desired scale - current_scale::Tuple{Float64,Float64} - desired_scale::Tuple{Float64,Float64} - mul_scale::Float64 # the multiplier of scale is between 0 and 1 -end - -ActionSetting() = ActionSetting(1.0, 1.0, 1.0, 1.0, 10.0, (1.0, 1.0), (1.0, 1.0), 1.0) - -""" - update_ActionSetting!(as::ActionSetting, by::ActionSetting) - -Set the fields of `as` to the same as `by`. Basically copying them over. -""" -function update_ActionSetting!(as::ActionSetting, by::ActionSetting) - as.line_width = by.line_width - as.mul_line_width = by.mul_line_width - as.opacity = by.opacity - as.mul_opacity = by.mul_opacity - as.fontsize = by.fontsize - as.current_scale = by.current_scale - as.desired_scale = by.desired_scale - as.mul_scale = by.mul_scale -end - -""" - Action - -Defines what is drawn in a defined frame range. - -# Fields -- `frames::Frames`: A range of frames for which the `Action` is called -- `id::Union{Nothing, Symbol}`: An id which can be used to save the result of `func` -- `func::Function`: The drawing function which draws something on the canvas. - It gets called with the arguments `video, action, frame` -- `transitions::Vector{Transition}` a list of transitions - which can be performed before the function gets called. -- `internal_transitions::Vector{InternalTransition}`: - Similar to `transitions` but holds the concrete information whereas `Transition` can - hold links to other actions which need to be computed first. - See [`compute_transformation!`](@ref) -- `opts::Any` can hold any options defined by the user -""" -mutable struct Action <: AbstractAction - frames::Frames - id::Union{Nothing,Symbol} - func::Function - transitions::Vector{Transition} - internal_transitions::Vector{InternalTransition} - subactions::Vector{SubAction} - current_setting::ActionSetting - opts::Dict{Symbol,Any} -end - -""" - set_frames!(a::AbstractAction, last_frames::UnitRange) - -Compute the frames based on a.frames and `last_frames`. -Save the result in `a.frames.frames` which can be accessed via [`get_frames`](@ref). -""" -function set_frames!(a::AbstractAction, last_frames::UnitRange) - frames = a.frames.user - a.frames.frames = get_frames(frames, last_frames) -end - -""" - get_frames(a::AbstractAction) - -Return `a.frames.frames` which holds the computed frames for the AbstractAction `a`. -""" -get_frames(a::AbstractAction) = a.frames.frames - -""" - get_frames(frames::Symbol, last_frames::UnitRange) - -Get the frames based on a symbol (currently only `same`) and the `last_frames`. -Throw `ArgumentError` if symbol is unknown -""" -function get_frames(frames::Symbol, last_frames::UnitRange) - if frames === :same - return last_frames - else - throw(ArgumentError("Currently the only symbol supported for defining frames is `:same`")) - end -end - -""" - get_frames(frames::Rel, last_frames::UnitRange) - -Return the frames based on a relative frames [`Rel`](@ref) object and the `last_frames`. -""" -function get_frames(frames::Rel, last_frames::UnitRange) - start_frame = last(last_frames) + first(frames.rel) - last_frame = last(last_frames) + last(frames.rel) - return start_frame:last_frame -end - -""" - CURRENT_ACTION - -holds the current action in an array to be declared as a constant -The current action can be accessed using CURRENT_ACTION[1] -""" -const CURRENT_ACTION = Array{Action,1}() - - -""" - Action(frames, func::Function, args...) - -The most simple form of an action (if there are no `args`/`kwargs`) just calls -`func(video, action, frame)` for each of the frames it is defined for. -`args` are defined it the next function definition and can be seen in action - in this example [`javis`](@ref) -""" -Action(frames, func::Function, args...; kwargs...) = - Action(frames, nothing, func, args...; kwargs...) - -""" - Action(frames_or_id::Symbol, func::Function, args...) - -This function decides whether you wrote `Action(frames_symbol, ...)`, - or `Action(id_symbol, ...)` -If the symbol `frames_or_id` is not a `FRAMES_SYMBOL` then it is used as an id_symbol. -""" -function Action(frames_or_id::Symbol, func::Function, args...; kwargs...) - if frames_or_id in FRAMES_SYMBOL - Action(frames_or_id, nothing, func, args...; kwargs...) - else - Action(:same, frames_or_id, func, args...; kwargs...) - end -end - -""" - Action(func::Function, args...) - -Similar to the above but uses the same as frames as the action above. -""" -Action(func::Function, args...; kwargs...) = - Action(:same, nothing, func, args...; kwargs...) - -""" - Action(frames, id::Union{Nothing,Symbol}, func::Function, - transitions::Transition...; kwargs...) - -# Arguments -- `frames`: defines for which frames this action is called -- `id::Symbol`: Is used if the `func` returns something which - shall be accessible by other actions later -- `func::Function` the function that is called after the `transitions` are performed -- `transitions::Transition...` a list of transitions that are performed before - the function `func` itself is called - -The keywords arguments will be saved inside `.opts` as a `Dict{Symbol, Any}` -""" -function Action( - frames, - id::Union{Nothing,Symbol}, - func::Function, - transitions::Transition...; - kwargs..., -) - CURRENT_VIDEO[1].defs[:last_frames] = frames - opts = Dict(kwargs...) - subactions = SubAction[] - if haskey(opts, :subactions) - subactions = opts[:subactions] - delete!(opts, :subactions) - end - Action(frames, id, func, collect(transitions), [], subactions, ActionSetting(), opts) -end - -""" - BackgroundAction(frames, func::Function, args...; kwargs...) - -Create an Action where `in_global_layer` is set to true such that -i.e the specified color in the background is applied globally (basically a new default) -""" -function BackgroundAction(frames, func::Function, args...; kwargs...) - Action(frames, nothing, func, args...; in_global_layer = true, kwargs...) -end - -""" - BackgroundAction(frames, id::Symbol, func::Function, args...; kwargs...) - -Create an Action where `in_global_layer` is set to true and saves the return into `id`. -""" -function BackgroundAction(frames, id::Symbol, func::Function, args...; kwargs...) - Action(frames, id, func, args...; in_global_layer = true, kwargs...) -end - -mutable struct InternalTranslation <: InternalTransition - by::Point -end - -mutable struct InternalRotation <: InternalTransition - angle::Float64 - center::Point -end - -mutable struct InternalScaling <: InternalTransition - scale::Tuple{Float64,Float64} -end - -""" - Translation <: Transition - -Stores the `Point` or a link for the start and end position of the translation - -# Fields -`from::Union{Point, Symbol}`: The start position or a link to the start position. - See `:red_ball` in [`javis`](@ref) -`to::Union{Point, Symbol}`: The end position or a link to the end position -""" -struct Translation <: Transition - from::Union{Point,Symbol} - to::Union{Point,Symbol} -end - -""" - Translation(p::Union{Point, Symbol}) - -Create a `Translation(O, p)` such that a translation is done from the origin. -""" -Translation(p::Union{Point,Symbol}) = Translation(O, p) - -""" - Translation(x::Real, y::Real) - -Create a `Translation(O, Point(x,y))` such that a translation is done from the origin. -Shorthand for writing `Translation(Point(x,y))`. -""" -Translation(x::Real, y::Real) = Translation(Point(x, y)) - -""" - Rotation <: Transition - -Stores the rotation similar to [`Translation`](@ref) with `from` and `to` -but also the rotation point. - -# Fields -- `from::Union{Float64, Symbol}`: The start rotation or a link to it -- `to::Union{Float64, Symbol}`: The end rotation or a link to it -- `center::Union{Point, Symbol}`: The center of the rotation or a link to it. -""" -struct Rotation <: Transition - from::Union{Float64,Symbol} - to::Union{Float64,Symbol} - center::Union{Point,Symbol} -end - -""" - Rotation(r::Union{Float64, Symbol}) - -Rotation as a transition from 0.0 to `r` . -Can be used as a short-hand. -""" -Rotation(r::Union{Float64,Symbol}) = Rotation(0.0, r) - -""" - Rotation(r::Union{Float64, Symbol}, center::Union{Point, Symbol}) - -Rotation as a transition from `0.0` to `r` around `center`. -Can be used as a short-hand for rotating around a `center` point. -""" -Rotation(r::Union{Float64,Symbol}, center::Union{Point,Symbol}) = Rotation(0.0, r, center) - -""" - Rotation(from, to) - -Rotation as a transition from `from` to `to` (in radians) around the origin. -""" -Rotation(from, to) = Rotation(from, to, O) - -""" - Scaling <: Transition - -Stores the scaling similar to [`Translation`](@ref) with `from` and `to`. - -# Example -- Can be called with different constructors like: -``` -Scaling(10) -> Scaling(CURRENT_SCALING, (10.0, 10.0)) -Scaling(10, :my-scale) -> Scaling((10.0, 10.0), :my_scale) -Scaling(10, 2) -> Scaling((10.0, 10.0), (2.0, 2.0)) -Scaling(10, (1,2)) -> Scaling((10.0, 10.0), (1.0, 2.0)) -``` - -# Fields -- `from::Union{Tuple{Float64, Float64}, Symbol}`: The start scaling or a link to it -- `to::Union{Tuple{Float64, Float64}, Symbol}`: The end scaling or a link to it -- `compute_from_once::Bool`: Saves whether the from is computed for the first frame or - every frame. Is true if from is `:_current_scale`. -""" -mutable struct Scaling <: Transition - from::Union{Tuple{Float64,Float64},Symbol} - to::Union{Tuple{Float64,Float64},Symbol} - compute_from_once::Bool -end - -Scaling(to::Tuple) = Scaling(:_current_scale, to, true) -Scaling(to::Real) = Scaling(:_current_scale, convert(Float64, to), true) -Scaling(to::Symbol) = Scaling(:_current_scale, to, true) - -function Scaling(from::Real, to::Real, compute_from_once = false) - from_flt = convert(Float64, from) - to_flt = convert(Float64, to) - Scaling((from_flt, from_flt), (to_flt, to_flt), compute_from_once) -end - -function Scaling(from::Real, to, compute_from_once = false) - flt = convert(Float64, from) - Scaling((flt, flt), to, compute_from_once) -end +include("structs/SubAction.jl") +include("structs/ActionSetting.jl") +include("structs/Action.jl") +include("structs/Transitions.jl") -function Scaling(from, to::Real, compute_from_once = false) - flt = convert(Float64, to) - Scaling(from, (flt, flt), compute_from_once) -end """ Line @@ -587,343 +85,16 @@ function Base.:*(m::Array{Float64,2}, transformation::Transformation) return Transformation(Point(gettranslation(res)...), getrotation(res)) end -# cache such that creating svgs from LaTeX don't need to be created every time -# this is also used for test cases such that `tex2svg` doesn't need to be installed on Github Actions -const LaTeXSVG = Dict{LaTeXString,String}( - L"\mathcal{O}(\log{n})" => - "\nscript upper O left-parenthesis log n right-parenthesis\n\n\n\n\n\n\n\n\n\n\n \n \n\n \n \n \n\n \n \n\n", - L"\mathcal{O}\left(\frac{\log{x}}{2}\right)" => - "\nscript upper O left-parenthesis StartFraction log x Over 2 EndFraction right-parenthesis\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n \n\n\n\n\n \n \n \n \n\n \n\n\n \n\n\n", - L"E=mc^2" => - "\nupper E equals m c squared\n\n\n\n\n\n\n\n\n \n \n \n\n \n \n\n\n\n", - L"8" => - "\n8\n\n\n\n\n \n\n\n", - L"$\begin{equation}\left[\begin{array}{ccc}1 & 2 & 3 \\4 & 5 & 6 \\7 & 8 & 9 \\\end{array}\right]\end{equation}$" => - "\nStart 3 By 3 Matrix 1st Row 1st Column 1 2nd Column 2 3rd Column 3 2nd Row 1st Column 4 2nd Column 5 3rd Column 6 3rd Row 1st Column 7 2nd Column 8 3rd Column 9 EndMatrix\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n \n\n \n\n\n\n \n \n \n\n\n \n \n \n\n\n \n \n \n\n\n\n \n\n \n\n \n\n\n\n", -) - include("util.jl") include("luxor_overrides.jl") include("backgrounds.jl") include("svg2luxor.jl") include("morphs.jl") include("subaction_animations.jl") - -latex(text::LaTeXString) = latex(text, O) -latex(text::LaTeXString, pos::Point) = latex(text, pos, :stroke) -latex(text::LaTeXString, x, y) = latex(text, Point(x, y), :stroke) -@deprecate latex(text::LaTeXString, fsize::Real) begin - fontsize(fsize) - latex(text) -end - -""" - latex(text::LaTeXString, pos::Point, action::Symbol) - -Add the latex string `text` to the top left corner of the LaTeX path. -Can be added to `Luxor.jl` graphics via [`Video`](@ref). - -**NOTES:** -- **This only works if `tex2svg` is installed.** - It can be installed using the following command (you may have to prefix this command with `sudo` depending on your installation): - - npm install -g mathjax-node-cli - -- **The `latex` method must be called from within an [`Action`](@ref).** - -# Arguments -- `text::LaTeXString`: a LaTeX string to render. -- `pos::Point`: position of the upper left corner of the latex text. Default: `O` - - can be written as `x, y` instead of `Point(x, y)` -- `action::Symbol`: graphics actions defined by `Luxor.jl`. Default `:stroke`. -Available actions: - - `:stroke` - Draws the latex string on the canvas. For more info check `Luxor.strokepath` - - `:path` - Creates the path of the latex string but does not render it to the canvas. - -# Throws -- `IOError`: mathjax-node-cli is not installed - -# Example - -``` -using Javis -using LaTeXStrings - -function ground(args...) - background("white") - sethue("black") -end - -function draw_latex(video, action, frame) - x = 100 - y = 120 - latex(L"\\sqrt{5}", x, y) -end - -demo = Video(500, 500) -javis(demo, [BackgroundAction(1:2, ground), Action(draw_latex)], - pathname = "latex.gif") -``` - -""" -function latex(text::LaTeXString, pos::Point, action::Symbol) - translate(pos) - # check if it's cached - if haskey(LaTeXSVG, text) - svg = LaTeXSVG[text] - else - # remove the $ - ts = text.s[2:(end - 1)] - command = `tex2svg $ts` - try - svg = read(command, String) - catch e - @warn "Using LaTeX needs the program `tex2svg` which might not be installed" - @info "It can be installed using `npm install -g mathjax-node-cli`" - throw(e) - end - LaTeXSVG[text] = svg - end - pathsvg(svg) - if action != :path - # stroke is also fill for letters - do_action(:fill) - end - translate(-pos) -end - -""" - compute_transformation!(action::AbstractAction, video::Video, frame::Int) - -Update action.internal_transitions for the current frame number -""" -function compute_transformation!(action::AbstractAction, video::Video, frame::Int) - for (trans, internal_trans) in zip(action.transitions, action.internal_transitions) - compute_transition!(internal_trans, trans, video, action, frame) - end -end - -""" - compute_transition!(internal_rotation::InternalRotation, rotation::Rotation, video, - action::AbstractAction, frame) - -Computes the rotation transformation for the `action`. -If the `Rotation` is given directly it uses the frame number for interpolation. -If `rotation` includes symbols the current definition of that look up is used for computation. -""" -function compute_transition!( - internal_rotation::InternalRotation, - rotation::Rotation, - video, - action::AbstractAction, - frame, -) - t = (frame - first(get_frames(action))) / (length(get_frames(action)) - 1) - # makes sense to only allow 0 โ‰ค t โ‰ค 1 - t = min(1.0, t) - from, to, center = rotation.from, rotation.to, rotation.center - - center isa Symbol && (center = pos(center)) - from isa Symbol && (from = angle(from)) - to isa Symbol && (to = angle(to)) - - internal_rotation.angle = from + t * (to - from) - internal_rotation.center = center -end - -""" - compute_transition!(internal_translation::InternalTranslation, translation::Translation, - video, action::AbstractAction, frame) - -Computes the translation transformation for the `action`. -If the `translation` is given directly it uses the frame number for interpolation. -If `translation` includes symbols the current definition of that symbol is looked up -and used for computation. -""" -function compute_transition!( - internal_translation::InternalTranslation, - translation::Translation, - video, - action::AbstractAction, - frame, -) - t = (frame - first(get_frames(action))) / (length(get_frames(action)) - 1) - # makes sense to only allow 0 โ‰ค t โ‰ค 1 - t = min(1.0, t) - from, to = translation.from, translation.to - - from isa Symbol && (from = pos(from)) - to isa Symbol && (to = pos(to)) - - internal_translation.by = from + t * (to - from) -end - -""" - compute_transition!(internal_translation::InternalScaling, translation::Scaling, - video, action::AbstractAction, frame) - -Computes the scaling transformation for the `action`. -If the `scaling` is given directly it uses the frame number for interpolation. -If `scaling` includes symbols, the current definition of that symbol is looked up -and used for computation. -""" -function compute_transition!( - internal_scale::InternalScaling, - scale::Scaling, - video, - action::AbstractAction, - frame, -) - t = (frame - first(get_frames(action))) / (length(get_frames(action)) - 1) - # makes sense to only allow 0 โ‰ค t โ‰ค 1 - t = min(1.0, t) - from, to = scale.from, scale.to - - if !scale.compute_from_once || frame == first(get_frames(action)) - from isa Symbol && (from = get_scale(from)) - if scale.compute_from_once - scale.from = from - end - end - to isa Symbol && (to = get_scale(to)) - internal_scale.scale = from .+ t .* (to .- from) -end - -""" - perform_transformation(action::AbstractAction) - -Perform the transformations as described in action.internal_transitions -""" -function perform_transformation(action::AbstractAction) - for trans in action.internal_transitions - perform_transformation(trans) - end -end - -""" - perform_transformation(trans::InternalTranslation) - -Translate as described in `trans`. -""" -function perform_transformation(trans::InternalTranslation) - translate(trans.by) -end - -""" - perform_transformation(trans::InternalRotation) - -Translate and rotate as described in `trans`. -""" -function perform_transformation(trans::InternalRotation) - translate(trans.center) - rotate(trans.angle) -end - -""" - perform_transformation(trans::InternalScaling) - -Scale as described in `trans`. -""" -function perform_transformation(trans::InternalScaling) - scaleto(trans.scale...) -end - -""" - get_value(s::Symbol) - -Get access to the value that got saved in `s` by a previous action. -If you want to access a position or angle check out [`get_position`](@ref) -and [`get_angle`](@ref). - -# Returns -- `Any`: the value stored by a previous action. -""" -function get_value(s::Symbol) - is_internal = first(string(s)) == '_' - if is_internal - internal_sym = Symbol(string(s)[2:end]) - if hasfield(ActionSetting, internal_sym) - return getfield(get_current_setting(), internal_sym) - end - end - - defs = CURRENT_VIDEO[1].defs - if haskey(defs, s) - return defs[s] - else - error("The symbol $s is not defined.") - end -end - -""" - val(x) - -`val` is just a short-hand for [`get_value`](@ref) -""" -val(x) = get_value(x) - -get_position(p::Point) = p -get_position(t::Transformation) = t.p - -""" - get_position(s::Symbol) - -Get access to the position that got saved in `s` by a previous action. - -# Returns -- `Point`: the point stored by a previous action. -""" -get_position(s::Symbol) = get_position(val(s)) - -""" - pos(x) - -`pos` is just a short-hand for [`get_position`](@ref) -""" -pos(x) = get_position(x) - -# As it is just the number tuple -> return it -get_scale(x::Tuple{<:Number,<:Number}) = x - -# If just the number -> return it as a tuple -get_scale(x::Number) = (x, x) - -""" - get_scale(s::Symbol) - -Get access to the scaling that got saved in `s` by a previous action. - -# Returns -- `Scaling`: the scale stored by a previous action. -""" -get_scale(s::Symbol) = get_scale(val(s)) - -""" - scl(x) - -`scl` is just a short-hand for [`get_scale`](@ref) -""" -scl(x) = get_scale(x) - -get_angle(t::Transformation) = t.angle - - -""" - get_angle(s::Symbol) - -Get access to the angle that got saved in `s` by a previous action. - -# Returns -- `Float64`: the angle stored by a previous action i.e via `return Transformation(p, angle)` -""" -get_angle(s::Symbol) = get_angle(val(s)) - -""" - ang(x) - -`ang` is just a short-hand for [`get_angle`](@ref) -""" -ang(x) = get_angle(x) +include("javis_viewer.jl") +include("latex.jl") +include("transition2transformation.jl") +include("symbol_values.jl") """ projection(p::Point, l::Line) @@ -942,31 +113,14 @@ function projection(p::Point, l::Line) return c * v + o end -""" - create_internal_transitions!(action::AbstractAction) - -For every translation an internal translation is added to `action.internal_transitions`. -Same is true for all other transitions. -""" -function create_internal_transitions!(action::AbstractAction) - for trans in action.transitions - if trans isa Translation - push!(action.internal_transitions, InternalTranslation(O)) - elseif trans isa Rotation - push!(action.internal_transitions, InternalRotation(0.0, O)) - elseif trans isa Scaling - push!(action.internal_transitions, InternalScaling((1.0, 1.0))) - end - end -end - """ javis( video::Video, actions::Vector{AbstractAction}; framerate=30, pathname="", - tempdirectory="" + tempdirectory="", + liveview=false ) Similar to `animate` in Luxor with a slightly different structure. @@ -981,6 +135,7 @@ Instead of using actions and a video instead of scenes in a movie. - `pathname::String`: The path for the rendered gif or mp4 (i.e `output.gif` or `output.mp4`) - `tempdirectory::String`: The folder where each frame is stored Defaults to a temporary directory when not set +- `liveview::Bool`: Causes a live image viewer to appear to assist with animation development # Example ``` @@ -1019,7 +174,8 @@ function javis( actions::Vector{AA}; framerate = 30, pathname = "javis_$(randstring(7)).gif", - tempdirectory = mktempdir(), + liveview = false, + tempdirectory = "", ) where {AA<:AbstractAction} compute_frames!(actions) @@ -1055,45 +211,46 @@ function javis( CURRENT_ACTION[1] = actions[1] end + if liveview == true + _javis_viewer(video, length(frames), actions) + return "Live preview started." + end + + path, ext = "", "" + if !isempty(pathname) + path, ext = splitext(pathname) + end + render_mp4 = ext == ".mp4" + codec_props = [:priv_data => ("crf" => "22", "preset" => "medium")] + if render_mp4 + video_io = Base.open("temp.stream", "w") + end + video_encoder = nothing + # if we render a gif and the user hasn't set a tempdirectory + if !render_mp4 && isempty(tempdirectory) + tempdirectory = mktempdir() + end + filecounter = 1 - for frame in frames - background_settings = ActionSetting() - Drawing( - video.width, - video.height, - "$(tempdirectory)/$(lpad(filecounter, 10, "0")).png", - ) - origin() - origin_matrix = cairotojuliamatrix(getmatrix()) - # this frame needs doing, see if each of the scenes defines it - for action in actions - # if action is not in global layer this sets the background_settings - # from the parent background action - update_action_settings!(action, background_settings) - CURRENT_ACTION[1] = action - if frame in get_frames(action) - # check if the action should be part of the global layer (i.e BackgroundAction) - # or in its own layer (default) - in_global_layer = get(action.opts, :in_global_layer, false) - if !in_global_layer - @layer begin - perform_action(action, video, frame, origin_matrix) - end - else - perform_action(action, video, frame, origin_matrix) - # update origin_matrix as it's inside the global layer - origin_matrix = cairotojuliamatrix(getmatrix()) - end + @showprogress 1 "Rendering frames..." for frame in frames + frame_image = convert.(RGB, get_javis_frame(video, actions, frame)) + if !isempty(tempdirectory) + Images.save("$(tempdirectory)/$(lpad(filecounter, 10, "0")).png", frame_image) + end + if render_mp4 + if frame == first(frames) + video_encoder = prepareencoder( + frame_image, + framerate = framerate, + AVCodecContextProperties = codec_props, + ) end - # if action is in global layer this changes the background settings - update_background_settings!(background_settings, action) + appendencode!(video_encoder, video_io, frame_image, filecounter) end - finish() filecounter += 1 end isempty(pathname) && return - path, ext = splitext(pathname) if ext == ".gif" # generate a colorpalette first so ffmpeg does not have to guess it ffmpeg_exe(`-loglevel panic -i $(tempdirectory)/%10d.png -vf @@ -1103,23 +260,59 @@ function javis( "$(tempdirectory)/palette.bmp" -lavfi "paletteuse=dither=sierra2_4a" -y $pathname`) elseif ext == ".mp4" - ffmpeg_exe(`-loglevel panic -framerate $framerate -i $(tempdirectory)/%10d.png -c:v libx264 -pix_fmt yuv420p $pathname`) - + finishencode!(video_encoder, video_io) + close(video_io) + mux("temp.stream", pathname, framerate; silent = true) else @error "Currently, only gif and mp4 creation is supported. Not a $ext." end return pathname end -function update_background_settings!(setting::ActionSetting, action::Action) - in_global_layer = get(action.opts, :in_global_layer, false) - if in_global_layer - update_ActionSetting!(setting, action.current_setting) - end -end +""" + get_javis_frame(video, actions, frame) -function update_action_settings!(action::Action, setting::ActionSetting) - update_ActionSetting!(action.current_setting, setting) +Get a frame from an animation given a video object, its actions, and frame. + +# Arguments +- `video::Video`: The video which defines the dimensions of the output +- `actions::Vector{Action}`: All actions that are performed +- `frame::Int`: Specific frame to be returned + +# Returns +- `Array{ARGB32, 2}` - request frame as a matrix +""" +function get_javis_frame(video, actions, frame) + background_settings = ActionSetting() + Drawing(video.width, video.height, :image) + origin() + origin_matrix = cairotojuliamatrix(getmatrix()) + # this frame needs doing, see if each of the scenes defines it + for action in actions + # if action is not in global layer this sets the background_settings + # from the parent background action + update_action_settings!(action, background_settings) + CURRENT_ACTION[1] = action + if frame in get_frames(action) + # check if the action should be part of the global layer (i.e BackgroundAction) + # or in its own layer (default) + in_global_layer = get(action.opts, :in_global_layer, false)::Bool + if !in_global_layer + @layer begin + perform_action(action, video, frame, origin_matrix) + end + else + perform_action(action, video, frame, origin_matrix) + # update origin_matrix as it's inside the global layer + origin_matrix = cairotojuliamatrix(getmatrix()) + end + end + # if action is in global layer this changes the background settings + update_background_settings!(background_settings, action) + end + img = image_as_matrix() + finish() + return img end """ @@ -1134,7 +327,7 @@ It is a 4-step process: """ function perform_action(action, video, frame, origin_matrix) # first compute and perform the global transformations of this action - compute_transformation!(action, video, frame) + compute_transition!(action, video, frame) perform_transformation(action) # relative frame number for subactions @@ -1143,13 +336,13 @@ function perform_action(action, video, frame, origin_matrix) for subaction in action.subactions if rel_frame in get_frames(subaction) subaction.func(video, action, subaction, rel_frame) - compute_transformation!(subaction, video, rel_frame) + compute_transition!(subaction, video, rel_frame) perform_transformation(subaction) elseif rel_frame > last(get_frames(subaction)) # call the subaction on the last frame i.e. disappeared things stay disappeared subaction.func(video, action, subaction, last(get_frames(subaction))) # have the transformation from the last active frame - compute_transformation!(subaction, video, last(get_frames(subaction))) + compute_transition!(subaction, video, last(get_frames(subaction))) perform_transformation(subaction) end end @@ -1157,6 +350,12 @@ function perform_action(action, video, frame, origin_matrix) # set the defaults for the frame like setline() and setopacity() # which can depend on the subactions set_action_defaults!(action) + + # if the scale would be 0.0 `show_action` is set to false => don't show the action + # (it wasn't actually scaled to 0 because it would break Cairo :D) + cs = get_current_setting() + !cs.show_action && return + res = action.func(video, action, frame) if action.id !== nothing current_global_matrix = cairotojuliamatrix(getmatrix()) @@ -1204,6 +403,7 @@ const LUXOR_DONT_EXPORT = [ :fontsize, :get_fontsize, :scale, + :text, ] # Export each function from Luxor @@ -1219,9 +419,11 @@ export Video, Action, BackgroundAction, SubAction, Rel export Line, Translation, Rotation, Transformation, Scaling export val, pos, ang, get_value, get_position, get_angle export projection, morph -export appear, disappear +export appear, disappear, rotate_around, follow_path +export rev +export scaleto # custom override of luxor extensions -export setline, setopacity, fontsize, get_fontsize, scale +export setline, setopacity, fontsize, get_fontsize, scale, text end diff --git a/src/javis_viewer.jl b/src/javis_viewer.jl new file mode 100644 index 000000000..86619bebc --- /dev/null +++ b/src/javis_viewer.jl @@ -0,0 +1,238 @@ +""" + _draw_image(video::Video, actions::Vector, frame::Int, canvas::Gtk.Canvas, + img_dims::Vector) + +Internal function to create an image that is drawn on a Gtk Canvas. +""" +function _draw_image( + video::Video, + actions::Vector, + frame::Int, + canvas::Gtk.Canvas, + img_dims::Vector, +) + @guarded draw(canvas) do widget + # Gets a specific frame from graphic; transposed due to returned matrix + frame_mat = transpose(get_javis_frame(video, actions, frame)) + + # Gets the correct Canvas context to draw on + context = getgc(canvas) + + # Uses Cairo to draw on Gtk canvas context + image(context, CairoImageSurface(frame_mat), 0, 0, img_dims[1], img_dims[2]) + end +end + +""" + _increment(video::Video, widgets::Vector, actions::Vector, dims::Vector, + canvas::Gtk.Canvas, frames::Int) + +Increments a given value and returns the associated frame. +""" +function _increment( + video::Video, + widgets::Vector, + actions::Vector, + dims::Vector, + canvas::Gtk.Canvas, + frames::Int, +) + # Get current frame from textbox as an Int value + curr_frame = parse(Int, get_gtk_property(widgets[2], :text, String)) + if frames > curr_frame + # `widgets[1]` represents the GtkReactive slider widget + push!(widgets[1], curr_frame + 1) + _draw_image(video, actions, curr_frame + 1, canvas, dims) + else + # `widgets[2]` represents the GtkReactive textboxwidget + push!(widgets[2], 1) # Sets the first frame shown to one + _draw_image(video, actions, 1, canvas, dims) + end +end + +""" + _decrement(video::Video, widgets::Vector, actions::Vector, dims::Vector, + canvas::Gtk.Canvas, frames::Int) + +Decrements a given value and returns the associated frame. +""" +function _decrement( + video::Video, + widgets::Vector, + actions::Vector, + dims::Vector, + canvas::Gtk.Canvas, + frames::Int, +) + # Get current frame from textbox as an Int value + curr_frame = parse(Int, get_gtk_property(widgets[2], :text, String)) + if curr_frame > 1 + # `widgets[1]` represents the GtkReactive slider widget + push!(widgets[1], curr_frame - 1) + _draw_image(video, actions, curr_frame - 1, canvas, dims) + else + # `widgets[2]` represents the GtkReactive textboxwidget + push!(widgets[2], frames) # Sets the first frame shown to one + _draw_image(video, actions, frames, canvas, dims) + end +end + +""" + _javis_viewer(video::Video, frames::Int, action_list::Vector, show::Bool) + +Internal Javis Viewer built on Gtk that is called for live previewing. +""" +function _javis_viewer( + video::Video, + total_frames::Int, + action_list::Vector, + show::Bool = true, +) + + ##################################################################### + # VIEWER WINDOW AND CONFIGURATION + ##################################################################### + + # Determine frame size of animation + frame_dims = [video.width, video.height] + + # Creates a GTK window for drawing; sized based on frame size + win = GtkWindow("Javis Viewer", frame_dims[1], frame_dims[2]) + + # Sets border size of window + set_gtk_property!(win, :border_width, 20) + + ##################################################################### + # DISPLAY WIDGETS + ##################################################################### + + # Create GtkScale internal widget + _slide = GtkScale(false, 1:total_frames) + + # Create GtkReactive slider widget + slide = slider(1:total_frames, value = 1, widget = _slide) + + #= + # + # NOTE: We must provide a named GtkScale widget named `_slide` to the + # GtkReactive `slider` widget so as to perform asynchronous calls + # via signal_connect. Otherwise, we will be unable to update the + # widget that is automatically created by the slider object. + # + # It should be stated that a `slider` object is essentially a + # GtkScale widget coupled with a Reactive object. + # + =# + + # Create a textbox + tbox = GtkReactive.textbox(Int; signal = signal(slide)) + + # Button for going forward through animation + forward = GtkButton("==>") + + # Button for going backward through animation + backward = GtkButton("<==") + + #= + + TODO: Enable widgets of window to dynamically resize based on user changing the size of a window. + + I think I can use the `configure-event` signal in GTK3 documentation (link: https://developer.gnome.org/gtk3/stable/GtkWidget.html#GtkWidget-configure-event). From there, I can then make a `signal_connect` set-up where I update `set_gtk_property!()` of the windows accordingly using `:width_request` and `height_request`. + + =# + + ##################################################################### + # VIEWER CANVAS AND GRID CONFIGURATION + ##################################################################### + + # Gtk Canvas object upon which to draw image; sized via frame size + canvas = Gtk.Canvas(frame_dims[1], frame_dims[2]) + + # Grid to allocate widgets + grid = Gtk.Grid() + + # Allocate the widgets in a 3x3 grid + grid[1:3, 1] = canvas + grid[1:3, 2] = slide + grid[1, 3] = backward + grid[2, 3] = tbox + grid[3, 3] = forward + + # Center all widgets vertically in grid + set_gtk_property!(grid, :valign, 3) + + # Center all widgets horizontally in grid + set_gtk_property!(grid, :halign, 3) + + # Adds grid to previously defined window + push!(win, grid) + + ##################################################################### + # DISPLAY FIRST FRAME + ##################################################################### + + _draw_image(video, action_list, 1, canvas, frame_dims) + + ##################################################################### + # SIGNAL CONNECTION FUNCTIONS + ##################################################################### + + # When the slider is changed, update currently viewed frame + signal_connect(_slide, "value-changed") do widget + # Collects GtkScale as an adjustable bounded value object + bound_slide = Gtk.GAccessor.adjustment(_slide) + + # Get frame number from bounded value object as Int + slide_val = Gtk.get_gtk_property(bound_slide, "value", Int) + + _draw_image(video, action_list, slide_val, canvas, frame_dims) + end + + # When the `Enter` key is pressed, update the frame + signal_connect(win, "key-press-event") do widget, event + if event.keyval == 65293 + # Get current frame from textbox as an Int value + curr_frame = parse(Int, get_gtk_property(tbox, :text, String)) + curr_frame = clamp(curr_frame, 1, total_frames) + _draw_image(video, action_list, curr_frame, canvas, frame_dims) + end + end + + # When the `forward` button is clicked, increment current frame number + # If at final frame, wrap viewer to first frame + signal_connect(forward, "clicked") do widget + _increment(video, [slide, tbox], action_list, frame_dims, canvas, total_frames) + end + + # When the `Right Arrow` key is pressed, increment current frame number + # If at final frame, wrap viewer to first frame + signal_connect(win, "key-press-event") do widget, event + if event.keyval == 65363 + _increment(video, [slide, tbox], action_list, frame_dims, canvas, total_frames) + end + end + + # When the `backward` button is clicked, decrement the current frame number + # If at first frame, wrap viewer to last frame + signal_connect(backward, "clicked") do widget + _decrement(video, [slide, tbox], action_list, frame_dims, canvas, total_frames) + end + + # When the `Left Arrow` key is pressed, decrement current frame number + # If at first frame, wrap viewer to last frame + signal_connect(win, "key-press-event") do widget, event + if event.keyval == 65361 + _decrement(video, [slide, tbox], action_list, frame_dims, canvas, total_frames) + end + end + + ##################################################################### + + if show + # Display image viewer + Gtk.showall(win) + else + return win, frame_dims, slide, tbox, canvas, action_list, total_frames, video + end + +end diff --git a/src/latex.jl b/src/latex.jl new file mode 100644 index 000000000..2de4320b5 --- /dev/null +++ b/src/latex.jl @@ -0,0 +1,95 @@ +# cache such that creating svgs from LaTeX don't need to be created every time +# this is also used for test cases such that `tex2svg` doesn't need to be installed on Github Actions +const LaTeXSVG = Dict{LaTeXString,String}( + L"\mathcal{O}(\log{n})" => + "\nscript upper O left-parenthesis log n right-parenthesis\n\n\n\n\n\n\n\n\n\n\n \n \n\n \n \n \n\n \n \n\n", + L"\mathcal{O}\left(\frac{\log{x}}{2}\right)" => + "\nscript upper O left-parenthesis StartFraction log x Over 2 EndFraction right-parenthesis\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n \n\n\n\n\n \n \n \n \n\n \n\n\n \n\n\n", + L"E=mc^2" => + "\nupper E equals m c squared\n\n\n\n\n\n\n\n\n \n \n \n\n \n \n\n\n\n", + L"8" => + "\n8\n\n\n\n\n \n\n\n", + L"$\begin{equation}\left[\begin{array}{ccc}1 & 2 & 3 \\4 & 5 & 6 \\7 & 8 & 9 \\\end{array}\right]\end{equation}$" => + "\nStart 3 By 3 Matrix 1st Row 1st Column 1 2nd Column 2 3rd Column 3 2nd Row 1st Column 4 2nd Column 5 3rd Column 6 3rd Row 1st Column 7 2nd Column 8 3rd Column 9 EndMatrix\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n \n\n \n\n\n\n \n \n \n\n\n \n \n \n\n\n \n \n \n\n\n\n \n\n \n\n \n\n\n\n", +) + + +latex(text::LaTeXString) = latex(text, O) +latex(text::LaTeXString, pos::Point) = latex(text, pos, :stroke) +latex(text::LaTeXString, x, y) = latex(text, Point(x, y), :stroke) + +""" + latex(text::LaTeXString, pos::Point, action::Symbol) + +Add the latex string `text` to the top left corner of the LaTeX path. +Can be added to `Luxor.jl` graphics via [`Video`](@ref). + +**NOTES:** +- **This only works if `tex2svg` is installed.** + It can be installed using the following command (you may have to prefix this command with `sudo` depending on your installation): + + npm install -g mathjax-node-cli + +- **The `latex` method must be called from within an [`Action`](@ref).** + +# Arguments +- `text::LaTeXString`: a LaTeX string to render. +- `pos::Point`: position of the upper left corner of the latex text. Default: `O` + - can be written as `x, y` instead of `Point(x, y)` +- `action::Symbol`: graphics actions defined by `Luxor.jl`. Default `:stroke`. +Available actions: + - `:stroke` - Draws the latex string on the canvas. For more info check `Luxor.strokepath` + - `:path` - Creates the path of the latex string but does not render it to the canvas. + +# Throws +- `IOError`: mathjax-node-cli is not installed + +# Example + +``` +using Javis +using LaTeXStrings + +function ground(args...) + background("white") + sethue("black") +end + +function draw_latex(video, action, frame) + fontsize(50) + x = 100 + y = 120 + latex(L"\\sqrt{5}", x, y) +end + +demo = Video(500, 500) +javis(demo, [BackgroundAction(1:2, ground), Action(draw_latex)], + pathname = "latex.gif") +``` + +""" +function latex(text::LaTeXString, pos::Point, action::Symbol) + translate(pos) + # check if it's cached + if haskey(LaTeXSVG, text) + svg = LaTeXSVG[text] + else + # remove the $ + ts = text.s[2:(end - 1)] + command = `tex2svg $ts` + try + svg = read(command, String) + catch e + @warn "Using LaTeX needs the program `tex2svg` which might not be installed" + @info "It can be installed using `npm install -g mathjax-node-cli`" + throw(e) + end + LaTeXSVG[text] = svg + end + pathsvg(svg) + if action != :path + # stroke is also fill for letters + do_action(:fill) + end + translate(-pos) +end diff --git a/src/luxor_overrides.jl b/src/luxor_overrides.jl index 9dad313fa..c9d3297fc 100644 --- a/src/luxor_overrides.jl +++ b/src/luxor_overrides.jl @@ -120,11 +120,17 @@ function scale(scl_x, scl_y) cs = get_current_setting() cs.desired_scale = (scl_x, scl_y) current_scale = cs.desired_scale .* cs.mul_scale - Luxor.scale(current_scale...) - cs.current_scale = cs.current_scale .* current_scale - # println("cs.current_scale: $(cs.current_scale)") + if current_scale[1] โ‰ˆ 0.0 || current_scale[2] โ‰ˆ 0.0 + cs.show_action = false + else + cs.show_action = true + Luxor.scale(current_scale...) + cs.current_scale = cs.current_scale .* current_scale + end end +scaleto(xy) = scaleto(xy, xy) + """ scaleto(x, y) @@ -137,12 +143,111 @@ function scaleto(x, y) scaling = (x, y) ./ cs.current_scale # we divided by 0 but clearly we want to scale to 0 # -> we want scaling to be 0 not Inf - if x โ‰ˆ 0 - scaling = (0.0, scaling[2]) - end - if y โ‰ˆ 0 - scaling = (scaling[1], 0.0) + if x โ‰ˆ 0 || y โ‰ˆ 0 + cs.show_action = false + return end + cs.show_action = true Luxor.scale(scaling...) cs.current_scale = (x, y) end + +""" + animate_text( + str, + pos::Point, + valign::Symbol, + halign::Symbol, + angle::Float64, + t::Float64, + ) + +This function is used as a subfunction of [`text`](@ref) and animates the `str` by +clipping the textoutlines and creating a growing circle in the lower left corner to display +the text from left to right in an animated fashion. +""" +function animate_text( + str, + pos::Point, + valign::Symbol, + halign::Symbol, + angle::Float64, + t::Float64, +) + if t >= 1 + return Luxor.text(str, pos; valign = valign, halign = halign, angle = angle) + end + + # copied from Luxor.text + xbearing, ybearing, textwidth, textheight, xadvance, yadvance = textextents(str) + halignment = findfirst(isequal(halign), [:left, :center, :right, :centre]) + + # if unspecified or wrong, default to left, also treat UK spelling centre as center + if halignment == nothing + halignment = 1 + elseif halignment == 4 + halignment = 2 + end + + textpointx = pos.x - [0, textwidth / 2, textwidth][halignment] + + valignment = findfirst(isequal(valign), [:top, :middle, :baseline, :bottom]) + + # if unspecified or wrong, default to baseline + if valignment == nothing + valignment = 3 + end + + textpointy = pos.y - [ybearing, ybearing / 2, 0, textheight + ybearing][valignment] + + + gsave() + translate(Point(textpointx, textpointy)) + rotate(angle) + # clipping region + textoutlines(str, O, :clip) + w = textwidth + r = t * w + circle(O, r, :fill) + grestore() + return Point(textpointx, textpointy) +end + +""" + text(str, pos = O; valign = :baseline, halign = :left, angle = 0.0) + +Has bacially the same functionality as Luxor.text but overrides that method to allow to +animate text with [`appear`](@ref). + +# Example +```julia +Action( + 1:100, + (args...) -> text("Hello Stream!"; halign = :center); + subactions = [ + SubAction(1:15, sineio(), appear(:draw_text)), + SubAction(76:100, sineio(), disappear(:draw_text)), + ], +) +``` +draws the text from left to right in the first 15 frames and in the last 15 frames it disappears. + +# Arguments +- `str::AbstractString` the string that should be shown +- `pos::Point` defaults to the origin and can be written as `x,y` as well as `Point(x,y)`. + +# Keywords +- `valign::Symbol` defaults to `:baseline` and takes `(:top, :middle, :bottom, :baseline)` +- `halign::Symbol` defaults to `:left` and takes `(:left, :center, :centre, :right)` +- `angle::Float64` defaults to `0.0` and specifies the angle of the text +""" +function text(str, pos = O; valign = :baseline, halign = :left, angle = 0.0) + action = CURRENT_ACTION[1] + opts = action.opts + t = get(opts, :draw_text_t, 1.0) + return animate_text(str, pos, valign, halign, angle, t) +end + +function text(str, x, y; kwargs...) + text(str, Point(x, y); kwargs...) +end diff --git a/src/morphs.jl b/src/morphs.jl index afc0716e4..62ad118d2 100644 --- a/src/morphs.jl +++ b/src/morphs.jl @@ -66,7 +66,7 @@ function match_num_point!(poly_1::Vector{Point}, poly_2::Vector{Point}) end """ - morph(from_func::Function, to_func::Function) + morph(from_func::Function, to_func::Function; action=:stroke) A closure for the [`_morph`](@ref) function. This makes it easier to write the function inside an `Action`. @@ -84,6 +84,9 @@ i.e. use `circle(Point(100,100), 50)` instead of `circle(Point(100,100), 50, :st - `to_func::Function`: Same as `from_func` but it defines the "result" polygon, which will be displayed at the end of the Action +# Keywords +- `action::Symbol` defines whether the object has a fill or just a stroke. Defaults to stroke. + # Example This creates a star that morphs into a circle and back. @@ -103,8 +106,9 @@ javis(video, [ pathname="star2circle.gif", deletetemp=true) ``` """ -function morph(from_func::Function, to_func::Function) - return (video, action, frame) -> _morph(video, action, frame, from_func, to_func) +function morph(from_func::Function, to_func::Function; action = :stroke) + return (video, scene_action, frame) -> + _morph(video, scene_action, frame, from_func, to_func; draw_action = action) end """ @@ -158,11 +162,18 @@ function save_morph_polygons!(action::Action, from_func::Function, to_func::Func end """ - _morph(video::Video, action::Action, frame, from_func::Function, to_func::Function) + _morph(video::Video, action::Action, frame, from_func::Function, to_func::Function; draw_action=:stroke) Internal version of [`morph`](@ref) but described there. """ -function _morph(video::Video, action::Action, frame, from_func::Function, to_func::Function) +function _morph( + video::Video, + action::Action, + frame, + from_func::Function, + to_func::Function; + draw_action = :stroke, +) # computation of the polygons and the best way to morph in the first frame if frame == first(get_frames(action)) save_morph_polygons!(action, from_func, to_func) @@ -174,12 +185,12 @@ function _morph(video::Video, action::Action, frame, from_func::Function, to_fun points = action.opts[:points] # compute the interpolation variable `t` for the current frame - t = (frame - first(get_frames(action))) / (length(get_frames(action)) - 1) + t = get_interpolation(action, frame) for (i, p1, p2) in zip(1:length(from_poly), from_poly, to_poly) new_point = p1 + t * (p2 - p1) points[i] = new_point end - poly(points, :stroke; close = true) + poly(points, draw_action; close = true) end diff --git a/src/structs/Action.jl b/src/structs/Action.jl new file mode 100644 index 000000000..ef154c8b9 --- /dev/null +++ b/src/structs/Action.jl @@ -0,0 +1,182 @@ +""" + Action + +Defines what is drawn in a defined frame range. + +# Fields +- `frames::Frames`: A range of frames for which the `Action` is called +- `id::Union{Nothing, Symbol}`: An id which can be used to save the result of `func` +- `func::Function`: The drawing function which draws something on the canvas. + It gets called with the arguments `video, action, frame` +- `anim::Animation`: defines the interpolation function for the transitions +- `transitions::Vector{Transition}` a list of transitions + which can be performed before the function gets called. +- `internal_transitions::Vector{InternalTransition}`: + Similar to `transitions` but holds the concrete information whereas `Transition` can + hold links to other actions which need to be computed first. + See [`compute_transition!`](@ref) +- `opts::Any` can hold any options defined by the user +""" +mutable struct Action <: AbstractAction + frames::Frames + id::Union{Nothing,Symbol} + func::Function + anim::Animation + transitions::Vector{Transition} + internal_transitions::Vector{InternalTransition} + subactions::Vector{SubAction} + current_setting::ActionSetting + opts::Dict{Symbol,Any} +end + +""" + CURRENT_ACTION + +holds the current action in an array to be declared as a constant +The current action can be accessed using CURRENT_ACTION[1] +""" +const CURRENT_ACTION = Array{Action,1}() + +""" + Action(frames, func::Function, args...) + +The most simple form of an action (if there are no `args`/`kwargs`) just calls +`func(video, action, frame)` for each of the frames it is defined for. +`args` are defined it the next function definition and can be seen in action + in this example [`javis`](@ref) +""" +Action(frames, func::Function, args...; kwargs...) = + Action(frames, nothing, func, args...; kwargs...) + +""" + Action(frames_or_id::Symbol, func::Function, args...) + +This function decides whether you wrote `Action(frames_symbol, ...)`, + or `Action(id_symbol, ...)` +If the symbol `frames_or_id` is not a `FRAMES_SYMBOL` then it is used as an id_symbol. +""" +function Action(frames_or_id::Symbol, func::Function, args...; kwargs...) + if frames_or_id in FRAMES_SYMBOL + Action(frames_or_id, nothing, func, args...; kwargs...) + else + Action(:same, frames_or_id, func, args...; kwargs...) + end +end + +""" + Action(func::Function, args...) + +Similar to the above but uses the same frames as the action above. +""" +Action(func::Function, args...; kwargs...) = + Action(:same, nothing, func, args...; kwargs...) + +""" + Action(frames, id::Union{Nothing,Symbol}, func::Function, + transitions::Transition...; kwargs...) + +Fallback constructor for an Action which doesn't define an animation. +A linear animation is assumed. +""" +function Action( + frames, + id::Union{Nothing,Symbol}, + func::Function, + transitions::Transition...; + kwargs..., +) + + Action(frames, id, func, easing_to_animation(linear()), transitions...; kwargs...) +end + +""" + Action(frames, id::Union{Nothing,Symbol}, func::Function, easing::Union{ReversedEasing, Easing}, + args...; kwargs...) + +Fallback constructor for an Action which does define an animation using an easing function. + +# Example +``` +javis( + video, [ + BackgroundAction(1:100, ground), + Action((args...)->t(), sineio(), Translation(250, 0)) + ] +) +``` +""" +function Action( + frames, + id::Union{Nothing,Symbol}, + func::Function, + easing::Union{ReversedEasing,Easing}, + args...; + kwargs..., +) + + Action(frames, id, func, easing_to_animation(easing), args...; kwargs...) +end + +""" + Action(frames, id::Union{Nothing,Symbol}, func::Function, + transitions::Transition...; kwargs...) + +# Arguments +- `frames`: defines for which frames this action is called +- `id::Symbol`: Is used if the `func` returns something which + shall be accessible by other actions later +- `func::Function` the function that is called after the `transitions` are performed +- `transitions::Transition...` a list of transitions that are performed before + the function `func` itself is called + +The keywords arguments will be saved inside `.opts` as a `Dict{Symbol, Any}` +""" +function Action( + frames, + id::Union{Nothing,Symbol}, + func::Function, + anim::Animation, + transitions::Transition...; + kwargs..., +) + if isempty(CURRENT_VIDEO) + throw(ErrorException("A `Video` must be defined before an `Action`")) + end + CURRENT_VIDEO[1].defs[:last_frames] = frames + opts = Dict(kwargs...) + subactions = SubAction[] + if haskey(opts, :subactions) + subactions = opts[:subactions] + delete!(opts, :subactions) + end + Action( + frames, + id, + func, + anim, + collect(transitions), + [], + subactions, + ActionSetting(), + opts, + ) +end + +""" + BackgroundAction(frames, func::Function, args...; kwargs...) + +Create an Action where `in_global_layer` is set to true such that +i.e the specified color in the background is applied globally (basically a new default) +""" +function BackgroundAction(frames, func::Function, args...; kwargs...) + Action(frames, nothing, func, args...; in_global_layer = true, kwargs...) +end + +""" + BackgroundAction(frames, id::Symbol, func::Function, args...; kwargs...) + +Create an Action where `in_global_layer` is set to true and saves the return into `id`. +""" +function BackgroundAction(frames, id::Symbol, func::Function, args...; kwargs...) + Action(frames, id, func, args...; in_global_layer = true, kwargs...) +end diff --git a/src/structs/ActionSetting.jl b/src/structs/ActionSetting.jl new file mode 100644 index 000000000..6ae056a28 --- /dev/null +++ b/src/structs/ActionSetting.jl @@ -0,0 +1,67 @@ +""" + ActionSetting + +The current settings of an [`Action`](@ref) which are saved in `action.current_setting`. + +# Fields +- `line_width::Float64`: the current line width +- `mul_line_width::Float64`: the current multiplier for line width. + The actual line width is then: `mul_line_width * line_width` +- `opacity::Float64`: the current opacity +- `mul_opacity::Float64`: the current multiplier for opacity. + The actual opacity is then: `mul_opacity * opacity` +- `fontsize::Float64` the current font size +- `show_action::Bool` is set to false if scale would be 0.0 which is forbidden by Cairo +- `current_scale::Tuple{Float64, Float64}`: the current scale +- `desired_scale::Tuple{Float64, Float64}`: the new desired scale +- `mul_scale::Float64`: the multiplier for the new desired scale. + The actual new scale is then: `mul_scale * desired_scale` +""" +mutable struct ActionSetting + line_width::Float64 + mul_line_width::Float64 # the multiplier of line width is between 0 and 1 + opacity::Float64 + mul_opacity::Float64 # the multiplier of opacity is between 0 and 1 + fontsize::Float64 + # scale has three fields instead of just the normal two + # current scale + # desired scale and scale multiplier => `desired_scale*mul_scale` is the new desired scale + # the scale change needs to be computed using `current_scale` and the desired scale + # current_scale should never be 0 as this breaks scaleto has various other bad effects + # see: https://github.com/JuliaGraphics/Luxor.jl/issues/114 + # in this case show will be set to false and the action will not be called + show_action::Bool + current_scale::Tuple{Float64,Float64} + desired_scale::Tuple{Float64,Float64} + mul_scale::Float64 # the multiplier of scale is between 0 and 1 +end + +ActionSetting() = ActionSetting(1.0, 1.0, 1.0, 1.0, 10.0, true, (1.0, 1.0), (1.0, 1.0), 1.0) + +""" + update_ActionSetting!(as::ActionSetting, by::ActionSetting) + +Set the fields of `as` to the same as `by`. Basically copying them over. +""" +function update_ActionSetting!(as::ActionSetting, by::ActionSetting) + as.line_width = by.line_width + as.mul_line_width = by.mul_line_width + as.opacity = by.opacity + as.mul_opacity = by.mul_opacity + as.fontsize = by.fontsize + as.show_action = by.show_action + as.current_scale = by.current_scale + as.desired_scale = by.desired_scale + as.mul_scale = by.mul_scale +end + +function update_background_settings!(setting::ActionSetting, action::AbstractAction) + in_global_layer = get(action.opts, :in_global_layer, false) + if in_global_layer + update_ActionSetting!(setting, action.current_setting) + end +end + +function update_action_settings!(action::AbstractAction, setting::ActionSetting) + update_ActionSetting!(action.current_setting, setting) +end diff --git a/src/structs/Easing.jl b/src/structs/Easing.jl new file mode 100644 index 000000000..55dff6312 --- /dev/null +++ b/src/structs/Easing.jl @@ -0,0 +1,33 @@ +""" + ReversedEasing + +Will be used to reverse an easing inside [`easing_to_animation`](@ref). +Can be constructed from an easing function using [`rev`](@ref). +""" +struct ReversedEasing + easing::Easing +end + +""" + rev(e::Easing) + +Reverse an easing function such that `easing_to_animation` maps it to `[1.0, 0.0]` instead of `[0.0, 1.0]`. +An example can be seen in [`rotate`](@ref) +""" +rev(e::Easing) = ReversedEasing(e) + + +""" + easing_to_animation(easing) + +Converts an easing to an Animation with time goes from `0.0` to `1.0` and value from `0` to `1`. +""" +easing_to_animation(easing) = Animation(0.0, 0.0, easing, 1.0, 1.0) + +""" + easing_to_animation(rev_easing::ReversedEasing) + +Converts an easing to an Animation with time goes from `0.0` to `1.0` and value from `1` to `0`. +""" +easing_to_animation(rev_easing::ReversedEasing) = + Animation(0.0, 1.0, rev_easing.easing, 1.0, 0.0) diff --git a/src/structs/Frames.jl b/src/structs/Frames.jl new file mode 100644 index 000000000..9f89e7488 --- /dev/null +++ b/src/structs/Frames.jl @@ -0,0 +1,58 @@ +""" + Frames + +Stores the actual computed frames and the user input +which can be `:same` or `Rel(10)`. +The `frames` are computed in `javis`. +""" +mutable struct Frames{T} + frames::Union{Nothing,UnitRange} + user::T +end + +Base.convert(::Type{Frames}, x::Union{Symbol,Rel}) = Frames(nothing, x) +Base.convert(::Type{Frames}, x::UnitRange) = Frames(x, x) + + +""" + set_frames!(a::AbstractAction, last_frames::UnitRange) + +Compute the frames based on a.frames and `last_frames`. +Save the result in `a.frames.frames` which can be accessed via [`get_frames`](@ref). +""" +function set_frames!(a::AbstractAction, last_frames::UnitRange) + frames = a.frames.user + a.frames.frames = get_frames(frames, last_frames) +end + +""" + get_frames(a::AbstractAction) + +Return `a.frames.frames` which holds the computed frames for the AbstractAction `a`. +""" +get_frames(a::AbstractAction) = a.frames.frames + +""" + get_frames(frames::Symbol, last_frames::UnitRange) + +Get the frames based on a symbol (currently only `same`) and the `last_frames`. +Throw `ArgumentError` if symbol is unknown +""" +function get_frames(frames::Symbol, last_frames::UnitRange) + if frames === :same + return last_frames + else + throw(ArgumentError("Currently the only symbol supported for defining frames is `:same`")) + end +end + +""" + get_frames(frames::Rel, last_frames::UnitRange) + +Return the frames based on a relative frames [`Rel`](@ref) object and the `last_frames`. +""" +function get_frames(frames::Rel, last_frames::UnitRange) + start_frame = last(last_frames) + first(frames.rel) + last_frame = last(last_frames) + last(frames.rel) + return start_frame:last_frame +end diff --git a/src/structs/Rel.jl b/src/structs/Rel.jl new file mode 100644 index 000000000..9251418de --- /dev/null +++ b/src/structs/Rel.jl @@ -0,0 +1,33 @@ +""" + Rel + +Ability to define frames in a relative fashion. + +# Example +``` + Action(1:100, ground; in_global_layer=true), + Action(1:90, :red_ball, (args...)->circ(p1, "red"), Rotation(from_rot, to_rot)), + Action(Rel(10), :blue_ball, (args...)->circ(p2, "blue"), Rotation(2ฯ€, from_rot, :red_ball)), + Action((video, args...)->path!(path_of_red, pos(:red_ball), "red")) +``` +is the same as +``` +Action(1:100, ground; in_global_layer=true), +Action(1:90, :red_ball, (args...)->circ(p1, "red"), Rotation(from_rot, to_rot)), +Action(91:100, :blue_ball, (args...)->circ(p2, "blue"), Rotation(2ฯ€, from_rot, :red_ball)), +Action(91:100, (video, args...)->path!(path_of_red, pos(:red_ball), "red")) +``` + +# Fields +- rel::UnitRange defines the frames in a relative fashion. +""" +struct Rel + rel::UnitRange +end + +""" + Rel(i::Int) + +Shorthand for Rel(1:i) +""" +Rel(i::Int) = Rel(1:i) diff --git a/src/structs/SubAction.jl b/src/structs/SubAction.jl new file mode 100644 index 000000000..5d8d12b39 --- /dev/null +++ b/src/structs/SubAction.jl @@ -0,0 +1,136 @@ +""" + SubAction <: AbstractAction + +A SubAction can be used in the keyword arguments of an [`Action`](@ref) to define small +sub actions on the action function, such as [`appear`](@ref). + +A SubAction should not be created by hand but instead by using one of the constructors. + +# Fields +- `frames::Frames`: the frames relative to the parent [`Action`](@ref) +- `anim::Animation`: defines the interpolation function for the transitions +- `func::Function`: the function that gets called in each of those frames. + Takes the following arguments: `video, action, subaction, rel_frame` +- `transitions::Vector{Transition}`: A list of transitions like [`Translation`](@ref) +- `internal_transitions::Vector{InternalTransition}`: + A list of internal transitions which store the current transition for a specific frame. +- `defs::Dict{Symbol, Any}` any kind of definitions that are relevant for the subaction. +""" +mutable struct SubAction <: AbstractAction + frames::Frames + anim::Animation + func::Function + transitions::Vector{Transition} + internal_transitions::Vector{InternalTransition} + defs::Dict{Symbol,Any} +end + +SubAction(transitions::Transition...) = SubAction(:same, transitions...) +SubAction(func::Function) = SubAction(:same, func) + +""" + SubAction(frames, easing::Union{ReversedEasing, Easing}, args...) + +A `SubAction` can be defined with frames an +[easing function](https://jkrumbiegel.github.io/Animations.jl/stable/#Easings-1) and either +a function or transformation(s). + + +# Example +In the following example a filled circle with radius 50 appears in the first 20 frames, +which means the opacity is increased from 0 to 1.0. The interpolation function used here is +sineio from [`Animations.jl`](https://github.com/jkrumbiegel/Animations.jl). +Then it stays at full opacity and disappears the same way in the last 20 frames using a +linear decay. + +```julia +javis(demo, [ + BackgroundAction(1:100, ground), + Action((args...)->circle(O, 50, :fill); subactions = [ + SubAction(1:20, sineio(), appear(:fade)), + SubAction(81:100, disappear(:fade)) + ]) +]) +``` + +# Arguments +- `frames`: A list of frames for which the function should be called. + - The frame numbers are relative to the parent [`Action`](@ref). +- `easing::Union{ReversedEasing, Easing}`: The easing function for `args...` +- `args...`: Either a function like [`appear`](@ref) or a Transformation + like [`Translation`](@ref) +""" +SubAction(frames, easing::Union{ReversedEasing,Easing}, args...) = + SubAction(frames, easing_to_animation(easing), args...) + +SubAction(frames, anim::Animation, transition::Transition...) = + SubAction(frames, anim, (args...) -> 1, transition...) + +SubAction(easing::Union{ReversedEasing,Easing}, args...) = + SubAction(:same, easing_to_animation(easing), args...) + +SubAction(anim::Animation, args...) = SubAction(:same, anim, args...) +""" + SubAction(frames, func::Function) + +A `SubAction` can be defined with frames and a function +inside the `subactions` kwarg of an [`Action`](@ref). +In the following example a filled circle with radius 50 appears in the first 20 frames, +which means the opacity is increased from 0 to 1.0. +Then it stays at full opacity and disappears the same way in the last 20 frames. + +# Example +javis(demo, [ + BackgroundAction(1:100, ground), + Action((args...)->circle(O, 50, :fill); subactions = [ + SubAction(1:20, appear(:fade)), + SubAction(81:100, disappear(:fade)) + ]) +]) + +# Arguments +- `frames`: A list of frames for which the function should be called. + - The frame numbers are relative to the parent [`Action`](@ref). +- `func::Function`: The function that gets called for the frames. + - Needs to have four arguments: `video, action, subaction, rel_frame` + - For [`appear`](@ref) and [`disappear`](@ref) a closure exists, + such that `appear(:fade)` works. +""" +SubAction(frames, func::Function) = SubAction(frames, easing_to_animation(linear()), func) + +""" + SubAction(frames, trans::Transition...) + +A `SubAction` can also be defined this way with having a list of transitions. +This is similar to defining transitions inside [`Action`](@ref) + +In the following example a circle is faded in during the first 25 frames then moves to +- `Point(100, 20)` then to `Point(120, -20)` (the translations are added) +- and then back to the origin +In the last 25 frames it disappears from the world. + +# Example +``` +javis(demo, [ + BackgroundAction(1:200, ground_opacity), + Action((args...)->circle(O, 50, :fill); subactions = [ + SubAction(1:25, appear(:fade)), + SubAction(26:75, Translation(Point(100, 20))), + SubAction(76:100, Translation(Point(20, -40))), + SubAction(101:175, Translation(Point(-120, 20))), + SubAction(176:200, disappear(:fade)) + ]), + ], tempdirectory="current/images", pathname="current/circle_square.gif") +``` + +# Arguments +- `frames`: A list of frames for which the function should be called. + - The frame numbers are relative to the parent [`Action`](@ref). +- `trans::Transition...`: A list of transitions that shall be performed. +""" +SubAction(frames, trans::Transition...) = + SubAction(frames, easing_to_animation(linear()), (args...) -> 1, trans...) + + +SubAction(frames, anim::Animation, func::Function, transitions::Transition...) = + SubAction(frames, anim, func::Function, collect(transitions), [], Dict{Symbol,Any}()) diff --git a/src/structs/Transitions.jl b/src/structs/Transitions.jl new file mode 100644 index 000000000..88829662a --- /dev/null +++ b/src/structs/Transitions.jl @@ -0,0 +1,146 @@ +""" + InternalTranslation <: InternalTransition + +Saves a translation as described by [`Translation`](@ref) for the current frame. +Is part of the [`Action`](@ref) struct. +""" +mutable struct InternalTranslation <: InternalTransition + by::Point +end + +""" + InternalRotation <: InternalTransition + +Saves a rotation as described by [`Rotation`](@ref) for the current frame. +Is part of the [`Action`](@ref) struct. +""" +mutable struct InternalRotation <: InternalTransition + angle::Float64 + center::Point +end + +""" + InternalScaling <: InternalTransition + +Saves a scaling as described by [`Scaling`](@ref) for the current frame. +Is part of the [`Action`](@ref) struct. +""" +mutable struct InternalScaling <: InternalTransition + scale::Tuple{Float64,Float64} +end + +""" + Translation <: Transition + +Stores the `Point` or a link for the start and end position of the translation + +# Fields +`from::Union{Point, Symbol}`: The start position or a link to the start position. + See `:red_ball` in [`javis`](@ref) +`to::Union{Point, Symbol}`: The end position or a link to the end position +""" +struct Translation <: Transition + from::Union{Point,Symbol} + to::Union{Point,Symbol} +end + +""" + Translation(p::Union{Point, Symbol}) + +Create a `Translation(O, p)` such that a translation is done from the origin. +""" +Translation(p::Union{Point,Symbol}) = Translation(O, p) + +""" + Translation(x::Real, y::Real) + +Create a `Translation(O, Point(x,y))` such that a translation is done from the origin. +Shorthand for writing `Translation(Point(x,y))`. +""" +Translation(x::Real, y::Real) = Translation(Point(x, y)) + +""" + Rotation <: Transition + +Stores the rotation similar to [`Translation`](@ref) with `from` and `to` +but also the rotation point. + +# Fields +- `from::Union{Float64, Symbol}`: The start rotation or a link to it +- `to::Union{Float64, Symbol}`: The end rotation or a link to it +- `center::Union{Point, Symbol}`: The center of the rotation or a link to it. +""" +struct Rotation <: Transition + from::Union{Float64,Symbol} + to::Union{Float64,Symbol} + center::Union{Point,Symbol} +end + +""" + Rotation(r::Union{Float64, Symbol}) + +Rotation as a transition from 0.0 to `r` . +Can be used as a short-hand. +""" +Rotation(r::Union{Float64,Symbol}) = Rotation(0.0, r) + +""" + Rotation(r::Union{Float64, Symbol}, center::Union{Point, Symbol}) + +Rotation as a transition from `0.0` to `r` around `center`. +Can be used as a short-hand for rotating around a `center` point. +""" +Rotation(r::Union{Float64,Symbol}, center::Union{Point,Symbol}) = Rotation(0.0, r, center) + +""" + Rotation(from, to) + +Rotation as a transition from `from` to `to` (in radians) around the origin. +""" +Rotation(from, to) = Rotation(from, to, O) + +""" + Scaling <: Transition + +Stores the scaling similar to [`Translation`](@ref) with `from` and `to`. + +# Example +- Can be called with different constructors like: +``` +Scaling(10) -> Scaling(CURRENT_SCALING, (10.0, 10.0)) +Scaling(10, :my-scale) -> Scaling((10.0, 10.0), :my_scale) +Scaling(10, 2) -> Scaling((10.0, 10.0), (2.0, 2.0)) +Scaling(10, (1,2)) -> Scaling((10.0, 10.0), (1.0, 2.0)) +``` + +# Fields +- `from::Union{Tuple{Float64, Float64}, Symbol}`: The start scaling or a link to it +- `to::Union{Tuple{Float64, Float64}, Symbol}`: The end scaling or a link to it +- `compute_from_once::Bool`: Saves whether the from is computed for the first frame or + every frame. Is true if from is `:_current_scale`. +""" +mutable struct Scaling <: Transition + from::Union{Tuple{Float64,Float64},Symbol} + to::Union{Tuple{Float64,Float64},Symbol} + compute_from_once::Bool +end + +Scaling(to::Tuple) = Scaling(:_current_scale, to, true) +Scaling(to::Real) = Scaling(:_current_scale, convert(Float64, to), true) +Scaling(to::Symbol) = Scaling(:_current_scale, to, true) + +function Scaling(from::Real, to::Real, compute_from_once = false) + from_flt = convert(Float64, from) + to_flt = convert(Float64, to) + Scaling((from_flt, from_flt), (to_flt, to_flt), compute_from_once) +end + +function Scaling(from::Real, to, compute_from_once = false) + flt = convert(Float64, from) + Scaling((flt, flt), to, compute_from_once) +end + +function Scaling(from, to::Real, compute_from_once = false) + flt = convert(Float64, to) + Scaling(from, (flt, flt), compute_from_once) +end diff --git a/src/structs/Video.jl b/src/structs/Video.jl new file mode 100644 index 000000000..2387251d8 --- /dev/null +++ b/src/structs/Video.jl @@ -0,0 +1,42 @@ +""" + Video + +Defines the video canvas for an animation. + +# Fields +- `width::Int` the width in pixel +- `height::Int` the height in pixel +- `defs::Dict{Symbol, Any}` Some definitions which should be accessible throughout the video. +""" +mutable struct Video + width::Int + height::Int + defs::Dict{Symbol,Any} +end + +""" + CURRENT_VIDEO + +holds the current video in an array to be declared as a constant +The current video can be accessed using CURRENT_VIDEO[1] +""" +const CURRENT_VIDEO = Array{Video,1}() + + +""" + Video(width, height) + +Create a video with a certain `width` and `height` in pixel. +This also sets `CURRENT_VIDEO`. +""" +function Video(width, height) + # some luxor functions need a drawing ;) + Drawing() + video = Video(width, height, Dict{Symbol,Any}()) + if isempty(CURRENT_VIDEO) + push!(CURRENT_VIDEO, video) + else + CURRENT_VIDEO[1] = video + end + return video +end diff --git a/src/subaction_animations.jl b/src/subaction_animations.jl index 30fcf64d4..94185c013 100644 --- a/src/subaction_animations.jl +++ b/src/subaction_animations.jl @@ -20,6 +20,9 @@ of the [`Action`](@ref) so `101-120`. or the value specified by [`setline`](@ref) - `:fade` which increases the opcacity up to the default value or the value specified by [`setopacity`](@ref) + - `:scale` which increases the scale up to the default value `1` + or the value specified by [`scale`](@ref) + - `:draw_text` which only works for [`text`](@ref) and lets it appear from left to right. """ function appear(s::Symbol) (video, action, subaction, rel_frame) -> @@ -27,23 +30,25 @@ function appear(s::Symbol) end function _appear(video, action, subaction, rel_frame, symbol::Val{:fade_line_width}) - # t is between 0 and 1 - t = (rel_frame - first(get_frames(subaction))) / (length(get_frames(subaction)) - 1) + t = get_interpolation(subaction, rel_frame) action.current_setting.mul_line_width = t end function _appear(video, action, subaction, rel_frame, symbol::Val{:fade}) - # t is between 0 and 1 - t = (rel_frame - first(get_frames(subaction))) / (length(get_frames(subaction)) - 1) + t = get_interpolation(subaction, rel_frame) action.current_setting.mul_opacity = t end function _appear(video, action, subaction, rel_frame, symbol::Val{:scale}) - # t is between 0 and 1 - t = (rel_frame - first(get_frames(subaction))) / (length(get_frames(subaction)) - 1) + t = get_interpolation(subaction, rel_frame) action.current_setting.mul_scale = t end +function _appear(video, action, subaction, rel_frame, symbol::Val{:draw_text}) + t = get_interpolation(subaction, rel_frame) + action.opts[:draw_text_t] = t +end + """ disappear(s::Symbol) @@ -62,10 +67,10 @@ of the [`Action`](@ref) so `181-200`. # Arguments - `s::Symbol`: the symbol defines the animation of disappearance The only symbols that are currently supported are: - - `:fade_line_width` which descreases the line width up to the default value - or the value specified by [`setline`](@ref) - - `:fade` which decreases the opcacity up to the default value - or the value specified by [`setopacity`](@ref) + - `:fade_line_width` which decreases the line width down to `0` + - `:fade` which decreases the opacity down to `0` + - `:scale` which decreases the scale down to `0` + - `:draw_text` which only works for text and let the text disappear from right to left. """ function disappear(s::Symbol) (video, action, subaction, rel_frame) -> @@ -73,19 +78,288 @@ function disappear(s::Symbol) end function _disappear(video, action, subaction, rel_frame, symbol::Val{:fade_line_width}) - # t is between 0 and 1 - t = (rel_frame - first(get_frames(subaction))) / (length(get_frames(subaction)) - 1) + t = get_interpolation(subaction, rel_frame) action.current_setting.mul_line_width = 1 - t end function _disappear(video, action, subaction, rel_frame, symbol::Val{:fade}) - # t is between 0 and 1 - t = (rel_frame - first(get_frames(subaction))) / (length(get_frames(subaction)) - 1) + t = get_interpolation(subaction, rel_frame) action.current_setting.mul_opacity = 1 - t end function _disappear(video, action, subaction, rel_frame, symbol::Val{:scale}) - # t is between 0 and 1 - t = (rel_frame - first(get_frames(subaction))) / (length(get_frames(subaction)) - 1) + t = get_interpolation(subaction, rel_frame) action.current_setting.mul_scale = 1 - t end + +function _disappear(video, action, subaction, rel_frame, symbol::Val{:draw_text}) + t = get_interpolation(subaction, rel_frame) + action.opts[:draw_text_t] = 1 - t +end + +""" + translate() + +Translate a function defined inside a [`SubAction`](@ref) using an Animation defined +with Animations.jl. + +If you're used to working with Animations.jl this should feel quite natural. +Instead of defining each movement in its own subaction it's possible to define it in one +by using an Animation. + +# Example +``` +using Javis, Animations + +function ground(args...) + background("black") + sethue("white") +end + +video = Video(500, 500) +circle_anim = Animation( + [0.0, 0.3, 0.6, 1.0], # must go from 0 to 1 + # the circle will move from the origin to `Point(150, 0)` then `Point(150, 150)` + # and back to the origin `O`. + [O, Point(150, 0), Point(150, 150), O], + [sineio(), polyin(5), expin(8)], +) +javis( + video, [ + BackgroundAction(1:150, ground), + Action((args...)->circle(O, 25, :fill); subactions=[ + SubAction(1:150, circle_anim, translate()) + ]) + ], pathname="moving_a_circle.gif" +) +``` + +This notation uses the Animations.jl library very explicitly. It's also possible to do the +same with: + +``` +javis( + video, + [ + BackgroundAction(1:150, ground), + Action((args...)->circle(O, 25, :fill); subactions = [ + SubAction(1:50, sineio(), Translation(150, 0)), + SubAction(51:100, polyin(2), Translation(0, 150)), + SubAction(101:150, expin(8), Translation(-150, -150)) + ]) + ], + pathname = "moving_a_circle_javis.gif", +) +``` + +which uses the `SubAction` syntax three times and only uses easing functions instead of +specifying the `Animation` directly. + +Here `circle_anim` defines the movement of the circle. The most important part is that the +time in animations has to be from `0.0` to `1.0`. +""" +function Luxor.translate() + (video, action, subaction, rel_frame) -> _translate(video, action, subaction, rel_frame) +end + +function _translate(video, action, subaction, rel_frame) + p = get_interpolation(subaction, rel_frame) + Luxor.translate(p) +end + +""" + rotate() + +Rotate a function defined inside a [`SubAction`](@ref) using an Animation defined +with Animations.jl. + +If you're used to working with Animations.jl this should feel quite natural. +Instead of defining each movement in its own subaction it's possible to define it in one +by using an Animation. + +# Example +``` +using Javis, Animations + +video = Video(500, 500) +translate_anim = Animation( + [0, 1], # must go from 0 to 1 + [O, Point(150, 0)], + [sineio()], +) + +translate_back_anim = Animation( + [0, 1], # must go from 0 to 1 + [O, Point(-150, 0)], + [sineio()], +) + +rotate_anim = Animation( + [0, 1], # must go from 0 to 1 + [0, 2ฯ€], + [linear()], +) + +javis( + video, + [ + BackgroundAction(1:150, ground), + Action( + (args...) -> circle(O, 25, :fill); + subactions = [ + SubAction(1:10, sineio(), scale()), + SubAction(11:50, translate_anim, translate()), + SubAction(51:100, rotate_anim, rotate_around(Point(-150, 0))), + SubAction(101:140, translate_back_anim, translate()), + SubAction(141:150, rev(sineio()), scale()) + ], + ), + ], + pathname = "animation.gif", +) +``` + +which uses the `SubAction` syntax five times with both easing functions directly and animation objects. +The `rev(sineio())` creates an `Animation` which goes from `1.0` to `0.0`. +""" +function Luxor.rotate() + (video, action, subaction, rel_frame) -> _rotate(video, action, subaction, rel_frame) +end + +function _rotate(video, action, subaction, rel_frame) + p = get_interpolation(subaction, rel_frame) + Luxor.rotate(p) +end + +""" + rotate_around(p::Point) + +Rotate a function defined inside a [`SubAction`](@ref) using an Animation defined +with Animations.jl around the point `p`. + +An example can be seen in [`rotate`](@ref). + +# Arguments +- `p::Point`: the point to rotate around +""" +function rotate_around(p::Point) + (video, action, subaction, rel_frame) -> + _rotate_around(video, action, subaction, rel_frame, p) +end + +function _rotate_around(video, action, subaction, rel_frame, p) + i = get_interpolation(subaction, rel_frame) + Luxor.translate(p) + Luxor.rotate(i) + Luxor.translate(-p) +end + +""" + scale() + +Scale a function defined inside a [`SubAction`](@ref) using an Animation defined +with Animations.jl around the point `p`. + +An example can be seen in [`rotate`](@ref). +""" +function scale() + (video, action, subaction, rel_frame) -> _scale(video, action, subaction, rel_frame) +end + +function _scale(video, action, subaction, rel_frame) + p = get_interpolation(subaction, rel_frame) + scale(p) +end + +""" + sethue() + +Set the color of a function defined inside a [`SubAction`](@ref) using an Animation defined +with Animations.jl. + +# Example +A possible animation would look like this: +```julia +color_anim = Animation( + [0, 0.5, 1], # must go from 0 to 1 + [ + Lab(colorant"red"), + Lab(colorant"cyan"), + Lab(colorant"black"), + ], + [sineio(), sineio()], +) +``` + +An example on how to integrate this into a SubAction can be seen in [`rotate`](@ref). +Where this would be a valid SubAction: `SubAction(1:150, color_anim, sethue())`. +""" +function Luxor.sethue() + (video, action, subaction, rel_frame) -> _sethue(video, action, subaction, rel_frame) +end + +function _sethue(video, action, subaction, rel_frame) + color = get_interpolation(subaction, rel_frame) + Luxor.sethue(color) +end + +""" + follow_path(points::Vector{Point}; closed=true) + +Can be applied inside a subaction such that the object defined in the parent action follows a path. +It takes a vector of points which can be created as an example by calling `circle(O, 50)` <- notice that the action is set to `:none` the default. + +# Example +```julia +SubAction(1:150, follow_path(star(O, 300))) +``` + +# Arguments +- `points::Vector{Point}` - the vector of points the object should follow + +# Keywords +- `closed::Bool` default: true, sets whether the path is a closed path as for example when + using a circle, ellipse or any polygon. For a bezier path it should be set to false. +""" +function follow_path(points::Vector{Point}; closed = true) + (video, action, subaction, rel_frame) -> + _follow_path(video, action, subaction, rel_frame, points; closed = closed) +end + +function _follow_path(video, action, subaction, rel_frame, points; closed = closed) + isfirstframe = rel_frame == first(get_frames(subaction)) + t = get_interpolation(subaction, rel_frame) + # if not closed it should be always between 0 and 1 + if !closed + t = clamp(t, 0.0, 1.0) + end + # if t is discrete and not 0.0 take the last point or first if closed + if !isfirstframe && isapprox_discrete(t) + if closed + translate(points[1]) + else + translate(points[end]) + end + return + end + # get only the fractional part to be between 0 and 1 + t -= floor(t) + if isfirstframe + # compute the distances only once for performance reasons + subaction.defs[:p_dist] = polydistances(points, closed = closed) + end + if isapprox(t, 0.0, atol = 1e-4) + translate(points[1]) + return + end + pdist = subaction.defs[:p_dist] + ind, surplus = nearestindex(pdist, t * pdist[end]) + + nextind = mod1(ind + 1, length(points)) + overshootpoint = between( + points[ind], + points[nextind], + surplus / distance(points[ind], points[nextind]), + ) + translate(overshootpoint) +end diff --git a/src/svg2luxor.jl b/src/svg2luxor.jl index 0e5a20a55..08933e2bc 100644 --- a/src/svg2luxor.jl +++ b/src/svg2luxor.jl @@ -160,7 +160,7 @@ Setting the attributes of the object `o` by calling `set_attr` methods. function set_attrs(o) for attribute in attributes(o) sym = Symbol(name(attribute)) - set_attr(Val{sym}(), value(attribute)) + set_attr(Val{sym}(), LightXML.value(attribute)) end end diff --git a/src/symbol_values.jl b/src/symbol_values.jl new file mode 100644 index 000000000..46babfdac --- /dev/null +++ b/src/symbol_values.jl @@ -0,0 +1,96 @@ +""" + get_value(s::Symbol) + +Get access to the value that got saved in `s` by a previous action. +If you want to access a position or angle check out [`get_position`](@ref) +and [`get_angle`](@ref). + +# Returns +- `Any`: the value stored by a previous action. +""" +function get_value(s::Symbol) + is_internal = first(string(s)) == '_' + if is_internal + internal_sym = Symbol(string(s)[2:end]) + if hasfield(ActionSetting, internal_sym) + return getfield(get_current_setting(), internal_sym) + end + end + + defs = CURRENT_VIDEO[1].defs + if haskey(defs, s) + return defs[s] + else + error("The symbol $s is not defined.") + end +end + +""" + val(x) + +`val` is just a short-hand for [`get_value`](@ref) +""" +val(x) = get_value(x) + +get_position(p::Point) = p +get_position(t::Transformation) = t.p + +""" + get_position(s::Symbol) + +Get access to the position that got saved in `s` by a previous action. + +# Returns +- `Point`: the point stored by a previous action. +""" +get_position(s::Symbol) = get_position(val(s)) + +""" + pos(x) + +`pos` is just a short-hand for [`get_position`](@ref) +""" +pos(x) = get_position(x) + +# As it is just the number tuple -> return it +get_scale(x::Tuple{<:Number,<:Number}) = x + +# If just the number -> return it as a tuple +get_scale(x::Number) = (x, x) + +""" + get_scale(s::Symbol) + +Get access to the scaling that got saved in `s` by a previous action. + +# Returns +- `Scaling`: the scale stored by a previous action. +""" +get_scale(s::Symbol) = get_scale(val(s)) + +""" + scl(x) + +`scl` is just a short-hand for [`get_scale`](@ref) +""" +scl(x) = get_scale(x) + +get_angle(t::Transformation) = t.angle + + +""" + get_angle(s::Symbol) + +Get access to the angle that got saved in `s` by a previous action. + +# Returns +- `Float64`: the angle stored by a previous action i.e via `return Transformation(p, angle)` +""" +get_angle(s::Symbol) = get_angle(val(s)) + +""" + ang(x) + +`ang` is just a short-hand for [`get_angle`](@ref) +""" +ang(x) = get_angle(x) diff --git a/src/transition2transformation.jl b/src/transition2transformation.jl new file mode 100644 index 000000000..fd776bf0f --- /dev/null +++ b/src/transition2transformation.jl @@ -0,0 +1,147 @@ +""" + create_internal_transitions!(action::AbstractAction) + +For every translation an internal translation is added to `action.internal_transitions`. +Same is true for all other transitions. +""" +function create_internal_transitions!(action::AbstractAction) + for trans in action.transitions + if trans isa Translation + push!(action.internal_transitions, InternalTranslation(O)) + elseif trans isa Rotation + push!(action.internal_transitions, InternalRotation(0.0, O)) + elseif trans isa Scaling + push!(action.internal_transitions, InternalScaling((1.0, 1.0))) + end + end +end + +""" + compute_transition!(action::AbstractAction, video::Video, frame::Int) + +Update action.internal_transitions for the current frame number +""" +function compute_transition!(action::AbstractAction, video::Video, frame::Int) + for (trans, internal_trans) in zip(action.transitions, action.internal_transitions) + compute_transition!(internal_trans, trans, video, action, frame) + end +end + +""" + compute_transition!(internal_rotation::InternalRotation, rotation::Rotation, video, + action::AbstractAction, frame) + +Computes the rotation transformation for the `action`. +If the `Rotation` is given directly it uses the frame number for interpolation. +If `rotation` includes symbols the current definition of that look up is used for computation. +""" +function compute_transition!( + internal_rotation::InternalRotation, + rotation::Rotation, + video, + action::AbstractAction, + frame, +) + t = get_interpolation(action, frame) + from, to, center = rotation.from, rotation.to, rotation.center + + center isa Symbol && (center = pos(center)) + from isa Symbol && (from = angle(from)) + to isa Symbol && (to = angle(to)) + + internal_rotation.angle = from + t * (to - from) + internal_rotation.center = center +end + +""" + compute_transition!(internal_translation::InternalTranslation, translation::Translation, + video, action::AbstractAction, frame) + +Computes the translation transformation for the `action`. +If the `translation` is given directly it uses the frame number for interpolation. +If `translation` includes symbols the current definition of that symbol is looked up +and used for computation. +""" +function compute_transition!( + internal_translation::InternalTranslation, + translation::Translation, + video, + action::AbstractAction, + frame, +) + t = get_interpolation(action, frame) + from, to = translation.from, translation.to + + from isa Symbol && (from = pos(from)) + to isa Symbol && (to = pos(to)) + + internal_translation.by = from + t * (to - from) +end + +""" + compute_transition!(internal_translation::InternalScaling, translation::Scaling, + video, action::AbstractAction, frame) + +Computes the scaling transformation for the `action`. +If the `scaling` is given directly it uses the frame number for interpolation. +If `scaling` includes symbols, the current definition of that symbol is looked up +and used for computation. +""" +function compute_transition!( + internal_scale::InternalScaling, + scale::Scaling, + video, + action::AbstractAction, + frame, +) + t = get_interpolation(action, frame) + from, to = scale.from, scale.to + + if !scale.compute_from_once || frame == first(get_frames(action)) + from isa Symbol && (from = get_scale(from)) + if scale.compute_from_once + scale.from = from + end + end + to isa Symbol && (to = get_scale(to)) + internal_scale.scale = from .+ t .* (to .- from) +end + +""" + perform_transformation(action::AbstractAction) + +Perform the transformations as described in action.internal_transitions +""" +function perform_transformation(action::AbstractAction) + for trans in action.internal_transitions + perform_transformation(trans) + end +end + +""" + perform_transformation(trans::InternalTranslation) + +Translate as described in `trans`. +""" +function perform_transformation(trans::InternalTranslation) + translate(trans.by) +end + +""" + perform_transformation(trans::InternalRotation) + +Translate and rotate as described in `trans`. +""" +function perform_transformation(trans::InternalRotation) + translate(trans.center) + rotate(trans.angle) +end + +""" + perform_transformation(trans::InternalScaling) + +Scale as described in `trans`. +""" +function perform_transformation(trans::InternalScaling) + scaleto(trans.scale...) +end diff --git a/src/util.jl b/src/util.jl index bca506a8b..bdd8be4bd 100644 --- a/src/util.jl +++ b/src/util.jl @@ -28,3 +28,33 @@ function get_current_setting() action = CURRENT_ACTION[1] return action.current_setting end + +""" + get_interpolation(frames::UnitRange, frame) + +Return a value between 0 and 1 which represents the relative `frame` inside `frames`. +""" +function get_interpolation(frames::UnitRange, frame) + frame == last(frames) && return 1.0 + t = (frame - first(frames)) / (length(frames) - 1) + # makes sense to only allow 0 โ‰ค t โ‰ค 1 + t = min(1.0, t) +end + +""" + get_interpolation(action::AbstractAction, frame) + +Return the value of the `action.anim` Animation based on the relative frame given by +`get_interpolation(get_frames(action), frame)` +""" +function get_interpolation(action::AbstractAction, frame) + t = get_interpolation(get_frames(action), frame) + if !(action.anim.frames[end].t โ‰ˆ 1) + @warn "Animations should be defined from 0.0 to 1.0" + end + return at(action.anim, t) +end + +function isapprox_discrete(val; atol = 1e-4) + return isapprox(val, round(val); atol = atol) +end diff --git a/test/Project.toml b/test/Project.toml index 1a1e1b28d..145c5d826 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,4 +1,7 @@ [deps] +Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" +Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" +GtkReactive = "27996c0f-39cd-5cc1-a27a-05f136f946b6" ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" @@ -8,5 +11,5 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" VideoIO = "d6d074c3-1acf-5d4c-9a43-ef38773959a2" [compat] -VideoIO = "0.8.1" LaTeXStrings = "1.1" +VideoIO = "0.8.1" diff --git a/test/animations.jl b/test/animations.jl index 29d651a78..8474a2ee5 100644 --- a/test/animations.jl +++ b/test/animations.jl @@ -102,40 +102,33 @@ end video, [ BackgroundAction( - 1:2, + 1:30, ground, Rotation(0.0), Translation(Point(25, 25), Point(25, 25)), ), Action(latex_title), + Action(:red_ball, (args...) -> circ(p1, "red"), Rotation(from_rot, to_rot)), Action( - Rel(-1:0), - :red_ball, - (args...) -> circ(p1, "red"), - Rotation(from_rot, to_rot), - ), - Action( - 1:2, + 1:30, :blue_ball, (args...) -> circ(p2, "blue"), Rotation(to_rot, from_rot, :red_ball), ), Action( - 1:2, + 1:30, (video, args...) -> path!(path_of_red, get_position(:red_ball), "red"), ), Action(:same, (video, args...) -> path!(path_of_blue, pos(:blue_ball), "blue")), - Action(1:2, (args...) -> rad(pos(:red_ball), pos(:blue_ball), "black")), + Action(1:30, (args...) -> rad(pos(:red_ball), pos(:blue_ball), "black")), ], tempdirectory = "images", pathname = "dancing.mp4", + framerate = 1, ) - # The `y` for isapprox was determined experimentally on a Fedora 32 OS. - # On that machine, the time duration was found to be `0.067` seconds. - # The `atol` was also experimentally determined based upon VideoIO's - # `get_duration` function. - @test isapprox(VideoIO.get_duration("dancing.mp4"), 0.07, atol = 0.01) + # 30 frames with a framerate of 1 should take about 30 seconds ;) + @test isapprox(VideoIO.get_duration("dancing.mp4"), 30.0, atol = 0.1) rm("dancing.mp4") end @@ -307,6 +300,31 @@ acirc(args...) = circle(Point(100, 100), 30) end end +@testset "morphing star2circle and back with fill" begin + video = Video(500, 500) + javis( + video, + [ + BackgroundAction( + 1:20, + :framenumber, + (args...) -> ground_color("white", "black", args[3]), + ), + Action(1:10, (args...) -> circle(Point(-100, 0), val(:framenumber), :fill)), + Action(1:10, morph(astar, acirc; action = :fill)), + Action(11:20, morph(acirc, astar; action = :fill)), + ], + tempdirectory = "images", + pathname = "", + ) + + @test_reference "refs/star2circle_fill5.png" load("images/0000000005.png") + @test_reference "refs/star2circle_fill15.png" load("images/0000000015.png") + for i in 1:20 + rm("images/$(lpad(i, 10, "0")).png") + end +end + function ground_nicholas(args...) background("white") sethue("blue") @@ -412,18 +430,19 @@ end [ BackgroundAction(1:50, ground_opacity), Action( + 1:42, (args...) -> circ(); subactions = [ SubAction(1:25, appear(:fade)), - SubAction(26:50, disappear(:fade)), + SubAction(26:42, disappear(:fade)), ], ), Action( 5:50, (args...) -> square_opacity(Point(-100, 0), 60); subactions = [ - SubAction(1:15, appear(:fade)), - SubAction(Rel(20), Translation(100, 50)), + SubAction(1:15, linear(), appear(:fade)), + SubAction(Rel(20), linear(), Translation(100, 50)), SubAction(Rel(5), disappear(:fade)), # for global frames 46-50 it should still be disappeared ], @@ -433,9 +452,9 @@ end pathname = "", ) - @test_reference "refs/circlerSquare07opacity.png" load("images/0000000007.png") - @test_reference "refs/circlerSquare25opacity.png" load("images/0000000025.png") - @test_reference "refs/circlerSquare42opacity.png" load("images/0000000042.png") + @test_reference "refs/circleSquare07opacity.png" load("images/0000000007.png") + @test_reference "refs/circleSquare25opacity.png" load("images/0000000025.png") + @test_reference "refs/circleSquare42opacity.png" load("images/0000000042.png") # test that the last frame is completely white @test sum(load("images/0000000050.png")) == RGB{Float64}(500 * 500, 500 * 500, 500 * 500) @@ -444,6 +463,137 @@ end end end +@testset "Animations.jl translate()" begin + video = Video(500, 500) + circle_anim = Animation( + [0, 0.3, 0.6, 1], # must go from 0 to 1 + [O, Point(150, 0), Point(150, 150), O], + [sineio(), polyin(5), expin(8)], + ) + javis( + video, + [ + BackgroundAction(1:150, ground), + Action( + (args...) -> circle(O, 25, :fill); + subactions = [SubAction(1:150, circle_anim, translate())], + ), + ], + tempdirectory = "images", + pathname = "", + ) + @test_reference "refs/anim_circle020.png" load("images/0000000020.png") + @test_reference "refs/anim_circle075.png" load("images/0000000075.png") + @test_reference "refs/anim_circle142.png" load("images/0000000142.png") + for i in 1:150 + rm("images/$(lpad(i, 10, "0")).png") + end +end + +@testset "Animations.jl @warn" begin + video = Video(500, 500) + circle_anim = Animation( + [0, 0.3, 0.6, 1.2], # must go from 0 to 1 + [O, Point(150, 0), Point(150, 150), O], + [sineio(), polyin(5), expin(8)], + ) + # warning as animation goes to 1.2 but should go to 1.0 + @test_logs (:warn,) (:warn,) javis( + video, + [ + BackgroundAction(1:2, ground), + Action( + (args...) -> circle(O, 25, :fill); + subactions = [SubAction(1:2, circle_anim, translate())], + ), + ], + pathname = "", + ) +end + +@testset "Animations.jl rotate, scale, translate" begin + video = Video(500, 500) + translate_anim = Animation( + [0, 1], # must go from 0 to 1 + [O, Point(150, 0)], + [sineio()], + ) + + translate_back_anim = Animation( + [0, 1], # must go from 0 to 1 + [O, Point(-150, 0)], + [sineio()], + ) + + rotate_anim = Animation( + [0, 1], # must go from 0 to 1 + [0, 2ฯ€], + [linear()], + ) + + color_anim = Animation( + [0, 0.5, 1], # must go from 0 to 1 + [Lab(colorant"red"), Lab(colorant"cyan"), Lab(colorant"black")], + [sineio(), sineio()], + ) + + javis( + video, + [ + BackgroundAction(1:150, ground), + Action( + (args...) -> circle(O, 25, :fill); + subactions = [ + SubAction(1:10, sineio(), scale()), + SubAction(11:50, translate_anim, translate()), + SubAction(51:100, rotate_anim, rotate_around(Point(-150, 0))), + SubAction(101:140, translate_back_anim, translate()), + SubAction(141:150, rev(sineio()), scale()), + SubAction(1:150, color_anim, sethue()), + ], + ), + ], + tempdirectory = "images", + pathname = "", + ) + + @test_reference "refs/animations_all_05.png" load("images/0000000005.png") + @test_reference "refs/animations_all_25.png" load("images/0000000025.png") + @test_reference "refs/animations_all_65.png" load("images/0000000065.png") + @test_reference "refs/animations_all_125.png" load("images/0000000125.png") + @test_reference "refs/animations_all_145.png" load("images/0000000145.png") + + for i in 1:150 + rm("images/$(lpad(i, 10, "0")).png") + end +end + +@testset "Rotate around center animation" begin + rotate_anim = Animation([0.0, 1.0], [0.0, 2ฯ€], [sineio()]) + + video = Video(500, 500) + javis( + video, + [ + BackgroundAction(1:50, ground), + BackgroundAction(1:50, (args...) -> scaleto(2)), + Action( + (args...) -> circ(Point(75, 0)), + subactions = [SubAction(1:50, rotate_anim, rotate())], + ), + ], + tempdirectory = "images", + pathname = "", + ) + + @test_reference "refs/rotate_center25.png" load("images/0000000025.png") + @test_reference "refs/rotate_center45.png" load("images/0000000045.png") + + for i in 1:50 + rm("images/$(lpad(i, 10, "0")).png") + end +end + @testset "Scaling circle" begin video = Video(500, 500) javis( @@ -497,6 +647,179 @@ end end end +@testset "animating text" begin + # does only test that it doesn't fail but I wasn't able to test this on all platforms + # with using reference tests + video = Video(400, 300) + + javis( + video, + [ + BackgroundAction(1:100, ground), + BackgroundAction(1:100, (args...) -> fontsize(30)), + Action( + 1:100, + (args...) -> text("Hello Stream!", -50, 50; halign = :centre); + subactions = [ + SubAction(1:15, sineio(), appear(:draw_text)), + SubAction(76:100, sineio(), disappear(:draw_text)), + ], + ), + Action( + 1:100, + (args...) -> + text("Hello World!", -50, -100; halign = :wrong, valign = :wrong); + subactions = [ + SubAction(1:15, sineio(), appear(:draw_text)), + SubAction(76:100, sineio(), disappear(:draw_text)), + ], + ), + ], + tempdirectory = "images", + pathname = "", + ) + + img07 = load("images/$(lpad(7, 10, "0")).png") + img30 = load("images/$(lpad(30, 10, "0")).png") + img82 = load("images/$(lpad(30, 10, "0")).png") + + # does only test that it doesn't fail but I wasn't able to test this on all platforms + # with using reference tests + video = Video(400, 300) + + javis( + video, + [ + BackgroundAction(1:100, ground), + BackgroundAction(1:100, (args...) -> fontsize(30)), + Action( + 1:100, + (args...) -> text("Hello Stream!", -50, 50; halign = :center); + subactions = [ + SubAction(1:15, sineio(), appear(:draw_text)), + SubAction(76:100, sineio(), disappear(:draw_text)), + ], + ), + Action( + 1:100, + (args...) -> text("Hello World!", -50, -100); + subactions = [ + SubAction(1:15, sineio(), appear(:draw_text)), + SubAction(76:100, sineio(), disappear(:draw_text)), + ], + ), + ], + tempdirectory = "images", + pathname = "", + ) + + img_other07 = load("images/$(lpad(7, 10, "0")).png") + img_other30 = load("images/$(lpad(30, 10, "0")).png") + img_other82 = load("images/$(lpad(30, 10, "0")).png") + + @test img07 == img_other07 + @test img30 == img_other30 + @test img82 == img_other82 + + for i in 1:100 + rm("images/$(lpad(i, 10, "0")).png") + end +end + +@testset "Following a path" begin + video = Video(800, 600) + + anim = Animation([0, 1], [0.0, 2.0], [sineio()]) + + color_anim = Animation( + [0, 0.5, 1], # must go from 0 to 1 + [Lab(colorant"red"), Lab(colorant"cyan"), Lab(colorant"red")], + [sineio(), sineio()], + ) + + actions = [ + Action( + frame_start:(frame_start + 149), + (args...) -> star(O, 20, 5, 0.5, 0, :fill); + subactions = [ + SubAction(1:150, anim, follow_path(star(O, 100))), + SubAction(1:150, color_anim, sethue()), + ], + ) for frame_start in 1:7:22 + ] + + javis( + video, + [BackgroundAction(1:180, ground), actions...], + tempdirectory = "images", + pathname = "", + ) + + @test_reference "refs/followPath10.png" load("images/0000000010.png") + @test_reference "refs/followPath30.png" load("images/0000000030.png") + @test_reference "refs/followPath100.png" load("images/0000000100.png") + @test_reference "refs/followPath160.png" load("images/0000000160.png") + + # Following along a bezier path + function simple_bezier() + P1 = Point(-300, 0) + CP1 = Point(-200, -200) + CP2 = Point(200, -200) + P2 = Point(300, 0) + + beziersegment = BezierPathSegment(P1, CP1, CP2, P2) + beziertopoly(beziersegment) + end + + video = Video(800, 600) + + anim = Animation([0, 1], [0.0, 1.0], [sineio()]) + + color_anim = Animation( + [0, 0.5, 1], # must go from 0 to 1 + [Lab(colorant"red"), Lab(colorant"cyan"), Lab(colorant"red")], + [sineio(), sineio()], + ) + + actions = [ + Action( + frame_start:(frame_start + 149), + (args...) -> star(O, 20, 5, 0.5, 0, :fill); + subactions = [ + SubAction(1:1, Translation(simple_bezier()[1] + Point(0, 3 * frame_start))), + SubAction(1:10, appear(:fade)), + SubAction( + 11:150, + anim, + follow_path( + simple_bezier() .- (simple_bezier()[1] + Point(0, 3 * frame_start)); + closed = false, + ), + ), + SubAction(1:150, color_anim, sethue()), + ], + ) for frame_start in 1:7:22 + ] + + javis( + video, + [BackgroundAction(1:180, ground), actions...], + tempdirectory = "images", + pathname = "", + ) + + @test_reference "refs/followPathBezier1.png" load("images/0000000001.png") + @test_reference "refs/followPathBezier5.png" load("images/0000000005.png") + @test_reference "refs/followPathBezier10.png" load("images/0000000010.png") + @test_reference "refs/followPathBezier30.png" load("images/0000000030.png") + @test_reference "refs/followPathBezier100.png" load("images/0000000100.png") + @test_reference "refs/followPathBezier160.png" load("images/0000000160.png") + + for i in 1:180 + rm("images/$(lpad(i, 10, "0")).png") + end +end + @testset "test default kwargs" begin video = Video(500, 500) pathname = javis(video, [Action(1:10, ground), Action(1:10, morph(astar, acirc))]) diff --git a/test/refs/anim_circle020.png b/test/refs/anim_circle020.png new file mode 100644 index 000000000..2888e4d6a Binary files /dev/null and b/test/refs/anim_circle020.png differ diff --git a/test/refs/anim_circle075.png b/test/refs/anim_circle075.png new file mode 100644 index 000000000..3388c15b8 Binary files /dev/null and b/test/refs/anim_circle075.png differ diff --git a/test/refs/anim_circle142.png b/test/refs/anim_circle142.png new file mode 100644 index 000000000..5b14bd74a Binary files /dev/null and b/test/refs/anim_circle142.png differ diff --git a/test/refs/animations_all_05.png b/test/refs/animations_all_05.png new file mode 100644 index 000000000..852f35856 Binary files /dev/null and b/test/refs/animations_all_05.png differ diff --git a/test/refs/animations_all_125.png b/test/refs/animations_all_125.png new file mode 100644 index 000000000..aacdaf5ef Binary files /dev/null and b/test/refs/animations_all_125.png differ diff --git a/test/refs/animations_all_145.png b/test/refs/animations_all_145.png new file mode 100644 index 000000000..75725ed7e Binary files /dev/null and b/test/refs/animations_all_145.png differ diff --git a/test/refs/animations_all_25.png b/test/refs/animations_all_25.png new file mode 100644 index 000000000..cff2a666e Binary files /dev/null and b/test/refs/animations_all_25.png differ diff --git a/test/refs/animations_all_65.png b/test/refs/animations_all_65.png new file mode 100644 index 000000000..acd732e36 Binary files /dev/null and b/test/refs/animations_all_65.png differ diff --git a/test/refs/circlerSquare07opacity.png b/test/refs/circleSquare07opacity.png similarity index 100% rename from test/refs/circlerSquare07opacity.png rename to test/refs/circleSquare07opacity.png diff --git a/test/refs/circlerSquare25opacity.png b/test/refs/circleSquare25opacity.png similarity index 100% rename from test/refs/circlerSquare25opacity.png rename to test/refs/circleSquare25opacity.png diff --git a/test/refs/circleSquare42opacity.png b/test/refs/circleSquare42opacity.png new file mode 100644 index 000000000..1a2e01b90 Binary files /dev/null and b/test/refs/circleSquare42opacity.png differ diff --git a/test/refs/circlerSquare42opacity.png b/test/refs/circlerSquare42opacity.png deleted file mode 100644 index 2f2c1f37b..000000000 Binary files a/test/refs/circlerSquare42opacity.png and /dev/null differ diff --git a/test/refs/followPath10.png b/test/refs/followPath10.png new file mode 100644 index 000000000..f5a40a050 Binary files /dev/null and b/test/refs/followPath10.png differ diff --git a/test/refs/followPath100.png b/test/refs/followPath100.png new file mode 100644 index 000000000..895010bc1 Binary files /dev/null and b/test/refs/followPath100.png differ diff --git a/test/refs/followPath160.png b/test/refs/followPath160.png new file mode 100644 index 000000000..c470547ba Binary files /dev/null and b/test/refs/followPath160.png differ diff --git a/test/refs/followPath30.png b/test/refs/followPath30.png new file mode 100644 index 000000000..39b891cf5 Binary files /dev/null and b/test/refs/followPath30.png differ diff --git a/test/refs/followPathBezier1.png b/test/refs/followPathBezier1.png new file mode 100644 index 000000000..b59921cc2 Binary files /dev/null and b/test/refs/followPathBezier1.png differ diff --git a/test/refs/followPathBezier10.png b/test/refs/followPathBezier10.png new file mode 100644 index 000000000..c00a751e2 Binary files /dev/null and b/test/refs/followPathBezier10.png differ diff --git a/test/refs/followPathBezier100.png b/test/refs/followPathBezier100.png new file mode 100644 index 000000000..3bfb54dab Binary files /dev/null and b/test/refs/followPathBezier100.png differ diff --git a/test/refs/followPathBezier160.png b/test/refs/followPathBezier160.png new file mode 100644 index 000000000..67b095adf Binary files /dev/null and b/test/refs/followPathBezier160.png differ diff --git a/test/refs/followPathBezier30.png b/test/refs/followPathBezier30.png new file mode 100644 index 000000000..a43d01214 Binary files /dev/null and b/test/refs/followPathBezier30.png differ diff --git a/test/refs/followPathBezier5.png b/test/refs/followPathBezier5.png new file mode 100644 index 000000000..47b368413 Binary files /dev/null and b/test/refs/followPathBezier5.png differ diff --git a/test/refs/rotate_center25.png b/test/refs/rotate_center25.png new file mode 100644 index 000000000..87acb328d Binary files /dev/null and b/test/refs/rotate_center25.png differ diff --git a/test/refs/rotate_center45.png b/test/refs/rotate_center45.png new file mode 100644 index 000000000..69bf8bb5a Binary files /dev/null and b/test/refs/rotate_center45.png differ diff --git a/test/refs/star2circle_fill15.png b/test/refs/star2circle_fill15.png new file mode 100644 index 000000000..b46afc9eb Binary files /dev/null and b/test/refs/star2circle_fill15.png differ diff --git a/test/refs/star2circle_fill5.png b/test/refs/star2circle_fill5.png new file mode 100644 index 000000000..e952fc7da Binary files /dev/null and b/test/refs/star2circle_fill5.png differ diff --git a/test/runtests.jl b/test/runtests.jl index 353d8d476..b8bbba7a2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,6 @@ +using Animations +using GtkReactive +using Gtk: get_gtk_property, visible using Images using Javis using LaTeXStrings @@ -5,6 +8,7 @@ using ReferenceTests using Test using VideoIO + @testset "Unit" begin include("unit.jl") end @@ -14,3 +18,6 @@ end @testset "Animations" begin include("animations.jl") end +@testset "Javis Viewer" begin + include("viewer.jl") +end diff --git a/test/svg.jl b/test/svg.jl index 40f73e430..9657bc8ff 100644 --- a/test/svg.jl +++ b/test/svg.jl @@ -102,32 +102,3 @@ end @test_reference "refs/ologn_circ.png" load("images/0000000001.png") rm("images/0000000001.png") end - -@testset "latex pos in function DEPRECATED" begin - function latex_ground(args...) - background("white") - sethue("black") - fontsize(30) - end - - function foreground(latex_string) - translate(50, 40) - fsize = get_fontsize() - latex(latex_string, fsize) # is automatically changed to fontsize(30); latex(latex_string) - translate(-50, -40) - circle(O, 20, :fill) # should be in the center and not affected by latex - end - - video = Video(400, 200) - javis( - video, - [ - BackgroundAction(1:1, latex_ground), - Action((args...) -> foreground(L"\mathcal{O}(\log{n})")), - ], - tempdirectory = "images", - pathname = "", - ) - @test_reference "refs/ologn_circ.png" load("images/0000000001.png") - rm("images/0000000001.png") -end diff --git a/test/unit.jl b/test/unit.jl index 6ca7b3144..ad4a71aef 100644 --- a/test/unit.jl +++ b/test/unit.jl @@ -29,25 +29,192 @@ action = Action(1:100, () -> 1, Translation(Point(1, 1), Point(100, 100))) # needs internal translation as well push!(action.internal_transitions, Javis.InternalTranslation(O)) - Javis.compute_transformation!(action, video, 1) + Javis.compute_transition!(action, video, 1) @test action.internal_transitions[1].by == Point(1, 1) - Javis.compute_transformation!(action, video, 50) + Javis.compute_transition!(action, video, 50) @test action.internal_transitions[1].by == Point(50, 50) - Javis.compute_transformation!(action, video, 100) + Javis.compute_transition!(action, video, 100) + @test action.internal_transitions[1].by == Point(100, 100) + + # with easing function + video = Video(500, 500) + # dummy action doesn't need a real function + action = Action(1:100, () -> 1, sineio(), Translation(Point(1, 1), Point(100, 100))) + anim = Animation([0.0, 1.0], [1.0, 100.0], [sineio()]) + m = 49 / 99 + # needs internal translation as well + push!(action.internal_transitions, Javis.InternalTranslation(O)) + Javis.compute_transition!(action, video, 1) + @test action.internal_transitions[1].by == Point(1, 1) + Javis.compute_transition!(action, video, 50) + @test action.internal_transitions[1].by == Point(at(anim, m), at(anim, m)) + Javis.compute_transition!(action, video, 100) + @test action.internal_transitions[1].by == Point(100, 100) + + # with animation function + anim_01 = Animation([0.0, 1.0], [0.0, 1.0], [sineio()]) + video = Video(500, 500) + # dummy action doesn't need a real function + action = Action(1:100, () -> 1, anim_01, Translation(Point(1, 1), Point(100, 100))) + anim = Animation([0.0, 1.0], [1.0, 100.0], [sineio()]) + m = 49 / 99 + # needs internal translation as well + push!(action.internal_transitions, Javis.InternalTranslation(O)) + Javis.compute_transition!(action, video, 1) + @test action.internal_transitions[1].by == Point(1, 1) + Javis.compute_transition!(action, video, 50) + @test action.internal_transitions[1].by == Point(at(anim, m), at(anim, m)) + Javis.compute_transition!(action, video, 100) @test action.internal_transitions[1].by == Point(100, 100) end + @testset "translation subaction" begin + video = Video(500, 500) + # dummy action doesn't need a real function + action = Action( + 1:100, + () -> 1; + subactions = [SubAction(1:100, Translation(Point(1, 1), Point(100, 100)))], + ) + # needs internal translation as well + push!(action.subactions[1].internal_transitions, Javis.InternalTranslation(O)) + Javis.compute_transition!(action.subactions[1], video, 1) + @test action.subactions[1].internal_transitions[1].by == Point(1, 1) + Javis.compute_transition!(action.subactions[1], video, 50) + @test action.subactions[1].internal_transitions[1].by == Point(50, 50) + Javis.compute_transition!(action.subactions[1], video, 100) + @test action.subactions[1].internal_transitions[1].by == Point(100, 100) + + ######## testing with easing function ############## + video = Video(500, 500) + # dummy action doesn't need a real function + action = Action( + 1:100, + () -> 1; + subactions = [SubAction( + 1:100, + polyout(10), + Translation(Point(1, 1), Point(100, 100)), + )], + ) + anim = Animation([0.0, 1.0], [1.0, 100.0], [polyout(10)]) + m = 49 / 99 + # needs internal translation as well + push!(action.subactions[1].internal_transitions, Javis.InternalTranslation(O)) + Javis.compute_transition!(action.subactions[1], video, 1) + @test action.subactions[1].internal_transitions[1].by == Point(1, 1) + Javis.compute_transition!(action.subactions[1], video, 50) + @test action.subactions[1].internal_transitions[1].by == + Point(at(anim, m), at(anim, m)) + Javis.compute_transition!(action.subactions[1], video, 100) + @test action.subactions[1].internal_transitions[1].by == Point(100, 100) + + ######## testing with animation ############## + video = Video(500, 500) + anim_01 = Animation([0.0, 1.0], [0.0, 1.0], [polyin(10)]) + # dummy action doesn't need a real function + action = Action( + 1:100, + () -> 1; + subactions = [SubAction( + 1:100, + anim_01, + Translation(Point(1, 1), Point(100, 100)), + )], + ) + anim = Animation([0.0, 1.0], [1.0, 100.0], [polyin(10)]) + m = 49 / 99 + # needs internal translation as well + push!(action.subactions[1].internal_transitions, Javis.InternalTranslation(O)) + Javis.compute_transition!(action.subactions[1], video, 1) + @test action.subactions[1].internal_transitions[1].by == Point(1, 1) + Javis.compute_transition!(action.subactions[1], video, 50) + @test action.subactions[1].internal_transitions[1].by == + Point(at(anim, m), at(anim, m)) + Javis.compute_transition!(action.subactions[1], video, 100) + @test action.subactions[1].internal_transitions[1].by == Point(100, 100) + end + + @testset "Relative frames" begin + video = Video(500, 500) + action = Action(Rel(10), (args...) -> 1, Translation(Point(1, 1), Point(100, 100))) + # dummy action doesn't need a real function + test_file = javis( + video, + [ + Action(1:100, (args...) -> 1, Translation(Point(1, 1), Point(100, 100))), + action, + ], + ) + @test Javis.get_frames(action) == 101:110 + rm(test_file) + end + + @testset "translation from origin" begin + video = Video(500, 500) + # dummy action doesn't need a real function + action = Action(1:100, () -> 1, Translation(Point(99, 99))) + # needs internal translation as well + push!(action.internal_transitions, Javis.InternalTranslation(O)) + + Javis.compute_transition!(action, video, 1) + @test action.internal_transitions[1].by == O + Javis.compute_transition!(action, video, 50) + @test action.internal_transitions[1].by == Point(49, 49) + Javis.compute_transition!(action, video, 100) + @test action.internal_transitions[1].by == Point(99, 99) + + video = Video(500, 500) + # dummy action doesn't need a real function + action = Action(1:100, () -> 1, Translation(99, 99)) + # needs internal translation as well + push!(action.internal_transitions, Javis.InternalTranslation(O)) + + Javis.compute_transition!(action, video, 1) + @test action.internal_transitions[1].by == O + Javis.compute_transition!(action, video, 50) + @test action.internal_transitions[1].by == Point(49, 49) + Javis.compute_transition!(action, video, 100) + @test action.internal_transitions[1].by == Point(99, 99) + end + + @testset "rotations" begin + video = Video(500, 500) + # dummy action doesn't need a real function + action = Action(1:100, () -> 1, Rotation(2ฯ€)) + # needs internal translation as well + push!(action.internal_transitions, Javis.InternalRotation(0.0, O)) + + Javis.compute_transition!(action, video, 1) + @test action.internal_transitions[1].angle == 0.0 + Javis.compute_transition!(action, video, 100) + @test action.internal_transitions[1].angle == 2ฯ€ + + video = Video(500, 500) + # dummy action doesn't need a real function + action = Action(1:100, () -> 1, Rotation(2ฯ€, Point(2.0, 5.0))) + # needs internal translation as well + push!(action.internal_transitions, Javis.InternalRotation(0.0, O)) + + Javis.compute_transition!(action, video, 1) + @test action.internal_transitions[1].angle == 0.0 + @test action.internal_transitions[1].center == Point(2.0, 5.0) + Javis.compute_transition!(action, video, 100) + @test action.internal_transitions[1].angle == 2ฯ€ + @test action.internal_transitions[1].center == Point(2.0, 5.0) + end + @testset "scaling" begin video = Video(500, 500) # dummy action doesn't need a real function action = Action(0:100, () -> 1, Scaling(0.0, 1.0)) # needs internal scaling as well push!(action.internal_transitions, Javis.InternalScaling((0, 0))) - Javis.compute_transformation!(action, video, 0) + Javis.compute_transition!(action, video, 0) @test action.internal_transitions[1].scale == (0.0, 0.0) - Javis.compute_transformation!(action, video, 50) + Javis.compute_transition!(action, video, 50) @test action.internal_transitions[1].scale == (0.5, 0.5) - Javis.compute_transformation!(action, video, 100) + Javis.compute_transition!(action, video, 100) @test action.internal_transitions[1].scale == (1.0, 1.0) end @@ -73,11 +240,11 @@ # needs internal translation as well push!(action.internal_transitions, Javis.InternalTranslation(O)) - Javis.compute_transformation!(action, video, 1) + Javis.compute_transition!(action, video, 1) @test action.internal_transitions[1].by == O - Javis.compute_transformation!(action, video, 50) + Javis.compute_transition!(action, video, 50) @test action.internal_transitions[1].by == Point(49, 49) - Javis.compute_transformation!(action, video, 100) + Javis.compute_transition!(action, video, 100) @test action.internal_transitions[1].by == Point(99, 99) video = Video(500, 500) @@ -86,11 +253,11 @@ # needs internal translation as well push!(action.internal_transitions, Javis.InternalTranslation(O)) - Javis.compute_transformation!(action, video, 1) + Javis.compute_transition!(action, video, 1) @test action.internal_transitions[1].by == O - Javis.compute_transformation!(action, video, 50) + Javis.compute_transition!(action, video, 50) @test action.internal_transitions[1].by == Point(49, 49) - Javis.compute_transformation!(action, video, 100) + Javis.compute_transition!(action, video, 100) @test action.internal_transitions[1].by == Point(99, 99) end @@ -101,9 +268,9 @@ # needs internal translation as well push!(action.internal_transitions, Javis.InternalRotation(0.0, O)) - Javis.compute_transformation!(action, video, 1) + Javis.compute_transition!(action, video, 1) @test action.internal_transitions[1].angle == 0.0 - Javis.compute_transformation!(action, video, 100) + Javis.compute_transition!(action, video, 100) @test action.internal_transitions[1].angle == 2ฯ€ video = Video(500, 500) @@ -112,17 +279,30 @@ # needs internal translation as well push!(action.internal_transitions, Javis.InternalRotation(0.0, O)) - Javis.compute_transformation!(action, video, 1) + Javis.compute_transition!(action, video, 1) @test action.internal_transitions[1].angle == 0.0 @test action.internal_transitions[1].center == Point(2.0, 5.0) - Javis.compute_transformation!(action, video, 100) + Javis.compute_transition!(action, video, 100) @test action.internal_transitions[1].angle == 2ฯ€ @test action.internal_transitions[1].center == Point(2.0, 5.0) end - @testset "Frames errors" begin + @testset "action with a single frame" begin video = Video(500, 500) + # dummy action doesn't need a real function + action = Action(1:1, () -> 1, Translation(Point(10, 10))) + # needs internal translation as well + push!(action.internal_transitions, Javis.InternalTranslation(O)) + Javis.compute_transition!(action, video, 1) + @test action.internal_transitions[1].by == Point(10, 10) + end + + @testset "Frames errors" begin + # throws because a Video object was not previously defined + empty!(Javis.CURRENT_VIDEO) + @test_throws ErrorException Action(1:10, (args...) -> star(O, 20, 5, 0.5, 0, :fill)) # throws because the frames of the first action need to be defined explicitly + video = Video(500, 500) @test_throws ArgumentError javis( video, [Action((args...) -> 1, Translation(Point(1, 1), Point(100, 100)))], diff --git a/test/viewer.jl b/test/viewer.jl new file mode 100644 index 000000000..1bdf9eaa4 --- /dev/null +++ b/test/viewer.jl @@ -0,0 +1,51 @@ +function ground(args...) + background("white") + sethue("black") +end + +@testset "Javis Viewer" begin + astar(args...) = star(O, 50) + acirc(args...) = circle(Point(100, 100), 50) + + vid = Video(500, 500) + action_list = [ + BackgroundAction(1:100, ground), + Action(1:100, morph(astar, acirc; action = :fill)), + ] + + javis(vid, action_list, pathname = "") + + viewer_win, frame_dims, r_slide, tbox, canvas, actions, total_frames, video = + Javis._javis_viewer(vid, 100, action_list, false) + visible(viewer_win, false) + + @test get_gtk_property(viewer_win, :title, String) == "Javis Viewer" + + Javis._increment(video, [r_slide, tbox], actions, frame_dims, canvas, total_frames) + sleep(0.1) + curr_frame = Reactive.value(r_slide) + second_frame = Javis.get_javis_frame(video, actions, curr_frame) + @test Reactive.value(r_slide) == 2 + + Javis._decrement(video, [r_slide, tbox], actions, frame_dims, canvas, total_frames) + sleep(0.1) + curr_frame = Reactive.value(r_slide) + first_frame = Javis.get_javis_frame(video, actions, curr_frame) + @test Reactive.value(r_slide) == 1 + + @test first_frame != second_frame + + Javis._decrement(video, [r_slide, tbox], actions, frame_dims, canvas, total_frames) + sleep(0.1) + curr_frame = Reactive.value(r_slide) + last_frame = Javis.get_javis_frame(video, actions, curr_frame) + @test curr_frame == total_frames + + Javis._increment(video, [r_slide, tbox], actions, frame_dims, canvas, total_frames) + sleep(0.1) + curr_frame = Reactive.value(r_slide) + first_frame = Javis.get_javis_frame(video, actions, curr_frame) + @test curr_frame == 1 + + @test last_frame != first_frame +end