-module(svg_path@ellipse).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/svg_path/ellipse.gleam").
-export([endpoint_arc_data/6, center_arc_data/5, ellipse_affine/6, point/2, transformed_axes/3, collapsed_arc_line/7, collapsed_arc_subpath/7, arc_end_angle/1, angle_at/2, split_arc_inside_many/2, arc_to_cubics/6, endpoint_to_center/1, point_at_angle/2, arc_sweep/1, arc_large_arc/1, center_to_endpoint/1, arc_point/2, derivative_at_angle/2, arc_derivative/2, arc_bounding_box/1, split_arc/2, split_arc_inside/2, split_arc_many/2]).
-export_type([point/0, bounding_box/0, endpoint_arc_data/0, center_arc_data/0, cubic/0, affine/0, error/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(
" Lower-level helpers for SVG elliptical arcs.\n"
"\n"
" Most users should work with `svg_path.Arc` values through the root module.\n"
" This module is the more technical layer for users who want the ellipse math\n"
" behind SVG arcs.\n"
"\n"
" SVG path data uses endpoint parameterization for elliptical arcs,\n"
" represented here by `EndpointArcData`. An `A` command stores the current\n"
" point, an end point, two radii, an `x_axis_rotation`, a `large_arc` flag,\n"
" and a `sweep` flag. The radii are the ellipse's semi-axes.\n"
" `x_axis_rotation` is the angle, in degrees, from the current coordinate\n"
" system's x-axis to the ellipse's x-axis. The `large_arc` flag selects an\n"
" arc spanning more than 180 degrees when it is `True`, and an arc spanning\n"
" at most 180 degrees when it is `False`. The `sweep` flag selects increasing\n"
" ellipse angles when it is `True`, and decreasing ellipse angles when it is\n"
" `False`.\n"
"\n"
" This endpoint form is compact and fits SVG paths nicely, but it is not the\n"
" most convenient form for evaluation, splitting, or analysis. SVG's\n"
" implementation notes also define center parameterization, represented here\n"
" by `CenterArcData`: an ellipse `center`, a corrected `radius`, the same\n"
" `x_axis_rotation`, a `start_angle`, and a signed `delta_angle`.\n"
"\n"
" `endpoint_to_center` converts `EndpointArcData` into `CenterArcData`. It\n"
" follows SVG's forgiving radius rules: radii are made\n"
" positive, and if the requested ellipse is too small to connect the\n"
" endpoints, both radii are scaled up uniformly until there is exactly one\n"
" solution. `CenterArcData.radius` is therefore the corrected radius, not\n"
" necessarily the input radius.\n"
"\n"
" Units are intentionally mixed to match their sources. `x_axis_rotation`\n"
" remains in degrees because SVG path data uses degrees. `start_angle` and\n"
" `delta_angle` are in radians because they are used with trigonometric\n"
" functions. `start_angle` is the angle before the ellipse is stretched and\n"
" rotated. `delta_angle` is the signed angular travel from the start point to\n"
" the end point.\n"
"\n"
" For values returned by `endpoint_to_center`, these invariants hold:\n"
"\n"
" - `arc_sweep(arc)` is `True` when `delta_angle >= 0.0`.\n"
" - `arc_large_arc(arc)` is `True` when `abs(delta_angle) > pi`.\n"
" - `arc_end_angle(arc) == arc.start_angle + arc.delta_angle`.\n"
" - `arc_point(arc, at: 0.0)` is the arc start point, modulo floating-point\n"
" roundoff.\n"
" - `arc_point(arc, at: 1.0)` is the arc end point, modulo floating-point\n"
" roundoff.\n"
" - `split_arc(arc, at: t)` preserves `center`, `radius`, and\n"
" `x_axis_rotation`, and divides `delta_angle` at angular progress `t`.\n"
"\n"
" The public `CenterArcData` constructor is intentionally available for\n"
" advanced callers. The invariants above are guaranteed for values produced by\n"
" `endpoint_to_center`; if you construct `CenterArcData` yourself, these\n"
" helpers will use the values you provide without trying to repair them.\n"
"\n"
" Evaluation with `arc_point(arc, at: t)` uses angular progress through\n"
" `CenterArcData`:\n"
"\n"
" ```gleam\n"
" angle = arc.start_angle +. t *. arc.delta_angle\n"
" ```\n"
"\n"
" This is not arc-length parameterization. Equal `t` steps correspond to\n"
" equal angle steps in the unstretched ellipse coordinate system, not equal\n"
" distances along the rendered curve. The `at` value is not clamped; values\n"
" outside `0.0..1.0` extrapolate along the same ellipse. `split_arc` follows\n"
" the same unclamped policy; use `split_arc_inside` when outside values should\n"
" return an error. `split_arc_many` and `split_arc_inside_many` sort their\n"
" split points, remove exact duplicates, and trim boundary `0.0` or `1.0`\n"
" split points that would only create zero-length boundary arcs.\n"
).
-type point() :: {point, float(), float()}.
-type bounding_box() :: {bounding_box, point(), point()}.
-type endpoint_arc_data() :: {endpoint_arc_data,
point(),
point(),
float(),
boolean(),
boolean(),
point()}.
-type center_arc_data() :: {center_arc_data,
point(),
point(),
float(),
float(),
float()}.
-type cubic() :: {cubic, point(), point(), point(), point()}.
-opaque affine() :: {affine,
float(),
float(),
float(),
float(),
float(),
float()}.
-type error() :: degenerate_input_arc |
not_collapsed_to_line |
split_outside_arc.
-file("src/svg_path/ellipse.gleam", 119).
?DOC(" Create endpoint arc data.\n").
-spec endpoint_arc_data(
point(),
point(),
float(),
boolean(),
boolean(),
point()
) -> endpoint_arc_data().
endpoint_arc_data(Start, Radius, X_axis_rotation, Large_arc, Sweep, End) ->
{endpoint_arc_data, Start, Radius, X_axis_rotation, Large_arc, Sweep, End}.
-file("src/svg_path/ellipse.gleam", 133).
?DOC(
" Create center arc data.\n"
"\n"
" This does not normalize or repair the given values.\n"
).
-spec center_arc_data(point(), point(), float(), float(), float()) -> center_arc_data().
center_arc_data(Center, Radius, X_axis_rotation, Start_angle, Delta_angle) ->
{center_arc_data, Center, Radius, X_axis_rotation, Start_angle, Delta_angle}.
-file("src/svg_path/ellipse.gleam", 169).
?DOC(" Create an affine matrix for ellipse helpers.\n").
-spec ellipse_affine(float(), float(), float(), float(), float(), float()) -> affine().
ellipse_affine(A, B, C, D, E, F) ->
{affine, A, B, C, D, E, F}.
-file("src/svg_path/ellipse.gleam", 181).
?DOC(" Transform a point by an affine matrix.\n").
-spec point(point(), affine()) -> point().
point(Point, Transform) ->
{point,
((erlang:element(2, Transform) * erlang:element(2, Point)) + (erlang:element(
4,
Transform
)
* erlang:element(3, Point)))
+ erlang:element(6, Transform),
((erlang:element(3, Transform) * erlang:element(2, Point)) + (erlang:element(
5,
Transform
)
* erlang:element(3, Point)))
+ erlang:element(7, Transform)}.
-file("src/svg_path/ellipse.gleam", 1064).
-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/ellipse.gleam", 1068).
-spec normalize_axis_rotation(float()) -> float().
normalize_axis_rotation(Degrees) ->
case Degrees < +0.0 of
true ->
normalize_axis_rotation(Degrees + 180.0);
false ->
case Degrees >= 180.0 of
true ->
normalize_axis_rotation(Degrees - 180.0);
false ->
Degrees
end
end.
-file("src/svg_path/ellipse.gleam", 1080).
-spec square_root(float()) -> float().
square_root(Value) ->
Root@1 = case gleam@float:square_root(Value) of
{ok, Root} -> Root;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"svg_path/ellipse"/utf8>>,
function => <<"square_root"/utf8>>,
line => 1081,
value => _assert_fail,
start => 32249,
'end' => 32295,
pattern_start => 32260,
pattern_end => 32268})
end,
Root@1.
-file("src/svg_path/ellipse.gleam", 1035).
-spec dot(point(), point()) -> float().
dot(A, B) ->
(erlang:element(2, A) * erlang:element(2, B)) + (erlang:element(3, A) * erlang:element(
3,
B
)).
-file("src/svg_path/ellipse.gleam", 1056).
-spec scale(point(), float()) -> point().
scale(Point, Factor) ->
{point,
erlang:element(2, Point) * Factor,
erlang:element(3, Point) * Factor}.
-file("src/svg_path/ellipse.gleam", 1043).
-spec length(point()) -> float().
length(Point) ->
square_root(
(erlang:element(2, Point) * erlang:element(2, Point)) + (erlang:element(
3,
Point
)
* erlang:element(3, Point))
).
-file("src/svg_path/ellipse.gleam", 1047).
-spec normalize(point()) -> point().
normalize(Point) ->
Point_length = length(Point),
case Point_length =< 0.000000001 of
true ->
{point, 1.0, +0.0};
false ->
scale(Point, case Point_length of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> 1.0 / Gleam@denominator
end)
end.
-file("src/svg_path/ellipse.gleam", 1008).
-spec eigenvector(float(), float(), float(), float()) -> point().
eigenvector(Sxx, Sxy, Syy, Lambda) ->
case gleam@float:absolute_value(Sxy) > 0.000000001 of
true ->
normalize({point, Sxy, Lambda - Sxx});
false ->
case Sxx >= Syy of
true ->
{point, 1.0, +0.0};
false ->
{point, +0.0, 1.0}
end
end.
-file("src/svg_path/ellipse.gleam", 965).
-spec extract_axes(point(), point()) -> {ok, {point(), float()}} |
{error, error()}.
extract_axes(X_axis, Y_axis) ->
Sxx = (erlang:element(2, X_axis) * erlang:element(2, X_axis)) + (erlang:element(
2,
Y_axis
)
* erlang:element(2, Y_axis)),
Sxy = (erlang:element(2, X_axis) * erlang:element(3, X_axis)) + (erlang:element(
2,
Y_axis
)
* erlang:element(3, Y_axis)),
Syy = (erlang:element(3, X_axis) * erlang:element(3, X_axis)) + (erlang:element(
3,
Y_axis
)
* erlang:element(3, Y_axis)),
Discriminant = square_root(
((Sxx - Syy) * (Sxx - Syy)) + ((4.0 * Sxy) * Sxy)
),
Lambda1 = ((Sxx + Syy) + Discriminant) / 2.0,
Lambda2 = ((Sxx + Syy) - Discriminant) / 2.0,
case (Lambda1 =< 0.000000001) orelse (Lambda2 =< 0.000000001) of
true ->
{error, degenerate_input_arc};
false ->
Axis1 = eigenvector(Sxx, Sxy, Syy, Lambda1),
Axis2 = {point,
+0.0 - erlang:element(3, Axis1),
erlang:element(2, Axis1)},
Choose_axis1 = gleam@float:absolute_value(dot(Axis1, X_axis)) >= gleam@float:absolute_value(
dot(Axis2, X_axis)
),
case Choose_axis1 of
true ->
{ok,
{{point, square_root(Lambda1), square_root(Lambda2)},
normalize_axis_rotation(
radians_to_degrees(
gleam_community@maths:atan2(
erlang:element(3, Axis1),
erlang:element(2, Axis1)
)
)
)}};
false ->
{ok,
{{point, square_root(Lambda2), square_root(Lambda1)},
normalize_axis_rotation(
radians_to_degrees(
gleam_community@maths:atan2(
erlang:element(3, Axis2),
erlang:element(2, Axis2)
)
)
)}}
end
end.
-file("src/svg_path/ellipse.gleam", 1028).
-spec linear_point(point(), affine()) -> point().
linear_point(Point, Transform) ->
{point,
(erlang:element(2, Transform) * erlang:element(2, Point)) + (erlang:element(
4,
Transform
)
* erlang:element(3, Point)),
(erlang:element(3, Transform) * erlang:element(2, Point)) + (erlang:element(
5,
Transform
)
* erlang:element(3, Point))}.
-file("src/svg_path/ellipse.gleam", 1060).
-spec degrees_to_radians(float()) -> float().
degrees_to_radians(Degrees) ->
(Degrees * gleam_community@maths:pi()) / 180.0.
-file("src/svg_path/ellipse.gleam", 943).
-spec arc_axes(point(), float()) -> {ok, {point(), point()}} | {error, error()}.
arc_axes(Radius, X_axis_rotation) ->
Rx = gleam@float:absolute_value(erlang:element(2, Radius)),
Ry = gleam@float:absolute_value(erlang:element(3, Radius)),
case (Rx =< 0.000000001) orelse (Ry =< 0.000000001) of
true ->
{error, degenerate_input_arc};
false ->
Phi = degrees_to_radians(X_axis_rotation),
Cos_phi = gleam_community@maths:cos(Phi),
Sin_phi = gleam_community@maths:sin(Phi),
{ok,
{{point, Rx * Cos_phi, Rx * Sin_phi},
{point, +0.0 - (Ry * Sin_phi), Ry * Cos_phi}}}
end.
-file("src/svg_path/ellipse.gleam", 191).
?DOC(
" Transform an arc's radius and x-axis rotation.\n"
"\n"
" Returns the new radius and x-axis rotation for the transformed ellipse.\n"
).
-spec transformed_axes(point(), float(), affine()) -> {ok, {point(), float()}} |
{error, error()}.
transformed_axes(Radius, X_axis_rotation, Transform) ->
case arc_axes(Radius, X_axis_rotation) of
{error, Error} ->
{error, Error};
{ok, {X_axis, Y_axis}} ->
X_axis@1 = linear_point(X_axis, Transform),
Y_axis@1 = linear_point(Y_axis, Transform),
extract_axes(X_axis@1, Y_axis@1)
end.
-file("src/svg_path/ellipse.gleam", 1024).
-spec offset(point(), point(), float()) -> point().
offset(Point, Direction, Distance) ->
{point,
erlang:element(2, Point) + (erlang:element(2, Direction) * Distance),
erlang:element(3, Point) + (erlang:element(3, Direction) * Distance)}.
-file("src/svg_path/ellipse.gleam", 929).
-spec positive_remainder(float()) -> float().
positive_remainder(Angle) ->
Turn = 2.0 * gleam_community@maths:pi(),
case Angle < +0.0 of
true ->
positive_remainder(Angle + Turn);
false ->
case Angle >= Turn of
true ->
positive_remainder(Angle - Turn);
false ->
Angle
end
end.
-file("src/svg_path/ellipse.gleam", 916).
-spec angle_in_sweep(float(), float(), float()) -> boolean().
angle_in_sweep(Angle, Start_angle, Delta_angle) ->
case Delta_angle >= +0.0 of
true ->
positive_remainder(Angle - Start_angle) =< (Delta_angle + 0.000000001);
false ->
positive_remainder(Start_angle - Angle) =< ((+0.0 - Delta_angle) + 0.000000001)
end.
-file("src/svg_path/ellipse.gleam", 834).
-spec collapsed_candidate_angles(float(), float(), float(), float()) -> list(float()).
collapsed_candidate_angles(Start_angle, Delta_angle, Alpha, Beta) ->
End_angle = Start_angle + Delta_angle,
Maximum_angle = gleam_community@maths:atan2(Beta, Alpha),
Minimum_angle = Maximum_angle + gleam_community@maths:pi(),
_pipe = [Minimum_angle, Maximum_angle],
_pipe@1 = gleam@list:filter(
_pipe,
fun(Angle) -> angle_in_sweep(Angle, Start_angle, Delta_angle) end
),
lists:append(_pipe@1, [Start_angle, End_angle]).
-file("src/svg_path/ellipse.gleam", 1039).
-spec cross(point(), point()) -> float().
cross(A, B) ->
(erlang:element(2, A) * erlang:element(3, B)) - (erlang:element(3, A) * erlang:element(
2,
B
)).
-file("src/svg_path/ellipse.gleam", 810).
-spec collapsed_axis(point(), point()) -> {ok, point()} | {error, error()}.
collapsed_axis(X_axis, Y_axis) ->
case gleam@float:absolute_value(cross(X_axis, Y_axis)) =< 0.000000001 of
false ->
{error, not_collapsed_to_line};
true ->
X_length = length(X_axis),
Y_length = length(Y_axis),
case (X_length > 0.000000001) orelse (Y_length > 0.000000001) of
true ->
case X_length >= Y_length of
true ->
{ok, scale(X_axis, case X_length of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> 1.0 / Gleam@denominator
end)};
false ->
{ok, scale(Y_axis, case Y_length of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> 1.0 / Gleam@denominator@1
end)}
end;
false ->
{error, not_collapsed_to_line}
end
end.
-file("src/svg_path/ellipse.gleam", 830).
-spec fully_collapsed(point(), point()) -> boolean().
fully_collapsed(X_axis, Y_axis) ->
(length(X_axis) =< 0.000000001) andalso (length(Y_axis) =< 0.000000001).
-file("src/svg_path/ellipse.gleam", 1020).
-spec vector_angle(point(), point()) -> float().
vector_angle(A, B) ->
gleam_community@maths:atan2(cross(A, B), dot(A, B)).
-file("src/svg_path/ellipse.gleam", 787).
-spec swept_delta_angle(point(), point(), boolean()) -> float().
swept_delta_angle(Start_vector, End_vector, Sweep) ->
Delta_angle = vector_angle(Start_vector, End_vector),
case Sweep of
true ->
case Delta_angle < +0.0 of
true ->
Delta_angle + (2.0 * gleam_community@maths:pi());
false ->
Delta_angle
end;
false ->
case Delta_angle > +0.0 of
true ->
Delta_angle - (2.0 * gleam_community@maths:pi());
false ->
Delta_angle
end
end.
-file("src/svg_path/ellipse.gleam", 766).
-spec center_prime(float(), float(), float(), float(), boolean(), boolean()) -> point().
center_prime(Rx, Ry, X1p, Y1p, Large_arc, Sweep) ->
Numerator = ((((Rx * Rx) * Ry) * Ry) - (((Rx * Rx) * Y1p) * Y1p)) - (((Ry * Ry)
* X1p)
* X1p),
Denominator = (((Rx * Rx) * Y1p) * Y1p) + (((Ry * Ry) * X1p) * X1p),
Sign = case Large_arc =:= Sweep of
true ->
-1.0;
false ->
1.0
end,
Coefficient = Sign * square_root(gleam@float:max(+0.0, case Denominator of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Numerator / Gleam@denominator
end)),
{point, case Ry of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> (Coefficient * Rx) * Y1p / Gleam@denominator@1
end, +0.0 - (case Rx of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@2 -> (Coefficient * Ry) * X1p / Gleam@denominator@2
end)}.
-file("src/svg_path/ellipse.gleam", 711).
-spec do_endpoint_to_center(
point(),
point(),
float(),
boolean(),
boolean(),
point()
) -> {ok, center_arc_data()} | {error, error()}.
do_endpoint_to_center(Start, Radius, X_axis_rotation, Large_arc, Sweep, End) ->
Rx = gleam@float:absolute_value(erlang:element(2, Radius)),
Ry = gleam@float:absolute_value(erlang:element(3, Radius)),
case (Rx =< 0.000000001) orelse (Ry =< 0.000000001) of
true ->
{error, degenerate_input_arc};
false ->
Phi = degrees_to_radians(X_axis_rotation),
Cos_phi = gleam_community@maths:cos(Phi),
Sin_phi = gleam_community@maths:sin(Phi),
Midpoint = {point,
(erlang:element(2, Start) + erlang:element(2, End)) / 2.0,
(erlang:element(3, Start) + erlang:element(3, End)) / 2.0},
Half_delta = {point,
(erlang:element(2, Start) - erlang:element(2, End)) / 2.0,
(erlang:element(3, Start) - erlang:element(3, End)) / 2.0},
X1p = (Cos_phi * erlang:element(2, Half_delta)) + (Sin_phi * erlang:element(
3,
Half_delta
)),
Y1p = (+0.0 - (Sin_phi * erlang:element(2, Half_delta))) + (Cos_phi
* erlang:element(3, Half_delta)),
Radius_scale = gleam@float:max(1.0, (case (Rx * Rx) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> X1p * X1p / Gleam@denominator
end) + (case (Ry * Ry) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> Y1p * Y1p / Gleam@denominator@1
end)),
Scale = square_root(Radius_scale),
Rx@1 = Rx * Scale,
Ry@1 = Ry * Scale,
Center_prime = center_prime(Rx@1, Ry@1, X1p, Y1p, Large_arc, Sweep),
Center = {point,
((Cos_phi * erlang:element(2, Center_prime)) - (Sin_phi * erlang:element(
3,
Center_prime
)))
+ erlang:element(2, Midpoint),
((Sin_phi * erlang:element(2, Center_prime)) + (Cos_phi * erlang:element(
3,
Center_prime
)))
+ erlang:element(3, Midpoint)},
Start_vector = {point, case Rx@1 of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@2 -> (X1p - erlang:element(
2,
Center_prime
))
/ Gleam@denominator@2
end, case Ry@1 of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@3 -> (Y1p - erlang:element(
3,
Center_prime
))
/ Gleam@denominator@3
end},
End_vector = {point, case Rx@1 of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@4 -> ((+0.0 - X1p) - erlang:element(
2,
Center_prime
))
/ Gleam@denominator@4
end, case Ry@1 of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@5 -> ((+0.0 - Y1p) - erlang:element(
3,
Center_prime
))
/ Gleam@denominator@5
end},
Start_angle = vector_angle({point, 1.0, +0.0}, Start_vector),
Delta_angle = swept_delta_angle(Start_vector, End_vector, Sweep),
{ok,
{center_arc_data,
Center,
{point, Rx@1, Ry@1},
X_axis_rotation,
Start_angle,
Delta_angle}}
end.
-file("src/svg_path/ellipse.gleam", 211).
?DOC(
" Convert an arc collapsed by an affine transform into a single line segment.\n"
"\n"
" If the collapsed arc's extrema require more than one segment to preserve its\n"
" out-and-back motion, use `collapsed_arc_subpath`.\n"
).
-spec collapsed_arc_line(
point(),
point(),
float(),
boolean(),
boolean(),
point(),
affine()
) -> {ok, {point(), point()}} | {error, error()}.
collapsed_arc_line(
Start,
Radius,
X_axis_rotation,
Large_arc,
Sweep,
End,
Transform
) ->
case do_endpoint_to_center(
Start,
Radius,
X_axis_rotation,
Large_arc,
Sweep,
End
) of
{error, Error} ->
{error, Error};
{ok, Arc} ->
{X_axis@1, Y_axis@1} = case arc_axes(
erlang:element(3, Arc),
erlang:element(4, Arc)
) of
{ok, {X_axis, Y_axis}} -> {X_axis, Y_axis};
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"svg_path/ellipse"/utf8>>,
function => <<"collapsed_arc_line"/utf8>>,
line => 225,
value => _assert_fail,
start => 8142,
'end' => 8226,
pattern_start => 8153,
pattern_end => 8174})
end,
X_axis@2 = linear_point(X_axis@1, Transform),
Y_axis@2 = linear_point(Y_axis@1, Transform),
case fully_collapsed(X_axis@2, Y_axis@2) of
true ->
{ok, {point(Start, Transform), point(End, Transform)}};
false ->
case collapsed_axis(X_axis@2, Y_axis@2) of
{error, Error@1} ->
{error, Error@1};
{ok, Axis} ->
Center = point(erlang:element(2, Arc), Transform),
Alpha = dot(X_axis@2, Axis),
Beta = dot(Y_axis@2, Axis),
Angles = collapsed_candidate_angles(
erlang:element(5, Arc),
erlang:element(6, Arc),
Alpha,
Beta
),
Scalars = gleam@list:map(
Angles,
fun(Angle) ->
(Alpha * gleam_community@maths:cos(Angle)) + (Beta
* gleam_community@maths:sin(Angle))
end
),
First@1 = case gleam@list:first(Scalars) of
{ok, First} -> First;
_assert_fail@1 ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"svg_path/ellipse"/utf8>>,
function => <<"collapsed_arc_line"/utf8>>,
line => 250,
value => _assert_fail@1,
start => 9120,
'end' => 9162,
pattern_start => 9131,
pattern_end => 9140})
end,
{Low@1, High@1} = gleam@list:fold(
Scalars,
{First@1, First@1},
fun(Bounds, Scalar) ->
{Low, High} = Bounds,
{gleam@float:min(Low, Scalar),
gleam@float:max(High, Scalar)}
end
),
{ok,
{offset(Center, Axis, Low@1),
offset(Center, Axis, High@1)}}
end
end
end.
-file("src/svg_path/ellipse.gleam", 887).
-spec insert_angle({float(), float()}, list({float(), float()})) -> list({float(),
float()}).
insert_angle(Angle, Sorted) ->
case Sorted of
[] ->
[Angle];
[First | Rest] ->
{Progress, _} = Angle,
{First_progress, _} = First,
case Progress =< First_progress of
true ->
[Angle | Sorted];
false ->
[First | insert_angle(Angle, Rest)]
end
end.
-file("src/svg_path/ellipse.gleam", 877).
-spec insert_sort_angles(list({float(), float()}), list({float(), float()})) -> list({float(),
float()}).
insert_sort_angles(Angles, Sorted) ->
case Angles of
[] ->
Sorted;
[First | Rest] ->
insert_sort_angles(Rest, insert_angle(First, Sorted))
end.
-file("src/svg_path/ellipse.gleam", 905).
-spec angle_progress(float(), float(), float()) -> float().
angle_progress(Angle, Start_angle, Delta_angle) ->
case Delta_angle >= +0.0 of
true ->
positive_remainder(Angle - Start_angle);
false ->
positive_remainder(Start_angle - Angle)
end.
-file("src/svg_path/ellipse.gleam", 849).
-spec collapsed_ordered_angles(float(), float(), float(), float()) -> list(float()).
collapsed_ordered_angles(Start_angle, Delta_angle, Alpha, Beta) ->
End_angle = Start_angle + Delta_angle,
Maximum_angle = gleam_community@maths:atan2(Beta, Alpha),
Minimum_angle = Maximum_angle + gleam_community@maths:pi(),
Interior_extrema = begin
_pipe = [Minimum_angle, Maximum_angle],
_pipe@1 = gleam@list:filter(
_pipe,
fun(Angle) ->
Progress = angle_progress(Angle, Start_angle, Delta_angle),
(Progress > 0.000000001) andalso (Progress < (gleam@float:absolute_value(
Delta_angle
)
- 0.000000001))
end
),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(Angle@1) ->
{angle_progress(Angle@1, Start_angle, Delta_angle), Angle@1}
end
),
_pipe@3 = insert_sort_angles(_pipe@2, []),
gleam@list:map(
_pipe@3,
fun(Pair) ->
{_, Angle@2} = Pair,
Angle@2
end
)
end,
[Start_angle | lists:append(Interior_extrema, [End_angle])].
-file("src/svg_path/ellipse.gleam", 551).
-spec collapsed_arc_points(
point(),
point(),
float(),
boolean(),
boolean(),
point(),
affine()
) -> {ok, list(point())} | {error, error()}.
collapsed_arc_points(
Start,
Radius,
X_axis_rotation,
Large_arc,
Sweep,
End,
Transform
) ->
case do_endpoint_to_center(
Start,
Radius,
X_axis_rotation,
Large_arc,
Sweep,
End
) of
{error, Error} ->
{error, Error};
{ok, Arc} ->
{X_axis@1, Y_axis@1} = case arc_axes(
erlang:element(3, Arc),
erlang:element(4, Arc)
) of
{ok, {X_axis, Y_axis}} -> {X_axis, Y_axis};
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"svg_path/ellipse"/utf8>>,
function => <<"collapsed_arc_points"/utf8>>,
line => 565,
value => _assert_fail,
start => 18212,
'end' => 18296,
pattern_start => 18223,
pattern_end => 18244})
end,
X_axis@2 = linear_point(X_axis@1, Transform),
Y_axis@2 = linear_point(Y_axis@1, Transform),
case fully_collapsed(X_axis@2, Y_axis@2) of
true ->
{ok, [point(Start, Transform), point(End, Transform)]};
false ->
case collapsed_axis(X_axis@2, Y_axis@2) of
{error, Error@1} ->
{error, Error@1};
{ok, Axis} ->
Center = point(erlang:element(2, Arc), Transform),
Alpha = dot(X_axis@2, Axis),
Beta = dot(Y_axis@2, Axis),
Angles = collapsed_ordered_angles(
erlang:element(5, Arc),
erlang:element(6, Arc),
Alpha,
Beta
),
{ok,
gleam@list:map(
Angles,
fun(Angle) ->
offset(
Center,
Axis,
(Alpha * gleam_community@maths:cos(
Angle
))
+ (Beta * gleam_community@maths:sin(
Angle
))
)
end
)}
end
end
end.
-file("src/svg_path/ellipse.gleam", 267).
?DOC(" Convert an arc collapsed by an affine transform into a line-based subpath.\n").
-spec collapsed_arc_subpath(
point(),
point(),
float(),
boolean(),
boolean(),
point(),
affine()
) -> {ok, list(point())} | {error, error()}.
collapsed_arc_subpath(
Start,
Radius,
X_axis_rotation,
Large_arc,
Sweep,
End,
Transform
) ->
collapsed_arc_points(
Start,
Radius,
X_axis_rotation,
Large_arc,
Sweep,
End,
Transform
).
-file("src/svg_path/ellipse.gleam", 701).
-spec ellipse_derivative(center_arc_data(), float()) -> point().
ellipse_derivative(Arc, Angle) ->
Phi = degrees_to_radians(erlang:element(4, Arc)),
Cos_phi = gleam_community@maths:cos(Phi),
Sin_phi = gleam_community@maths:sin(Phi),
X = +0.0 - (erlang:element(2, erlang:element(3, Arc)) * gleam_community@maths:sin(
Angle
)),
Y = erlang:element(3, erlang:element(3, Arc)) * gleam_community@maths:cos(
Angle
),
{point, (Cos_phi * X) - (Sin_phi * Y), (Sin_phi * X) + (Cos_phi * Y)}.
-file("src/svg_path/ellipse.gleam", 686).
-spec ellipse_point(center_arc_data(), float()) -> point().
ellipse_point(Arc, Angle) ->
Phi = degrees_to_radians(erlang:element(4, Arc)),
Cos_phi = gleam_community@maths:cos(Phi),
Sin_phi = gleam_community@maths:sin(Phi),
Cos_angle = gleam_community@maths:cos(Angle),
Sin_angle = gleam_community@maths:sin(Angle),
X = erlang:element(2, erlang:element(3, Arc)) * Cos_angle,
Y = erlang:element(3, erlang:element(3, Arc)) * Sin_angle,
{point,
(erlang:element(2, erlang:element(2, Arc)) + (Cos_phi * X)) - (Sin_phi * Y),
(erlang:element(3, erlang:element(2, Arc)) + (Sin_phi * X)) + (Cos_phi * Y)}.
-file("src/svg_path/ellipse.gleam", 446).
?DOC(" Return the arc's end angle in radians.\n").
-spec arc_end_angle(center_arc_data()) -> float().
arc_end_angle(Arc) ->
erlang:element(5, Arc) + erlang:element(6, Arc).
-file("src/svg_path/ellipse.gleam", 634).
-spec cubic_for_arc(center_arc_data()) -> cubic().
cubic_for_arc(Arc) ->
Start_angle = erlang:element(5, Arc),
End_angle = arc_end_angle(Arc),
Delta = erlang:element(6, Arc),
Alpha = (4.0 / 3.0) * gleam_community@maths:tan(Delta / 4.0),
Start = ellipse_point(Arc, Start_angle),
End = ellipse_point(Arc, End_angle),
Start_tangent = ellipse_derivative(Arc, Start_angle),
End_tangent = ellipse_derivative(Arc, End_angle),
{cubic,
Start,
offset(Start, Start_tangent, Alpha),
offset(End, End_tangent, +0.0 - Alpha),
End}.
-file("src/svg_path/ellipse.gleam", 619).
-spec cubic_split_progresses_from(float(), float(), list(float())) -> list(float()).
cubic_split_progresses_from(Next, Step, Points) ->
case Next >= (1.0 - 0.000000001) of
true ->
lists:reverse(Points);
false ->
cubic_split_progresses_from(Next + Step, Step, [Next | Points])
end.
-file("src/svg_path/ellipse.gleam", 604).
-spec cubic_split_progresses(center_arc_data()) -> list(float()).
cubic_split_progresses(Arc) ->
Quarter_turn = gleam_community@maths:pi() / 2.0,
Delta = gleam@float:absolute_value(erlang:element(6, Arc)),
case Delta =< (Quarter_turn + 0.000000001) of
true ->
[];
false ->
cubic_split_progresses_from(case Delta of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Quarter_turn / Gleam@denominator
end, case Delta of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> Quarter_turn / Gleam@denominator@1
end, [])
end.
-file("src/svg_path/ellipse.gleam", 441).
?DOC(" Return the angle at `t` using this module's angular-progress parameterization.\n").
-spec angle_at(center_arc_data(), float()) -> float().
angle_at(Arc, T) ->
erlang:element(5, Arc) + (T * erlang:element(6, Arc)).
-file("src/svg_path/ellipse.gleam", 460).
-spec arc_between(center_arc_data(), float(), float()) -> center_arc_data().
arc_between(Arc, From, To) ->
{center_arc_data,
erlang:element(2, Arc),
erlang:element(3, Arc),
erlang:element(4, Arc),
angle_at(Arc, From),
erlang:element(6, Arc) * (To - From)}.
-file("src/svg_path/ellipse.gleam", 481).
-spec split_arc_between_progresses(
center_arc_data(),
float(),
list(float()),
list(center_arc_data())
) -> list(center_arc_data()).
split_arc_between_progresses(Arc, Previous, Points, Pieces) ->
case Points of
[] ->
lists:reverse([arc_between(Arc, Previous, 1.0) | Pieces]);
[Next | Rest] ->
split_arc_between_progresses(
Arc,
Next,
Rest,
[arc_between(Arc, Previous, Next) | Pieces]
)
end.
-file("src/svg_path/ellipse.gleam", 474).
-spec split_arc_at_progresses(center_arc_data(), list(float())) -> list(center_arc_data()).
split_arc_at_progresses(Arc, Points) ->
split_arc_between_progresses(Arc, +0.0, Points, []).
-file("src/svg_path/ellipse.gleam", 527).
-spec trim_reversed_end_progress(list(float())) -> list(float()).
trim_reversed_end_progress(Points) ->
case Points of
[1.0 | Rest] ->
trim_reversed_end_progress(Rest);
_ ->
Points
end.
-file("src/svg_path/ellipse.gleam", 520).
-spec trim_end_progress(list(float())) -> list(float()).
trim_end_progress(Points) ->
_pipe = Points,
_pipe@1 = lists:reverse(_pipe),
_pipe@2 = trim_reversed_end_progress(_pipe@1),
lists:reverse(_pipe@2).
-file("src/svg_path/ellipse.gleam", 513).
-spec trim_start_progress(list(float())) -> list(float()).
trim_start_progress(Points) ->
case Points of
[+0.0 | Rest] ->
trim_start_progress(Rest);
_ ->
Points
end.
-file("src/svg_path/ellipse.gleam", 534).
-spec insert_unique_progress(list(float()), float()) -> list(float()).
insert_unique_progress(Sorted, Point) ->
case Sorted of
[] ->
[Point];
[First | Rest] ->
case Point =:= First of
true ->
Sorted;
false ->
case Point =< First of
true ->
[Point | Sorted];
false ->
[First | insert_unique_progress(Rest, Point)]
end
end
end.
-file("src/svg_path/ellipse.gleam", 505).
-spec sort_unique_progresses(list(float())) -> list(float()).
sort_unique_progresses(Points) ->
case Points of
[] ->
[];
[First | Rest] ->
_pipe = sort_unique_progresses(Rest),
insert_unique_progress(_pipe, First)
end.
-file("src/svg_path/ellipse.gleam", 498).
-spec normalized_progresses(list(float())) -> list(float()).
normalized_progresses(Points) ->
_pipe = Points,
_pipe@1 = sort_unique_progresses(_pipe),
_pipe@2 = trim_start_progress(_pipe@1),
trim_end_progress(_pipe@2).
-file("src/svg_path/ellipse.gleam", 418).
?DOC(
" Split an arc at multiple angular progress values, erroring outside `0.0..1.0`.\n"
"\n"
" Split points are sorted, exact duplicates are removed, and boundary `0.0`\n"
" or `1.0` split points are trimmed when they would only create zero-length\n"
" boundary arcs. Values exactly at `0.0` or `1.0` are accepted.\n"
).
-spec split_arc_inside_many(center_arc_data(), list(float())) -> {ok,
list(center_arc_data())} |
{error, error()}.
split_arc_inside_many(Arc, Points) ->
Points@1 = normalized_progresses(Points),
case gleam@list:any(Points@1, fun(T) -> (T < +0.0) orelse (T > 1.0) end) of
true ->
{error, split_outside_arc};
false ->
{ok, split_arc_at_progresses(Arc, Points@1)}
end.
-file("src/svg_path/ellipse.gleam", 292).
?DOC(
" Convert an elliptical arc to one or more cubic Bezier curves.\n"
"\n"
" The arc is split into chunks of at most a quarter turn. This is the common\n"
" deterministic SVG arc approximation strategy. This function does not accept\n"
" a tolerance; use a higher-level helper if you want SVG path segments back.\n"
).
-spec arc_to_cubics(point(), point(), float(), boolean(), boolean(), point()) -> {ok,
list(cubic())} |
{error, error()}.
arc_to_cubics(Start, Radius, X_axis_rotation, Large_arc, Sweep, End) ->
case do_endpoint_to_center(
Start,
Radius,
X_axis_rotation,
Large_arc,
Sweep,
End
) of
{error, Error} ->
{error, Error};
{ok, Arc} ->
case split_arc_inside_many(Arc, cubic_split_progresses(Arc)) of
{error, Error@1} ->
{error, Error@1};
{ok, Chunks} ->
{ok, gleam@list:map(Chunks, fun cubic_for_arc/1)}
end
end.
-file("src/svg_path/ellipse.gleam", 318).
?DOC(
" Convert endpoint arc data to center parameterization.\n"
"\n"
" Radii are corrected according to SVG's implementation notes: negative radii\n"
" are made positive, and radii that are too small to reach between the\n"
" endpoints are scaled up uniformly.\n"
).
-spec endpoint_to_center(endpoint_arc_data()) -> {ok, center_arc_data()} |
{error, error()}.
endpoint_to_center(Data) ->
do_endpoint_to_center(
erlang:element(2, Data),
erlang:element(3, Data),
erlang:element(4, Data),
erlang:element(5, Data),
erlang:element(6, Data),
erlang:element(7, Data)
).
-file("src/svg_path/ellipse.gleam", 431).
?DOC(" Evaluate an arc at a center-parameter angle in radians.\n").
-spec point_at_angle(center_arc_data(), float()) -> point().
point_at_angle(Arc, Angle) ->
ellipse_point(Arc, Angle).
-file("src/svg_path/ellipse.gleam", 456).
?DOC(" Return whether the arc sweeps through increasing center-parameter angles.\n").
-spec arc_sweep(center_arc_data()) -> boolean().
arc_sweep(Arc) ->
erlang:element(6, Arc) >= +0.0.
-file("src/svg_path/ellipse.gleam", 451).
?DOC(" Return whether the arc spans more than 180 degrees.\n").
-spec arc_large_arc(center_arc_data()) -> boolean().
arc_large_arc(Arc) ->
gleam@float:absolute_value(erlang:element(6, Arc)) > gleam_community@maths:pi(
).
-file("src/svg_path/ellipse.gleam", 335).
?DOC(
" Convert center arc data back to endpoint arc data.\n"
"\n"
" The returned endpoint data uses corrected radii from the center form, and\n"
" derives `large_arc` and `sweep` from `delta_angle`.\n"
).
-spec center_to_endpoint(center_arc_data()) -> endpoint_arc_data().
center_to_endpoint(Data) ->
{endpoint_arc_data,
point_at_angle(Data, erlang:element(5, Data)),
erlang:element(3, Data),
erlang:element(4, Data),
arc_large_arc(Data),
arc_sweep(Data),
point_at_angle(Data, arc_end_angle(Data))}.
-file("src/svg_path/ellipse.gleam", 351).
?DOC(
" Evaluate an arc at angular progress `t`.\n"
"\n"
" `t` is not clamped. `0.0` evaluates the start of the arc, `1.0` evaluates\n"
" the end of the arc, and values outside that range extrapolate along the\n"
" same ellipse.\n"
).
-spec arc_point(center_arc_data(), float()) -> point().
arc_point(Arc, T) ->
point_at_angle(Arc, angle_at(Arc, T)).
-file("src/svg_path/ellipse.gleam", 436).
?DOC(" Return the derivative with respect to the center-parameter angle.\n").
-spec derivative_at_angle(center_arc_data(), float()) -> point().
derivative_at_angle(Arc, Angle) ->
ellipse_derivative(Arc, Angle).
-file("src/svg_path/ellipse.gleam", 360).
?DOC(
" Return the derivative with respect to angular progress `t`.\n"
"\n"
" This is the tangent direction followed from the arc start to the arc end.\n"
" For the raw derivative with respect to the ellipse angle, use\n"
" `derivative_at_angle`.\n"
).
-spec arc_derivative(center_arc_data(), float()) -> point().
arc_derivative(Arc, T) ->
scale(derivative_at_angle(Arc, angle_at(Arc, T)), erlang:element(6, Arc)).
-file("src/svg_path/ellipse.gleam", 679).
-spec include_point(bounding_box(), point()) -> bounding_box().
include_point(Box, Point) ->
{bounding_box,
{point,
gleam@float:min(
erlang:element(2, erlang:element(2, Box)),
erlang:element(2, Point)
),
gleam@float:min(
erlang:element(3, erlang:element(2, Box)),
erlang:element(3, Point)
)},
{point,
gleam@float:max(
erlang:element(2, erlang:element(3, Box)),
erlang:element(2, Point)
),
gleam@float:max(
erlang:element(3, erlang:element(3, Box)),
erlang:element(3, Point)
)}}.
-file("src/svg_path/ellipse.gleam", 671).
-spec start_angle_extremum(float(), float()) -> float().
start_angle_extremum(Alpha, Beta) ->
gleam_community@maths:atan2(Beta, Alpha).
-file("src/svg_path/ellipse.gleam", 675).
-spec opposite_angle_extremum(float(), float()) -> float().
opposite_angle_extremum(Alpha, Beta) ->
start_angle_extremum(Alpha, Beta) + gleam_community@maths:pi().
-file("src/svg_path/ellipse.gleam", 652).
-spec arc_bounding_box_candidate_angles(center_arc_data()) -> list(float()).
arc_bounding_box_candidate_angles(Arc) ->
Phi = degrees_to_radians(erlang:element(4, Arc)),
X_alpha = erlang:element(2, erlang:element(3, Arc)) * gleam_community@maths:cos(
Phi
),
X_beta = +0.0 - (erlang:element(3, erlang:element(3, Arc)) * gleam_community@maths:sin(
Phi
)),
Y_alpha = erlang:element(2, erlang:element(3, Arc)) * gleam_community@maths:sin(
Phi
),
Y_beta = erlang:element(3, erlang:element(3, Arc)) * gleam_community@maths:cos(
Phi
),
_pipe = [start_angle_extremum(X_alpha, X_beta),
opposite_angle_extremum(X_alpha, X_beta),
start_angle_extremum(Y_alpha, Y_beta),
opposite_angle_extremum(Y_alpha, Y_beta)],
_pipe@1 = gleam@list:filter(
_pipe,
fun(Angle) ->
angle_in_sweep(
Angle,
erlang:element(5, Arc),
erlang:element(6, Arc)
)
end
),
lists:append(_pipe@1, [erlang:element(5, Arc), arc_end_angle(Arc)]).
-file("src/svg_path/ellipse.gleam", 365).
?DOC(" Return the arc's exact axis-aligned bounding box.\n").
-spec arc_bounding_box(center_arc_data()) -> bounding_box().
arc_bounding_box(Arc) ->
Points = begin
_pipe = arc_bounding_box_candidate_angles(Arc),
gleam@list:map(_pipe, fun(Angle) -> point_at_angle(Arc, Angle) end)
end,
{First@1, Rest@1} = case Points of
[First | Rest] -> {First, Rest};
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"svg_path/ellipse"/utf8>>,
function => <<"arc_bounding_box"/utf8>>,
line => 369,
value => _assert_fail,
start => 12794,
'end' => 12829,
pattern_start => 12805,
pattern_end => 12820})
end,
_pipe@1 = Rest@1,
gleam@list:fold(
_pipe@1,
{bounding_box, First@1, First@1},
fun include_point/2
).
-file("src/svg_path/ellipse.gleam", 379).
?DOC(
" Split an arc at angular progress `t`.\n"
"\n"
" `t` is not clamped. Values outside `0.0..1.0` extrapolate along the same\n"
" ellipse, matching `arc_point`.\n"
).
-spec split_arc(center_arc_data(), float()) -> {center_arc_data(),
center_arc_data()}.
split_arc(Arc, T) ->
{arc_between(Arc, +0.0, T), arc_between(Arc, T, 1.0)}.
-file("src/svg_path/ellipse.gleam", 390).
?DOC(
" Split an arc at angular progress `t`, returning an error outside `0.0..1.0`.\n"
"\n"
" Values exactly at `0.0` or `1.0` are accepted and produce one zero-length\n"
" arc.\n"
).
-spec split_arc_inside(center_arc_data(), float()) -> {ok,
{center_arc_data(), center_arc_data()}} |
{error, error()}.
split_arc_inside(Arc, T) ->
case (T < +0.0) orelse (T > 1.0) of
true ->
{error, split_outside_arc};
false ->
{ok, split_arc(Arc, T)}
end.
-file("src/svg_path/ellipse.gleam", 406).
?DOC(
" Split an arc at multiple angular progress values.\n"
"\n"
" Split points are sorted, exact duplicates are removed, and boundary `0.0`\n"
" or `1.0` split points are trimmed when they would only create zero-length\n"
" boundary arcs. Values outside `0.0..1.0` are allowed and extrapolate along\n"
" the same ellipse, matching `split_arc`.\n"
).
-spec split_arc_many(center_arc_data(), list(float())) -> list(center_arc_data()).
split_arc_many(Arc, Points) ->
split_arc_at_progresses(Arc, normalized_progresses(Points)).