diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aed621db6..58601c4da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: version: - - '1.5' + - '1.6' - '1' os: - ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index f67cd9eb4..910b017b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,14 @@ # PR changes - changed render method for mp4 to use ffmpeg directly inplace of VideoIO - +- Added jpaths a field in Object that is useful for morphs and partial drawing +- Added morphs to arbitrary objects and functions. +- Keyframed morphs with Animations.jl are possible. +- Added ability to partially draw any object, and have animations of showing them get created. +- One tutorial added on how to use morphs +- tutorial on partial draw / show creation +- Few tests for morphs added +- test for partial draw/ show creation ## v0.9.0 (26th of May 2022) - Ability to use Luxor functionality without rendering an animation diff --git a/Project.toml b/Project.toml index d3f156e44..13773b8ae 100644 --- a/Project.toml +++ b/Project.toml @@ -36,6 +36,6 @@ Images = "0.20, 0.21, 0.22, 0.23, 0.24, 0.25" Interact = "0.10" LaTeXStrings = "1.1" LightXML = "0.9" -Luxor = "2.12, 3" +Luxor = "3.5" ProgressMeter = "1" -julia = "1.5" +julia = "1.6" diff --git a/docs/make.jl b/docs/make.jl index 6cc9c832e..7261648f6 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -24,6 +24,8 @@ makedocs(; "tutorials/tutorial_6.md", "tutorials/tutorial_7.md", "tutorials/tutorial_8.md", + "tutorials/tutorial_morphing.md", + "tutorials/tutorial_partialdraw.md", ], "HowTo" => "howto.md", "Workflows" => "workflows.md", diff --git a/docs/src/assets/box_to_circ_anim.gif b/docs/src/assets/box_to_circ_anim.gif new file mode 100644 index 000000000..0ccc365d4 Binary files /dev/null and b/docs/src/assets/box_to_circ_anim.gif differ diff --git a/docs/src/assets/box_to_star_to_circ_anim.gif b/docs/src/assets/box_to_star_to_circ_anim.gif new file mode 100644 index 000000000..b7c03cbfc Binary files /dev/null and b/docs/src/assets/box_to_star_to_circ_anim.gif differ diff --git a/docs/src/assets/circ_to_box.gif b/docs/src/assets/circ_to_box.gif new file mode 100644 index 000000000..79ba3a2a4 Binary files /dev/null and b/docs/src/assets/circ_to_box.gif differ diff --git a/docs/src/assets/circ_to_box_func.gif b/docs/src/assets/circ_to_box_func.gif new file mode 100644 index 000000000..ed2672e18 Binary files /dev/null and b/docs/src/assets/circ_to_box_func.gif differ diff --git a/docs/src/assets/createcircle.gif b/docs/src/assets/createcircle.gif new file mode 100644 index 000000000..7102f4bab Binary files /dev/null and b/docs/src/assets/createcircle.gif differ diff --git a/docs/src/assets/createcircle2.gif b/docs/src/assets/createcircle2.gif new file mode 100644 index 000000000..5bf2910e1 Binary files /dev/null and b/docs/src/assets/createcircle2.gif differ diff --git a/docs/src/assets/createcircle3.gif b/docs/src/assets/createcircle3.gif new file mode 100644 index 000000000..b222b12b6 Binary files /dev/null and b/docs/src/assets/createcircle3.gif differ diff --git a/docs/src/assets/createcircle4.gif b/docs/src/assets/createcircle4.gif new file mode 100644 index 000000000..ceb834bc5 Binary files /dev/null and b/docs/src/assets/createcircle4.gif differ diff --git a/docs/src/assets/demo_partialdraw.gif b/docs/src/assets/demo_partialdraw.gif new file mode 100644 index 000000000..2a9dc87db Binary files /dev/null and b/docs/src/assets/demo_partialdraw.gif differ diff --git a/docs/src/tutorials.md b/docs/src/tutorials.md index 5bb6ab1e5..873a336ee 100644 --- a/docs/src/tutorials.md +++ b/docs/src/tutorials.md @@ -21,5 +21,7 @@ Currently, these tutorials are available: - [**Tutorial 6: Taming the Elements**](tutorials/tutorial_6.md) - how to use `change` to grow or shrink arbitrary objects and using `Javis` with other Julia packages. - [**Tutorial 7: Using Animations.jl to Create something with more Pep!**](tutorials/tutorial_7.md) - an advanced tutorial to make your animations more interesting. - [**Tutorial 8: Fun with Layers! An Intro to `@JLayer`**](tutorials/tutorial_8.md) - a `Javis` state of the art tutorial on how to use `@JLayer` to make composable animations. +- [**Tutorial 9: Morphing**](tutorials/tutorial_morphing.md) - Morphing `Javis` Objects. +- [**Tutorial 10: Animate Object Creation **](tutorials/tutorial_partialdraw.md) - Animate Object Creations. If you spot an issue with any of these tutorials, please let us know! Thank you! diff --git a/docs/src/tutorials/tutorial_morphing.md b/docs/src/tutorials/tutorial_morphing.md new file mode 100644 index 000000000..dcc3b3501 --- /dev/null +++ b/docs/src/tutorials/tutorial_morphing.md @@ -0,0 +1,217 @@ +# **Tutorial 9:** Morphing `Javis` Objects +There are multiple ways to morph an object in Javis. + +- Using `morph_to(::Object)` method. Any Object can be morphed to any other object using this method. +- Using `morph_to(::Function)` method. Similar to `morph_to(::Object)` but morphs to a function instead. Can morph an object to a function that contains Luxor calls to draw what it should morphed into. +- Specifying an Action with an `Animation` along with `morph()` to make keyframed morphings. This helps making and timing a sequence of morph animations easier. + +## Morphing one object to another. + +Like other animations `morph_to(::Object)` is to be used with action. To learn more about Actions refer to [Tutorial 5](tutorial_5.md). +Here is a simple code snippet on how to use `morph_to`... +```julia +using Javis + +video = Video(500,500) +nframes = 160 + +function circdraw(color) + sethue(color) + setopacity(0.5) + circle(O,100,:fillpreserve) + setopacity(1.0) + sethue("white") + strokepath() +end + +function boxdraw(color) + sethue(color) + box(O,100,100,:fillpreserve) + setopacity(1.0) + sethue("white") + strokepath() +end +Background(1:nframes,(args...)->background("black")) +boxobj = Object((v,o,f) -> boxdraw("green")) +circobj = Object((v,o,f) -> circdraw("red")) + +transform_to_box = Action(20:nframes-20, morph_to(boxobj)) +act!(circobj, transform_to_box) +render(video,pathname="circ_to_box.gif") +``` + +![](../assets/circ_to_box.gif) + +If you aren't familiar with this syntax `(v,o,f)-> circdraw("red")` its an "anonymous" function or sometimes called a lambda function. +Basically a nameless function that is written on the spot in that line of code . One might as well use any other function `func` in place of it +(which takes at least 3 arguments `video,object,frame`). Elsewhere in the docs/tutorials you will come across +something of the form `Object( (args...) -> ("some code here") )`. This is [slurping](https://docs.julialang.org/en/v1/manual/faq/#The-two-uses-of-the-...-operator:-slurping-and-splatting) and is similar to packing `*args` in python. + +We created two objects `circobj` and `boxobj` . `circobj` ofcourse is a circle because its drawing function `(v,o,f) -> circdraw("red")` +draws a circle, with a `color=red`, filling at `0.5` opacity, and then makes a white outline (stroke). +`boxobj`'s function draws an opaque green box, with white outline. + +This Object function is called repeatedly at render-time at every frame that the object exists to draw this object. The appropriate `video`,`object`, and `frame` are passed to +this function at render time. +Javis then has other tricks up its sleeve to scale/move/morph whats going to be drawn depending on the +frame and object to effect out animations through Actions. This is roughly the idea behind Javis's Object-Action mechanism + +We defined a `transform_to_box` Action which runs from frame 20 to lastframe-20 . The `Action` morphs whatever object its acted upon, into what looks +like `boxobj`. Note that `boxobj` and `circobj` are separate objects all the time, even after the `Action` (it just happens that they overlap each other). As the Action keeps getting applied at render time frame by frame, the "drawing" of `circobj` starts to look like `boxobj`'s drawing. + +The Action is applied to the `circobj` with the `act!` function. + +Note that the `boxobj` is present throughout as the `circobj` is morphing. +If you want to hide it you can set its opacity to 0 with another action (to make it disappear) and set its frames to be drawn for 1 frame only (for efficiency). +```julia +Background(1:nframes,(args...)->background("black")) +boxobj = Object(1:1 , (args...) -> boxdraw("green") ) +circobj = Object(1:nframes,(args...) -> circdraw("red")) + +transform_to_box = Action(20:nframes-20, morph_to(boxobj)) +hide_action = Action(1:1, (args...)->setopacity(0.0) ) + +act!(circobj, transform_to_box) +act!(boxobj, hide_action) + +render(video,pathname="circ_to_box_hidden.gif") +``` + +However you can directly specify a shape an object has to morph to without making an Object using `morph_to(f::Function)` i.e passing a function as an argument. + +## Morphing an `Object` using a `Function` + +```julia +Background(1:nframes,(args...)->background("black")) +#boxobj = Object(1:1 , (args...) -> boxdraw("green") ) +circobj = Object(1:nframes,(args...) -> circdraw("red")) + +transform_to_box = Action(20:nframes-20, morph_to(boxdraw,["blue"])) +#hide_action = Action(1:1, (args...)->setopacity(0.0) ) + +act!(circobj, transform_to_box) +#act!(boxobj, hide_action) + +render(video,pathname="circ_to_box_func.gif") +``` + +![](../assets/circ_to_box_func.gif) + +Here we have morphed the circle without defining an object to morph to. Rather the shape it has to morph into +is given by a `Function`. +The general syntax is `morph_to(fn::Function,args::Array=[])` +. `args` is an array of arguments that is to be passed to the function. +Here we morph `circobj` to a shape +that would is drawn by `boxdraw("blue")`. Morphed Objects can be furthur morphed into +other shapes by carrying out another `Action` further in the timeline. + +## Keyframed morphs using Animations.jl + +Another mechanism for morphing is by passing `morph()` to `Action` along with an `Animation` +For a tutorial on how to use Animations.jl look at [Tutorial 7](tutorial_7.md), + +```julia +using Javis +using Animations +video = Video(500,500) +nframes = 160 + +function circdraw(color) + sethue(color) + setopacity(0.5) + circle(O,50,:fillpreserve) + setopacity(1.0) + sethue("white") + strokepath() +end + +function boxdraw(color) + sethue(color) + box(O,100,100,:fillpreserve) + setopacity(1.0) + sethue("white") + strokepath() +end + +function stardraw() + sethue("white") + star(O,100,5,0.5,0.0,:stroke) +end + +Background(1:nframes+10,(args...)->background("black")) +boxobj = Object(1:nframes+10 , (args...) -> boxdraw("green") ) + +anim = Animation([0,1],MorphFunction[(boxdraw,["green"]),(circdraw,["red"])] ) + +action = Action(1:nframes,anim,morph()) +act!(boxobj,action) +render(video,pathname="box_to_circ_hidden.gif") +``` + +Take a look at `anim`. It is of type `Animation`. +First lets look at a simpler instance of `Animation`. +``` +Ex:1 +Animation([0,1],[2,4]) +``` +Think of Animations like a "map" or a "function" (in the math sense) thats maps values from its first argument (`[0,1]` above) to another set of values (`[2,4]`) . This means that +0 gets mapped to 2 and 1 gets mapped to 4 and all values inbetween are linearly interpolated. +Another Example +``` +Ex:2 +Animation([0,0.3,1],[3,4.5,7]) +``` + +This animation maps 0 to 3 , 0.3 -> 4.5 and 1->7. And all values inbetween are linear interpolations. + +Take a look at the `Animations.jl` package for an indepth explanation on how to have different interpolations to make your animations look way cooler. ( for example the `sineio` interpolaation is slow at first speeds up in between and gradually slows to a halt ) + +One can in principle provide values beyond 0 and 1 for the first argument however Javis requires `Animation` objects to have the first argument to be from 0 to 1. +This `Animation` object is passed to an `Action`, and Javis interprets 0 to be the first frame +of the Action and 1 to be the final frame of the Action. + +In the big code snippet above we can see that the second array passed to Animation is +an array of `MorphFunction`s. +`MorphFunction` is a struct . This struct has 2 fields. The fields are `func` and `args`. These arguments are used to specify drawing functions and the arguments to be passed to them , The `Array` of `MorphFunction` passed to the `Animation` defines a sequence of shapes/drawings that the `Object` should be morphed into one by one in that order. Each shape/drawing is what would have been got by calling `func(args...)` of the respective `MorphFunction`. In the example above there are only two in shapes in the sequence a green box and a red circle (`boxdraw("green")` and `circdraw("red")`). +Typically the first `MorphFunction` should draw the same thing that `Object` is. + +The general idea of whats going on is we are making an `Animation` that maps `0` (i.e the first frame of the action.) to `MorphFunction(boxdraw,["green"])` and `1` (last frame of the action) to ` MorphFunction(circdraw,["red"])` and Javis handles the interpolation between them. + +Thus we have made an `Animation` called `anim`. Then we made an `action` with this `anim`. We called it `action` . Then we applied the action on our object `boxobj` to get ... + +![](../assets/box_to_circ_anim.gif) + +The way of morphing shines when you have to do multiple morphs in a sequence and with different timings. Lets look at another example taking object to morph box(initial shape) -> star -> circle in a sequence. + +Change the lines describing the animation to +```julia +anim = Animation([0, 0.7, 1],MorphFunction[(boxdraw, ["green"]), (stardraw, []), (circdraw, ["red"])]) +``` +`stardraw` draws a white star without fill. The function does not take an argument and therefore the `Tuple` with `stardraw` should have an empty `Array` at its +second index. If your drawing functions do not take any arguments you can pass it as function itself, and need not wrap it in a `Tuple`. + +Ex. suppose `mydraw1` , `mydraw2` and `mydraw4` take a color as an argument but `mydraw3` does not take any arguments. + +```julia +anim = Animation([0, t1, t2, 1],MorphFunction[ (mydraw1,["red"]), (mydraw2,["blue"]), mydraw3, (mydraw4,["black"]) ]) +``` + +A third way to pass functions to morph into is to simply pass a function an its arguments in a `Tuple` + +```julia +anim = Animation([0, t1, t2, 1],MorphFunction[ (mydraw1,"red"), (mydraw2,"blue"), mydraw3, (mydraw4,"black") ]) +``` + +When passed this way the first element of the Tuple is taken to be the function and the subsequent elements are the arguments to be +passed to the function. + +![](../assets/box_to_star_to_circ_anim.gif) + +What we see now is from the beginning to 0.7 fraction of the `Action`'s frames it carries +out the morphing from a `boxdraw("green")` to `stardraw()`. +And the remainder of the `Action`'s frames it morphs from `stardraw()` to `circdraw("red")`. Once again , do look up Animations.jl and Tutorial 7 to see how you pass easing functions to manipulate the timing of animations (for example ... initially slow - fast in the middle - slow in the end) . +Now you know a bit about Morphing . Remember just like any other Action you can stack morphing actions with other Actions (translations, scaling etc) to bring about effects you desire. + +> **Author(s):** John George Francis (@arbitrandomuser) +> **Date:** May 28th, 2022 \ +> **Tag(s):** action, morphing, object, animation diff --git a/docs/src/tutorials/tutorial_partialdraw.md b/docs/src/tutorials/tutorial_partialdraw.md new file mode 100644 index 000000000..c27d0289a --- /dev/null +++ b/docs/src/tutorials/tutorial_partialdraw.md @@ -0,0 +1,89 @@ +# **Tutorial 10:** Animating Creation of Objects + +A cool effect of bringing in `Objects` into your video/gif is to show it being +drawn out incrementally from nothing. + +![](../assets/demo_partialdraw.gif) + +In this tutorial we'll take a look at how you can make these kind of animations + + +## The `showcreation` function + +The general syntax to animate the creation of an `object` is + +```julia +action = Action(startframe:endframe, showcreation() ) +act!(obj, action) +``` + +This starts the creation of the object at `startframe` and the `object` is completely +drawn when the timeline reaches `endframe`. + +lets see this in example. + +```julia +using Javis +video = Video(500,500) + +Background(1:120,(args...)->begin + background("black") + sethue("white") +end) +circ = Object(1:120, (args...)-> circle(O,100,:stroke)) + +action_showcreate = Action(1:60,showcreation()) +act!(circ,action_showcreate) + +render(video,pathname="createcircle.gif") +``` + +![](../assets/createcircle.gif) + +You should see a circle being created in your video. ( I've added in the frame numbers +in the gif so that the beginning and end of the gif are easily identifiable ) + +What if we wanted to show the object being created at a later point in the timeline. Say +we want it to be created at frame 30 and finish at frame 90. +Thats simple! we change the frames that the Action works on . Change the line with `action_showcreate` in the above example to... + +```julia +action_showcreate = Action(30:90, showcreation()) +``` + +![](../assets/createcircle2.gif) + +Oops! , Thats (probably) not what we wanted. (look at the frame numbers) . What happened was `obj` exists from frame 1 to 120. But the show creation acts on it from 30 to 90. So the object exists from frame 1 to 30 as it is . Then its creation is animated from 30 to 90 and from 90 to 120 it remains as such. + +One way to mitigate this is to change the frames `obj` exists. Make this change in the code +above + +```julia +circ = Object(30:120, (args...)->circle(O,100,:stroke)) +``` + +![](../assets/createcircle3.gif) + +Somethings still wrong!. One thing we forgot is the frames you mention in `Action` are +the frames relative to the `Object`s existence. So what happened now is the object is put +on the scene from frame 30 onwards. The action acts on it from frame 30 relative to when +the object was put. So `30+30` i.e `60` is the frame at which `action` starts. +Can you fix this ? + +```julia +action_showcreate = Action(1:60,showcreation()) +``` + +![](../assets/createcircle4.gif) + +There we go ! It turned out we dont need to change the frames of the action, but the frames of the Object. +Hopefully by intentionally showing you a wrong way to do it you understood the working of `Actions` a little better. + +Another way is to have the object present throughout the video and to "hide" it initially till it is to be shown, with an action +that sets the objects opacity to 0. + +There exists a similar function `showdestruction()` which does exactly the opposite of `showcreation()`. + +> **Author(s):** John George Francis (@arbitrandomuser) +> **Date:** May 28th, 2022 \ +> **Tag(s):** action, morphing, object, animation diff --git a/src/Javis.jl b/src/Javis.jl index 857d7261a..c459dc2fe 100644 --- a/src/Javis.jl +++ b/src/Javis.jl @@ -32,6 +32,9 @@ include("structs/GFrames.jl") include("structs/Frames.jl") include("structs/Scale.jl") +struct JavisLuxorDispatcher <: Luxor.LDispatcher end +#Luxor.DISPATCHER[1] is assigned and instance of this struct in `render` + """ Transformation @@ -53,6 +56,7 @@ Transformation(p, a) = Transformation(p, a, 1.0) Transformation(p, a, s::Float64) = Transformation(p, a, (s, s)) Transformation(p, a, s::Tuple{Float64,Float64}) = Transformation(p, a, Scale(s...)) +include("structs/JPath.jl") include("structs/ObjectSetting.jl") include("structs/Object.jl") include("structs/Transitions.jl") @@ -99,6 +103,7 @@ include("layers.jl") include("util.jl") include("scales.jl") include("luxor_overrides.jl") +include("luxor_utils.jl") include("backgrounds.jl") include("svg2luxor.jl") include("morphs.jl") @@ -106,6 +111,7 @@ include("action_animations.jl") include("javis_viewer.jl") include("latex.jl") include("object_values.jl") +include("partial_draw.jl") """ projection(p::Point, l::Line) @@ -259,6 +265,7 @@ function render( postprocess_frame = default_postprocess, ) CURRENTLY_RENDERING[1] = true + Luxor.DISPATCHER[1] = JavisLuxorDispatcher() layers = video.layers objects = video.objects frames = preprocess_frames!(video) @@ -734,7 +741,7 @@ export Video, Object, Background, Action, RFrames, GFrames export @JLayer, background export Line, Transformation export val, pos, ang, scl, get_value, get_position, get_angle, get_scale -export projection, morph_to +export projection export appear, disappear, rotate_around, follow_path, change export rev export scaleto @@ -751,4 +758,7 @@ export setup_stream, cancel_stream # scales export scale_linear, @scale_layer +export morph, morph_to +export MorphFunction +export showcreation, showdestruction, drawpartial end diff --git a/src/action_animations.jl b/src/action_animations.jl index 449697e4e..9f50c97e6 100644 --- a/src/action_animations.jl +++ b/src/action_animations.jl @@ -486,3 +486,164 @@ function _change(video, object, action, rel_frame, s) val = get_interpolation(action, rel_frame) object.change_keywords[s] = val end + +""" + morph(samples = 100) + +morph() to be used with Action, when an animation from Animations.jl is +provided with `Animation{MorphFunction}` . Default samples for every polygon is 100, increase this if needed. +Animation must be of type Animation{MorphFunction} or Animation{Object} +when passing `morph()` to `Action`. + +Animation{MorphFunction} can be made using the following syntax. (constructors for the following signatures +are written to return the apropriate Animation{MorphFunction}) +``` +anim = Animation([0,a1,a2,...,an,1] , MorphFunction[ (func0,args0), (func1,args1) , (func2,args2) ... (funcn,argsn), (func_fin,args_fin) ]) +``` +0< a1 < a2 < a3... < an < 1.0 + +if your functions dont take any arguments then you may also use... +``` +Animation([0,a1...,a_n,1] , [ f0 , f1 , f2 ...,f_n, f_fin] ) +``` + +The first element is a function. Arguments to be passed to the function can either be wrapped in an Array or as subsequent elements in the Tuple +for example the following two lines have the same effect. +``` +MorphFunction[(func1,[arg11,arg12,arg13]), (func2,[arg21,arg22]) ] +MorphFunction[(func1,arg1,arg2,arg3), (func2,arg21,arg22)] +``` + +Animation can also be of type Animation{Object} +``` +anim = Animation([0,a1 ... , a_n , 1 ] , [obj, obj1, obj2 , ... objn , obj_fin] ) +``` + +Example +``` +using Javis +using Animations +video = Video(500,500) +nframes = 160 + +function circdraw(colo) + sethue(colo) + setopacity(0.5) + circle(O,50,:fillpreserve) + setopacity(1.0) + sethue("white") + strokepath() +end + +function boxdraw(colo) + sethue(colo) + box(O,100,100,:fillpreserve) + setopacity(1.0) + sethue("white") + strokepath() +end + +function stardraw() + sethue("white") + star(O,100,5,0.5,0.0,:stroke) +end + +Background(1:nframes+10,(args...)->background("black")) +boxobj = Object(1:nframes+10 , (args...) -> boxdraw("green") ) +anim = Animation([0, 0.7, 1],[(boxdraw, ["green"]), stardraw, (circdraw, "red")]) + + +action = Action(1:nframes,anim,morph()) +act!(boxobj,action) +render(video,pathname="box_to_star_to_circ.gif") +``` +Above snippet morphs a Box to a Star then to a Circle +""" +function morph(samples = 100) + (video, object, action, rel_frame) -> begin + action.keep = false #refer morph_to as to why. + _morph(video, object, action, rel_frame, samples) + end +end + +function _morph(video, object, action, rel_frame, samples) + #Theres a lot going on here ... + if rel_frame == action.frames.frames[begin] + # If first frame of Action... + + # If action.anim is of type Animation{MorphFunction} + # make it of type Animation{Vector{JPath}},... + if action.anim isa Animation{MorphFunction} + keyframes = Keyframe{Vector{JPath}}[] + for kf in action.anim.frames + push!(keyframes, Keyframe(kf.t, getjpaths(kf.value.func, kf.value.args))) + end + action.anim = Animation(keyframes, action.anim.easings) + end + + # If action.anim is of type Animation{Object} + # make it of type Animation{Vector{JPath}},... + if action.anim isa Animation{Object} + keyframes = Keyframe{Vector{JPath}}[] + for kf in action.anim.frames + isempty(kf.value.jpaths) && getjpaths!( + video, + object, + rel_frame, + kf.value, + kf.value.opts[:original_func], + ) + push!(keyframes, Keyframe(kf.t, kf.value.jpaths)) + end + action.anim = Animation(keyframes, action.anim.easings) + end + + # Make all the jpaths at every keyframe of the same length by + # appending necesarry amount of null_jpaths + long_jpaths_len = max([length(kf.value) for kf in action.anim.frames]...) + for kf in action.anim.frames + newval = vcat( + deepcopy(kf.value), + repeat([null_jpath(3)], long_jpaths_len - length(kf.value)), #null_jpath(samples=3) is fine, it will be resampled after this + ) + empty!(kf.value) + append!(kf.value, newval) + end + + # Resample all the polys inside all the jpath. polymorph_noresample which is + # for interpolation , unlike luxors polymorph does not resample polys and expects + # them to be of the same number of points + + for kf in action.anim.frames + for jpath in kf.value #kf.value is an array of jpaths + for i in 1:length(jpath.polys) + jpath.polys[i] = [ + jpath.polys[i][1] + polysample(jpath.polys[i], samples, closed = jpath.closed[i]) + ] + end + end + end + # All that is to be done in the first frame of the Action has been done + end + + interp_jpaths = get_interpolation(action, rel_frame) + object.func = (args...) -> begin + drawjpaths(interp_jpaths) + global DISABLE_LUXOR_DRAW[] = true + ret = object.opts[:original_func](args...) + global DISABLE_LUXOR_DRAW[] = false + newpath() + ret + end + if frame == action.frames.frames[end] + #make the objects jpaths the last objects (of the Animation) jpath + empty!(object.jpaths) + append!(object.jpaths, interp_jpaths) + end + # TODO if keep is true..then at rel_frame end + # replace obj.jpaths with interp_jpaths + # this allows it to be morphed again later + # if keep is false replace object.func and object.jpaths + # with original +end diff --git a/src/luxor_overrides.jl b/src/luxor_overrides.jl index 85a42f7fb..6ef07acf2 100644 --- a/src/luxor_overrides.jl +++ b/src/luxor_overrides.jl @@ -1,3 +1,11 @@ +""" + DISABLE_LUXOR_DRAW :: Ref{Bool} + +If true disables any drawing to the canvas by luxor , irresepective of what luxor functions are called +and what actions are passed to them (:stroke/:fill/:strokepath/:fillpath) . +""" +const DISABLE_LUXOR_DRAW = Ref(false) + """ setline(linewidth) @@ -280,3 +288,256 @@ function background(background_color) end Luxor.background(background_color) end + +""" + pathtopoly(::Val{:costate}) + +Method similar to Luxors `pathtopoly()`. Converts the current path to an array of polygons +and returns them. This function also returns an array of Bool (`co_states::Array{Bool}`) of exactly the same length as number of polygons that are being returned . +`co_states[i]` is `true/false` means `polygonlist[i]` is a closed/open polygon respectively. + +Another minor change from luxors `pathtopoly()` is when a CAIRO_PATH_MOVE_TO is encountered , a new poly is started. + +Returns Tuple(Array{Point},Array{Bool}) +""" +function Luxor.pathtopoly(::Val{:costate}) + originalpath = getpathflat() + polygonlist = Array{Point,1}[] + sizehint!(polygonlist, length(originalpath)) + co_states = Bool[] + sizehint!(co_states, length(polygonlist)) + if length(originalpath) > 0 + pointslist = Point[] + for e in originalpath + if e.element_type == Luxor.Cairo.CAIRO_PATH_MOVE_TO # 0 + if !isempty(pointslist) + #if poinstlist is not empty and we come across a move + #we flush and create a new subpath + if (last(pointslist) == first(pointslist)) && length(pointslist) > 2 + #but first lets check if what we flush is closed or open. + push!(co_states, true) + pop!(pointslist) + else + push!(co_states, false) + end + push!(polygonlist, pointslist) + pointslist = Point[] + end + push!(pointslist, Point(first(e.points), last(e.points))) + elseif e.element_type == Luxor.Cairo.CAIRO_PATH_LINE_TO # 1 + push!(pointslist, Point(first(e.points), last(e.points))) + elseif e.element_type == Luxor.Cairo.CAIRO_PATH_CLOSE_PATH # 3 + push!(co_states, true) + if last(pointslist) == first(pointslist) + # don’t repeat first point, we can close it ourselves + if length(pointslist) > 2 + pop!(pointslist) + end + end + if length(pointslist) == 2 + insert!(pointslist, 2, sum(pointslist) / 2)#insert midpoint if only 2 points are there + end + push!(polygonlist, pointslist) + pointslist = Point[] + else + error("pathtopoly(): unknown CairoPathEntry " * repr(e.element_type)) + error("pathtopoly(): unknown CairoPathEntry " * repr(e.points)) + end + end + # the path was never closed, so flush + if length(pointslist) > 1 #dont flush paths if only 1 point remains + if length(pointslist) == 2 + insert!(pointslist, 2, sum(pointslist) / 2)#insert midpoint if only 2 points are there + end + if (last(pointslist) == first(pointslist)) && length(pointslist) > 2 + #but first lets check if what we flush is closed or open. + push!(co_states, true) + pop!(pointslist) + else + push!(co_states, false) + end + push!(polygonlist, pointslist) + end + end + #"""check if everything went well""" + @assert length(polygonlist) == length(co_states) + #"""return polygonlist, and its closed/open state""" + return polygonlist, co_states +end + + +""" + _betweenpoly_noresample(loop1,loop2,k; easingfunction = easingflat) + +Just like _betweenpoly from Luxor , but expects polygons `loop1` and `loop2` to be of same size , and +therefore does not resample them to be of same size. + +From Luxor Docs: +Find a simple polygon between the two simple polygons loop1 and loop2 corresponding to k, + where 0.0 < k < 1.0. + +Arguments +loop1: first polygon +loop2: second polygon +k: interpolation factor +offset: a Tuple , first element is `:former` or `:latter` , second element is an Int. + decides if loop1/loop2 is offset and by how much for matching points from loop1 to loop2 + note that its not strictly an offset , it ranges from 1->N (no of points) and an offset + of 1 means no offset , offset of 2 means offset by 1 and so on .. TODO change this name + to startidx +""" +function _betweenpoly_noresample( + loop1, + loop2, + k, + offset = (:former, 1); + easingfunction = easingflat, +) + @assert length(loop1) == length(loop2) + result = Point[] + eased_k = easingfunction(k, 0.0, 1.0, 1.0) + for j in 1:length(loop1) + indj = mod1(j + offset[2] - 1, length(loop1)) + if offset[1] == :former + push!(result, between(loop1[indj], loop2[j], eased_k)) + else + push!(result, between(loop1[j], loop2[indj], eased_k)) + end + end + return result +end +#this is lifted from luxor, we should ask cormullion if its okay +""" + + polymorph_noresample( + pgon1::Array{Array{Point,1}}, + pgon2::Array{Array{Point,1}}, + k; + offsets::Array{Tuple{Symbol,Int}}; + easingfunction = easingflat, + kludge = true, + ) + +like luxors `polymorph` , but does not resample the polygon , therefore every +polygon in `pgon1` and `pgon2` should have the same number of points. used by +`_morph_jpath`. + +Also takes in an additional argument an array of `offset` . +Check `_between_poly` for more detail on `offset` + +From Luxor Docs: + +"morph" is to gradually change from one thing to another. This function changes one polygon + into another. + + It returns an array of polygons, [p_1, p_2, p_3, ... ], where each polygon p_n is the + intermediate shape between the corresponding shape in pgon1[1...n] and pgon2[1...n] at k, + where 0.0 < k < 1.0. If k ≈ 0.0, the pgon1[1...n] is returned, and if `k ≈ 1.0, + pgon2[1...n] is returned. + + pgon1 and pgon2 can be either simple polygons or arrays of one or more polygonal shapes (eg + as created by pathtopoly()). For example, pgon1 might consist of two polygonal shapes, a + square and a triangular shaped hole inside; pgon2 might be a triangular shape with a square + hole. + + It makes sense for both arguments to have the same number of polygonal shapes. If one has + more than another, some shapes would be lost when it morphs. But the suggestively-named + kludge keyword argument, when set to (the default) true, tries to compensate for this. + + By default, easingfunction = easingflat, so the intermediate steps are linear. If you use + another easing function, intermediate steps are determined by the value of the easing + function at k. + +""" +function polymorph_noresample( + pgon1::Array{Array{Point,1}}, + pgon2::Array{Array{Point,1}}, + k, + offsets::Array{Tuple{Symbol,Int}}; + easingfunction = easingflat, + kludge = true, +) + isapprox(k, 0.0) && return pgon1 + isapprox(k, 1.0) && return pgon2 + loopcount1 = length(pgon1) + loopcount2 = length(pgon2) + result = Array{Point,1}[] + centroid1 = centroid2 = O # kludge-y eh? + for i in 1:max(loopcount1, loopcount2) + from_ok = to_ok = false + not_empty1 = i <= loopcount1 + not_empty2 = i <= loopcount2 + if (not_empty1 && length(pgon1[i]) >= 3) + from_ok = true + end + if (not_empty2 && length(pgon2[i]) >= 3) + to_ok = true + end + if from_ok && to_ok + # a simple morph should suffice + push!( + result, + _betweenpoly_noresample( + pgon1[i], + pgon2[i], + k, + offsets[i], + easingfunction = easingfunction, + ), + ) + centroid1 = polycentroid(pgon1[i]) + centroid2 = polycentroid(pgon2[i]) + elseif from_ok && !to_ok && kludge + # nothing to morph to, so make something up + pdir = !ispolyclockwise(pgon1[i]) + loop2 = + ngon(centroid2, 0.1, reversepath = pdir, length(pgon1[i]), vertices = true) + push!( + result, + _betweenpoly_noresample( + pgon1[i], + loop2, + k, + offsets[i], + easingfunction = easingfunction, + ), + ) + centroid1 = polycentroid(pgon1[i]) + elseif !from_ok && to_ok && kludge + # nothing to morph from, so make something up + pdir = !ispolyclockwise(pgon2[i]) + loop1 = + ngon(centroid1, 0.1, reversepath = pdir, length(pgon2[i]), vertices = true) + push!( + result, + _betweenpoly_noresample( + loop1, + pgon2[i], + k, + offsets[i], + easingfunction = easingfunction, + ), + ) + centroid2 = polycentroid(pgon2[i]) + end + end + return result +end + + +for funcname in [:strokepath, :strokepreserve, :fillpath, :fillpreserve] + expr = quote + function Luxor.$funcname(::JavisLuxorDispatcher) + if CURRENT_FETCHPATH_STATE[] + occursin("stroke", string($funcname)) ? update_currentjpath(:stroke) : + update_currentjpath(:fill) + end + if !DISABLE_LUXOR_DRAW[] + $funcname(Luxor.DefaultLuxor()) + elseif !occursin("preserve", string($funcname)) + newpath() + end + end + end + eval(expr) +end diff --git a/src/luxor_utils.jl b/src/luxor_utils.jl new file mode 100644 index 000000000..d9b0f4eb2 --- /dev/null +++ b/src/luxor_utils.jl @@ -0,0 +1,58 @@ +""" + apply_transform(transform::Vector{Float64} , poly::Vector{Point}) + +applies the transform , got by getmatrix() on every point in the poly +and returns a new poly. + +move this to luxor_overrides_util.jl later. +""" +function apply_transform(transform::Vector{Float64}, poly::Vector{Point}) + retpoly = Point[] + for pt in poly + res = cairotojuliamatrix(transform) * [pt[1], pt[2], 1] + push!(retpoly, Point(res[1], res[2])) + end + retpoly +end + +""" + update_currentjpath(action::Symbol) + +Updates the CURRENT_JPATHS +This function is used inside the strokepath/strokepreserve/fillpath/fillpreserve. +Converts the current Path and other drawing states into a JPath and appends to the CURRENT_JPATHS global. + +the argument is a symbol either `:stroke` or `:fill` , to change behaviour +for stroke vs fill. +""" +function update_currentjpath(action::Symbol) + #println("test strokepaths") + cur_polys, cur_costates = pathtopoly(Val(:costate))#this pathtopoly is defined in javis + @assert length(cur_polys) == length(cur_costates) + transform = getmatrix() + cur_polys = [apply_transform(transform, poly) for poly in cur_polys] + #cur_polys is of 2 element Tuple + #containg 2 arrays 1 with Polygons and one with the bools + r = Luxor.get_current_redvalue() + g = Luxor.get_current_greenvalue() + b = Luxor.get_current_bluevalue() + a = Luxor.get_current_alpha() + #by default they are black transparent + fillstroke = Dict(:fill => [0.0, 0, 0, 0], :stroke => [0.0, 0, 0, 0]) + fillstroke[action] .= [r, g, b, a] + #if polys didnt change we just modify the last JPath in the CURRENT_JPATHS + if length(CURRENT_JPATHS) > 0 && cur_polys == CURRENT_JPATHS[end].polys + setproperty!(CURRENT_JPATHS[end], action, fillstroke[action]) + setproperty!(CURRENT_JPATHS[end], :lastaction, action) + else + currpath = JPath( + cur_polys, + cur_costates, + fillstroke[:fill], + fillstroke[:stroke], + action, + 2, + ) + push!(CURRENT_JPATHS, currpath) + end +end diff --git a/src/morphs.jl b/src/morphs.jl index 92e5fd890..dab78cdc4 100644 --- a/src/morphs.jl +++ b/src/morphs.jl @@ -1,52 +1,331 @@ include("Shape.jl") +#old morph func +#function morph_to(to_func::Function; do_action = :stroke) +# return (video, object, action, frame) -> +# _morph_to(video, object, action, frame, to_func; do_action = do_action) +#end +# """ - morph_to(to_func::Function; object=:stroke) + morph_to(to_func::Function; samples=100) A closure for the [`_morph_to`](@ref) function. -This makes it easier to write the function inside an `Object`. +To be used with Action. `morph_to` will morph an object into whatever is drawn +by the `to_func` passed to it. -Currently morphing is quite simple and only works for basic shapes. -It especially does not work with functions which produce more than one polygon -or which produce filled polygons. -Blending between fills of polygons is definitely coming at a later stage. - -**Important:** The functions itself should not draw the polygon -i.e. use `circle(Point(100,100), 50)` instead of `circle(Point(100,100), 50, :stroke)` # Arguments -- `to_func::Function`: Same as `from_func` but it defines the "result" polygon, - which will be displayed at the end of the Object - +- `to_func::Function`: Function that defines what the object should be morphed into + # Keywords -- `do_action::Symbol` defines whether the object has a fill or just a stroke. Defaults to `:stroke`. +- `samples` : Number of points to resample every polygon to for the morphing + +# Limitations +- cant handle clips inside `to_func` or the `object` +- sethue animation doesnt work with this , since the color's to be morphed into are derived from the `object` and `to_func`. to change hue while morphing , change it in the `to_func` # Example This creates a star that morphs into a circle and back. ```julia -astar(args...; do_action=:stroke) = star(O, 50, 5, 0.5, 0, do_action) -acirc(args...; do_action=:stroke) = circle(Point(100,100), 50, do_action) +astar() = star(O, 50, 5, 0.5, 0) +acirc() = circle(O, 50) video = Video(500, 500) back = Background(1:20, ground) -star_obj = Object(1:10, astar) +star_obj = Object(1:10,(args...)-> astar()) +act!(star_obj, Action(linear(), morph_to(acirc))) +act!(star_obj, Action(11:20, morph_to(astar))) + + morph_to(to_obj::Object; samples=100) + +Morphs one object into another object. + +# Arguments +- `to_obj::Object`: Object that defines what the object should be morphed into + +# Keywords +- `samples` : Number of points to resample every polygon to for the morphing + +# Limitations +- cant handle clips inside `to_func` or the `object` +- sethue animation doesnt work with this , since the color's to be morphed into are derived from the `object` and `to_func`. to change hue while morphing , change it in the `to_func` + +# Example + +This creates a star that morphs into a circle. + +```julia +astar() = star(O, 50, 5, 0.5, 0) +acirc() = circle(O, 50) + +video = Video(500, 500) +back = Background(1:20, ground) +star_obj = Object(1:10,(args...)-> astar()) +circ_obj = Object(1:10,(args...)-> acirc()) act!(star_obj, Action(linear(), morph_to(acirc))) -circle_obj = Object(11:20, acirc) -act!(circle_obj, Action(:same, morph_to(astar))) ``` """ -function morph_to(to_func::Function; do_action = :stroke) - return (video, object, action, frame) -> - _morph_to(video, object, action, frame, to_func; do_action = do_action) +function morph_to(to_obj::Object; samples = 100) + return (video, object, action, frame) -> begin + action.keep = false + # We dont want `keep=true`. The "persistance" of this action + # - after its frames is effected by changing the drawing function. + # + # `keep=true` continues to apply the action after the action frames are + # over in the render loop. The consequence of this is that when an + # object has two morphs applied at different parts of the timeline , + # one can potentialy interfere with the other, if the morphs are not + # called in the right order. + # + # TODO For now the implementation does not allow to revert the morph at + # the end of the action. maybe implement this by checking action.keep + # before the action starts and setting another flag . + _morph_to(video, object, action, frame, to_obj, samples) + end +end + +function morph_to(to_func::Function, args = []; samples = 100) + return (video, object, action, frame) -> begin + action.keep = false + _morph_to(video, object, action, frame, to_func, args, samples) + end +end + +""" + a very small jpath to appear from/disappear into + has 1 poly of `samples` points in the shape of an ngon + black fill 0 alpha, black stroke opaque + linewidth 2 + """ +null_jpath(samples = 100) = JPath( + [ngon(O, 0.1, samples, vertices = true)], + [true], + [0, 0, 0, 0], + [0, 0, 0, 0], + :stroke, + 2, +) + +""" + _morph_to( video::Video, object::Object, action::Action, frame, to_obj::Object, samples,) + +Internal function used to morph one `object` to another. +""" +function _morph_to( + video::Video, + object::Object, + action::Action, + frame, + to_obj::Object, + samples, +) + interp_jpaths = JPath[] + #resample all polys to same number of points at first frame of the action... + if frame == action.frames.frames[begin] + isempty(object.jpaths) && + getjpaths!(video, object, frame, object.opts[:original_func]) + isempty(to_obj.jpaths) && getjpaths!(video, to_obj, frame, to_obj.func) + + for obj in [object, to_obj] + for jpath in obj.jpaths #kf.value is an array of jpaths + for i in 1:length(jpath.polys) + jpath.polys[i] = [ + jpath.polys[i][1] + polysample(jpath.polys[i], samples - 1, closed = jpath.closed[i]) + ] + end + end + end + end + + #need to handle different number of jpaths + #for to_obj less jpaths , we can shrink the extras down + #for to_obj having more jpaths , we need to create extra polys + #the jpath it vanishes into has 1 poly with 3 points very close around the objects start_pos. ideally should have been 3 same points but Luxor doesnt like polys with 3 same points on top of each other, + l1 = length(object.jpaths) + l2 = length(to_obj.jpaths) + jpaths1 = vcat(object.jpaths, repeat([null_jpath(samples)], max(0, l2 - l1))) + jpaths2 = vcat(to_obj.jpaths, repeat([null_jpath(samples)], max(0, l1 - l2))) + #above lines should make jpaths1 and jpaths2 have the same no of jpaths + for (jpath1, jpath2) in zip(jpaths1, jpaths2) + offsets = get_offsets(jpath1, jpath2) + push!( + interp_jpaths, + _morph_jpath(jpath1, jpath2, get_interpolation(action, frame), offsets), + ) + end + + object.func = (args...) -> begin + drawjpaths(interp_jpaths) + global DISABLE_LUXOR_DRAW[] = true + ret = object.opts[:original_func](args...) + global DISABLE_LUXOR_DRAW[] = false + newpath() + ret + end + + if frame == last(get_frames(action)) + object.jpaths = to_obj.jpaths + end +end + +const offset_cache = Dict{Tuple{JPath,JPath},Vector{Tuple{Symbol,Int64}}}() +""" + get_offsets(jpath1,jpath2) + +returns an Array of Tuples , each Tuple is of the form `(s::Symbol,offsetvalue::Int)` + +while interpolating polys inside the jpath. Javis tries to find a good offsetvalue +if poly1 is being morphed into poly2 +`poly1[i]` goes to `poly2[i + offsetvalue -1]` (modulo length(poly2) addition). + +`s` is either `:former` or `:latter` indicating if the offset should be applied on poly1 or poly2 + +morphing from closed to closed offsets the former. +morphing from closed to open poly offsets the former. +morphing from open to closed poly offsets the latter. +morphing from open to open poly does no offsetting. + +`offset` of 1 means no offset . It should technically be called best starting +indes. +""" +function get_offsets(jpath1, jpath2) + #calculate offset + minl = min(length(jpath1.polys), length(jpath2.polys)) + maxl = max(length(jpath1.polys), length(jpath2.polys)) + offsets = Array{Tuple{Symbol,Int}}([]) + if haskey(offset_cache, (jpath1, jpath2)) + return offset_cache[(jpath1, jpath2)] + else + for i in 1:minl + if i <= length(jpath1.closed) && jpath1.closed[i] + offset, _ = compute_shortest_morphing_dist(jpath1.polys[i], jpath2.polys[i]) + push!(offsets, (:former, offset)) + elseif i <= length(jpath2.closed) && jpath2.closed[i] + offset, _ = compute_shortest_morphing_dist(jpath2.polys[i], jpath1.polys[i]) + push!(offsets, (:latter, offset)) + else + push!(offsets, (:former, 1)) + end + end + offset_cache[(jpath1, jpath2)] = offsets + for i in (minl + 1):maxl + push!(offsets, (:former, 1)) + end + return offsets + end +end + + +""" + _morph_to(video::Video, object::Object, action::Action, frame, to_func::Function, args::Array, samples=100) + +Internal version of [`morph_to`](@ref) but described there. +""" +function _morph_to( + video::Video, + object::Object, + action::Action, + frame, + to_func::Function, + args::Array, + samples = 100, +) + #total number of points is samples+1 + + interp_jpaths = JPath[] + # If first frame .... + if frame == first(get_frames(action)) + # Get jpaths to morph from and into + isempty(object.jpaths) && getjpaths!(video, object, frame, object.func) + action.defs[:toJPaths] = getjpaths(to_func, args) + # Resample all polys in all jpaths to `samples` number of points + for jpath in [object.jpaths..., action.defs[:toJPaths]...] + for i in 1:length(jpath.polys) + jpath.polys[i] = [ + jpath.polys[i][1] + polysample(jpath.polys[i], samples - 1, closed = jpath.closed[i]) + ] #prepend the first point becauce polysample doesnt + end + end + end + jpaths1 = object.jpaths + jpaths2 = action.defs[:toJPaths] + l1 = length(jpaths1) + l2 = length(jpaths2) + # Make jpaths have the same number + jpaths1 = vcat(jpaths1, repeat([null_jpath(samples)], max(0, l2 - l1))) + jpaths2 = vcat(jpaths2, repeat([null_jpath(samples)], max(0, l1 - l2))) + + # Interpolate jpaths pairwise and store interp_jpaths + for (jpath1, jpath2) in zip(jpaths1, jpaths2) + #calculate offset + offsets = get_offsets(jpath1, jpath2) + push!( + interp_jpaths, + _morph_jpath(jpath1, jpath2, get_interpolation(action, frame), offsets), + ) + end + + # Change drawing function + object.func = (args...) -> begin + drawjpaths(interp_jpaths) + global DISABLE_LUXOR_DRAW[] = true + ret = object.opts[:original_func](args...) + global DISABLE_LUXOR_DRAW[] = false + newpath() + ret + end + if frame == action.frames.frames[end] + object.jpaths = jpaths2 + #object.func = object.opts[:original_func] + end end +""" + _morph_jpath(jpath1::JPath, jpath2::JPath, k) + +Returns an interpolated jpath between jpath1 and jpath2 with interpolation factor 0<`k`<1. +""" +function _morph_jpath(jpath1::JPath, jpath2::JPath, k, offsets) + polys1 = jpath1.polys + polys2 = jpath2.polys + retpolys = polymorph_noresample(polys1, polys2, k, offsets, kludge = true) + # The logic to figure out if intermediate poly should be closed or open. + # From To During Morph (intermediate) + # -------------------------------------------------- + # open -> open : remain open during morph + # closed -> closed : remain closed during morph + # closed -> open : open during morph + # open -> closed : open during morph , but closed when k ≈ 1 + retclosed = ones(Bool, length(retpolys)) + jp2closed_paded = + [jpath2.closed; ones(Bool, max(0, diff(length.([jpath2.closed, retpolys]))[1]))] + jp1closed_paded = + [jpath1.closed; ones(Bool, max(0, diff(length.([jpath1.closed, retpolys]))[1]))] + + for i in 1:length(retclosed) + if jp2closed_paded[i] && !jp1closed_paded[i] + # Intermediates are open , but at k=1 they close + retclosed[i] = isapprox(k, 1) ? true : false + else + retclosed[i] = jp2closed_paded[i] + end + end + retfill = k .* jpath2.fill + (1 - k) .* jpath1.fill + retstroke = k .* jpath2.stroke + (1 - k) .* jpath1.stroke + retlinewidth = k .* jpath2.linewidth + (1 - k) .* jpath1.linewidth + @assert length(retpolys) == length(retclosed) + JPath(retpolys, retclosed, retfill, retstroke, jpath1.lastaction, retlinewidth) +end """ _morph_to(video::Video, object::Object, action::Action, frame, to_func::Function; do_action=:stroke) Internal version of [`morph_to`](@ref) but described there. +older morph. """ function _morph_to( video::Video, @@ -452,3 +731,77 @@ function reorder_match(from_shapes::Vector{Shape}, to_shapes::Vector{Shape}) end return new_from_shapes, new_to_shapes end + + +""" +bunch of methods extending functions in Animations.jl that make morphs possible. +need to find a better place to put these functions, +this will do for now +""" + +import Base +""" + struct MorphFunction + func::Function + args::Array + jpaths::Vector{JPath} + + +# Fields + - func::Function : a function with luxor calls to draw something that objects will be morphed into + - args : args to the function . Object will be morphed into what is drawn by calling `func(args...)` + - jpaths : The jpaths returned by what is drawn. `JPath[]` by default, this is populated the first instance it encounters a morph/partial draw at render time. + +TODO: find a better place(file) to put these functions and structs. +""" +mutable struct MorphFunction + func::Function + args::Array + jpaths::Vector{JPath} +end + +MorphFunction(f::Function, args::Array) = MorphFunction(f, args, JPath[]) +Base.convert(::Type{MorphFunction}, f::Function) = MorphFunction(f, [], JPath[]) +Base.convert(::Type{MorphFunction}, t::Tuple{Function,Array}) = + MorphFunction(t[1], t[2], JPath[]) +Base.convert(::Type{MorphFunction}, t::Tuple) = MorphFunction(t[1], [t[2:end]...]) + +""" + Animation(timestamps,funcs,easings) + +returns an `Animation` from an array of MorphFunctions. +""" +function Animations.Animation( + timestamps::AbstractVector{<:Real}, + funcs::AbstractVector{MorphFunction}, + easings::AbstractVector{<:Easing}, +) + keyframes = Keyframe{MorphFunction}.(timestamps, funcs) + Animation(keyframes, easings) +end + +""" + Animations.linear_interpolate( + fraction::Real, + jpaths1::Vector{JPath}, + jpaths2::Vector{Jpath} + ) + + A method so that Animations.jl can interpolate between Arrays of JPaths. + Note that the array of jpaths should be of the same size. +""" +function Animations.linear_interpolate( + fraction::Real, + jpaths1::Vector{JPath}, + jpaths2::Vector{JPath}, +) + l1 = length(jpaths1) + l2 = length(jpaths2) + @assert l1 == l2 + interp_jpaths = JPath[] + for (jpath1, jpath2) in zip(jpaths1, jpaths2) + offsets = get_offsets(jpath1, jpath2) + push!(interp_jpaths, _morph_jpath(jpath1, jpath2, fraction, offsets)) + end + return interp_jpaths +end diff --git a/src/partial_draw.jl b/src/partial_draw.jl new file mode 100644 index 000000000..7e3d33345 --- /dev/null +++ b/src/partial_draw.jl @@ -0,0 +1,162 @@ +#code to enable partially drawing an object +#and animate its creation . +# +#Move these function to appropriate files later +# +""" + showcreation() + +Returns a function to be used with Action. Shows the creation of the object +incrementally. + +Ex. + +``` +Background(1:nframes+60,(args...)->ground()) +boxobj = Object(1:nframes+60 , (args...) -> boxdraw("green") ) +action_create = Action(1:nframes÷2,sineio(),showcreation()) +act!(boxobj,action_create) +``` + +""" +function showcreation() + return (video, object, action, rel_frame) -> begin + action.keep = false + # We almost certainly never want `keep=true` for showcreation. In + # general actions which change the drawing function shouldn't have + # `keep=true`. This is because in the render loop `keep=true` will + # execute the action.func on every frame after the actions frames are + # over. This messes up animations. For example if the object morphs + # after its "showcreated" on the timeline but in the script the morph + # is called before showcreation. what happens is morph gets applied and + # changes drawing function,then showcreation gets appplied reverts back + # the drawing function. + fraction = get_interpolation(action, rel_frame) + _drawpartial(video, action, object, rel_frame, fraction) + if rel_frame == last(get_frames(action)) + object.func = object.opts[:original_func] + end + end +end + +""" + showdestruction() + +Similar to [`showcreation`](@ref) but un-draws the object. +""" +function showdestruction() + return (video, object, action, rel_frame) -> begin + fraction = get_interpolation(action, rel_frame) + _drawpartial(video, action, object, rel_frame, 1.0 - fraction) + if rel_frame == last(get_frames(action)) + object.func = (args...) -> nothing + end + end +end + +""" + drawpartial(fraction::Real) + +Returns a function to be used with Action , draws the object upto a `fraction` of +itself. +""" +function drawpartial(fraction::Real) + return (video, object, action, rel_frame) -> + _drawpartial(video, action, object, rel_frame, fraction::Real) +end + +""" + _drawpartial(video, action, object, rel_frame, fraction::Real) + +Internal function that is used with [`showcreation`](@ref) [`showdestruction`](@ref) [`drawpartial`](@ref). +makes an array of `JPaths` called `partialjpaths` that represents a partial drawing of the object. Then it replaces the +`object.func` with `drawjpaths(partialjpaths)` +""" +function _drawpartial(video, action, object, rel_frame, fraction::Real) + isempty(object.jpaths) && + getjpaths!(video, object, rel_frame, object.opts[:original_func]) + rel_frame == first(get_frames(action)) && jpath_polylengths!.(object.jpaths) + partialjpaths = getpartialjpaths(object, fraction) + object.func = (args...) -> begin + drawjpaths(partialjpaths) + global DISABLE_LUXOR_DRAW[] = true + object.opts[:original_func](args...) + global DISABLE_LUXOR_DRAW[] = false + end + #isapprox(fraction, 1) && (object.func = object.opts[:original_func]) +end + +""" + len_jpath(jpath::JPaht) + +returns the sum of all the lengths of the polys that this `jpath` contains +""" +len_jpath(jpath::JPath) = sum( + polydistances(jpath.polys[i], closed = jpath.closed[i])[end] for + i in 1:length(jpath.polys) +) + +""" + getpartialjpaths(object,fraction) + +returns an array of jpaths , that if drawn will look like partially drawn `object` +partially drawn upto `fraction` +""" +function getpartialjpaths(object, fraction) + # there are 3 layers to this + # first we need to figure out which JPath the fraction is in + # then we need to figure out which poly of that JPath it is in + # then we need to figure out how much of that poly to draw, (use polyportion on that poly with an appropriate fraction) + # naming variables is a @#*!$ :( + + ret_jpaths = JPath[] + lengths_of_jpaths = len_jpath.(object.jpaths) + #fraction cumulative sum of lengths of jpaths + frac_cs_len_jp = (cumsum(lengths_of_jpaths) ./ sum(lengths_of_jpaths)) + jp_idx = 1 #index at which the fraction lies + for i in 1:length(object.jpaths) + jp_idx = i + if frac_cs_len_jp[i] > fraction + break + end + end + #we got which jpath , + + append!(ret_jpaths, object.jpaths[1:(jp_idx - 1)]) + #now which poly in this jpath ?... + fin_jpath = deepcopy(object.jpaths[jp_idx]) #the jpath we determined the fraction is in. + #this is a deepcopy because we will change this jpath's polys appropriatly and append to ret_jpaths + + #cumsum of final jpath poly distances + cs_fin_jpd = cumsum([ + polydistances(poly, closed = close)[end] for + (poly, close) in zip(fin_jpath.polys, fin_jpath.closed) + ]) + cs_fin_jpd = cs_fin_jpd ./ cs_fin_jpd[end] + #fraction in this final jpath that we should stop at. + prevfrac = jp_idx == 1 ? 0 : frac_cs_len_jp[jp_idx - 1] + currfrac = frac_cs_len_jp[jp_idx] + fraction_jpath = (fraction - prevfrac) / (currfrac - prevfrac) + p_idx = 1 #index for poly in fin_jpath + for i in 1:length(fin_jpath.polys) + p_idx = i + if cs_fin_jpd[i] > fraction_jpath + break + end + end + prevfrac = p_idx == 1 ? 0 : cs_fin_jpd[p_idx - 1] + currfrac = cs_fin_jpd[p_idx] + fraction_poly = (fraction_jpath - prevfrac) / (currfrac - prevfrac) + + fin_jpath.polys = [ + fin_jpath.polys[1:(p_idx - 1)]..., + polyportion( + fin_jpath.polys[p_idx], + fraction_poly, + closed = fin_jpath.closed[p_idx], + ), + ] + fin_jpath.closed = [fin_jpath.closed[1:(p_idx - 1)]..., false] + push!(ret_jpaths, fin_jpath) + return ret_jpaths +end diff --git a/src/structs/JPath.jl b/src/structs/JPath.jl new file mode 100644 index 000000000..858850fd5 --- /dev/null +++ b/src/structs/JPath.jl @@ -0,0 +1,126 @@ +""" + JPath + +a polygon representation of a path, every Object will have a list of JPaths under the +field jpaths. +Object JPaths are calculated and updated using `getjpaths!`, +Every call to stroke/fill in the objects `obj.func` typically adds a JPath to the objects jpaths. +a JPath can be drawn using drawjpath(jpath). Usually if one were to draw out the `object.jpath` +it would result in the exact same picture/drawing as running `object.func`. +JPaths are typically used for morphing and drawing partially. + +Every JPath has the following fields. + +- `polys` a list of polygons that makes up the path +- `closed` a list of bools of same length as `polys`. closed[i] +states if polys[i] is a closed polygon or not +- `fill` a vector of 4 numbers , R, G ,B and A the color it +should be filled with. if the path was not filled its A is set to 0 +(along with R,G,B) this way its an "invisible" fill. +- `stroke a vector of 4 numbers just like `fill` for the stroke color. +- linewidth for stroke +- polylengths , length of every poly in `polys` , usually computed at rendertime +after polys are populated + +""" +mutable struct JPath + polys::Vector{Vector{Point}} + closed::Vector{Bool} + fill::Vector{Number} + stroke::Vector{Number} + lastaction::Symbol #last action was fill or stroke + linewidth::Number + polylengths::Union{Nothing,Vector{Real}} + #TODO transform and dashstyle + #maybe dont have to store the transform here, just apply the transform + #on the polys before storing it ?. This way things like bounding + #boxes of the object can be computed easily at compute time. +end + +JPath(polys, closed, fill, stroke, lastaction, linewidth) = + JPath(polys, closed, fill, stroke, lastaction, linewidth, nothing) + +""" +An array to accumulate the JPATHs as the obj.func is being executed to get the objects jpaths +""" +const CURRENT_JPATHS = JPath[] + +""" + CURRENT_FETCHPATH_STATE::Bool + +If true all drawing functions convert the current path to a JPath and append them to the CURRENT_JPATHS +(does not work on `text`). +""" +const CURRENT_FETCHPATH_STATE = Ref(false) + +""" + getjpaths(func::Function, args = []) + +getjpaths runs the function `func`. `func` is usually a function +with some calls to luxor functions inside to draw something onto a +canvas. Although `getjpaths` does call `func` it does not draw on the canvas. +getjpaths will return an array of JPaths that represent what +would be drawn by the `func`. Also see getjpath!(object,func) +""" +function getjpaths(func::Function, args = []) + m = getmatrix() + setmatrix([1.0, 0, 0, 1.0, 0, 0]) + newpath() + empty!(CURRENT_JPATHS) + global CURRENT_FETCHPATH_STATE[] = true + global DISABLE_LUXOR_DRAW[] = true + func(args...) + global CURRENT_FETCHPATH_STATE[] = false + global DISABLE_LUXOR_DRAW[] = false + newpath()#clear all the paths + retpaths = JPath[] + jpath_polylengths!.(CURRENT_JPATHS) + append!(retpaths, CURRENT_JPATHS) + empty!(CURRENT_JPATHS) + setmatrix(m) + return retpaths +end + +""" + jpath_polylengths!(jpath::JPath) + +updates the polylengths field in jpath with the lengths of the polys +""" +function jpath_polylengths!(jp::JPath) + jp.polylengths = + [polyperimeter(jp.polys[i], closed = jp.closed[i]) for i in 1:length(jp.polys)] +end + + +""" + drawjpaths(jpaths::Array{JPath}) + +draws out the jpaths onto the current canvas +""" +function drawjpaths(jpaths::Array{JPath}) + for jpath in jpaths + @assert length(jpath.polys) == length(jpath.closed) + for (polyi, co_state) in zip(jpath.polys, jpath.closed) + #place the polys + if length(polyi) > 1 + poly(polyi; action = :path, close = co_state) + end + end + if jpath.lastaction == :stroke + Luxor.setcolor(jpath.fill[1:3]...) + Luxor.setopacity(jpath.fill[4]) + Luxor.fillpreserve() + Luxor.setcolor(jpath.stroke[1:3]...) + Luxor.setopacity(jpath.stroke[4]) + Luxor.strokepath() + else + Luxor.setcolor(jpath.stroke[1:3]...) + Luxor.setopacity(jpath.stroke[4]) + Luxor.strokepreserve() + Luxor.setcolor(jpath.fill[1:3]...) + Luxor.setopacity(jpath.fill[4]) + Luxor.fillpath() + end + end + newpath() +end diff --git a/src/structs/Object.jl b/src/structs/Object.jl index 591e7c8f7..694b33899 100644 --- a/src/structs/Object.jl +++ b/src/structs/Object.jl @@ -23,8 +23,10 @@ mutable struct Object <: AbstractObject opts::Dict{Symbol,Any} change_keywords::Dict{Symbol,Any} result::Vector + jpaths::Vector{JPath} end + """ CURRENT_OBJECT @@ -33,7 +35,6 @@ The current object can be accessed using CURRENT_OBJECT[1] """ const CURRENT_OBJECT = Array{Object,1}() - const PREVIOUS_OBJECT = Array{Object,1}() const CURRENT_OBJECT_ACTION_TYPE = Array{Symbol,1}() @@ -85,6 +86,7 @@ function Object(frames, func::Function, start_pos::Union{Object,Point}; kwargs.. union(CURRENT_VIDEO[1].background_frames, frames) end + object = Object( frames, func, @@ -94,6 +96,7 @@ function Object(frames, func::Function, start_pos::Union{Object,Point}; kwargs.. opts, Dict{Symbol,Any}(), Any[nothing], + JPath[], ) # store the original object func @@ -110,6 +113,36 @@ function Object(frames, func::Function, start_pos::Union{Object,Point}; kwargs.. return object end +""" + getjpaths!(video::Video,obj::Object,frame::Int, func::Function , args=[]) + +This is called at rendertime . `getjpaths!` updates `obj.jpaths` with jpaths that +are generated by drawing `func` . Typically `func` is `obj.opts[:original_func]` +""" +function getjpaths!(v::Video, obj::Object, f::Int, func::Function, args = []) + m = getmatrix() + setmatrix([1.0, 0, 0, 1.0, 0, 0]) + empty!(CURRENT_JPATHS) + global CURRENT_FETCHPATH_STATE[] = true + global DISABLE_LUXOR_DRAW[] = true + func(v, obj, f, args...) #figure out a way to pass args of object.func here + global CURRENT_FETCHPATH_STATE[] = false + global DISABLE_LUXOR_DRAW[] = false + jpath_polylengths!.(CURRENT_JPATHS) + append!(obj.jpaths, CURRENT_JPATHS) + empty!(CURRENT_JPATHS) + setmatrix(m) +end + +""" + drawobj_jpaths(obj::Object) + +Draws JPaths of the object. (not typically used.) +""" +function drawobj_jpaths(obj::Object) + drawjpaths(obj.jpaths) +end + """ act! diff --git a/test/morphing.jl b/test/morphing.jl index 3f56ccd69..4fca6e92f 100644 --- a/test/morphing.jl +++ b/test/morphing.jl @@ -1,158 +1,76 @@ -astar(args...; do_action = :stroke) = star(Point(-100, -100), 30, 5, 0.5, 0, do_action) -acirc(args...; do_action = :stroke) = circle(Point(100, 100), 30, do_action) +#circfunc() = circle(Point(-100,-100),30,5,0.5,0,:stroke) +function bluecircfunc() + sethue("blue") + circle(O, 30, :fillpreserve) + sethue("black") + strokepath() +end -@testset "morphing star2circle and back" begin - video = Video(500, 500) +function starfunc(color) + sethue(color) + star(O, 30, 5, 0.5, 0, :stroke) +end - back = Background(1:20, (args...) -> ground_color("white", "black", args[3])) - Object(1:10, (args...) -> circle(Point(-100, 0), val(back), :fill)) - star_obj = Object(1:10, astar) - act!(star_obj, Action(linear(), morph_to(acirc))) - circle_obj = Object(11:20, acirc) - act!(circle_obj, Action(:same, morph_to(astar))) +function boxfunc(color) + sethue(color) + box(O - 50, 50, 50, :stroke) +end +@testset "Morphing star to circle and back , morph to function" begin + video = Video(500, 500) + back = Background(1:20, (args...) -> background("white")) + star_obj = Object(1:20, (args...) -> starfunc("red"), Point(-100, -100)) + act!(star_obj, Action(2:10, morph_to(bluecircfunc))) + act!(star_obj, Action(2:10, anim_translate(Point(200, 200)))) + act!(star_obj, Action(11:20, morph_to(starfunc, ["red"]))) + act!(star_obj, Action(11:20, anim_translate(Point(-200, -200)))) render(video; tempdirectory = "images", pathname = "") @test_reference "refs/star2circle5.png" load("images/0000000005.png") @test_reference "refs/star2circle15.png" load("images/0000000015.png") + for i in 1:20 rm("images/$(lpad(i, 10, "0")).png") end end -@testset "morphing star2circle and back with fill" begin +@testset "Morphing star to circle (morph to Object)" begin video = Video(500, 500) - back = Background(1:20, (args...) -> ground_color("white", "black", args[3])) - Object(1:10, (args...) -> circle(Point(-100, 0), val(back), :fill)) - star_obj = Object(1:10, astar) - act!(star_obj, Action(morph_to(acirc; do_action = :fill))) - - circle_obj = Object(11:20, acirc) - act!(circle_obj, Action(morph_to(astar; do_action = :fill))) + back = Background(1:20, (args...) -> background("white")) + circ_obj = Object(1:20, (args...) -> bluecircfunc()) + star_obj = Object(1:20, (args...) -> starfunc("red")) + act!(star_obj, Action(2:20, morph_to(circ_obj))) render(video; tempdirectory = "images", pathname = "") - @test_reference "refs/star2circle_fill5.png" load("images/0000000005.png") - @test_reference "refs/star2circle_fill15.png" load("images/0000000015.png") + @test_reference "refs/star2circle_obj5.png" load("images/0000000005.png") + @test_reference "refs/star2circle_obj15.png" load("images/0000000015.png") + for i in 1:20 rm("images/$(lpad(i, 10, "0")).png") end end -@testset "test default kwargs" begin +@testset "Morphing using keyframes" begin video = Video(500, 500) - back = Background(1:10, (args...) -> ground_color("white", "black", args[3])) - Object(1:10, (args...) -> circle(Point(-100, 0), val(back), :fill)) - star_obj = Object(1:10, astar) - act!(star_obj, Action(morph_to(acirc))) - - pathname = render(video) - - path, ext = splitext(pathname) - @test ext == ".gif" - @test isfile(pathname) - rm(pathname) -end - - -@testset "transposing a matrix" begin - function latex_ground(args...) - translate(-200, -100) - background("white") - sethue("black") - fontsize(50) - end - - function matrix(args...; do_transpose = false, do_action = :stroke) - fontsize(50) - str = - L"$\begin{equation}\left[\begin{array}{ccc}\alpha & \beta & \gamma \\x^{2} & \sqrt{y} & \lambda \\1 & 2 & y \\\end{array}\right]\end{equation}$" - if do_transpose - str = - L"$\begin{equation}\left[\begin{array}{ccc}\alpha & x^{2} & 1 \\\beta & \sqrt{y} & 2 \\\gamma & \lambda & y \\\end{array}\right]\end{equation}$" - end - latex(str, O, do_action) - end - - video = Video(600, 400) - Background(1:62, latex_ground) - m = Object(1:60, matrix) - act!( - m, - Action( - 31:60, - morph_to( - (args...; do_action = :fill) -> - matrix(; do_transpose = true, do_action = do_action); - do_action = :fill, - ), - ), - ) - Object(61:62, (args...) -> matrix(; do_transpose = true)) - render(video; tempdirectory = "images", pathname = "") - - @test_reference "refs/matrix_transpose1.png" load("images/0000000001.png") - @test_reference "refs/matrix_transpose50.png" load("images/0000000050.png") - @test_reference "refs/matrix_transpose55.png" load("images/0000000055.png") - @test_reference "refs/matrix_transpose62.png" load("images/0000000062.png") - for i in 1:62 - rm("images/$(lpad(i, 10, "0")).png") - end -end - -@testset "Matrix sequence" begin - function sequence(args...; second = false, do_action = :stroke) - fontsize(50) - - str = - L"$\begin{equation}\left[\begin{array}{cc}2 & -1\\-1 & 2 \\\end{array}\right]\end{equation}$" - if second - str = - L"$\begin{equation}\left[\begin{array}{ccc} 2 & -1 & 0 \\ -1 & 2 & -1 \\ 0 & -1 & 2 \\\end{array}\right]\end{equation}$" - end - if !second - latex(str, Point(-150, -60), do_action) - else - latex(str, Point(-200, -120), do_action) - end - end - - video = Video(600, 400) - Background(1:120, ground) - seq = Object(1:60, sequence) + back = Background(1:20, (args...) -> background("white")) + circ_obj = Object(1:20, (args...) -> bluecircfunc()) act!( - seq, + circ_obj, Action( - 31:60, - morph_to( - (args...; do_action = :stroke) -> - sequence(; second = true, do_action = do_action); - do_action = :fill, - ), - ), - ) - seq2 = Object( - 61:120, - (args...; do_action = :stroke) -> - sequence(; second = true, do_action = do_action), - ) - act!( - seq2, - Action( - 31:60, - morph_to( - (args...; do_action = :fill) -> sequence(; do_action = do_action); - do_action = :fill, + 2:20, + Animation( + [0, 0.3, 1], + MorphFunction[bluecircfunc, (boxfunc, ["red"]), (starfunc, "blue")], ), + morph(), ), ) render(video; tempdirectory = "images", pathname = "") + @test_reference "refs/star2circle_keyframe5.png" load("images/0000000005.png") + @test_reference "refs/star2circle_keyframe15.png" load("images/0000000015.png") - @test_reference "refs/matrix_sequence01.png" load("images/0000000001.png") - @test_reference "refs/matrix_sequence50.png" load("images/0000000050.png") - @test_reference "refs/matrix_sequence80.png" load("images/0000000080.png") - @test_reference "refs/matrix_sequence99.png" load("images/0000000099.png") - for i in 1:120 + for i in 1:20 rm("images/$(lpad(i, 10, "0")).png") end end @@ -163,26 +81,32 @@ end sethue("white") end - astar(args...; do_action = :stroke) = star(O, 50, 5, 0.5, 0, do_action) - abox(args...; do_action = :stroke) = rect(-50, -50, 100, 100, do_action) - acirc(args...; do_action = :stroke) = circle(Point(0, 0), 50, do_action) + #astar(args...; do_action = :stroke) = star(O, 50, 5, 0.5, 0, do_action) + #abox(args...; do_action = :stroke) = rect(-50, -50, 100, 100, do_action) + #acirc(args...; do_action = :stroke) = circle(Point(0, 0), 50, do_action) + function greenbox() + sethue("green") + rect(-50, -50, 100, 100, action = :fill) + end video = Video(500, 500) back = Background(1:200, ground) - star_obj = Object(1:200, abox) - act!(star_obj, Action(10:20, morph_to(acirc))) - act!(star_obj, Action(30:40, anim_translate(Point(100, -100)))) + start_obj = Object(1:200, (args...) -> greenbox()) + act!(start_obj, Action(10:20, morph_to(bluecircfunc))) + act!(start_obj, Action(30:40, anim_translate(Point(100, -100)))) + #TODO: the following comment is from the old test. + # ask Ole if new morph behaviour is working right. # this is also a bug with the morph_to function # the star is formed at the incorrect position # the the origin is shifted and neve restored back # you can see this in the result of anim_translate actions # TODO: fix this at some point! - act!(star_obj, Action(40:60, morph_to(astar))) - act!(star_obj, Action(70:90, anim_translate(Point(100, -100)))) - act!(star_obj, Action(100:120, morph_to(abox))) - act!(star_obj, Action(130:150, anim_translate(Point(-100, 50)))) - act!(star_obj, Action(160:180, morph_to(acirc))) + act!(start_obj, Action(40:60, morph_to(starfunc, ["red"]))) + act!(start_obj, Action(70:90, anim_translate(Point(100, -100)))) + act!(start_obj, Action(100:120, morph_to(greenbox))) + act!(start_obj, Action(130:150, anim_translate(Point(-100, 50)))) + act!(start_obj, Action(160:180, morph_to(bluecircfunc))) render(video; tempdirectory = "images", pathname = "") for i in [ diff --git a/test/partialdrawtest.jl b/test/partialdrawtest.jl new file mode 100644 index 000000000..49219cd67 --- /dev/null +++ b/test/partialdrawtest.jl @@ -0,0 +1,27 @@ +function draw_complex_shape() + circle(O + 20, 100, :stroke) + sethue("red") + box(O, 100, 120, :fillpreserve) + sethue("black") + strokepath() + sethue("green") + ngon(Point(-200, -200), 50; action = :fillstroke) +end + + +@testset "Testing Partial drawing of Object" begin + video = Video(500, 500) + back = Background(1:20, (args...) -> background("white")) + obj = Object(1:20, (args...) -> draw_complex_shape()) + act!(obj, Action(2:20, showcreation())) + render(video; tempdirectory = "images", pathname = "") + + @test_reference "refs/partialdraw5.png" load("images/0000000005.png") + @test_reference "refs/partialdraw10.png" load("images/0000000010.png") + @test_reference "refs/partialdraw15.png" load("images/0000000015.png") + @test_reference "refs/partialdraw18.png" load("images/0000000018.png") + + for i in 1:20 + rm("images/$(lpad(i, 10, "0")).png") + end +end diff --git a/test/refs/morph_mutate1.png b/test/refs/morph_mutate1.png index 354f85dad..e5b462eef 100644 Binary files a/test/refs/morph_mutate1.png and b/test/refs/morph_mutate1.png differ diff --git a/test/refs/morph_mutate10.png b/test/refs/morph_mutate10.png index 354f85dad..e5b462eef 100644 Binary files a/test/refs/morph_mutate10.png and b/test/refs/morph_mutate10.png differ diff --git a/test/refs/morph_mutate121.png b/test/refs/morph_mutate121.png index 5cdb7ed0c..2fc070102 100644 Binary files a/test/refs/morph_mutate121.png and b/test/refs/morph_mutate121.png differ diff --git a/test/refs/morph_mutate126.png b/test/refs/morph_mutate126.png index 5cdb7ed0c..2fc070102 100644 Binary files a/test/refs/morph_mutate126.png and b/test/refs/morph_mutate126.png differ diff --git a/test/refs/morph_mutate131.png b/test/refs/morph_mutate131.png index 05d05c377..e896a3826 100644 Binary files a/test/refs/morph_mutate131.png and b/test/refs/morph_mutate131.png differ diff --git a/test/refs/morph_mutate150.png b/test/refs/morph_mutate150.png index fe8a910d7..ac87c9ea1 100644 Binary files a/test/refs/morph_mutate150.png and b/test/refs/morph_mutate150.png differ diff --git a/test/refs/morph_mutate151.png b/test/refs/morph_mutate151.png index fe8a910d7..ac87c9ea1 100644 Binary files a/test/refs/morph_mutate151.png and b/test/refs/morph_mutate151.png differ diff --git a/test/refs/morph_mutate161.png b/test/refs/morph_mutate161.png index 6ec3453ec..56332ac25 100644 Binary files a/test/refs/morph_mutate161.png and b/test/refs/morph_mutate161.png differ diff --git a/test/refs/morph_mutate180.png b/test/refs/morph_mutate180.png index db2f8ed07..f7fdb0b0d 100644 Binary files a/test/refs/morph_mutate180.png and b/test/refs/morph_mutate180.png differ diff --git a/test/refs/morph_mutate190.png b/test/refs/morph_mutate190.png index db2f8ed07..f7fdb0b0d 100644 Binary files a/test/refs/morph_mutate190.png and b/test/refs/morph_mutate190.png differ diff --git a/test/refs/morph_mutate20.png b/test/refs/morph_mutate20.png index 89f461ec1..3d5b937ab 100644 Binary files a/test/refs/morph_mutate20.png and b/test/refs/morph_mutate20.png differ diff --git a/test/refs/morph_mutate21.png b/test/refs/morph_mutate21.png index 89f461ec1..c8869e188 100644 Binary files a/test/refs/morph_mutate21.png and b/test/refs/morph_mutate21.png differ diff --git a/test/refs/morph_mutate30.png b/test/refs/morph_mutate30.png index 89f461ec1..c8869e188 100644 Binary files a/test/refs/morph_mutate30.png and b/test/refs/morph_mutate30.png differ diff --git a/test/refs/morph_mutate31.png b/test/refs/morph_mutate31.png index acc203e48..bc16d2c46 100644 Binary files a/test/refs/morph_mutate31.png and b/test/refs/morph_mutate31.png differ diff --git a/test/refs/morph_mutate40.png b/test/refs/morph_mutate40.png index 698c0bec4..f26f1af1a 100644 Binary files a/test/refs/morph_mutate40.png and b/test/refs/morph_mutate40.png differ diff --git a/test/refs/morph_mutate41.png b/test/refs/morph_mutate41.png index 41981d55c..1ff2e123c 100644 Binary files a/test/refs/morph_mutate41.png and b/test/refs/morph_mutate41.png differ diff --git a/test/refs/partialdraw10.png b/test/refs/partialdraw10.png new file mode 100644 index 000000000..c467e328b Binary files /dev/null and b/test/refs/partialdraw10.png differ diff --git a/test/refs/partialdraw15.png b/test/refs/partialdraw15.png new file mode 100644 index 000000000..a65f225ad Binary files /dev/null and b/test/refs/partialdraw15.png differ diff --git a/test/refs/partialdraw18.png b/test/refs/partialdraw18.png new file mode 100644 index 000000000..965af9733 Binary files /dev/null and b/test/refs/partialdraw18.png differ diff --git a/test/refs/partialdraw5.png b/test/refs/partialdraw5.png new file mode 100644 index 000000000..20afe9424 Binary files /dev/null and b/test/refs/partialdraw5.png differ diff --git a/test/refs/star2circle5.png b/test/refs/star2circle5.png index 6d955736e..6fefb2691 100644 Binary files a/test/refs/star2circle5.png and b/test/refs/star2circle5.png differ diff --git a/test/refs/star2circle_keyframe15.png b/test/refs/star2circle_keyframe15.png new file mode 100644 index 000000000..bff4a05db Binary files /dev/null and b/test/refs/star2circle_keyframe15.png differ diff --git a/test/refs/star2circle_keyframe5.png b/test/refs/star2circle_keyframe5.png new file mode 100644 index 000000000..72a4c240f Binary files /dev/null and b/test/refs/star2circle_keyframe5.png differ diff --git a/test/refs/star2circle_obj15.png b/test/refs/star2circle_obj15.png new file mode 100644 index 000000000..1bb3da010 Binary files /dev/null and b/test/refs/star2circle_obj15.png differ diff --git a/test/refs/star2circle_obj5.png b/test/refs/star2circle_obj5.png new file mode 100644 index 000000000..82b8dfb64 Binary files /dev/null and b/test/refs/star2circle_obj5.png differ diff --git a/test/runtests.jl b/test/runtests.jl index d9e2eaf52..c697a42c7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -53,9 +53,14 @@ end @testset "Layers" begin include("layers.jl") end + @testset "Partial Drawing" begin + include("partialdrawtest.jl") + end @testset "Postprocessing" begin include("postprocessing.jl") end + # keep this the last test to avoid glitch where prompt + # gets stuck during test. @testset "Javis Viewer" begin include("viewer.jl") end diff --git a/test/stream_local.gif b/test/stream_local.gif index 094a9e6db..6387b4fff 100644 Binary files a/test/stream_local.gif and b/test/stream_local.gif differ diff --git a/test/svg.jl b/test/svg.jl index 15e3b8a49..7df57fcf2 100644 --- a/test/svg.jl +++ b/test/svg.jl @@ -77,6 +77,7 @@ end (args...) -> Javis.animate_latex(L"8", O - Point(-20, 20), 0, :middle, :center, :stroke), ) + #4 warnings expected 2 from getjpath and 2 from the usual render @test_logs (:warn,) (:warn,) render(video; tempdirectory = "images", pathname = "") @test_reference "refs/latex_alignment.png" load("images/0000000001.png") rm("images/0000000001.png") diff --git a/test/viewer.jl b/test/viewer.jl index f5ec2a928..4b6220834 100644 --- a/test/viewer.jl +++ b/test/viewer.jl @@ -57,13 +57,13 @@ end @testset "Livestreaming" begin - astar(args...; do_action = :stroke) = star(O, 50, 5, 0.5, 0, do_action) - acirc(args...; do_action = :stroke) = circle(Point(100, 100), 50, do_action) + astar() = star(O, 50, 5, 0.5, 0, :fill) + acirc() = circle(Point(100, 100), 50, :fill) vid = Video(500, 500) back = Background(1:100, ground) - star_obj = Object(1:100, astar) - act!(star_obj, Action(morph_to(acirc; do_action = :fill))) + star_obj = Object(1:100, (args...) -> astar()) + act!(star_obj, Action(morph_to(acirc))) conf_local = setup_stream(:local, address = "0.0.0.0", port = 8081) @test conf_local isa Javis.StreamConfig @@ -96,8 +96,8 @@ end vid = Video(500, 500) back = Background(1:100, ground) - star_obj = Object(1:100, astar) - act!(star_obj, Action(morph_to(acirc; do_action = :fill))) + star_obj = Object(1:100, (args...) -> astar()) + act!(star_obj, Action(morph_to(acirc))) @test_throws ErrorException render( vid,