diff --git a/CHANGELOG.md b/CHANGELOG.md index 36741de73..6b8f74414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Use [VideoIO](https://github.com/JuliaIO/VideoIO.jl) for faster rendering without temporary images - Ability to draw text in an animated way - Ability to morph with `fill` or `stroke` and using `SubAction` to specify changes in color - +- An object described by an action can follow a path (a vector of points). See `follow_path` ## 0.1.5 (14th of September 2020) - Bugfix in svg parser when a layer gets both transformed and scaled diff --git a/src/Javis.jl b/src/Javis.jl index 52ccb460f..2260ed340 100644 --- a/src/Javis.jl +++ b/src/Javis.jl @@ -67,6 +67,8 @@ 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) @@ -164,6 +166,7 @@ A SubAction should not be created by hand but instead by using one of the constr - `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 @@ -171,6 +174,7 @@ mutable struct SubAction <: AbstractAction func::Function transitions::Vector{Transition} internal_transitions::Vector{InternalTransition} + defs::Dict{Symbol,Any} end SubAction(transitions::Transition...) = SubAction(:same, transitions...) @@ -281,7 +285,7 @@ SubAction(frames, trans::Transition...) = SubAction(frames, anim::Animation, func::Function, transitions::Transition...) = - SubAction(frames, anim, func::Function, collect(transitions), []) + SubAction(frames, anim, func::Function, collect(transitions), [], Dict{Symbol,Any}()) """ ActionSetting @@ -1428,7 +1432,7 @@ 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, rotate_around +export appear, disappear, rotate_around, follow_path export rev export scaleto diff --git a/src/subaction_animations.jl b/src/subaction_animations.jl index 55c4fc67f..cdf2f1733 100644 --- a/src/subaction_animations.jl +++ b/src/subaction_animations.jl @@ -302,3 +302,63 @@ 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) + 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 rel_frame != 1 && 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 rel_frame == 1 + # 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/util.jl b/src/util.jl index 5dc174926..66a77e7ab 100644 --- a/src/util.jl +++ b/src/util.jl @@ -53,3 +53,7 @@ function get_interpolation(action::AbstractAction, frame) 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/animations.jl b/test/animations.jl index 1fe5805d1..7cccc8509 100644 --- a/test/animations.jl +++ b/test/animations.jl @@ -725,6 +725,89 @@ end 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:150, anim, follow_path(simple_bezier(); 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/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/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/followPathBezier10.png b/test/refs/followPathBezier10.png new file mode 100644 index 000000000..148dfd396 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..b9ed8519c 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..17ea0ce07 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..e2dcb65c9 Binary files /dev/null and b/test/refs/followPathBezier30.png differ