ux(ocr): Material Symbols icons, no default crop box, more padding

- Material Symbols Outlined webfont (Google Fonts) — used for the
  FAB camera (photo_camera), and the rotate_left / rotate_right /
  restart_alt / check buttons in the crop modal. Replaces the
  platform-rendered 📷 emoji that was off-centre, and replaces
  text labels on rotate/use buttons. Cancel stays text.
- Cropper: autoCrop=false — no default selection. User drags on
  the image to draw the crop region; if they "Use this" without
  drawing, falls back to the full (possibly rotated) image.
- Modal-body padding bumped from 1.25 rem to 2 rem so the
  bottom-right corner handle sits ~48 px from the screen edge,
  outside Android's edge-swipe back-gesture zone.
- Header copy updated to "Drag on the image to select the text
  region" since there's no longer a default box to convey intent.
This commit is contained in:
João Pedro Battistella Nadas
2026-06-09 19:18:52 +02:00
parent a4064d0d72
commit cc249fb444
2 changed files with 40 additions and 16 deletions

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>finrod — translator</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cg fill='%23d4a566'%3E%3Cpath d='M16 1 L18 14 L31 16 L18 18 L16 31 L14 18 L1 16 L14 14 Z'/%3E%3Cpath d='M16 4 L22 16 L16 28 L10 16 Z' opacity='.55'/%3E%3C/g%3E%3C/svg%3E" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=block" />
<style>
:root {
color-scheme: light dark;
@@ -156,6 +157,14 @@
box-shadow: none;
}
/* Material Symbols Outlined — used for FAB camera, rotate, reset, check */
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
line-height: 1;
vertical-align: middle;
user-select: none;
}
/* Floating action button for OCR scan — bottom-right, mobile-thumb friendly */
.scan-fab {
position: fixed;
@@ -170,8 +179,6 @@
color: #fff;
border: none;
cursor: pointer;
font-size: 1.75rem;
line-height: 1;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
z-index: 100;
display: flex;
@@ -179,6 +186,7 @@
justify-content: center;
transition: transform 0.15s;
}
.scan-fab .material-symbols-outlined { font-size: 28px; }
.scan-fab:hover:not(:disabled) { transform: scale(1.06); }
.scan-fab:disabled { opacity: 0.6; cursor: progress; }
.scan-status {
@@ -223,8 +231,9 @@
min-height: 0;
max-height: 70vh;
background: #000;
/* Inset so cropper's corner handles never sit on a screen-edge gesture zone */
padding: 1.25rem;
/* Inset so cropper's corner handles never sit on a screen-edge gesture zone.
2 rem + backdrop 1 rem = ~48 px clear from screen edges on a phone. */
padding: 2rem;
}
.modal-body img { display: block; max-width: 100%; }
.modal-footer {
@@ -252,7 +261,13 @@
font-size: 0.75rem;
letter-spacing: 0.04em;
text-transform: uppercase;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
}
.modal-btn .material-symbols-outlined { font-size: 20px; }
.modal-btn.icon { padding: 0.35rem 0.55rem; min-width: 2.4rem; }
.modal-btn:hover { border-color: var(--accent); color: var(--fg); }
.modal-btn.primary {
background: var(--accent);
@@ -328,7 +343,9 @@
<div id="meta" class="meta"></div>
</div>
<button type="button" id="scan-btn" class="scan-fab" aria-label="Scan image (OCR)" title="Scan image (OCR)">📷</button>
<button type="button" id="scan-btn" class="scan-fab" aria-label="Scan image (OCR)" title="Scan image (OCR)">
<span class="material-symbols-outlined">photo_camera</span>
</button>
<script type="module">
import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6";
@@ -420,17 +437,17 @@
modal.className = "modal";
const url = URL.createObjectURL(file);
modal.innerHTML = `
<div class="modal-header">Crop &amp; rotate before OCR</div>
<div class="modal-header">Drag on the image to select the text region</div>
<div class="modal-body"><img alt="" /></div>
<div class="modal-footer">
<div class="group">
<button class="modal-btn" data-act="rotL">Rotate left</button>
<button class="modal-btn" data-act="rotR">Rotate right</button>
<button class="modal-btn" data-act="reset">Reset</button>
<button class="modal-btn icon" data-act="rotL" title="Rotate left"><span class="material-symbols-outlined">rotate_left</span></button>
<button class="modal-btn icon" data-act="rotR" title="Rotate right"><span class="material-symbols-outlined">rotate_right</span></button>
<button class="modal-btn icon" data-act="reset" title="Reset"><span class="material-symbols-outlined">restart_alt</span></button>
</div>
<div class="group">
<button class="modal-btn" data-act="cancel">Cancel</button>
<button class="modal-btn primary" data-act="use">Use this</button>
<button class="modal-btn primary icon" data-act="use" title="Use this"><span class="material-symbols-outlined">check</span></button>
</div>
</div>
`;
@@ -441,7 +458,8 @@
img.src = url;
const cropper = new Cropper(img, {
viewMode: 1,
autoCropArea: 1,
// No default crop box — user draws their own by dragging on the image.
autoCrop: false,
background: false,
// Honour EXIF orientation from the phone shot.
checkOrientation: true,
@@ -467,13 +485,15 @@
else if (act === "reset") cropper.reset();
else if (act === "cancel") finish(null);
else if (act === "use") {
// With autoCrop:false, if the user never drew a region, force a
// default crop covering the full image so getCroppedCanvas() works.
if (!cropper.cropped) cropper.crop();
const canvas = cropper.getCroppedCanvas({
// Hand off something Tesseract can chew on without being huge.
// preprocessForOCR will downscale further if needed.
maxWidth: OCR_MAX_DIM * 2,
maxHeight: OCR_MAX_DIM * 2,
});
canvas.toBlob(blob => finish(blob), "image/png");
if (canvas) canvas.toBlob(blob => finish(blob), "image/png");
else finish(null);
}
}