"""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)
