From 484fe3e6ddd9654e0d944618a4d9e09e6436879e Mon Sep 17 00:00:00 2001 From: TheCedarPrince Date: Fri, 28 Aug 2020 11:47:47 -0400 Subject: [PATCH 1/6] Applied JuliaFormatter to all of Javis.jl files (#124) * Applied JuliaFormatter to all of Javis.jl files * Formatted an example file to meet standards * Formatted javis better using JuliaFormatter * Automatic formatting for whenever there is a push to master * Finished initial formatting of repo * Revert back to unformatted code * Added information about GitHub Action * Fix for failing Action * Reversion for failing GitHub Action * Formatted everything with JuliaFormatter --- .github/workflows/format_check.yml | 40 +++++ CHANGELOG.md | 3 + docs/make.jl | 28 ++-- src/backgrounds.jl | 34 ++--- src/luxor_overrides.jl | 14 +- src/morphs.jl | 27 ++-- src/subaction_animations.jl | 12 +- src/svg2luxor.jl | 50 ++++--- src/util.jl | 2 +- test/animations.jl | 16 +- test/svg.jl | 43 ++++-- test/unit.jl | 229 ++++++++++++++++------------- 12 files changed, 289 insertions(+), 209 deletions(-) create mode 100644 .github/workflows/format_check.yml diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml new file mode 100644 index 000000000..d0841520a --- /dev/null +++ b/.github/workflows/format_check.yml @@ -0,0 +1,40 @@ +name: format-check + +on: + push: + branches: + - 'master' + tags: '*' + pull_request: + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + julia-version: [1.5.0] + julia-arch: [x86] + os: [ubuntu-latest] + steps: + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.julia-version }} + + - uses: actions/checkout@master + - name: Install JuliaFormatter and format + run: | + julia -e 'using Pkg; Pkg.add("JuliaFormatter")' + julia -e 'using JuliaFormatter; format(".", annotate_untyped_fields_with_any=false, always_for_in=true, whitespace_in_kwargs=true, whitespace_ops_in_indices=true)' + - name: Format check + run: | + julia -e ' + out = Cmd(`git diff --name-only`) |> read |> String + if out == "" + exit(0) + else + @error "Some files have not been formatted !!!" + write(stdout, out) + exit(1) + end' + + diff --git a/CHANGELOG.md b/CHANGELOG.md index b240b624d..636022968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Javis.jl - Changelog +## Unreleased +- Added JuliaFormatter GitHub Action + ## 0.1.2 (24th of August 2020) - Added capabilities for generating `.mp4` files - Updated testing scheme for `Javis.jl` diff --git a/docs/make.jl b/docs/make.jl index 4bf8dc252..5c0520d08 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,30 +2,28 @@ using Javis using Documenter makedocs(; - modules=[Javis], - authors="Ole Kröger and contributors", - repo="https://github.com/Wikunia/Javis.jl/blob/{commit}{path}#L{line}", - sitename="Javis.jl", - format=Documenter.HTML(; - prettyurls=get(ENV, "CI", "false") == "true", - canonical="https://Wikunia.github.io/Javis.jl", - assets=String[], + modules = [Javis], + authors = "Ole Kröger and contributors", + repo = "https://github.com/Wikunia/Javis.jl/blob/{commit}{path}#L{line}", + sitename = "Javis.jl", + format = Documenter.HTML(; + prettyurls = get(ENV, "CI", "false") == "true", + canonical = "https://Wikunia.github.io/Javis.jl", + assets = String[], ), - pages=[ + pages = [ "Home" => "index.md", - "Tutorials" => [ - "tutorials.md", + "Tutorials" => [ + "tutorials.md", "tutorials/tutorial_1.md", "tutorials/tutorial_2.md", "tutorials/tutorial_3.md", "tutorials/tutorial_4.md", - ], + ], "Mission" => "mission.md", "References" => "references.md", "Contributing" => "contributing.md", ], ) -deploydocs(; - repo="github.com/Wikunia/Javis.jl", -) +deploydocs(; repo = "github.com/Wikunia/Javis.jl") diff --git a/src/backgrounds.jl b/src/backgrounds.jl index c82088101..5b666ef0a 100644 --- a/src/backgrounds.jl +++ b/src/backgrounds.jl @@ -23,11 +23,7 @@ Example call of this function within an `Action`. """ function draw_grid(; direction::AbstractString = "TR", line_gap = 25) return (video, action, frame) -> - _draw_grid(video, - action, - frame; - direction = direction, - line_gap = line_gap) + _draw_grid(video, action, frame; direction = direction, line_gap = line_gap) end function _draw_grid( @@ -49,7 +45,7 @@ function _draw_grid( if direction[1] == 'T' # Bottom to top for vertical grid lines - for x_point = min_width:line_gap:max_width + for x_point in min_width:line_gap:max_width start_point = Point(x_point, max_height) finish_point = Point(x_point, min_height) end_point = start_point + step * (finish_point - start_point) @@ -57,7 +53,7 @@ function _draw_grid( end else # Top to bottom motion for vertical grid lines - for x_point = min_width:line_gap:max_width + for x_point in min_width:line_gap:max_width start_point = Point(x_point, min_height) finish_point = Point(x_point, max_height) end_point = start_point + step * (finish_point - start_point) @@ -67,7 +63,7 @@ function _draw_grid( if direction[2] == 'R' # Left to right motion for horizontal grid lines - for y_point = min_height:line_gap:max_height + for y_point in min_height:line_gap:max_height start_point = Point(min_width, y_point) finish_point = Point(max_width, y_point) end_point = start_point + step * (finish_point - start_point) @@ -75,7 +71,7 @@ function _draw_grid( end else # Right to left motion for horizontal grid lines - for y_point = min_height:line_gap:max_height + for y_point in min_height:line_gap:max_height start_point = Point(max_width, y_point) finish_point = Point(min_width, y_point) end_point = start_point + step * (finish_point - start_point) @@ -118,12 +114,12 @@ One will need to define their own path for `tempdirectory` and `pathname`. """ function zero_lines(; direction::AbstractString = "TR", line_thickness = 10) - return (video, action, frame) -> - _zero_lines(video, - action, - frame; - direction = direction, - line_thickness = line_thickness, + return (video, action, frame) -> _zero_lines( + video, + action, + frame; + direction = direction, + line_thickness = line_thickness, ) end @@ -147,21 +143,21 @@ function _zero_lines( step = (frame - first(get_frames(action))) / (length(get_frames(action)) - 1) if direction[1] == 'B' - # Top to bottom motion for vertical line + # Top to bottom motion for vertical line start_point_y = Point(0, min_height) end_point_y = start_point_y + step * (Point(0, max_height) - start_point_y) else - # Bottom to top motion for vertical line + # Bottom to top motion for vertical line start_point_y = Point(0, max_height) end_point_y = start_point_y + step * (Point(0, min_height) - start_point_y) end if direction[2] == 'R' - # Left to right motion for horizontal line + # Left to right motion for horizontal line start_point_x = Point(min_width, 0) end_point_x = start_point_x + step * (Point(max_width, 0) - start_point_x) else - # Right to left motion for horizontal line + # Right to left motion for horizontal line start_point_x = Point(max_width, 0) end_point_x = start_point_x + step * (Point(min_width, 0) - start_point_x) end diff --git a/src/luxor_overrides.jl b/src/luxor_overrides.jl index 712398300..c143f5097 100644 --- a/src/luxor_overrides.jl +++ b/src/luxor_overrides.jl @@ -16,8 +16,8 @@ line(O, Point(10, 10)) - `linewidth`: the line width in pixel """ function setline(linewidth) - cs = get_current_setting() - cs.line_width = linewidth + cs = get_current_setting() + cs.line_width = linewidth current_line_width = cs.line_width * cs.mul_line_width Luxor.setline(current_line_width) end @@ -40,19 +40,19 @@ circle(O, 20, :fill) - `opacity`: the opacity between 0.0 and 1.0 """ function setopacity(opacity) - cs = get_current_setting() - cs.opacity = opacity + cs = get_current_setting() + cs.opacity = opacity current_opacity = cs.opacity * cs.mul_opacity Luxor.setopacity(current_opacity) end function fontsize(fsize) - cs = get_current_setting() - cs.fontsize = fsize + cs = get_current_setting() + cs.fontsize = fsize Luxor.fontsize(fsize) end function get_fontsize() - cs = get_current_setting() + cs = get_current_setting() return cs.fontsize end diff --git a/src/morphs.jl b/src/morphs.jl index a41fd9cd7..afc0716e4 100644 --- a/src/morphs.jl +++ b/src/morphs.jl @@ -25,7 +25,7 @@ function match_num_point!(poly_1::Vector{Point}, poly_2::Vector{Point}) end # the difference of the length of points - diff = l2-l1 + diff = l2 - l1 points_per_edge = div(diff, l1) # how many extra points do we need @@ -39,20 +39,20 @@ function match_num_point!(poly_1::Vector{Point}, poly_2::Vector{Point}) # p1 is the current point in the original polygon p1 = poly_1_orig[i] # p2 is the next point (which is the first of the polygon in the last iteration) - if i+1 > l1 + if i + 1 > l1 p2 = poly_1_orig[1] else - p2 = poly_1_orig[i+1] + p2 = poly_1_orig[i + 1] end # if we need 5 points and have only 4 edges we add 2 points for the first edge rem = 0 if i <= points_per_edge_extra rem = 1 end - for j in 1:points_per_edge+rem + for j in 1:(points_per_edge + rem) # create the interpolated point between p1 and p2 - t = j/(points_per_edge+rem+1) - new_point = p1+t*(p2-p1) + t = j / (points_per_edge + rem + 1) + new_point = p1 + t * (p2 - p1) insert!(poly_1, index, new_point) index += 1 end @@ -137,8 +137,8 @@ function save_morph_polygons!(action::Action, from_func::Function, to_func::Func for i in 1:length(from_poly) overall_distance = 0.0 - for j = 1:length(from_poly) - p1 = from_poly[(j+i-1) % length(from_poly) + 1] + for j in 1:length(from_poly) + p1 = from_poly[(j + i - 1) % length(from_poly) + 1] p2 = to_poly[j] overall_distance += distance(p1, p2) end @@ -149,7 +149,7 @@ function save_morph_polygons!(action::Action, from_func::Function, to_func::Func end new_from_poly = copy(from_poly) for i in 1:length(from_poly) - new_from_poly[i] = from_poly[(i+smallest_i-1) % length(from_poly) + 1] + new_from_poly[i] = from_poly[(i + smallest_i - 1) % length(from_poly) + 1] end action.opts[:from_poly] = new_from_poly @@ -162,8 +162,7 @@ end 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) # 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) @@ -175,12 +174,12 @@ function _morph(video::Video, action::Action, frame, 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 = (frame - first(get_frames(action))) / (length(get_frames(action)) - 1) for (i, p1, p2) in zip(1:length(from_poly), from_poly, to_poly) - new_point = p1+t*(p2-p1) + new_point = p1 + t * (p2 - p1) points[i] = new_point end - poly(points, :stroke; close=true) + poly(points, :stroke; close = true) end diff --git a/src/subaction_animations.jl b/src/subaction_animations.jl index 6bee3ebff..2204f006d 100644 --- a/src/subaction_animations.jl +++ b/src/subaction_animations.jl @@ -28,13 +28,13 @@ 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)) + 1)/length(get_frames(subaction)) + t = (rel_frame - first(get_frames(subaction)) + 1) / length(get_frames(subaction)) 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)) + 1)/length(get_frames(subaction)) + t = (rel_frame - first(get_frames(subaction)) + 1) / length(get_frames(subaction)) action.current_setting.mul_opacity = t end @@ -68,12 +68,12 @@ 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)) + 1)/length(get_frames(subaction)) - action.current_setting.mul_line_width = 1-t + t = (rel_frame - first(get_frames(subaction)) + 1) / length(get_frames(subaction)) + 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)) + 1)/length(get_frames(subaction)) - action.current_setting.mul_opacity = 1-t + t = (rel_frame - first(get_frames(subaction)) + 1) / length(get_frames(subaction)) + action.current_setting.mul_opacity = 1 - t end diff --git a/src/svg2luxor.jl b/src/svg2luxor.jl index 8a8ba0291..47934675b 100644 --- a/src/svg2luxor.jl +++ b/src/svg2luxor.jl @@ -91,8 +91,9 @@ function draw_obj(::Val{:path}, o, defs) l_pt, c_pt = path_quadratic(c_pt, parse.(Float64, split(args))...) elseif command == 'T' # the control point is a reflection based on the current and last point - control_pt = l_pt+2*(c_pt-l_pt) - l_pt, c_pt = path_quadratic(c_pt, control_pt..., parse.(Float64, split(args))...) + control_pt = l_pt + 2 * (c_pt - l_pt) + l_pt, c_pt = + path_quadratic(c_pt, control_pt..., parse.(Float64, split(args))...) elseif command == 'L' new_pt = Point(parse.(Float64, split(args))...) line(new_pt) @@ -124,8 +125,8 @@ end Moving to the specified point """ -function path_move(x,y) - p = Point(x,y) +function path_move(x, y) + p = Point(x, y) move(p) p end @@ -135,12 +136,12 @@ end Drawing a quadratic bezier curve by computing a cubic one as that is supported by Luxor """ -function path_quadratic(c_pt::Point, x,y, xe, ye) - e_pt = Point(xe,ye) - qc = Point(x,y) +function path_quadratic(c_pt::Point, x, y, xe, ye) + e_pt = Point(xe, ye) + qc = Point(x, y) # compute the control points of a cubic bezier curve - c1 = c_pt+2/3*(qc-c_pt) - c2 = e_pt+2/3*(qc-e_pt) + c1 = c_pt + 2 / 3 * (qc - c_pt) + c2 = e_pt + 2 / 3 * (qc - e_pt) curve(c1, c2, e_pt) return qc, e_pt end @@ -170,16 +171,19 @@ function set_attr(::Val{:transform}, transform_str) if transform_str !== nothing m = match(r"(.+)\((.+)\)", transform_str) type = Symbol(m.captures[1]) - set_transform(Val{type}(), parse.(Float64, strip.(split(m.captures[2],r"[, ]")))...) + set_transform( + Val{type}(), + parse.(Float64, strip.(split(m.captures[2], r"[, ]")))..., + ) end end -set_attr(::Val{:x}, x) = translate(parse(Float64,x),0) -set_attr(::Val{:y}, y) = translate(0,parse(Float64,y)) +set_attr(::Val{:x}, x) = translate(parse(Float64, x), 0) +set_attr(::Val{:y}, y) = translate(0, parse(Float64, y)) set_attr(::Val{Symbol("stroke-width")}, sw) = setline(parse(Float64, sw)) -set_transform(::Val{:translate}, x,y) = translate(x,y) -set_transform(::Val{:scale}, x,y=x) = scale(x,y) +set_transform(::Val{:translate}, x, y) = translate(x, y) +set_transform(::Val{:scale}, x, y = x) = scale(x, y) """ set_transform(::Val{:matrix}, args...) @@ -189,17 +193,17 @@ Multiply the new matrix with the current matrix and set it. function set_transform(::Val{:matrix}, args...) old = cairotojuliamatrix(getmatrix()) update = cairotojuliamatrix(collect(args)) - new = old*update + new = old * update # only the first two rows are considered - setmatrix(vec(new[1:2,:])) + setmatrix(vec(new[1:2, :])) end #= General fallbacks =# -draw_obj(::Union{Val{:title}, Val{:defs}}, o, defs) = nothing +draw_obj(::Union{Val{:title},Val{:defs}}, o, defs) = nothing # no support for colors at this point -set_attr(t::Union{Val{:stroke}, Val{:fill}, Val{Symbol("aria-hidden")}}, args...) = nothing +set_attr(t::Union{Val{:stroke},Val{:fill},Val{Symbol("aria-hidden")}}, args...) = nothing draw_obj(t, o, defs) = @warn "Can't draw $t" set_transform(t, args...) = @warn "Can't transform $t" @@ -218,23 +222,23 @@ function pathsvg(svg) fsize = get_current_setting().fontsize xdoc = parse_string(svg) xroot = root(xdoc) - def_element = get_elements_by_tagname(xroot, "defs")[1] + def_element = get_elements_by_tagname(xroot, "defs")[1] # create a dict for all the definitions - defs = Dict{String, Any}() + defs = Dict{String,Any}() for def in collect(child_elements(def_element)) defs[attribute(def, "id")] = def end x, y, width, height = parse.(Float64, split(attribute(xroot, "viewBox"))) # remove ex in the end - ex_width = parse(Float64, attribute(xroot, "width")[1:end-2]) - ex_height = parse(Float64, attribute(xroot, "height")[1:end-2]) + ex_width = parse(Float64, attribute(xroot, "width")[1:(end - 2)]) + ex_height = parse(Float64, attribute(xroot, "height")[1:(end - 2)]) # everything capsulated in a layer @layer begin # unit ex: half of font size # such that we can scale half of a font size (size of a lower letter) # with the corresponding height of the svg canvas # and the ex_height given in it's description - scale((fsize/2)/(height/ex_height)) + scale((fsize / 2) / (height / ex_height)) translate(-x, -y) for child in collect(child_elements(xroot)) diff --git a/src/util.jl b/src/util.jl index 6f88cbbb6..ddf055e61 100644 --- a/src/util.jl +++ b/src/util.jl @@ -3,7 +3,7 @@ Set action.frames.frames to the computed frames for each action in actions. """ -function compute_frames!(actions::Vector{AA}) where AA <: AbstractAction +function compute_frames!(actions::Vector{AA}) where {AA<:AbstractAction} last_frames = nothing for action in actions if last_frames === nothing && get_frames(action) === nothing diff --git a/test/animations.jl b/test/animations.jl index f92a7482b..04af8ab6f 100644 --- a/test/animations.jl +++ b/test/animations.jl @@ -82,7 +82,7 @@ end @test_reference "refs/dancing_circles_16.png" load("images/0000000016.png") @test isfile("dancing.gif") - for i = 1:25 + for i in 1:25 rm("images/$(lpad(i, 10, "0")).png") end rm("images/palette.bmp") @@ -178,7 +178,7 @@ end ) @test_reference "refs/dancing_circles_16_rot_trans.png" load("images/0000000016.png") - for i = 1:25 + for i in 1:25 rm("images/$(lpad(i, 10, "0")).png") end end @@ -224,7 +224,7 @@ end ) @test_reference "refs/dancing_circles_16_rot_trans.png" load("images/0000000016.png") - for i = 1:25 + for i in 1:25 rm("images/$(lpad(i, 10, "0")).png") end end @@ -252,7 +252,7 @@ end @test_reference "refs/grid_drawing_br.png" load("images/0000000018.png") @test_reference "refs/grid_drawing_tl.png" load("images/0000000028.png") @test_reference "refs/grid_drawing_tr.png" load("images/0000000038.png") - for i = 1:40 + for i in 1:40 rm("images/$(lpad(i, 10, "0")).png") end end @@ -274,7 +274,7 @@ end ) @test_reference "refs/circle_angle.png" load("images/0000000008.png") - for i = 1:10 + for i in 1:10 rm("images/$(lpad(i, 10, "0")).png") end end @@ -302,7 +302,7 @@ acirc(args...) = circle(Point(100, 100), 30) @test_reference "refs/star2circle5.png" load("images/0000000005.png") @test_reference "refs/star2circle15.png" load("images/0000000015.png") - for i = 1:20 + for i in 1:20 rm("images/$(lpad(i, 10, "0")).png") end end @@ -356,7 +356,7 @@ end @test_reference "refs/nicholas15.png" load("images/0000000015.png") @test_reference "refs/nicholas25.png" load("images/0000000025.png") @test_reference "refs/nicholas35.png" load("images/0000000035.png") - for i = 1:50 + for i in 1:50 rm("images/$(lpad(i, 10, "0")).png") end end @@ -407,7 +407,7 @@ end # test that the last frame is completely white @test sum(load("images/0000000050.png")) == RGB{Float64}(500 * 500, 500 * 500, 500 * 500) - for i = 1:50 + for i in 1:50 rm("images/$(lpad(i, 10, "0")).png") end end diff --git a/test/svg.jl b/test/svg.jl index 4cb5b8d94..dda97d96f 100644 --- a/test/svg.jl +++ b/test/svg.jl @@ -14,11 +14,18 @@ end video = Video(400, 200) - javis(video, [ - BackgroundAction(1:1, latex_ground), - Action((args...)->latex(L"\mathcal{O}(\log{n})")), # default fontsize 50 - Action((args...)->latex_blend(L"\mathcal{O}\left(\frac{\log{x}}{2}\right)", 20)), - ], tempdirectory="images", pathname="") + javis( + video, + [ + BackgroundAction(1:1, latex_ground), + Action((args...) -> latex(L"\mathcal{O}(\log{n})")), # default fontsize 50 + Action( + (args...) -> latex_blend(L"\mathcal{O}\left(\frac{\log{x}}{2}\right)", 20), + ), + ], + tempdirectory = "images", + pathname = "", + ) @test_reference "refs/ologn.png" load("images/0000000001.png") rm("images/0000000001.png") end @@ -36,10 +43,15 @@ end end video = Video(400, 200) - javis(video, [ - BackgroundAction(1:1, latex_ground), - Action((args...)->foreground(L"\mathcal{O}(\log{n})")), - ], tempdirectory="images", pathname="") + 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 @@ -60,10 +72,15 @@ end end video = Video(400, 200) - javis(video, [ - BackgroundAction(1:1, latex_ground), - Action((args...)->foreground(L"\mathcal{O}(\log{n})")), - ], tempdirectory="images", pathname="") + 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 c209a33ef..c7266355c 100644 --- a/test/unit.jl +++ b/test/unit.jl @@ -1,119 +1,142 @@ @testset "unit tests" begin -@testset "projection" begin - x0 = Line(Point(0, 10), O) - p = Point(10, 20) - @test projection(p, x0) == Point(0, 20) + @testset "projection" begin + x0 = Line(Point(0, 10), O) + p = Point(10, 20) + @test projection(p, x0) == Point(0, 20) - y0 = Line(Point(10, 0), O) - @test projection(p, y0) == Point(10, 0) -end + y0 = Line(Point(10, 0), O) + @test projection(p, y0) == Point(10, 0) + end -@testset "translation" begin - video = Video(500, 500) - # dummy action doesn't need a real function - 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) - @test action.internal_transitions[1].by == Point(1,1) - Javis.compute_transformation!(action, video, 50) - @test action.internal_transitions[1].by == Point(50,50) - Javis.compute_transformation!(action, video, 100) - @test action.internal_transitions[1].by == Point(100,100) -end + @testset "translation" begin + video = Video(500, 500) + # dummy action doesn't need a real function + 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) + @test action.internal_transitions[1].by == Point(1, 1) + Javis.compute_transformation!(action, video, 50) + @test action.internal_transitions[1].by == Point(50, 50) + Javis.compute_transformation!(action, video, 100) + @test action.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 "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)) + @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_transformation!(action, video, 1) - @test action.internal_transitions[1].by == O - Javis.compute_transformation!(action, video, 50) - @test action.internal_transitions[1].by == Point(49,49) - Javis.compute_transformation!(action, video, 100) - @test action.internal_transitions[1].by == Point(99,99) + Javis.compute_transformation!(action, video, 1) + @test action.internal_transitions[1].by == O + Javis.compute_transformation!(action, video, 50) + @test action.internal_transitions[1].by == Point(49, 49) + Javis.compute_transformation!(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)) + 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_transformation!(action, video, 1) - @test action.internal_transitions[1].by == O - Javis.compute_transformation!(action, video, 50) - @test action.internal_transitions[1].by == Point(49,49) - Javis.compute_transformation!(action, video, 100) - @test action.internal_transitions[1].by == Point(99,99) -end + Javis.compute_transformation!(action, video, 1) + @test action.internal_transitions[1].by == O + Javis.compute_transformation!(action, video, 50) + @test action.internal_transitions[1].by == Point(49, 49) + Javis.compute_transformation!(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)) + @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_transformation!(action, video, 1) - @test action.internal_transitions[1].angle == 0.0 - Javis.compute_transformation!(action, video, 100) - @test action.internal_transitions[1].angle == 2π + Javis.compute_transformation!(action, video, 1) + @test action.internal_transitions[1].angle == 0.0 + Javis.compute_transformation!(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)) + 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_transformation!(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) - @test action.internal_transitions[1].angle == 2π - @test action.internal_transitions[1].center == Point(2.0, 5.0) -end + Javis.compute_transformation!(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) + @test action.internal_transitions[1].angle == 2π + @test action.internal_transitions[1].center == Point(2.0, 5.0) + end -@testset "Frames errors" begin - video = Video(500, 500) - # throws because the frames of the first action need to be defined explicitly - @test_throws ArgumentError javis(video, [Action((args...)->1, Translation(Point(1,1), Point(100, 100)))]) - # throws because the frames of the first action need to be defined explicitly - @test_throws ArgumentError javis(video, [Action(Rel(10), (args...)->1, Translation(Point(1,1), Point(100, 100)))]) - # throws because :some is not supported as Symbol for `frames` - @test_throws ArgumentError javis(video, [ - Action(1:100, (args...)->1, Translation(Point(1,1), Point(100, 100))), - Action(:some, :id, (args...)->1, Translation(Point(1,1), Point(100, 100))) - ]) -end + @testset "Frames errors" begin + video = Video(500, 500) + # throws because the frames of the first action need to be defined explicitly + @test_throws ArgumentError javis( + video, + [Action((args...) -> 1, Translation(Point(1, 1), Point(100, 100)))], + ) + # throws because the frames of the first action need to be defined explicitly + @test_throws ArgumentError javis( + video, + [Action(Rel(10), (args...) -> 1, Translation(Point(1, 1), Point(100, 100)))], + ) + # throws because :some is not supported as Symbol for `frames` + @test_throws ArgumentError javis( + video, + [ + Action(1:100, (args...) -> 1, Translation(Point(1, 1), Point(100, 100))), + Action( + :some, + :id, + (args...) -> 1, + Translation(Point(1, 1), Point(100, 100)), + ), + ], + ) + end -@testset "Unspecified symbol error" begin - video = Video(500, 500) - # throws because the frames of the first action need to be defined explicitly - @test_throws ErrorException javis(video, [ - Action(1:100, (args...)->1), - Action(1:100, (args...)->line(O, pos(:non_existent), :stroke)) - ]) + @testset "Unspecified symbol error" begin + video = Video(500, 500) + # throws because the frames of the first action need to be defined explicitly + @test_throws ErrorException javis( + video, + [ + Action(1:100, (args...) -> 1), + Action(1:100, (args...) -> line(O, pos(:non_existent), :stroke)), + ], + ) - video = Video(500, 500) - # throws because the frames of the first action need to be defined explicitly - @test_throws ErrorException javis(video, [ - Action(1:100, (args...)->1), - Action(1:100, (args...)->line(O, ang(:non_existent), :stroke)) - ]) -end + video = Video(500, 500) + # throws because the frames of the first action need to be defined explicitly + @test_throws ErrorException javis( + video, + [ + Action(1:100, (args...) -> 1), + Action(1:100, (args...) -> line(O, ang(:non_existent), :stroke)), + ], + ) + end end From 562d051d687f70290f3a98fdf9af6f8d2fb519a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Kr=C3=B6ger?= Date: Sun, 30 Aug 2020 19:42:11 +0200 Subject: [PATCH 2/6] Project.toml with upper bound --- pull_request_template.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pull_request_template.md b/pull_request_template.md index 6bd53c3a4..03148d6e3 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -6,12 +6,13 @@ If you are contributing to `Javis.jl`, please make sure you are able to check of - [ ] Did I update the `TODO.md` (if applicable)? - [ ] Did I make sure to only change the part of the file where I introduced a new change/feature? - [ ] Did I cover all corner cases to be close to 100% test coverage (if applicable)? +- [ ] Did I properly add Javis dependencies to the `Project.toml` + set an upper bound of the dependency (if applicable)? - [ ] Did I properly add test dependencies to the `test` directory (if applicable)? - [ ] Did I check relevant tutorials that may be affected by changes in this PR? - [ ] Did I clearly articulate why this PR was made the way it was and how it was made? **Link to relevant issue(s)** - +Closes # **How did you address these issues with this PR? What methods did you use?** From d0cc7d9973cda818854aa0b53a05222e90db64b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Kr=C3=B6ger?= Date: Sun, 30 Aug 2020 19:42:11 +0200 Subject: [PATCH 3/6] Project.toml with upper bound --- TODO.md | 2 -- pull_request_template.md | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index 1597bf054..d88dd7134 100644 --- a/TODO.md +++ b/TODO.md @@ -8,8 +8,6 @@ graph TD B(#102) C(#55) - B --> A - C --> A ``` - [ ] 102 OPEN Ability to see animation live without creating pngs enhancement 2020-08-18 16:46:49 +0000 UTC diff --git a/pull_request_template.md b/pull_request_template.md index 6bd53c3a4..03148d6e3 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -6,12 +6,13 @@ If you are contributing to `Javis.jl`, please make sure you are able to check of - [ ] Did I update the `TODO.md` (if applicable)? - [ ] Did I make sure to only change the part of the file where I introduced a new change/feature? - [ ] Did I cover all corner cases to be close to 100% test coverage (if applicable)? +- [ ] Did I properly add Javis dependencies to the `Project.toml` + set an upper bound of the dependency (if applicable)? - [ ] Did I properly add test dependencies to the `test` directory (if applicable)? - [ ] Did I check relevant tutorials that may be affected by changes in this PR? - [ ] Did I clearly articulate why this PR was made the way it was and how it was made? **Link to relevant issue(s)** - +Closes # **How did you address these issues with this PR? What methods did you use?** From 499670c46c89c8d00127ddd95adcc6b7eb2ebe2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Kr=C3=B6ger?= Date: Mon, 31 Aug 2020 15:09:26 +0200 Subject: [PATCH 4/6] show progress of rendering --- Project.toml | 2 ++ TODO.md | 2 +- src/Javis.jl | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 961db926d..ac7442f37 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" 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" [compat] @@ -15,4 +16,5 @@ FFMPEG = "0.3, 0.4" LaTeXStrings = "1.1" LightXML = "0.9" Luxor = "2" +ProgressMeter = "1" julia = "1.4" diff --git a/TODO.md b/TODO.md index d88dd7134..1b2e0dd7c 100644 --- a/TODO.md +++ b/TODO.md @@ -74,7 +74,7 @@ graph TD - [ ] 64 OPEN Javis Templates Low Priority, To the Moon!, documentation, … 2020-08-09 18:31:06 +0000 UTC -- [ ] 56 OPEN ProgressMeter enhancement 2020-08-09 22:21:42 +0000 UTC +- [x] 56 OPEN ProgressMeter enhancement 2020-08-09 22:21:42 +0000 UTC - [ ] 42 OPEN Using Animations.jl enhancement, question 2020-08-05 13:42:17 +0000 UTC diff --git a/src/Javis.jl b/src/Javis.jl index cc3637d61..d8a511d12 100644 --- a/src/Javis.jl +++ b/src/Javis.jl @@ -5,6 +5,7 @@ using LaTeXStrings using LightXML import Luxor import Luxor: Point, @layer +using ProgressMeter using Random const FRAMES_SYMBOL = [:same] @@ -914,7 +915,7 @@ function javis( background_settings = ActionSetting() filecounter = 1 - for frame in frames + @showprogress 1 "Rendering frames..." for frame in frames Drawing( video.width, video.height, From 8e4826c4ecfaddad11112350b07dbe665168a0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Kr=C3=B6ger?= Date: Mon, 31 Aug 2020 15:10:50 +0200 Subject: [PATCH 5/6] changelog update --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 636022968..ef1f8e76c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Javis.jl - Changelog +## Unreleased v0.2 +- Show progress of rendering using [ProgressMeter.jl](https://github.com/timholy/ProgressMeter.jl) + ## Unreleased - Added JuliaFormatter GitHub Action From ce2dba6a18e7b86b56f4c8a5b4522d26f22d88d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Kr=C3=B6ger?= Date: Mon, 31 Aug 2020 15:42:43 +0200 Subject: [PATCH 6/6] Scaling support (#121) * first verison of scaling * ability to appear and disappear with scaling --- CHANGELOG.md | 1 + TODO.md | 2 +- docs/src/tutorials/tutorial_4.md | 2 +- src/Javis.jl | 161 ++++++++++++++++++++++++++- src/luxor_overrides.jl | 92 ++++++++++++++- src/subaction_animations.jl | 20 +++- test/animations.jl | 85 ++++++++++++++ test/refs/circlerSquare07opacity.png | Bin 2790 -> 2780 bytes test/refs/circlerSquare42opacity.png | Bin 2827 -> 2819 bytes test/refs/nicholas15.png | Bin 5597 -> 5447 bytes test/refs/nicholas35.png | Bin 5597 -> 5593 bytes test/refs/scalingCircle07.png | Bin 0 -> 2910 bytes test/refs/scalingCircle25.png | Bin 0 -> 4406 bytes test/refs/scalingCircle42.png | Bin 0 -> 3321 bytes test/refs/squeeze15.png | Bin 0 -> 4618 bytes test/refs/squeeze25.png | Bin 0 -> 4742 bytes test/refs/squeeze35.png | Bin 0 -> 4824 bytes test/refs/squeeze65.png | Bin 0 -> 4554 bytes test/refs/squeeze85.png | Bin 0 -> 4935 bytes test/unit.jl | 14 +++ 20 files changed, 364 insertions(+), 13 deletions(-) create mode 100644 test/refs/scalingCircle07.png create mode 100644 test/refs/scalingCircle25.png create mode 100644 test/refs/scalingCircle42.png create mode 100644 test/refs/squeeze15.png create mode 100644 test/refs/squeeze25.png create mode 100644 test/refs/squeeze35.png create mode 100644 test/refs/squeeze65.png create mode 100644 test/refs/squeeze85.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 636022968..9d2ce1db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Javis.jl - Changelog ## Unreleased +- Ability to scale an object with `Scaling`. Works similar to `Translation` and `Rotation` - Added JuliaFormatter GitHub Action ## 0.1.2 (24th of August 2020) diff --git a/TODO.md b/TODO.md index d88dd7134..7ddf5a34d 100644 --- a/TODO.md +++ b/TODO.md @@ -35,7 +35,7 @@ B-->A --- -- [ ] 103 OPEN Transformation: Scale enhancement 2020-08-18 16:54:28 +0000 UTC +- [X] 103 OPEN Transformation: Scale enhancement 2020-08-18 16:54:28 +0000 UTC - [ ] 98 OPEN Hijack Plotting Library for Object Positioning enhancement, question 2020-08-17 19:32:37 +0000 UTC diff --git a/docs/src/tutorials/tutorial_4.md b/docs/src/tutorials/tutorial_4.md index f86d31f99..08aef86e5 100644 --- a/docs/src/tutorials/tutorial_4.md +++ b/docs/src/tutorials/tutorial_4.md @@ -63,7 +63,7 @@ Although one could use standard an `Action` to achieve the same functionality, ` The `subactions` keyword uses a list of [`SubAction`](@ref) structs which are defined in a similar fashion as `Action` with `frames` and a `function` but are in some sense simpler than the `Action`. -A function of a `SubAction` is normally either [`appear`](@ref) or [`disappear`](@ref) at the moment or one of two transformations: [`Translation`](@ref) and [`Rotation`](@ref). +A function of a `SubAction` is normally either [`appear`](@ref) or [`disappear`](@ref) at the moment or one of these transformations: [`Translation`](@ref), [`Rotation`](@ref) and [`Scaling`](@ref). In theory you can define your own but that is way outside of this tutorial. diff --git a/src/Javis.jl b/src/Javis.jl index cc3637d61..a1c76cb2f 100644 --- a/src/Javis.jl +++ b/src/Javis.jl @@ -219,6 +219,10 @@ The current settings of an [`Action`](@ref) which are saved in `action.current_s - `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 @@ -226,9 +230,16 @@ mutable struct ActionSetting 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) +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) @@ -241,6 +252,9 @@ function update_ActionSetting!(as::ActionSetting, by::ActionSetting) 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 """ @@ -417,6 +431,10 @@ mutable struct InternalRotation <: InternalTransition center::Point end +mutable struct InternalScaling <: InternalTransition + scale::Tuple{Float64,Float64} +end + """ Translation <: Transition @@ -487,6 +505,52 @@ 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 + """ Line @@ -687,6 +751,37 @@ function compute_transition!( 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) @@ -717,6 +812,15 @@ function perform_transformation(trans::InternalRotation) 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) @@ -728,6 +832,14 @@ and [`get_angle`](@ref). - `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] @@ -763,6 +875,29 @@ get_position(s::Symbol) = get_position(val(s)) """ 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 @@ -812,6 +947,8 @@ function create_internal_transitions!(action::AbstractAction) 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 @@ -911,10 +1048,10 @@ function javis( else CURRENT_ACTION[1] = actions[1] end - background_settings = ActionSetting() filecounter = 1 for frame in frames + background_settings = ActionSetting() Drawing( video.width, video.height, @@ -1039,6 +1176,7 @@ end Set the default action values - line_width and calls `Luxor.setline`. - opacity and calls `Luxor.opacity`. +- scale and calls `Luxor.scale`. """ function set_action_defaults!(action) cs = action.current_setting @@ -1046,10 +1184,21 @@ function set_action_defaults!(action) Luxor.setline(current_line_width) current_opacity = cs.opacity * cs.mul_opacity Luxor.setopacity(current_opacity) + + desired_scale = cs.desired_scale .* cs.mul_scale + scaleto(desired_scale...) end -const LUXOR_DONT_EXPORT = - [:boundingbox, :Boxmaptile, :Sequence, :setline, :setopacity, :fontsize, :get_fontsize] +const LUXOR_DONT_EXPORT = [ + :boundingbox, + :Boxmaptile, + :Sequence, + :setline, + :setopacity, + :fontsize, + :get_fontsize, + :scale, +] # Export each function from Luxor for func in names(Luxor; imported = true) @@ -1061,12 +1210,12 @@ end export javis, latex export Video, Action, BackgroundAction, SubAction, Rel -export Line, Translation, Rotation, Transformation +export Line, Translation, Rotation, Transformation, Scaling export val, pos, ang, get_value, get_position, get_angle export projection, morph export appear, disappear # custom override of luxor extensions -export setline, setopacity, fontsize, get_fontsize +export setline, setopacity, fontsize, get_fontsize, scale end diff --git a/src/luxor_overrides.jl b/src/luxor_overrides.jl index c143f5097..9dad313fa 100644 --- a/src/luxor_overrides.jl +++ b/src/luxor_overrides.jl @@ -23,7 +23,7 @@ function setline(linewidth) end """ - setopacity(linewidth) + setopacity(opacity) Set the opacity and multiply it with the current multiplier which is i.e. set by [`appear`](@ref) and [`disappear`](@ref). @@ -46,13 +46,103 @@ function setopacity(opacity) Luxor.setopacity(current_opacity) end +""" + fontsize(fsize) + +Same as `Luxor.fontsize`: Sets the current font size. + +# Example +``` +fontsize(12) +text("Hello World!") +``` + +# Arguments: +- `fsize`: the new font size +""" function fontsize(fsize) cs = get_current_setting() cs.fontsize = fsize Luxor.fontsize(fsize) end +""" + get_fontsize(fsize) + +Same as `Luxor.get_fontsize` but works with every version of Luxor that is supported by Javis. + +# Example +``` +fontsize(12) +fsize = get_fontsize() +text("Hello World! \$fsize") +``` + +# Returns +- `Float64`: the current font size +""" function get_fontsize() cs = get_current_setting() return cs.fontsize end + +""" + scale(scl) + +Set the scale and multiply it with the current multiplier +which is i.e. set by [`appear`](@ref) and [`disappear`](@ref). + +Normal behavior without any animation is the same as `Luxor.scale`. + +# Example +``` +scale(0.5) +circle(O, 20, :fill) # the radius would be 10 because of the scaling +``` + +# Arguments: +- `scl`: the new default scale +""" +function scale(scl::Number) + scale(scl, scl) +end + +""" + scale(scl_x, scl_y) + +Same as [`scale`](@ref) but the x scale and y scale can be changed independently. + +# Arguments: +- `scl_x`: scale in x direction +- `scl_y`: scale in y direction +""" +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)") +end + +""" + scaleto(x, y) + +Scale to a specific scaling instead of multiplying it with the current scale. +For scaling on top of the current scale have a look at [`scale`](@ref). +""" +function scaleto(x, y) + cs = get_current_setting() + cs.desired_scale = (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) + end + Luxor.scale(scaling...) + cs.current_scale = (x, y) +end diff --git a/src/subaction_animations.jl b/src/subaction_animations.jl index 2204f006d..30fcf64d4 100644 --- a/src/subaction_animations.jl +++ b/src/subaction_animations.jl @@ -28,16 +28,22 @@ 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)) + 1) / length(get_frames(subaction)) + t = (rel_frame - first(get_frames(subaction))) / (length(get_frames(subaction)) - 1) 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)) + 1) / length(get_frames(subaction)) + t = (rel_frame - first(get_frames(subaction))) / (length(get_frames(subaction)) - 1) 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) + action.current_setting.mul_scale = t +end + """ disappear(s::Symbol) @@ -68,12 +74,18 @@ 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)) + 1) / length(get_frames(subaction)) + t = (rel_frame - first(get_frames(subaction))) / (length(get_frames(subaction)) - 1) 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)) + 1) / length(get_frames(subaction)) + t = (rel_frame - first(get_frames(subaction))) / (length(get_frames(subaction)) - 1) 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) + action.current_setting.mul_scale = 1 - t +end diff --git a/test/animations.jl b/test/animations.jl index 04af8ab6f..29d651a78 100644 --- a/test/animations.jl +++ b/test/animations.jl @@ -361,6 +361,38 @@ end end end +@testset "Squeezing a circle using scale" begin + demo = Video(500, 500) + javis( + demo, + [ + BackgroundAction(1:100, ground), + Action(:start_scale, (args...) -> (1.0, 1.0)), + Action( + (args...) -> circ(); + subactions = [ + SubAction(1:25, Scaling((1.0, 1.5))), + SubAction(Rel(25), Scaling((2.0, 1.0))), + SubAction(Rel(25), Scaling(:start_scale)), + SubAction(Rel(25), Scaling(2.0)), + ], + ), + Action((args...) -> circ(Point(-100, 0))), + ], + tempdirectory = "images", + pathname = "", + ) + + @test_reference "refs/squeeze15.png" load("images/0000000015.png") + @test_reference "refs/squeeze25.png" load("images/0000000025.png") + @test_reference "refs/squeeze35.png" load("images/0000000035.png") + @test_reference "refs/squeeze65.png" load("images/0000000065.png") + @test_reference "refs/squeeze85.png" load("images/0000000085.png") + for i in 1:100 + rm("images/$(lpad(i, 10, "0")).png") + end +end + function ground_opacity(args...) background("white") sethue("black") @@ -412,6 +444,59 @@ end end end +@testset "Scaling circle" begin + video = Video(500, 500) + javis( + video, + [ + BackgroundAction(1:50, ground), + Action(:set_scale, (args...) -> 2), + Action( + (args...) -> circ(), + subactions = [ + SubAction(1:15, Scaling(0.0, :set_scale)), + SubAction(36:50, Scaling(0.0)), + ], + ), + ], + tempdirectory = "images", + pathname = "", + ) + + @test_reference "refs/scalingCircle07.png" load("images/0000000007.png") + @test_reference "refs/scalingCircle25.png" load("images/0000000025.png") + @test_reference "refs/scalingCircle42.png" load("images/0000000042.png") + + # test using appear and disappear + video = Video(500, 500) + javis( + video, + [ + BackgroundAction(1:50, ground), + BackgroundAction(1:50, (args...) -> scale(2)), + Action( + (args...) -> circ(), + subactions = [ + SubAction(1:15, appear(:scale)), + SubAction(36:50, disappear(:scale)), + ], + ), + ], + tempdirectory = "images", + pathname = "", + ) + + @test_reference "refs/scalingCircle07.png" load("images/0000000007.png") + @test_reference "refs/scalingCircle25.png" load("images/0000000025.png") + @test_reference "refs/scalingCircle42.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) + for i in 1:50 + 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/circlerSquare07opacity.png b/test/refs/circlerSquare07opacity.png index 968e0ea027c67fbec987bc4e557c9ddb858bde53..dbbdb63ea7f71ce89427f9019bda0c47ba312bec 100644 GIT binary patch delta 1272 zcmYL}drVqq7>BJ`v?fZHM%OWiTei5gW9pKP;+1u6vo%X4S=V|8x`tY@4&YU{&sqldBkLGKN>pvX)`|V!josWv%BxPX@@eg?=gFjz;ZTPdQPoCU! zPK=|s%if11y2m@AP6*N+&4yQvt}YGYi^LYU22;zSP{_-xEXqIQ4+Mr}H_6bi6c)+!y-=0`M~VhUvQ38MPwo)TWU+$|Q1ySiQ=x8;qh z6HqFdh?%|L=^sFPSSXy;YLQNoRK0HK6+uw)pRL72LXzxIn!R>yk6bR-Xf)&N$GA7} zCzDAoc^*oLbhPZ%vlx$?SE=g_xxL<_u-a%e9_MVt(OuPUxRMKB zz?0tsO9LA^u}HK`aeBR8AEMDG(cO;e+)+7pbyrn zCDK{CB9Vy2Vo|J=a7*yy!)b!4q-0?z5^&H#wX|fdp1`y@T}HXmeAMyK%R~2rt~E-k zh@71_`lSk{>-_?4U%l4ur>9WM`ucfB5Kqn?tCcKg0&vCp_}<6hO~bq^xW8>0AsPUd z@XmR}F!kP~5I`RI%@N?R*_o!t1}Eqa@jhFlotryEY{r-b)dZaEWTA^oE|ME(qzA7u ztc2oXbveL>_(+qCJR)K?!{-X!yB3v{3BKMmUMh5R$yvh_Qf5_Ae&a1y@JPc9cG$K#qpi^19$JclKem{z$iC6^K1^*ZpNqTxT*3Qhf zacTFAwG-Okk^wrBWV`~t?i0jz*2f111`HKfHy=${b`b>e!Gc&cU_5#<5KDE9L%{E$B9V$PN= nkrZbn)#}?hW$C}t@xml#kvKY4qr8#G0Cnbc-Kl#YFuwQ~jg#D{ literal 2790 zcmeHJdrVVj6#uYLC$8Y!Rt!UM4<$M>%;H=DvC(0COs*Sj5fEsd0*cUfBgNXsEnS?| zu-Qd7F&1sIM9HRDq(Xtxha(j?#?T;^T5e0}TBQxGZD~vCYaeEbfA5d^$KpwDZq6g; zobPvj=ezgRC&+d}Vrn7)fUt8%K@k9PvkT8li=d_8<_-+L;%l}PZUNwiXlZzV0@ULg zinebB*Z)kLhRUh~JBkVcV6Oq7={NxM(A4x80OooC>{I}9&jXOcXzlxS699<^cNT2< zj1@O?%TOdvGDPsCV{FOu0<3gd(gof%B zn2rCKLE;8&)}a->gdM)95Ok%wG)r%Y4XsV z+u~;ZAvq{FU{EF|uIU__-)bi} zgUK12Cos&K-N4~+NNGkDPf^yssO^fNxM5SeNzIS>9J;X9ABuzmE|ZcYhtAB)g0r4* zI1IDF7HxDl4B^K@CYMo}mzTG+^)w#&y={-g>2!KNeCP^DHuL&=dzUPj2WfJJQaNmL z`u%>py;fPd7%9MIPEAiwTLwizWHTCOF_}yhO(!J~T2GEh$QhzRZswrH?R_*tj?5NM zzP5pp=ybUt4yvm&jUkJOTv=WoPm~IZpE#uI>Nqsw(@FTzoDLQt%V=iI!GKPnz?}2* zef3m*YDqsvYtfF=Z!L< zOS&Vd-M(%? z*=I<}u7q7>4QRF73=6%n#KX3smh93KKK9Czems#b({qEK)k7Wnx<$=8%~V%~RHwk+ zGa1-Q+8Zzcxg{b;dkixkrIPJi$4E>iCA74(h=;2oMML_!jVUQ9g5tZHQ#~qG6rv2z zrV@_TGI_j}q*vhVyk4(ZEQZ|ri__EF`;@9D;r!W3qbnen%Y&TAlYwN1FPD*848;FD4+naBbCHWlB!K zIUr0vdh}@U-HM6|B`?$naYPDnnTMd07xIs`?OCXHBn+$RbUK((@alcd;oQ7DMQ?9! zU*G&ci&N88_H1i@XxBMfvMFfLV-1-2Ba;0xeL009kw_LJrMOHQ%zVsilf=T0=;}x} zM)RtD9_x@!N}*I%;t>k&?eg;S@a*UwxEn^ucg$%vzZuJicsYD*nf@kW_dU(3dJ4}w zH`ilwK}j}k%Fl-c$;^S<1NYTEY_(e9Fb(jtx%+;jw{S9c8Q=6}D!6?ZtZ0jaSIU2` wkeKT5@}}6e=Thu>9qL7$=SQG_g>?)#e!BSaIL54oKcZmgHl*PCR_fP(1844?+W-In diff --git a/test/refs/circlerSquare42opacity.png b/test/refs/circlerSquare42opacity.png index 40f4ba0f9d89e76f9d21ea137c5218c5c7f8e46e..2f2c1f37b570663227d3cb29599c660ed76443f0 100644 GIT binary patch delta 1311 zcmX|AYfMvT7*;Ca#M1c(6cIGeMGb>2(^4q3+0+?bY+bfcZUSagj4S2hXzBIrNDOW) zVAP1QK;u|+Ne3-3tmXi1WsG3O%1AA>macL$(o%Xqr4*2{(ixBU`#WB0~f==K|z%eA_?I&+^XqeBvr=oLrBWHL|q0|8QU za*^C%v)QJnr$N!hp)EhpXm+=^w`bBNuh?V~iG<16YC%)eLj=*W^xcJJ3L`)-?=6B4 zq;0nW#Awi{CQDD@M?kGp9b8463}$Rh#;llVe^?1ce9*ECnq!l(D5g*_7>s9ny*Ytf zLO|Gqrt&jqYEN@0d`O3;l_{9UWV{vg&&QgHRfZrcS z{SDsb^?JYO^Z7!d)H>z!`Nqb^q`9@=J_O_*e8vN^eup^}3Wc8mSZHok92z6h?M`PR z_auP|pmRDpIs$3nJRSR6TBJa?>-kVpzjkbVM8VWJXjN>>LnYte1^;R!@j!L1-a#SVPo12?81BaDt zx#6wNw&x2sWH$}22U=QMcsyRo^=f^0{CY!v{*%WA5MH2U12mVbK4odVH5EShU6l!# zaoI`<2tc%72BiB0BPO=nF)YH|Z{F;WDYLD*x>}`DnH^;7 zW35(eGL=*vKcw9IvZB1ayrN>Mj6$c=DU{7I(I+M+CxIPfc9;-Ef&eSo-j=|9f?*h7 z-QlRg_fJer06e(sZwo9*%rAnPWK(yb9L?W3I)aG`( z{eH6gK0wH=om=V6vqW7?Ize|GsP&-Y*E=3BAY?VP-vW(1q~xH8x$STED>CQR=~9d+TL_z zA)96;Hlrq`)MeQ)>1aDpieCGo4XXyOWn)-ty>x&Ru@qWL3zys8?v!Mo_k6nN+&t&y ze}3obVqRkQG#7tcR(kAHagzToe!8#whwYQ)!+Ysm-pB9toOkscK@Za-$yJZwTj!@^XQ?oMNNS0ySp0A1^slR8+*_SVu>> z2ABNZJ-cVUacbSkiAy>=)$z&tVIttJs*{FaPV1CPrHyLjIb)kX11GjNh4}r=6e1&* z#x7-4RaG^okiW{MLXk)$m6|>N)xEc@RzxDXbNe>LXDkh-MxcJJ-e3^$`KxC=ejySk zy(|{Xpx5u*xih)*y1aNT8jVshvb(!G%7+6Lio8=T+nzb7Zlwi-kl#sJpM=P~wh`Cj~~vp2u&- zHyj)xT4U(}owYJ#bI~KPrZ07+M($2cP2qTr<+{h?u~K1!!{LyRHS$=|kTyP%EZGRm zVrq866CCmQV=*c7Z;Fzkcw#a`hmB&B$y9wAMNw5=U25;3d<;Ivzj!(`W04Stj7{me z9wCZ16;xlw>W6W@%l8%U?jG)l193QVC!n3Nu@hU4fq{YC2`J=08>?4-<8CUDpKIi~ zVdhLgBoX~wr%d+n-jWbaq;7ILoz+*N9Wb_Cp0W)MR_buRYJc@$<<`AQ-8A1cekC(I zd+RE|0?buk33p_1N08KB|BDwdW@q0z)q3&bF761}&z|ku(bn4f#+I?RmcOG-C}bv# z<}Dzb3)H#xV6err)i$JMl}PR%>eFgzOart%Itof1@12cCj~d14xicMRS#WL)^s?Lt zg+hs>cR=fLRo=?VO6X_AoRA0RLd(g?$;ik66^Yh^2M>O)P^6I8c>W3i diff --git a/test/refs/nicholas15.png b/test/refs/nicholas15.png index 1788a1acbcc45452636a0364b9b15fe40950b51c..ec3e2be8e43cef7bd2696496fd1a0fa157ba1a52 100644 GIT binary patch literal 5447 zcmeI0i&Iop9LFy#f{7c$C@_lCuJ~xu5?LOrsO%*mcTKENhn~Bd7=^*w0Fu{WTdFZD z*hG~u6C=s!EEeZKxQBmMl0?NkYa(8cMfpgC7KWG|e*kN`*_xv8` z{C>aty?5WU8`er^%$-3|lvJZmOra<*r{^=>8~&2`ow@`5^GT0Sj;E+^iv0Pl)8Kcn z?3A@nQss^FSolT0O`VcVQOshBdiQ;b;^0s3_EJ>ta*Fz63q{2qp{S6ZWtQhwQ`GEQ zO=A3}x4rJw*vf|*HXpflz3uhqcV#qsojLV+)!7BA7nUYHe!zI{)E4ijUwlVB&nIcg zf%(O)7lT{h`R4uE^WAg#zG_ptbcxYJOykX zYb=d)an3R0LN<2lf~kF&s)zq=U$A~R`-S7S5&wd&9#zk9K>*80*j)x;nCE-=zjcG5 zhM{!6RyVN6wl}BBm{XZO(k~pdxpx!{MRwmg-#?-k_Adzo%VI!Z?!!lO&INDgN9xk- z4TH}n`PjeqwV!F6Uy{{!>Z9oe+w=;yd-oS%-t0OF`&19mD0e)U-b)1 zq%RVbXHOVO94Kn2cOX04eeJIxJMQmSUM*d3+5O0uAJUe3+Hz?nyz0eGfio-ZNX^PTWJ(tO z=$;advMC`bP_56DSD(wiWG=1zBCJp-BT+Fr37d&+Efd*FmYRoikgfBgu=1jSqZuVq zr4Ttumsgh}C;A;EL&q;PvlMNjY^w-dq8S+m=^{`|GqW(r6oH>;#t#A0HUhMrG_xCn zH$>nD&17Pr7lErZvlRi|b`fZ!navo8^3b-@j0S-UQ88$nX=V)u;;0&ECISP|p=&SG z%pwfL;nva291PwRMRkE@q!@_vsG%7z1o~ekE37wYE&~Hmg4Q;gOTplE(TZl8i^V{c zx%D#5g<+5-T5*BqW+0GvgcNU+62x_&DoGaARf*KxG}(QLJ58h#x%Cou7OKP>8N@|g z1I;~-U5E;?DFVdRA{PNQWq&0v?pPhLIZ>WpXms93Jy@_p4D_}8t=PNX^1{bU z!kSg0EzxJm^LL;`xH?f-*XOD`uL-;+GILSiUiOKEtW)t8k#NfT>PoV8p z%QEHpKj6K;>317#FXtm+H~xx;Z2LqEa16)Q!Q#N1UsH=t-TX`>h-<-t?L32>N5c9e zlkw$l>>^FVwvyv*M@I4v4$|xc4`EQX z95^nJ6(=#sivW%~0^%`HDuClM0S##UsQb6WTNx?D(Add-298FWNg&0bL^p1E^=YH? zIH~r|mdU|nbe{FKU;hjbMt3XrJ{%1kSBZDi&dI?fudXyYyNN9U;Npch_+cgKoKLYW zf2b8me+cDx#lsRdn8a(lh05J>5y}zbJRE13r2u{8rq1KLupJ7UTv1T{*isX1A)Pi5 ziYA=>5L8rX!RG9e=SK}ViP->5@E)v0i{kB&Bt_N^-DN>`nR zm!6l7Kj@{xcDIflFu6Wi3E{Z>n$mE%I_W75HzCibG@R$b#5$$nJP$cU!-2TpXl^;0 zN~*NUX~@@`F?~V13-VLA7d;+|oHOO3n zhOsKOEDdTq8y94AM|(uua2{VHJ7H)MTsE~V-Glf=Herm1`mA?tuWnH1_E)(c^IXp_ tvMZrf{xRIJ&kW_k)cdUe+Pc6KKYY_kX_7W1mOr_r@E>$=)s+AM literal 5597 zcmeI0|5sD@8ON_*?yyGZC2L$DsToWV$RTJamiX02>hLYb{_r3wlP+yw4ELGyf`KVfIbA2^)k zmFLUn^Lak+=ehU2SZ4(Hr<1Gm?CxJELzQ|3}fh;?}auS27O##2Rzq$&H^KTXbx5 zuCaO7AIcU^o*--$^{ezfYsCXEskBi?Y*mjJ*xh$t*ZR9h9Te*#rW%@K^arccgsC|d zYD4XE?x3}-V5*eB8a&%n2gT0kKX|?m&+Fm;_Fjlx=jQnzM)z10Nt+Vud93o>Z)(#H zCE4XP`vcct{E*L4ktxM~ZI~gOc7R?}BmK_!(ijbH2CX^H)8GRGxiJlM0sd>+(L>WB&Ni zy!X__vw!~ko5F)Mq1rMwEY~xCN6!L(&e=I&vR38E0Tb8B%)3(ymR?%*M%Ln516O-_ zo=z}yeb?C2UGx{~tU5*5v;~~~bN$-o!Mx^2h_Oe5dAZ)tDk&Z_C#vx5JE77@RvLti zU4KWUB~pyHfj7&SSC8t1E3cebvO%9$rLI5yZ1L@F=8tG@yqT3YQg^>Y?v_Q0eO7RH z4<%dtx{UvUC{poo3g(S42UOmT018G3p<6Xk> z;Xslqk+(%09|EKuc{7!@TF3FBKsqtuIDQ0>I+THPB`2>3 zvIAw{%;e-$AoEcM&SXwb0OG^sEl!>cqz+}^)N!&3$ZM!}+(tpu60Dug#i-tjBSrVN zV7=Y0pfxHlWu z&J7NKL++zaSCiS#Y6o7BLk7D?xrRFZMaubd{|m~1cFN9dR?7AjY8NVI$77?vT)E&L z9O~-vx!lo1lay~=>YcHHA2hap6K^kc4NMqnbgd=|r4(8E%UatUN}f0_RdAk-9^Bh1 zzhU>)Tp3o$-WsB7TE!v;7JIJ9elaL49qj?Zvp)(SYFBi!nw8d2Eyy2ce0!h0NRiD4 z0=qGbs%2}{GnLTFQ-8A|=-H18(}7PhPItQcrCrveIdq;mYr598yR+8poKhXUrU_tbY3s zMR&TOG1A=D(lDf6R~TuSeKbryE5)KPUqhI{wU|u9nuZ8H9utwR*@%2gZHUw*BJv+fh&&o7 zF{2EKq-G$3;t&~LjR^8aqp=U8rKI(C@Mf#)4;S=qqF31YC#Vp9J}ZTy;)Nzx*t=il*+CVqKN8fyQ~D!)UL~m2 z9W!03*U0hfXbQ?mP@UeJDo-iZr2cjDps;O5!E2_49Vfy5%uE)llcBI5Dj>>w+w1xo zDy981U_$ZCRe6@sr4DhBfwj26dKi>+pV9!^lk<@}hpPOYJi5}IWcZ@a?6g9v(}kHC z!OWC<=CG&ZM4`2k40ppp2Ktl_qLm|B-rj?Yp@Uc*C1ybBE)QPrksIY z4VA{S5^c9}sHfE~|QX}lSvXa)1Cp%W(1=ZDm7)=R*O3Ma!Cyzln zF6Pr%DG}VcYEg2pk-3yMPjpv2w_@~Na4-8mMN92V6}(r2rP3XSNIW4>MBQ5uv84#! zi(vCPA~nf^_dJk!h%_4olNU${BI60U1!MpbW2RvG7my4@4rd6aFM*szq&ZzMd4Q-8 z8Bd5CNCG0pG{Hnii}i>cP8Cd7z~+yLG^YrrOF*t7GMr~XA=-7A}7gcL7#&jAQG1%IR6o}*}MnUP7i6a@Jw@Cr1d7} zvXTYrtq;|xCXMuRsL?7EW(65vu0fa@6vjeE26zz5ABSzoutp1HkgA`c$s)4}#OXs1 zRdO;bO@Z86D}sTB$C+8w=y27mMqs*FIrXbUv!O4o&W#< diff --git a/test/refs/nicholas35.png b/test/refs/nicholas35.png index 1788a1acbcc45452636a0364b9b15fe40950b51c..1afa6e326ce9ba16c3f05e152d788d172de4e4c1 100644 GIT binary patch literal 5593 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 diff --git a/test/refs/scalingCircle07.png b/test/refs/scalingCircle07.png new file mode 100644 index 0000000000000000000000000000000000000000..bf7c682642f1f6a9b209b81d3799455e319235d7 GIT binary patch literal 2910 zcmeHJjZ@NR9DmHhPDZurv{k-T*X$M-ZdO{N%(Be0w3nUQq_DY$%Y1`M6OgD}b5yL_ z>D*LqdSQFuoux3Db1;%V!n}YpeXMGd+^Wb{N4TTci->td7j_%d3)}cdpt4> z<+{-o0DuZV5_%EqoM!R|r+b@^9f|}jnb|>(SbQTAo!0=MB#KEWMgEn1f z6z)v@rEw-k_>46Arm5-h{kRnVc~yH6ML)$?6;o2`#~+H1qi&N;+R=4Bk%fU7>QuT# zVNlZ9bQMpLq7YNClZ-w_Pl6YXW}WJ-k=*r&i;F{|@y-!EKHoG|BNN(f7Sr~N7cXMx+Wg!x zXQ`7FbYVhJS9a!{)ZZoVG2^_vuMe9xD9%B; zlTd{?y3HW6TYcD6?Gu9aEAF=_bZ3n$n4<3EGmvWzlWv_P zV1&k@mIpr0%nor0)n-z8(dJKZN>7zX()7JVkx81mto5C^UPrhMb(BTKRGyWhwEY=MnpM{g zg2!FE%W6;ROrwiKrHGJ}DnU|wb*unS zsTA4FkUEdpzYuPT$;s10ubC2-m#p?FkEfsSjB;~hhAd`QTBE>ZDV1zO`B4e zFZz*}tT3C+HmgO(#<2-exUv4wzXg88Vv3q?7@%N8g-|%yBP)2x zjMo?9TcGV(UN|A-%l&NTxrBsmc7H`(Djmw`u&?+9j1>$LE7<%I67^qqcO8$XWyhU9 zy)ScS@Lg$`BXTf%zArdu1d+v9lS#!EY2WPhqA$#jjs0XJ!sQJbrM)8pV>Ne+RvmpO z_J${dJ$IcC2hS&IM_S-nVAWO9t!jZrtLe+OOv6~b)npq4j!fMRRIhC75C;TanF^S% zsi>%k$lpaXjpi#I<8TYV|7g)qXAE3b6$eqE#TuZ_8~$bRqobqM*3*Ob0>riSjiBjg nu%X=HuBBp$gHOx{DiJ_Z?jw$GolS>7z##lkWGLrA{Pq6<5GdM; literal 0 HcmV?d00001 diff --git a/test/refs/scalingCircle25.png b/test/refs/scalingCircle25.png new file mode 100644 index 0000000000000000000000000000000000000000..bf4d21d883db5efd3d691621f7d1206bf9975889 GIT binary patch literal 4406 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 F{{crJ(J24` literal 0 HcmV?d00001 diff --git a/test/refs/squeeze15.png b/test/refs/squeeze15.png new file mode 100644 index 0000000000000000000000000000000000000000..d699315b80daeb64a208684e2383d5e3727f6afa GIT binary patch literal 4618 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 H`Tp`B0jazD literal 0 HcmV?d00001 diff --git a/test/refs/squeeze25.png b/test/refs/squeeze25.png new file mode 100644 index 0000000000000000000000000000000000000000..dd371317c85c70c58ace4e811b993804cf7e5a38 GIT binary patch literal 4742 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 D67}f{ literal 0 HcmV?d00001 diff --git a/test/refs/squeeze65.png b/test/refs/squeeze65.png new file mode 100644 index 0000000000000000000000000000000000000000..24a5efb4c70c09aa87ea68b1142723d45365cafb GIT binary patch literal 4554 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 literal 0 HcmV?d00001 diff --git a/test/refs/squeeze85.png b/test/refs/squeeze85.png new file mode 100644 index 0000000000000000000000000000000000000000..c5599c87fe8d5a7971d9fbf89d2894aa82dfdcd2 GIT binary patch literal 4935 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 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) + @test action.internal_transitions[1].scale == (0.0, 0.0) + Javis.compute_transformation!(action, video, 50) + @test action.internal_transitions[1].scale == (0.5, 0.5) + Javis.compute_transformation!(action, video, 100) + @test action.internal_transitions[1].scale == (1.0, 1.0) + end + @testset "Relative frames" begin video = Video(500, 500) action = Action(Rel(10), (args...) -> 1, Translation(Point(1, 1), Point(100, 100)))