<!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>ツーサム v9 (修正完全版)</title>
<style>
:root {
--bg-color: #121212;
--text-color: #eee;
--accent: #00b4d8;
--key-bg: #1f1f1f;
--tab-active: #2a9d8f;
--del-color: #e63946;
--wait-color: #fca311; /* 1回タップ時の色(黄色) */
}
body {
margin: 0 ; padding: 0 ;
background-color: var (--bg-color);
color: var (--text-color);
font-family: "Hiragino Kaku Gothic ProN" , sans-serif;
height: 100 vh;
width: 100 vw;
display: flex;
flex-direction: column;
overflow: hidden;
user-select : none;
touch-action: pan-y;
}
#top-area {
height: 14 vh;
background: #000;
border-bottom: 1 px solid #444;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 5 px 10 px;
box-sizing: border-box;
}
#display-wrapper {
display: flex;
align-items: flex-start;
flex: 1 ;
overflow: hidden;
}
#input-line {
font-size: 20 px;
line-height: 1.3 ;
white-space: pre-wrap;
overflow-y: auto;
flex: 1 ;
margin-right: 10 px;
height: 100 %;
border: 1 px solid #333;
padding: 5 px;
border-radius: 4 px;
word-break : break -all;
}
.composing {
background: #003566;
border-bottom: 2 px solid #00b4d8;
}
.tool-btn-group {
display: flex;
flex-direction: column;
gap: 5 px;
justify-content: flex-start;
}
.tool-btn {
border: none;
border-radius: 4 px;
width: 60 px;
height: 35 px;
font-weight: bold;
font-size: 13 px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
#btn-copy { background: #2a9d8f; color: #fff; }
#btn-del { background: var(--del-color); color: #fff; }
.tool-btn:active { transform: scale(0.95 ); opacity: 0.8 ; }
#candidate-bar {
height: 35 px;
display: flex;
overflow-x: auto;
align-items: center;
border-top: 1 px solid #333;
white-space: nowrap;
background: #1a1a1a;
padding-left: 5 px;
}
.cand-chip {
background: #333; padding: 4px 12px; margin-right: 6px;
border-radius: 12 px; font-size: 14 px; border: 1 px solid #555;
}
.cand-chip:active { background: var (--accent); color: #000; }
#main-stage {
flex: 1 ;
position: relative;
overflow: hidden;
background: #111;
}
#twothumb-board {
display: grid;
grid-template-columns: 1.4f r 1f r;
gap: 2 px;
padding: 2 px;
height: 100 %;
width: 100 %;
box-sizing: border-box;
}
.panel-grid { display: grid; gap: 2 px; }
#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; background: var (--key-bg);
border: 1 px solid #333; border-radius: 4px;
font-size: 18 px;
cursor: pointer;
}
.key:active { background: #444; }
.key-left.active { background: var (--accent); color: #000; font-weight: 900; border: 2px solid #fff; }
.key-right { font-size: 26 px; background: #222; }
#scroll-grid {
display: none;
grid-template-columns: repeat(auto-fill, minmax(65 px, 1f r));
grid-auto-rows: 65 px;
gap: 6 px;
padding: 10 px;
overflow-y: auto;
height: 100 %;
align-content: start;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
padding-bottom: 80 px;
}
.scroll-key {
background: #222; border: 1px solid #444; border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 24 px; color: #fff;
position: relative;
cursor: pointer;
transition: background-color 0.1 s;
}
.scroll-key.waiting { background: var (--wait-color); color: #000; border-color: var(--wait-color); }
.scroll-key.tapped { background: #2a9d8f; color: #fff; transform: scale(0.95); }
.key-special { font-size: 14 px; color: #aaa; border: 1px dashed #666; flex-direction: column; }
.key-enter { background: #223344; color: #aab; }
#tab-bar {
height: 8 vh;
background: #000;
border-top: 1 px solid #444;
display: flex;
}
.tab {
flex: 1 ;
display: flex; align-items: center; justify-content: center;
font-size: 12 px; font-weight: bold; color: #666;
border-right: 1 px solid #222;
flex-direction: column;
cursor: pointer;
}
.tab span { font-size: 18 px; margin-bottom: 2 px; }
.tab.active { background: #1a1a1a; color: var(--tab-active); border-top: 3px solid var(--tab-active); }
</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" class ="panel-grid" >
<div class ="key key-left" onclick="setRow('a', this)" >あ</div>
<div class ="key key-left" onclick="setRow('k', this)" >か</div>
<div class ="key key-left" onclick="setRow('s', this)" >さ</div>
<div class ="key key-left" onclick="setRow('t', this)" >た</div>
<div class ="key key-left" onclick="setRow('n', this)" >な</div>
<div class ="key key-left" onclick="setRow('h', this)" >は</div>
<div class ="key key-left" onclick="setRow('m', this)" >ま</div>
<div class ="key key-left" onclick="setRow('y', this)" >や</div>
<div class ="key key-left" onclick="setRow('r', this)" >ら</div>
<div class ="key key-left" onclick="setRow('w', this)" >わ</div>
<div class ="key" style="background:#4a4e69; grid-column: span 2;" onclick="modifyChar()" >゛゜小文字</div>
</div>
<div id="right-panel" class ="panel-grid" >
<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 active" onclick="switchMode('hira', this)" ><span>あ</span>平仮名</div>
<div class ="tab" onclick="switchMode('kata', this)" ><span>ア</span>カタカナ</div>
<div class ="tab" onclick="switchMode('alpha', this)" ><span>Aa</span>英字</div>
<div class ="tab" onclick="switchMode('num', this)" ><span>12 </span>数字</div>
<div class ="tab" onclick="switchMode('sym', this)" ><span>#%</span>記号</div>
<div class ="tab" 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 = {
'alpha' : ["ENTER" , "SPACE" ].concat("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.,!?@_" .split("" )),
'num' : ["ENTER" , "SPACE" ].concat("1234567890+-*/=.,:;()¥%" .split("" )),
'sym' : ["ENTER" , "SPACE" ].concat("!?”’()[]{}<>〜=+−/*@#$%&¥|_…↑↓←→★☆♪◯●◎" .split("" )),
'emoji' : ["ENTER" , "SPACE" ].concat("😀😂😊😍😢😡👍👎🙏❤️🔥✨🎉🍺🍜🚗🏠💡🐶🐱" .split("" ))
};
let currentMode = 'hira' ;
let currentRow = 'a' ;
let committedStr = "" ;
let composingStr = "" ;
let lastTapTarget = null ;
let tapTimeout = null ;
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 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];
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' );
}
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>
copy
コメント