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",
|
"id": "screenshot",
|
||||||
"name": "Screenshot",
|
"name": "Screenshot",
|
||||||
"version": "0.6.4",
|
"version": "0.6.5",
|
||||||
"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.",
|
"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",
|
"author": "Silent Mode",
|
||||||
"icon": "📸",
|
"icon": "📸",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ body { background: var(--bg); color: var(--ink);
|
||||||
.stage #base { position: static; }
|
.stage #base { position: static; }
|
||||||
.stage #over { pointer-events: none; }
|
.stage #over { pointer-events: none; }
|
||||||
.stage[data-tool="arrow"] { cursor: crosshair; }
|
.stage[data-tool="arrow"] { cursor: crosshair; }
|
||||||
|
.stage[data-tool="line"] { cursor: crosshair; }
|
||||||
.stage[data-tool="rect"] { cursor: crosshair; }
|
.stage[data-tool="rect"] { cursor: crosshair; }
|
||||||
.stage[data-tool="ellipse"]{ cursor: crosshair; }
|
.stage[data-tool="ellipse"]{ cursor: crosshair; }
|
||||||
.stage[data-tool="pen"] { 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);
|
background: rgba(20,26,36,.92); color: var(--ink);
|
||||||
border: 1px solid var(--acid); padding: 2px 6px;
|
border: 1px solid var(--acid); padding: 2px 6px;
|
||||||
font: 15px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
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 {
|
.toast {
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,9 @@
|
||||||
<button class="tool" data-tool="arrow" title="Arrow (A)">
|
<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>
|
<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>
|
||||||
|
<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)">
|
<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>
|
<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>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -392,6 +392,14 @@ function drawArrow(a, b) {
|
||||||
octx.stroke();
|
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) {
|
function drawRect(a, b) {
|
||||||
strokeStyle();
|
strokeStyle();
|
||||||
const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
|
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;
|
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 };
|
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); }
|
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 === "rect") { clearOver(); drawRect(a, b); }
|
||||||
else if (state.tool === "ellipse") { clearOver(); drawEllipse(a, b); }
|
else if (state.tool === "ellipse") { clearOver(); drawEllipse(a, b); }
|
||||||
else if (state.tool === "crop"
|
else if (state.tool === "crop"
|
||||||
|
|
@ -539,13 +548,16 @@ base.addEventListener("pointerup", (ev) => {
|
||||||
function openTextInput(pt) {
|
function openTextInput(pt) {
|
||||||
cancelTextInput();
|
cancelTextInput();
|
||||||
const size = Math.max(16, state.width * 4);
|
const size = Math.max(16, state.width * 4);
|
||||||
const el = document.createElement("input");
|
// Textarea (not input) so the user can drag the corner to resize the
|
||||||
el.type = "text";
|
// 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.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.autocomplete = "off";
|
||||||
el.setAttribute("autocorrect", "off");
|
el.setAttribute("autocorrect", "off");
|
||||||
el.setAttribute("spellcheck", "false");
|
el.setAttribute("spellcheck", "false");
|
||||||
|
el.rows = 1;
|
||||||
// Visual weight — a bright acid halo around the box + the actual
|
// Visual weight — a bright acid halo around the box + the actual
|
||||||
// ink colour on the text itself, so users see something clearly
|
// ink colour on the text itself, so users see something clearly
|
||||||
// happened when they clicked.
|
// happened when they clicked.
|
||||||
|
|
@ -555,6 +567,9 @@ function openTextInput(pt) {
|
||||||
el.style.background = "rgba(11,14,20,0.94)";
|
el.style.background = "rgba(11,14,20,0.94)";
|
||||||
el.style.border = "2px solid var(--acid, #d6ff3d)";
|
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.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 rect = base.getBoundingClientRect();
|
||||||
const scale = stage._scale || 1;
|
const scale = stage._scale || 1;
|
||||||
const cssX = pt.x * scale + rect.left;
|
const cssX = pt.x * scale + rect.left;
|
||||||
|
|
@ -569,13 +584,18 @@ function openTextInput(pt) {
|
||||||
requestAnimationFrame(() => el.focus());
|
requestAnimationFrame(() => el.focus());
|
||||||
setTimeout(() => { if (state.textInput && state.textInput.el === el) el.focus(); }, 30);
|
setTimeout(() => { if (state.textInput && state.textInput.el === el) el.focus(); }, 30);
|
||||||
state.textInput = { x: pt.x, y: pt.y, size, el };
|
state.textInput = { x: pt.x, y: pt.y, size, el };
|
||||||
// Don't let the input's own pointerdown / mousedown / click bubble to
|
// Don't let the box's own pointer events bubble to the canvas —
|
||||||
// the canvas — otherwise every click-through re-fires openTextInput on
|
// otherwise every click-through re-fires openTextInput on the base and
|
||||||
// the base and spawns duplicate boxes.
|
// 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();
|
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) => {
|
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(); }
|
else if (ev.key === "Escape") { cancelTextInput(); ev.preventDefault(); ev.stopPropagation(); }
|
||||||
// Every other key stays inside the input — belt against a stray
|
// Every other key stays inside the input — belt against a stray
|
||||||
// document-level keydown handler stealing focus.
|
// document-level keydown handler stealing focus.
|
||||||
|
|
@ -594,7 +614,14 @@ function commitTextInput() {
|
||||||
bctx.fillStyle = state.color;
|
bctx.fillStyle = state.color;
|
||||||
bctx.font = `${ti.size}px system-ui, -apple-system, Segoe UI, Roboto, sans-serif`;
|
bctx.font = `${ti.size}px system-ui, -apple-system, Segoe UI, Roboto, sans-serif`;
|
||||||
bctx.textBaseline = "alphabetic";
|
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();
|
pushSnapshot();
|
||||||
}
|
}
|
||||||
function cancelTextInput() {
|
function cancelTextInput() {
|
||||||
|
|
@ -751,6 +778,7 @@ window.addEventListener("keydown", (ev) => {
|
||||||
if (ev.key === "Escape") { cancelCrop(); ev.preventDefault(); return; }
|
if (ev.key === "Escape") { cancelCrop(); ev.preventDefault(); return; }
|
||||||
}
|
}
|
||||||
if (ev.key === "a") setTool("arrow");
|
if (ev.key === "a") setTool("arrow");
|
||||||
|
else if (ev.key === "l") setTool("line");
|
||||||
else if (ev.key === "r") setTool("rect");
|
else if (ev.key === "r") setTool("rect");
|
||||||
else if (ev.key === "o") setTool("ellipse");
|
else if (ev.key === "o") setTool("ellipse");
|
||||||
else if (ev.key === "p") setTool("pen");
|
else if (ev.key === "p") setTool("pen");
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button class="act" id="btn-discard" disabled>Discard</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue