Алгоритм генерации смежных границ таблиц в эмуляторах терминала
Вопрос ко всем полиглотам! Не только знающих JavaScript. Пойдет любой язык программирования, просто JavaScript самый удобный при использовании на SO.
Пытаюсь написать небольшой тренажер для Vim. С помощью таблицы символов UTF-8 я отрисовываю в эмуляторе терминала xterm вот такую таблицу:
╔═════════════════════════════════════════════════════════════════════════╗
║ ┌─────┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬────┬────┬─────┐ ║
║ │ Esc │F1 │F2 │F3 │F4 │F5 │F6 │F7 │F8 │F9 │F10│F11│F12│Ins │PrSc│ Del │ ║
║ ├───┬─┴──┬┴───┼───┴┬──┴─┬─┴──┬┴───┼───┴┬──┴─┬─┴──┬┴───┼────┼────┼─────┤ ║
║ │`~ │ 1! │ 2@ │ 3# │ 4$ │ 5% │ 6^ │ 7& │ 8* │ 9( │ 0) │ -_ │ =+ │ BkSp│ ║
║ ├───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬────┤ ║
║ │Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │ [ │ ] │ \| │ ║
║ ├────┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴────┤ ║
║ │CpsLck│ A │ S │ D │ F │ G │ H │ J │ K │ L │ ;: │ '" │ Enter │ ║
║ ├──────┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴───────┤ ║
║ │Shift │ Z │ X │ C │ V │ B │ N │ M │ ,< │ .> │ /? │ Shift │ ║
║ ├─────┬──┴─┬──┴─┬──┴─┬──┴────┴────┴────┴─┬──┴─┬──┴─┬┬─┴───┬┴────┬─────┤ ║
║ │Ctrl │ Fn │Win │Alt │ │Alt │Ctrl││PgUp │ Up │PgDn │ ║
║ └─────┴────┴────┴────┴─────────────────┬─┴──┬─┴──┬─┘├─────┼─────┼─────┤ ║
║ │Home│End │ │Left │Down │Right│ ║
║ └────┴────┘ └─────┴─────┴─────┘ ║
╚═════════════════════════════════════════════════════════════════════════╝
Вскоре я понял, что по ходу работы программы мне необходимо выделять некоторые ячейки таблицы по их смежным границам, например как тут:
╔═════════════════════════════════════════════════════════════════════════╗
║ ┌─────┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬────┬────┬─────┐ ║
║ │ Esc │F1 │F2 │F3 │F4 │F5 │F6 │F7 │F8 │F9 │F10│F11│F12│Ins │PrSc│ Del │ ║
║ ├───┬─┴──┬┴───┼───┴┬──┴─┬─┴──┬┴───┼───┴┬──┴─┬─┴──┬┴───┼────┼────┼─────┤ ║
║ │`~ │ 1! │ 2@ │ 3# │ 4$ │ 5% │ 6^ │ 7& │ 8* │ 9( │ 0) │ -_ │ =+ │ BkSp│ ║
║ ├───┴┲━━━┷┳━━━┷┱───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬────┤ ║
║ │Tab ┃ Q ┃ W ┃ E │ R │ T │ Y │ U │ I │ O │ P │ [ │ ] │ \| │ ║
║ ├────┺━┯━━┻━┯━━┹─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┲━━┷━┱──┴─┬──┴────┤ ║
║ │CpsLck│ A │ S │ D │ F │ G │ H │ J │ K │ L ┃ ;: ┃ '" │ Enter │ ║
║ ┢━━━━━━┷━┱──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┺━┯━━┹─┬──┴───────┤ ║
║ ┃Shift ┃ Z │ X │ C │ V │ B │ N │ M │ ,< │ .> │ /? │ Shift │ ║
║ ┡━━━━━┯━━┹─┬──┴─┬──┴─┬──┴────┴────┴────┴─┬──┴─┬──┴─┬┬─┴───┬┴────┬─────┤ ║
║ │Ctrl │ Fn │Win │Alt │ │Alt │Ctrl││PgUp │ Up │PgDn │ ║
║ └─────┴────┴────┴────┴─────────────────┬─┴──┬─┴──┬─┘├─────┼─────┼─────┤ ║
║ │Home│End │ │Left │Down │Right│ ║
║ └────┴────┘ └─────┴─────┴─────┘ ║
╚═════════════════════════════════════════════════════════════════════════╝
Здесь выделены ячейки "Q", "W", ";", "Shift". Первая мысль, которая пришла на ум - захардкодить всё к чертовой бабушке. Но, очевидно, что это будет очень непросто, так как ячейки имеют разные размеры и формы, а самое главное - разные положения соседских ячеек. Да и потом, мне хотелось бы предоставить возможность пользователю с помощью конфигов составлять схемы своих раскладок. Но как это сделать - в голове ни единой мысли.
Работая с такой псевдо-графикой, таблица символов UTF-8 должна быть всегда под рукой. Но, при работе с символами рисования таблиц, всё равно возникают сложности поиска того или иного символа в таблице. И с опытом постоянных наблюдений таблицы символов ситуация меняется только в худшую сторону, так как поиск нужных символов превращается нудную рутину. Мои пытки понять логику распределения порядка символов рисования привели меня к логическому утверждению - символы рисования внесены в таблицу UFT-8 в случайно-хаотичном порядке.
Через какое-то время я придумал как мне систематизировать порядок
символов, чтобы быстро находить нужный мне символ даже не заглядывая в
саму таблицу. Я даже написал функцию draw(), которая принимает восьми-битное
целое число и возвращает нужный мне символ. Каждый символ рисования состоит
из 1-4 отростков направленных из центра в 4 стороны. Если пронумеровать биты
числа так, чтобы впереди были старшие биты, а в конце младшие (76543210),
то первая пара бит (76) соответствует отростку в северном направлении,
вторая пара бит (54) соответствует отростку в восточном направлении,
третья пара бит (32) соответствует отростку в южном направлении,
и последняя пара бит (01) соответствует отростку в западном направлении.
Очень легко запомнить, прям как в правилах border в CSS по часовой стрелке:
вверх, вправо, вниз, влево. Каждая пара бит может хранить в себе значение
от 0 до 3 (Карл!), где 0 соответствует тому, что отростка в эту сторону
нет, 1 соответствует отростку с тонкой линией, 2 соответствует отростку
с жирной линией и 3 соответствует отростку с двойной линией.
Код функции на языке JavaScript можно наблюдать ниже вместе с небольшой демкой её работы.
/**
* @var Unicode Box Draw Map
* @usedBy draw()
*/
const boxDraw = new Map([
[ 1, 116],[ 2, 120],[ 4, 119],[ 5, 16],[ 6, 17],[ 7, 85],
[ 8, 123],[ 9, 18],[ 10, 19],[ 13, 86],[ 15, 87],[ 16, 118],
[ 17, 0],[ 18, 126],[ 20, 12],[ 21, 44],[ 22, 45],[ 24, 14],
[ 25, 48],[ 26, 49],[ 28, 83],[ 29, 101],[ 32, 122],[ 33, 124],
[ 34, 1],[ 36, 13],[ 37, 46],[ 38, 47],[ 40, 15],[ 41, 50],
[ 42, 51],[ 51, 80],[ 52, 82],[ 55, 100],[ 60, 84],[ 63, 102],
[ 64, 117],[ 65, 24],[ 66, 25],[ 67, 91],[ 68, 2],[ 69, 36],
[ 70, 37],[ 71, 97],[ 72, 125],[ 73, 39],[ 74, 42],[ 80, 20],
[ 81, 52],[ 82, 53],[ 84, 28],[ 85, 60],[ 86, 61],[ 88, 31],
[ 89, 65],[ 90, 69],[ 96, 21],[ 97, 54],[ 98, 55],[100, 29],
[101, 62],[102, 63],[104, 34],[105, 70],[106, 72],[112, 88],
[115, 103],[116, 94],[119, 106],[128, 121],[129, 26],[130, 27],
[132, 127],[133, 38],[134, 41],[136, 3],[137, 40],[138, 43],
[144, 22],[145, 56],[146, 57],[148, 30],[149, 64],[150, 67],
[152, 32],[153, 66],[154, 73],[160, 23],[161, 58],[162, 59],
[164, 33],[165, 68],[166, 71],[168, 35],[169, 74],[170, 75],
[193, 92],[195, 93],[204, 81],[205, 98],[207, 99],[208, 89],
[209, 104],[220, 95],[221, 107],[240, 90],[243, 105],[252, 96],
[255, 108],
]);
/**
* @function draw
* @param {Number} code Must be integer in range [0-255]
* @returns {String}
*/
function draw(code) {
return !code ? ' ' : boxDraw.has(code) ? String.fromCharCode(boxDraw.get(code) + 0x2500) : null;
}
const getValue = () =>
[...document.querySelectorAll('input[type="radio"]:checked')]
.map(input => +input.value)
.reduce((a, v, i) => ((a |= v << (6 - 2 * i)), a), 0);
const updateOutput = () => {
const value = getValue();
document.getElementById('output-bin').value = `draw(0b${value.toString(2).padStart(8, 0)})`;
document.getElementById('output-dec').value = `draw(${value})`;
document.getElementById('output-char').value = draw(value) ? `${draw(value)}` : '☒';
document
.querySelectorAll('.right .val .pair')
.forEach(
(pair, i) =>
(pair.innerHTML = [...((value >> (6 - 2 * i)) & 3).toString(2).padStart(2, 0)].map(c => `<b>${c}</b>`).join``)
);
};
document.forms[0].oninput = updateOutput;
const onfocus = e => e.target.select();
document.getElementById('output-bin').onfocus = onfocus;
document.getElementById('output-dec').onfocus = onfocus;
document.getElementById('output-char').onfocus = onfocus;
* { box-sizing: border-box;}
html,
body {
height: 100vh;
width: 100vw;
margin: 0;
display: flex;
font-family: Monaco;
font-size: 10vh;
flex-flow: row nowrap;
background: #999;
text-align: center;
}
.half { flex: 1;}
.legend { display: flex; flex-flow: row nowrap; justify-content: space-evenly;}
input[type='radio'] { display: none;}
label {
border: 0.1vw solid #000;
color: #444;
font-size: 12vh;
line-height: 15vh;
padding: 0 0.6vw 0 0.6vw;
border-radius: 0.3vw;
background: #ccc;
}
input[type='radio']:checked + label {
border: 0.1vw solid #eba309;
box-shadow: inset 0 0 1vw #eba309, 0 0 1vw #eba309;
text-shadow: 0 0 0.3vw #eba309;
color: #624404;
}
.char {
border: 1px solid #fff;
border-radius: 5px;
display: inline-block;
color: #000;
padding: 5px 5px 0 5px;
text-shadow: 0 0 0.2rem #fff;
box-shadow: 0 0 0.1rem #000;
}
.pair {
background: #888;
border-radius: 0.6vw;
font-size: 0.8em;
white-space: nowrap;
/* line-height: 0.7em; */
padding: 0.2vw 0.2vw 0.2vw 0.2vw;
}
.pair b {
padding: 0 0.3vw 0 0.3vw;
background: #aaa;
border-radius: 0.3vw;
margin: 0.2vw;
font-size: 0.9em;
display: inline-block;
/* line-height: 0.7em; */
}
.legend,.math,.radio-group { height: 18vh; font-size: 12vh; line-height: 15vh;}
.left { flex: 1;}
form { display: flex; flex-flow: row wrap;}form div { width: 50%;}
.no-symbol {
margin: 3vh 0 0 0;
font-size: 0.4em;
display: block;
width: 6vw;
height: 8vh;
line-height: 0.8em;
}
.output-group {
height: 25vh;
width: 100%;
display: flex;
flex-flow: row nowrap;
justify-content: space-evenly;
align-items: center;
padding-top: 3vh;
}
#output-bin { width: 44vw;}
#output-dec { width: 24vw;}
#output-bin,
#output-dec,
#output-char {
font-size: 12vh;
font-family: Monaco;
text-align: center;
background: #ccc;
border-radius: 1vh;
}
.right {
display: grid;
grid-template: '1 n 3' 'w c e' ' 7 s 9';
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
max-width: 100vh;
max-height: 100vw;
}
.north { grid-area: n;}
.east { grid-area: e;}
.south { grid-area: s;}
.west { grid-area: w;}
.center { grid-area: c;}
.north,.south,.east,.west { display: flex;}
.north,.south { flex-flow: column nowrap;}
.east,.west { flex-flow: row nowrap;}
.north .var,.west .var,.south .val,.east .val { order: 1;}
.north .val,.west .val,.south .var,.east .var { order: 2;}
.right .var,
.right .val {
display: flex;
text-align: center;
justify-content: center;
align-items: center;
flex: 1;
}
#output-char {
width: 90%;
height: 100%;
font-size: 20vh;
text-align: center;
}
<div class="half left">
<div class="legend">
<span class="char"> </span>
<span class="pair"><b>0</b><b>0</b></span>
<span class="char">┼</span>
<span class="pair"><b>0</b><b>1</b></span>
<span class="char">╋</span>
<span class="pair"><b>1</b><b>0</b></span>
<span class="char">╬</span>
<span class="pair"><b>1</b><b>1</b></span>
<span class="char">☒</span>
<span class="no-symbol">No Symbol</span>
</div>
<div class="math">
<span class="var">a</span>
<span class="action"><<</span>
<span class="const">6</span>
<span class="action">|</span>
<span class="var">b</span>
<span class="action"><<</span>
<span class="const">4</span>
<span class="action">|</span>
<span class="var">c</span>
<span class="action"><<</span>
<span class="const">2</span>
<span class="action">|</span>
<span class="var">d</span>
</div>
<form>
<div class="radio-group">
<span class="static">a =</span>
<input type="radio" name="a" value="0" id="a-0" /><label for="a-0">0</label>
<input type="radio" name="a" value="1" id="a-1" checked /><label for="a-1">1</label>
<input type="radio" name="a" value="2" id="a-2" /><label for="a-2">2</label>
<input type="radio" name="a" value="3" id="a-3" /><label for="a-3">3</label>
</div>
<div class="radio-group">
<span class="static">b =</span>
<input type="radio" name="b" value="0" id="b-0" /><label for="b-0">0</label>
<input type="radio" name="b" value="1" id="b-1" checked /><label for="b-1">1</label>
<input type="radio" name="b" value="2" id="b-2" /><label for="b-2">2</label>
<input type="radio" name="b" value="3" id="b-3" /><label for="b-3">3</label>
</div>
<div class="radio-group">
<span class="static">c =</span>
<input type="radio" name="c" value="0" id="c-0" /><label for="c-0">0</label>
<input type="radio" name="c" value="1" id="c-1" checked /><label for="c-1">1</label>
<input type="radio" name="c" value="2" id="c-2" /><label for="c-2">2</label>
<input type="radio" name="c" value="3" id="c-3" /><label for="c-3">3</label>
</div>
<div class="radio-group">
<span class="static">d =</span>
<input type="radio" name="d" value="0" id="d-0" /><label for="d-0">0</label>
<input type="radio" name="d" value="1" id="d-1" checked /><label for="d-1">1</label>
<input type="radio" name="d" value="2" id="d-2" /><label for="d-2">2</label>
<input type="radio" name="d" value="3" id="d-3" /><label for="d-3">3</label>
</div>
<div class="output-group">
<input id="output-bin" type="text" value="draw(0b01010101)" readonly />
<input id="output-dec" type="text" value="draw(85)" readonly />
</div>
</form>
</div>
<div class="half right">
<div class="north">
<div class="var">a</div>
<div class="val">
<span class="pair"><b>0</b><b>1</b></span>
</div>
</div>
<div class="east">
<div class="var">b</div>
<div class="val">
<span class="pair"><b>0</b><b>1</b></span>
</div>
</div>
<div class="south">
<div class="var">c</div>
<div class="val">
<span class="pair"><b>0</b><b>1</b></span>
</div>
</div>
<div class="west">
<div class="var">d</div>
<div class="val">
<span class="pair"><b>0</b><b>1</b></span>
</div>
</div>
<div class="center">
<input type="text" id="output-char" value="┼" readonly />
</div>
</div>
К сожалению, других идей кроме написания этой функции мне найти не удалось. Да и в целом прогресса никакого нет. Всю теоретическую часть вопроса, такую как управляющие последовательности Xterm я вроде освоил. Но алгоритм генерации смежных границ ячеек в таблицах псевдо-графики терминальных эмуляторов я придумать совсем не могу. Если здесь есть люди с опытом работы с графикой в эмуляторах терминала, я буду рад любым идеям.
Чтобы вопрос был с четкой задачей, которую можно выполнить или не выполнить надо бы обозначить условия. Язык предпочтительнее JavaScript. В простом теге <pre> есть псевдо-таблица клавиатуры. У окна есть слушатели keydown и keyup. Необходимо дописать код слушателей таким образом, чтобы при нажатии на клавишу, она выделялась жирной границей в таблице клавиатуры, и, когда клавиша отпускалась, граница клавиши переходила в исходный вид. Для примера сделаю Win, Shift, Ctrl и Alt.
window.onkeydown = updateKbd;
window.onkeyup = updateKbd;
function updateKbd(event){
const { ctrlKey, shiftKey, altKey, metaKey } = event;
const content = [[/&/g,'&'],[/</g,'<'],[/>/g,'>']].reduce((str,[rx,rp])=>str.replace(rx,rp), pre.textContent);
const modifiers = [
[ 698, shiftKey ? '┢━━━━━━┷━┱' : '├──────┴─┬',],
[ 757, shiftKey ? '┲━━┷━━━━━━━┪': '┬──┴───────┤',],
[ 775, shiftKey ? `┃Shift ┃` : '│Shift │',],
[ 840, shiftKey ? `┃ Shift ┃` : '│ Shift │',],
[ 858, shiftKey && !ctrlKey ? '┡' : !shiftKey && ctrlKey ? '┢' : shiftKey && ctrlKey ? '┣' : '├',],
[ 859, shiftKey || ctrlKey ? '━━━━━' : '─────',],
[ 864, shiftKey && !ctrlKey ? '┯' : !shiftKey && ctrlKey? '┱' : shiftKey && ctrlKey ? '┳' : '┬',],
[ 865, shiftKey ? '━━┹' : '──┴',],
[ 869, metaKey ? '┲━━┷━' : '┬──┴─',],
[ 874, metaKey && !altKey ? '┱' : !metaKey && altKey ? '┲' : metaKey && altKey ? '┳' : '┬',],
[ 875, altKey ? '━━┷━┱' : '──┴─┬',],
[ 899, altKey ? '┲━━┷━' : '┬──┴─',],
[ 904, altKey && !ctrlKey ? '┱' : !altKey && ctrlKey ? '┲' : altKey && ctrlKey ? '┳' : '┬',],
[ 905, ctrlKey ? '━━┷━┱' : '──┴─┬',],
[ 917, shiftKey ? '┺━━━━┯━━━━━┩' : '┴────┬─────┤' ,],
[ 935, ctrlKey ? `┃Ctrl ┃` : '│Ctrl │',],
[ 946, metaKey ? `┃Win ` : '│Win ',],
[ 951, metaKey || altKey ? '┃' : '│',],
[ 952, altKey ? `Alt ┃` : 'Alt │',],
[ 976, altKey ? `┃Alt ` : '│Alt ',],
[ 981, altKey || ctrlKey ? '┃' : '│',],
[ 982, ctrlKey ? `Ctrl┃` : 'Ctrl│',],
[1012, ctrlKey ? '┗━━━━━┹' : '└─────┴',],
[1023, metaKey ? '┺━━━━' : '┴────',],
[1028, metaKey && !altKey ? '┹' : !metaKey && altKey ? '┺' : metaKey && altKey ? '┻' : '┴',],
[1029, altKey ? '━━━━┹' : '────┴',],
[1053, altKey ? '┺━━┯━' : '┴──┬─',],
[1058, altKey && ! ctrlKey ? '┹' : !altKey && ctrlKey ? '┺' : altKey && ctrlKey ? '┻' : '┴',], // TODO
[1059, ctrlKey ? '━━┯━┛' : '──┬─┘',],
];
let pos = 0, out = '';
for(let [mpos,str] of modifiers){
out += content.substring(pos,mpos)+str;
pos = mpos + str.length;
}
out+=content.substring(pos);
pre.innerHTML = out;
}
<pre id="pre">
╔═════════════════════════════════════════════════════════════════════════╗
║ ┌─────┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬────┬────┬─────┐ ║
║ │ Esc │F1 │F2 │F3 │F4 │F5 │F6 │F7 │F8 │F9 │F10│F11│F12│Ins │PrSc│ Del │ ║
║ ├───┬─┴──┬┴───┼───┴┬──┴─┬─┴──┬┴───┼───┴┬──┴─┬─┴──┬┴───┼────┼────┼─────┤ ║
║ │`~ │ 1! │ 2@ │ 3# │ 4$ │ 5% │ 6^ │ 7& │ 8* │ 9( │ 0) │ -_ │ =+ │ BkSp│ ║
║ ├───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬───┴┬────┤ ║
║ │Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │ [ │ ] │ \| │ ║
║ ├────┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴────┤ ║
║ │CpsLck│ A │ S │ D │ F │ G │ H │ J │ K │ L │ ;: │ '" │ Enter │ ║
║ ├──────┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴───────┤ ║
║ │Shift │ Z │ X │ C │ V │ B │ N │ M │ ,< │ .> │ /? │ Shift │ ║
║ ├─────┬──┴─┬──┴─┬──┴─┬──┴────┴────┴────┴─┬──┴─┬──┴─┬┬─┴───┬┴────┬─────┤ ║
║ │Ctrl │ Fn │Win │Alt │ │Alt │Ctrl││PgUp │ Up │PgDn │ ║
║ └─────┴────┴────┴────┴─────────────────┬─┴──┬─┴──┬─┘├─────┼─────┼─────┤ ║
║ │Home│End │ │Left │Down │Right│ ║
║ └────┴────┘ └─────┴─────┴─────┘ ║
╚═════════════════════════════════════════════════════════════════════════╝
</pre>
Ответы (2 шт):
makeKeyboard создаёт клавитуру. Клавиатура позволяет создавать кнопки с произвольными надписями и границей. Граница может быть любым многоугольником со сторонами параллельными осям. Координаты соответствуют прямоугольной решётке из символов. Начало координат в левом верхем углу.
Каждая кнопка может быть нажата (press) и отпущена (release).
Клавиатура хранит состояние для отображения но сама рисовать себя не умеет. За рисование отвечает makeScreen, который отображает клавиатуру в элементе pre.
Основная функциональность закончилась. makeKeyboardBuilder делает создание клавиатуры более удобным, но не использует все возможности клавиатуры, например неквадратные кнопки.
Конфигурация клавиатуры и привязка кнопок к событиям сделана кодом в main. Это самая грязная часть. Нормальное решение было бы создавать кнопки по одной и немедленно привязывать их к событиям. К сожалению, "правильный" подход раздует пример и отвлечёт внимание от клавиатуры и экрана.
const updateElement = (element, attributes) => {
for (const key in attributes) {
element.setAttribute(key, attributes[key]);
}
};
const createElement = (parent_, tag, attributes) => {
const element = document.createElement(tag);
updateElement(element, attributes);
parent_.appendChild(element);
return element;
};
const createTextNode = (parent_, text) => {
const node = document.createTextNode(text);
parent_.appendChild(node);
return node;
};
const makeKeyboard = () => {
const makeCell = () => {
let char_ = ' ';
const pseudographics =
' ╷╻╴┐┒╸┑┓' +
'╵│╽┘┤┧┙┥┪' +
'╹╿┃┚┦┨┛┩┫' +
'╶┌┎─┬┰╾┭┱' +
'└├┟┴┼╁┵┽╅' +
'┖┞┠┸╀╂┹╃╉' +
'╺┍┏╼┮┲━┯┳' +
'┕┝┢┝┾╆┷┿╈' +
'┗┡┣┺╄╇┻╇╋'
;
const weights = [0, 0, 0, 0];
const style = () =>
weights.reduce((a, b) => 3 * a + Math.min(b, 2), 0);
const dirs = {'r': 0, 'u': 1, 'l': 2, 'd': 3};
return {
'setChar': c => char_ = c,
'getChar': () => char_,
'setLine': dir => {
weights[dirs[dir]] = 1;
char_ = pseudographics[style()];
},
'changeWeight': (dir, w) => {
weights[dirs[dir]] += w;
char_ = pseudographics[style()];
}
};
};
const cells = [];
let screen = {
'clear': () => {},
'putc': () => {}
};
const getCell = (i, j) => {
while (cells.length <= i) {
cells.push([]);
}
const row = cells[i];
while (row.length <= j) {
row.push(makeCell());
}
return row[j];
};
const makeButton = (labels, border) => {
for (const [i, j, text] of labels) {
for (let k = 0; k < text.length; ++k) {
getCell(i, j + k).setChar(text.charAt(k));
}
};
const processBorder = cb => {
let [pi, pj] = border[border.length - 1];
for (let k = 0; k < border.length; ++k) {
const [ni, nj] = border[k];
if (pi == ni) {
const j1 = Math.min(pj, nj);
const j2 = Math.max(pj, nj);
for (let j = j1; j < j2; ++j) {
cb(pi, j, 'r');
}
for (let j = j1 + 1; j < j2 + 1; ++j) {
cb(pi, j, 'l');
}
} else if (pj == nj) {
const i1 = Math.min(pi, ni);
const i2 = Math.max(pi, ni);
for (let i = i1; i < i2; ++i) {
cb(i, pj, 'd');
}
for (let i = i1 + 1; i < i2 + 1; ++i) {
cb(i, pj, 'u');
}
}
[pi, pj] = [ni, nj];
}
};
processBorder((i, j, dir) => {
const cell = getCell(i, j);
cell.setLine(dir);
screen.putc(i, j, cell.getChar());
});
const changeWeight = w => {
processBorder((i, j, dir) => {
const cell = getCell(i, j);
cell.changeWeight(dir, w);
screen.putc(i, j, cell.getChar());
});
};
let down = false;
const press = () => {
if (!down) {
changeWeight(1);
down = true;
}
};
const release = () => {
if (down) {
changeWeight(-1);
down = false;
}
};
return {
'press': press,
'release': release
};
};
const paintKeyboard = () => {
screen.clear();
for (let i = 0; i < cells.length; ++i) {
const row = cells[i];
for (let j = 0; j < row.length; ++j) {
const cell = row[j];
if (cell !== undefined) {
screen.putc(i, j, cell.getChar());
}
}
}
};
return {
'makeButton': makeButton,
'h': () => cells.length,
'w': () => cells.reduce((a, b) => Math.max(a, b.length), 0),
'attachScreen': screen_ => {
screen = screen_;
paintKeyboard();
}
};
};
const makeScreen = (parent_, h, w) => {
const pre = createElement(parent_, 'pre');
const spans = new Array(h);
for (let i = 0; i < h; ++i) {
spans[i] = new Array(w);
for (let j = 0; j < w; ++j) {
const span = createElement(pre, 'span');
createTextNode(span, ' ');
spans[i][j] = span;
}
createTextNode(pre, '\n');
}
return {
'clear': () => {
for (const row of spans) {
for (const span of row) {
span.textContent = ' ';
}
}
},
'putc': (i, j, c) => {
if (0 <= i && i < spans.length && 0 <= j && j < spans[i].length) {
spans[i][j].textContent = c;
}
}
};
};
const makeKeyboardBuilder = () => {
const keyboard = makeKeyboard();
const buttons = {};
let i = 0;
let j = 0;
const add = label => {
const tag = label.trim();
const jj = j + label.length + 1;
const button = keyboard.makeButton(
[[i + 1, j + 1, label]],
[[i, j], [i, jj], [i + 2, jj], [i + 2, j]]
);
if (!(tag in buttons)) {
buttons[tag] = [];
}
buttons[tag].push(button);
j = jj;
};
return {
'row': labels => labels.forEach(add),
'crlf': () => { i += 2; j = 0; },
'space': j_ => j += j_,
'keyboard': () => keyboard,
'press': tag => buttons[tag].forEach(b => b.press()),
'release': tag => buttons[tag].forEach(b => b.release())
};
};
const main = () => {
const builder = makeKeyboardBuilder();
builder.row([
' Esc ', 'F1 ', 'F2 ', 'F3 ', 'F4 ', 'F5 ', 'F6 ', 'F7 ', 'F8 ', 'F9 ',
'F10', 'F11', 'F12', 'Ins ', 'PrSc', ' Del '
]);
builder.crlf();
builder.row([
'`~ ', ' 1! ', ' 2@ ', ' 3# ', ' 4$ ', ' 5% ', ' 6^ ', ' 7& ', ' 8* ',
' 9( ', ' 0) ', ' -_ ', ' =+ ', ' BkSp'
]);
builder.crlf();
builder.row([
'Tab ', ' Q ', ' W ', ' E ', ' R ', ' T ', ' Y ', ' U ', ' I ',
' O ', ' P ', ' [ ', ' ] ', ' \\| '
]);
builder.crlf();
builder.row([
'CpsLck', ' A ', ' S ', ' D ', ' F ', ' G ', ' H ', ' J ',
' K ', ' L ', ' ;: ', ' \'" ', ' Enter '
]);
builder.crlf();
builder.row([
'Shift ', ' Z ', ' X ', ' C ', ' V ', ' B ', ' N ', ' M ',
' ,< ', ' .> ', ' /? ', ' Shift '
]);
builder.crlf();
builder.row([
'Ctrl ', ' Fn ', 'Win ', 'Alt ', ' ', 'Alt ', 'Ctrl '
]);
builder.space(1);
builder.row(['PgUp ', ' Up ', 'PgDn ']);
builder.crlf();
builder.space(37);
builder.row(['Home ', ' End ']);
builder.space(3);
builder.row(['Left ', 'Down ', 'Right']);
const keyboard = builder.keyboard();
const body = document.getElementsByTagName('body')[0];
const screen = makeScreen(body, keyboard.h(), keyboard.w());
keyboard.attachScreen(screen);
const input = {
'Control': 'Ctrl',
'Escape': 'Esc',
'OS': 'Win',
'Backspace': 'BkSp',
'Delete': 'Del',
'`': '`~',
'1': '1!',
'2': '2@',
'3': '3#',
'4': '4$',
'5': '5%',
'6': '6^',
'7': '7&',
'8': '8*',
'9': '9(',
'0': '0)',
'-': '-_',
'=': '=+',
'\\': '\\|',
';': ';:',
"'": '\'"',
',': ',<',
'.': '.>',
'/': '/?',
' ': '',
'PageUp': 'PgUp',
'ArrowUp': 'Up',
'PageDown': 'PgDn',
'ArrowLeft': 'Left',
'ArrowDown': 'Down',
'ArrowRight': 'Right'
};
for (const c of 'abcdefghijklmnopqrstuvwxyz') {
input[c] = c.toUpperCase();
input[c.toUpperCase()] = c.toUpperCase();
}
for (const w of ['Shift', 'Alt', 'Tab', '[', ']', 'Enter', 'Home', 'End']) {
input[w] = w;
}
for (let i = 1; i <= 12; ++i) {
input['F' + i] = 'F' + i;
}
body.addEventListener('keydown', event => {
const key = event.key;
if (key in input) {
builder.press(input[key]);
}
event.preventDefault();
});
body.addEventListener('keyup', event => {
const key = event.key;
if (key in input) {
builder.release(input[key]);
}
event.preventDefault();
});
};
document.addEventListener('DOMContentLoaded', main);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Screen keyboard</title>
<script type="module" src="main.js"></script>
</head>
<body>
Click on keyboard. Press Shift-A.
</body>
</html>
Мое предложение: рендерить клавиатуру на основе заданного массива строк кнопок.
const rows = [
['Tab ', ' Q', ' W', ' E', ' R', ' T', ' Y', ' U', ' I', ' O', ' P', ' [', ' ]', ' |'],
['CapsLock', ' A', ' S', ' D', ' F', ' G', ' H', ' J', ' K', ' L', ' ;:', ` '"`, ' Enter'],
['Shift ', ' Z', ' X', ' C', ' V', ' B', ' N', ' M', ' ,<', ' .>', ' /?', ' Shift']
];
В моей реализации важно, чтобы суммарно по символам все строки были одинаковой длины, точнее следующая строка не должна быть длиннее предыдущей.
Я не делал решение для секции, где находится pagedown/pageup, можно попробовать реализовать отступы, ну либо немного переиграть раскладку, например, расположить эти кнопки справа на верхних уровнях.
Самое, наверное, геморное было написать функцию getSymbolBetweenRows, в ней, кстати, info объект можно заменить на тот же числовой code и тогда весь код легко будет переписать на любой сиподобный язык.
const container = document.getElementById("container");
const rows = [
['Tab ', ' Q', ' W', ' E', ' R', ' T', ' Y', ' U', ' I', ' O', ' P', ' [', ' ]', ' |'],
['CapsLock', ' A', ' S', ' D', ' F', ' G', ' H', ' J', ' K', ' L', ' ;:', ` '"`, ' Enter'],
['Shift ', ' Z', ' X', ' C', ' V', ' B', ' N', ' M', ' ,<', ' .>', ' /?', ' Shift']
];
/**
* @var Unicode Box Draw Map
* @usedBy draw()
*/
const boxDraw = new Map([
[ 1, 116],[ 2, 120],[ 4, 119],[ 5, 16],[ 6, 17],[ 7, 85],
[ 8, 123],[ 9, 18],[ 10, 19],[ 13, 86],[ 15, 87],[ 16, 118],
[ 17, 0],[ 18, 126],[ 20, 12],[ 21, 44],[ 22, 45],[ 24, 14],
[ 25, 48],[ 26, 49],[ 28, 83],[ 29, 101],[ 32, 122],[ 33, 124],
[ 34, 1],[ 36, 13],[ 37, 46],[ 38, 47],[ 40, 15],[ 41, 50],
[ 42, 51],[ 51, 80],[ 52, 82],[ 55, 100],[ 60, 84],[ 63, 102],
[ 64, 117],[ 65, 24],[ 66, 25],[ 67, 91],[ 68, 2],[ 69, 36],
[ 70, 37],[ 71, 97],[ 72, 125],[ 73, 39],[ 74, 42],[ 80, 20],
[ 81, 52],[ 82, 53],[ 84, 28],[ 85, 60],[ 86, 61],[ 88, 31],
[ 89, 65],[ 90, 69],[ 96, 21],[ 97, 54],[ 98, 55],[100, 29],
[101, 62],[102, 63],[104, 34],[105, 70],[106, 72],[112, 88],
[115, 103],[116, 94],[119, 106],[128, 121],[129, 26],[130, 27],
[132, 127],[133, 38],[134, 41],[136, 3],[137, 40],[138, 43],
[144, 22],[145, 56],[146, 57],[148, 30],[149, 64],[150, 67],
[152, 32],[153, 66],[154, 73],[160, 23],[161, 58],[162, 59],
[164, 33],[165, 68],[166, 71],[168, 35],[169, 74],[170, 75],
[193, 92],[195, 93],[204, 81],[205, 98],[207, 99],[208, 89],
[209, 104],[220, 95],[221, 107],[240, 90],[243, 105],[252, 96],
[255, 108],
]);
/**
* @function getSymbolByCode
* @param {Number} code Must be integer in range [0-255]
* @returns {String}
*/
function getSymbolByCode(code) {
return !code ? ' ' : boxDraw.has(code) ? String.fromCharCode(boxDraw.get(code) + 0x2500) : null;
}
const getDefaultDash = () => ({draw: false, bold: false});
const getDefaultSymbolInfo = () => ({
top: getDefaultDash(),
right: getDefaultDash(),
bottom: getDefaultDash(),
left: getDefaultDash()
});
const getSymbolByInfo = info => {
let code = 0;
const directions = ['left', 'bottom', 'right', 'top'];
for (let i = 0; i < directions.length; i++) {
const {draw, bold} = info[directions[i]];
code |= (draw ? 1 + bold : 0) << (i * 2);
}
return getSymbolByCode(code);
}
const getRowSum = row => {
return row.reduce((sum, {length}) => sum + length + 2, 0);
}
const findKeyByPosition = (row, position) => {
for (let i = 0, sum = 0; i < row.length; i++) {
let left = sum;
sum += row[i].length + 2;
if (position >= left && position <= sum) {
const result = {
value: null,
next: null,
isDelimiter: false,
left,
right: sum
};
if (position !== 0) {
result.value = row[i];
} else {
result.next = row[i];
}
if (position === sum) {
result.next = row[i + 1];
}
result.isDelimiter = position === left || position === sum;
return result;
}
}
throw "Position is out of bounds";
}
const active = new Set();
const isActive = key => {
if (!key) {
return false;
}
key = key.trim();
for (let a of active.keys()) {
if (key === a || key === a.toUpperCase() || (key.length === 2 && key.includes(a))) {
return true;
}
}
return false;
};
const getSymbolBetweenRows = (topRow, bottomRow, position) => {
const info = getDefaultSymbolInfo();
const checkHorizontal = key => {
if (key.value) {
info.left.draw = true;
}
if (isActive(key.value)) {
info.left.bold = true;
}
if (key.isDelimiter) {
if (key.next || position === 0) {
info.right.draw = true;
}
if (isActive(key.next)) {
info.right.bold = true;
}
} else {
info.right.draw = true;
if (isActive(key.value) || isActive(key.next)) {
info.right.bold = true;
}
}
}
if (topRow) {
const top = findKeyByPosition(topRow, position);
info.top.draw = top.isDelimiter;
info.top.bold = isActive(top.value) || isActive(top.next);
checkHorizontal(top);
}
if (bottomRow) {
const bottom = findKeyByPosition(bottomRow, position);
info.bottom.draw = bottom.isDelimiter;
info.bottom.bold = isActive(bottom.value) || isActive(bottom.next);
checkHorizontal(bottom);
}
return getSymbolByInfo(info);
}
const renderTop = (row, prevRow) => {
let line = '';
const sum = getRowSum(row);
for (let i = 0; i <= sum; i++) {
line += getSymbolBetweenRows(prevRow, row, i);
}
return line;
}
const renderCenter = (row) => {
let line = '';
const sum = getRowSum(row);
for (let i = 0; i <= sum; i++) {
const current = findKeyByPosition(row, i);
const info = getDefaultSymbolInfo();
if (current.isDelimiter) {
info.top.draw = info.bottom.draw = true;
info.top.bold = info.bottom.bold = isActive(current.value) || isActive(current.next);
line += getSymbolByInfo(info);
} else {
let value = ' ' + current.value + ' ';
line += value[i - current.left];
}
}
return line;
}
const renderBottom = (row, nextRow) => {
let line = '';
const sum = getRowSum(row);
for (let i = 0; i <= sum; i++) {
line += getSymbolBetweenRows(row, nextRow, i);
}
return line;
}
const render = (rows) => {
const lines = [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const prevRow = i > 0 ? rows[i - 1] : null;
lines.push(renderTop(row, prevRow));
lines.push(renderCenter(row));
}
lines.push(renderBottom(rows[rows.length - 1]));
container.innerHTML = lines.join('\n');
}
render(rows);
document.addEventListener('keydown', event => {
active.add(event.key);
render(rows);
});
document.addEventListener('keyup', event => {
active.delete(event.key);
render(rows);
});
<pre id="container"></pre>