スマホで手書き入力する「Hand Write Input」
【更新履歴】
・2026/1/31 バージョン1.0公開。
《このツールの概要》
・スマホ画面のテキスト入力方式といえば、
フリック入力が一般的ですが、
このツールでは、手書きで入力します。
・まあ、フリック以外の入力方式を試すために作った
という感じですが、意外とはやるかもしれません。^^;
・入力したテキストは、
コピーボタンを押すことで
他の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>Hand Write Input 1.0</title>
<style>
:root {
--bg-color: #121212;
--text-color: #fff;
--panel-bg: #1e1e1e;
--border-color: #333;
/* ボタンカラー設定 (two_same_1_4.html準拠) */
--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;
}
/* --- 上部エリア(テキスト表示 + ボタン群)--- */
#top-area {
height: 160px; /* 約4行分確保 */
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;
}
/* 全画面モード時 */
#top-area.fullscreen {
height: 100%;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
z-index: 100;
}
/* テキスト入力欄 */
#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; /* 3列 */
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; }
/* --- 手書きエリア --- */
#canvas-wrapper {
flex: 1;
position: relative;
background: var(--panel-bg);
margin: 10px;
border-radius: 8px;
border: 1px solid #444;
touch-action: none;
}
canvas {
display: block;
width: 100%;
height: 100%;
border-radius: 8px;
cursor: crosshair;
}
/* 候補表示バー */
#candidate-bar {
height: 40px;
background: #222;
display: flex;
align-items: center;
overflow-x: auto;
padding: 0 10px;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.cand-chip {
background: #444;
color: #fff;
padding: 5px 12px;
border-radius: 15px;
margin-right: 8px;
font-size: 16px;
white-space: nowrap;
cursor: pointer;
}
/* ステータス表示 */
#status-overlay {
position: absolute;
top: 10px;
right: 10px;
color: rgba(255,255,255,0.3);
font-size: 12px;
pointer-events: none;
}
/* クリアボタン(手書きエリア用) */
.float-clear {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(255, 59, 48, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
z-index: 10;
}
</style>
</head>
<body>
<div id="top-area">
<div id="input-wrapper">
<textarea id="text-input" placeholder="手書きで入力..."></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="toggleFull()">□</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="candidate-bar">
<span style="color:#666; font-size:12px;">認識候補:</span>
</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);
// --- 手書きイベント処理 (Pointer Events) ---
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); // 800msの無操作で認識開始
}
// --- 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();
}
// 削除 (BS)
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 style="color:#666; font-size:12px;">認識候補:</span>';
}
}
// 候補表示
function showCandidates(results) {
candBar.innerHTML = "";
results.forEach(char => {
const chip = document.createElement('div');
chip.className = 'cand-chip';
chip.innerText = char;
chip.onclick = () => {
// 直前の入力を置換する簡易実装
// (厳密なUndoには複雑な履歴管理が必要だが、ここでは直前の1文字/単語を差し替える動きを模倣)
const val = textarea.value;
// 簡易的に末尾を書き換える(本来はカーソル位置管理が必要)
// ここでは「修正用」として、カーソル直前の文字を削除して挿入
deleteChar();
insertChar(char);
};
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');
}
// AI
function askAI() {
const text = textarea.value;
if (text) window.open(`https://chatgpt.com/?q=${encodeURIComponent(text)}`, '_blank');
}
// スクロール
function scrollText(dir) {
textarea.scrollTop += dir * 24; // 1行分程度スクロール
}
// 全画面切り替え
function toggleFull() {
topArea.classList.toggle('fullscreen');
// 全画面時は手書きエリアを隠すなどの調整が必要だが、
// ユーザー要望の「□ボタン」の挙動(文章全体を表示)として、
// topAreaを画面いっぱいに広げる実装にしている。
}
</script>
</body>
</html>・ダウンロードされる方はこちら。↓


コメント