You make one character picture and one background โ any way you like. Then, with the free tools already on your Mac (Preview and the Terminal), a little Python, and Blender, you'll cut your character into a paper-doll puppet and make it walk across the screen.
โถ Where this trick can take you: two royal cats walking and talking across a palace. You'll build the simple version โ one character, two legs, one walk.
By the end of this page you'll have walk.mp4 โ your own character strolling across your own scene. The whole project is just four little pictures and one script:
background.png (the scene) + main.png (the body) + near_leg.png + far_leg.png (the two legs) โ fed to walk.py โ out pops a movie. ๐ฌ
A cartoon looks alive, but it's a trick. Animators don't redraw the whole character for every tiny movement. They make a cut-out puppet โ like a paper doll โ and move the pieces a little at a time.
Picture a paper bird held together with split-pins at the hips: you can swing its legs without redrawing it. That's the entire recipe:
Here's our puppet โ a bird with three parts: a body and two legs, pinned at the hips. Every step below builds exactly this:
The blue dots are the pivots (the split-pins). A leg swings around its pivot โ at the hip, never the middle.
python3 in the Terminal.Before the fun starts, Blender needs to live on your Mac โ and your Terminal needs to understand the word blender. You do this once and you're set forever. (A grown-up can help with the first launch.)
.dmg, waiting in your Downloads folder).blender work in the TerminalAt the end of this guide you'll run Blender by typing blender in the Terminal. But on a Mac the real program hides inside the app, here:
So teach your Terminal a short-cut (called an alias). Open Terminal (find it with the ๐ Spotlight search) and paste these two lines, pressing Return after each:
The first line writes the short-cut into your Terminal's settings file; the second loads it. Now test it:
Blender 5.0. Now every command in this guide will work.This part is all yours โ draw, paint, photograph, or generate. You need:
background.png.character.png.Make a new folder for the project (call it walk) and put both pictures inside.
walk with two files inside: background.png and character.png.Your character needs to sit on top of the scene โ so the plain background around it must become invisible. Pictures store this as a fourth number per dot called alpha: 255 = solid, 0 = see-through.
character.png in Preview (double-click it).This does the same thing for a plain white background: it looks at every dot, and any dot that's nearly white gets its alpha set to 0. In the Terminal, run pip3 install pillow once, then save this as cutout.py in your folder and run python3 cutout.py:
from PIL import Image
img = Image.open("character.png").convert("RGBA") # RGBA = Red, Green, Blue + Alpha
dots = img.load()
w, h = img.size
for y in range(h):
for x in range(w):
r, g, b, a = dots[x, y]
if r > 240 and g > 240 and b > 240: # nearly white? that's the paper
dots[x, y] = (r, g, b, 0) # alpha 0 = invisible
img.save("character.png")
character.png โ your character sits on grey checkers (or nothing), with no colored box around it.Now the puppet magic. A character in one piece can't move its legs โ so you'll turn one picture into three: a body and two legs. Each part is just a rectangle cut from the original:
Three rectangles. The body box reaches a little below the hips, and each leg box starts exactly at the hip โ so the boxes overlap a bit.
Two rules make or break the puppet:
character.png and press โD three times. Rename the copies main.png, near_leg.png, far_leg.png.main.png in Preview. Drag a rectangle around the body โ from the top of the head down to just below the hips. Press โK (Tools โ Crop). Save with โS.near_leg.png. Drag a rectangle around just the front leg โ top edge exactly at the hip, down past the foot. โK, โS.far_leg.png. Same thing for the back leg. โK, โS.Crops are rectangles too: crop((left, top, right, bottom)) โ four numbers, all counted in pixels from the picture's top-left corner. So first you need to measure where the hips and legs are in your picture.
Open character.png in Preview and start dragging a selection at the picture's very top-left corner. The little grey badge by the cursor counts the pixels โ so wherever you stop, the badge is reading that spot's (left, top) position. Drag from the corner to the front leg's hip and the badge says something like 240 ร 575 โ you just measured: left = 240, top = 575. Measure each corner you need this way and jot the numbers down. (Press Esc to drop a selection and measure again.)
pip3 install matplotlib once, thenpython3 -c "import matplotlib.pyplot as plt; plt.imshow(plt.imread('character.png')); plt.show()"x=โฆ y=โฆ โ live pixel coordinates.Now put your numbers into this script. Save it as parts.py, run python3 parts.py:
from PIL import Image
bird = Image.open("character.png")
w, h = bird.size # the whole picture: w wide, h tall, in pixels
bird.crop(( 0, 0, w, 620)).save("main.png") # body: full width, top -> just BELOW the hips
bird.crop((240, 575, 400, h)).save("near_leg.png") # front leg: hip -> foot
bird.crop((415, 575, 575, h)).save("far_leg.png") # back leg: hip -> foot
Those numbers fit our bird โ yours will be different. But notice the overlap: the body reaches down to 620 while the legs start up at 575 โ that 45-pixel band is where the body hides the seam.
parts.py, open the three PNGs, fix the worst number, run again. Two or three rounds and it's perfect โ that's how real animators work too.background.png, main.png, near_leg.png, far_leg.png โ and each leg file shows only a leg, starting at its hip.In a paper puppet you'd push a split-pin through the hip. In Blender, the pin is an invisible point called an Empty: the leg is attached ("parented") to it, so when the pin turns, the whole leg swings from the hip.
The script you'll run (walk.py โ download in step 6) does this for you, automatically:
A walk is just legs taking turns. Each frame of the video, the script rotates the two hip-pins by a few degrees โ in opposite directions:
One leg swings forward while the other swings back โ exactly like your own legs.
Inside walk.py it's one little formula, run once per frame:
angle = SWING_DEG * sin(...) # a smooth back-and-forth wave near_hip.rotate( angle) # near leg swings one way... far_hip.rotate(-angle) # ...far leg swings the OPPOSITE way
sin(...) makes a smooth wave that glides from +1 to โ1 and back โ so the legs speed up and slow down gently instead of snapping. The minus sign on the far leg is the whole secret of walking.
SWING_DEG (how far the legs kick) and STEPS_SEC (how fast they step).Swinging legs alone is walking on the spot. The last trick: every frame, the script also slides the whole puppet (body, pins, legs โ everything) a little to the left:
Legs swinging + the whole puppet sliding = a real walk across the scene.
walk folder, next to the four pictures.cd with a space, drag the folder from Finder onto the Terminal window, press Return).-b means "no window", -P means "run this Python file":Blender draws all 100 frames (a minute or two), then drops walk.mp4 into the folder. Double-click it.
Open walk.py in any text editor โ the top has friendly knobs. Change one, run the command again, watch what happens:
| Knob | What it does | Try |
|---|---|---|
SECONDS | how long the walk lasts | 8 for a slow stroll |
SWING_DEG | how far the legs kick | 35 for a silly march |
STEPS_SEC | steps per second | 4 for scurrying |
TRAVEL | how far across it walks | 0.45 edge to edge |
Here it is โ all of it. Every line has a comment saying what it does, and it's the exact same recipe you just learned: flat pictures โ pins at the hips โ opposite swings โ slide across โ render. Grab it either way:
"""walk.py - animate a 4-image puppet walking right->left, render to an MP4.
Run it with Blender, no window needed:
blender -b -P walk.py -- /path/to/folder
The folder must contain four pictures:
background.png main.png near_leg.png far_leg.png
(the character parts should be PNGs with a see-through background)
"""
import bpy, sys, os, math
# ---- knobs you can tweak ----------------------------------------------------
FPS = 25 # frames per second
SECONDS = 4 # how long the walk lasts
SWING_DEG = 22 # how far the legs swing, in degrees
STEPS_SEC = 2 # how many steps per second
WORLD_W = 10.0 # width of the scene, in Blender units
RES_X, RES_Y = 1280, 720
CHAR_FRAC = 0.55 # the character's height as a fraction of the screen height
TRAVEL = 0.40 # how far across to walk (fraction of the screen each side)
# ----------------------------------------------------------------------------
# read the folder path that comes after the "--" on the command line
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
folder = argv[0] if argv else "."
FRAMES = FPS * SECONDS
FRAME_W = WORLD_W # the camera frame is WORLD_W units wide
FRAME_H = WORLD_W * RES_Y / RES_X # ...and this tall (keeps the 16:9 shape)
# start from a totally empty scene
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.render.fps = FPS
scene.frame_start, scene.frame_end = 0, FRAMES
def flat_material(img):
"""A material that shows a picture flat (no lighting) and respects its alpha."""
mat = bpy.data.materials.new(img.name)
mat.use_nodes = True
nt = mat.node_tree
nt.nodes.clear()
tex = nt.nodes.new("ShaderNodeTexImage"); tex.image = img
emit = nt.nodes.new("ShaderNodeEmission")
clear = nt.nodes.new("ShaderNodeBsdfTransparent")
mix = nt.nodes.new("ShaderNodeMixShader")
out = nt.nodes.new("ShaderNodeOutputMaterial")
nt.links.new(tex.outputs["Color"], emit.inputs["Color"]) # picture colour -> glows flat
nt.links.new(tex.outputs["Alpha"], mix.inputs["Fac"]) # picture alpha -> see-through-ness
nt.links.new(clear.outputs["BSDF"], mix.inputs[1]) # where alpha=0 -> transparent
nt.links.new(emit.outputs["Emission"], mix.inputs[2]) # where alpha=1 -> the picture
nt.links.new(mix.outputs["Shader"], out.inputs["Surface"])
for attr, val in (("blend_method", "BLEND"), ("surface_render_method", "BLENDED")):
try: setattr(mat, attr, val) # the name for "let alpha show" differs by Blender version
except Exception: pass
return mat
def plane(name, filename, z):
"""Make a flat plane that displays one picture. Returns the plane + its pixel size."""
img = bpy.data.images.load(os.path.join(folder, filename))
w, h = img.size
bpy.ops.mesh.primitive_plane_add(size=2) # a 2x2 plane (so half-width = 1 before scaling)
pl = bpy.context.active_object
pl.name = name
pl.rotation_euler = (0, 0, 0)
pl.location.z = z # bigger z = nearer the camera = drawn in front
pl.data.materials.append(flat_material(img))
return pl, w, h
def fit(pl, w_px, h_px, units_per_px):
"""Scale a plane so each pixel is the same size everywhere (keeps shapes correct)."""
pl.scale = (w_px * units_per_px / 2, h_px * units_per_px / 2, 1)
# --- background: scale it to COVER the whole frame -----------------------------
bg, bw, bh = plane("background", "background.png", 0.0)
cover = max(FRAME_W / bw, FRAME_H / bh) # the bigger ratio guarantees full coverage
fit(bg, bw, bh, cover)
# --- character parts: all share ONE pixel scale so body + legs stay consistent -
body, mw, mh = plane("main", "main.png", 0.3)
near, nw, nh = plane("near_leg", "near_leg.png", 0.2)
far, fw, fh = plane("far_leg", "far_leg.png", 0.1)
upx = (CHAR_FRAC * FRAME_H) / mh # pick a scale so the body is CHAR_FRAC tall
for pl, (w, h) in ((body, (mw, mh)), (near, (nw, nh)), (far, (fw, fh))):
fit(pl, w, h, upx)
# vertical layout: feet on the ground, hips above the feet, body stacked on the hips
FOOT_Y = -FRAME_H / 2 + 0.20 # the feet rest just above the bottom edge
HIP_Y = FOOT_Y + nh * upx # the hips sit one leg-length up from the feet
# place the body: centred left-right, resting on the hips (overlapping them a touch)
body.location = (0, HIP_Y - 0.15 + mh * upx / 2, 0.3)
bpy.context.view_layer.update() # make Blender recompute positions before we read them
def hip(leg, w_px, h_px, x_off, z):
"""Give a leg a pivot (an Empty) at its TOP edge, so it swings from the hip."""
leg.location = (x_off, FOOT_Y + h_px * upx / 2, z) # stand the leg on the ground
pin = bpy.data.objects.new(leg.name + "_hip", None) # an Empty = an invisible pin
scene.collection.objects.link(pin)
pin.location = (x_off, FOOT_Y + h_px * upx, z) # pin at the TOP of the leg = the hip
bpy.context.view_layer.update()
leg.parent = pin # the leg now hangs from the pin...
leg.matrix_parent_inverse = pin.matrix_world.inverted() # ...without jumping when parented
return pin
near_hip = hip(near, nw, nh, -0.12 * mw * upx, 0.2) # front leg, slightly forward
far_hip = hip(far, fw, fh, +0.12 * mw * upx, 0.1) # back leg, slightly back
# one ROOT pin that carries the whole character, so we can slide it across
root = bpy.data.objects.new("root", None) # sits at the origin (0,0,0)
scene.collection.objects.link(root)
bpy.context.view_layer.update()
for ob in (body, near_hip, far_hip): # body + both hips ride on the root
ob.parent = root
ob.matrix_parent_inverse = root.matrix_world.inverted()
# --- the animation: keyframe every frame -------------------------------------
for f in range(FRAMES + 1):
t = f / FPS
angle = math.radians(SWING_DEG) * math.sin(2 * math.pi * STEPS_SEC * t) # smooth back-and-forth
near_hip.rotation_euler[2] = angle # legs swing in OPPOSITE directions...
far_hip.rotation_euler[2] = -angle # ...so it looks like real walking
near_hip.keyframe_insert("rotation_euler", index=2, frame=f)
far_hip.keyframe_insert("rotation_euler", index=2, frame=f)
root.location.x = FRAME_W * TRAVEL - 2 * FRAME_W * TRAVEL * (f / FRAMES) # slide right -> left
root.keyframe_insert("location", index=0, frame=f)
# --- an orthographic camera, looking straight on for a flat 2D look ----------
cam_data = bpy.data.cameras.new("cam")
cam_data.type = 'ORTHO'
cam_data.ortho_scale = WORLD_W
cam = bpy.data.objects.new("cam", cam_data)
scene.collection.objects.link(cam)
cam.location = (0, 0, 10) # above the planes, looking down the -Z axis
scene.camera = cam
# --- render straight to an MP4 -----------------------------------------------
scene.render.resolution_x, scene.render.resolution_y = RES_X, RES_Y
try: scene.render.image_settings.media_type = 'VIDEO' # Blender 5.x: switch to video output first
except Exception: pass # (older Blender has no media_type)
scene.render.image_settings.file_format = 'FFMPEG'
scene.render.ffmpeg.format = 'MPEG4'
scene.render.ffmpeg.codec = 'H264'
scene.render.filepath = os.path.join(folder, "walk.mp4")
bpy.ops.render.render(animation=True)
print("Saved", scene.render.filepath)
walk.py in your walk folder (not .txt, not .rtf!). Downloading skips all that โ the file lands in Downloads, ready to drag into your folder.main.png a little lower.Same recipe, different puppet: a flat Peppa-style dog crossing a sunny field โ a scene, a cut-out character, a body and two legs, pinned at the hips, swinging opposite, sliding across.
โถ Built with the exact same six steps. Now try it with your character!
Your character can walk โ give it a voice next. The companion guide shows four ways to make a cartoon talk: AI lip-sync with Wav2Lip, sharpening the face, the on-style mouth-transplant trick, and โ for shapes-and-lines characters like a Peppa-style piglet โ drawing a mouth that opens to the sound.
walk.mp4.blender -b -P walk.py -- ., runs the whole show.Liked making things move? There are two more ways to build cartoons on this site โ one where AI draws and animates for you in the cloud, and one where the AI art studio runs on your own Mac: