<!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:
--text-color:
/* --- 行カラー --- */
--c-a:
--c-k:
--c-s:
--c-t:
--c-n:
--c-h:
--c-m:
--c-y:
--c-r:
--c-w:
/* --- タブカラー --- */
--tab-hira:
--tab-kata:
--tab-lower:
--tab-upper:
--tab-num:
--tab-sym:
--tab-emoji:
}
/* --- スクロールバーのカスタマイズ (幅を2 倍に) --- */
::-webkit-scrollbar {
width: 18px; /* 通常(8px前後)の約2 倍 */
background:
}
::-webkit-scrollbar-thumb {
background:
border-radius: 9px;
border: 3px solid
}
::-webkit-scrollbar-thumb:hover {
background:
}
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;
}
/* --- 上部エリア --- */
padding-top: max(env(safe-area-inset-top), 30px);
padding-bottom: 5px;
padding-left: 10px;
padding-right: 10px;
height: 22 %;
min-height: 140px;
background:
border-bottom: 2px solid
display: flex;
flex-direction: column;
justify-content: flex-end;
flex-shrink: 0 ;
z-index: 10 ;
box-sizing: border-box;
}
display: flex;
align-items: stretch;
flex: 1 ;
overflow: hidden;
margin-bottom: 8px;
}
font-size: 18px;
line-height: 1.5 ;
white-space: pre-wrap;
overflow-y: scroll; /* 常にスクロールバーを意識させる */
flex: 1 ;
margin-right: 10px;
border: 1px solid
background:
padding: 8px;
border-radius: 8px;
word-break : break -all;
}
.composing { background:
.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:
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 0 rgba(0 ,0 ,0 ,0.3 );
}
.tool-btn:active { transform: translateY(2px); box-shadow: none; opacity: 0.8 ; }
/* 候補バー */
height: 32px;
display: flex;
overflow-x: auto;
align-items: center;
border-top: 1px solid
white-space: nowrap;
background:
padding-left: 5px;
border-radius: 4px;
flex-shrink: 0 ;
}
.cand-chip {
background:
border-radius: 12px; font-size: 14px; border: 1px solid
}
.cand-chip:active { background:
/* --- メインエリア --- */
flex: 1 ;
position: relative;
overflow: hidden;
background:
padding: 4px;
}
/* モードA: 日本語ボード */
display: grid;
grid-template-columns: 1. 3fr 1fr;
gap: 4px;
height: 100 %;
width: 100 %;
box-sizing: border-box;
}
.key {
display: flex; align-items: center; justify-content: center;
font-weight: bold; border-radius: 8px; font-size: 18px;
cursor: pointer; color:
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:
.row-t { background: var(--c-t); color:
.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:
.row-r { background: var(--c-r); }
.row-w { background: var(--c-w); }
.key-func { background:
.key-left.active { border: 3px solid
.key-right { font-size: 28px; background:
/* モードB: スクロールグリッド */
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:
display: flex; align-items: center; justify-content: center;
font-size: 22px; color:
text-shadow: 1px 1px 2px rgba(0 ,0 ,0 ,0.5 );
}
.scroll-key.waiting { border: 3px solid
.scroll-key.tapped { filter: brightness(0.5 ); transform: scale(0.95 ); }
.key-special { font-size: 14px; color:
.key-enter { background:
/* --- 下部タブ --- */
height: 60px;
background:
border-top: 1px solid
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:
border-right: 1px solid
flex-direction: column;
cursor: pointer;
background:
transition: all 0. 2s;
}
.tab span { font-size: 14px; margin-bottom: 3px; }
.t-hira.active { background: var(--tab-hira); color:
.t-kata.active { background: var(--tab-kata); color:
.t-lower.active { background: var(--tab-lower); color:
.t-upper.active { background: var(--tab-upper); color:
.t-num.active { background: var(--tab-num); color:
.t-sym.active { background: var(--tab-sym); color:
.t-emoji.active { background: var(--tab-emoji); color:
</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>
<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>
copy
コメント