Skip to main content

src/etui@widgets@canvas.erl

-module(etui@widgets@canvas).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/widgets/canvas.gleam").
-export([canvas_new/1, series_new/1, with_series_fill/2, with_max/2, with_bg/2, with_period/2, render/4]).
-export_type([series_fill/0, series/0, canvas/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 series_fill() :: {series_solid, etui@style:color()} |
    {series_gradient, list(etui@style:color())} |
    series_rainbow |
    series_animated_rainbow.

-type series() :: {series, list(integer()), series_fill()}.

-type canvas() :: {canvas,
        list(series()),
        integer(),
        etui@style:color(),
        integer()}.

-file("src/etui/widgets/canvas.gleam", 45).
-spec canvas_new(list(series())) -> canvas().
canvas_new(Series) ->
    {canvas, Series, 0, default, 60}.

-file("src/etui/widgets/canvas.gleam", 49).
-spec series_new(list(integer())) -> series().
series_new(Data) ->
    {series, Data, series_rainbow}.

-file("src/etui/widgets/canvas.gleam", 53).
-spec with_series_fill(series(), series_fill()) -> series().
with_series_fill(S, Fill) ->
    {series, erlang:element(2, S), Fill}.

-file("src/etui/widgets/canvas.gleam", 57).
-spec with_max(canvas(), integer()) -> canvas().
with_max(C, Max) ->
    {canvas,
        erlang:element(2, C),
        gleam@int:max(1, Max),
        erlang:element(4, C),
        erlang:element(5, C)}.

-file("src/etui/widgets/canvas.gleam", 61).
-spec with_bg(canvas(), etui@style:color()) -> canvas().
with_bg(C, Bg) ->
    {canvas,
        erlang:element(2, C),
        erlang:element(3, C),
        Bg,
        erlang:element(5, C)}.

-file("src/etui/widgets/canvas.gleam", 65).
-spec with_period(canvas(), integer()) -> canvas().
with_period(C, Period) ->
    {canvas,
        erlang:element(2, C),
        erlang:element(3, C),
        erlang:element(4, C),
        gleam@int:max(1, Period)}.

-file("src/etui/widgets/canvas.gleam", 219).
-spec series_color(series_fill(), integer(), integer(), integer(), integer()) -> etui@style:color().
series_color(Fill, Px, Pw, Frame, Period) ->
    P = gleam@int:max(1, Period),
    W = gleam@int:max(1, Pw),
    case Fill of
        {series_solid, C} ->
            C;

        {series_gradient, Stops} ->
            etui@color:gradient(Stops, Px, W - 1);

        series_rainbow ->
            etui@color:hue_to_rgb(case W of
                    0 -> 0;
                    Gleam@denominator -> Px * 360 div Gleam@denominator
                end);

        series_animated_rainbow ->
            etui@color:hue_to_rgb(((case W of
                    0 -> 0;
                    Gleam@denominator@1 -> Px * 360 div Gleam@denominator@1
                end) + (case P of
                    0 -> 0;
                    Gleam@denominator@2 -> Frame * 360 div Gleam@denominator@2
                end)) rem 360)
    end.

-file("src/etui/widgets/canvas.gleam", 186).
-spec bresenham_loop(
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    list({integer(), integer()})
) -> list({integer(), integer()}).
bresenham_loop(X0, Y0, X1, Y1, Sx, Sy, Dx, Dy, Err, Acc) ->
    Acc@1 = [{X0, Y0} | Acc],
    case (X0 =:= X1) andalso (Y0 =:= Y1) of
        true ->
            Acc@1;

        false ->
            E2 = 2 * Err,
            {Err@1, X0@1} = case E2 >= Dy of
                true ->
                    {Err + Dy, X0 + Sx};

                false ->
                    {Err, X0}
            end,
            {Err@2, Y0@1} = case E2 =< Dx of
                true ->
                    {Err@1 + Dx, Y0 + Sy};

                false ->
                    {Err@1, Y0}
            end,
            bresenham_loop(X0@1, Y0@1, X1, Y1, Sx, Sy, Dx, Dy, Err@2, Acc@1)
    end.

-file("src/etui/widgets/canvas.gleam", 172).
-spec bresenham(integer(), integer(), integer(), integer()) -> list({integer(),
    integer()}).
bresenham(X0, Y0, X1, Y1) ->
    Dx = gleam@int:absolute_value(X1 - X0),
    Dy = 0 - gleam@int:absolute_value(Y1 - Y0),
    Sx = case X0 < X1 of
        true ->
            1;

        false ->
            -1
    end,
    Sy = case Y0 < Y1 of
        true ->
            1;

        false ->
            -1
    end,
    bresenham_loop(X0, Y0, X1, Y1, Sx, Sy, Dx, Dy, Dx + Dy, []).

-file("src/etui/widgets/canvas.gleam", 143).
-spec draw_segments(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    series(),
    list({integer(), integer()}),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
draw_segments(Pixels, Ser, Coords, Pw, Frame, Period) ->
    case Coords of
        [] ->
            Pixels;

        [_] ->
            Pixels;

        [P0, P1 | Rest] ->
            {X0, Y0} = P0,
            {X1, Y1} = P1,
            Pts = bresenham(X0, Y0, X1, Y1),
            Pixels@1 = gleam@list:fold(
                Pts,
                Pixels,
                fun(Px_dict, Pt) ->
                    {Px, Py} = Pt,
                    Fg = series_color(
                        erlang:element(3, Ser),
                        Px,
                        Pw,
                        Frame,
                        Period
                    ),
                    etui@braille:put(Px_dict, Px, Py, Fg)
                end
            ),
            draw_segments(Pixels@1, Ser, [P1 | Rest], Pw, Frame, Period)
    end.

-file("src/etui/widgets/canvas.gleam", 122).
-spec data_to_coords(
    list(integer()),
    integer(),
    integer(),
    integer(),
    integer()
) -> list({integer(), integer()}).
data_to_coords(Data, N, Pw, Ph, Max) ->
    Range = gleam@int:max(1, Max),
    Max_px = gleam@int:max(0, Pw - 1),
    Max_py = gleam@int:max(0, Ph - 1),
    gleam@list:index_map(
        Data,
        fun(Val, I) ->
            Px = case N =< 1 of
                true ->
                    0;

                false ->
                    case (N - 1) of
                        0 -> 0;
                        Gleam@denominator -> I * Max_px div Gleam@denominator
                    end
            end,
            Clamped = gleam@int:clamp(Val, 0, Range),
            Py = Max_py - (case Range of
                0 -> 0;
                Gleam@denominator@1 -> Clamped * Max_py div Gleam@denominator@1
            end),
            {Px, Py}
        end
    ).

-file("src/etui/widgets/canvas.gleam", 103).
-spec draw_series(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    series(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
draw_series(Pixels, Ser, Pw, Ph, Max, Frame, Period) ->
    N = erlang:length(erlang:element(2, Ser)),
    case N of
        0 ->
            Pixels;

        _ ->
            Coords = data_to_coords(erlang:element(2, Ser), N, Pw, Ph, Max),
            draw_segments(Pixels, Ser, Coords, Pw, Frame, Period)
    end.

-file("src/etui/widgets/canvas.gleam", 73).
?DOC(" Render canvas into `area`. `frame` drives animated fills.\n").
-spec render(etui@buffer:buffer(), etui@geometry:rect(), canvas(), integer()) -> etui@buffer:buffer().
render(Buf, Area, C, Frame) ->
    case (erlang:element(2, erlang:element(3, Area)) =< 0) orelse (erlang:element(
        3,
        erlang:element(3, Area)
    )
    =< 0) of
        true ->
            Buf;

        false ->
            Pw = erlang:element(2, erlang:element(3, Area)) * 2,
            Ph = erlang:element(3, erlang:element(3, Area)) * 4,
            Max = case erlang:element(3, C) of
                0 ->
                    gleam@list:fold(
                        erlang:element(2, C),
                        1,
                        fun(Acc, Ser) ->
                            gleam@list:fold(
                                erlang:element(2, Ser),
                                Acc,
                                fun gleam@int:max/2
                            )
                        end
                    );

                M ->
                    M
            end,
            Pixels = gleam@list:index_fold(
                erlang:element(2, C),
                etui@braille:new(),
                fun(Px_dict, Ser@1, _) ->
                    draw_series(
                        Px_dict,
                        Ser@1,
                        Pw,
                        Ph,
                        Max,
                        Frame,
                        erlang:element(5, C)
                    )
                end
            ),
            etui@braille:flush(Buf, Area, Pixels, erlang:element(4, C))
    end.