diff --git a/docs/PEN_TOOL_FEATURE.md b/docs/PEN_TOOL_FEATURE.md index fd97f05..6ac7ffb 100644 --- a/docs/PEN_TOOL_FEATURE.md +++ b/docs/PEN_TOOL_FEATURE.md @@ -17,6 +17,31 @@ tool-mode system with flanking toolbars to host it. contextual options toolbar, styled to match the existing bottom zoom toolbar. +### Implementation status — 2026-05-19 +- **Phase A** — done. `_editorTool` UI state, left tool palette, right + contextual options toolbar, pen overlay element, V/P/B + Esc shortcuts. +- **Phase B** — done. Pencil freehand capture, raw polyline, open-path + shape-layer commit. +- **Phase C** — done. Pen click/drag anchors, rubber-band preview, close / + Enter / dbl-click / Esc, closed-fill-on-close, `shape.pen.nodes` stored. +- **Phase D** — partial. Editable anchors + Bézier handles for selected + `tool:'pen'` paths; drag-to-move anchor (carries handles) and drag-to-move + handle, regenerates `d`, one undo per drag. *Deferred:* add/delete anchor, + corner↔smooth toggle, click-segment-to-insert, and bbox-refit when an + anchor is dragged past the original 0–100 viewBox (path clips at the box + edge until refit lands). Rotated paths are not node-editable in v1. +- **Phase E** — done. `layer.hidden` flag, eye toggle in the layer list, all + six renderers + Canvas2D export skip hidden layers. +- **Unified vector editing** — done. With the Pen tool active, the options + panel edits the *selected* shape's fill/stroke (any kind, not just pen + paths). A **"Make editable path"** button converts a selected primitive + (rect / rounded-rect / circle / ellipse / triangle / polygon / star / + line) or single-path uploaded SVG into a `tool:'pen'` editable path, after + which Phase D anchor editing applies. Primitives convert via exact node + generators; SVG `d` strings are parsed (M/L/H/V/C/S/Q/T/Z + elliptical + arcs, abs & rel). *Not convertible:* multi-element SVGs and compound + (multi-subpath) paths — the panel shows a hint instead. + ### Non-goals (v1) - A raster paint layer (true pixel buffers). Explicitly out of scope — the app's design-as-JSON model and Nostr-friendliness depend on storing diff --git a/src/index.html b/src/index.html index 11826aa..d97df18 100644 --- a/src/index.html +++ b/src/index.html @@ -388,53 +388,27 @@ font-weight: 850; } - .secondaryPanel summary[data-step]::before { - content: attr(data-step); - display: inline-grid; - place-items: center; - width: 22px; - height: 22px; - margin-right: 7px; - border-radius: 0px; - background: #eef2ff; - color: var(--accent); - font-size: .74rem; - font-weight: 850; + /* Collapsible sidebar panels — click the legend to toggle. */ + .workflowStep > legend { + cursor: pointer; + user-select: none; } - - .secondaryPanel { - background: #fbfcfe; + .workflowStep > legend::after { + content: '▾'; + display: inline-block; + margin-left: 8px; + font-size: .78em; + opacity: .55; + transition: transform .15s ease; } - - .compactDetails { - margin: 0; - border: 0; - padding: 0; - background: transparent; + .workflowStep.collapsed > legend::after { + transform: rotate(-90deg); } - - .compactDetails > summary { - list-style: none; - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; - color: var(--ink); + .workflowStep.collapsed > :not(legend) { + display: none !important; } - - .compactDetails > summary::-webkit-details-marker { - display: none; - } - - .compactDetails > summary::after { - content: "Open"; - color: var(--muted); - font-size: .72rem; - font-weight: 700; - } - - .compactDetails[open] > summary::after { - content: "Close"; + .workflowStep.collapsed { + padding-bottom: 6px; } #layoutFieldset { order: 10; } @@ -623,6 +597,16 @@ to { transform: rotate(360deg); } } + /* Non-scrolling wrapper: the previewShell scrolls inside it, but the + flanking pen toolbars anchor here so they stay locked on screen. */ + .canvasArea { + flex: 1; + min-height: 0; + min-width: 0; + position: relative; + display: flex; + } + .previewShell { background: #e5e7eb; border: 1px solid #cbd5e1; @@ -631,6 +615,11 @@ overflow: auto; flex: 1; min-height: 0; + min-width: 0; + position: relative; + /* Own stacking context: traps the high z-index pen overlay inside so + the flanking toolbars (siblings in .canvasArea) stay clickable. */ + z-index: 0; box-shadow: inset 0 1px 0 rgba(255,255,255,.7); } @@ -1249,6 +1238,11 @@ min-width: 0; overflow: hidden; } + .layerItemRow { display:flex; align-items:stretch; gap:3px; } + .layerItemRow .layerItem { flex:1; min-width:0; } + .layerEyeToggle { flex:0 0 auto; width:30px; border:1px solid var(--line); border-radius:0; background:var(--panel); color:var(--ink); cursor:pointer; font-size:.8rem; line-height:1; } + .layerEyeToggle:hover { background:rgba(127,127,127,.12); } + .layerItemRow.layerHiddenRow .layerItem { opacity:.5; } .layerItem > span:first-child { min-width: 0; @@ -1658,6 +1652,29 @@ body.export-print .canvasVeil { display:none !important; } body.exporting .canvasVeil { display:none !important; } + /* Pen/pencil tool — left tool palette + right contextual options. + Float over the preview area, matching the bottom zoom toolbar's look. */ + .toolPalette { position:absolute; left:12px; top:50%; transform:translateY(-50%); display:flex; flex-direction:column; gap:4px; background:rgba(255,255,255,.95); border:1px solid var(--line); border-radius:8px; padding:5px; z-index:20; box-shadow:0 4px 12px rgba(0,0,0,.12); } + body.dark .toolPalette { background:rgba(15,22,34,.95); } + .toolPaletteBtn { width:38px; height:38px; display:flex; align-items:center; justify-content:center; border:1px solid transparent; border-radius:6px; background:transparent; color:var(--ink); cursor:pointer; font-size:1.15rem; line-height:1; } + .toolPaletteBtn:hover { background:rgba(127,127,127,.14); } + .toolPaletteBtn.active { background:var(--accent); color:#fff; } + .toolOptions { position:absolute; right:12px; top:50%; transform:translateY(-50%); display:flex; flex-direction:column; gap:8px; background:rgba(255,255,255,.97); border:1px solid var(--line); border-radius:8px; padding:12px; z-index:20; box-shadow:0 4px 12px rgba(0,0,0,.12); width:194px; } + body.dark .toolOptions { background:rgba(15,22,34,.97); } + .toolOptions h4 { margin:0 0 2px; font-size:.82rem; font-family:inherit; } + .toolOptions label { font-size:.74rem; display:block; margin-top:4px; } + .toolOptions .toolOptRow { display:flex; gap:8px; } + .toolOptions .toolOptRow > div { flex:1; min-width:0; } + .toolOptions input[type="color"] { width:100%; height:28px; padding:0; border:1px solid var(--line); border-radius:4px; } + .toolOptions input[type="number"], .toolOptions input[type="range"] { width:100%; } + .toolOptions .buttonRow { margin:6px 0 0; } + .toolOptHint { font-size:.7rem; color:var(--muted); margin:4px 0 0; line-height:1.35; } + .penOverlay { position:absolute; inset:0; z-index:998; pointer-events:none; } + body.tool-drawing .penOverlay { pointer-events:auto; cursor:crosshair; } + body.tool-drawing .freeLayer { pointer-events:none !important; } + @media print { .toolPalette, .toolOptions, .penOverlay { display:none !important; } } + body.exporting .toolPalette, body.exporting .toolOptions, body.exporting .penOverlay { display:none !important; } + .canvasToolbar { display:flex; align-items:center; gap:6px; padding:6px 10px; background:rgba(255,255,255,.95); border:1px solid var(--line); border-radius:8px; z-index:6; box-shadow:0 4px 12px rgba(0,0,0,.08); margin:0 auto; width:fit-content; max-width:100%; flex-shrink:0; } body.dark .canvasToolbar { background:rgba(15,22,34,.92); } body.app-designer .canvasToolbar { display:none; } @@ -1806,7 +1823,7 @@ display: none !important; } - .app, main, .previewShell { + .app, main, .canvasArea, .previewShell { display: block !important; padding: 0 !important; margin: 0 !important; @@ -1832,6 +1849,7 @@ body.template-cover .app, body.template-cover main, + body.template-cover .canvasArea, body.template-cover .previewShell { width: 11in !important; height: 8.5in !important; @@ -2070,9 +2088,8 @@
IMDb import uses OMDb-compatible endpoint + API key. Title and category are required to save or export.
Click a layer on the preview to select it. Drag selected image or text layers directly on the page. Text layers are editable in place.
Title and category are required to save or publish.
Click for corner points, click-drag for curves. Click the first point or press Enter to finish, Esc to cancel.
Click and drag to draw freehand. Each stroke becomes its own layer.