基于Python的Manim面向对象(2)

一、简介

  这篇文章是展示我第一次认为对于Manim结构化编程的重要性的,以及我第一次尝试去做短视频时代码展示,其中展示了我整个的思考过程,而且整体问题是很显而易见的——缺少结构、思维混乱。

二、源代码

1、原汁原味代码

  下面的代码是我将之前第一次尝试去做小项目时整个的代码,一点也没删,注释是讲解,由于都是由一个一个小动画块组成的,单个看肯定没问题。

2、整体思想

  整个项目是想创建一个链表,并添加一个结点。对象选择都是基本的mobject(长方形、箭头、文本等等)动画构造也是基本的移动、创建等。内部的*操作,看过本杰明的应该都知道这种用法,解包操作。整体的逻辑实现是非常简单的,但是结构非常混乱。
  下面的代码大体看看就行,不用细究。

"""
下面是我自己面向过程式编程
从上到下一步一步地来进行
想到什么做什么,没有任何操作可言
"""




from manim import*
class Try1(Scene):
    def construct(self):
        #我们先一步一步地来,整一个长方形
        
        # rectangle=Rectangle(color=BLUE,fill_opacity=1).scale(0.2)
        # self.add(rectangle)

        #接下来,我想创建5个长方形节点,并且依次排列 印象里有VGroup的方法,现在先不使用这个方式,而是选择使用for循环来解决
        
        # for i in range(-2,3):
        #     self.add(
        #         rectangle.copy().set_x(i*1.5)
        #     )

        #接下来,我打算使用本杰明,通过VGroup解包的方式来创建一样的效果

        # rectangles=VGroup(*[Rectangle(color=BLUE,fill_opacity=0.5).scale(0.2).set_x(i*1.5) for i in range(-2,3)] )
        # for rectangle in rectangles:
        #     self.add(rectangle)

        # 使用这种方法,我觉得还是一种编程范式吧,这样可以对这长方形整体进行打包,而上述循环方式,同样也是可以通过VGroup.add()应该是哈,忘了
        # 来进行打包,整体的效果是一样的

        #上面的编程格式十分不太美观,不容易去读,再尝试一下美观写法

        rectangles=VGroup(
            *[Rectangle(color=BLUE,fill_opacity=1).scale(0.2).set_x(i*1.5)for i in range(-2,3)]
        )
        for rectangle in rectangles:
            self.play(Write(rectangle),run_time=0.5) 

        # 5个节点建立好了,接下来可以思考一下箭头来怎么表示,每个正方形可以通过rectangle[0]的方式来访问,然后再arrow方式来进行创建
        # 接下来先创建一个箭头

        # arrow=Arrow()
        # self.add(arrow)
        # 接下来打算细致弄箭头了,需要是从一个长方形的RIGHT到另一个长方形的LEFF

        #这里困惑了一段时间,因为箭头头尾不对应,一开始觉得可能是因为scale,导致get_right和get_edge_center(RIGHT)会存在偏差
        #可是呢,其实对于Arrow而言,我们一开始在文档中就训练过,问题也是出现了,其实就是因为buff导致的,默认值不是0
        #所以只需要将buff设置为0就好了。
        #这个问题一方面说明,官方文档例子需要多去练习的————关键时候可能想不起来,另一方面也说明了练习官方文档的必要性————反正我通过deepseek没有解决,最后还是脑海里残存的官方文档中Arrow
        #的例子,才想起来的buff控制其举例。现在问问chatgpt去试试,看看它能不能解决
        arrows=VGroup(
            *[Arrow(start=rectangles[i].get_right(),end=rectangles[i+1].get_left(),color=PURPLE,fill_opacity=1,buff=0)for i in range(0,4)]
        )
        for arrow in arrows:
            self.play(
                Write(arrow),
                run_time=0.5
            )
        self.wait()

        #我们先创建一个头结点
        head=rectangles[0].copy().set_color(RED).rotate(PI/2).set_x(-4.5)
        head_arrow=Arrow(start=head.get_right(),end=rectangles[0].get_left(),color=PURPLE,fill_opacity=1,buff=0)
        self.play(Write(head))
        self.wait()
        self.play(Write(head_arrow))

        #现在来看,我们都创建好了,还需要的是next指针,初始时,next指针指向头结点
        #这个指针打算用长方形和箭头的组合方式来表示,现在我们先创建一个指针实例

        next_part1=rectangles[0].copy().set_color(YELLOW).scale(0.8).move_to(ORIGIN)
        dot=Dot([0,-1,0])
        next_part2=Arrow(start=next_part1.get_bottom(),end=dot.get_center(),fill_opacity=1,color=YELLOW,buff=0)
        next=VGroup(next_part1,next_part2).next_to(head,UP,buff=0)
        self.play(Write(next))

        #整体的样式是构造完成了,接下来需要的是添加文本,要不然谁能知道每个结点指代的什么东西吗?
        head_text=Text("Head\nNode").scale(0.2).move_to(head.get_center())
        next_text=Text("Next\nPointer",color=GREEN).scale(0.2).move_to(next_part1.get_center())
        self.play(Write(head_text),run_time=0.5)
        self.play(Write(next_text),run_time=0.5)

        #接下来打算给长方形中间整个分隔线,分别用来表示地址和数据域,所以还是通过解包的方式完成
        lines=VGroup(
            *[Line(start=rectangles[i].get_top(),end=rectangles[i].get_bottom(),color=RED,fill_opacity=1)for i in range(0,5)]
        )
        for line in lines:
            self.play(
                Write(line),
                run_time=0.5
            )
        
        # 接下来需要在各个结点上赋值,本来想用D(data)、A(address)来表示数据域和指针域,看了看课本还是用ei和pi来表示吧
        #下面的i越界访问了,可以用双变量表示,但是没太有必要,正常地来就好,等会再看看双变量怎么来写的吧
        texts1=VGroup(
            *[MathTex(f"e_{i}",color=ORANGE).scale(0.9).align_to(rectangles[i].get_left(),LEFT)for i in range(0,5)]
        )
        texts2=VGroup(
            *[MathTex(f"p_{i}",color=ORANGE).scale(0.8).align_to(rectangles[i].get_right(),RIGHT)for i in range(0,4)]
        )
        #由于最后一个文本是NULL,所以这里需要再重新写一个
        text1=Text("NULL",color=RED,fill_opacity=1).scale(0.2).align_to(rectangles[4].get_right(),RIGHT)
        for text in texts1:
            self.play(
                Write(text),
                run_time=0.5
            )

        for text in texts2:
            self.play(
                Write(text),
                run_time=0.5
            )

        self.play(Write(text1))

            #这差不多就是一些最基础的模型了,接下来我们想要创作的是链表的增添、删减等等,而这里肯定就会包含这些代码的复用,所以说,如何建立起自己的
            #一个数据库是十分有必要的,这在你之后的创作过程中是十分有必要的,当然,目前来看,现在只是测试创建,整体上来看还是有些混乱的

            #由于Rectangle和Text并没有联动起来,为了方便,将文本去除后再进行其他操作


        self.wait(3)

        self.play(Unwrite(next_text),Unwrite(text1))

        for i in range(0,5):
            if i==4:
                self.play(Unwrite(lines[i]),Unwrite(texts1[i]),run_time=0.5)

            elif i<4:
                self.play(Unwrite(lines[i]),Unwrite(texts1),Unwrite(texts2),run_time=0.5)

        self.wait(3)


        rec=rectangles[0].copy().set(color=ORANGE,fill_opacity=1).scale(0.8).move_to(ORIGIN).to_edge(DOWN,buff=1)
        self.play(Create(rec))
        self.wait(2)

        #接下来需要给这个待插入结点增加一个文本说明

        rec_text=Text("待插入结点",color=RED).scale(0.5).next_to(rec,DOWN)
        self.play(Create(rec_text))

        self.wait(2)

        self.play(FadeOut(rec_text))

        #接下来,如果我们想插入2、3结点中,应该如何操作呢?

        self.play(rec.animate.move_to([0,2.5,0]))
        
        #这里来看,整体位置是没有太大问题的,但是呢,想强行插入的话肯定是不合理的,还需要将整体拆分,然后留出来足够的空间,再将其插入就好

        #中间插入  这些代码是一步一步来的,缺少整体性,所以现在存在的问题是,想再重新利用之前代码,发现似乎利用不起来,需要反复地回看

        self.play(Unwrite(arrows[1]),run_time=0.5)

动画生成展示
  最后的节点插入部分没弄,当时只是觉得思维混乱,也明白了面向对象存在的意义。
  本来想用gif放在这,后来发现通过图片来放gif,文件最大只能5M,而我生成的gif,测试时是1s差不多1M,哈哈有点无语。
  哎,视频上传到b站,设为仅自己可见,链接放在这直接不行。。。
  优酷视频怎么自己上传视频自己分享不了。。。
  只能通过云链这种方式了
  动画展示

三、分析

  我们不是分析代码,而是分析结构。我写的这个代码,虽然包装到类Try1中,表面上是面向对象的,但是类的内部实现是面向过程的。
  不知道大家编程能力怎么样,是否知道面向过程的意思。我觉得可以这样理解:想到什么写什么。当我们一开始学习编程时,遇到一个题目,我们会想一步一步如何解决,然后将每一步用代码实现,最后得到的完整代码,这种编程方式就是面向过程。再简单回看一下我的源代码,我下一步想干什么,都提前在注释中写好了,其实就是面向过程的。
  所以说,并不是定义了个类,就是面向对象了,而是要看整体结构的。对于manim来说,我们使用的各种mobject、animation等,其实都是封装好个一个一个的类,看过manim官方文档的肯定也知道,内部类的继承关系都十分明确。
  当然,我这肯定不是否定面向过程式编程,它也是不可或缺的。只不过面向对象更适用于大的项目,而面向过程,则适用于一个又一个小的问题实现。面向对象中,各个方法(这里的方法指面向对象的方法,简单说其实就是类中定义的函数)的编写,其实也是面向过程的。另外,除了面向过程、面向对象外,还有函数式编程。函数式编程,顾名思义,解剖问题,然后拆分为一个又一个的函数,最后调用函数并解决问题。日常做题中,整体结构一般是面向过程,而解决问题时,为了方便,我们也许会定义一个又一个的函数,避免代码重复编写。

四、Python的面向对象

  注意,我这里说的是Python的面向对象,Manim的面向对象跟它还是有一定区别的,二者肯定是以Python面向对象为主体,而Manim面向对象只是其延伸。另外,我这里也仍是将结构,不讲代码。
  结合上面第三点的分析,我们知道,结构上来看,面向对象包含着一系列的方法(或者函数),各个方法(或者函数)内部,又有对应面向过程的解决问题方式。简单来看,如果我们单纯地将类视为面向对象、将类的方法视为函数的话,那么面向对象、函数式编程、面向过程可以用以下框架概括:
类1:
 对象
 函数1:
  过程
 函数2:
  过程
 函数3:
  过程

类2:
 对象
 函数1:
  过程
 函数2:
  过程
 函数3:
  过程

类1()
类2()

  面向对象,操作的对象可别忘记了。最下面的两行是类的实例化,也就是调用类,实现对应功能(要不然你只定义了类,不去调用的话,对应的功能也就没有实现。而调用的这个过程,就是类的实例化)。
  上面全是文字描述,看着有些乏味,但是是我个人实践中的一些理解,我觉得说的比较明白了,如果还看不明确的话,一定要再多多去看看对于书籍与视频,自己再进行些实践,才能有自己独特的见解。

五、Manim的面向对象

  这里也还是只谈论结构,后续会再深入讲解。
  前面说过了,Manim的面向对象是对Python面向对象的延伸。我们编写的函数construct,内部每一个动画的编写,其实都调用了类。而这个类,是存储在manim包中,并不是由我们编写的,我们只是编写了construct函数。注意再看一看我上面的Python面向对象的结构,函数式编程内部其实就是面向过程的。所以这么来看,严格意义上来讲,我们编写construct函数,其实是面向过程的,内部各种mobject(类)、animation(类)的调用,是别人写好的包供我们调用的。
  当然,编程是灵活的,不是死板的,等熟知之后想怎么写就怎么写,没必要被任何框架束缚。但是在熟知掌握之前,最好还是按照前人总结来写,要不然基础本身就没多少还想按照自己想法来,最后肯定会乱的。
  所以说,一开始我上面的写法,完完本本的面向过程写法,可以拆分为好几个类,分别编写对应的construct,也可以在一个类中再编写好几个函数,最后通过construct函数来调用,都是不错的解法。而像我那样,只靠一个类,一个construct函数解决所有问题,随着问题的深入,肯定会越来越混乱的。
  最后,我们再看一眼Python面向对象的结构,末尾出现了类1、类2的调用,也就是实例化,而在manim中并没有存在这个过程。其实答案藏在了manim包中。我们来看一看manimce的官方解释。其中的代码注释是我当时第一次理解时写的,这里就不删去了。

"""
        说了这么久的oop,我们知道,封装好了一个类,需要通过类的实例来去运行,
    可是在manim中,我们只需要在construct中写好对应的代码,然而并没有在全局中
    调用对应的实例就可以渲染出图像,为什么呢?

        其实很简单,manim自动调用了,举个例子:在某处,一定会有sc=Scene()
    然后再经过一系列的接口,从而渲染出对应的图像.这一点其实在manim官方文档中
    说明了,感兴趣的可以去看一下.
    
"""

from manim import *

"""
        这是官方文档中的,下面的wtih...是不需要我们显式书写的,这样我们可以明显看出
    Secne是如何实例化的.
"""
class ToyExample(Scene):
    def construct(self):
        orange_square = Square(color=ORANGE, fill_opacity=0.5)
        blue_circle = Circle(color=BLUE, fill_opacity=0.5)
        self.add(orange_square)
        self.play(ReplacementTransform(orange_square, blue_circle, run_time=3))
        small_dot = Dot()
        small_dot.add_updater(lambda mob: mob.next_to(blue_circle, DOWN))
        self.play(Create(small_dot))
        self.play(blue_circle.animate.shift(RIGHT))
        self.wait()
        self.play(FadeOut(blue_circle, small_dot))

with tempconfig({"quality": "medium_quality", "preview": True}):
    scene = ToyExample()
    scene.render()

  我觉得应该没必要再多说什么了,我们自定义类,最后总会在manim包中隐式调用的,然后经过渲染,生成了一系列动画。

六、总结

  上面内容是我对面向对象结构的分析,后续的话会深入讲解Manim中面向对象代码的解释。如果有什么建议、错误、疑问,欢迎在评论区留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值