screenshot 0.6.5: line tool + resizable text box
Two annotation-editor asks: - Line tool: same drag flow as the arrow, no arrowhead. New toolbar button between arrow and rect, shortcut L. - Text box is resizable: swapped the single-line input for a textarea with resize:both and a drag corner. Enter still commits, Shift+Enter inserts a newline, blur commits. Multi-line rendering steps the fillText baseline by 1.15x the font size per line so the baked pixels match the live layout. The textarea swallows its own pointer events so drag-resizing the corner doesn't leak to the canvas underneath.
This commit is contained in:
parent
ed323713d7
commit
cd098bc9f3
5 changed files with 50 additions and 13 deletions
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "screenshot",
|
||||
"name": "Screenshot",
|
||||
"version": "0.6.4",
|
||||
"description": "Capture the current tab — visible viewport, full page, or a rectangle you draw. Preview + annotate editor (crop, arrow, rect, ellipse, pen, text, blur/mosaic redaction, undo, copy, save) live inside the sidebar. Expand the sidebar to full window for a canvas-sized editor.",
|
||||
"version": "0.6.5",
|
||||
"description": "Capture the current tab — visible viewport, full page, or a rectangle you draw. Preview + annotate editor (crop, arrow, line, rect, ellipse, pen, resizable text, blur/mosaic redaction, undo, copy, save) live inside the sidebar. Expand the sidebar to full window for a canvas-sized editor.",
|
||||
"author": "Silent Mode",
|
||||
"icon": "📸",
|
||||
"main": "index.js",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ body { background: var(--bg); color: var(--ink);
|
|||
.stage #base { position: static; }
|
||||
.stage #over { pointer-events: none; }
|
||||
.stage[data-tool="arrow"] { cursor: crosshair; }
|
||||
.stage[data-tool="line"] { cursor: crosshair; }
|
||||
.stage[data-tool="rect"] { cursor: crosshair; }
|
||||
.stage[data-tool="ellipse"]{ cursor: crosshair; }
|
||||
.stage[data-tool="pen"] { cursor: crosshair; }
|
||||
|
|
@ -149,7 +150,12 @@ body { background: var(--bg); color: var(--ink);
|
|||
background: rgba(20,26,36,.92); color: var(--ink);
|
||||
border: 1px solid var(--acid); padding: 2px 6px;
|
||||
font: 15px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
outline: none; border-radius: 3px; min-width: 80px;
|
||||
outline: none; border-radius: 3px;
|
||||
/* Textarea variant — corner drag-to-resize is on (see resize:both in
|
||||
JS). These floors keep the box visible even if the user resizes it
|
||||
down to almost nothing. */
|
||||
min-width: 80px; min-height: 24px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.toast {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@
|
|||
<button class="tool" data-tool="arrow" title="Arrow (A)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 13L13 3M13 3H8M13 3v5"/></svg>
|
||||
</button>
|
||||
<button class="tool" data-tool="line" title="Line (L)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 13L13 3"/></svg>
|
||||
</button>
|
||||
<button class="tool" data-tool="rect" title="Rectangle (R)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2.5" y="3.5" width="11" height="9"/></svg>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -392,6 +392,14 @@ function drawArrow(a, b) {
|
|||
octx.stroke();
|
||||
}
|
||||
|
||||
// Plain line — same drag flow as arrow, minus the head. Useful for
|
||||
// underlines / separators / crossing-out without pointing at anything.
|
||||
function drawLine(a, b) {
|
||||
strokeStyle();
|
||||
octx.beginPath();
|
||||
octx.moveTo(a.x, a.y); octx.lineTo(b.x, b.y); octx.stroke();
|
||||
}
|
||||
|
||||
function drawRect(a, b) {
|
||||
strokeStyle();
|
||||
const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
|
||||
|
|
@ -492,6 +500,7 @@ base.addEventListener("pointermove", (ev) => {
|
|||
state.drag.x = p.x; state.drag.y = p.y;
|
||||
const a = { x: state.drag.x0, y: state.drag.y0 }, b = { x: p.x, y: p.y };
|
||||
if (state.tool === "arrow") { clearOver(); drawArrow(a, b); }
|
||||
else if (state.tool === "line") { clearOver(); drawLine(a, b); }
|
||||
else if (state.tool === "rect") { clearOver(); drawRect(a, b); }
|
||||
else if (state.tool === "ellipse") { clearOver(); drawEllipse(a, b); }
|
||||
else if (state.tool === "crop"
|
||||
|
|
@ -539,13 +548,16 @@ base.addEventListener("pointerup", (ev) => {
|
|||
function openTextInput(pt) {
|
||||
cancelTextInput();
|
||||
const size = Math.max(16, state.width * 4);
|
||||
const el = document.createElement("input");
|
||||
el.type = "text";
|
||||
// Textarea (not input) so the user can drag the corner to resize the
|
||||
// box, and so text can wrap / span multiple lines. Enter commits;
|
||||
// Shift+Enter inserts a newline.
|
||||
const el = document.createElement("textarea");
|
||||
el.className = "text-input";
|
||||
el.placeholder = "type, then Enter";
|
||||
el.placeholder = "type, then Enter (Shift+Enter for newline · drag corner to resize)";
|
||||
el.autocomplete = "off";
|
||||
el.setAttribute("autocorrect", "off");
|
||||
el.setAttribute("spellcheck", "false");
|
||||
el.rows = 1;
|
||||
// Visual weight — a bright acid halo around the box + the actual
|
||||
// ink colour on the text itself, so users see something clearly
|
||||
// happened when they clicked.
|
||||
|
|
@ -555,6 +567,9 @@ function openTextInput(pt) {
|
|||
el.style.background = "rgba(11,14,20,0.94)";
|
||||
el.style.border = "2px solid var(--acid, #d6ff3d)";
|
||||
el.style.boxShadow = "0 0 0 3px rgba(214,255,61,.25), 0 4px 14px rgba(0,0,0,.45)";
|
||||
el.style.resize = "both";
|
||||
el.style.overflow = "auto";
|
||||
el.style.whiteSpace = "pre";
|
||||
const rect = base.getBoundingClientRect();
|
||||
const scale = stage._scale || 1;
|
||||
const cssX = pt.x * scale + rect.left;
|
||||
|
|
@ -569,13 +584,18 @@ function openTextInput(pt) {
|
|||
requestAnimationFrame(() => el.focus());
|
||||
setTimeout(() => { if (state.textInput && state.textInput.el === el) el.focus(); }, 30);
|
||||
state.textInput = { x: pt.x, y: pt.y, size, el };
|
||||
// Don't let the input's own pointerdown / mousedown / click bubble to
|
||||
// the canvas — otherwise every click-through re-fires openTextInput on
|
||||
// the base and spawns duplicate boxes.
|
||||
// Don't let the box's own pointer events bubble to the canvas —
|
||||
// otherwise every click-through re-fires openTextInput on the base and
|
||||
// spawns duplicate boxes, and drag-to-resize on the corner handle would
|
||||
// start a canvas drag on the tool underneath.
|
||||
const swallow = (ev) => ev.stopPropagation();
|
||||
for (const t of ["pointerdown", "mousedown", "click"]) el.addEventListener(t, swallow);
|
||||
for (const t of ["pointerdown", "mousedown", "click", "pointerup", "pointermove"]) {
|
||||
el.addEventListener(t, swallow);
|
||||
}
|
||||
el.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Enter") { commitTextInput(); ev.preventDefault(); ev.stopPropagation(); }
|
||||
if (ev.key === "Enter" && !ev.shiftKey) {
|
||||
commitTextInput(); ev.preventDefault(); ev.stopPropagation();
|
||||
}
|
||||
else if (ev.key === "Escape") { cancelTextInput(); ev.preventDefault(); ev.stopPropagation(); }
|
||||
// Every other key stays inside the input — belt against a stray
|
||||
// document-level keydown handler stealing focus.
|
||||
|
|
@ -594,7 +614,14 @@ function commitTextInput() {
|
|||
bctx.fillStyle = state.color;
|
||||
bctx.font = `${ti.size}px system-ui, -apple-system, Segoe UI, Roboto, sans-serif`;
|
||||
bctx.textBaseline = "alphabetic";
|
||||
bctx.fillText(value, ti.x, ti.y);
|
||||
// Multi-line rendering — one fillText per line, stepping down by the
|
||||
// font's line height so the visible box's layout matches the baked-in
|
||||
// pixels.
|
||||
const lineH = ti.size * 1.15;
|
||||
const lines = value.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
bctx.fillText(lines[i], ti.x, ti.y + i * lineH);
|
||||
}
|
||||
pushSnapshot();
|
||||
}
|
||||
function cancelTextInput() {
|
||||
|
|
@ -751,6 +778,7 @@ window.addEventListener("keydown", (ev) => {
|
|||
if (ev.key === "Escape") { cancelCrop(); ev.preventDefault(); return; }
|
||||
}
|
||||
if (ev.key === "a") setTool("arrow");
|
||||
else if (ev.key === "l") setTool("line");
|
||||
else if (ev.key === "r") setTool("rect");
|
||||
else if (ev.key === "o") setTool("ellipse");
|
||||
else if (ev.key === "p") setTool("pen");
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<button class="act" id="btn-discard" disabled>Discard</button>
|
||||
<button class="act primary" id="btn-edit" disabled title="Open the annotate editor — arrow, rect, ellipse, pen, text, undo, save, copy">Edit</button>
|
||||
<button class="act primary" id="btn-edit" disabled title="Open the annotate editor — arrow, line, rect, ellipse, pen, resizable text, blur, undo, save, copy">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue