Commit Graph
162 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 4.7 e7e3039748 Add collapsible sidebar with a chunky early-web toggle tab
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]>
2026-05-26 10:48:44 -04:00
thePR0M3TH3ANandClaude Opus 4.7 a13ef22b92 Add image → pixelize (Phase D) and move it into the pixel editor
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]>
2026-05-26 10:33:44 -04:00
thePR0M3TH3ANandClaude Opus 4.7 130e0218d8 Add GIF export + animate multi-frame previews (Phase C5)
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]>
2026-05-26 10:08:40 -04:00
thePR0M3TH3ANandClaude Opus 4.7 20a389f0ee Add sprite-sheet PNG export (Phase C4)
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]>
2026-05-26 09:19:14 -04:00
thePR0M3TH3ANandClaude Opus 4.7 aebd86c11c Add onion-skin to the pixel editor (Phase C3)
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]>
2026-05-26 09:12:09 -04:00
thePR0M3TH3ANandClaude Opus 4.7 b8e1a11605 Add playback + move the frame strip to a vertical left sidebar (Phase C2)
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]>
2026-05-26 09:09:35 -04:00
thePR0M3TH3ANandClaude Opus 4.7 6dad832a8d Add multi-frame timeline UI to the pixel editor (Phase C1)
_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]>
2026-05-26 08:44:40 -04:00
thePR0M3TH3ANandClaude Opus 4.7 2a8d0e3707 Import a published pixel-art design as a layer (Phase B)
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]>
2026-05-26 08:26:34 -04:00
thePR0M3TH3ANandClaude Opus 4.7 f20a90929d Add grid resize to the pixel editor (Phase B)
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]>
2026-05-25 15:46:37 -04:00
thePR0M3TH3ANandClaude Opus 4.7 335afdab4b Integrate pixel-editor undo/redo with the top-level toolbar
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]>
2026-05-25 15:37:19 -04:00
thePR0M3TH3ANandClaude Opus 4.7 f9f800985d Add line / rectangle / ellipse drawing tools (Phase B)
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]>
2026-05-25 15:21:05 -04:00
thePR0M3TH3ANandClaude Opus 4.7 06af73aeff Add PNG export to the pixel editor (Phase B)
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]>
2026-05-25 15:14:55 -04:00
thePR0M3TH3ANandClaude Opus 4.7 07fcde0b97 Fix pixel editor colour picker + close layout gaps
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]>
2026-05-25 09:41:04 -04:00
thePR0M3TH3AN 8483718abc Keep canvas size pills on one row 2026-05-25 08:05:45 -04:00
thePR0M3TH3AN af3bc03c71 Wrap publish event IDs in sidebar 2026-05-24 15:42:12 -04:00
thePR0M3TH3AN 866d59e7f3 Improve layer panel group controls 2026-05-24 15:38:12 -04:00
thePR0M3TH3AN 216f176aab Fix canvas group shift selection 2026-05-24 15:33:24 -04:00
thePR0M3TH3AN 237e6ffbf4 Add multi-layer transform box 2026-05-24 15:29:14 -04:00
thePR0M3TH3AN ad22985ca5 Add layer keyboard and reorder controls 2026-05-24 15:25:17 -04:00
thePR0M3TH3AN 86dd560587 Fix layer delete focus handling 2026-05-24 15:10:34 -04:00
thePR0M3TH3AN 279a529b92 Add layer panel range selection 2026-05-24 15:07:09 -04:00
thePR0M3TH3AN 1f2581e1d4 Render layer groups as collapsible folders 2026-05-24 14:59:12 -04:00
thePR0M3TH3AN fc4adb151d Add layer multi-select and groups 2026-05-23 19:57:19 -04:00
thePR0M3TH3AN 9d5c2a8e43 Prevent design object preview flex squish 2026-05-23 19:49:40 -04:00
thePR0M3TH3AN 473e7f6714 Center scaled design object previews 2026-05-23 19:47:47 -04:00
thePR0M3TH3AN 9423f6c2b3 Lock edit banner and Nostr widget heights 2026-05-23 19:45:22 -04:00
thePR0M3TH3AN 3836f4003a Match Nostr widget height to edit banners 2026-05-23 19:44:05 -04:00
thePR0M3TH3AN a610d020e9 Reserve Nostr account space for edit banners 2026-05-23 19:42:35 -04:00
thePR0M3TH3AN daa623271d Preserve transparent pixels in design objects 2026-05-23 19:35:51 -04:00
thePR0M3TH3AN 5e2f98ecd7 Add editable design object layers 2026-05-23 13:22:27 -04:00
thePR0M3TH3AN 6fc0fc8a98 Trim duplicate pixel art TODOs 2026-05-23 13:13:22 -04:00
thePR0M3TH3AN e26025f679 Add working TODO list 2026-05-23 13:09:12 -04:00
thePR0M3TH3AN 8a2c146408 added pixel art editor 2026-05-22 16:06:36 -04:00
thePR0M3TH3ANandClaude Opus 4.7 b24511ad80 Add Pixel Art Phase A1 — editable pixel layers
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]>
2026-05-22 15:30:43 -04:00
thePR0M3TH3ANandClaude Opus 4.7 3e6e89e16f Add private (encrypted) publishing — Phases 1 & 2
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]>
2026-05-22 13:48:59 -04:00
thePR0M3TH3ANandClaude Opus 4.7 a0ebd9c9e2 Mark editor / tool UX TODO items as shipped
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]>
2026-05-22 07:49:20 -04:00
thePR0M3TH3ANandClaude Opus 4.7 1a517c9552 Add on-canvas resize handles and expand the Text tool panel
- 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]>
2026-05-22 07:48:24 -04:00
thePR0M3TH3ANandClaude Opus 4.7 cf006ce1aa Add editor / tool UX improvements to TODO
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]>
2026-05-22 07:33:30 -04:00
thePR0M3TH3ANandClaude Opus 4.7 5e44c9e2e4 Draft Pixel Art feature plan with event-size budgeting
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]>
2026-05-22 07:29:14 -04:00
thePR0M3TH3ANandClaude Opus 4.7 39f0ed0eb0 Expand Artstr Stacks spec into a HyperCard behaviour model
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]>
2026-05-22 07:05:12 -04:00
thePR0M3TH3ANandClaude Opus 4.7 19ee00f14a Tighten Artstr Stacks spec after review pass
- 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]>
2026-05-21 22:40:41 -04:00
thePR0M3TH3ANandClaude Opus 4.7 22256182d2 Draft Artstr Stacks (interactive pages) feature plan
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]>
2026-05-21 22:37:51 -04:00
thePR0M3TH3ANandClaude Opus 4.7 0870ccfa08 Draft private (encrypted) publishing feature plan
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]>
2026-05-21 22:31:21 -04:00
thePR0M3TH3ANandClaude Opus 4.7 9bf99f1abb Add Slide Decks & Presenter Mode (v1: Phases A–D)
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]>
2026-05-21 22:10:06 -04:00
thePR0M3TH3ANandClaude Opus 4.7 06c5ab6768 Draft slide-deck & presenter-mode feature plan
- 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]>
2026-05-21 14:30:59 -04:00
thePR0M3TH3ANandClaude Opus 4.7 c872b5210d Uniform shape strokes & corners on non-square layers
- 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]>
2026-05-21 13:47:03 -04:00
thePR0M3TH3ANandClaude Opus 4.7 2298512dce Optional background image, disc-image placement fixes, browser polish
- 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]>
2026-05-20 22:44:55 -04:00
thePR0M3TH3ANandClaude Opus 4.7 159f60385a Update favicon
Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 22:06:50 -04:00
thePR0M3TH3ANandClaude Opus 4.7 8d07c81667 Support-the-dev publish flow, clip-to-shape fix, rebrand assets
- 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]>
2026-05-20 22:05:55 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ef7d4803f7 Text/Shape/Hand tools, Designer-tab restructure, rename to Artstr Studio
- 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]>
2026-05-20 15:30:46 -04:00