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]>
Add an inline GIF89a decoder - the mirror of the existing encoder, so
still no vendored dependency - and wire it into "From image / GIF...".
Every frame of an animated GIF becomes a frame in the pixel editor. A
browser <img> only ever exposes frame 0 to canvas, so decoding is the
only route to the rest; the decoder covers LZW, interlacing, local
colour tables, transparency and all three disposal methods.
The whole animation is quantized against one shared palette (per-frame
palettes made colours shimmer and burned through the 256-entry cap),
playback FPS is derived from the median GIF frame delay, and a Frames
control samples long animations down evenly. Frame count and working
resolution are bounded so a large GIF cannot exhaust memory, and
whatever gets dropped is reported rather than silently truncated.
Fix the reported pixel-editor import bugs:
- Dead canvas. applyLayout() tore the pixel editor down before its
"layout unchanged" early return, and only the render() at the end
rebuilds a standalone one - so clicking Pixel art while already in
Pixel art left the grid on screen with nothing behind it and
painting, the palette and import all silently did nothing.
- Locked W/H with no explanation, and a multi-image batch forced to the
New frame target could never set dimensions at all. Batches can now
target Whole sprite, and the lock states why and how to change it.
- Cover/Fit/Stretch previews were indistinguishable: the pixelized
canvas rendered at its natural size (32 CSS px in a 250px box). Both
panes now fill the box, and the source pane is framed exactly as the
conversion frames it, so the two read as before/after.
- Photos were point-sampled on the way down, which threw away almost
the whole image and read as noise. Area-average via progressive
halving, switching to nearest at 1:1 and integer ratios so pixel-art
sources stay crisp and import exactly.
Also fixed while verifying:
- Standalone pixel work was lost on refresh - pixelCommitIfStandalone()
mutates state without calling render(), which is what writes the
autosave. persistAutosave() existed for this case but was never
called here.
- The quantizer always split to the requested colour count, padding
small palettes with duplicate dead entries that cost event-size
budget. It now keeps the source's exact colours when there are fewer
than requested.
- sanitizePixelArtBlob truncated imported palettes at 64 though the
model allows 256 (cells past the cut rendered transparent) and
dropped per-frame durations.
- Stale grid-size pill after a resize, zoom not refitting when the grid
grows, and a dead pixelImportFromImage() pointing at a nonexistent
element.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
PIXEL_ART_FEATURE.md: rewrite Implementation status to reflect that
Phases A–D all shipped (embedded layer, standalone design, raster
export + editor extras, animation, image → pixelize), with the
backlog (WebP/APNG, Blossom offload, animated pixel layers driving
Stack actions) called out as deferred.
TODO.md: drop stale items now that they're resolved —
- Pixel Art (moved to a "Shipped (recent)" section pointing at the
spec doc).
- Pixel editor layout expansion (subsumed by the collapsible
sidebar — both the pixel editor and vector canvas can now use
the full viewport width).
- Print / Save event ID wrapping (the `<dd>` already uses
`overflow-wrap: anywhere; word-break: break-word`).
- TMDB auto-fill cleanup (the doc was already deleted).
The Color picker fixes item is kept but narrowed to the vector
tools — the pixel editor's in-app HSV picker shipped in Phase A.
README.md: add Pixel Art everywhere it's relevant —
- One-liner mention in the intro paragraph.
- New "Pixel art" Designer-tab layout entry alongside Slide / Disc
design.
- `pixelart` layer type added to the Layers list.
- New dedicated "Pixel art" section before Editor canvas, covering
tools / transforms / animation / export / import / undo /
animated previews.
- Editor canvas section mentions the new sidebar collapse toggle.
- Schema section lists `pixelart` template mode and layer type,
documents the `pixelArt` data-model shape, and notes that the
inline GIF89a encoder lives in `index.html` (not a vendored dep).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
A new `pixelart` layer type: a palette-indexed, RLE-encoded grid stored
in the project JSON, rendered crisp (image-rendering: pixelated) across
the editor, preview-DOM renderers, and the export canvas.
A dedicated #pixelEditorView with pencil / eraser / fill / eyedropper
(drag-paint with line interpolation), a palette panel (pick / add
colours), grid toggle, mirror X/Y, zoom, and per-stroke in-editor undo.
Reachable via a tool-palette button (adds a 32x32 layer + opens the
editor) and an "Edit pixels" button / double-click on a pixel layer.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Spec for a Nostr-native pixel-art editor: an editable `pixelart` layer
type (palette-indexed + RLE grid stored in JSON, raster as export-only),
a dedicated Pixel Editor canvas-area view, a standalone casewrap-pixelart
Designer-tab design importable into any design as a layer, PNG export,
and a phased path to animation frames + GIF.
Includes a Size limits section: relays cap event size (commonly 64 KB),
so publishing is gated on a measured event-size budget derived from the
relays' NIP-11 max_event_size (min across the user's set, 64 KB
fallback) — soft warning at ~50%, hard block at ~90%, with a live size
readout in the editor. Encoder stores the smaller of RLE / base64 to
bound the worst case; rough size table per grid dimension included.
Also index the spec in TODO.md under drafted-but-unbuilt features.
Co-Authored-By: Claude Opus 4.7 <[email protected]>