Skip to main content

src/svg_path@transform@serialize.erl

-module(svg_path@transform@serialize).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/svg_path/transform/serialize.gleam").
-export([default_options/0, decimal_options/1, fixed_decimal_options/1, force_matrix/1, to_string_with_options/2, to_string/1]).
-export_type([linear_transform/0, options/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.

?MODULEDOC(
    " SVG transform attribute serializer.\n"
    "\n"
    " This module serializes affine matrices as SVG transform attribute strings.\n"
    " It prefers readable transform functions such as `translate`, `scale`,\n"
    " `rotate`, and `skew` when a matrix clearly matches them, and falls back to\n"
    " `matrix(a b c d e f)` otherwise.\n"
).

-type linear_transform() :: matrix2x2 |
    identity2x2 |
    {scale2x2, float(), float()} |
    {skew_x2x2, float()} |
    {skew_y2x2, float()} |
    {rotate_scale2x2, float(), float(), float()}.

-type options() :: {options,
        gleam@option:option(integer()),
        boolean(),
        boolean()}.

-file("src/svg_path/transform/serialize.gleam", 35).
?DOC(
    " Default transform serialization options.\n"
    "\n"
    " Defaults to up to 5 decimal places, stripped trailing zeroes, and readable\n"
    " transform functions when possible.\n"
).
-spec default_options() -> options().
default_options() ->
    {options, {some, 5}, false, false}.

-file("src/svg_path/transform/serialize.gleam", 42).
?DOC(
    " Create options that round numbers to the given number of decimal places.\n"
    "\n"
    " Trailing zeroes are stripped. Negative decimal places are clamped to zero.\n"
).
-spec decimal_options(integer()) -> options().
decimal_options(Decimal_places) ->
    {options, {some, Decimal_places}, false, false}.

-file("src/svg_path/transform/serialize.gleam", 54).
?DOC(
    " Create options that round numbers and keep exactly the given number of\n"
    " decimal places.\n"
    "\n"
    " Negative decimal places are clamped to zero.\n"
).
-spec fixed_decimal_options(integer()) -> options().
fixed_decimal_options(Decimal_places) ->
    {options, {some, Decimal_places}, true, false}.

-file("src/svg_path/transform/serialize.gleam", 66).
?DOC(
    " Force serialization as `matrix(a b c d e f)`.\n"
    "\n"
    " This disables the nicer `translate`, `scale`, `rotate`, and `skew`\n"
    " representations.\n"
).
-spec force_matrix(options()) -> options().
force_matrix(Options) ->
    {options, erlang:element(2, Options), erlang:element(3, Options), true}.

-file("src/svg_path/transform/serialize.gleam", 246).
-spec transform_function(binary(), binary()) -> binary().
transform_function(Name, Arguments) ->
    <<<<<<Name/binary, "("/utf8>>/binary, Arguments/binary>>/binary, ")"/utf8>>.

-file("src/svg_path/transform/serialize.gleam", 328).
-spec strip_trailing_zeros(binary()) -> binary().
strip_trailing_zeros(String) ->
    case gleam_stdlib:string_ends_with(String, <<"0"/utf8>>) of
        true ->
            _pipe = String,
            _pipe@1 = gleam@string:drop_end(_pipe, 1),
            strip_trailing_zeros(_pipe@1);

        false ->
            String
    end.

-file("src/svg_path/transform/serialize.gleam", 314).
-spec strip_trailing_decimal_zeros(binary()) -> binary().
strip_trailing_decimal_zeros(Number) ->
    case gleam@string:split_once(Number, <<"."/utf8>>) of
        {error, _} ->
            Number;

        {ok, {Whole, Fractional}} ->
            Fractional@1 = strip_trailing_zeros(Fractional),
            case Fractional@1 of
                <<""/utf8>> ->
                    Whole;

                _ ->
                    <<<<Whole/binary, "."/utf8>>/binary, Fractional@1/binary>>
            end
    end.

-file("src/svg_path/transform/serialize.gleam", 343).
-spec power_of_ten_int(integer()) -> integer().
power_of_ten_int(Exponent) ->
    case Exponent =< 0 of
        true ->
            1;

        false ->
            10 * power_of_ten_int(Exponent - 1)
    end.

-file("src/svg_path/transform/serialize.gleam", 339).
-spec power_of_ten(integer()) -> float().
power_of_ten(Exponent) ->
    _pipe = power_of_ten_int(Exponent),
    erlang:float(_pipe).

-file("src/svg_path/transform/serialize.gleam", 289).
-spec fixed_decimal(float(), integer()) -> binary().
fixed_decimal(Number, Decimal_places) ->
    Decimal_places@1 = gleam@int:max(Decimal_places, 0),
    Scale = power_of_ten(Decimal_places@1),
    Scaled = begin
        _pipe = Number * Scale,
        erlang:round(_pipe)
    end,
    Sign = case Scaled < 0 of
        true ->
            <<"-"/utf8>>;

        false ->
            <<""/utf8>>
    end,
    Absolute_scaled = gleam@int:absolute_value(Scaled),
    case Decimal_places@1 of
        0 ->
            <<Sign/binary, (erlang:integer_to_binary(Absolute_scaled))/binary>>;

        _ ->
            Whole = case power_of_ten_int(Decimal_places@1) of
                0 -> 0;
                Gleam@denominator -> Absolute_scaled div Gleam@denominator
            end,
            Fractional = case power_of_ten_int(Decimal_places@1) of
                0 -> 0;
                Gleam@denominator@1 -> Absolute_scaled rem Gleam@denominator@1
            end,
            Fractional@1 = begin
                _pipe@1 = Fractional,
                _pipe@2 = erlang:integer_to_binary(_pipe@1),
                gleam@string:pad_start(_pipe@2, Decimal_places@1, <<"0"/utf8>>)
            end,
            <<<<<<Sign/binary, (erlang:integer_to_binary(Whole))/binary>>/binary,
                    "."/utf8>>/binary,
                Fractional@1/binary>>
    end.

-file("src/svg_path/transform/serialize.gleam", 280).
-spec decimal(float(), integer(), boolean()) -> binary().
decimal(Number, Decimal_places, Fixed_decimals) ->
    Fixed = fixed_decimal(Number, Decimal_places),
    case Fixed_decimals of
        true ->
            Fixed;

        false ->
            strip_trailing_decimal_zeros(Fixed)
    end.

-file("src/svg_path/transform/serialize.gleam", 272).
-spec number(float(), options()) -> binary().
number(Number, Options) ->
    case erlang:element(2, Options) of
        none ->
            gleam_stdlib:float_to_string(Number);

        {some, Decimal_places} ->
            decimal(Number, Decimal_places, erlang:element(3, Options))
    end.

-file("src/svg_path/transform/serialize.gleam", 93).
-spec translate_transform(float(), float(), options()) -> binary().
translate_transform(X, Y, Options) ->
    Arguments = case Y =:= +0.0 of
        true ->
            number(X, Options);

        false ->
            <<<<(number(X, Options))/binary, " "/utf8>>/binary,
                (number(Y, Options))/binary>>
    end,
    transform_function(<<"translate"/utf8>>, Arguments).

-file("src/svg_path/transform/serialize.gleam", 115).
-spec translate_optional_transform(float(), float(), options()) -> binary().
translate_optional_transform(X, Y, Options) ->
    case (X =:= +0.0) andalso (Y =:= +0.0) of
        true ->
            <<""/utf8>>;

        false ->
            translate_transform(X, Y, Options)
    end.

-file("src/svg_path/transform/serialize.gleam", 102).
-spec scale_transform(float(), float(), options()) -> binary().
scale_transform(X, Y, Options) ->
    Arguments = case X =:= Y of
        true ->
            number(X, Options);

        false ->
            <<<<(number(X, Options))/binary, " "/utf8>>/binary,
                (number(Y, Options))/binary>>
    end,
    transform_function(<<"scale"/utf8>>, Arguments).

-file("src/svg_path/transform/serialize.gleam", 268).
-spec close(float(), float()) -> boolean().
close(Left, Right) ->
    gleam@float:absolute_value(Left - Right) =< 0.000001.

-file("src/svg_path/transform/serialize.gleam", 206).
-spec scale_optional_transform(float(), float(), options()) -> binary().
scale_optional_transform(X, Y, Options) ->
    case close(X, 1.0) andalso close(Y, 1.0) of
        true ->
            <<""/utf8>>;

        false ->
            scale_transform(X, Y, Options)
    end.

-file("src/svg_path/transform/serialize.gleam", 111).
-spec rotate_transform(float(), options()) -> binary().
rotate_transform(Degrees, Options) ->
    transform_function(<<"rotate"/utf8>>, number(Degrees, Options)).

-file("src/svg_path/transform/serialize.gleam", 250).
-spec degrees_from_tangent(float()) -> float().
degrees_from_tangent(Tangent) ->
    case gleam_community@maths:pi() of
        +0.0 -> +0.0;
        -0.0 -> -0.0;
        Gleam@denominator -> gleam_community@maths:atan(Tangent) * 180.0 / Gleam@denominator
    end.

-file("src/svg_path/transform/serialize.gleam", 217).
-spec skew_y_transform(float(), options()) -> binary().
skew_y_transform(Tangent, Options) ->
    transform_function(
        <<"skewY"/utf8>>,
        number(degrees_from_tangent(Tangent), Options)
    ).

-file("src/svg_path/transform/serialize.gleam", 213).
-spec skew_x_transform(float(), options()) -> binary().
skew_x_transform(Tangent, Options) ->
    transform_function(
        <<"skewX"/utf8>>,
        number(degrees_from_tangent(Tangent), Options)
    ).

-file("src/svg_path/transform/serialize.gleam", 140).
-spec linear_transform(linear_transform(), options()) -> binary().
linear_transform(Linear, Options) ->
    case Linear of
        matrix2x2 ->
            <<""/utf8>>;

        identity2x2 ->
            <<""/utf8>>;

        {scale2x2, X, Y} ->
            scale_transform(X, Y, Options);

        {skew_x2x2, Tangent} ->
            skew_x_transform(Tangent, Options);

        {skew_y2x2, Tangent@1} ->
            skew_y_transform(Tangent@1, Options);

        {rotate_scale2x2, Degrees, Scale_x, Scale_y} ->
            <<(rotate_transform(Degrees, Options))/binary,
                (scale_optional_transform(Scale_x, Scale_y, Options))/binary>>
    end.

-file("src/svg_path/transform/serialize.gleam", 126).
-spec affine_transform(linear_transform(), float(), float(), options()) -> binary().
affine_transform(Linear, Translate_x, Translate_y, Options) ->
    case linear_transform(Linear, Options) of
        <<""/utf8>> ->
            translate_transform(Translate_x, Translate_y, Options);

        Transform ->
            <<(translate_optional_transform(Translate_x, Translate_y, Options))/binary,
                Transform/binary>>
    end.

-file("src/svg_path/transform/serialize.gleam", 221).
-spec matrix_transform(
    float(),
    float(),
    float(),
    float(),
    float(),
    float(),
    options()
) -> binary().
matrix_transform(A, B, C, D, E, F, Options) ->
    transform_function(
        <<"matrix"/utf8>>,
        <<<<<<<<<<<<<<<<<<<<(number(A, Options))/binary, " "/utf8>>/binary,
                                            (number(B, Options))/binary>>/binary,
                                        " "/utf8>>/binary,
                                    (number(C, Options))/binary>>/binary,
                                " "/utf8>>/binary,
                            (number(D, Options))/binary>>/binary,
                        " "/utf8>>/binary,
                    (number(E, Options))/binary>>/binary,
                " "/utf8>>/binary,
            (number(F, Options))/binary>>
    ).

-file("src/svg_path/transform/serialize.gleam", 254).
-spec radians_to_degrees(float()) -> float().
radians_to_degrees(Radians) ->
    case gleam_community@maths:pi() of
        +0.0 -> +0.0;
        -0.0 -> -0.0;
        Gleam@denominator -> Radians * 180.0 / Gleam@denominator
    end.

-file("src/svg_path/transform/serialize.gleam", 264).
-spec close_to_zero(float()) -> boolean().
close_to_zero(Value) ->
    gleam@float:absolute_value(Value) =< 0.000001.

-file("src/svg_path/transform/serialize.gleam", 258).
-spec length(float(), float()) -> float().
length(X, Y) ->
    Result@1 = case gleam@float:square_root((X * X) + (Y * Y)) of
        {ok, Result} -> Result;
        _assert_fail ->
            erlang:error(#{gleam_error => let_assert,
                        message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                        file => <<?FILEPATH/utf8>>,
                        module => <<"svg_path/transform/serialize"/utf8>>,
                        function => <<"length"/utf8>>,
                        line => 259,
                        value => _assert_fail,
                        start => 6826,
                        'end' => 6885,
                        pattern_start => 6837,
                        pattern_end => 6847})
    end,
    Result@1.

-file("src/svg_path/transform/serialize.gleam", 180).
-spec analyze_rotation_scale(float(), float(), float(), float()) -> linear_transform().
analyze_rotation_scale(A, B, C, D) ->
    Scale_x = length(A, B),
    Scale_y = length(C, D),
    Determinant = (A * D) - (B * C),
    Dot_product = (A * C) + (B * D),
    case (((Scale_x > 0.000001) andalso (Scale_y > 0.000001)) andalso (Determinant
    > 0.000001))
    andalso close_to_zero(Dot_product) of
        false ->
            matrix2x2;

        true ->
            Rotation_degrees = radians_to_degrees(
                gleam_community@maths:atan2(B, A)
            ),
            {rotate_scale2x2, Rotation_degrees, Scale_x, Scale_y}
    end.

-file("src/svg_path/transform/serialize.gleam", 153).
-spec analyze_linear_transform(float(), float(), float(), float()) -> linear_transform().
analyze_linear_transform(A, B, C, D) ->
    case (((A =:= 1.0) andalso (B =:= +0.0)) andalso (C =:= +0.0)) andalso (D
    =:= 1.0) of
        true ->
            identity2x2;

        false ->
            case (B =:= +0.0) andalso (C =:= +0.0) of
                true ->
                    {scale2x2, A, D};

                false ->
                    case ((A =:= 1.0) andalso (B =:= +0.0)) andalso (D =:= 1.0) of
                        true ->
                            {skew_x2x2, C};

                        false ->
                            case ((A =:= 1.0) andalso (C =:= +0.0)) andalso (D
                            =:= 1.0) of
                                true ->
                                    {skew_y2x2, B};

                                false ->
                                    analyze_rotation_scale(A, B, C, D)
                            end
                    end
            end
    end.

-file("src/svg_path/transform/serialize.gleam", 76).
?DOC(" Serialize a transform matrix with custom options.\n").
-spec to_string_with_options(svg_path@transform:matrix(), options()) -> binary().
to_string_with_options(Transform, Options) ->
    {A, B, C, D, E, F} = svg_path@transform:to_tuple(Transform),
    case erlang:element(4, Options) of
        true ->
            matrix_transform(A, B, C, D, E, F, Options);

        false ->
            case analyze_linear_transform(A, B, C, D) of
                matrix2x2 ->
                    matrix_transform(A, B, C, D, E, F, Options);

                Linear ->
                    affine_transform(Linear, E, F, Options)
            end
    end.

-file("src/svg_path/transform/serialize.gleam", 71).
?DOC(" Serialize a transform matrix with default options.\n").
-spec to_string(svg_path@transform:matrix()) -> binary().
to_string(Transform) ->
    to_string_with_options(Transform, default_options()).