A vertical handle tab on the seam between the sidebar and the canvas
toggles the side panel open / closed. State persists to localStorage
so the panel stays where the user left it across reloads.
Style choices to match the existing chunky early-web aesthetic:
- Hard right edge with a small 4px corner, no left border (visually
fuses with the sidebar edge).
- Manual two-tone bevel via inset box-shadow — light top, dark
bottom — for the old-school extruded-tab look, without using
border-style: ridge/outset (which modern browsers render
inconsistently).
- Monospace bold guillemet glyph: « when open, » when collapsed.
- Hover paints accent-blue; active state nudges 1px right (chunky
click feedback).
- Dark-mode-aware bevel colours.
Positioning:
- Fixed at top: 24px so it aligns with the logo row instead of
floating mid-screen on top of editor toolbars (the original
mid-screen position obscured the pixel and vector editor
toolbars).
- Height 44px — compact enough not to dominate the header.
- Slides smoothly with the sidebar via `left` transition matched
to the grid-template-columns animation (220ms ease).
- When collapsed, the pixelEditorBar / canvasToolbar / toolPalette
get a 26px padding-left so their leftmost buttons don't sit
under the tab.
Trade-off worth noting: the responsive `minmax(340px, 420px)`
sidebar width has been replaced with a fixed `380px` because
grid-template-columns doesn't smoothly interpolate between
minmax() and a plain length. 380px is in the middle of the
original range and works on every viewport size we care about.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
A new "From image…" button in the pixel editor toolbar opens a file
picker and then a pixelize modal. The modal previews source and
pixelized output side by side, with live updates as the user changes:
- W / H (4–128, matching the editor's resize cap; default = the
editor's current dims so Apply lands as a drop-in replacement)
- Colors (2–64; index 0 reserved as transparent)
- Fit mode — Cover / Fit / Stretch
Algorithm:
- _pixelDownsampleImage draws the image onto an off-screen canvas
with imageSmoothingEnabled=false; fit-mode math computes source
and dest rects so Cover crops edges, Fit letterboxes with
transparent, Stretch ignores aspect.
- _pixelMedianCut quantizes the downscaled pixels: collect opaque
samples, recursively split the bucket with the widest single-
channel range until N buckets, average each bucket → palette
entry. Pixels with alpha < 128 become transparent (index 0).
- _pixelNearestPaletteIdx maps each opaque pixel to the nearest
palette entry by squared RGB distance.
Apply replaces the editor's current pixel art — frames, palette,
and dims all get swapped to match the pixelized output. One
pixelSnapshot before mutating, so Ctrl+Z restores everything.
Standalone mode commits immediately.
Drive-bys in this commit:
- Move the From image entry out of the vector-canvas ▦ popup and
into the pixel editor toolbar (per request — the editor is where
the workflow lives).
- Fix the modal show bug: .modal CSS rule sets display:none, so the
modal must be opened with style.display = 'flex' (matching the
publish-confirm modal). The previous '' didn't override the rule.
- Drop the long hint paragraph under the colour picker — the SV
square + hue strip + Add button are self-explanatory and the
eyedropper has its own tooltip.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Inline GIF89a encoder — no vendored dependency. Pixel art is
already palette-indexed, which is exactly what GIF needs, so the
encoder hands frame index buffers straight to LZW and writes the
GIF structure directly.
Encoder:
- encodePixelArtToGif assembles a full GIF89a: header, global
colour table sized to the smallest 2^(N+1) that fits the
palette, NETSCAPE2.0 application extension for loop count
(0 = infinite, 1 = play once), per-frame Graphic Control
Extension with disposal=2 (restore to background) and
transparent-colour-index=0 (matches our palette convention),
Image Descriptor, LZW-encoded image data in ≤255-byte sub-
blocks, trailer.
- _gifLzwEncode does standard variable-bit-width GIF LZW: codes
start at minCodeSize+1 bits, grow to 12, dictionary resets via
clear codes when full.
- exportPixelArtToGifBlob decodes RLE frames, nearest-neighbour
upscales to the chosen scale (so cells stay crisp in the GIF),
encodes, returns a Blob.
- Smoke-tested at 4×4, 8×8, and 32×32: `file` recognizes all
outputs as valid GIF89a.
Export UI:
- Format picker at the top of the export menu: PNG / GIF. PNG-
only controls (Transparent bg, Grid lines, Sprite sheet) hide
when GIF is active. GIF uses the design's stored fps + loop.
- Refuses GIF export with a toast when the design has <2 frames.
- Filename includes the frame count, e.g. mysprite_32x32_6f_4x.gif.
Animated previews (publish modal + community browser + feed cards
+ profile page) — all four entry points share one preview helper,
so this lights up everywhere:
- renderPixelArtPreviewDOM detects multi-frame designs and animates
via a requestAnimationFrame loop with an accumulator (correct
pacing across dropped frames).
- Per-frame canvases are pre-rendered once and cached; the tick
just blits the right cached canvas — no RLE decode per tick.
- The RAF loop self-stops on `cv.isConnected === false`, so closing
a modal, swapping the preview panel, or scrolling a feed card
out of the DOM ends the loop. No leaks.
- fps and loop come from the payload; non-looping animations run
once and freeze on the last frame.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Extends the existing Export PNG menu with a "Sprite sheet (all
frames)" checkbox that tiles every frame into one PNG. A layout
picker (Grid / Row) appears when sprite-sheet is on — Grid is the
default and uses a square-ish layout (ceil(sqrt(N)) columns); Row
is a single horizontal strip. Scale, transparent background, and
grid-lines options all apply to the sheet as a whole.
Filename includes the frame count and a _sheet tag — e.g.
mysprite_32x32_4f_4x_sheet.png — so game-engine tools can parse
the dims from the name.
renderPixelGridToCanvas(pixelArt, frameIndex) now takes an optional
frame index (defaults to 0, so every existing single-frame caller
keeps working unchanged). pixelExportCurrentPixelArt now packages
all frames + fps + loop, so the exporter sees the full animation.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
A new "Onion" checkbox below Loop in the frame strip. When on, the
canvas draws the previous frame as a flat red tint at 30% opacity
and the next frame as a flat blue tint at 30% opacity, both behind
the editing frame in its real colours. Aseprite convention — flat
tints make the neighbouring frames immediately distinguishable from
the frame you're painting.
- Only renders while editing (skipped when overrideIndices is set,
i.e. during playback — the playing frame stays unobscured).
- Only renders when there are neighbouring frames to show.
- Onion is editor-only state (like the grid toggle); not persisted
to the design payload.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Playback:
- ▶ / ■ Play button toggles a requestAnimationFrame-driven playback
loop that uses an accumulator, so framing stays correct across
tab-switches and dropped frames.
- FPS input (1–60) and Loop checkbox, both saved with the design.
- Space key toggles play / stop while the pixel editor has focus.
- Clicking the canvas during playback stops it and resumes editing.
- Arrow keys, tool shortcuts, and any frame-op stop playback first
so the canvas and `_pixelEdit.currentFrame` stay in sync. Frame
switches via pixelSetCurrentFrame auto-stop playback for the same
reason.
- Refuses to start with fewer than 2 frames.
- Playback frame is rendered via a new optional `overrideIndices`
argument to renderPixelGrid — `_pixelEdit.indices` (and thus
`currentFrame`) keep pointing at the editing frame the whole time.
Layout:
- The frame strip is now a vertical sidebar to the left of the
canvas instead of a horizontal row underneath. Frame thumbnails
scroll independently (overflow-y: auto on the thumb list), so
the main art area never gets pushed when there are many frames.
- Strip layout, top to bottom: Play, FPS, Loop, the 3 frame-op
buttons (+, ⎘, −) in a row, then the scrolling thumb column.
- Thumbs use `height: auto` so their aspect matches the grid:
square grids → square thumbs, non-square grids → correctly
proportioned thumbs (cells stay square in the preview).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
_pixelEdit now tracks frames[] with currentFrame index and the
animation's fps/loop fields; _pixelEdit.indices is a live reference
to the active frame's Uint8Array so every existing paint / fill /
shape / mirror code path keeps mutating the right frame with no
further changes. decodePixelArtFrames / encodePixelArtFrames round-
trip the whole frame set between JSON RLE and in-memory typed arrays.
A timeline strip below the canvas holds:
- + / ⎘ / − buttons to add a blank frame, duplicate the current
frame, and delete the current frame (delete disables when only
one frame remains).
- A scrollable row of frame thumbnails — each thumb is the frame's
indices rendered at native size and scaled up with image-rendering:
pixelated. Active frame has an accent-coloured border.
- Click-to-switch and HTML5 drag-reorder.
- Thumbnails refresh on stroke end, transform, resize, and frame ops.
Left / Right arrow keys cycle to the previous / next frame (wraps).
Transforms (flip / rotate / resize) now iterate over every frame so
dims stay coherent across the animation. Undo snapshots capture the
full frame set plus currentFrame; restoring an edit also restores
the editing frame so the user lands back where they edited.
Commit paths (closePixelEditor, pixelCommitIfStandalone,
pixelEditTearDown) all encode all frames + fps + loop. Old single-
frame saves decode correctly because decodePixelArtFrames falls back
to a one-element array.
sanitizePixelArtBlob caps at 32 frames as a defensive ceiling and
preserves fps / loop.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The pixel-art tool button (▦) in the tool palette now opens a small
popup with three options, mirroring the disc-slot and deck-slide
patterns:
- Add blank pixel-art layer — original behavior (32×32 default).
- Browse community… — opens the existing Nostr browser filtered
to nostrType='pixelart'. Selecting a design imports it as a new
pixelart layer in the current design (rather than loading it as
the current document).
- Import from file… — file picker for .json designs.
Plumbing:
- sanitizePixelArtBlob clamps dimensions to 4–128 (matches resize
cap), trims palettes to 64 entries, defaults missing palettes to
DEFAULT_PIXEL_PALETTE, decodes/re-encodes RLE so corrupt runs
either coerce to something valid or get rejected.
- importPixelArtAsLayer accepts either a full design payload (with
.pixelArt) or a raw pixelArt blob; rejected payloads toast an
error rather than creating a broken layer.
- tryPixelArtImportRow intercepts community-browser selections
while state._pixelArtImportMode is true — added alongside the
existing tryDeckImportRow at the four selection sites.
- nostrClose handler clears state._pixelArtImportMode so closing
the modal without picking doesn't leave the flag stuck on.
- Menu closes on outside click, Escape, and after picking — same
pattern as the export-PNG popover.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The dims badge in the pixel editor toolbar is now a button — click
it to open a popover with W / H number inputs, a row of preset
buttons (16 / 32 / 48 / 64 / 96 / 128), and an Apply button.
Resize behavior:
- Bounds clamped 4–128 per side. The cap is set to keep the
serialized event comfortably under conservative relay limits;
raise later if real-world art shows it's safe.
- Top-left anchor: existing pixels at (x, y) stay where they are.
- New cells = palette index 0 = transparent (Uint8Array zero-
initializes; index 0 is the conventional transparent slot in
DEFAULT_PIXEL_PALETTE).
- Oversize is cropped.
- pixelSnapshot is called before the mutation, so Ctrl+Z reverts
the resize correctly — snapshots already capture {indices, w, h}.
- pixelCommitIfStandalone is called after, so standalone mode
pushes a debounced global history entry.
The popover closes on Apply, outside click, Escape, the Done
button, or any tool / transform button click — same pattern as
the Export PNG menu.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Pixel editor undo/redo were a local stack with no redo and no
connection to the global design-history stack. Several gaps:
- No redo in the pixel editor.
- The pixel snapshot only captured `indices`, not `w`/`h` — so
undoing a rotate left the editor with swapped dims and stale data.
- Standalone pixelart mode never pushed global history, so leaving
the editor (or switching templates) lost the ability to undo
pixel work from the global stack.
- The pixel toolbar had its own ↶ Undo button duplicating the
top-level toolbar's ↶.
Changes:
- `pixelSnapshot` now captures `{ indices, w, h }` and clears the
redo stack on every new mutation. `_pixelRestoreState` updates
the dims badge alongside indices, so rotate-then-undo is correct.
- Added `pixelRedo` mirroring `pixelUndo`. `_pixelEdit.redo` lives
on both editor inits.
- Top-level `#undoBtn` / `#redoBtn` are now context-aware: while
the pixel editor is open they route to `pixelUndo` / `pixelRedo`;
otherwise they route to the global `undo` / `redo`. Tooltips and
disabled state flip accordingly via `updateUndoRedoUI`.
- Pixel-editor keydown handles Ctrl+Shift+Z and Ctrl+Y for redo.
- `pixelCommitIfStandalone` pushes one debounced global history
entry per stroke / shape / transform (600ms window), guarded by
`_pixelHistorySuppress` so pixel undo/redo and serialize-for-
publish don't pollute the global stack.
- Removed the redundant local ↶ button from the pixel toolbar
(the top-level button now drives it).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Three new shape tools in the pixel editor toolbar:
- ╱ Line (L): Bresenham line from drag start to current cell.
- □ Rectangle (R): outline by default; filled when the new "Fill" toggle
is on.
- ○ Ellipse (O): inscribed in the dragged bounding box; outline /
filled per the Fill toggle. Outline cells are those inside the ellipse
with at least one outside 4-neighbour.
A new "Fill" checkbox lives next to Mirror Y in the toolbar; applies to
rect / ellipse only. Mirror X / Y still apply to all shape tools.
Live-preview during the drag works by taking a one-shot snapshot of the
indices on pointerdown, then on every pointermove restoring from the
snapshot and re-applying the shape — the in-progress preview never
accumulates. One in-editor undo entry per shape (snapshot taken on
pointerdown), so Ctrl+Z removes the whole shape, not individual cells.
Standalone mode commits the final shape on stroke end so autosave /
publish always see it.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
A new "Export PNG ▾" dropdown in the pixel editor toolbar, available in
both the embedded-layer and standalone editing modes:
- Scale: 1× / 2× / 4× / 8× / 16× / 32× (3-column grid).
- Transparent background on/off (index-0 transparent cells stay
transparent in the PNG when on; a white fill is laid down first when
off).
- Optional 1-px grid line overlay at ≥ 2× scale.
- Filename is "<title>_<W>x<H>_<scale>x.png"; toast confirms output
dimensions.
Self-contained <canvas> → toBlob('image/png') pipeline. Reads the
editor's current working grid for both layer-edit and standalone modes,
so you can export while you're still editing. Settings persist for the
session.
Menu dismissal is strict: opens only on the Export button, closes on
outside click, outside pointerdown (catches drag-starts), Esc, picking
a pixel tool, hitting Done, and after Download. Fixed a default-open
bug where the .pixelExportMenu CSS display:flex was beating the [hidden]
attribute via specificity tie + later-rule-wins — switched to a class-
toggled open state.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The OS <input type="color"> popup eyedropper and the standalone EyeDropper
API are both broken on the user's Brave build — they return R=NaN /
always-white sample regardless of where you point. Replace them with
controls that don't touch any OS picker:
- New in-app HSV picker: Saturation/Brightness square + Hue strip drawn
to canvases; pure sRGB math, no OS calls. The cursor position IS the
picked colour. Render is wrapped in try/catch so SES lockdown (e.g.
MetaMask) freezing canvas helpers can't break the commit path.
- New 32x32 preview swatch next to the hex input, updated by the
picker — gives unambiguous visual feedback of the pending colour.
- A typeable #rrggbb hex field with live validation (red border on
invalid), syncs both ways with the picker.
- A "Pick from screen" button backed by the Screen Capture API: take a
one-frame screenshot of a chosen surface, show it in our own overlay,
click any pixel to sample. The capture stream is kept alive for the
duration of the pixel-edit session so subsequent picks don't re-prompt
for permission (with a Stop button to end the share early). Auto-
released when the editor closes.
- Drop the OS <input type="color"> swatch and "More options" disclosure
entirely; they only confused users on browsers where they don't work.
Also close the editor layout gaps: in pixel-editing / template-pixelart
modes, .previewShell padding goes to 0, #pixelEditorView is height:100%
min-height:0 so it reaches the screen edges, the inner canvas wrap
padding tightens to 12px, and the fit-zoom reserved margin tightens to
28px so the canvas auto-fits at a higher integer zoom in the freed
space.
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]>
Phase 1 — publish a design encrypted, readable only by the author:
- Crypto helpers: encryptProjectEnvelope / decryptProjectEnvelope —
AES-256-GCM over gzipped JSON, AES key NIP-44 key-wrapped to the
author (NIP-04 fallback), via a nostrEncryptCapability() probe. All
in-browser, no new dependencies.
- publishEncryptedDesign: a feed-invisible kind:30078 event — random
persistent d-tag, only d / alt / encrypted tags, no casewrap tag.
- Publish-confirm modal gains a Public / Private control with the
privacy warning and a capability gate; wired into the template and
disc-design publish paths.
- My Designs decrypts the author's own private events and shows a
lock badge; buildRowsFromEvents skips any stray encrypted event so it
can never surface in the public feed.
- state.visibility / state.privateDTag — restored on load, cleared on
Close project, autosaved.
Phase 2 — flip a published design's visibility in place:
- Owner-only Make Public / Make Private button on the preview page;
republishDesignVisibility re-publishes under the same d-tag with the
opposite wrapper (NIP-33 replace), behind a confirm that states the
public-to-private caveat.
Spec status documented in docs/PRIVATE_PUBLISHING_FEATURE.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The on-canvas resize handles, text-box-resize behaviour, and expanded
Text tool panel all shipped in 1a517c9; collapse the section to a
shipped note and keep it open for future editor-UX items.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Selected layers (text / shape / image / color / QR) now show 8
drag-resize handles — 4 corners + 4 edge midpoints. Edge handles are
single-axis (resize one direction only); corner handles resize toward
the corner with the opposite corner anchored. Resizing is computed in
the layer's own rotated frame, so rotated layers behave correctly, and
the aspect-lock toggle applies to corner handles only.
- Resizing a text box changes only its w/h (the text flow area) — the
point size is never rescaled.
- Expand #textToolOptions to match the pen/shape panels: font family,
point size, color, alignment, and bold/italic — sets the default for
the next text box, and restyles the selected text layer live.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
On-canvas resize handles for all layer types, a constraint that text-box
resize never rescales the point size, and expanding the existing
#textToolOptions panel like the pen-tool panel.
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]>
Replace the thin single-action-per-layer model with real interactive
logic:
- Cards gain onEnter/onExit chains; layers gain an onClick chain. Each is
an ordered action chain, not a single link.
- v1 action set: goto-card, go-back, open-url, show-layer, hide-layer,
toggle-layer — reveals, secret panels, multi-step buttons, branching.
- New action-builder surface: a no-JSON panel with reorderable rows,
id-based target pickers, and authoring warnings.
- Runtime tracks a per-card layer-visibility map (reset on re-entry) and
executes chains; action overlay regions are focusable buttons.
- Safety: onEnter/onExit cannot navigate (no enter->goto->enter loops);
chains capped and non-recursive; unknown action types ignored.
- Re-phased A (format) -> B (behaviour authoring) -> C (runtime) ->
D (route) -> E (stack logic: variables/conditionals, future tier).
Interactivity stays a declarative, interpreted attribute of existing
layers — no new layer types, no HTML/JS, no eval.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Aspect-ratio changes now re-fit cards via the existing import-letterbox
remap instead of stranding layers off-canvas; imports target the
stack's aspect, not a hardcoded 16:9.
- Clarify how the shared deck renderer is fed for stacks (dims from
stack, background from card) and startCard deletion fallback.
- Viewer: action regions are focusable buttons (Tab/Enter, screen
readers); empty-space clicks are no-ops; goto-card keywords clamp at
the ends; touch taps act as clicks; persist the edited card before
launching; encrypted-stack locked state on the /p/ route.
- Fix the hit-testing risk note (rotation is exact; only clip-masked
layers fall back to a bounding box) and add an accessibility row.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Spec for "HyperCard for Nostr" — fullscreen interactive card stacks built
as a focused extension of the Slide Deck: a new templateMode 'stack'
reusing the deck card-container engine, with a per-stack aspect ratio,
optional per-layer actions (goto-card / open-url), transparent hotspots,
and a fullscreen interactive viewer with click routing. Declarative
safe schema only — no HTML/JS. Phased A-D plus a /p/<naddr> viewer route.
Also index both PRIVATE_PUBLISHING_FEATURE.md and STACKS_FEATURE.md under
the drafted-but-unbuilt features in TODO.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Spec for publishing any design to Nostr encrypted, readable only by the
author (Phase 1 "private to me"). Covers the AES-256-GCM + NIP-44/NIP-04
key-wrap envelope, leak-free tagging (no casewrap feed tag, random d-tag),
progressive decryption in My Designs, an explicit threat model, and a
Phase 0 validation spike. Phases 2-4 (public/private toggle, recipients,
NIP-59/Blossom) sketched as future work.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Build a Nostr-native presentation system on top of the existing canvas
editor:
- Slide (templateMode 'slide') — a 16:9 Custom Art canvas with speaker
notes; publishes/forks/loads as casewrap-slide.
- Deck Builder (templateMode 'deck') — a Template-tab container with an
inline ordered slide list. Slide-sorter view: add blank, import from
Nostr or file (non-16:9 imports letterboxed), duplicate, delete,
drag-reorder, inline-rename; edit a slide in the canvas editor with
write-back. Self-contained save/load/publish/fork as casewrap-deck;
deck feed-card and publish previews show the first slide + count badge.
- Deck theme — deck-level font + background, render-time composition,
per-slide ignoreDeckTheme opt-out.
- Presenter Mode — full-screen runtime: themed slide, notes, next
thumbnail, counter; keyboard/click/on-screen nav; Presenter ⇄ Audience
view toggle (slide-only full-screen with auto-hiding chrome). Launches
from the deck tool palette and a deck's community-browser preview.
Supporting changes:
- New categories: slide ("Slide design") and slide-deck ("Slide deck");
both use the free-form Custom tag field, not media identifiers.
- Swap the Template/Designer tab icons; 50% larger.
- Remove the IMDb/OMDb metadata import (button, handler, omdbApiKey);
the imdbId field stays.
- Update README, SLIDE_DECK_FEATURE.md status, and TODO.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- docs/SLIDE_DECK_FEATURE.md: phased spec for Slide Designer (16:9
canvas + speaker notes), Deck Builder (embedded ordered slides,
slide-sorter view, deck theme), and Presenter Mode (single-window
v1, dual-window deferred to Phase E). Phases A-D with ship gates.
- Select tool icon changed to a rotated arrowhead so it reads as a
cursor, not a play button (distinct from the planned Present icon).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Shapes now render into a viewBox whose height tracks the layer's
aspect ratio (0 0 100 vbH), so the viewBox->layer mapping is a uniform
scale. Strokes stay even thickness and rounded-rect corners stay
circular on any aspect — fixes the squished-stroke/corner bug class.
Applied to both the SVG renderer and the Canvas/PDF/JPEG exporter.
- Clip/mask to a rounded-rect no longer squishes its corners on a
non-square clipped layer: clipShapeToPathD pre-distorts rx/ry.
- Removed a dead empty if-block in the export clip path.
Square layers render identically to before; circles/polygons still
fill the box (intended bounding-box behavior).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Publishing no longer requires a background image: cover/jewel/disc
designs publish with layers-only content. "Save design (JSON)" works
for layer-only disc designs (removed a silent return + a payload throw).
- Disc background image now lands correctly: renderDesignerDisc() runs
after the designer view is laid out, and the publish/feed preview uses
the same cover-pan math as the editor instead of a naive translate(%).
- Publish-modal preview no longer squishes the disc vertically (dropped
axis-independent max-width/height clamps; fitMiniPreview scales it).
- Template browser: "Fork off!" button renamed to "Use/Fork".
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- After a successful publish, a "support the developer" prompt asks if
the user likes the project and offers to zap the dev — resolves the
developer's Lightning address from their Nostr profile and hands off
to the existing tip/zap modal. Shown once per session.
- Fix clip/mask to a custom vector (path) shape: the <clipPath> wrapped
the path in a <g>, which browsers drop, so path-shape clips silently
failed. Moved the transform onto the <path> itself.
- A shape used as a clip/mask source now renders no fill or stroke of
its own (editor, export, and publish preview) so the clipped layer
shows through cleanly.
- New animated logo (cropped tight, resized), Designer-tab icon, updated
slogan, GitHub URL, and an artstr.org footer link.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- New left-palette tools: Hand (pan), Shape (drag to draw a primitive),
Text (click to place). Removed the redundant sidebar add-buttons.
- Pencil strokes now keep a constant on-screen thickness (pixel-space
viewBox) instead of scaling with stroke length/aspect.
- Restructured tabs: "Disc Designer" -> "Designer", which now hosts a
Layout panel with Custom Art (default) + Disc Design; Custom Art moved
off the Template tab. Underlying appMode/save-format unchanged.
- Renamed the app from CaseWrap Studio to Artstr Studio (brand text only;
Nostr tags and localStorage keys kept for compatibility); smaller
one-line logo wordmark.
- README updated for the new tabs, tools, and keyboard shortcuts.
Co-Authored-By: Claude Opus 4.7 <[email protected]>