Skip to main content

src/etui@widgets@scene.erl

-module(etui@widgets@scene).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/widgets/scene.gleam").
-export([scene_new/1, with_bg/2, render/4]).
-export_type([scene_fill/0, shape/0, scene/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 scene_fill() :: {scene_solid, etui@style:color()} |
    {scene_gradient, list(etui@style:color())} |
    scene_rainbow |
    scene_animated_rainbow.

-type shape() :: {circle_outline, integer(), integer(), integer(), scene_fill()} |
    {disc, integer(), integer(), integer(), scene_fill()} |
    {planet,
        integer(),
        integer(),
        integer(),
        integer(),
        scene_fill(),
        integer()} |
    {mandelbrot, integer()}.

-type scene() :: {scene, list(shape()), etui@style:color()}.

-file("src/etui/widgets/scene.gleam", 47).
-spec scene_new(list(shape())) -> scene().
scene_new(Shapes) ->
    {scene, Shapes, default}.

-file("src/etui/widgets/scene.gleam", 51).
-spec with_bg(scene(), etui@style:color()) -> scene().
with_bg(S, Bg) ->
    {scene, erlang:element(2, S), Bg}.

-file("src/etui/widgets/scene.gleam", 372).
-spec mandelbrot_iter(
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer()
) -> integer().
mandelbrot_iter(Cr, Ci, Zr, Zi, Iter, Max_iter) ->
    case Iter >= Max_iter of
        true ->
            Iter;

        false ->
            Zr2 = (Zr * Zr) div 1024,
            Zi2 = (Zi * Zi) div 1024,
            case (Zr2 + Zi2) > 4096 of
                true ->
                    Iter;

                false ->
                    mandelbrot_iter(
                        Cr,
                        Ci,
                        (Zr2 - Zi2) + Cr,
                        (((2 * Zr) * Zi) div 1024) + Ci,
                        Iter + 1,
                        Max_iter
                    )
            end
    end.

-file("src/etui/widgets/scene.gleam", 344).
-spec mandelbrot_cols(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
mandelbrot_cols(Pixels, Pw, Pw1, Max_iter, By, Ci, Bx) ->
    case Bx >= Pw of
        true ->
            Pixels;

        false ->
            Cr = -2560 + (case Pw1 of
                0 -> 0;
                Gleam@denominator -> Bx * 3584 div Gleam@denominator
            end),
            Iter = mandelbrot_iter(Cr, Ci, 0, 0, 0, Max_iter),
            Pixels@1 = case Iter >= Max_iter of
                true ->
                    Pixels;

                false ->
                    Hue = case Max_iter of
                        0 -> 0;
                        Gleam@denominator@1 -> Iter * 300 div Gleam@denominator@1
                    end,
                    Fg = etui@color:hue_to_rgb(Hue),
                    etui@braille:put(Pixels, Bx, By, Fg)
            end,
            mandelbrot_cols(Pixels@1, Pw, Pw1, Max_iter, By, Ci, Bx + 1)
    end.

-file("src/etui/widgets/scene.gleam", 324).
-spec mandelbrot_rows(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
mandelbrot_rows(Pixels, Pw, Ph, Pw1, Ph1, Max_iter, By) ->
    case By >= Ph of
        true ->
            Pixels;

        false ->
            Ci = 1280 - (case Ph1 of
                0 -> 0;
                Gleam@denominator -> By * 2560 div Gleam@denominator
            end),
            Pixels@1 = mandelbrot_cols(Pixels, Pw, Pw1, Max_iter, By, Ci, 0),
            mandelbrot_rows(Pixels@1, Pw, Ph, Pw1, Ph1, Max_iter, By + 1)
    end.

-file("src/etui/widgets/scene.gleam", 310).
-spec draw_mandelbrot(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
draw_mandelbrot(Pixels, Pw, Ph, Max_iter) ->
    Pw1 = gleam@int:max(1, Pw - 1),
    Ph1 = gleam@int:max(1, Ph - 1),
    mandelbrot_rows(Pixels, Pw, Ph, Pw1, Ph1, Max_iter, 0).

-file("src/etui/widgets/scene.gleam", 405).
-spec scene_color(scene_fill(), integer(), integer(), integer(), integer()) -> etui@style:color().
scene_color(Fill, Px, Pw, Frame, Period) ->
    P = gleam@int:max(1, Period),
    W = gleam@int:max(1, Pw),
    case Fill of
        {scene_solid, C} ->
            C;

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

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

        scene_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/scene.gleam", 203).
-spec disc_dy_loop(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    integer(),
    integer(),
    integer(),
    scene_fill(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
disc_dy_loop(Pixels, Cx, Cy, R, Fill, Pw, Ph, Frame, Period, Dx, Dy) ->
    case Dy > R of
        true ->
            Pixels;

        false ->
            Pixels@1 = case ((Dx * Dx) + (Dy * Dy)) =< (R * R) of
                false ->
                    Pixels;

                true ->
                    Bx = Cx + Dx,
                    By = Cy + Dy,
                    case (((Bx >= 0) andalso (By >= 0)) andalso (Bx < Pw))
                    andalso (By < Ph) of
                        false ->
                            Pixels;

                        true ->
                            Fg = scene_color(Fill, Bx, Pw, Frame, Period),
                            etui@braille:put(Pixels, Bx, By, Fg)
                    end
            end,
            disc_dy_loop(
                Pixels@1,
                Cx,
                Cy,
                R,
                Fill,
                Pw,
                Ph,
                Frame,
                Period,
                Dx,
                Dy + 1
            )
    end.

-file("src/etui/widgets/scene.gleam", 181).
-spec disc_dx_loop(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    integer(),
    integer(),
    integer(),
    scene_fill(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
disc_dx_loop(Pixels, Cx, Cy, R, Fill, Pw, Ph, Frame, Period, Dx) ->
    case Dx > R of
        true ->
            Pixels;

        false ->
            Pixels@1 = disc_dy_loop(
                Pixels,
                Cx,
                Cy,
                R,
                Fill,
                Pw,
                Ph,
                Frame,
                Period,
                Dx,
                0 - R
            ),
            disc_dx_loop(
                Pixels@1,
                Cx,
                Cy,
                R,
                Fill,
                Pw,
                Ph,
                Frame,
                Period,
                Dx + 1
            )
    end.

-file("src/etui/widgets/scene.gleam", 167).
-spec draw_disc(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    integer(),
    integer(),
    integer(),
    scene_fill(),
    integer(),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
draw_disc(Pixels, Cx, Cy, R, Fill, Pw, Ph, Frame, Period) ->
    disc_dx_loop(Pixels, Cx, Cy, R, Fill, Pw, Ph, Frame, Period, 0 - R).

-file("src/etui/widgets/scene.gleam", 270).
-spec cos_sin_256(integer()) -> {integer(), integer()}.
cos_sin_256(Step) ->
    case Step rem 32 of
        0 ->
            {256, 0};

        1 ->
            {251, 50};

        2 ->
            {236, 98};

        3 ->
            {213, 142};

        4 ->
            {181, 181};

        5 ->
            {142, 213};

        6 ->
            {98, 236};

        7 ->
            {50, 251};

        8 ->
            {0, 256};

        9 ->
            {-50, 251};

        10 ->
            {-98, 236};

        11 ->
            {-142, 213};

        12 ->
            {-181, 181};

        13 ->
            {-213, 142};

        14 ->
            {-236, 98};

        15 ->
            {-251, 50};

        16 ->
            {-256, 0};

        17 ->
            {-251, -50};

        18 ->
            {-236, -98};

        19 ->
            {-213, -142};

        20 ->
            {-181, -181};

        21 ->
            {-142, -213};

        22 ->
            {-98, -236};

        23 ->
            {-50, -251};

        24 ->
            {0, -256};

        25 ->
            {50, -251};

        26 ->
            {98, -236};

        27 ->
            {142, -213};

        28 ->
            {181, -181};

        29 ->
            {213, -142};

        30 ->
            {236, -98};

        _ ->
            {251, -50}
    end.

-file("src/etui/widgets/scene.gleam", 258).
?DOC(" 32-step integer orbit using precomputed cos/sin × 256.\n").
-spec orbit_position(integer(), integer(), integer(), integer(), integer()) -> {integer(),
    integer()}.
orbit_position(Cx, Cy, R, Frame, Period) ->
    Step = (case gleam@int:max(1, Period) of
        0 -> 0;
        Gleam@denominator -> Frame * 32 div Gleam@denominator
    end) rem 32,
    {C256, S256} = cos_sin_256(Step),
    {Cx + ((R * C256) div 256), Cy + ((R * S256) div 256)}.

-file("src/etui/widgets/scene.gleam", 241).
-spec draw_planet(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    integer(),
    integer(),
    integer(),
    integer(),
    scene_fill(),
    integer(),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
draw_planet(Pixels, Cx, Cy, Orbit_r, Dot_r, Fill, Pw, Ph, Frame, Period) ->
    {Px, Py} = orbit_position(Cx, Cy, Orbit_r, Frame, Period),
    draw_disc(Pixels, Px, Py, Dot_r, Fill, Pw, Ph, Frame, Period).

-file("src/etui/widgets/scene.gleam", 132).
-spec midpoint_loop(
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    list({integer(), integer()})
) -> list({integer(), integer()}).
midpoint_loop(Cx, Cy, Y, X, D, Acc) ->
    case Y > X of
        true ->
            Acc;

        false ->
            Pts = [{Cx + X, Cy + Y},
                {Cx - X, Cy + Y},
                {Cx + X, Cy - Y},
                {Cx - X, Cy - Y},
                {Cx + Y, Cy + X},
                {Cx - Y, Cy + X},
                {Cx + Y, Cy - X},
                {Cx - Y, Cy - X}],
            Acc@1 = lists:append(Acc, Pts),
            Y@1 = Y + 1,
            {X@1, D@1} = case D < 0 of
                true ->
                    {X, (D + (2 * Y@1)) + 1};

                false ->
                    {X - 1, (D + (2 * (Y@1 - X))) + 1}
            end,
            midpoint_loop(Cx, Cy, Y@1, X@1, D@1, Acc@1)
    end.

-file("src/etui/widgets/scene.gleam", 125).
-spec circle_outline_pts(integer(), integer(), integer()) -> list({integer(),
    integer()}).
circle_outline_pts(Cx, Cy, R) ->
    case R =< 0 of
        true ->
            [{Cx, Cy}];

        false ->
            midpoint_loop(Cx, Cy, 0, R, 1 - R, [])
    end.

-file("src/etui/widgets/scene.gleam", 100).
-spec draw_circle_outline(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    integer(),
    integer(),
    integer(),
    scene_fill(),
    integer(),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
draw_circle_outline(Pixels, Cx, Cy, R, Fill, Pw, Ph, Frame, Period) ->
    Pts = circle_outline_pts(Cx, Cy, R),
    N = erlang:length(Pts),
    gleam@list:index_fold(
        Pts,
        Pixels,
        fun(Px_dict, Pt, I) ->
            {Bx, By} = Pt,
            case (((Bx >= 0) andalso (By >= 0)) andalso (Bx < Pw)) andalso (By < Ph) of
                false ->
                    Px_dict;

                true ->
                    Fg = scene_color(Fill, I, N, Frame, Period),
                    etui@braille:put(Px_dict, Bx, By, Fg)
            end
        end
    ).

-file("src/etui/widgets/scene.gleam", 79).
-spec render_shape(
    gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}),
    shape(),
    integer(),
    integer(),
    integer()
) -> gleam@dict:dict({integer(), integer()}, {integer(), etui@style:color()}).
render_shape(Pixels, Shape, Pw, Ph, Frame) ->
    case Shape of
        {circle_outline, Cx, Cy, R, Fill} ->
            draw_circle_outline(Pixels, Cx, Cy, R, Fill, Pw, Ph, Frame, 60);

        {disc, Cx@1, Cy@1, R@1, Fill@1} ->
            draw_disc(Pixels, Cx@1, Cy@1, R@1, Fill@1, Pw, Ph, Frame, 60);

        {planet, Cx@2, Cy@2, Orbit_r, Dot_r, Fill@2, Period} ->
            draw_planet(
                Pixels,
                Cx@2,
                Cy@2,
                Orbit_r,
                Dot_r,
                Fill@2,
                Pw,
                Ph,
                Frame,
                Period
            );

        {mandelbrot, Max_iter} ->
            draw_mandelbrot(Pixels, Pw, Ph, Max_iter)
    end.

-file("src/etui/widgets/scene.gleam", 59).
?DOC(" Render scene into `area`. `frame` drives animated shapes.\n").
-spec render(etui@buffer:buffer(), etui@geometry:rect(), scene(), integer()) -> etui@buffer:buffer().
render(Buf, Area, Scene, 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,
            Pixels = gleam@list:fold(
                erlang:element(2, Scene),
                etui@braille:new(),
                fun(Px_dict, Shape) ->
                    render_shape(Px_dict, Shape, Pw, Ph, Frame)
                end
            ),
            etui@braille:flush(Buf, Area, Pixels, erlang:element(3, Scene))
    end.