スマホで手書き入力する「Hand Write Input」
【更新履歴】
・2026/1/31 バージョン1.0公開。
・2026/1/31 バージョン1.2公開。
・2026/1/31 バージョン1.2(英語版)公開。
《このツールの概要》
・スマホ画面のテキスト入力方式といえば、
フリック入力が一般的ですが、
このツールでは、手書きで入力します。
・手書き入力自体は、珍しくはないと思うんですが、
AIに質問したりする分には便利かもしれません。
・入力したテキストは、
コピーボタンを押すことで
他のwebページなどで使用できる他、
検索ボタンやAIボタンを押すことで
Googleの検索結果を表示したり、
ChatGptからの解答を表示することができます。
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Handwriting Input 1.2</title>
<style>
:root {
--bg-color: #121212;
--text-color: #fff;
--panel-bg: #ffffff; /* 手書きエリア: 白 */
--border-color: #333;
/* Button Colors */
--col-ent: #ffd900; /* 改行: 黄色 */
--col-bs: #d62828; /* 削除: 赤 */
--col-spc: #777777; /* 空白: グレー */
--col-ctl: #40e0d0; /* 操作系: 水色 */
--col-ai: #ff1493; /* AI: ピンク */
--col-sch: #00e676; /* 検索: 若葉色 */
--col-cpy: #00bfff; /* コピー: 水色 */
--col-pst: #ff8c00; /* 貼付: オレンジ */
}
body {
font-family: "Hiragino Kaku Gothic ProN", sans-serif;
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--bg-color);
color: var(--text-color);
touch-action: none;
overflow: hidden;
/* スマホ下部のセーフエリア対応 */
padding-bottom: env(safe-area-inset-bottom);
}
/* --- 上部エリア --- */
#top-area {
height: 160px;
padding: 10px;
display: flex;
gap: 8px;
background: #000;
border-bottom: 2px solid var(--border-color);
box-sizing: border-box;
flex-shrink: 0;
transition: height 0.3s;
z-index: 50;
}
/* エディタ全画面モード */
#top-area.fullscreen-editor {
height: 100%;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
z-index: 100;
}
/* 候補全画面モード */
body.cand-fullscreen #mid-bar-wrapper {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
height: 100%;
z-index: 200;
background: var(--bg-color);
flex-direction: column;
}
body.cand-fullscreen #candidate-bar {
flex-wrap: wrap;
overflow-y: auto;
align-content: flex-start;
padding: 10px;
}
body.cand-fullscreen #cand-toggle-btn {
height: 50px;
border-bottom: 1px solid #333;
}
body.cand-fullscreen #top-area,
body.cand-fullscreen #canvas-wrapper {
display: none;
}
#input-wrapper {
flex: 1;
display: flex;
flex-direction: column;
}
textarea {
width: 100%;
height: 100%;
background: #fff;
color: #000;
border: 2px solid #555;
border-radius: 6px;
padding: 8px;
font-size: 18px;
line-height: 1.5;
resize: none;
box-sizing: border-box;
outline: none;
}
#control-panel {
width: 130px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 4px;
}
.btn-col {
display: flex;
flex-direction: column;
gap: 4px;
}
button {
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
cursor: pointer;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: opacity 0.1s;
}
button:active { opacity: 0.7; }
.btn-ent { background: var(--col-ent); color: #000; font-size: 20px; flex: 1; }
.btn-spc { background: var(--col-spc); color: #fff; font-size: 12px; flex: 1; }
.btn-bs { background: var(--col-bs); color: #fff; font-size: 16px; flex: 1; }
.btn-ctl { background: var(--col-ctl); color: #000; font-size: 16px; flex: 1; }
.btn-cpy { background: var(--col-cpy); color: #fff; font-size: 12px; flex: 1; }
.btn-pst { background: var(--col-pst); color: #fff; font-size: 12px; flex: 1; }
.btn-sch { background: var(--col-sch); color: #fff; font-size: 12px; flex: 1; }
.btn-ai { background: var(--col-ai); color: #fff; font-size: 12px; flex: 1; }
/* --- Middle Bar (候補 + 切替ボタン) --- */
#mid-bar-wrapper {
height: 44px;
background: #222;
display: flex;
align-items: stretch;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
#candidate-bar {
flex: 1;
display: flex;
align-items: center;
overflow-x: auto;
padding: 0 10px;
gap: 6px;
}
#candidate-bar::-webkit-scrollbar { height: 4px; }
#candidate-bar::-webkit-scrollbar-thumb { background: #555; border-radius: 2px; }
/* 全画面切替ボタン */
#cand-toggle-btn {
width: 50px;
background: var(--col-ctl);
color: #000;
font-size: 20px;
border-left: 1px solid #000;
}
.cand-label {
color: #888; font-size: 10px; margin-right: 4px; white-space: nowrap;
}
.cand-chip {
padding: 5px 14px;
border-radius: 6px;
font-size: 16px;
white-space: nowrap;
cursor: pointer;
border: 1px solid #ccc;
flex-shrink: 0;
/* 白背景・黒文字 */
background: #ffffff;
color: #000000;
font-weight: bold;
}
/* --- 手書きエリア --- */
#canvas-wrapper {
flex: 1;
position: relative;
background: var(--panel-bg);
margin: 10px;
/* 下部余白を確保 (ボタン回避) */
margin-bottom: 70px;
border-radius: 8px;
border: 1px solid #ccc;
touch-action: none;
}
canvas {
display: block;
width: 100%;
height: 100%;
border-radius: 8px;
cursor: crosshair;
}
#status-overlay {
position: absolute;
top: 10px;
right: 10px;
color: rgba(0,0,0,0.3); /* 背景が白なので文字色は黒系透過 */
font-size: 12px;
pointer-events: none;
}
.float-clear {
position: absolute;
/* キャンバス枠内の下部 */
bottom: 10px;
left: 15px;
background: rgba(255, 59, 48, 0.9);
color: white;
padding: 10px 20px;
border-radius: 25px;
font-size: 14px;
z-index: 10;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
</style>
</head>
<body>
<div id="top-area">
<div id="input-wrapper">
<textarea id="text-input" placeholder="手書きで入力..." inputmode="none"></textarea>
</div>
<div id="control-panel">
<div class="btn-col">
<button class="btn-ent" onclick="insertChar('\n')">↵</button>
<button class="btn-spc" onclick="insertChar(' ')">空白</button>
<button class="btn-bs" onclick="deleteChar()">⌫</button>
</div>
<div class="btn-col">
<button class="btn-ctl" onclick="scrollText(-1)">▲</button>
<button class="btn-ctl" onclick="toggleEditorFull()">□</button>
<button class="btn-ctl" onclick="scrollText(1)">▼</button>
</div>
<div class="btn-col">
<button class="btn-cpy" onclick="copyText()">コピ</button>
<button class="btn-pst" onclick="pasteText()">貼付</button>
<button class="btn-sch" onclick="webSearch()">検索</button>
<button class="btn-ai" onclick="askAI()">AI</button>
</div>
</div>
</div>
<div id="mid-bar-wrapper">
<div id="candidate-bar">
<span class="cand-label">認識候補:</span>
</div>
<button id="cand-toggle-btn" onclick="toggleCandFull()">□</button>
</div>
<div id="canvas-wrapper">
<div id="status-overlay">入力待ち</div>
<button class="float-clear" onclick="clearCanvas(true)">全消去</button>
<canvas id="main-canvas"></canvas>
</div>
<script>
// --- 要素取得 ---
const textarea = document.getElementById('text-input');
const canvas = document.getElementById('main-canvas');
const ctx = canvas.getContext('2d');
const statusDiv = document.getElementById('status-overlay');
const candBar = document.getElementById('candidate-bar');
const topArea = document.getElementById('top-area');
// --- 変数 ---
let isDrawing = false;
let traces = [];
let currentStroke = { x: [], y: [], t: [] };
let rect = canvas.getBoundingClientRect();
let timer = null;
let startTime = 0;
// --- 初期化 ---
function initCanvas() {
rect = canvas.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = '#00BFFF'; // 筆跡は水色
}
window.addEventListener('resize', initCanvas);
setTimeout(initCanvas, 100);
// --- 手書きイベント処理 ---
canvas.addEventListener('pointerdown', startDrawing);
canvas.addEventListener('pointermove', draw);
canvas.addEventListener('pointerup', endDrawing);
canvas.addEventListener('pointerleave', endDrawing);
function startDrawing(e) {
e.preventDefault();
clearTimeout(timer);
isDrawing = true;
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
startTime = Date.now();
currentStroke = { x: [x], y: [y], t: [0] };
ctx.beginPath();
ctx.moveTo(x, y);
statusDiv.innerText = "描画中...";
}
function draw(e) {
if (!isDrawing) return;
e.preventDefault();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const t = Date.now() - startTime;
currentStroke.x.push(x);
currentStroke.y.push(y);
currentStroke.t.push(t);
ctx.lineTo(x, y);
ctx.stroke();
}
function endDrawing(e) {
if (!isDrawing) return;
isDrawing = false;
if (currentStroke.x.length > 0) {
traces.push([currentStroke.x, currentStroke.y, currentStroke.t]);
}
statusDiv.innerText = "認識待機中...";
timer = setTimeout(recognize, 800);
}
// --- API連携 (日本語) ---
function recognize() {
if (traces.length === 0) return;
statusDiv.innerText = "解析中...";
const data = {
app_version: 0.4,
api_level: "537.36",
device: window.navigator.userAgent,
input_type: 0,
options: "enable_pre_space",
requests: [{
writing_guide: { writing_area_width: canvas.width, writing_area_height: canvas.height },
pre_context: textarea.value,
max_num_results: 10,
max_completions: 0,
language: "ja", // 日本語設定
ink: traces
}]
};
fetch('https://inputtools.google.com/request?itc=ja-t-i0-handwrit&app=demo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(json => {
if (json[0] === "SUCCESS") {
const results = json[1][0][1];
insertChar(results[0]);
showCandidates(results);
clearCanvas(false);
statusDiv.innerText = "完了";
}
})
.catch(err => {
console.error(err);
statusDiv.innerText = "エラー";
});
}
// --- UI操作関数 ---
function insertChar(char) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const val = textarea.value;
textarea.value = val.substring(0, start) + char + val.substring(end);
textarea.selectionStart = textarea.selectionEnd = start + char.length;
textarea.focus();
}
function deleteChar() {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const val = textarea.value;
if (start === end) {
if (start === 0) return;
textarea.value = val.substring(0, start - 1) + val.substring(end);
textarea.selectionStart = textarea.selectionEnd = start - 1;
} else {
textarea.value = val.substring(0, start) + val.substring(end);
textarea.selectionStart = textarea.selectionEnd = start;
}
textarea.focus();
}
function clearCanvas(clearAll = false) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
traces = [];
if (clearAll) {
statusDiv.innerText = "クリア";
candBar.innerHTML = '<span class="cand-label">認識候補:</span>';
}
}
function showCandidates(results) {
candBar.innerHTML = "";
// ラベル再表示
const lbl = document.createElement('span');
lbl.className = 'cand-label';
lbl.innerText = "候補:";
candBar.appendChild(lbl);
results.forEach(char => {
const chip = document.createElement('div');
chip.className = 'cand-chip';
chip.innerText = char;
chip.onclick = () => {
deleteChar();
insertChar(char);
// 修正: 選択したら全画面表示を閉じる
document.body.classList.remove('cand-fullscreen');
};
candBar.appendChild(chip);
});
}
// --- ボタン群の機能 ---
function copyText() {
navigator.clipboard.writeText(textarea.value).then(() => {
statusDiv.innerText = "コピーしました";
setTimeout(() => statusDiv.innerText = "", 1000);
});
}
async function pasteText() {
try {
const text = await navigator.clipboard.readText();
insertChar(text);
} catch (err) {
alert("貼り付け権限がありません");
}
}
function webSearch() {
const text = textarea.value;
if (text) window.open(`https://www.google.com/search?q=${encodeURIComponent(text)}`, '_blank');
}
function askAI() {
const text = textarea.value;
if (text) window.open(`https://chatgpt.com/?q=${encodeURIComponent(text)}`, '_blank');
}
function scrollText(dir) {
textarea.scrollTop += dir * 24;
}
function toggleEditorFull() {
topArea.classList.toggle('fullscreen-editor');
}
// 候補全画面切り替え
function toggleCandFull() {
document.body.classList.toggle('cand-fullscreen');
}
</script>
</body>
</html>・英語版はこちら。↓
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Handwriting Input 1.2</title>
<style>
:root {
--bg-color: #121212;
--text-color: #fff;
--panel-bg: #ffffff;
--border-color: #333;
/* Button Colors */
--col-ent: #ffd900; /* Enter */
--col-bs: #d62828; /* Backspace */
--col-spc: #777777; /* Space */
--col-ctl: #40e0d0; /* Control */
--col-ai: #ff1493; /* AI */
--col-sch: #00e676; /* Search */
--col-cpy: #00bfff; /* Copy */
--col-pst: #ff8c00; /* Paste */
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--bg-color);
color: var(--text-color);
touch-action: none;
overflow: hidden;
padding-bottom: env(safe-area-inset-bottom);
}
/* --- Top Area --- */
#top-area {
height: 160px;
padding: 10px;
display: flex;
gap: 8px;
background: #000;
border-bottom: 2px solid var(--border-color);
box-sizing: border-box;
flex-shrink: 0;
transition: height 0.3s;
z-index: 50;
}
#top-area.fullscreen-editor {
height: 100%;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
z-index: 100;
}
body.cand-fullscreen #mid-bar-wrapper {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
height: 100%;
z-index: 200;
background: var(--bg-color);
flex-direction: column;
}
body.cand-fullscreen #candidate-bar {
flex-wrap: wrap;
overflow-y: auto;
align-content: flex-start;
padding: 10px;
}
body.cand-fullscreen #cand-toggle-btn {
height: 50px;
border-bottom: 1px solid #333;
}
body.cand-fullscreen #top-area,
body.cand-fullscreen #canvas-wrapper {
display: none;
}
#input-wrapper {
flex: 1;
display: flex;
flex-direction: column;
}
textarea {
width: 100%;
height: 100%;
background: #fff;
color: #000;
border: 2px solid #555;
border-radius: 6px;
padding: 8px;
font-size: 18px;
line-height: 1.5;
resize: none;
box-sizing: border-box;
outline: none;
}
#control-panel {
width: 130px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 4px;
}
.btn-col {
display: flex;
flex-direction: column;
gap: 4px;
}
button {
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
cursor: pointer;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: opacity 0.1s;
}
button:active { opacity: 0.7; }
.btn-ent { background: var(--col-ent); color: #000; font-size: 20px; flex: 1; }
.btn-spc { background: var(--col-spc); color: #fff; font-size: 14px; flex: 1; }
.btn-bs { background: var(--col-bs); color: #fff; font-size: 16px; flex: 1; }
.btn-ctl { background: var(--col-ctl); color: #000; font-size: 16px; flex: 1; }
.btn-cpy { background: var(--col-cpy); color: #fff; font-size: 11px; flex: 1; }
.btn-pst { background: var(--col-pst); color: #fff; font-size: 11px; flex: 1; }
.btn-sch { background: var(--col-sch); color: #fff; font-size: 11px; flex: 1; }
.btn-ai { background: var(--col-ai); color: #fff; font-size: 11px; flex: 1; }
/* --- Middle Bar --- */
#mid-bar-wrapper {
height: 44px;
background: #222;
display: flex;
align-items: stretch;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
#candidate-bar {
flex: 1;
display: flex;
align-items: center;
overflow-x: auto;
padding: 0 10px;
gap: 6px;
}
#candidate-bar::-webkit-scrollbar { height: 4px; }
#candidate-bar::-webkit-scrollbar-thumb { background: #555; border-radius: 2px; }
#cand-toggle-btn {
width: 50px;
background: var(--col-ctl);
color: #000;
font-size: 20px;
border-left: 1px solid #000;
}
.cand-label {
color: #888; font-size: 10px; margin-right: 4px; white-space: nowrap;
}
.cand-chip {
padding: 5px 14px;
border-radius: 6px;
font-size: 16px;
white-space: nowrap;
cursor: pointer;
border: 1px solid #ccc;
flex-shrink: 0;
background: #ffffff;
color: #000000;
font-weight: bold;
}
.chip-recog { border-color: #00bfff; }
.chip-pred { border-color: #ffd900; }
.cand-separator {
width: 1px; height: 20px; background: #555; margin: 0 5px; flex-shrink: 0;
}
/* --- Writing Area --- */
#canvas-wrapper {
flex: 1;
position: relative;
background: var(--panel-bg);
margin: 10px;
margin-bottom: 70px;
border-radius: 8px;
border: 1px solid #ccc;
touch-action: none;
}
canvas {
display: block;
width: 100%;
height: 100%;
border-radius: 8px;
cursor: crosshair;
}
#status-overlay {
position: absolute;
top: 10px;
right: 10px;
color: rgba(0,0,0,0.3);
font-size: 12px;
pointer-events: none;
}
.float-clear {
position: absolute;
bottom: 10px;
left: 15px;
background: rgba(255, 59, 48, 0.9);
color: white;
padding: 10px 20px;
border-radius: 25px;
font-size: 14px;
z-index: 10;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
</style>
</head>
<body>
<div id="top-area">
<div id="input-wrapper">
<textarea id="text-input" placeholder="Handwrite here..." inputmode="none"></textarea>
</div>
<div id="control-panel">
<div class="btn-col">
<button class="btn-ent" onclick="insertChar('\n')">↵</button>
<button class="btn-spc" onclick="insertChar(' ')">_</button>
<button class="btn-bs" onclick="deleteChar()">⌫</button>
</div>
<div class="btn-col">
<button class="btn-ctl" onclick="scrollText(-1)">▲</button>
<button class="btn-ctl" onclick="toggleEditorFull()">□</button>
<button class="btn-ctl" onclick="scrollText(1)">▼</button>
</div>
<div class="btn-col">
<button class="btn-cpy" onclick="copyText()">COPY</button>
<button class="btn-pst" onclick="pasteText()">PASTE</button>
<button class="btn-sch" onclick="webSearch()">WEB</button>
<button class="btn-ai" onclick="askAI()">AI</button>
</div>
</div>
</div>
<div id="mid-bar-wrapper">
<div id="candidate-bar">
<span class="cand-label">Suggestions appear here</span>
</div>
<button id="cand-toggle-btn" onclick="toggleCandFull()">□</button>
</div>
<div id="canvas-wrapper">
<div id="status-overlay">Ready</div>
<button class="float-clear" onclick="clearCanvas(true)">CLEAR</button>
<canvas id="main-canvas"></canvas>
</div>
<script>
const textarea = document.getElementById('text-input');
const canvas = document.getElementById('main-canvas');
const ctx = canvas.getContext('2d');
const statusDiv = document.getElementById('status-overlay');
const candBar = document.getElementById('candidate-bar');
const topArea = document.getElementById('top-area');
// Basic English Dictionary
const commonEnglishWords = [
"the","be","to","of","and","a","in","that","have","I","it","for","not","on","with","he","as","you","do","at",
"this","but","his","by","from","they","we","say","her","she","or","an","will","my","one","all","would","there","their","what",
"so","up","out","if","about","who","get","which","go","me","when","make","can","like","time","no","just","him","know","take",
"people","into","year","your","good","some","could","them","see","other","than","then","now","look","only","come","its","over","think","also",
"back","after","use","two","how","our","work","first","well","way","even","new","want","because","any","these","give","day","most","us",
"hello","hi","thanks","please","sorry","yes","why","where","right","left","help","love","great","ok","sure",
"friend","school","house","night","world","life","hand","write","input","tool","code","program","design","text","best","better"
].sort();
let isDrawing = false;
let traces = [];
let currentStroke = { x: [], y: [], t: [] };
let rect = canvas.getBoundingClientRect();
let timer = null;
let startTime = 0;
let lastRecogCandidates = [];
function initCanvas() {
rect = canvas.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = '#00BFFF';
}
window.addEventListener('resize', initCanvas);
setTimeout(initCanvas, 100);
canvas.addEventListener('pointerdown', startDrawing);
canvas.addEventListener('pointermove', draw);
canvas.addEventListener('pointerup', endDrawing);
canvas.addEventListener('pointerleave', endDrawing);
textarea.addEventListener('keyup', () => updateCandidatesUI());
textarea.addEventListener('click', () => updateCandidatesUI());
function startDrawing(e) {
e.preventDefault();
clearTimeout(timer);
isDrawing = true;
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
startTime = Date.now();
currentStroke = { x: [x], y: [y], t: [0] };
ctx.beginPath();
ctx.moveTo(x, y);
statusDiv.innerText = "Drawing...";
}
function draw(e) {
if (!isDrawing) return;
e.preventDefault();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const t = Date.now() - startTime;
currentStroke.x.push(x);
currentStroke.y.push(y);
currentStroke.t.push(t);
ctx.lineTo(x, y);
ctx.stroke();
}
function endDrawing(e) {
if (!isDrawing) return;
isDrawing = false;
if (currentStroke.x.length > 0) traces.push([currentStroke.x, currentStroke.y, currentStroke.t]);
statusDiv.innerText = "Waiting...";
timer = setTimeout(recognize, 800);
}
function recognize() {
if (traces.length === 0) return;
statusDiv.innerText = "Processing...";
const data = {
app_version: 0.4,
api_level: "537.36",
device: window.navigator.userAgent,
input_type: 0,
options: "enable_pre_space",
requests: [{
writing_guide: { writing_area_width: canvas.width, writing_area_height: canvas.height },
pre_context: textarea.value,
max_num_results: 10,
max_completions: 0,
language: "en",
ink: traces
}]
};
fetch('https://inputtools.google.com/request?itc=en-t-i0-handwrit&app=demo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(json => {
if (json[0] === "SUCCESS") {
const results = json[1][0][1];
insertChar(results[0], false);
lastRecogCandidates = results;
updateCandidatesUI();
clearCanvas(false);
statusDiv.innerText = "Done";
}
})
.catch(err => {
console.error(err);
statusDiv.innerText = "Error";
});
}
function insertChar(char, clearRecog = true) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const val = textarea.value;
textarea.value = val.substring(0, start) + char + val.substring(end);
textarea.selectionStart = textarea.selectionEnd = start + char.length;
textarea.focus();
if(clearRecog) lastRecogCandidates = [];
updateCandidatesUI();
}
function deleteChar() {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const val = textarea.value;
if (start === end) {
if (start === 0) return;
textarea.value = val.substring(0, start - 1) + val.substring(end);
textarea.selectionStart = textarea.selectionEnd = start - 1;
} else {
textarea.value = val.substring(0, start) + val.substring(end);
textarea.selectionStart = textarea.selectionEnd = start;
}
textarea.focus();
lastRecogCandidates = [];
updateCandidatesUI();
}
function clearCanvas(clearAll = false) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
traces = [];
if (clearAll) {
statusDiv.innerText = "Cleared";
lastRecogCandidates = [];
updateCandidatesUI();
}
}
function getWordAtCursor() {
const text = textarea.value;
const cursorPos = textarea.selectionStart;
let start = cursorPos;
while (start > 0 && /[a-zA-Z']/.test(text[start - 1])) {
start--;
}
let end = cursorPos;
while (end < text.length && /[a-zA-Z']/.test(text[end])) {
end++;
}
const word = text.substring(start, end);
return { word, start, end };
}
function replaceWord(newWord) {
const { start, end } = getWordAtCursor();
const text = textarea.value;
const insertText = newWord + " ";
textarea.value = text.substring(0, start) + insertText + text.substring(end);
textarea.selectionStart = textarea.selectionEnd = start + insertText.length;
textarea.focus();
lastRecogCandidates = [];
updateCandidatesUI();
}
function updateCandidatesUI() {
candBar.innerHTML = "";
// 1. Recognition Candidates
if (lastRecogCandidates.length > 0) {
const lbl = document.createElement('span');
lbl.className = 'cand-label';
lbl.innerText = "Fix:";
candBar.appendChild(lbl);
lastRecogCandidates.forEach(char => {
const chip = document.createElement('div');
chip.className = 'cand-chip chip-recog';
chip.innerText = char;
chip.onclick = () => {
deleteChar();
insertChar(char, true);
document.body.classList.remove('cand-fullscreen'); // Close fullscreen
};
candBar.appendChild(chip);
});
const sep = document.createElement('div');
sep.className = 'cand-separator';
candBar.appendChild(sep);
}
// 2. Predictive Candidates
const { word } = getWordAtCursor();
if (word && word.length > 0) {
const lowerInput = word.toLowerCase();
const predictions = commonEnglishWords.filter(w => w.toLowerCase().startsWith(lowerInput) && w.toLowerCase() !== lowerInput);
if (predictions.length > 0) {
const lbl = document.createElement('span');
lbl.className = 'cand-label';
lbl.innerText = "Next:";
candBar.appendChild(lbl);
predictions.slice(0, 10).forEach(pred => {
const chip = document.createElement('div');
chip.className = 'cand-chip chip-pred';
chip.innerText = pred;
chip.onclick = () => {
replaceWord(pred);
document.body.classList.remove('cand-fullscreen'); // Close fullscreen
};
candBar.appendChild(chip);
});
}
}
}
function copyText() {
navigator.clipboard.writeText(textarea.value).then(() => {
const original = statusDiv.innerText;
statusDiv.innerText = "Copied!";
setTimeout(() => statusDiv.innerText = original, 1000);
});
}
async function pasteText() {
try {
const text = await navigator.clipboard.readText();
insertChar(text);
} catch (err) {
alert("Paste permission denied");
}
}
function webSearch() {
const text = textarea.value;
if (text) window.open(`https://www.google.com/search?q=${encodeURIComponent(text)}`, '_blank');
}
function askAI() {
const text = textarea.value;
if (text) window.open(`https://chatgpt.com/?q=${encodeURIComponent(text)}`, '_blank');
}
function scrollText(dir) {
textarea.scrollTop += dir * 24;
}
function toggleEditorFull() {
topArea.classList.toggle('fullscreen-editor');
}
// Toggle Candidate Fullscreen
function toggleCandFull() {
document.body.classList.toggle('cand-fullscreen');
}
</script>
</body>
</html>

コメント