Canvas sprite animation: keep trim and time
HTML for this example
Select the code and use your copy shortcut. Changes in this preview are temporary.
<!doctype html>
<html lang="en-US"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Canvas sprite animation: keep trim and time</title><style>:root{--paper:#f6f3fc;--ink:#302c43;--accent:#65478f;--surface:#fff;--border:#d8cee5;--wash:#eee7f6;--muted:#5e526e;--title:system-ui,sans-serif;--radius:12px;--buttonText:#fff}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:17px/1.65 system-ui,sans-serif}main{max-width:980px;margin:auto;padding:24px 26px 48px}header{margin-bottom:20px;border-bottom:2px solid var(--accent);padding-bottom:16px}h1{font:700 clamp(28px,5vw,42px)/1.18 var(--title);margin:0 0 10px;letter-spacing:-.025em}h2{font-size:23px;line-height:1.35;margin:24px 0 12px}h3{font-size:19px}p{margin:0 0 16px}.eyebrow{font-size:16px;color:var(--accent);font-weight:700;margin:0 0 5px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:22px}.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:22px;min-width:0;margin-bottom:20px}label{display:block;font-weight:650;margin-bottom:14px}input,textarea,select{display:block;font:inherit;line-height:1.5;width:100%;min-width:0;color:var(--ink);background:var(--surface);border:1px solid var(--accent);border-radius:5px;padding:9px 11px;margin-top:5px}textarea{resize:vertical}button{font:650 16px/1.5 system-ui;min-height:44px;padding:9px 16px;color:var(--buttonText);background:var(--accent);border:1px solid var(--accent);border-radius:5px;cursor:pointer;margin:4px 6px 4px 0}button[aria-pressed=true]{outline:3px solid var(--accent);outline-offset:2px}button:disabled{opacity:.55;cursor:default}:focus-visible{outline:3px solid #b86d10;outline-offset:3px}.result{border-left:4px solid var(--accent);background:var(--wash);padding:15px;margin:16px 0;white-space:pre-line;overflow-wrap:anywhere;font-weight:600}.result:empty{display:none}pre{font:16px/1.55 ui-monospace,Consolas,monospace;background:var(--wash);padding:16px;white-space:pre-wrap;overflow-wrap:anywhere;border:1px solid var(--border);border-radius:5px}code{overflow-wrap:anywhere}.note{font-size:16px;color:var(--muted)}.row{border-bottom:1px solid var(--border);padding:12px 0}.row strong{display:block}table{width:100%;border-collapse:collapse;font-size:16px;table-layout:fixed}th,td{padding:10px 8px;text-align:left;border-bottom:1px solid var(--border);overflow-wrap:anywhere}th{background:var(--wash)}a{color:var(--accent);overflow-wrap:anywhere}footer{border-top:1px solid var(--border);padding-top:16px;font-size:16px;color:var(--muted)}.tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;max-width:330px}.tile{padding:16px;text-align:center;background:var(--wash);border:1px solid var(--border);font-size:22px}svg,canvas{max-width:100%;height:auto}mark{background:#d5f525;color:#202600}ol,ul{padding-left:24px}li{margin-bottom:9px}[hidden]{display:none!important}@media(max-width:640px){main{padding:18px 16px 36px}.grid{grid-template-columns:1fr;gap:0}.panel{padding:17px}header{padding-bottom:12px;margin-bottom:16px}h1{font-size:29px}th,td{padding:8px 5px}h2{margin-top:18px}}#canvas{width:200px;height:200px;image-rendering:pixelated;background:repeating-conic-gradient(#ccc 0% 25%,#fff 0% 50%) 0/40px 40px;border:1px solid var(--border)}</style></head><body><main><header><p class="eyebrow">Original worked example</p><h1>Canvas sprite animation: keep trim and time</h1><p>A 100ms · B 200ms · C 100ms. B uses only the middle two columns of the logical 4×4 frame.</p></header><section class="panel"><label for="elapsed">Elapsed milliseconds<input id="elapsed" type="number" value="0"></label><button id="inspect" type="button">Inspect frame</button><button id="b" type="button">100 ms</button><button id="c" type="button">300 ms</button><button id="reset" type="button">Reset</button><div class="result" id="result" role="status" aria-live="polite"></div><canvas id="canvas" width="4" height="4" aria-label="Logical frame, enlarged"></canvas><pre id="row" tabindex="0"></pre><pre id="rect" tabindex="0"></pre></section><section class="panel"><p>A: [0,100) · B: [100,300) · C: [300,400). Total 400 ms. B is trimmed; do not stretch its two columns into four.</p></section><footer>Manual frame inspection does not establish animation timing, browser pixel readback or playback performance. Changes are temporary. No data is sent or saved.</footer></main><script>
(() => {
'use strict';
const $=selector=>document.querySelector(selector);
const on=(selector,handler)=>$(selector).addEventListener('click',handler);
(function(){const atlas=document.createElement('canvas');atlas.width=8;atlas.height=8;const ac=atlas.getContext('2d'),ctx=$('#canvas').getContext('2d');if(ac){ac.fillStyle='#e93636';ac.fillRect(0,0,4,4);ac.fillStyle='#2675d1';ac.fillRect(4,0,2,4);ac.fillStyle='#299b53';ac.fillRect(0,4,4,4);}function inspect(){const raw=$('#elapsed').value,n=Number(raw);if(!raw.trim()||!Number.isFinite(n)||n<0){$('#result').textContent='Enter a finite nonnegative elapsed time.';$('#row').textContent='';$('#rect').textContent='';if(ctx)ctx.clearRect(0,0,4,4);return;}const local=n%400,id=local<100?'A':local<300?'B':'C',rect=id==='A'?[0,0,4,4,0,0,4,4]:id==='B'?[4,0,2,4,1,0,2,4]:[0,4,4,4,0,0,4,4];$('#result').textContent='Frame '+id+' at local '+local+' ms.';$('#row').textContent=id==='B'?'Each logical row: transparent | blue | blue | transparent':'Each logical row: '+(id==='A'?'red | red | red | red':'green | green | green | green');$('#rect').textContent='Source '+rect.slice(0,4).join(',')+' → destination '+rect.slice(4).join(',');if(ctx&&ac){ctx.clearRect(0,0,4,4);ctx.drawImage(atlas,...rect);}}$('#elapsed').addEventListener('input',()=>{$('#result').textContent='Elapsed time changed. Inspect the current value.';$('#row').textContent='';$('#rect').textContent='';if(ctx)ctx.clearRect(0,0,4,4);});on('#inspect',inspect);on('#b',()=>{$('#elapsed').value='100';inspect();});on('#c',()=>{$('#elapsed').value='300';inspect();});on('#reset',()=>{$('#elapsed').value='0';inspect();});inspect();})();
})();
</script></body></html>