{"course":{"slug":"motion-and-audio","title":"Motion, Sound and Music","url":"https://addaly.com/learn/motion-and-audio","level":"intermediate","track":"create"},"lesson":{"slug":"sprite-sheets","title":"Frames in a grid, and the memory they cost","url":"https://addaly.com/learn/motion-and-audio/sprite-sheets","position":24,"of":72,"minutes":9,"answer":"A sprite sheet gives instant random access to any frame with steps() timing, but its cost is decoded memory (width times height times four bytes.","keyIdea":"A sprite sheet gives instant random access to any frame with steps() timing, but its cost is decoded memory (width times height times four bytes, regardless of how well the PNG compressed) which is why a 4096-square sheet can cost 67MB and evict everything else on the page."},"body":{"format":"markdown","text":"## When frames are the only honest representation\n\nSome animation is not describable as shapes with animated properties. Hand-drawn frame-by-frame work, a painted effect, a cel-animated character with squash that no rig would produce, a rendered 3D loop with real lighting. For these, the animation *is* the sequence of pictures, and the format that admits this is a sprite sheet: every frame laid out in a grid in one image, shown one at a time.\n\nIt sounds primitive. It is also the fastest possible playback, because there is no decode work per frame and no geometry to rebuild: the browser or GPU is showing a different window onto an image it already holds.\n\n## The CSS, which is shorter than people expect\n\nOne image, 24 frames in a row, each 200px wide:\n\n```css\n.flame {\n  width: 200px; height: 200px;\n  background: url(/motion/flame.png) 0 0 / 4800px 200px;\n  animation: play 1s steps(24) infinite;\n}\n@keyframes play { to { background-position: -4800px 0; } }\n```\n\nThe whole technique is in `steps(24)`. A normal timing function interpolates smoothly, which would slide the image continuously and show two half-frames at once. `steps()` holds each value and jumps, so the background position lands exactly on frame boundaries.\n\nThe end position is the *full* sheet width, not the width minus one frame. With `steps(24)`, the animation is divided into 24 equal holds, and the last hold shows frame 24: the step after that would be the wrap. Getting this off by one is the classic bug: a flash of blank at the end of every loop means you subtracted a frame you should not have.\n\nFor a grid rather than a strip, animate `background-position` in two keyframe tracks, or use two nested animations: one stepping across columns fast, one stepping down rows slowly.\n\n## The number that matters is memory, not file size\n\nA PNG sprite sheet might be 400KB on disk. That is not what it costs.\n\n> Figure: What a sprite sheet costs in memory, whatever the PNG weighed. Bars (MB of decoded pixels): 4800 × 200 strip, 24 frames 3.8; 2048 × 2048 sheet 16.8; 4096 × 4096 sheet 67. Width times height times four bytes, held for as long as the element exists, regardless of how well the file compressed on disk. This is the usual way a sprite-sheet animation kills a phone, and the symptom is not a slow animation: it is the tab reloading, or unrelated images flickering as they are evicted and decoded again.\n\nTo display it, the browser decodes it to uncompressed pixels: width × height × 4 bytes. A 4800×200 strip is 3.8MB in memory. A 4096×4096 sheet (the size people reach for when they have a hundred frames) is 67MB, and that is per sheet, held for as long as the element exists.\n\nThis is the single most common way a sprite-sheet animation kills a phone. The symptoms are not a slow animation; they are the whole tab reloading, or other images being evicted and re-decoded, which looks like unrelated flickering elsewhere on the page.\n\nConstraints that follow:\n\n- **Cap the total pixel area.** Many mobile GPUs cap a single texture at 4096×4096; some older ones at 2048. Exceed it and the image may silently fail to render, or be downscaled.\n- **Keep the frame count honest.** Twelve frames looping at 12fps reads as animation. Ninety-six frames at 60fps costs eight times the memory for a difference most viewers will not name.\n- **Use WebP for the sheet.** Lossy WebP with alpha is typically a third of the PNG size on disk. The decoded memory is identical, so this only helps download, but download is still worth a third.\n\n## Choosing a frame rate deliberately\n\nFrame-by-frame animation does not have to run at the display rate. Traditional animation has always worked \"on twos\" (a new drawing every second frame, so 12 drawings a second at 24fps) and it reads as smooth for most subjects while halving the work and the memory.\n\nFast action sometimes needs ones. Slow drifting motion can go on threes or fours. Choosing per shot rather than globally is how hand-drawn animation has always been budgeted, and it applies directly here: the frame count is your file size and your memory.\n\n## Where sprite sheets are still the right answer\n\n- **Short, looping, drawn effects**: a flame, a sparkle, a loading character.\n- **Scroll-driven sequences** where the user controls the frame. Showing frame 37 of 60 as a background-position is instant; seeking a video to an exact frame is not, because a video decoder must decode from the last keyframe forward.\n- **Anything inside a game engine**, where texture atlases are the native idiom.\n- **Interfaces where the animation must not depend on a runtime library.**\n\nThat second case is worth underlining. The product-page effect where an object rotates as you scroll is almost always a frame sequence, not a video, precisely because video seeking is unreliable and a sprite sheet's random access is free.\n\n## Building one\n\n```bash\n# frames out of a source clip, 12 per second\nffmpeg -i source.mov -vf fps=12,scale=200:-1 frames/f_%03d.png\n\n# assemble a single row\nffmpeg -i frames/f_%03d.png -filter_complex tile=24x1 sheet.png\n\n# and a smaller version for download\ncwebp -q 80 sheet.png -o sheet.webp\n```\n\n`tile=6x4` gives a grid instead. **ImageMagick**'s `montage` does the same job with more control over padding, and padding matters: leave none, or GPU texture filtering will bleed the neighbouring frame's edge pixels into yours.\n\n## The free path\n\nEverything above is free. **ffmpeg** extracts and tiles. **ImageMagick** assembles and trims. **Krita** and **OpenToonz** are both open source and both do genuine frame-by-frame animation with onion skinning. OpenToonz is the tool Studio Ghibli's in-house system became. **Blender**'s grease pencil does drawn animation inside a 3D scene and renders sequences directly. **TexturePacker** is the paid standard for atlases and **free-tex-packer** does the same job open source.\n\n## The limitation\n\nSprite sheets do not scale: in either sense. You cannot display one larger than it was drawn without softness, so a responsive layout means either shipping the largest size to everyone or shipping several sheets. And the cost grows linearly with every frame and every pixel, with no compression across frames at all, which is exactly the weakness that video codecs exist to fix.\n\nThe moment your sequence is longer than a couple of seconds, or larger than a few hundred pixels square, the arithmetic turns against you and a video becomes the correct format, with all the seeking limitations that brings back."},"check":{"question":"A 4096x4096 PNG sprite sheet compresses to 800KB on disk. A tester reports that on an older phone, opening the page causes unrelated images elsewhere to flicker and reload. What is the mechanism?","options":[{"text":"The 800KB download is saturating the connection, so other images are being fetched after a timeout and re-requested.","correct":false,"tempting":"Reasons from file size, which is the number that is not causing the problem here."},{"text":"The steps() timing function forces a repaint of the whole document on each step, and repaints evict other decoded images from the browser's cache.","correct":false,"tempting":"A background-position change repaints the element, not the document, and repainting does not evict decoded images."},{"text":"The sheet occupies about 67MB once decoded, and under memory pressure the browser discards other decoded images and re-decodes them when they are needed.","correct":true},{"text":"The sheet exceeds the maximum texture size on that device's GPU, so the compositor is refusing to upload it as a single texture and is instead re-uploading tiles of it on every frame, which starves the texture memory that other images are using.","correct":false,"tempting":"Texture limits are a real hazard at this size, but they cause the sheet itself to fail or downscale rather than producing repeated eviction of unrelated images."}]},"prev":{"slug":"alpha-on-the-web","title":"Transparent video, and why it is two files","url":"https://addaly.com/learn/motion-and-audio/alpha-on-the-web"},"next":{"slug":"interactive-motion","title":"Motion that responds to state","url":"https://addaly.com/learn/motion-and-audio/interactive-motion"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Frames in a grid, and the memory they cost', in Motion, Sound and Music. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/motion-and-audio/sprite-sheets (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Frames in a grid, and the memory they cost","courseTitle":"Motion, Sound and Music","module":{"index":3,"slug":"shipping-the-motion","title":"Shipping the motion"},"answer":null,"keyIdea":"A sprite sheet gives instant random access to any frame with steps() timing, but its cost is decoded memory (width times height times four bytes, regardless of how well the PNG compressed) which is why a 4096-square sheet can cost 67MB and evict everything else on the page.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"type":"bars","unit":"MB of decoded pixels","title":"What a sprite sheet costs in memory, whatever the PNG weighed","series":[{"label":"4800 × 200 strip, 24 frames","value":3.8},{"label":"2048 × 2048 sheet","value":16.8},{"label":"4096 × 4096 sheet","value":67,"highlight":true}],"caption":"Width times height times four bytes, held for as long as the element exists, regardless of how well the file compressed on disk. This is the usual way a sprite-sheet animation kills a phone, and the symptom is not a slow animation: it is the tab reloading, or unrelated images flickering as they are evicted and decoded again."},"alt":"Figure: What a sprite sheet costs in memory, whatever the PNG weighed. Bars (MB of decoded pixels): 4800 × 200 strip, 24 frames 3.8; 2048 × 2048 sheet 16.8; 4096 × 4096 sheet 67. Width times height times four bytes, held for as long as the element exists, regardless of how well the file compressed on disk. This is the usual way a sprite-sheet animation kills a phone, and the symptom is not a slow animation: it is the tab reloading, or unrelated images flickering as they are evicted and decoded again."}},"blocks":[{"t":"h2","id":"when-frames-are-the-only-honest-representation","runs":[{"text":"When frames are the only honest representation"}]},{"t":"p","runs":[{"text":"Some animation is not describable as shapes with animated properties. Hand-drawn frame-by-frame work, a painted effect, a cel-animated character with squash that no rig would produce, a rendered 3D loop with real lighting. For these, the animation "},{"text":"is","i":true},{"text":" the sequence of pictures, and the format that admits this is a sprite sheet: every frame laid out in a grid in one image, shown one at a time."}]},{"t":"p","runs":[{"text":"It sounds primitive. It is also the fastest possible playback, because there is no decode work per frame and no geometry to rebuild: the browser or GPU is showing a different window onto an image it already holds."}]},{"t":"h2","id":"the-css-which-is-shorter-than-people-expect","runs":[{"text":"The CSS, which is shorter than people expect"}]},{"t":"p","runs":[{"text":"One image, 24 frames in a row, each 200px wide:"}]},{"t":"code","lang":"css","label":"CSS","text":".flame {\n  width: 200px; height: 200px;\n  background: url(/motion/flame.png) 0 0 / 4800px 200px;\n  animation: play 1s steps(24) infinite;\n}\n@keyframes play { to { background-position: -4800px 0; } }"},{"t":"p","runs":[{"text":"The whole technique is in "},{"text":"steps(24)","code":true},{"text":". A normal timing function interpolates smoothly, which would slide the image continuously and show two half-frames at once. "},{"text":"steps()","code":true},{"text":" holds each value and jumps, so the background position lands exactly on frame boundaries."}]},{"t":"p","runs":[{"text":"The end position is the "},{"text":"full","i":true},{"text":" sheet width, not the width minus one frame. With "},{"text":"steps(24)","code":true},{"text":", the animation is divided into 24 equal holds, and the last hold shows frame 24: the step after that would be the wrap. Getting this off by one is the classic bug: a flash of blank at the end of every loop means you subtracted a frame you should not have."}]},{"t":"p","runs":[{"text":"For a grid rather than a strip, animate "},{"text":"background-position","code":true},{"text":" in two keyframe tracks, or use two nested animations: one stepping across columns fast, one stepping down rows slowly."}]},{"t":"h2","id":"the-number-that-matters-is-memory-not-file-size","runs":[{"text":"The number that matters is memory, not file size"}]},{"t":"p","runs":[{"text":"A PNG sprite sheet might be 400KB on disk. That is not what it costs."}]},{"t":"p","runs":[{"text":"To display it, the browser decodes it to uncompressed pixels: width × height × 4 bytes. A 4800×200 strip is 3.8MB in memory. A 4096×4096 sheet (the size people reach for when they have a hundred frames) is 67MB, and that is per sheet, held for as long as the element exists."}]},{"t":"p","runs":[{"text":"This is the single most common way a sprite-sheet animation kills a phone. The symptoms are not a slow animation; they are the whole tab reloading, or other images being evicted and re-decoded, which looks like unrelated flickering elsewhere on the page."}]},{"t":"p","runs":[{"text":"Constraints that follow:"}]},{"t":"ul","items":[[{"text":"Cap the total pixel area.","b":true},{"text":" Many mobile GPUs cap a single texture at 4096×4096; some older ones at 2048. Exceed it and the image may silently fail to render, or be downscaled."}],[{"text":"Keep the frame count honest.","b":true},{"text":" Twelve frames looping at 12fps reads as animation. Ninety-six frames at 60fps costs eight times the memory for a difference most viewers will not name."}],[{"text":"Use WebP for the sheet.","b":true},{"text":" Lossy WebP with alpha is typically a third of the PNG size on disk. The decoded memory is identical, so this only helps download, but download is still worth a third."}]]},{"t":"h2","id":"choosing-a-frame-rate-deliberately","runs":[{"text":"Choosing a frame rate deliberately"}]},{"t":"p","runs":[{"text":"Frame-by-frame animation does not have to run at the display rate. Traditional animation has always worked “on twos” (a new drawing every second frame, so 12 drawings a second at 24fps) and it reads as smooth for most subjects while halving the work and the memory."}]},{"t":"p","runs":[{"text":"Fast action sometimes needs ones. Slow drifting motion can go on threes or fours. Choosing per shot rather than globally is how hand-drawn animation has always been budgeted, and it applies directly here: the frame count is your file size and your memory."}]},{"t":"h2","id":"where-sprite-sheets-are-still-the-right-answer","runs":[{"text":"Where sprite sheets are still the right answer"}]},{"t":"ul","items":[[{"text":"Short, looping, drawn effects","b":true},{"text":": a flame, a sparkle, a loading character."}],[{"text":"Scroll-driven sequences","b":true},{"text":" where the user controls the frame. Showing frame 37 of 60 as a background-position is instant; seeking a video to an exact frame is not, because a video decoder must decode from the last keyframe forward."}],[{"text":"Anything inside a game engine","b":true},{"text":", where texture atlases are the native idiom."}],[{"text":"Interfaces where the animation must not depend on a runtime library.","b":true}]]},{"t":"p","runs":[{"text":"That second case is worth underlining. The product-page effect where an object rotates as you scroll is almost always a frame sequence, not a video, precisely because video seeking is unreliable and a sprite sheet’s random access is free."}]},{"t":"h2","id":"building-one","runs":[{"text":"Building one"}]},{"t":"code","lang":"bash","label":"Bash","text":"# frames out of a source clip, 12 per second\nffmpeg -i source.mov -vf fps=12,scale=200:-1 frames/f_%03d.png\n\n# assemble a single row\nffmpeg -i frames/f_%03d.png -filter_complex tile=24x1 sheet.png\n\n# and a smaller version for download\ncwebp -q 80 sheet.png -o sheet.webp"},{"t":"p","runs":[{"text":"tile=6x4","code":true},{"text":" gives a grid instead. "},{"text":"ImageMagick","b":true},{"text":"’s "},{"text":"montage","code":true},{"text":" does the same job with more control over padding, and padding matters: leave none, or GPU texture filtering will bleed the neighbouring frame’s edge pixels into yours."}]},{"t":"h2","id":"the-free-path","runs":[{"text":"The free path"}],"role":"free"},{"t":"p","runs":[{"text":"Everything above is free. "},{"text":"ffmpeg","b":true},{"text":" extracts and tiles. "},{"text":"ImageMagick","b":true},{"text":" assembles and trims. "},{"text":"Krita","b":true},{"text":" and "},{"text":"OpenToonz","b":true},{"text":" are both open source and both do genuine frame-by-frame animation with onion skinning. OpenToonz is the tool Studio Ghibli’s in-house system became. "},{"text":"Blender","b":true},{"text":"’s grease pencil does drawn animation inside a 3D scene and renders sequences directly. "},{"text":"TexturePacker","b":true},{"text":" is the paid standard for atlases and "},{"text":"free-tex-packer","b":true},{"text":" does the same job open source."}]},{"t":"h2","id":"the-limitation","runs":[{"text":"The limitation"}],"role":"limit"},{"t":"p","runs":[{"text":"Sprite sheets do not scale: in either sense. You cannot display one larger than it was drawn without softness, so a responsive layout means either shipping the largest size to everyone or shipping several sheets. And the cost grows linearly with every frame and every pixel, with no compression across frames at all, which is exactly the weakness that video codecs exist to fix."}]},{"t":"p","runs":[{"text":"The moment your sequence is longer than a couple of seconds, or larger than a few hundred pixels square, the arithmetic turns against you and a video becomes the correct format, with all the seeking limitations that brings back."}]}],"check":{"question":[{"text":"A 4096x4096 PNG sprite sheet compresses to 800KB on disk. A tester reports that on an older phone, opening the page causes unrelated images elsewhere to flicker and reload. What is the mechanism?"}],"options":[{"text":[{"text":"The 800KB download is saturating the connection, so other images are being fetched after a timeout and re-requested."}],"correct":false,"tempting":[{"text":"Reasons from file size, which is the number that is not causing the problem here."}]},{"text":[{"text":"The steps() timing function forces a repaint of the whole document on each step, and repaints evict other decoded images from the browser’s cache."}],"correct":false,"tempting":[{"text":"A background-position change repaints the element, not the document, and repainting does not evict decoded images."}]},{"text":[{"text":"The sheet occupies about 67MB once decoded, and under memory pressure the browser discards other decoded images and re-decodes them when they are needed."}],"correct":true,"tempting":null},{"text":[{"text":"The sheet exceeds the maximum texture size on that device’s GPU, so the compositor is refusing to upload it as a single texture and is instead re-uploading tiles of it on every frame, which starves the texture memory that other images are using."}],"correct":false,"tempting":[{"text":"Texture limits are a real hazard at this size, but they cause the sheet itself to fail or downscale rather than producing repeated eviction of unrelated images."}]}],"order":[1,0,2,3],"why":null},"prev":{"slug":"alpha-on-the-web","title":"Transparent video, and why it is two files","minutes":10},"next":{"slug":"interactive-motion","title":"Motion that responds to state","minutes":10}}}