TwoSameInput


画像
日本語入力の画面


画像
英語入力の画面
<!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>ツーサム v13 (英数分割&レインボー)</title>
<style>
    :root {
        --bg-color: #121212;
        --text-color: #fff;
        
        /* --- 行カラー --- */
        --c-a: #e60012; /* あ: 赤 */
        --c-k: #004098; /* か: 青 */
        --c-s: #40e0d0; /* さ: 水色 */
        --c-t: #ffd900; /* た: 黄色 */
        --c-n: #009944; /* な: 緑 */
        --c-h: #ff69b4; /* は: ピンク */
        --c-m: #800080; /* ま: 紫 */
        --c-y: #f39800; /* や: オレンジ */
        --c-r: #b08d55; /* ら: 黄土色 */
        --c-w: #6cbb5a; /* わ: 草色 */
        
        /* --- タブカラー --- */
        --tab-hira: #6f4b3e; /* 茶 */
        --tab-kata: #1d2088; /* 紺 */
        --tab-lower: #ff69b4;/* 英小: ピンク */
        --tab-upper: #c71585;/* 英大: 赤紫 */
        --tab-num:  #40e0d0; /* 数: 水色 */
        --tab-sym:  #009944; /* 記: 緑 */
        --tab-emoji:#ffd900; /* 絵: 黄 */
    }

    /* --- スクロールバーのカスタマイズ (幅を2倍に) --- */
    ::-webkit-scrollbar {
        width: 18px; /* 通常(8px前後)の約2倍 */
        background: #222;
    }
    ::-webkit-scrollbar-thumb {
        background: #555;
        border-radius: 9px;
        border: 3px solid #222; /* 隙間を作って見やすく */
    }
    ::-webkit-scrollbar-thumb:hover {
        background: #777;
    }

    body {
        margin: 0; padding: 0;
        background-color: var(--bg-color);
        color: var(--text-color);
        font-family: "Hiragino Kaku Gothic ProN", sans-serif;
        height: 100vh; 
        height: 100dvh; 
        width: 100vw;
        display: flex;
        flex-direction: column;
        overflow: hidden;
        user-select: none;
        touch-action: none; 
    }

    /* --- 上部エリア --- */
    #top-area {
        padding-top: max(env(safe-area-inset-top), 30px);
        padding-bottom: 5px;
        padding-left: 10px;
        padding-right: 10px;
        height: 22%; 
        min-height: 140px;
        background: #000;
        border-bottom: 2px solid #333;
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
        flex-shrink: 0;
        z-index: 10;
        box-sizing: border-box;
    }

    #display-wrapper {
        display: flex;
        align-items: stretch;
        flex: 1;
        overflow: hidden;
        margin-bottom: 8px;
    }

    #input-line {
        font-size: 18px;
        line-height: 1.5;
        white-space: pre-wrap;
        overflow-y: scroll; /* 常にスクロールバーを意識させる */
        flex: 1;
        margin-right: 10px;
        border: 1px solid #555;
        background: #1a1a1a;
        padding: 8px;
        border-radius: 8px;
        word-break: break-all;
    }

    .composing { background: #003566; border-bottom: 2px solid #00b4d8; }

    .tool-btn-group {
        display: flex;
        flex-direction: column;
        gap: 6px;
        width: 60px;
    }
    .tool-btn {
        border: none; border-radius: 6px; flex: 1;
        font-weight: bold; font-size: 13px; cursor: pointer; color: #fff;
        display: flex; align-items: center; justify-content: center;
        box-shadow: 0 2px 0 rgba(0,0,0,0.3);
    }
    #btn-copy { background: #2a9d8f; }
    #btn-del { background: #d62828; }
    .tool-btn:active { transform: translateY(2px); box-shadow: none; opacity: 0.8; }

    /* 候補バー */
    #candidate-bar {
        height: 32px;
        display: flex;
        overflow-x: auto;
        align-items: center;
        border-top: 1px solid #333;
        white-space: nowrap;
        background: #222;
        padding-left: 5px;
        border-radius: 4px;
        flex-shrink: 0;
    }
    .cand-chip {
        background: #444; padding: 3px 12px; margin-right: 8px;
        border-radius: 12px; font-size: 14px; border: 1px solid #666;
    }
    .cand-chip:active { background: #fff; color: #000; }

    /* --- メインエリア --- */
    #main-stage {
        flex: 1;
        position: relative;
        overflow: hidden;
        background: #111;
        padding: 4px;
    }

    /* モードA: 日本語ボード */
    #twothumb-board {
        display: grid;
        grid-template-columns: 1.3fr 1fr;
        gap: 4px;
        height: 100%;
        width: 100%;
        box-sizing: border-box;
    }
    #left-panel, #right-panel { display: grid; gap: 4px; }
    #left-panel { grid-template-columns: 1fr 1fr; grid-template-rows: repeat(6, 1fr); }
    #right-panel { grid-template-columns: 1fr; grid-template-rows: repeat(5, 1fr); }

    .key {
        display: flex; align-items: center; justify-content: center;
        font-weight: bold; border-radius: 8px; font-size: 18px;
        cursor: pointer; color: #fff;
        text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
        box-shadow: 0 3px 0 rgba(0,0,0,0.2);
    }
    .key:active { transform: translateY(2px); box-shadow: none; }

    /* 行カラー */
    .row-a { background: var(--c-a); }
    .row-k { background: var(--c-k); }
    .row-s { background: var(--c-s); color: #000; text-shadow: none; }
    .row-t { background: var(--c-t); color: #000; text-shadow: none; }
    .row-n { background: var(--c-n); }
    .row-h { background: var(--c-h); }
    .row-m { background: var(--c-m); }
    .row-y { background: var(--c-y); color: #000; text-shadow: none;}
    .row-r { background: var(--c-r); }
    .row-w { background: var(--c-w); }
    .key-func { background: #444; font-size: 14px; color: #fff; }
    .key-left.active { border: 3px solid #fff; transform: scale(0.96); filter: brightness(1.1); z-index: 2; }
    .key-right { font-size: 28px; background: #333; }
    
    /* モードB: スクロールグリッド */
    #scroll-grid {
        display: none;
        grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
        grid-auto-rows: 60px;
        gap: 6px;
        padding: 5px;
        overflow-y: scroll; /* スクロールバー常時表示 */
        height: 100%;
        align-content: start;
        box-sizing: border-box;
        padding-bottom: 60px;
    }
    .scroll-key {
        background: #333; border: 1px solid #555; border-radius: 8px;
        display: flex; align-items: center; justify-content: center;
        font-size: 22px; color: #fff; cursor: pointer;
        text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
    }
    .scroll-key.waiting { border: 3px solid #fff; filter: brightness(1.3); }
    .scroll-key.tapped { filter: brightness(0.5); transform: scale(0.95); }
    .key-special { font-size: 14px; color: #aaa; border: 1px dashed #666; flex-direction: column; text-shadow: none; background: #222 !important; }
    .key-enter { background: #264653 !important; }

    /* --- 下部タブ --- */
    #tab-bar {
        height: 60px;
        background: #111;
        border-top: 1px solid #333;
        display: flex;
        flex-shrink: 0;
        z-index: 10;
        padding-bottom: max(env(safe-area-inset-bottom), 0px);
    }
    .tab {
        flex: 1;
        display: flex; align-items: center; justify-content: center;
        font-size: 10px; font-weight: bold; color: #888;
        border-right: 1px solid #222;
        flex-direction: column;
        cursor: pointer;
        background: #222;
        transition: all 0.2s;
    }
    .tab span { font-size: 14px; margin-bottom: 3px; }
    
    .t-hira.active  { background: var(--tab-hira); color: #fff; }
    .t-kata.active  { background: var(--tab-kata); color: #fff; }
    .t-lower.active { background: var(--tab-lower); color: #fff; }
    .t-upper.active { background: var(--tab-upper); color: #fff; }
    .t-num.active   { background: var(--tab-num); color: #000; }
    .t-sym.active   { background: var(--tab-sym); color: #fff; }
    .t-emoji.active { background: var(--tab-emoji); color: #000; }

</style>
</head>
<body>

<div id="top-area">
    <div id="display-wrapper">
        <div id="input-line">
            <span id="committed-text"></span><span id="composing-text" class="composing"></span>
        </div>
        <div class="tool-btn-group">
            <button id="btn-del" class="tool-btn" onclick="deleteChar()">削除</button>
            <button id="btn-copy" class="tool-btn" onclick="copyAll()">コピー</button>
        </div>
    </div>
    <div id="candidate-bar"><span style="color:#666;font-size:12px;">変換候補なし</span></div>
</div>

<div id="main-stage">
    <div id="twothumb-board">
        <div id="left-panel">
            <div class="key key-left row-a" onclick="setRow('a', this)">あ</div>
            <div class="key key-left row-k" onclick="setRow('k', this)">か</div>
            <div class="key key-left row-s" onclick="setRow('s', this)">さ</div>
            <div class="key key-left row-t" onclick="setRow('t', this)">た</div>
            <div class="key key-left row-n" onclick="setRow('n', this)">な</div>
            <div class="key key-left row-h" onclick="setRow('h', this)">は</div>
            <div class="key key-left row-m" onclick="setRow('m', this)">ま</div>
            <div class="key key-left row-y" onclick="setRow('y', this)">や</div>
            <div class="key key-left row-r" onclick="setRow('r', this)">ら</div>
            <div class="key key-left row-w" onclick="setRow('w', this)">わ</div>
            
            <div class="key key-func" style="grid-column: span 2;" onclick="modifyChar()">゛゜小</div>
        </div>
        <div id="right-panel">
            <div class="key key-right" id="r0" onclick="inputJP(0)"></div>
            <div class="key key-right" id="r1" onclick="inputJP(1)"></div>
            <div class="key key-right" id="r2" onclick="inputJP(2)"></div>
            <div class="key key-right" id="r3" onclick="inputJP(3)"></div>
            <div class="key key-right" id="r4" onclick="inputJP(4)"></div>
        </div>
    </div>

    <div id="scroll-grid"></div>
</div>

<div id="tab-bar">
    <div class="tab t-hira active" onclick="switchMode('hira', this)"><span>あ</span>平仮名</div>
    <div class="tab t-kata" onclick="switchMode('kata', this)"><span>ア</span>カタカナ</div>
    <div class="tab t-lower" onclick="switchMode('lower', this)"><span>abc</span>英小</div>
    <div class="tab t-upper" onclick="switchMode('upper', this)"><span>ABC</span>英大</div>
    <div class="tab t-num" onclick="switchMode('num', this)"><span>12</span>数字</div>
    <div class="tab t-sym" onclick="switchMode('sym', this)"><span>#%</span>記号</div>
    <div class="tab t-emoji" onclick="switchMode('emoji', this)"><span>😀</span>絵文字</div>
</div>

<script>
    // --- データ ---
    const jpMap = {
        'hira': {
            'a':['あ','い','う','え','お'], 'k':['か','き','く','け','こ'],
            's':['さ','し','す','せ','そ'], 't':['た','ち','つ','て','と'],
            'n':['な','に','ぬ','ね','の'], 'h':['は','ひ','ふ','へ','ほ'],
            'm':['ま','み','む','め','も'], 'y':['や','(','ゆ',')','よ'],
            'r':['ら','り','る','れ','ろ'], 'w':['わ','を','ん','ー','、']
        },
        'kata': {
            'a':['ア','イ','ウ','エ','オ'], 'k':['カ','キ','ク','ケ','コ'],
            's':['サ','シ','ス','セ','ソ'], 't':['タ','チ','ツ','テ','ト'],
            'n':['ナ','ニ','ヌ','ネ','ノ'], 'h':['ハ','ヒ','フ','ヘ','ホ'],
            'm':['マ','ミ','ム','メ','モ'], 'y':['ヤ','(','ユ',')','ヨ'],
            'r':['ラ','リ','ル','レ','ロ'], 'w':['ワ','ヲ','ン','ー','、']
        }
    };

    const dakutenMap = {
        'か':'が','き':'ぎ','く':'ぐ','け':'げ','こ':'ご', 'さ':'ざ','し':'じ','す':'ず','せ':'ぜ','そ':'ぞ',
        'た':'だ','ち':'ぢ','つ':'づ','て':'で','と':'ど', 'は':'ば','ひ':'び','ふ':'ぶ','へ':'べ','ほ':'ぼ',
        'ば':'ぱ','び':'ぴ','ぶ':'ぷ','べ':'ぺ','ぼ':'ぽ', 'ぱ':'は','ぴ':'ひ','ぷ':'ふ','ぺ':'へ','ぽ':'ほ',
        'つ':'っ','や':'ゃ','ゆ':'ゅ','よ':'ょ', 'あ':'ぁ','い':'ぃ','う':'ぅ','え':'ぇ','お':'ぉ',
        'カ':'ガ','サ':'ザ','タ':'ダ','ハ':'バ','バ':'パ','パ':'ハ', 'ツ':'ッ','ヤ':'ャ','ユ':'ュ','ヨ':'ョ','ア':'ァ'
    };

    const listData = {
        'lower': ["ENTER", "SPACE"].concat("abcdefghijklmnopqrstuvwxyz.,!?@_".split("")),
        'upper': ["ENTER", "SPACE"].concat("ABCDEFGHIJKLMNOPQRSTUVWXYZ.,!?@_".split("")),
        'num':   ["ENTER", "SPACE"].concat("1234567890+-*/=.,:;()¥%".split("")),
        'sym':   ["ENTER", "SPACE"].concat("!?”’()[]{}<>〜=+−/*@#$%&¥|_…↑↓←→★☆♪◯●◎".split("")),
        'emoji': ["ENTER", "SPACE"].concat("😀😂😊😍😢😡👍👎🙏❤️🔥✨🎉🍺🍜🚗🏠💡🐶🐱".split(""))
    };

    // レインボー配色の定義 (10色)
    const rainbowColors = [
        '#e60012', // 赤
        '#f39800', // オレンジ
        '#ffd900', // 黄 (文字黒)
        '#99cc00', // 黄緑 (文字黒)
        '#009944', // 緑
        '#00a0e9', // 青緑 (シアン寄り)
        '#004098', // 青
        '#4b0082', // 青紫
        '#800080', // 紫
        '#e60033'  // 赤紫
    ];

    // --- 状態 ---
    let currentMode = 'hira';
    let currentRow = 'a';
    let committedStr = ""; 
    let composingStr = "";
    let lastTapTarget = null;
    let tapTimeout = null;

    // --- DOM ---
    const elCommitted = document.getElementById('committed-text');
    const elComposing = document.getElementById('composing-text');
    const elTwothumb = document.getElementById('twothumb-board');
    const elScroll = document.getElementById('scroll-grid');
    const rightKeys = Array.from(document.querySelectorAll('.key-right'));
    const inputLine = document.getElementById('input-line');

    setRow('a', document.querySelector('.key-left'));

    function switchMode(mode, tabEl) {
        currentMode = mode;
        document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
        tabEl.classList.add('active');

        if (mode === 'hira' || mode === 'kata') {
            elTwothumb.style.display = 'grid';
            elScroll.style.display = 'none';
            setRow(currentRow, null);
        } else {
            elTwothumb.style.display = 'none';
            elScroll.style.display = 'grid';
            renderScrollGrid(mode);
        }
    }

    function setRow(rowKey, element) {
        currentRow = rowKey;
        if(element) {
            document.querySelectorAll('.key-left').forEach(k => k.classList.remove('active'));
            element.classList.add('active');
            
            const computedStyle = window.getComputedStyle(element);
            const bgColor = computedStyle.backgroundColor;
            const textColor = computedStyle.color;
            
            rightKeys.forEach(k => {
                k.style.borderColor = bgColor; 
                k.style.backgroundColor = bgColor; 
                k.style.color = textColor;
                k.style.textShadow = (textColor === 'rgb(0, 0, 0)') ? 'none' : '1px 1px 2px rgba(0,0,0,0.5)';
                k.style.filter = "brightness(0.85)"; 
            });
        }
        const map = (currentMode === 'kata') ? jpMap['kata'] : jpMap['hira'];
        const chars = map[rowKey];
        rightKeys.forEach((k, i) => k.innerText = chars[i]);
    }

    function inputJP(index) {
        const map = (currentMode === 'kata') ? jpMap['kata'] : jpMap['hira'];
        const char = map[currentRow][index];
        composingStr += char;
        updateDisplay();
        updateCandidates();
    }

    function modifyChar() {
        if (!composingStr) return;
        let lastChar = composingStr.slice(-1);
        let prefix = composingStr.slice(0, -1);
        if (dakutenMap[lastChar]) {
            composingStr = prefix + dakutenMap[lastChar];
            updateDisplay();
            updateCandidates();
        }
    }

    function renderScrollGrid(mode) {
        elScroll.innerHTML = "";
        const chars = listData[mode];
        
        // 英字モード(lower/upper)の場合のみカウントして色付け
        let alphaCounter = 0;
        const isAlphaMode = (mode === 'lower' || mode === 'upper');

        chars.forEach(item => {
            const btn = document.createElement('div');
            btn.className = 'scroll-key';
            
            let displayLabel = item;
            let actualInput = item;

            // 特殊キー
            if(item === "SPACE") {
                displayLabel = "空白"; actualInput = " "; btn.classList.add('key-special');
            } else if (item === "ENTER") {
                displayLabel = "改行"; actualInput = "\n"; btn.classList.add('key-special', 'key-enter');
            } else {
                // 英字モードの色付けロジック (特殊キー以外)
                if (isAlphaMode) {
                    const colorIndex = Math.floor(alphaCounter / 3) % rainbowColors.length;
                    const thisColor = rainbowColors[colorIndex];
                    btn.style.backgroundColor = thisColor;
                    
                    // 文字色の調整(黄色・黄緑・オレンジ系は黒文字)
                    if (['#ffd900', '#99cc00', '#f39800', '#40e0d0'].includes(thisColor)) {
                        btn.style.color = "#000";
                        btn.style.textShadow = "none";
                    }
                    alphaCounter++;
                }
            }

            btn.innerText = displayLabel;
            btn.addEventListener('click', (e) => handleDoubleTapLogic(btn, actualInput));
            elScroll.appendChild(btn);
        });
    }

    function handleDoubleTapLogic(btnElement, char) {
        if (lastTapTarget !== btnElement) {
            if (lastTapTarget) lastTapTarget.classList.remove('waiting');
            lastTapTarget = btnElement;
            btnElement.classList.add('waiting'); 
            if (tapTimeout) clearTimeout(tapTimeout);
            tapTimeout = setTimeout(() => {
                if(lastTapTarget === btnElement) {
                    btnElement.classList.remove('waiting');
                    lastTapTarget = null;
                }
            }, 1500);
        } else {
            clearTimeout(tapTimeout);
            inputDirect(char);
            btnElement.classList.remove('waiting');
            btnElement.classList.add('tapped');
            setTimeout(() => btnElement.classList.remove('tapped'), 150);
            lastTapTarget = null;
        }
    }

    function inputDirect(char) {
        if (composingStr.length > 0) {
            committedStr += composingStr;
            composingStr = "";
        }
        committedStr += char;
        updateDisplay();
        updateCandidates();
    }

    function deleteChar() {
        if (composingStr.length > 0) {
            composingStr = composingStr.slice(0, -1);
        } else if (committedStr.length > 0) {
            committedStr = committedStr.slice(0, -1);
        }
        updateDisplay();
        updateCandidates();
    }

    function updateDisplay() {
        elCommitted.innerText = committedStr;
        elComposing.innerText = composingStr;
        setTimeout(() => { inputLine.scrollTop = inputLine.scrollHeight; }, 0);
    }

    function copyAll() {
        const text = committedStr + composingStr;
        if(!text) return;
        navigator.clipboard.writeText(text).then(() => {
            const btn = document.getElementById('btn-copy');
            const orig = btn.innerText;
            btn.innerText = "OK!";
            setTimeout(() => btn.innerText = orig, 1000);
        }).catch(e => alert("コピー失敗"));
    }

    function updateCandidates() {
        const bar = document.getElementById('candidate-bar');
        bar.innerHTML = "";
        if(!composingStr) {
            bar.innerHTML = '<span style="color:#666;font-size:12px;padding:0 5px;">変換候補なし</span>';
            return;
        }
        const list = [composingStr];
        if(currentMode === 'hira') list.push(hiraToKata(composingStr));
        list.forEach(c => {
            const chip = document.createElement('div');
            chip.className = 'cand-chip';
            chip.innerText = c;
            chip.onclick = () => commitCandidate(c);
            bar.appendChild(chip);
        });
    }
    
    function commitCandidate(text) {
        committedStr += text;
        composingStr = "";
        updateDisplay();
        updateCandidates();
    }

    function hiraToKata(str) {
        return str.replace(/[\u3041-\u3096]/g, ch => String.fromCharCode(ch.charCodeAt(0) + 0x60));
    }
</script>
</body>
</html>

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

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
小ぶりなプログラムを試しに作っているんですが、 ここではその説明書きをしていこうと思います。 こういう機能をつけてみてほしいだとかいった要望があれば コメント欄に書いてみて下さい。 ひまをみて対応します。
TwoSameInput|古井和雄
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