<!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>ツーサム v11 (見切れ修正&カラフル版)</title>
<style>
:root {
--bg-color: #121212;
--text-color: #fff;
--c-a: #ef476f; /* あ: ピンク赤 */
--c-k: #f78c6b; /* か: オレンジ */
--c-s: #ffd166; /* さ: 黄色 */
--c-t: #06d6a0; /* た: エメラルド */
--c-n: #118ab2; /* な: 青 */
--c-h: #073b4c; /* は: 紺 */
--c-m: #9d4edd; /* ま: 紫 */
--c-y: #ff006e; /* や: マゼンタ */
--c-r: #8338ec; /* ら: すみれ */
--c-w: #3a86ff; /* わ: スカイブルー */
--tab-hira: #ef476f;
--tab-kata: #f78c6b;
--tab-alpha: #118ab2;
--tab-num: #06d6a0;
--tab-sym: #9d4edd;
--tab-emoji: #ffd166;
}
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;
height: 100 dvh;
width: 100 vw;
display: flex;
flex-direction: column;
overflow: hidden;
user-select : none;
touch-action: none;
}
#top-area {
padding-top: max(env(safe-area-inset-top), 20 px);
padding-bottom: 5 px;
padding-left: 8 px;
padding-right: 8 px;
height: 18 %;
min-height: 100 px;
background: #000;
border-bottom: 2 px 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: 6 px;
}
#input-line {
font-size: 20 px;
line-height: 1.4 ;
white-space: pre-wrap;
overflow-y: auto;
flex: 1 ;
margin-right: 8 px;
border: 1 px solid #555;
background: #1a1a1a;
padding: 8 px;
border-radius: 8 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;
width: 60 px;
}
.tool-btn {
border: none;
border-radius: 6 px;
flex: 1 ;
font-weight: bold;
font-size: 13 px;
cursor: pointer;
color: #fff;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2 px 0 rgba(0 ,0 ,0 ,0.3 );
}
#btn-copy { background: #2a9d8f; }
#btn-del { background: #d62828; }
.tool-btn:active { transform: translateY(2 px); box-shadow: none; opacity: 0.8 ; }
#candidate-bar {
height: 36 px;
display: flex;
overflow-x: auto;
align-items: center;
border-top: 1 px solid #333;
white-space: nowrap;
background: #222;
padding-left: 5 px;
border-radius: 4 px;
flex-shrink: 0 ;
}
.cand-chip {
background: #444; padding: 4px 12px; margin-right: 8px;
border-radius: 12 px; font-size: 14 px; border: 1 px solid #666;
}
.cand-chip:active { background: #fff; color: #000; }
#main-stage {
flex: 1 ;
position: relative;
overflow: hidden;
background: #111;
padding: 4 px;
}
#twothumb-board {
display: grid;
grid-template-columns: 1.3f r 1f r;
gap: 4 px;
height: 100 %;
width: 100 %;
box-sizing: border-box;
}
#left-panel {
display: grid;
grid-template-columns: 1f r 1f r;
grid-template-rows: repeat(6 , 1f r);
gap: 4 px;
}
#right-panel {
display: grid;
grid-template-columns: 1f r;
grid-template-rows: repeat(5 , 1f r);
gap: 4 px;
}
.key {
display: flex; align-items: center; justify-content: center;
font-weight: bold;
border-radius: 8 px;
font-size: 18 px;
cursor: pointer;
color: #fff;
text-shadow: 1 px 1 px 2 px rgba (0 ,0 ,0 ,0.5 ) ;
box-shadow: 0 3 px 0 rgba(0 ,0 ,0 ,0.3 );
transition: transform 0.05 s, background-color 0.2 s;
}
.key:active { transform: translateY(2 px); box-shadow: none; }
.row-a { background: var (--c-a); }
.row-k { background: var (--c-k); }
.row-s { background: var (--c-s); color:#222; text-shadow:none; } /* 黄色は文字黒 */
.row-t { background: var (--c-t); }
.row-n { background: var (--c-n); }
.row-h { background: var (--c-h); }
.row-m { background: var (--c-m); }
.row-y { background: var (--c-y); }
.row-r { background: var (--c-r); }
.row-w { background: var (--c-w); }
.key-func { background: #444; font-size: 14px; }
.key-left.active {
border: 3 px solid #fff;
transform: scale(0.96 );
filter: brightness(1.1 );
z-index: 2 ;
}
.key-right { font-size: 28 px; background: #333; }
#scroll-grid {
display: none;
grid-template-columns: repeat(auto-fill, minmax(70 px, 1f r));
grid-auto-rows: 60 px;
gap: 6 px;
padding: 5 px;
overflow-y: auto;
height: 100 %;
align-content: start;
box-sizing: border-box;
padding-bottom: 60 px;
}
.scroll-key {
background: #333; border: 1px solid #555; border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 22 px; color: #fff;
cursor: pointer;
}
.scroll-key.waiting { background: #ffd166; color: #000; border: 2px solid #fff; }
.scroll-key.tapped { background: #06d6a0; transform: scale(0.95); }
.key-special { font-size: 14 px; color: #aaa; border: 1px dashed #666; flex-direction: column; }
.key-enter { background: #264653; }
#tab-bar {
height: 60 px;
background: #000;
border-top: 1 px solid #333;
display: flex;
flex-shrink: 0 ;
z-index: 10 ;
padding-bottom: max(env(safe-area-inset-bottom), 0 px);
}
.tab {
flex: 1 ;
display: flex; align-items: center; justify-content: center;
font-size: 11 px; font-weight: bold; color: #888;
border-right: 1 px solid #111;
flex-direction: column;
cursor: pointer;
border-bottom: 4 px solid transparent;
transition: all 0.2 s;
background: #1a1a1a;
}
.tab span { font-size: 16 px; margin-bottom: 3 px; }
.t-hira.active { background: #2b0a10; color: var(--tab-hira); border-bottom-color: var(--tab-hira); }
.t-kata.active { background: #2b1600; color: var(--tab-kata); border-bottom-color: var(--tab-kata); }
.t-alpha.active { background: #00121a; color: var(--tab-alpha); border-bottom-color: var(--tab-alpha); }
.t-num.active { background: #001a14; color: var(--tab-num); border-bottom-color: var(--tab-num); }
.t-sym.active { background: #1a002b; color: var(--tab-sym); border-bottom-color: var(--tab-sym); }
.t-emoji.active { background: #2b2200; color: var(--tab-emoji); border-bottom-color: var(--tab-emoji); }
</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-alpha" onclick="switchMode('alpha', this)" ><span>Aa</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 = {
'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 computedStyle = window.getComputedStyle(element);
const bgColor = computedStyle.backgroundColor;
const color = computedStyle.color;
rightKeys.forEach(k => {
k.style.borderColor = bgColor;
k.style.color = "#fff" ;
k.style.backgroundColor = bgColor;
if (rowKey === 's' ) {
k.style.color = "#000" ;
} else {
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];
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
コメント