Skip to main content

src/etui@anim.erl

-module(etui@anim).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/anim.gleam").
-export([anim_new/0, tick/1, reset/1, is_done/2, lerp/4, ease_out/4, ease_in/4, oscillate/4, blink/2, cycle/2, ease_in_out/4, interpolate/5, sequence/3]).
-export_type([anim_state/0, easing/0, keyframe/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

-type anim_state() :: {anim_state, integer()}.

-type easing() :: linear | ease_in | ease_out | ease_in_out.

-type keyframe() :: {keyframe, integer(), integer()}.

-file("src/etui/anim.gleam", 11).
-spec anim_new() -> anim_state().
anim_new() ->
    {anim_state, 0}.

-file("src/etui/anim.gleam", 15).
-spec tick(anim_state()) -> anim_state().
tick(State) ->
    {anim_state, erlang:element(2, State) + 1}.

-file("src/etui/anim.gleam", 19).
-spec reset(anim_state()) -> anim_state().
reset(_) ->
    {anim_state, 0}.

-file("src/etui/anim.gleam", 23).
-spec is_done(anim_state(), integer()) -> boolean().
is_done(State, Duration) ->
    erlang:element(2, State) >= Duration.

-file("src/etui/anim.gleam", 31).
?DOC(" Linear interpolation from `start` to `end_` over `duration` frames.\n").
-spec lerp(integer(), integer(), integer(), integer()) -> integer().
lerp(Start, End_, Frame, Duration) ->
    case Duration =< 0 of
        true ->
            End_;

        false ->
            T = gleam@int:clamp(Frame, 0, Duration),
            Start + (case Duration of
                0 -> 0;
                Gleam@denominator -> (End_ - Start) * T div Gleam@denominator
            end)
    end.

-file("src/etui/anim.gleam", 42).
?DOC(" EaseOut (fast start, slow end). Quadratic approximation with integers.\n").
-spec ease_out(integer(), integer(), integer(), integer()) -> integer().
ease_out(Start, End_, Frame, Duration) ->
    case Duration =< 0 of
        true ->
            End_;

        false ->
            T = case Duration of
                0 -> 0;
                Gleam@denominator -> gleam@int:clamp(Frame, 0, Duration) * 100
                div Gleam@denominator
            end,
            Curve = (T * (200 - T)) div 100,
            Start + (((End_ - Start) * Curve) div 100)
    end.

-file("src/etui/anim.gleam", 54).
?DOC(" EaseIn (slow start, fast end). Quadratic approximation.\n").
-spec ease_in(integer(), integer(), integer(), integer()) -> integer().
ease_in(Start, End_, Frame, Duration) ->
    case Duration =< 0 of
        true ->
            End_;

        false ->
            T = case Duration of
                0 -> 0;
                Gleam@denominator -> gleam@int:clamp(Frame, 0, Duration) * 100
                div Gleam@denominator
            end,
            Curve = (T * T) div 100,
            Start + (((End_ - Start) * Curve) div 100)
    end.

-file("src/etui/anim.gleam", 67).
?DOC(
    " Oscillate between `min` and `max` with a given `period` (in frames).\n"
    " Returns current value in the triangle wave.\n"
).
-spec oscillate(integer(), integer(), integer(), integer()) -> integer().
oscillate(Min, Max, Frame, Period) ->
    case Period =< 0 of
        true ->
            Min;

        false ->
            Range = Max - Min,
            Half = Period div 2,
            Pos = case Period of
                0 -> 0;
                Gleam@denominator -> Frame rem Gleam@denominator
            end,
            case Pos < Half of
                true ->
                    Min + (case gleam@int:max(1, Half) of
                        0 -> 0;
                        Gleam@denominator@1 -> Range * Pos div Gleam@denominator@1
                    end);

                false ->
                    Max - (case gleam@int:max(1, Period - Half) of
                        0 -> 0;
                        Gleam@denominator@2 -> Range * (Pos - Half) div Gleam@denominator@2
                    end)
            end
    end.

-file("src/etui/anim.gleam", 84).
?DOC(
    " Returns True during the \"on\" half of each blink period.\n"
    " `period` ≤ 0 means always on.\n"
).
-spec blink(integer(), integer()) -> boolean().
blink(Frame, Period) ->
    case Period =< 0 of
        true ->
            true;

        false ->
            (case Period of
                0 -> 0;
                Gleam@denominator -> Frame rem Gleam@denominator
            end) < (Period div 2)
    end.

-file("src/etui/anim.gleam", 92).
?DOC(" Cycle through [0, count) returning the current index for the given frame.\n").
-spec cycle(integer(), integer()) -> integer().
cycle(Frame, Count) ->
    case Count =< 0 of
        true ->
            0;

        false ->
            case Count of
                0 -> 0;
                Gleam@denominator -> Frame rem Gleam@denominator
            end
    end.

-file("src/etui/anim.gleam", 126).
?DOC(" EaseInOut (slow start, fast middle, slow end). Quadratic approximation.\n").
-spec ease_in_out(integer(), integer(), integer(), integer()) -> integer().
ease_in_out(Start, End_, Frame, Duration) ->
    case Duration =< 0 of
        true ->
            End_;

        false ->
            T = case Duration of
                0 -> 0;
                Gleam@denominator -> gleam@int:clamp(Frame, 0, Duration) * 100
                div Gleam@denominator
            end,
            Curve = case T < 50 of
                true ->
                    (T * T) div 50;

                false ->
                    Inv = 100 - T,
                    100 - ((Inv * Inv) div 50)
            end,
            Start + (((End_ - Start) * Curve) div 100)
    end.

-file("src/etui/anim.gleam", 110).
?DOC(" Unified interpolation with selectable easing curve.\n").
-spec interpolate(integer(), integer(), integer(), integer(), easing()) -> integer().
interpolate(Start, End_, Frame, Duration, Easing) ->
    case Easing of
        linear ->
            lerp(Start, End_, Frame, Duration);

        ease_in ->
            ease_in(Start, End_, Frame, Duration);

        ease_out ->
            ease_out(Start, End_, Frame, Duration);

        ease_in_out ->
            ease_in_out(Start, End_, Frame, Duration)
    end.

-file("src/etui/anim.gleam", 158).
-spec find_segment(list(keyframe()), integer(), easing()) -> integer().
find_segment(Kfs, Frame, Easing) ->
    case Kfs of
        [] ->
            0;

        [{keyframe, _, V}] ->
            V;

        [{keyframe, At1, V1}, {keyframe, At2, V2} | Rest] ->
            case Frame < At2 of
                true ->
                    interpolate(V1, V2, Frame - At1, At2 - At1, Easing);

                false ->
                    find_segment([{keyframe, At2, V2} | Rest], Frame, Easing)
            end
    end.

-file("src/etui/anim.gleam", 153).
?DOC(
    " Interpolate along a keyframe sequence at the given frame.\n"
    " Keyframes are sorted automatically, so order at the call site doesn't matter.\n"
    " Returns the last keyframe value if frame exceeds the sequence.\n"
).
-spec sequence(list(keyframe()), integer(), easing()) -> integer().
sequence(Keyframes, Frame, Easing) ->
    Sorted = gleam@list:sort(
        Keyframes,
        fun(A, B) ->
            gleam@int:compare(erlang:element(2, A), erlang:element(2, B))
        end
    ),
    find_segment(Sorted, Frame, Easing).