In this part of the series, we’ll explore some more layout-related animations, explore the animation flow and discover signals + effects.
We’ll also play around with colors a bit 🙂.
Grouping objects
Motion Canvas doesn’t fully support the same grouping as Manim (i.e. change the color of all objects in this particular group).
Instead, we should always be working with the scene hierarchy and layout objects, which do supports certain operations, mostly related to their position, scale/size and rotation.
In this example, we’re also using the fact that Motion Canvas supports any X11 colors names – feel free to browse through them and pick the ones that you like!
import{Layout,makeScene2D,Rect}from'@motion-canvas/2d';import{all,createRef,sequence}from'@motion-canvas/core';import{appear}from"../../utilities";exportdefaultmakeScene2D(function*(view){constrectangles=Array.from({length: 3},()=>createRef<Rect>());// we can use any of the names from the X11 color standard!
// a nice website for picking colors: https://x11.linci.co/
constrectangleColors=['crimson','forestgreen','deepskyblue'];constlayout=createRef<Layout>();view.add(<Layoutlayoutgap={50}ref={layout}>{rectangles.map((ref,i)=><Rectref={ref}opacity={0}stroke={rectangleColors[i]}lineWidth={5}size={300}/>)}</Layout>)yield*sequence(0.15,...rectangles.map(ref=>appear(ref())));// scale the entire group
yield*all(layout().scale(1.5,1),layout().position.y(-200,1),)// suppress the layout for a while and remember the positions
layout().children().forEach(ref=>ref.save())layout().layout(false);layout().children().forEach(ref=>ref.restore())// now we can move it
yield*rectangles[1]().position.y(300,1);// layout doesn't have attributes of the children, so setting colors on it won't work
yield*all(...rectangles.map(ref=>ref().stroke('white',1)));yield*all(...rectangles.map(ref=>ref().fill('white',1)));});
frommanimimport*classVGroupExample(Scene):defconstruct(self):s1=Square(color=RED)s2=Square(color=GREEN)s3=Square(color=BLUE)s1.next_to(s2,LEFT)s3.next_to(s2,RIGHT)self.play(Write(s1),Write(s2),Write(s3))group=VGroup(s1,s2,s3)# scale the entire groupself.play(group.animate.scale(1.5).shift(UP))# only work with one of the group's objectsself.play(group[1].animate.shift(DOWN*2))# change color and fillself.play(group.animate.set_color(WHITE))self.play(group.animate.set_fill(WHITE,1))
Arranging objects in Manim is usually done via the arrange
function.
In Motion Canvas, we again utilize the almighty flexbox… well, kind of – animating certain flexbox properties is still not supported (but will likely be in the future):
import{Circle,Layout,makeScene2D}from'@motion-canvas/2d';import{all,createRef,sequence,useRandom}from'@motion-canvas/core';import{appear}from"../../utilities";exportdefaultmakeScene2D(function*(view){letrandom=useRandom(0xdeadbeef);constcircles=Array.from({length: 15},()=>createRef<Circle>());constlayout=createRef<Layout>();view.add(<Layoutlayoutgap={50}ref={layout}alignItems={'center'}>{circles.map((ref,i)=><Circlex={random.nextInt(-700,700)}y={random.nextInt(-300,300)}ref={ref}opacity={0}stroke={'red'}lineWidth={5}size={random.nextInt(10,100)}/>)}</Layout>)layout().children().forEach(ref=>ref.save())layout().layout(false);yield*sequence(0.05,...circles.map(ref=>appear(ref())));yield*sequence(0.05,...circles.map(ref=>all(ref().restore(1),ref().opacity(1,1))),);layout().layout(true);// some layout properties are not supported yet :(
// https://github.com/motion-canvas/motion-canvas/issues/363
yield*all(layout().gap(10,1),layout().direction('column',1),);});
frommanimimport*fromrandomimportseed,uniformclassArrangeExample(Scene):defconstruct(self):seed(0xDEADBEEF)circles=VGroup(*[Circle(radius=0.1).scale(uniform(0.5,4)).shift(UP*uniform(-3,3)+RIGHT*uniform(-5,5))for_inrange(12)])self.play(FadeIn(circles))# left-to-right arrangementself.play(circles.animate.arrange())# specify the direction of arrangement and spacing between the objectsself.play(circles.animate.arrange(direction=DOWN,buff=0.3))self.play(circles.animate.arrange(direction=LEFT+DOWN,buff=0.1))
For grids, Manim’s arrange_in_grid
is just a special case of Motion Canvas’ flexbox shenanigans.
The only difference here is that we’re newly using the wrap property, since the circles would otherwise be squished and not wrapped to form a grid.
To make the animation a bit more interesting, we can utilize the chroma.js (which Motion Canvas internally uses to work with colors) to assign colors using a color scale.
import{Circle,Layout,makeScene2D}from'@motion-canvas/2d';import{all,createRef,sequence,useRandom}from'@motion-canvas/core';import{appear}from"../../utilities";importchromafrom'chroma-js';exportdefaultmakeScene2D(function*(view){letrandom=useRandom(0xdeadbeef);constcircles=Array.from({length: 12**2},()=>createRef<Circle>());constlayout=createRef<Layout>();// we can use chroma.js for nice color shenanigans
// we're using the lightness-chroma-hue for a nicer-looking scale
letcolors=chroma.scale(['#fafa6e','#2A4858']).mode('lch').colors(circles.length)view.add(<Layoutlayoutgap={50}ref={layout}wrap={'wrap'}width={1400}alignItems={'center'}>{circles.map((ref,i)=><Circlex={random.nextInt(-600,600)}y={random.nextInt(-300,300)}ref={ref}opacity={0}stroke={colors[i]}lineWidth={5}size={random.nextInt(10,50)}/>)}</Layout>)layout().children().forEach(ref=>ref.save())layout().layout(false);yield*sequence(0.01,...circles.map(ref=>appear(ref())));yield*sequence(0.01,...circles.map(ref=>all(ref().restore(1),ref().opacity(1,1))),);layout().layout(true);yield*all(layout().gap(30,1),layout().width(1200,1),);yield*all(layout().gap(60,1),layout().width(1600,1),);});
Add, remove and ordering
The order in which the objects are rendered are based on the scene hierarchy – the higher they are, the sooner they are rendered (i.e. the more at the bottom they are).
However, if they differ in their z-index, the one with a higher z-index will always be drawn on top of the other:
import{Circle,Layout,makeScene2D,Polygon,Rect}from'@motion-canvas/2d';import{all,createRef,sequence,Vector2,waitFor}from'@motion-canvas/core';import{appear}from"../../utilities";exportdefaultmakeScene2D(function*(view){// start with a square
constsquare=createRef<Rect>();view.add(<Rectref={square}fill={'white'}size={300}/>);yield*waitFor(1);// will be added to the very top (i.e. will now be the _last child of root_)
constcircle=createRef<Circle>();view.add(<Circleref={circle}fill={'red'}size={350}/>);yield*waitFor(1);// this moves the object "to the bottom" (i.e. below the square)
// this means that it will be the _first child of root_
circle().moveToBottom();yield*waitFor(1);// circle: hi again!
circle().moveToTop();yield*waitFor(1);// this is a bit more interesting; we move to the bottom, but also
// increase the Z-index, which is the more important parameter wrt. rendering
//
// circle: I'm staying on top!
circle().moveToBottom();circle().zIndex(10);circle().fill('blue');yield*waitFor(1);// circle: *dies*
circle().remove();yield*waitFor(1);});
Animation flow
We’ve already seen a few of these in the previous post, but we can use different functions for working with animation flow.
The main difference between Manim and one of the main features of Motion Canvas is that the animation model inherently allows for a lot of concurrency, since you can have multiple threads concurrently changing different properties, even of the same object:
import{Layout,makeScene2D,Rect,Txt}from'@motion-canvas/2d';import{all,any,chain,createRef,delay,loop,sequence,waitFor}from'@motion-canvas/core';import{appear}from"../../utilities";exportdefaultmakeScene2D(function*(view){constrectangles=Array.from({length: 3},()=>createRef<Rect>());constrectangleColors=['crimson','forestgreen','deepskyblue'];consttext=createRef<Txt>();view.add(<Layoutgap={50}fontSize={100}layoutdirection={'column'}alignItems={'center'}><Txtref={text}fill={'white'}></Txt><Layoutlayoutgap={50}>{rectangles.map((ref,i)=><Rectref={ref}opacity={0}stroke={rectangleColors[i]}lineWidth={5}size={300}/>)}</Layout></Layout>)yield*text().text('sequence(t, ...)',1)// sequence: launch animations in a delayed sequence
yield*sequence(0.15,...rectangles.map(ref=>appear(ref())));yield*text().text('all(...)',1)// all: launch all animations together
yield*all(...rectangles.map(ref=>ref().opacity(0,1)))yield*text().text('chain(...)',1)// chain: launch animations one after another
yield*chain(rectangles[0]().opacity(1,0.5),rectangles[1]().opacity(1,1),rectangles[2]().opacity(1,0.25),)yield*text().text('delay(t, .)',1)// delay: launch animations with a delay
// we can also nest!
yield*all(delay(0.6,rectangles[0]().opacity(0,1)),delay(0.3,rectangles[1]().opacity(0,1)),delay(0.9,rectangles[2]().opacity(0,1)),)// any: continue when at least one finishes!!!
yield*text().text('any(...)',1)yield*any(delay(1,rectangles[0]().opacity(1,1)),delay(0.25,rectangles[1]().opacity(1,1)),delay(2,rectangles[2]().opacity(1,1)),)yield*text().text('one finished!',1)yield*waitFor(1);yield*text().text('loop(...)',1)// loop: starts an infinite animation; expects an animation factory to do this
// because of this, we have to use yield (without star!), running this in the background
yieldloop(()=>rectangles[1]().opacity(0,0.25).to(1,0.25),)yield*waitFor(1);// we can, however, still animate some of its other properties!
yield*all(rectangles[1]().scale(0.5,3),text().text('change while looping!',1),);});
Instead of object’s characteristics being static values, they are usually signals, which are (as the documentation describes) values that can change over time and define dependencies between objects.
This means that, as opposed to Manim’s updater we don’t need to explicitly say that an object’s attribute should be set to this value at every frame – we say that it is that value:
import{Circle,Layout,makeScene2D,Rect,Txt,Node}from'@motion-canvas/2d';import{all,createRef,sequence,useRandom}from'@motion-canvas/core';import{appear}from"../../utilities";exportdefaultmakeScene2D(function*(view){constouterSquare=createRef<Rect>();constinnerSquare=createRef<Rect>();consttext=createRef<Txt>();view.add(// we're using two squares, since rotation changes the position of the top
// view the scene graph in the UI editor if it's unclear what we're doing
<Rectref={outerSquare}size={300}><Rectref={innerSquare}stroke={'white'}lineWidth={5}size={300}/></Rect>);view.add(<Txtref={text}text={"A neat square."}fill={'white'}// make the properties of text based on signals of the square
opacity={outerSquare().opacity}bottom={outerSquare().top}scale={outerSquare().scale}padding={30}/>);yield*appear(outerSquare());// we change the position of outer, dragging the text with
yield*outerSquare().position.x(-300,1);// we change the rotation of the inner, meaning that the text doesn't change
// (it relies only on the attributes of the outer square)
yield*innerSquare().rotation(-180,1);yield*all(innerSquare().rotation(0,1),outerSquare().scale(0.5,1),outerSquare().position.x(0,1),)// now we add another signal -- we want the text to rotate according to the outer square
text().rotation(outerSquare().rotation);yield*all(outerSquare().rotation(-90,1),outerSquare().scale(2,1),)});
Note that we don’t necessarily need to only assign one signal to another – we can assign a function that takes the value of the signal and modifies it how we want, for example taking the position of an object and converting it to a text to display it:
import{Circle,Layout,makeScene2D,Rect,Txt,Node,Latex}from'@motion-canvas/2d';import{all,createEaseInOutBack,createRef,sequence,useRandom,Vector2}from'@motion-canvas/core';import{appear}from"../../utilities";exportdefaultmakeScene2D(function*(view){constcircle=createRef<Circle>();consttext=createRef<Txt>();view.add(<><Circleref={circle}stroke={'white'}lineWidth={5}size={300}/><Latexref={text}fill={'white'}// make the properties of text based on functions of the signals of the circle
tex={()=>`p = [${circle().x().toFixed(0)}, ${circle().y().toFixed(0)}]`}opacity={circle().opacity}scale={circle().scale}bottom={()=>circle().top().addY(-30)}/></>);yield*appear(circle());yield*circle().position.x(100,1);yield*all(circle().position(newVector2(-200,-100),1),circle().scale(1.25,1),)yield*all(circle().position(newVector2(0,300),1),circle().scale(.5,1),)yield*all(circle().position(newVector2(0,0),1),circle().scale(1,1),)});
A crucial detail is that signals are lazy – assuming that the value of a signal is some complicated function that relies on other signals, it is only calculated when the value is requested, making it a perfect fit for creating dependencies between properties, but not so much for e.g. a simulation function that needs to change things each frame.
For that, we have effects…
Effects
Effects are functions that are run on their dependency changes, but unlike signals are no longer lazy.This means that all of their dependencies are no longer lazy as well, so if you have many things going on at the same time, things might run a bit slow…
Effects come in two flavors; directly quoting the documentation:
createEffect()
runs immediately after any of its dependencies changes.
createDeferredEffect
runs at the end of each frame if any of its dependencies changed
For example, we could use it to create a simple particle simulation like this one:
import{Circle,makeScene2D,Spline}from'@motion-canvas/2d';import{createDeferredEffect,createRef,createSignal,loop,sequence,useRandom,Vector2}from'@motion-canvas/core';import{appear}from"../../utilities";importchromafrom'chroma-js';exportdefaultmakeScene2D(function*(view){letrandom=useRandom(0xdeadbef2);// random circles at random base positions
constcircles=Array.from({length: 8**2},()=>createRef<Circle>());constbasePositions=Array.from({length: 8**2},()=>newVector2(random.nextInt(-600,600),random.nextInt(-300,300)));constcircle=createRef<Circle>();letcolors=chroma.scale(['#fafa6e','#2A4858']).mode('lch').colors(circles.length)view.add(<><Circleref={circle}fill={'white'}/>{circles.map((ref,i)=><Circlex={basePositions[i].x}y={basePositions[i].y}ref={ref}stroke={colors[i]}fill={colors[i]}lineWidth={5}size={15}/>)}</>)circles.forEach(ref=>ref().opacity(0));yield*sequence(0.01,...circles.map(ref=>appear(ref())));// create a repulsion force around the white circle
createDeferredEffect(()=>{circles.forEach((ref,i)=>{constpos=basePositions[i];constvector=pos.sub(circle().position())constdirection=vector.normalized;constdistance=vector.magnitude;conststrength=1/50;// we need to push at least as much as the radius of the circle (to not collide)
constpushStrength=Math.max(Math.sqrt(distance)*circle().width()*strength,circle().width(),)constpushVector=pos.add(direction.mul(pushStrength));ref().position(pushVector);});})yield*circle().size(100,1);// move it using a nice hand-crafted spline :)
constspline=createRef<Spline>();constprogress=createSignal(0);leth=150;letw=400;view.add(<Splineref={spline}points={[[0,0],[0,-h],[-w,-h],[-w,h],[w,h],[w,-h],[0,-h],[0,0]]}smoothness={0.6}/>)circle().position(()=>spline().getPointAtPercentage(progress()).position)// loop the size scaling indefinitely in the background to make the dots pulse
// do this 3 times (loop can take a number of times :)
yieldloop(3,()=>circle().size(50,1).to(100,1))yield*progress(1,10);});
Tasks
Shuffle
Before trying to animate this, here are a few useful things:
to animate a value from 0 to 1 that we can use for the percentage value, we can create and animate a new signal (more about what that is in the next Motion Canvas post)
the easeInOutExpo
easing curve is nicer for shuffing since it’s more sudden than the default
All of the above can be summarized in the following animation:
import{Circle,Curve,Layout,makeScene2D,Spline}from'@motion-canvas/2d';import{all,createRef,createSignal,easeInOutExpo,sequence,ThreadGenerator,useRandom,Vector2}from'@motion-canvas/core';import{appear}from"../../utilities";/**
* Swaps the positions of two layouts in a visual view by animating them along spline paths.
*
* @param {Layout} view - The parent layout that contains both `a` and `b`.
* @param {Layout} a - The first layout to be swapped.
* @param {Layout} b - The second layout to be swapped.
* @param duration - How long should the animation be?
*/function*swap(view: Layout,a: Layout,b: Layout,duration: number=1):ThreadGenerator{letstart=a.position();letend=b.position();letmid=newVector2().add(start).add(end).div(2);constprogress=createSignal(0);lets1=createRef<Spline>();lets2=createRef<Spline>();letyOffset=Math.abs(a.position().x-b.position().x)/2.5;view.add(<Splineref={s1}points={[start,mid.addY(yOffset),end]}smoothness={1}/>)view.add(<Splineref={s2}points={[end,mid.addY(-yOffset),start]}smoothness={1}/>)a.position(()=>s1().getPointAtPercentage(progress()).position)b.position(()=>s2().getPointAtPercentage(progress()).position)yield*progress(1,duration,easeInOutExpo);// nicer curve :)
// don't clutter the scene!
s1().remove();s2().remove();}/**
* Highlight a curve object.
*/function*highlightCircle(object:Curve):ThreadGenerator{yield*all(object.stroke('red',1).to('white',1),object.lineWidth(20,1).to(object.lineWidth(),1),object.fill('red',1).to('white',1));}exportdefaultmakeScene2D(function*(view){constcircles=Array.from({length: 5},()=>createRef<Circle>());constlayout=createRef<Layout>();view.add(<Layoutlayoutref={layout}gap={100}>{circles.map(ref=><Circleref={ref}size={170}fill={'white'}stroke={'white'}opacity={0}/>)}</Layout>)// position them using the layout and stop using it so we can move the nodes freely
circles.forEach(ref=>ref().save())layout().layout(false);circles.forEach(ref=>ref().restore())yield*sequence(0.15,...circles.map(ref=>appear(ref())));yield*highlightCircle(circles[0]());letswaps=20;letspeedStart=1;letspeedEnd=0.15;// use MotionCanvas' RNG so that the results are seeded
letrandom=useRandom(0xdeadbeef);for(leti=0;i<swaps;++i){lets1=Math.floor(random.nextFloat()*circles.length);lets2;do{s2=Math.floor(random.nextFloat()*circles.length);}while(s2===s1);letduration=speedStart-Math.abs(speedStart-speedEnd)/swaps*i;yield*swap(view,circles[s1](),circles[s2](),duration,);}yield*highlightCircle(circles[0]());});
frommanimimport*fromrandomimport*classShuffle(Scene):defconstruct(self):seed(0xDEADBEEF)# number of values to shufflen=5circles=[Circle(color=WHITE,fill_opacity=0.8,fill_color=WHITE).scale(0.6)for_inrange(n)]# spacing between the circlesspacing=2fori,circleinenumerate(circles):circle.shift(RIGHT*(i-(len(circles)-1)/2)*spacing)self.play(*[Write(circle)forcircleincircles])# selected circleselected=randint(0,n-1)self.play(circles[selected].animate.set_color(RED))self.play(circles[selected].animate.set_color(WHITE))# slowly increase speed when swappingswaps=13speed_start=1speed_end=0.2foriinrange(swaps):speed=speed_start-abs(speed_start-speed_end)/swaps*i# pick two random circles (ensuring a != b)a,b=sample(range(n),2)# swap with a slightly larger arc angleself.play(Swap(circles[a],circles[b]),run_time=speed,path_arc=135*DEGREES)# highlight the initial circle againself.play(circles[selected].animate.set_color(RED))self.play(circles[selected].animate.set_color(WHITE))
Triangle
Unlike Manim’s Circle.from_three_points
, Motion Canvas doesn’t have a utility function for doing this.
Instead, the following functions (along with a createDeferredEffect
) could be useful:
/**
* Cross product of two 3D points.
*/functioncross(a:[number,number,number],b:[number,number,number]):[number,number,number]{return[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]];}/**
* Calculate intersection coordinates of two lines.
*/functionlineIntersection(p1: Vector2,p2: Vector2,p3: Vector2,p4: Vector2):Vector2{constpad=(point: Vector2):[number,number,number]=>[point.x,point.y,1];constline1Padded=[pad(p1),pad(p2)];constline2Padded=[pad(p3),pad(p4)];constline1Cross=cross(line1Padded[0],line1Padded[1]);constline2Cross=cross(line2Padded[0],line2Padded[1]);constintersection=cross(line1Cross,line2Cross);const[x,y,z]=intersection;// Return the intersection point in the xy-plane
returnnewVector2(x/z,y/z);}/**
* Calculate the bisector of a given line segment.
*/functionperpendicularBisector(p1: Vector2,p2: Vector2):[Vector2,Vector2]{constdiff=p1.sub(p2);letdirection=cross([diff.x,diff.y,0],[0,0,1]);letdirectionVector=newVector2(direction[0],direction[1]);constmidpoint=p1.add(p2).div(2);return[midpoint.add(directionVector),midpoint.sub(directionVector)];}
import{Circle,Latex,Line,makeScene2D,Txt}from'@motion-canvas/2d';import{all,createDeferredEffect,createRef,delay,Reference,sequence,Vector2}from'@motion-canvas/core';import{appear}from"../../utilities";/**
* Cross product of two 3D points.
*/functioncross(a:[number,number,number],b:[number,number,number]):[number,number,number]{return[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]];}/**
* Calculate intersection coordinates of two lines.
*/functionlineIntersection(p1: Vector2,p2: Vector2,p3: Vector2,p4: Vector2):Vector2{constpad=(point: Vector2):[number,number,number]=>[point.x,point.y,1];constline1Padded=[pad(p1),pad(p2)];constline2Padded=[pad(p3),pad(p4)];constline1Cross=cross(line1Padded[0],line1Padded[1]);constline2Cross=cross(line2Padded[0],line2Padded[1]);constintersection=cross(line1Cross,line2Cross);const[x,y,z]=intersection;// Return the intersection point in the xy-plane
returnnewVector2(x/z,y/z);}/**
* Calculate the bisector of a given line segment.
*/functionperpendicularBisector(p1: Vector2,p2: Vector2):[Vector2,Vector2]{constdiff=p1.sub(p2);letdirection=cross([diff.x,diff.y,0],[0,0,1]);letdirectionVector=newVector2(direction[0],direction[1]);constmidpoint=p1.add(p2).div(2);return[midpoint.add(directionVector),midpoint.sub(directionVector)];}/**
* Calculate the position for the label of the given circle to always point towards the center of the triangle.
*/functiongetLabelPosition(circle: Reference<Circle>,circles: Array<Reference<Circle>>):Vector2{letcenter=newVector2();circles.forEach(c=>{center=center.add(c().position())});center=center.div(circles.length);constvector=circle().position().sub(center).normalized;returncircle().position().add(vector.mul(70));}exportdefaultmakeScene2D(function*(view){constcircles=Array.from({length: 3},()=>createRef<Circle>());constpositions=[newVector2(0,-300),newVector2(-300,200),newVector2(300,200)];constlabels=Array.from({length: 3},()=>createRef<Txt>());constlabelStrings=['a','b','c'];constlines=Array.from({length: 3},()=>createRef<Line>());view.add(<>{circles.map((ref,i)=><Circleref={ref}opacity={0}size={30}fill={'white'}position={positions[i]}/>)}{lines.map((ref,i)=>// always keep the lines on adjacent circles
<Linepoints={[circles[i]().position,circles[(i+1)%circles.length]().position]}ref={ref}stroke={'white'}lineWidth={8}size={30}end={0}/>)}{labels.map((ref,i)=>// always keep the label away from the center of the triangle
<Latextex={labelStrings[i]}ref={ref}fill={'white'}opacity={circles[i]().opacity}scale={circles[i]().scale}position={()=>getLabelPosition(circles[i],circles)}fontSize={70}/>)}</>)constcircumscribedCircle=createRef<Circle>();view.add(<Circleref={circumscribedCircle}size={30}lineWidth={5}start={1}stroke={'red'}zIndex={-1}/>)// change the center and size of the circumscribed circle to be within the bisectors
createDeferredEffect(()=>{letposition=lineIntersection(...perpendicularBisector(circles[0]().position(),circles[1]().position()),...perpendicularBisector(circles[1]().position(),circles[2]().position()),);letsize=position.sub(circles[0]().position()).magnitude;circumscribedCircle().position(position);circumscribedCircle().size(size*2);})// nice appearing
yield*all(sequence(0.25,...circles.map(ref=>appear(ref()))),delay(0.5,sequence(0.25,...lines.map(ref=>ref().end(1,0.5))),),delay(1.5,circumscribedCircle().start(0,1),));// move circles
yield*all(...circles.map((ref,i)=>ref().position(circles[(i+1)%circles.length]().position(),1)));// move circles again
yield*all(circles[0]().position(circles[0]().position().addX(-100),1),circles[1]().position(circles[1]().position().addX(100).addY(-300),1),circles[2]().position(circles[2]().position().addY(100),1),);});
frommanimimport*fromrandomimport*classTriangle(Scene):defconstruct(self):seed(0xDEADBEEF)# scale everything up a bitc=2p1=Dot().scale(c).shift(UP*c)p2=Dot().scale(c).shift(DOWN*c+LEFT*c)p3=Dot().scale(c).shift(DOWN*c+RIGHT*c)points=VGroup(p1,p2,p3)self.play(Write(points,lag_ratio=0.5),run_time=1.5)l1=Line()l2=Line()l3=Line()lines=VGroup(l1,l2,l3)defcreate_line_updater(a,b):"""Returns a function that acts as an updater for the given segment."""returnlambdax:x.become(Line(start=a.get_center(),end=b.get_center()))l1.add_updater(create_line_updater(p1,p2))l2.add_updater(create_line_updater(p2,p3))l3.add_updater(create_line_updater(p3,p1))self.play(Write(lines,lag_ratio=0.5),run_time=1.5)x=Tex("x")y=Tex("y")z=Tex("z")x.add_updater(lambdax:x.next_to(p1,UP))y.add_updater(lambdax:x.next_to(p2,DOWN+LEFT))z.add_updater(lambdax:x.next_to(p3,DOWN+RIGHT))labels=VGroup(x,y,z).scale(c*0.8)self.play(FadeIn(labels,shift=UP*0.2))circle=Circle()circle.add_updater(lambdac:c.become(Circle.from_three_points(p1.get_center(),p2.get_center(),p3.get_center())))self.play(Write(circle))self.play(p2.animate.shift(LEFT+UP),p1.animate.shift(RIGHT),)
Wave
Noting extra here, just a BFS.
Here is the text input that I used to generate the maze, if you wish to use it.
you can define the spline using <Knot>, which allows you to access their absolutePosition
, which will make the code a lot simpler easier (the clones will likely be scaled + rotated at this point, which doesn’t change their relative position
)
setting the smoothness of a spline to 0 will make it line segments
you can animate drawing of a spline with the end
signal
import{Knot,makeScene2D,Spline}from'@motion-canvas/2d';import{all,createRef,Reference,Vector2}from'@motion-canvas/core';exportdefaultmakeScene2D(function*(view){constgetSpline=(ref: Reference<Spline>,positions: Vector2[])=>{return<Splineref={ref}lineWidth={5}stroke={'white'}smoothness={0}end={0}>{positions.map(pos=>(<Knotposition={pos}/>))}</Spline>}letsize=300;letiterations=3;letline=createRef<Spline>();letpositions=[newVector2(-size,size),newVector2(-size,-size),newVector2(size,-size),newVector2(size,size)];view.add(getSpline(line,positions))yield*line().end(1,1);// not pretty code, but not sure how to do this cleaner / clearer
for(leti=1;i<=iterations;i++){letlineRef=line().clone();lineRef.opacity(0);view.add(lineRef)letnewSegmentScale=(2**(i)-1)/(2**(i+1)-1)yield*all(line().scale(newSegmentScale,1),line().topLeft(line().topLeft(),1),line().stroke('gray',1))letl2=line().clone();view.add(l2);yield*all(l2.topRight(lineRef.topRight(),1),)letl3=line().clone();letl4=l2.clone();view.add(l3);view.add(l4);yield*all(l3.bottomRight(lineRef.bottomLeft(),1),l3.rotation(90,1),l4.bottomLeft(lineRef.bottomRight(),1),l4.rotation(-90,1),)letnewPositionKnots=[...l3.children().reverse(),...line().children(),...l2.children(),...l4.children().reverse(),]letnewPositions=newPositionKnots.map(pos=>pos.absolutePosition().sub(lineRef.absolutePosition()));letnewLine=createRef<Spline>();view.add(getSpline(newLine,newPositions))yield*newLine().end(1,1);line().remove();l2.remove();l3.remove();l4.remove();line=newLine;}});
frommanimimport*classPath(VMobject):def__init__(self,points,*args,**kwargs):super().__init__(*args,**kwargs)self.set_points_as_corners(points)defget_important_points(self):returnlist(self.get_start_anchors())+[self.get_end_anchors()[-1]]classHilbert(Scene):defconstruct(self):points=[LEFT+DOWN,LEFT+UP,RIGHT+UP,RIGHT+DOWN]hilbert=Path(points).scale(3)self.play(Create(hilbert),rate_func=linear)foriinrange(1,6):# length of a single segment in the curvenew_segment_length=1/(2**(i+1)-1)# scale the curve such that it it is centerednew_scale=(1-new_segment_length)/2# save the previous (large) curve to align smaller ones by itlu=hilbert.copy()lu,hilbert=hilbert,luself.play(lu.animate.scale(new_scale).set_color(DARK_GRAY).align_to(hilbert,points[1]))ru=lu.copy()self.play(ru.animate.align_to(hilbert,points[2]))ld,rd=lu.copy(),ru.copy()self.play(ld.animate.align_to(hilbert,points[0]).rotate(-PI/2),rd.animate.align_to(hilbert,points[3]).rotate(PI/2),)new_hilbert=Path(list(ld.flip(LEFT).get_important_points())+list(lu.get_important_points())+list(ru.get_important_points())+list(rd.flip(LEFT).get_important_points()))# Create will be exponentially longer so it looks niceself.play(Create(new_hilbert,run_time=1.5**(i-1)),rate_func=linear)self.remove(lu,ru,ld,rd)hilbert=new_hilbert