From 1c205b3ea88f2214feab64c8cab0afdce6d6fa6a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 31 May 2026 09:18:41 -0400 Subject: [PATCH] Pixel-art layer: animate multi-frame designs when embedded A multi-frame pixel-art design imported as a layer into another canvas (a cover, slide, custom-art, etc.) was rendering only its first frame as a static image. Now it animates at its stored fps + loop settings, matching what the standalone preview already does. renderPixelArtIntoElement now branches on frames.length > 1: - Multi-frame: pre-render every frame to a native-size canvas (cheap reuse of renderPixelGridToCanvas), then a RAF loop blits the current frame onto a display canvas, advancing by ts deltas to honour the source fps regardless of monitor refresh rate. - The RAF self-stops when the display canvas leaves the DOM (cv.isConnected === false), so a re-render of the editor / preview pane doesn't leak loops. Same pattern as the standalone-design animation. - Single-frame: unchanged static render. Applies everywhere pixel-art-as-layer surfaces: the live editor free-layer overlay, the preview pane in the community browser, and any host design that picks up the layer. --- src/index.html | 53 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/src/index.html b/src/index.html index 25d4705..695ede5 100644 --- a/src/index.html +++ b/src/index.html @@ -11204,10 +11204,57 @@ // Render a pixel-art layer into its freeLayer element (crisp scaling). function renderPixelArtIntoElement(el, layer) { el.innerHTML = ''; + const pa = layer.pixelArt || {}; try { - const cv = renderPixelGridToCanvas(layer.pixelArt || {}); - cv.style.cssText = 'width:100%;height:100%;display:block;image-rendering:pixelated;image-rendering:crisp-edges;pointer-events:none'; - el.appendChild(cv); + const frames = Array.isArray(pa.frames) ? pa.frames : []; + const animate = frames.length > 1; + if (animate) { + // Multi-frame pixel-art layer: pre-render every frame to its + // own native-size canvas, then animate by blitting onto a + // display canvas. Mirrors the standalone-preview animation + // path (renderPixelArtPreviewDOM). The RAF self-stops when + // the host canvas leaves the DOM (cv.isConnected === false) + // so a re-render of the editor doesn't leak loops. + const fps = Math.max(1, Math.min(60, (pa.fps | 0) || 8)); + const loop = pa.loop !== false; + const w = (pa.width | 0) || 32; + const h = (pa.height | 0) || 32; + const cached = frames.map((_, i) => renderPixelGridToCanvas(pa, i)); + const cv = document.createElement('canvas'); + cv.width = w; cv.height = h; + cv.style.cssText = 'width:100%;height:100%;display:block;image-rendering:pixelated;image-rendering:crisp-edges;pointer-events:none'; + const ctx = cv.getContext('2d'); + ctx.drawImage(cached[0], 0, 0); + let frameIdx = 0; + let lastTs = 0, acc = 0; + const tick = (ts) => { + if (!cv.isConnected) return; + const dt = lastTs ? (ts - lastTs) : 0; + lastTs = ts; + acc += dt; + const msPerFrame = 1000 / fps; + let ended = false; + while (acc >= msPerFrame) { + acc -= msPerFrame; + const next = frameIdx + 1; + if (next >= cached.length) { + if (loop) frameIdx = 0; + else { frameIdx = cached.length - 1; ended = true; break; } + } else { + frameIdx = next; + } + } + ctx.clearRect(0, 0, w, h); + ctx.drawImage(cached[frameIdx], 0, 0); + if (!ended) requestAnimationFrame(tick); + }; + requestAnimationFrame(tick); + el.appendChild(cv); + } else { + const cv = renderPixelGridToCanvas(pa); + cv.style.cssText = 'width:100%;height:100%;display:block;image-rendering:pixelated;image-rendering:crisp-edges;pointer-events:none'; + el.appendChild(cv); + } } catch (e) {} }