スマホで手書き入力する「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>


いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
スマホで手書き入力する「Hand Write Input」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1