From 8e8defa0ef41fec68e932cdf777ad211ff1955df Mon Sep 17 00:00:00 2001 From: MathItYT Date: Sun, 10 Dec 2023 16:43:25 -0300 Subject: [PATCH 1/8] Added cap_style feature to VMobject --- manim/camera/camera.py | 21 +++++++++--- manim/constants.py | 39 +++++++++++++++++++++++ manim/mobject/types/vectorized_mobject.py | 20 ++++++++---- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index ea42a5c085..f36a4199a4 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -29,6 +29,7 @@ from ..utils.iterables import list_difference_update from ..utils.space_ops import angle_of_vector + LINE_JOIN_MAP = { LineJointType.AUTO: None, # TODO: this could be improved LineJointType.ROUND: cairo.LineJoin.ROUND, @@ -37,6 +38,14 @@ } +CAP_STYLE_MAP = { + CapStyleType.AUTO: None, # TODO: this could be improved + CapStyleType.ROUND: cairo.LineCap.ROUND, + CapStyleType.BUTT: cairo.LineCap.BUTT, + CapStyleType.SQUARE: cairo.LineCap.SQUARE +} + + class Camera: """Base camera class. @@ -326,8 +335,8 @@ def set_pixel_array( """ converted_array = self.convert_pixel_array(pixel_array, convert_from_floats) if not ( - hasattr(self, "pixel_array") - and self.pixel_array.shape == converted_array.shape + hasattr(self, "pixel_array") and + self.pixel_array.shape == converted_array.shape ): self.pixel_array = converted_array else: @@ -776,13 +785,15 @@ def apply_stroke( vmobject, ) ctx.set_line_width( - width - * self.cairo_line_width_multiple + width * + self.cairo_line_width_multiple * # This ensures lines have constant width as you zoom in on them. - * (self.frame_width / self.frame_width), + (self.frame_width / self.frame_width), ) if vmobject.joint_type != LineJointType.AUTO: ctx.set_line_join(LINE_JOIN_MAP[vmobject.joint_type]) + if vmobject.cap_style != CapStyleType.AUTO: + ctx.set_line_cap(CAP_STYLE_MAP[vmobject.cap_style]) ctx.stroke_preserve() return self diff --git a/manim/constants.py b/manim/constants.py index 065a10fcfc..30a20e050b 100644 --- a/manim/constants.py +++ b/manim/constants.py @@ -76,6 +76,7 @@ "CTRL_VALUE", "RendererType", "LineJointType", + "CapStyleType", ] # Messages @@ -305,3 +306,41 @@ def construct(self): ROUND = 1 BEVEL = 2 MITER = 3 + + +class CapStyleType(Enum): + """Collection of available cap styles. + + See the example below for a visual illustration of the different + cap styles. + + Examples + -------- + + .. manim:: CapStyleVariants + :save_last_frame: + + class CapStyleVariants(Scene): + def construct(self): + arcs = VGroup(*[ + Arc( + radius=1, + start_angle=0, + angle=TAU / 4, + stroke_width=20, + color=GREEN, + cap_style=cap_style, + ) + for cap_style in CapStyleType + ]) + arcs.arrange(RIGHT, buff=1) + self.add(arcs) + for arc in arcs: + label = Text(arc.cap_style.name, font_size=24).next_to(arc, DOWN) + self.add(label) + """ + + AUTO = 0 + ROUND = 1 + BUTT = 2 + SQUARE = 3 diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 6048fe4c67..73505194b8 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -123,6 +123,7 @@ def __init__( # TODO, do we care about accounting for varying zoom levels? tolerance_for_point_equality: float = 1e-6, n_points_per_cubic_curve: int = 4, + cap_style: CapStyleType = CapStyleType.AUTO, **kwargs, ): self.fill_opacity = fill_opacity @@ -150,6 +151,7 @@ def __init__( self.shade_in_3d: bool = shade_in_3d self.tolerance_for_point_equality: float = tolerance_for_point_equality self.n_points_per_cubic_curve: int = n_points_per_cubic_curve + self.cap_style: CapStyleType = cap_style super().__init__(**kwargs) self.submobjects: list[VMobject] @@ -340,6 +342,10 @@ def set_stroke( self.background_stroke_color = ManimColor(color) return self + def set_cap_style(self, cap_style: CapStyleType) -> Self: + self.cap_style = cap_style + return self + def set_background_stroke(self, **kwargs) -> Self: kwargs["background"] = True self.set_stroke(**kwargs) @@ -1096,7 +1102,7 @@ def gen_cubic_bezier_tuples_from_points( remainder = len(points) % nppcc points = points[: len(points) - remainder] # Basically take every nppcc element. - return tuple(points[i : i + nppcc] for i in range(0, len(points), nppcc)) + return tuple(points[i: i + nppcc] for i in range(0, len(points), nppcc)) def get_cubic_bezier_tuples(self) -> npt.NDArray[Point3D_Array]: return self.get_cubic_bezier_tuples_from_points(self.points) @@ -1178,7 +1184,7 @@ def get_nth_curve_points(self, n: int) -> Point3D_Array: """ assert n < self.get_num_curves() nppcc = self.n_points_per_cubic_curve - return self.points[nppcc * n : nppcc * (n + 1)] + return self.points[nppcc * n: nppcc * (n + 1)] def get_nth_curve_function(self, n: int) -> Callable[[float], Point3D]: """Returns the expression of the nth curve. @@ -1456,7 +1462,7 @@ def get_end_anchors(self) -> Point3D_Array: Starting anchors """ nppcc = self.n_points_per_cubic_curve - return self.points[nppcc - 1 :: nppcc] + return self.points[nppcc - 1:: nppcc] def get_anchors(self) -> Point3D_Array: """Returns the anchors of the curves forming the VMobject. @@ -1747,7 +1753,7 @@ def pointwise_become_partial( self.append_points( partial_bezier_points(bezier_quads[lower_index], lower_residue, 1), ) - for quad in bezier_quads[lower_index + 1 : upper_index]: + for quad in bezier_quads[lower_index + 1: upper_index]: self.append_points(quad) self.append_points( partial_bezier_points(bezier_quads[upper_index], 0, upper_residue), @@ -2458,7 +2464,8 @@ def _throw_error_if_no_submobjects(self): if len(self.submobjects) == 0: caller_name = sys._getframe(1).f_code.co_name raise Exception( - f"Cannot call CurvesAsSubmobjects.{caller_name} for a CurvesAsSubmobject with no submobjects" + f"Cannot call CurvesAsSubmobjects.{ + caller_name} for a CurvesAsSubmobject with no submobjects" ) def _get_submobjects_with_points(self): @@ -2468,7 +2475,8 @@ def _get_submobjects_with_points(self): if len(submobjs_with_pts) == 0: caller_name = sys._getframe(1).f_code.co_name raise Exception( - f"Cannot call CurvesAsSubmobjects.{caller_name} for a CurvesAsSubmobject whose submobjects have no points" + f"Cannot call CurvesAsSubmobjects.{ + caller_name} for a CurvesAsSubmobject whose submobjects have no points" ) return submobjs_with_pts From 7c1060307e15735569ab2c28da73ba8da51e2e70 Mon Sep 17 00:00:00 2001 From: MathItYT Date: Sun, 10 Dec 2023 16:51:29 -0300 Subject: [PATCH 2/8] Added an example to `set_cap_style` method --- manim/mobject/types/vectorized_mobject.py | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 73505194b8..6289bffa07 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -343,6 +343,30 @@ def set_stroke( return self def set_cap_style(self, cap_style: CapStyleType) -> Self: + """ + Sets the cap style of the :class:`VMobject`. + + Parameters + ---------- + cap_style + The cap style to be set. See :class:`.CapStyleType` for options. + + Returns + ------- + :class:`VMobject` + ``self`` + + Examples + -------- + .. manim:: CapStyleExample + :save_last_frame: + + class CapStyleExample(Scene): + def construct(self): + line = Line(LEFT, RIGHT, color=YELLOW, stroke_width=20) + line.set_cap_style(CapStyleType.ROUND) + self.add(line) + """ self.cap_style = cap_style return self From 057b6e5e7cd3ce5adeff4d782ea6d70c969e01c6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 10 Dec 2023 19:59:14 +0000 Subject: [PATCH 3/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/camera/camera.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index f36a4199a4..1a0e6af736 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -29,7 +29,6 @@ from ..utils.iterables import list_difference_update from ..utils.space_ops import angle_of_vector - LINE_JOIN_MAP = { LineJointType.AUTO: None, # TODO: this could be improved LineJointType.ROUND: cairo.LineJoin.ROUND, @@ -42,7 +41,7 @@ CapStyleType.AUTO: None, # TODO: this could be improved CapStyleType.ROUND: cairo.LineCap.ROUND, CapStyleType.BUTT: cairo.LineCap.BUTT, - CapStyleType.SQUARE: cairo.LineCap.SQUARE + CapStyleType.SQUARE: cairo.LineCap.SQUARE, } @@ -335,8 +334,8 @@ def set_pixel_array( """ converted_array = self.convert_pixel_array(pixel_array, convert_from_floats) if not ( - hasattr(self, "pixel_array") and - self.pixel_array.shape == converted_array.shape + hasattr(self, "pixel_array") + and self.pixel_array.shape == converted_array.shape ): self.pixel_array = converted_array else: @@ -785,8 +784,9 @@ def apply_stroke( vmobject, ) ctx.set_line_width( - width * - self.cairo_line_width_multiple * + width + * self.cairo_line_width_multiple + * # This ensures lines have constant width as you zoom in on them. (self.frame_width / self.frame_width), ) From d3f3435f3e158151f35c0d631df2ec2198ae9e35 Mon Sep 17 00:00:00 2001 From: MathItYT Date: Sun, 10 Dec 2023 18:06:09 -0300 Subject: [PATCH 4/8] Unsplitted line 2501 --- manim/mobject/types/vectorized_mobject.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 6289bffa07..4ad97cbc94 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -2488,8 +2488,7 @@ def _throw_error_if_no_submobjects(self): if len(self.submobjects) == 0: caller_name = sys._getframe(1).f_code.co_name raise Exception( - f"Cannot call CurvesAsSubmobjects.{ - caller_name} for a CurvesAsSubmobject with no submobjects" + f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject with no submobjects" # noqa ) def _get_submobjects_with_points(self): @@ -2499,8 +2498,7 @@ def _get_submobjects_with_points(self): if len(submobjs_with_pts) == 0: caller_name = sys._getframe(1).f_code.co_name raise Exception( - f"Cannot call CurvesAsSubmobjects.{ - caller_name} for a CurvesAsSubmobject whose submobjects have no points" + f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject whose submobjects have no points" # noqa ) return submobjs_with_pts From a21fd46e6dd831269311902e4658b35289482e7b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 10 Dec 2023 21:06:49 +0000 Subject: [PATCH 5/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/mobject/types/vectorized_mobject.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 4ad97cbc94..de6b06f186 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -1126,7 +1126,7 @@ def gen_cubic_bezier_tuples_from_points( remainder = len(points) % nppcc points = points[: len(points) - remainder] # Basically take every nppcc element. - return tuple(points[i: i + nppcc] for i in range(0, len(points), nppcc)) + return tuple(points[i : i + nppcc] for i in range(0, len(points), nppcc)) def get_cubic_bezier_tuples(self) -> npt.NDArray[Point3D_Array]: return self.get_cubic_bezier_tuples_from_points(self.points) @@ -1208,7 +1208,7 @@ def get_nth_curve_points(self, n: int) -> Point3D_Array: """ assert n < self.get_num_curves() nppcc = self.n_points_per_cubic_curve - return self.points[nppcc * n: nppcc * (n + 1)] + return self.points[nppcc * n : nppcc * (n + 1)] def get_nth_curve_function(self, n: int) -> Callable[[float], Point3D]: """Returns the expression of the nth curve. @@ -1486,7 +1486,7 @@ def get_end_anchors(self) -> Point3D_Array: Starting anchors """ nppcc = self.n_points_per_cubic_curve - return self.points[nppcc - 1:: nppcc] + return self.points[nppcc - 1 :: nppcc] def get_anchors(self) -> Point3D_Array: """Returns the anchors of the curves forming the VMobject. @@ -1777,7 +1777,7 @@ def pointwise_become_partial( self.append_points( partial_bezier_points(bezier_quads[lower_index], lower_residue, 1), ) - for quad in bezier_quads[lower_index + 1: upper_index]: + for quad in bezier_quads[lower_index + 1 : upper_index]: self.append_points(quad) self.append_points( partial_bezier_points(bezier_quads[upper_index], 0, upper_residue), From 663231b962baaa03757272dda378db4d4c459cc3 Mon Sep 17 00:00:00 2001 From: MathItYT Date: Sun, 10 Dec 2023 18:29:06 -0300 Subject: [PATCH 6/8] Added graphical test for cap_style --- manim/camera/camera.py | 3 +-- tests/test_graphical_units/test_mobjects.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 1a0e6af736..1844b40869 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -786,9 +786,8 @@ def apply_stroke( ctx.set_line_width( width * self.cairo_line_width_multiple - * + * (self.frame_width / self.frame_width), # This ensures lines have constant width as you zoom in on them. - (self.frame_width / self.frame_width), ) if vmobject.joint_type != LineJointType.AUTO: ctx.set_line_join(LINE_JOIN_MAP[vmobject.joint_type]) diff --git a/tests/test_graphical_units/test_mobjects.py b/tests/test_graphical_units/test_mobjects.py index 21ea20437c..88e30cacfa 100644 --- a/tests/test_graphical_units/test_mobjects.py +++ b/tests/test_graphical_units/test_mobjects.py @@ -53,3 +53,22 @@ def test_vmobject_joint_types(scene): lines.arrange(RIGHT, buff=1) scene.add(lines) + + +@frames_comparison +def test_vmobject_cap_styles(scene): + arcs = VGroup( + *[ + Arc( + radius=1, + start_angle=0, + angle=TAU / 4, + stroke_width=20, + color=GREEN, + cap_style=cap_style, + ) + for cap_style in CapStyleType + ] + ) + arcs.arrange(RIGHT, buff=1) + scene.add(arcs) From e81ae8c9c8eb67aef0447c9f214e34f9e7844868 Mon Sep 17 00:00:00 2001 From: MathItYT Date: Sun, 10 Dec 2023 18:37:16 -0300 Subject: [PATCH 7/8] Added vmobject_cap_styles.npz for testing cap_styles --- .../mobjects/vmobject_cap_styles.npz | Bin 0 -> 3783 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/test_graphical_units/control_data/mobjects/vmobject_cap_styles.npz diff --git a/tests/test_graphical_units/control_data/mobjects/vmobject_cap_styles.npz b/tests/test_graphical_units/control_data/mobjects/vmobject_cap_styles.npz new file mode 100644 index 0000000000000000000000000000000000000000..c7ca1b2dc10fef4ef29d63e1088620c208b883ec GIT binary patch literal 3783 zcmeHK`&Ux=7C+k9nzF3!EL8gB1)*AUGw|B|Qf%J+Q~T5mFW z-M-U&udL&`V6tr6-kpyHih8l5k9GIeB7DVd6b+oUPTA$_`O%31GcDygGMd{I3*0W* zyv-}w@Ep0zBswBXWW0?zsb%|D+Z--x(qX$*%apJ-ZGY)|-zGLt75~@wR|fvR4CKe~oDH<1ob|tr z5R0}rr_2kiWP?*y(<8Oh!t|QsY<^2!DzCoo+&j4yZ&J#SeaE(;>`$M=q(0#jN5|e( zyUHTHLB}>3iHRz=1j;9H^jVa3+DR+JR`9v7uHBt#CqA@_>Ny3D5@pyx!&?0rv}vGP^k_2xqq& zokKe#`q^&bgGoA9IqFgx+1w0F^by%vNDoA;&^!IbtupmT z14UK(aD^cJ+?n&9M+Yf(`wtvA1om*WS$8oS z5E97$zJG5=m!jU%Q9kgZm-8N$Q~4{0@LLO5BrqtYru?@-{9Y5d5#_*}7v$nKb2rs) zh(iYxP-o+N+#GBBY>lzATQT5kU0}DxGdUc+czVdX&f(!`c$!b10KUZ+EI0@{KIJO& zzkJ1>#t1XMUxFK#6<;pOSqi*4*XO}V4}i6}y1L3)ra@9AG-ws-)T-&R^3c?*Z6$|K za(i)a9RA_P$%Sl1U3hm56G<@ajE`NRR}iRx)n0Ox6c@5^zFbuTY{1rAAI8@9R?N9HEp$rgQ&&oJFL`7dZ_M_} zW+F=m1Vy&k*?X42+lq$vU1rBHXNMf>Ev~i=n^?%v=U(vUd&MSO(5TYyu6oNN5=~MemOlq-k0}Q6x{WHd^)5htgOeZtS;?!my$xFd2@3glt zn4fb8d#d_uQL;%dB=ECeS9aXkH6CjKkF_42#ju%4`1Ep1S(zo^@18)#&HU2sXJS}9 zFueSBA#bFCL#B}4OXx+^lzv^H&#(AmeIw4z4!t;CPM>n64Q~q`DI}l_WRu=d44QyI zkk8h^+cj4v=W@IAM>}xmo(S;=|1?MQ`q)pN6BUOGMmrxM3WJN$);IW{xF{#yCI4{B zp%?Zq#?fJP#MTbR6L@wY@N}AMNg7fCgwJecFRhVRxl;xS;p|{5V`))xTs8)4qicOx8HVrzp zni!@2C_8Wm6`(4Tx#n$fThHso=HF*dy<8eJyGq^6M+B&N_e|(H+|KZ|Umm;Lo9r4Z ziE{y0ivoG0(nOdfFf)cVvzf`tr3R?7es~JrR8`xI9h<$jD&#LKiQ5@ImGi?lJ5Umq z0xMjzlTwo3iN&PYWYP%J2)oQjoK$&#S%yzZ3N>AZ&QP=Wyixt$|0n)EU-W&f-_O`qJ}8XypmBYCdeHLlLeNj z7Ct_$^dj=4m0Pqj?&Uq>$9jNOD#9E_cs*KCaCk^+GKt%4CL3W=Ry73`sbP7~5r$|2 zm)4lP*+tkuK)qHmX+$^o8XEw&Q$J)ey|g4^A4e!_{#%BJ#*V9;qYGO}$P&zxRDUlIuJ&6_rL`+UHS5FUfK3fTmdD2Eyeh5S=5 z||s=%NU*aLtwp{qmk)(MwO4mevH6_$m&vW`w{E*SM)9k_R|& zT%4V(kg1EUc0mFc3MM$O?~3JAA;%e?OAUL|V%-&xKJ-BYc>Wq94_tIQHq%OGX@>Bc zHUi<1M`;y27|nD~nyobTKOYQ()PPr;lqyT7l*$I@BRa*~X$F|~M@c!=01%ne!*)=J z$pJGfS>WY%?>992T6G}*SeCdfYgV2Wdq0fo;O{4IUMLV(hDV+W(fIrR%6yIND+8+x wJe8n)J=N6Bfxot$%7#+eTz={+SD_cwl*3<}P+w1t^~zZ_=yrt0q*a6e135z*;s5{u literal 0 HcmV?d00001 From 301fc020c05a3497ee3c3dc8e59b5b48c2cb07ec Mon Sep 17 00:00:00 2001 From: MathItYT Date: Sun, 10 Dec 2023 18:49:28 -0300 Subject: [PATCH 8/8] Removed # noqa comments from vectorized_mobject.py --- manim/mobject/types/vectorized_mobject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index de6b06f186..9e8c97358b 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -2488,7 +2488,7 @@ def _throw_error_if_no_submobjects(self): if len(self.submobjects) == 0: caller_name = sys._getframe(1).f_code.co_name raise Exception( - f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject with no submobjects" # noqa + f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject with no submobjects" ) def _get_submobjects_with_points(self): @@ -2498,7 +2498,7 @@ def _get_submobjects_with_points(self): if len(submobjs_with_pts) == 0: caller_name = sys._getframe(1).f_code.co_name raise Exception( - f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject whose submobjects have no points" # noqa + f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject whose submobjects have no points" ) return submobjs_with_pts