From a52526200bbd12bd9f3195b9f12b7dc59c6530b4 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 14 Jul 2024 10:39:25 +0530 Subject: [PATCH 1/2] Refactor all current path changes to their own folder --- src/FlyThroughPaths.jl | 10 +- src/pathchange.jl | 156 ----------------------------- src/pathchanges/beziermove.jl | 58 +++++++++++ src/pathchanges/constrainedmove.jl | 94 +++++++++++++++++ src/pathchanges/pause.jl | 39 ++++++++ 5 files changed, 200 insertions(+), 157 deletions(-) create mode 100644 src/pathchanges/beziermove.jl create mode 100644 src/pathchanges/constrainedmove.jl create mode 100644 src/pathchanges/pause.jl diff --git a/src/FlyThroughPaths.jl b/src/FlyThroughPaths.jl index 22d09ef..0eb6c83 100644 --- a/src/FlyThroughPaths.jl +++ b/src/FlyThroughPaths.jl @@ -1,8 +1,9 @@ module FlyThroughPaths using StaticArrays +using Rotations -export ViewState, Path, Pause, ConstrainedMove, BezierMove +export ViewState, Path # exports for the extensions export capture_view, set_view! @@ -11,6 +12,13 @@ include("viewstate.jl") include("pathchange.jl") include("path.jl") +# path changes +include("pathchanges/beziermove.jl") +include("pathchanges/constrainedmove.jl") +include("pathchanges/pause.jl") + +export BezierMove, ConstrainedMove, Pause + function __init__() if isdefined(Base.Experimental, :register_error_hint) Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs diff --git a/src/pathchange.jl b/src/pathchange.jl index 3eed4a5..6b7a509 100644 --- a/src/pathchange.jl +++ b/src/pathchange.jl @@ -23,121 +23,11 @@ an `action` callback. """ abstract type PathChange{T<:Real} end -struct Pause{T} <: PathChange{T} - duration::T - action - - function Pause{T}(t, action=nothing) where T - t >= zero(T) || throw(ArgumentError("t must be non-negative")) - new{T}(t, action) - end -end - -""" - Pause(duration, [action]) - -Pause at the current position for `duration`. -""" -Pause(duration::T) where T = Pause{T}(duration) - -Base.convert(::Type{Pause{T}}, p::Pause) where T = Pause{T}(p.duration, p.action) -Base.convert(::Type{PathChange{T}}, p::Pause) where T = convert(Pause{T}, p) - -struct ConstrainedMove{T} <: PathChange{T} - duration::T - target::ViewState{T} - constraint::Symbol - speed::Symbol - action - function ConstrainedMove{T}(t, target, constraint, speed, action=nothing) where T - t >= zero(T) || throw(ArgumentError("t must be non-negative")) - constraint in (:none,:rotation) || throw(ArgumentError("Unknown constraint: $constraint")) - speed in (:constant,:sinusoidal) || throw(ArgumentError("Unknown speed: $speed")) - new{T}(t, target, constraint, speed, action) - end -end - -""" - ConstrainedMove(duration::T, target::ViewState{T}, [constraint, speed, action]) where T <: Real - -Create a `ConstrainedMove` which represents a movement from the current -[`ViewState`](@ref) to a `target` [`ViewState`](@ref) over a specified -`duration`. The movement can be constrained by `:rotation` or -unconstrained by `:none`, and can proceed at a `:constant` or `:sinusoidal` -speed. - -# Arguments -- `duration::T`: The duration of the movement, where `T` is a subtype of `Real`. -- `target::ViewState{T}`: The target state to reach at the end of the movement. -- `constraint::Symbol`: The type of constraint on the movement (`:none` or `:rotation`). -- `speed::Symbol`: The speed pattern of the movement (`:constant` or `:sinusoidal`). -- `action`: An optional callback to be called at each step of the movement. - - -# Examples -```julia -# Move to a new view state over 5 seconds with no rotation and constant speed -move = ConstrainedMove(5.0, new_view_state, :none, :constant) -path *= move -``` - -This type of `PathChange` is useful for animations where the view needs to -transition smoothly between two states under certain constraints. -""" -ConstrainedMove{T}(duration, target; constraint=:none, speed=:constant, action=nothing) where T = - ConstrainedMove{T}(duration, target, constraint, speed, action) -ConstrainedMove(duration, target::ViewState{T}, args...) where T = ConstrainedMove{T}(duration, target, args...) -ConstrainedMove(duration, target::ViewState{T}; kwargs...) where T = ConstrainedMove{T}(duration, target; kwargs...) - -Base.convert(::Type{ConstrainedMove{T}}, m::ConstrainedMove) where T = ConstrainedMove{T}(m.duration, m.target, m.constraint, m.speed, m.action) -Base.convert(::Type{PathChange{T}}, m::ConstrainedMove) where T = convert(ConstrainedMove{T}, m) - -struct BezierMove{T} <: PathChange{T} - duration::T - target::ViewState{T} - controls::Vector{ViewState{T}} - action - - function BezierMove{T}(t, target, controls, action=nothing) where T - t >= zero(T) || throw(ArgumentError("t must be non-negative")) - new{T}(t, target, controls, action) - end -end - -""" - BezierMove(duration::T, target::ViewState{T}, controls::Vector{ViewState{T}}, [action]) where T <: Real - -Create a `BezierMove` which represents a movement from the current -[`ViewState`](@ref) to a `target` [`ViewState`](@ref) over a specified -`duration`. The movement is defined by a series of control points, which -are interpolated between to form a smooth curve. - -See the [Wikipedia article on Bezier curves](https://en.wikipedia.org/wiki/B%C3%A9zier_curve) for more details. - -# Arguments -- `duration::T`: The duration of the movement, where `T` is a subtype of `Real`. -- `target::ViewState{T}`: The target state to reach at the end of the movement. -- `controls::Vector{ViewState{T}}`: The control points of the movement. -- `action`: An optional callback to be called at each step of the movement. - -# Examples -```julia -# Move to a new view state over 5 seconds with no rotation and constant speed -move = BezierMove(5.0, new_view_state, [new_view_state]) -path *= move -``` -""" -BezierMove(duration, target::ViewState{R}, controls::Vector{ViewState{S}}, args...) where {R,S} = BezierMove{promote_type(R,S)}(duration, target, controls, args...) - -Base.convert(::Type{BezierMove{T}}, m::BezierMove) where T = BezierMove{T}(m.duration, m.target, m.controls, m.action) -Base.convert(::Type{PathChange{T}}, m::BezierMove) where T = convert(BezierMove{T}, m) - # Common API duration(c::PathChange{T}) where T = c.duration::T target(oldtarget::ViewState{T}, c::PathChange{T}) where T = c.target::ViewState{T} -target(oldtarget::ViewState{T}, ::Pause{T}) where T = oldtarget Base.@nospecializeinfer function act(@nospecialize(action), t::Real) action === nothing && return nothing @@ -145,52 +35,6 @@ Base.@nospecializeinfer function act(@nospecialize(action), t::Real) return nothing end -# Compute the view from a PathChange at (relative) time t - -function (pause::Pause{T})(view::ViewState{T}, t) where T - checkt(t, pause) - action = pause.action - if action !== nothing - tf = t / duration(move) - act(action, tf) - end - return view -end - -function (move::ConstrainedMove{T})(view::ViewState{T}, t) where T - checkt(t, move) - (; target, constraint, speed, action) = move - tf = t / duration(move) - f = speed === :constant ? tf : (1 - cospi(tf))/2 - (; eyeposition, lookat, upvector, fov) = view - eyeposition_new = something(target.eyeposition, eyeposition) - lookat_new = something(target.lookat, lookat) - upvector_new = something(target.upvector, upvector) - fov_new = something(target.fov, fov) - lookatf = (1 - f) * lookat + f * lookat_new - if constraint === :none - eyeposition = (1 - f) * eyeposition + f * eyeposition_new - elseif constraint === :rotation - vold = eyeposition - lookat - vnew = eyeposition_new - lookat_new - eyeposition = cospi(f/2) * vold + sinpi(f/2) * vnew + lookatf - end - upvector = (1 - f) * upvector + f * upvector_new - fov = (1 - f) * fov + f * fov_new - lookat = lookatf - act(action, f) - return ViewState{T}(eyeposition, lookat, upvector, fov) -end - -function (move::BezierMove{T})(view::ViewState{T}, t) where T - filldef(vs) = filldefaults(vs, view) - checkt(t, move) - tf = t / duration(move) - act(move.action, tf) - list = [view, filldef.(move.controls)..., filldef(move.target)] - return evaluate(list, tf) -end - # Recursive evaluation of bezier curves, https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Recursive_definition function evaluate(list, t) length(list) == 1 && return list[1] diff --git a/src/pathchanges/beziermove.jl b/src/pathchanges/beziermove.jl new file mode 100644 index 0000000..0fb5d22 --- /dev/null +++ b/src/pathchanges/beziermove.jl @@ -0,0 +1,58 @@ +#= +# BezierMove + +This moves the camera along a Bezier path parametrized by control points. + +## Example + +## Implementation +=# + +struct BezierMove{T} <: PathChange{T} + duration::T + target::ViewState{T} + controls::Vector{ViewState{T}} + action + + function BezierMove{T}(t, target, controls, action=nothing) where T + t >= zero(T) || throw(ArgumentError("t must be non-negative")) + new{T}(t, target, controls, action) + end +end + +function (move::BezierMove{T})(view::ViewState{T}, t) where T + filldef(vs) = filldefaults(vs, view) + checkt(t, move) + tf = t / duration(move) + act(move.action, tf) + list = [view, filldef.(move.controls)..., filldef(move.target)] + return evaluate(list, tf) +end + +""" + BezierMove(duration::T, target::ViewState{T}, controls::Vector{ViewState{T}}, [action]) where T <: Real + +Create a `BezierMove` which represents a movement from the current +[`ViewState`](@ref) to a `target` [`ViewState`](@ref) over a specified +`duration`. The movement is defined by a series of control points, which +are interpolated between to form a smooth curve. + +See the [Wikipedia article on Bezier curves](https://en.wikipedia.org/wiki/B%C3%A9zier_curve) for more details. + +# Arguments +- `duration::T`: The duration of the movement, where `T` is a subtype of `Real`. +- `target::ViewState{T}`: The target state to reach at the end of the movement. +- `controls::Vector{ViewState{T}}`: The control points of the movement. +- `action`: An optional callback to be called at each step of the movement. + +# Examples +```julia +# Move to a new view state over 5 seconds with no rotation and constant speed +move = BezierMove(5.0, new_view_state, [new_view_state]) +path *= move +``` +""" +BezierMove(duration, target::ViewState{R}, controls::Vector{ViewState{S}}, args...) where {R,S} = BezierMove{promote_type(R,S)}(duration, target, controls, args...) + +Base.convert(::Type{BezierMove{T}}, m::BezierMove) where T = BezierMove{T}(m.duration, m.target, m.controls, m.action) +Base.convert(::Type{PathChange{T}}, m::BezierMove) where T = convert(BezierMove{T}, m) diff --git a/src/pathchanges/constrainedmove.jl b/src/pathchanges/constrainedmove.jl new file mode 100644 index 0000000..eccba21 --- /dev/null +++ b/src/pathchanges/constrainedmove.jl @@ -0,0 +1,94 @@ +#= +# ConstrainedMove + +A [`ConstrainedMove`](@ref) is a [`PathChange`](@ref) that moves the camera +from the current [`ViewState`](@ref) to a `target` [`ViewState`](@ref) under +a specified `constraint` and some interpolation / easing in `speed`. + +## Example + +TODO: add an example here. + +## Implementation + +First, we define the actual struct according to the contract for [`PathChange`](@ref). +=# + + +struct ConstrainedMove{T} <: PathChange{T} + duration::T + target::ViewState{T} + constraint::Symbol + speed::Symbol + action + function ConstrainedMove{T}(t, target, constraint, speed, action=nothing) where T + t >= zero(T) || throw(ArgumentError("t must be non-negative")) + constraint in (:none,:rotation) || throw(ArgumentError("Unknown constraint: $constraint")) + speed in (:constant,:sinusoidal) || throw(ArgumentError("Unknown speed: $speed")) + new{T}(t, target, constraint, speed, action) + end +end + +# This is the implementation of the constrained move. + +function (move::ConstrainedMove{T})(view::ViewState{T}, t) where T + checkt(t, move) + (; target, constraint, speed, action) = move + tf = t / duration(move) + f = speed === :constant ? tf : (1 - cospi(tf))/2 + (; eyeposition, lookat, upvector, fov) = view + eyeposition_new = something(target.eyeposition, eyeposition) + lookat_new = something(target.lookat, lookat) + upvector_new = something(target.upvector, upvector) + fov_new = something(target.fov, fov) + lookatf = (1 - f) * lookat + f * lookat_new + if constraint === :none + eyeposition = (1 - f) * eyeposition + f * eyeposition_new + elseif constraint === :rotation + vold = eyeposition - lookat + vnew = eyeposition_new - lookat_new + eyeposition = cospi(f/2) * vold + sinpi(f/2) * vnew + lookatf + end + upvector = (1 - f) * upvector + f * upvector_new + fov = (1 - f) * fov + f * fov_new + lookat = lookatf + act(action, f) + return ViewState{T}(eyeposition, lookat, upvector, fov) +end + +# Then, we define more constructors, as well as the [`PathChange`](@ref) API. + +""" + ConstrainedMove(duration::T, target::ViewState{T}, [constraint, speed, action]) where T <: Real + +Create a `ConstrainedMove` which represents a movement from the current +[`ViewState`](@ref) to a `target` [`ViewState`](@ref) over a specified +`duration`. The movement can be constrained by `:rotation` or +unconstrained by `:none`, and can proceed at a `:constant` or `:sinusoidal` +speed. + +# Arguments +- `duration::T`: The duration of the movement, where `T` is a subtype of `Real`. +- `target::ViewState{T}`: The target state to reach at the end of the movement. +- `constraint::Symbol`: The type of constraint on the movement (`:none` or `:rotation`). +- `speed::Symbol`: The speed pattern of the movement (`:constant` or `:sinusoidal`). +- `action`: An optional callback to be called at each step of the movement. + + +# Examples +```julia +# Move to a new view state over 5 seconds with no rotation and constant speed +move = ConstrainedMove(5.0, new_view_state, :none, :constant) +path *= move +``` + +This type of `PathChange` is useful for animations where the view needs to +transition smoothly between two states under certain constraints. +""" +ConstrainedMove{T}(duration, target; constraint=:none, speed=:constant, action=nothing) where T = + ConstrainedMove{T}(duration, target, constraint, speed, action) +ConstrainedMove(duration, target::ViewState{T}, args...) where T = ConstrainedMove{T}(duration, target, args...) +ConstrainedMove(duration, target::ViewState{T}; kwargs...) where T = ConstrainedMove{T}(duration, target; kwargs...) + +Base.convert(::Type{ConstrainedMove{T}}, m::ConstrainedMove) where T = ConstrainedMove{T}(m.duration, m.target, m.constraint, m.speed, m.action) +Base.convert(::Type{PathChange{T}}, m::ConstrainedMove) where T = convert(ConstrainedMove{T}, m) diff --git a/src/pathchanges/pause.jl b/src/pathchanges/pause.jl new file mode 100644 index 0000000..ef2f6fa --- /dev/null +++ b/src/pathchanges/pause.jl @@ -0,0 +1,39 @@ +#= +# Pause + +[`Pause`](@ref) is a move that encodes a pause, i.e., no movement in the camera state at all. +The pause lasts for `duration` time, and has an `action` callback. + +The construction is very simple - simply `Pause(duration)`. +=# +struct Pause{T} <: PathChange{T} + duration::T + action + + function Pause{T}(t, action=nothing) where T + t >= zero(T) || throw(ArgumentError("t must be non-negative")) + new{T}(t, action) + end +end + +function (pause::Pause{T})(view::ViewState{T}, t) where T + checkt(t, pause) + action = pause.action + if action !== nothing + tf = t / duration(move) + act(action, tf) + end + return view +end + +target(oldtarget::ViewState{T}, ::Pause{T}) where T = oldtarget + +""" + Pause(duration, [action]) + +Pause at the current position for `duration`. +""" +Pause(duration::T) where T = Pause{T}(duration) + +Base.convert(::Type{Pause{T}}, p::Pause) where T = Pause{T}(p.duration, p.action) +Base.convert(::Type{PathChange{T}}, p::Pause) where T = convert(Pause{T}, p) From 920d8f44c6d8e43928119e2a57c4b968247fc96a Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 14 Jul 2024 10:54:29 +0530 Subject: [PATCH 2/2] Add Rotations.jl --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index 0b43618..6b6bb50 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["Tim Holy and contributors"] version = "0.1.0" [deps] +Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [weakdeps]