mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
Keep the background when importing delta-encoded GIFs
Frames after the first came in with the background punched out. The
cause is a common exporter pattern: mark the unchanged background as
the frame's transparent index so each frame compresses down to just
what moved, then tag every frame disposal 2. Those two are
contradictory - disposal 2 ("restore to background") is implemented as
a clear to transparent by every browser, which wipes the very pixels
the delta relies on showing through. The reported file also declares
its background colour as an unrelated cyan palette slot, so honouring
the spec literally is no better.
Suppress the disposal-2 clear in exactly one case: a full-canvas frame
in an animation whose first frame was fully opaque. Such a file
demonstrably has a baked-in background, so "restore to background"
cannot coherently mean "erase to nothing".
Both conditions are load-bearing, and each has a regression test:
- full-canvas only, so sprite erasure - disposal 2 on a small frame
rect, the case the method genuinely exists for - still clears and
leaves no trail over an opaque background;
- opaque-first-frame only, so animations on real transparency keep it.
That includes our own exporter's output, which also writes
full-canvas disposal-2 frames: without this condition a sprite
re-imported from an Artstr GIF trailed its previous position.
Verified on the reported 640x640 40-frame GIF (every frame now solid,
100% opaque, black background intact) and on the transparent-background
Artstr icon animation (corners still transparent on every frame).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
8702043abf
commit
f8869368c9
3 files changed
+51
-3
No files matched your search
@@ -133,6 +133,22 @@ All four phases shipped.
|
||||
local colour tables, transparency, and all three disposal methods
|
||||
(frames composite over their predecessor, so partial
|
||||
"changed-rectangle" frames render correctly).
|
||||
- **Disposal 2 and mis-tagged deltas.** Disposal 2 ("restore to
|
||||
background") is implemented as a clear to transparent, as every
|
||||
browser does — the spec's background colour is routinely garbage,
|
||||
pointing at an unrelated palette slot. The clear is suppressed in
|
||||
exactly one case: a **full-canvas** frame in an animation whose
|
||||
**first frame was fully opaque**. Such a file demonstrably has a
|
||||
baked-in background, so "restore to background" cannot mean "erase
|
||||
to nothing"; in practice these are deltas whose exporter marked the
|
||||
unchanged background as the transparent index to shrink each frame,
|
||||
and honouring the clear punches the background out of every later
|
||||
frame. Both conditions are load-bearing — full-canvas only, so
|
||||
sprite erasure (disposal 2 on a small rect, the case the method
|
||||
exists for) still leaves no trail; opaque-first-frame only, so an
|
||||
animation on genuine transparency — including our own exporter's
|
||||
output, which also writes full-canvas disposal-2 frames — keeps its
|
||||
transparency and leaves no trail either.
|
||||
- **One shared palette** across every frame, quantized from all
|
||||
frames together — per-frame palettes made an animation shimmer
|
||||
and burned through the 256-entry cap.
|
||||
|
||||
@@ -43,6 +43,9 @@ Palette-indexed grid editor for sprite work + simple animation.
|
||||
between frames, playback FPS is derived from the GIF's own frame
|
||||
delays, and **Frames** samples a long animation down evenly. A GIF
|
||||
already at grid size and palette-limited imports pixel-exact.
|
||||
Animations with a baked-in background keep it on every frame, even
|
||||
when the file was exported as space-saving delta frames; animations
|
||||
on genuine transparency stay transparent.
|
||||
- Selecting several image files at once imports them as consecutive
|
||||
frames, in pick order.
|
||||
- **Apply to** decides where the import lands. Current frame / New
|
||||
|
||||
+32
-3
@@ -21338,6 +21338,7 @@
|
||||
const comp = new Uint8ClampedArray(gw * gh * 4);
|
||||
let gce = { delayMs: 100, transparentIndex: -1, disposal: 0 };
|
||||
const raw = [];
|
||||
let opaqueBase = false;
|
||||
const HARD_FRAME_CAP = 400;
|
||||
|
||||
while (p < d.length) {
|
||||
@@ -21400,9 +21401,37 @@
|
||||
}
|
||||
raw.push({ rgba: comp.slice(), delayMs: gce.delayMs });
|
||||
|
||||
if (gce.disposal === 2) {
|
||||
// Restore to background — the spec's background colour, but every
|
||||
// real renderer clears to transparent, and so do we.
|
||||
// Does this animation open on a solid, fully painted frame? Decided
|
||||
// once, from the first frame, and used by the disposal rule below.
|
||||
if (raw.length === 1) {
|
||||
let opaque = 0;
|
||||
for (let o = 3; o < comp.length; o += 4) if (comp[o] === 255) opaque++;
|
||||
opaqueBase = opaque >= (gw * gh) * 0.98;
|
||||
}
|
||||
|
||||
// Disposal 2 = "restore to background", which every real renderer
|
||||
// implements as clearing to transparent (the spec's background colour
|
||||
// is routinely garbage — plenty of files point it at an unrelated
|
||||
// palette slot).
|
||||
//
|
||||
// We suppress that clear in exactly one situation: a full-canvas
|
||||
// frame in an animation that *opened on a fully opaque frame*. Such
|
||||
// an animation demonstrably has a baked-in background, so "restore to
|
||||
// background" cannot coherently mean "erase to nothing" — and in
|
||||
// practice these files are mis-tagged deltas, where an exporter
|
||||
// marked the unchanged background as the transparent index to shrink
|
||||
// each frame. Honouring the clear punches that background out of
|
||||
// every later frame.
|
||||
//
|
||||
// Both conditions are load-bearing:
|
||||
// - full-canvas only, so sprite erasure (disposal 2 on a small
|
||||
// frame rect, the case the method genuinely exists for) still
|
||||
// clears and leaves no trail;
|
||||
// - opaque-first-frame only, so an animation on real transparency —
|
||||
// including our own exporter's output, which writes full-canvas
|
||||
// disposal-2 frames — still clears and leaves no trail either.
|
||||
const fullCanvas = ix === 0 && iy === 0 && iw === gw && ih === gh;
|
||||
if (gce.disposal === 2 && !(fullCanvas && opaqueBase)) {
|
||||
for (let r = 0; r < ih; r++) {
|
||||
const gy = iy + r;
|
||||
if (gy < 0 || gy >= gh) continue;
|
||||
|
||||
Reference in new issue
Block a user