Animation: animated PNG, frames and GIF loops
Animate a render: format=apng returns the whole pose cycle as an animated PNG, with frames, fps and loop, and stepping frame yourself still builds a GIF.
Two ways to animate
The animated poses are cycles: walk, run, dance, wave each run from one end of a loop to the other as frame goes from 0 to 1. There are two ways to turn that into something that moves.
- Ask for the whole loop at once with
format=apng. One request, one file, an animated PNG you can put straight in an<img>. - Render the frames yourself by stepping
frame, and assemble them with whatever encoder you already have. More requests, more code, and still the only way to get a GIF, because the API does not produce GIFs.
If you want a moving image on a web page, use the first. If you need a GIF specifically — an old forum, a signature image, a chat platform that will not play APNG — use the second.
format=apng
format=apng on a bust or body render returns an animated PNG of the entire pose cycle rather than a single frame. format=png is the default and is exactly what it always was: one still image, same bytes as before.
https://skinrender.dev/render/Notch/body?pose=walk&format=apng
https://skinrender.dev/render/Notch/body.apng?pose=walk The two lines above are the same request. The .apng ending is the animated counterpart of the .png ending on a still render: it exists for the forum tags, webhooks and site builders that will not accept a URL unless it ends in a file extension, and it means exactly format=apng.
An animated PNG is a PNG. Every browser released in the last several years plays one in an ordinary <img> tag, and anything that does not play it shows the first frame instead of breaking.
<img src="https://skinrender.dev/render/853c80ef3c3749fdaa49938b674adae6/body.apng?pose=run&size=256"
width="160" height="256" alt="jeb_ running" loading="lazy"> frames, fps and loop
Three parameters describe the animation itself. They mean nothing to a still render and are ignored there.
| Parameter | Range | Default | What it does |
|---|---|---|---|
format | png | apng | png | Whether the response is a single frame or the whole cycle as an animated PNG. |
frames | 2 – 60 | 16 | How many frames one loop is sampled at. More frames are smoother and larger; the cycle takes the same time either way. |
fps | 1 – 50 | 20 | Playback rate. With the defaults, 16 frames at 20 fps is a loop of 0.8 seconds. |
loop | 0 – 100 | 0 | How many times it plays before stopping. 0, the default, loops forever. |
The loop length in seconds is frames ÷ fps, so a slower walk is fps=10 rather than more frames, and a smoother one at the same speed is frames=32&fps=40.
https://skinrender.dev/render/Notch/body?pose=walk&format=apng&frames=24&fps=24
https://skinrender.dev/render/Notch/bust?pose=wave&format=apng&loop=3&size=256 What can be animated
- Only the animated poses may be animated:
walk, run, dance, wave. Every other pose is a fixed set of angles with nothing to cycle through, soformat=apngon one of them is a400rather than a file with sixteen identical frames in it. - Only
bustandbody, the two render types that have a pose. The flat types have no pose at all, and aheadis a single box with nothing to swing, sohead.apngis a400that says so. frameis ignored withformat=apng. The response is the whole cycle, so asking for one phase of it means nothing.fitdefaults to false withformat=apng, the opposite of its default on a still render. Auto-fit frames each image to fill the canvas on its own, which makes a swinging arm change the scale from frame to frame and the model appear to jitter; with it off the camera is fixed for the whole loop. Passfit=trueif you want the jitter.
Everything else — size, bg, the camera, the layer toggles, the per-joint overrides — applies to every frame, exactly as it applies to a still render.
Animating a mob
The mob renders take format=apng, frames, fps and loop in exactly the same way, on the body render with a pose the mob declares as a cycle. Each mob has its own list — a chicken flaps, a wolf does not — and its page says which of its poses cycle.
https://skinrender.dev/render/mob/creeper/body.apng?pose=walk
https://skinrender.dev/render/mob/zombie/body.apng?pose=walk
https://skinrender.dev/render/mob/skeleton/body.apng?pose=walk A mob head render is refused, the way a player head is: the head may bob within a walk cycle, but a file of sixteen nearly identical frames is not what the request meant. Use frame on the head render if you do want that one phase.
Stepping frame yourself
The older approach, and still the right one when you need a specific container: request each frame as its own PNG and assemble them. frame is the phase of the cycle from 0 to 1, frame=1 is the same picture as frame=0, and the frames of an n-frame loop are therefore 0, 1/n, 2/n ….
Pass fit=false on every frame. This is the same reason format=apng turns it off by default: with auto-fit on, each frame is scaled individually and the animation wobbles.
https://skinrender.dev/render/069a79f444e94726a5befca90e38aaf5/body?size=256&fit=false&pose=walk&frame=0.25 Each frame is an ordinary cacheable PNG, so re-assembling the animation later costs nothing, and a sprite sheet is the same loop laid out in a row instead of stacked in time.
Making a GIF
The API returns PNG frames and animated PNGs; it does not return GIFs. Encoding one is a step you run yourself, with a tool you almost certainly already have.
for f in 0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9; do
curl -s -o "walk-$f.png" \
"https://skinrender.dev/render/069a79f444e94726a5befca90e38aaf5/body?pose=walk&fit=false&size=256&frame=$f"
done
# ImageMagick (frames sorted numerically; 10 fps; keep transparency)
convert -delay 10 -loop 0 -dispose previous $(ls walk-*.png | sort -V) walk.gif import requests
from PIL import Image
from io import BytesIO
base = "https://skinrender.dev/render/069a79f444e94726a5befca90e38aaf5/body"
frames = []
for i in range(16):
r = requests.get(base, params={"pose": "run", "fit": "false", "size": 256, "frame": i / 16})
r.raise_for_status()
frames.append(Image.open(BytesIO(r.content)).convert("RGBA"))
frames[0].save("run.gif", save_all=True, append_images=frames[1:],
duration=50, loop=0, disposal=2, transparency=0) A GIF has 256 colours and one bit of transparency, so a Minecraft skin survives it well but the edges of a supersampled render do not: use aa=1 for a GIF, or accept a fringe. An animated PNG has none of those limits, which is why it is what the API produces.
File size and support
An animated PNG is roughly one frame multiplied by the number of frames, less whatever the encoder saves between them: doubling frames roughly doubles the file, and so does doubling size, since the canvas grows in both directions. Ten to twenty frames is enough for a walk or a run and twenty-four is smoother for a wave or a dance, so the defaults are a deliberate middle. Sixty frames at 1024 px is a large download and not something to put in a page that lists a hundred players.
The animated response is cached and revalidated exactly like a still one: the same URL is the same bytes, it carries an ETag, and the parameters that describe the animation are part of the cache key like every other parameter.
Mob renders take frame in the same way for their own cycling poses — see mob poses — and the poses reference lists which player poses are cycles and what each one does.