diff --git a/README.md b/README.md index c22d3e7..7fe259d 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ # Artstr Studio -Artstr Studio is a static, browser-only design tool for printable physical media packaging — and increasingly, a general-purpose Nostr-native graphics editor. It started as a DVD/Blu-ray case wrap maker and now also covers Avery 8960/8944 disc labels, Avery 8693/8943 CD jewel-case inserts, single-disc designs, and free-form custom art canvases. Designs publish to and load from Nostr, are forkable and editable in place, and can be tipped via Lightning. +Artstr Studio is a static, browser-only design tool for printable physical media packaging — and increasingly, a general-purpose Nostr-native graphics editor. It started as a DVD/Blu-ray case wrap maker and now also covers Avery 8960/8944 disc labels, Avery 8693/8943 CD jewel-case inserts, single-disc designs, free-form custom art canvases, and 16:9 slides assembled into presentable slide decks. Designs publish to and load from Nostr, are forkable and editable in place, and can be tipped via Lightning. ## Tabs and layouts The editor has two tabs at the top level, each with its own Layout panel: -- **Template** — designs that map to a real printed surface. +- **Template** — designs that map to a real printed surface, plus the deck container. - **Case cover** — DVD / Blu-ray wraps with separate front / spine / back artwork or a single combined wrap image. - **Disc labels** — Avery 8960 / 8944 sheets with two disc positions. - **CD jewel inserts** — Avery 8693 / 8943 sheets with a front insert + tray insert (chosen via the Template preset). + - **Slide deck** — an ordered, self-contained presentation deck that embeds its slides inline (see below). - **Designer** — free-form and reusable designs. - **Custom art** (the Designer tab's default) — a free-form canvas with size presets (1920×1080, square sizes, etc.) or custom dimensions. Treats the project as generic art rather than packaging. + - **Slide** — a 16:9 design surface (a Custom Art canvas locked to 16:9) with a speaker-notes field. A standalone, publishable design that can be imported into a slide deck. - **Disc design** — a reusable single-disc design you can publish on Nostr and import later into either disc position of a disc-label sheet. Each layout keeps its own metadata (title / identifiers / category / language) so switching layouts — or loading a project — doesn't overwrite a design already in progress. @@ -56,6 +58,16 @@ Renders consistently in the editor, the preview surfaces, and the canvas / PDF / Cross-target and cross-tool copy/paste — Copy / Cut on a selected layer, Copy all / Paste for the whole panel. Paste retargets to the current sub-mode and clamps positions so layers copied from a wide cover don't land off-canvas on a narrow disc. +## Slide decks & presenting + +Artstr Studio doubles as a Nostr-native presentation tool. + +- **Slide** — a 16:9 design surface on the Designer tab. Mechanically a Custom Art canvas locked to 16:9, plus a **speaker-notes** field. A slide is a first-class design: publish it, fork it, load it. +- **Slide deck** — a Template-tab container holding an ordered, variable-length list of slides. The deck **embeds every slide's full data inline**, so a published deck is a single self-contained event — no external lookups. + - **Deck Builder** — a reorderable grid of slide thumbnails. Add a blank slide, import a slide from Nostr or a file (non-16:9 imports are scaled-to-fit and letterboxed), duplicate, delete, and drag to reorder. Click a thumbnail to edit that slide in the canvas editor; edits write back into the deck. + - **Deck theme** — a deck-level font and background that restyle every slide at render time without altering the slides themselves; any slide can opt out. +- **Presenter Mode** — a full-screen runtime that plays a deck: the themed current slide, speaker notes, a next-slide thumbnail, and a slide counter. Navigate with on-screen controls, click-to-advance, or the keyboard (arrows / space / Page keys / Home / End / Esc). A **Presenter ⇄ Audience** view toggle switches between the notes-and-thumbnail layout and a clean slide-only full-screen view. Launch it from the ▶ button in the Deck Builder tool palette, or from **Start Presentation** on a deck's preview page in the community browser. + ## Editor canvas The canvas is a unified, Illustrator-style workspace: the tool palette, the @@ -117,14 +129,14 @@ Two flavors of Lightning are wired in: - `src/index.html` — the complete single-file app. Open it directly in a browser, or serve it from any static host. Vercel rewrites in `vercel.json` map `/share/:id` and `/u/:npub` to the SPA entry. - `src/vendor/qrcode.min.js` — vendored QR encoder (used for Lightning tip invoices). -- `docs/` — feature specs. `PEN_TOOL_FEATURE.md` covers the now-shipped pen / pencil / vector tooling; badges, collections, NWC zaps, and zap-gated templates are still unbuilt. +- `docs/` — feature specs. `PEN_TOOL_FEATURE.md` covers the shipped pen / pencil / vector tooling; `SLIDE_DECK_FEATURE.md` covers the shipped slide / deck / presenter system (Phases A–D; dual-window presenter is future). Badges, collections, NWC zaps, and zap-gated templates are still unbuilt. - `TODO.md` — running list of deferred work and cleanup items. ## Schema - Current project schema version: **5**. - Minimum supported version: **4**. Older payloads load but won't round-trip cleanly. -- Template-mode discriminator in payloads: `cover`, `disc`, `jewel`, `customart`, `disc-design`. +- Template-mode discriminator in payloads: `cover`, `disc`, `jewel`, `customart`, `disc-design`, `slide`, `deck`. A `slide` payload carries a `slide` object (canvas dims + speaker `notes`); a `deck` payload carries a `deck` object (`theme` + an inline ordered `slides` array). - Layer types: `image`, `text`, `color`, `shape`, `qr`. Shape kinds: `rect`, `rounded-rect`, `circle`, `ellipse`, `triangle`, `polygon`, `star`, `line`, `path`, `svg`. Any layer may carry a `clip` field for masking. ## Defaults and stack diff --git a/TODO.md b/TODO.md index 94dc6c2..042de49 100644 --- a/TODO.md +++ b/TODO.md @@ -19,13 +19,9 @@ surface + canvas export, JSON round-trip). ## Cleanup -- **Remove the IMDb import.** The OMDb prompt-based lookup isn't pulling its - weight — rip out the `imdbImportBtn` button, its handler, and the OMDb - fetch. (The `imdbId` metadata field itself stays — only the auto-import - goes.) - **Delete `docs/TMDB_AUTOFILL_FEATURE.md`.** TMDB auto-fill was declined — - the metadata fields work fine filled by hand and the IMDb-style import is - being removed anyway. + the metadata fields work fine filled by hand, and the IMDb-style import + has been removed. ## Profile pages — deferred polish diff --git a/docs/SLIDE_DECK_FEATURE.md b/docs/SLIDE_DECK_FEATURE.md index 0fbaf85..e529378 100644 --- a/docs/SLIDE_DECK_FEATURE.md +++ b/docs/SLIDE_DECK_FEATURE.md @@ -36,6 +36,56 @@ the deck — all Nostr-native, forkable, and driven entirely off JSON data. - A per-property theme matrix — v1 theme overrides a small fixed set (font family, background); per-property locking is future polish. +### Implementation status + +- **Phase A — Slide format: done.** `templateMode: 'slide'` (a thin alias + of the Custom Art canvas engine via `isCanvasMode()`), Slide Designer + layout on the Designer tab, 16:9-locked Canvas panel + Speaker notes + textarea (`state.customArt.notes`), and `slide` save / load / publish / + fork as `casewrap-slide`. Slides appear in the community browser. +- **Phase B — Deck Builder: done.** `templateMode: 'deck'` with the + embedded-slide `deck` data model; Deck Builder layout on the Template + tab; the slide-sorter view (thumbnail grid with add-blank / import from + Nostr / import from file / duplicate / delete / drag-reorder); open a + slide in the canvas editor + and write back via `persistEditingDeckSlide()`; deck save / load / + publish / fork as `casewrap-deck`, self-contained; deck feed-card and + publish-confirm previews show the first slide with a slide-count badge. + Import letterboxes non-16:9 sources onto the slide. +- **Phase C — Deck theme: done.** `deck.theme` (font family + optional + background-color override) with controls in the Deck Builder sidebar; + per-slide `ignoreDeckTheme` opt-out in the slide editor; render-time + theme composition (`composeSlideForDeck`) applied non-destructively to + deck thumbnails and the community-browser preview. +- **Phase D — Presenter Mode (single-window): done.** Full-screen runtime + (`#presenterMode`): the themed current slide, speaker notes, a next-slide + thumbnail, and a `n / total` counter. On-screen prev/next, click-to- + advance, and keyboard nav (arrows / space / PageUp-Down / Home / End / + Esc); a Presenter-view ⇄ Audience-view toggle (Audience view shows the + slide alone, full-screen, with auto-hiding chrome — for single-screen + use). Presenter owns the keyboard via a + capture-phase handler, so editor shortcuts are suppressed while it runs. + Launches from a ▶ button in the Deck Builder tool palette and a + "▶ Start Presentation" button on a deck's community-browser preview. +- **Phase E (dual-window presenter):** not started — explicit future work. + +**The v1 plan (Phases A–D) is complete and shipping.** Two new categories — +`slide` ("Slide design") and `slide-deck` ("Slide deck") — were added so +slides and decks classify on their own terms, and both use the free-form +"Custom tag" field rather than the media-identifier panel. + +### Remaining work + +- **Phase E — dual-window presenter** (audience window + a separate + presenter window with notes/timer on a second screen). +- v1 non-goals still deferred: slide transitions/animations, a presenter + timer / rehearsal stats / laser pointer, rich-text speaker notes, + PowerPoint/Keynote/PDF import, deck → PDF/video export, per-property + theme locking, and a Keynote-style always-on filmstrip. +- Minor polish: the "Back to deck" bar is absolutely positioned (scrolls + away if the slide-edit canvas overflows); deck theme background is + color-only (an image-URL background was floated as future). + --- ## Why this fits the repo diff --git a/src/design_icon_32px.png b/src/design_icon_32px.png index 2be2f92..a429f40 100644 Binary files a/src/design_icon_32px.png and b/src/design_icon_32px.png differ diff --git a/src/index.html b/src/index.html index 2fa7412..7e0cc60 100644 --- a/src/index.html +++ b/src/index.html @@ -429,10 +429,27 @@ off .template-only / .designer-only — show it for either. */ #designerLayoutFieldset { display: none; } body.app-designer #designerLayoutFieldset, - body.template-customart #designerLayoutFieldset { display: block; } - /* Custom Art now lives on the Designer tab, so the Template-tab Layout - panel hides while Custom Art is active. */ - body.template-customart #layoutFieldset { display: none !important; } + body.template-customart #designerLayoutFieldset, + body.template-slide #designerLayoutFieldset { display: block; } + /* Custom Art and Slide Designer live on the Designer tab, so the + Template-tab Layout panel hides while either is active. */ + body.template-customart #layoutFieldset, + body.template-slide #layoutFieldset { display: none !important; } + /* Editing a deck's slide opens the slide editor; suppress the layout + panels so the only way out is the "Back to deck" control. */ + body.editing-deck-slide #designerLayoutFieldset, + body.editing-deck-slide #layoutFieldset, + body.editing-deck-slide #metadataFieldset, + body.editing-deck-slide #exportFieldset, + body.editing-deck-slide #printSaveFieldset { display: none !important; } + /* The Deck Builder's "Slides" and "Deck theme" panels show only in + deck mode; the per-slide theme opt-out shows only while editing a + deck slide. */ + #deckFieldset, #deckThemeFieldset { display: none; } + body.template-deck #deckFieldset, + body.template-deck #deckThemeFieldset { display: block; } + #deckSlideThemeWrap { display: none; margin-top: 10px; } + body.editing-deck-slide #deckSlideThemeWrap { display: block; } #layersFieldset { order: 40; } #designerMetadataFieldset { order: 45; } #exportFieldset { order: 50; } @@ -524,6 +541,175 @@ body.app-designer #discDesignerView { display: flex; } + /* ---- Deck Builder (slide sorter) ---- */ + #deckSorterView { display: none; } + #deckEditBar { display: none; } + body.template-deck:not(.editing-deck-slide) #deckSorterView { + display: flex; + flex-direction: column; + width: 100%; + gap: 16px; + padding: 22px 26px; + box-sizing: border-box; + min-height: calc(100vh - 160px); + } + /* In the sorter view there is nothing to draw on or zoom — hide the + sheet, the tool palette, the tool options, and the zoom bar. */ + body.template-deck:not(.editing-deck-slide) #sheetWrap, + body.template-deck:not(.editing-deck-slide) #toolOptions, + body.template-deck:not(.editing-deck-slide) #canvasToolbar, + body.template-deck:not(.editing-deck-slide) #layersFieldset { display: none !important; } + /* Deck sorter: the tool palette shows only the Present (▶) action. */ + body.template-deck:not(.editing-deck-slide) #tool-select, + body.template-deck:not(.editing-deck-slide) #tool-hand, + body.template-deck:not(.editing-deck-slide) #tool-pen, + body.template-deck:not(.editing-deck-slide) #tool-pencil, + body.template-deck:not(.editing-deck-slide) #tool-shape, + body.template-deck:not(.editing-deck-slide) #tool-text, + body.template-deck:not(.editing-deck-slide) .toolPaletteDivider { display: none !important; } + #deckPresentBtn { display: none; font-size: .9rem; } + body.template-deck #deckPresentBtn, + body.editing-deck-slide #deckPresentBtn { display: flex; } + .deckSorterBar { + display: flex; align-items: center; justify-content: space-between; + gap: 12px; flex-wrap: wrap; + } + .deckSorterCount { font-weight: 600; color: var(--ink); font-size: .92rem; } + .deckSlideGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); + gap: 16px; align-content: start; + } + .deckSlideEmpty { + grid-column: 1 / -1; text-align: center; padding: 48px 16px; + color: #64748b; border: 1px dashed var(--line); border-radius: 10px; + } + .deckSlideCard { + position: relative; border: 1px solid var(--line); border-radius: 9px; + background: #fff; overflow: hidden; cursor: pointer; + transition: box-shadow .12s ease, transform .12s ease; + } + .deckSlideCard:hover { box-shadow: 0 6px 20px rgba(15,23,42,.16); } + .deckSlideCard.dragging { opacity: .35; } + .deckSlideCard.dragover { outline: 2px solid var(--accent); outline-offset: -2px; } + .deckSlideThumb { + position: relative; width: 100%; aspect-ratio: 16 / 9; + overflow: hidden; background: #fff; + display: flex; align-items: center; justify-content: center; + } + /* Stop flexbox from horizontally shrinking the fixed-width mini-preview + (it would squish the slide); scale uniformly via fitMiniPreview. */ + .deckSlideThumb > * { flex: 0 0 auto; transform-origin: center center; } + .deckSlideMeta { + display: flex; align-items: center; gap: 6px; + padding: 6px 8px; border-top: 1px solid var(--line); font-size: .78rem; + } + .deckSlideNum { + font-weight: 700; color: #fff; background: var(--accent); + border-radius: 4px; padding: 1px 6px; flex: 0 0 auto; + } + .deckSlideName { + flex: 1; min-width: 0; + border: 1px solid transparent; background: transparent; + font: inherit; color: inherit; border-radius: 4px; padding: 1px 5px; + } + .deckSlideName:hover { border-color: var(--line); } + .deckSlideName:focus { border-color: var(--accent); background: #fff; outline: none; } + .deckSlideName::placeholder { color: #94a3b8; } + body.dark .deckSlideName:focus { background: #0b1220; } + .deckSlideActions { display: flex; gap: 4px; flex: 0 0 auto; } + .deckSlideActions button { + border: 1px solid var(--line); background: #fff; border-radius: 4px; + cursor: pointer; font-size: .8rem; padding: 2px 6px; line-height: 1.1; + } + .deckSlideActions button:hover { background: #f1f5f9; } + #deckEditBar { + position: absolute; top: 10px; left: 50%; transform: translateX(-50%); + z-index: 60; align-items: center; gap: 10px; + background: var(--panel, #fff); border: 1px solid var(--line); + border-radius: 999px; padding: 5px 14px 5px 8px; + box-shadow: 0 6px 20px rgba(15,23,42,.18); font-size: .82rem; + } + body.editing-deck-slide #deckEditBar { display: flex; } + #deckBackBtn { + border: 0; background: var(--accent); color: #fff; cursor: pointer; + border-radius: 999px; padding: 4px 12px; font-weight: 600; font-size: .8rem; + } + #deckEditLabel { color: var(--ink); font-weight: 600; } + body.dark .deckSlideCard, + body.dark .deckSlideThumb, + body.dark .deckSlideActions button { background: var(--panel); } + body.dark .deckSlideActions button:hover { background: #1e293b; } + .deckCountBadge { + position: absolute; right: 6px; bottom: 6px; z-index: 5; + background: rgba(15,23,42,.82); color: #fff; + font-size: .68rem; font-weight: 700; padding: 2px 7px; + border-radius: 999px; pointer-events: none; + } + + /* ---- Presenter Mode ---- */ + #presenterMode { + position: fixed; inset: 0; z-index: 100000; + background: #0b0d12; display: none; flex-direction: column; + } + #presenterMode.open { display: flex; } + #presenterStage { + flex: 1; min-height: 0; position: relative; + display: flex; align-items: center; justify-content: center; + overflow: hidden; background: #000; cursor: pointer; + } + #presenterStage > * { flex: 0 0 auto; transform-origin: center center; } + #presenterNotesPanel { + display: flex; gap: 18px; padding: 14px 22px; box-sizing: border-box; + height: 188px; background: #14171f; border-top: 1px solid #2a2f3a; + } + #presenterNotesInner { flex: 1; min-width: 0; display: flex; flex-direction: column; } + #presenterNotesInner h4, + .presenterNextLabel { + margin: 0 0 6px; font-size: .7rem; letter-spacing: .08em; + text-transform: uppercase; color: #8b93a7; + } + #presenterNotesText { + flex: 1; overflow-y: auto; color: #e6e9f0; + font-size: 1.02rem; line-height: 1.5; white-space: pre-wrap; + } + #presenterNextWrap { flex: 0 0 auto; width: 256px; display: flex; flex-direction: column; } + #presenterNextThumb { + flex: 1; position: relative; overflow: hidden; + background: #000; border: 1px solid #2a2f3a; border-radius: 6px; + display: flex; align-items: center; justify-content: center; + } + #presenterNextThumb > * { flex: 0 0 auto; transform-origin: center center; } + #presenterBar { + display: flex; align-items: center; gap: 12px; + padding: 8px 18px; background: #0b0d12; border-top: 1px solid #2a2f3a; + } + .presenterBarSpacer { flex: 1; } + #presenterCounter { + color: #e6e9f0; font-weight: 700; font-size: .95rem; + min-width: 72px; text-align: center; + } + .presenterBtn { + border: 1px solid #2a2f3a; background: #1c2029; color: #e6e9f0; + border-radius: 7px; padding: 7px 14px; font-size: .85rem; font-weight: 600; + cursor: pointer; + } + .presenterBtn:hover:not(:disabled) { background: #262b36; } + .presenterBtn:disabled { opacity: .4; cursor: default; } + #presenterExitBtn { border-color: #5b2330; background: #3a1820; } + #presenterExitBtn:hover { background: #4d1f2a; } + /* Audience view — the slide fills the screen; the control bar floats + over the bottom and auto-hides when the mouse goes idle. */ + #presenterMode.audience #presenterNotesPanel { display: none !important; } + #presenterMode.audience #presenterBar { + position: absolute; left: 0; right: 0; bottom: 0; + background: rgba(11,13,18,.92); + transition: transform .25s ease, opacity .25s ease; + } + #presenterMode.audience.chromeHidden #presenterBar { + transform: translateY(115%); opacity: 0; pointer-events: none; + } + .designerDiscWrap { position: relative; width: 380px; @@ -856,14 +1042,20 @@ /* Export toggles: fold/safe/trim/bleed/crops only apply to case covers */ body.template-disc .cover-toggle, body.template-jewel .cover-toggle, - body.template-customart .cover-toggle { + body.template-customart .cover-toggle, + body.template-slide .cover-toggle { display: none !important; } - /* Custom Art mode: hide all the slot/region UI and decorations, show only the canvas + layers */ - body:not(.template-customart) #customArtFieldset { + /* Custom Art / Slide mode: hide all the slot/region UI and decorations, + show only the canvas + layers (the Canvas panel serves both). */ + body:not(.template-customart):not(.template-slide) #customArtFieldset { display: none !important; } + /* Speaker notes belong to slides only. */ + #slideNotesWrap { display: none; } + body.template-slide #slideNotesWrap { display: block; } + #slideNotes { width: 100%; box-sizing: border-box; resize: vertical; font-family: inherit; font-size: .82rem; } body.template-customart #coverSizeFieldset, body.template-customart #coverArtworkFieldset, body.template-customart #discArtworkFieldset, @@ -875,10 +1067,33 @@ body.template-customart .trimArea, body.template-customart .foldLine, body.template-customart .discSpineStrip, - body.template-customart .jewelSpineGuide { + body.template-customart .jewelSpineGuide, + body.template-slide #coverSizeFieldset, + body.template-slide #coverArtworkFieldset, + body.template-slide #discArtworkFieldset, + body.template-slide #jewelArtworkFieldset, + body.template-slide .coverOnly, + body.template-slide .diskLayer, + body.template-slide .jewelLayer, + body.template-slide .bleedFill, + body.template-slide .trimArea, + body.template-slide .foldLine, + body.template-slide .discSpineStrip, + body.template-slide .jewelSpineGuide { display: none !important; } + /* Deck Builder (sorter): only Layout, Slides, Theme, Metadata and + Print/Save make sense — hide every artwork / canvas / layers panel. */ + body.template-deck #coverSizeFieldset, + body.template-deck #coverArtworkFieldset, + body.template-deck #discArtworkFieldset, + body.template-deck #jewelArtworkFieldset, + body.template-deck #customArtFieldset, + body.template-deck #exportFieldset, + body.template-deck #layersFieldset, + body.template-deck #printExportRow { display: none !important; } + .templateLayerPanels { display: grid; gap: 10px; @@ -2079,8 +2294,8 @@
- - + +
@@ -2102,6 +2317,7 @@
+
@@ -2180,10 +2396,7 @@ -
- -
-

IMDb import uses OMDb-compatible endpoint + API key. Title and category are required to save or export.

+

Title and category are required to save or publish.

@@ -2208,7 +2421,7 @@
Print & Save -
+
@@ -2245,7 +2458,41 @@
-

100–8192 px. Add layers (image / text / color) using the Layers panel below.

+

100–8192 px. Add layers (image / text / color) using the Layers panel below.

+
+ + +
+
+ +

The deck theme restyles fonts and backgrounds across the deck — turn this on to keep this slide's own look.

+
+
+ +
+ Slides +
+ +
+
+ + +
+ +

No slides yet — add one to start your deck.

+

Click a slide thumbnail to edit it. Drag thumbnails to reorder.

+
+ +
+ Deck theme +

A deck theme restyles every slide in the thumbnails and Presenter Mode — the slides themselves are never changed.

+ + + +
@@ -2382,6 +2629,7 @@ Layout
+
@@ -2905,6 +3153,19 @@
Design A
+ +
+
+
0 slides
+ +
+
+
+ +
+ + +
@@ -2915,6 +3176,7 @@ +
+ +